Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/api-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:
OAC_TEST_OFFICIAL_SDK_PYTHON: python
run: |
python services/core/tests/official_schema_test.py
python -m unittest discover -s services/core/tests -p qualify_public_native_test.py
python services/core/tests/official_client.py
go test ./services/core/tests/integration -run '^(TestFunctionStateOfficialClientReadsAndLiveEvents|TestSavedReferenceRetryOfficialClient|TestAgentUpdateOfficialClient|TestAgentDeletionOfficialClient|TestSessionAgentFilterOfficialClient|TestSessionDeletionOfficialClient|TestEnvironmentInitialFailureOfficialClient|TestSelfHostedInitialCreationOfficialClient|TestSelfHostedCancellationOfficialClient)$' -count=1
- uses: ./.github/actions/e2b-provider
Expand Down
1 change: 0 additions & 1 deletion contracts/agents-api/environment-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ The response is `{"object": "page", "data": [...], "next": …, "has_more": …}
- A `path` that does not exist, names a regular file, or passes through a symbolic link returns an empty page. Links are never followed.
- Each page reads the directory again; there is no snapshot. If the directory's regular files (their names or sizes) or the request's parameters changed since the token was issued, the token is rejected. Unchanged names and sizes do not prove unchanged contents.
- A directory with more than 1,024 entries of any kind returns 503 and no partial page. Permission errors, a missing workspace root and transport failures also return 503.
- When the daemon has no local workspace binding, the Claude Code adapter answers the read instead: a missing path returns 404, and a regular file or symbolic link returns 503.

Query errors, all with type and code `invalid_request_error` and a null `param` unless noted:

Expand Down
28 changes: 28 additions & 0 deletions contracts/agents-api/harness-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,34 @@ Keep provider keys in private operator files, never in commits or logs. Existing
| MiniMax native history binding | `mcode/session_test.go` |
| Explicit refusals without native effects or fabricated results | `mcode/unsupported_test.go` |

## Qualify the public path

`services/core/tests/qualify_public_native.py` runs the pinned official SDK and raw HTTP assertions against an already installed, isolated Core deployment with its real agent host. It creates and deletes its own Sessions and uses the selected real model; it does not provision a deployment or replace the executor. Set `OPENAI_BASE_URL` to the deployment's `/v1` endpoint and `OPENAI_API_KEY` to its Project key. Supply a second Project's key in a private file. [Installation](../../docs/getting-started/install.md) owns deployment setup; [Projects and keys](./admin-api.md#projects-and-keys) owns credential issuance.

The private settings JSON has exactly `agent`, `model_provider` and `environment`. `agent` contains `model` and an explicit `x_agents_core.harness`, with optional `harness_config` inside that extension. `model_provider` is the complete [provider bundle](./model-execution.md#session-override); `environment` is the public Session Environment input. Keep settings and foreign-key files absolute and mode 0600, outside the repository. Evidence must be a new absolute path under `~/.oac`; it contains public responses and check names, never the settings. The runner refuses to write evidence containing any of its three supplied credentials.

```bash
python services/core/tests/qualify_public_native.py \
--settings "$HOME/.oac/qualification/codex-none.json" \
--foreign-key-file "$HOME/.oac/qualification/foreign-project.key" \
--suite none \
--evidence "$HOME/.oac/qualification/codex-none-result.json"
```

| Suite | Operations | Placement |
| --- | --- | --- |
| `none` | Creation retry, foreign history rejection, two native text Turns, history recall, SSE ordering and SDK/raw Item and Turn parity | `none` |
| `pending-actions` | Query/reconnect pending calls, success/error results, cancellation, exact target rejection, retries and durable Items | Any declared placement with function tools |
| `functions` | SDK handlers, success/error, native file and Artifact bytes, continuation, pending cancellation and tenant isolation | Workspace |
| `images` | Initial and active images, image results, retry/atomic rejection, native files/Artifacts, isolation and continuation | Workspace with declared image and function support |
| `structured` | Saved and inline schema, function-assisted native files, exact JSON/SSE, cancellation and text override | Workspace with declared structured output and function support |

For `self_hosted`, choose a custom absolute `workspace_directory`. When the runner prints each new Session ID, connect a separate isolated machine or container using that Session's public installation command; the runner waits up to five minutes. Multiple Sessions must not share a workspace. Use the existing Environment setup, package and capability assertions separately to qualify preparation semantics. The separate `official_environment_files_native.py` check accepts two already connected self-hosted Sessions and checks Files.list sorting, pagination and isolation through the Environment owner.

Warm continuation is the default and records cold recovery as `unverified`. To qualify cold continuation, additionally pass `--compose-directory` with the owned installation's absolute directory and `--compose-project` with its exact project name. The runner restarts only that project's `agent-host`, confirms its container start time changed, and then runs the unchanged history assertions. This does not qualify a Core restart or a sandbox checkpoint restore. The `pending-actions` suite reconnects the public client, not the agent-host process, and rejects those restart options. Select cold recovery only where the [declaration and coverage ledger](./index.md#known-gaps) support it; unsupported recovery remains a gap, never a successful skipped check. An API rejection fails the selected suite.

Run `python -m unittest discover -s services/core/tests -p qualify_public_native_test.py` with the pinned SDK to check credential handling and the owned restart boundary without a model. Existing deterministic Core integration tests remain the authority for schema validation, atomic admission, durable receipts and rejection semantics. Real-model results qualify only the selected suite, protocol, Harness and placement. Provider lifecycle, native identity, credential isolation, deferred discovery and unselected suites need separate evidence; view-only results do not qualify the public path.

## Native installer participation

An adapter supplies `agent.Installation` from `installation.go` in its own package: its registered kind and activation environment. The agent host uses this declaration to activate the packaged Harness. Adapters own native layout; validate the packaged content and execution on the Linux agent host. Missing or incompatible native content fails; it never installs itself during a Turn. Self-hosted installers carry no Harness or Node.js.
Expand Down
3 changes: 1 addition & 2 deletions contracts/agents-api/zh/environment-files.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Environment 文件与 Artifact"
source: contracts/agents-api/environment-files.md
source_hash: 1b58aa02aaccddb9675ef41ebfe2506a6fba0bb12139efb67e0da378d879aee7
source_hash: 11d3609d81455a3f0c203f341802e3fec1a9f318ab0e9977d2de0f2a84b8b63c
---

Session 工作区保存由 agent 及其工具修改的实时文件。`/agents/environments/{environment_id}/files` 列出一个工作区目录,并在其中创建文件。Turn 完成时,Core 将工作区 `outputs/` 目录中的文件复制为不可变 Artifact,通过 `/agents/sessions/{session_id}/artifacts` 读取。Artifact 的生命周期长于 Environment;工作区文件则不是。
Expand Down Expand Up @@ -32,7 +32,6 @@ Session 工作区保存由 agent 及其工具修改的实时文件。`/agents/en
- `path` 不存在、指向普通文件或经过符号链接时返回空页。不跟随链接。
- 每页重新读取目录,不提供快照。token 签发后目录普通文件(名称或大小)或请求参数改变时,token 被拒绝。名称和大小未变不证明内容未变。
- 目录中任何类型条目合计超过 1,024 个时返回 503,不返回部分页。权限错误、工作区根缺失和传输失败也返回 503。
- daemon 无本地工作区绑定时,改由 Claude Code 适配器回答读取:路径缺失返回 404,普通文件或符号链接返回 503。

查询错误的 type 和 code 均为 `invalid_request_error`,除另有说明外 `param` 为 null:

Expand Down
30 changes: 29 additions & 1 deletion contracts/agents-api/zh/harness-onboarding.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "添加 Harness"
source: contracts/agents-api/harness-onboarding.md
source_hash: 2436c12691cc2f2753f39df73a6f480f081c3ddef1936e40c7da12c09c36a35e
source_hash: 8bf3becb666da1ba0e1f6470bb468eba8d2352e63c2bddda78be6511414ed98a
---

**Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、支持声明和验收。
Expand Down Expand Up @@ -210,6 +210,34 @@ Environment 验收使用 `services/core/tests/official_environment_{templates,se
| MiniMax 原生历史绑定 | `mcode/session_test.go` |
| 无原生副作用或伪造结果的明确拒绝 | `mcode/unsupported_test.go` |

## 验证公共调用路径 {#qualify-the-public-path}

`services/core/tests/qualify_public_native.py` 使用锁定的官方 SDK 和原始 HTTP 断言,访问已经安装、隔离且运行真实 agent host 的 Core 部署。它创建并删除自己的 Session,使用所选真实模型;不负责部署,也不替代执行器。将 `OPENAI_BASE_URL` 设置为部署的 `/v1` 端点,将 `OPENAI_API_KEY` 设置为其 Project 密钥。另一个 Project 的密钥通过私有文件提供。[安装](../../../docs/zh/getting-started/install.md) 负责部署设置;[Projects and keys](./admin-api.md#projects-and-keys) 负责凭据签发。

私有设置 JSON 恰好包含 `agent`、`model_provider` 和 `environment`。`agent` 包含 `model` 和显式的 `x_agents_core.harness`,可在该扩展内提供 `harness_config`。`model_provider` 是完整的 [Provider 配置包](./model-execution.md#session-override);`environment` 是公共 Session Environment 输入。设置文件和外部 Project 密钥文件必须使用绝对路径、权限 0600,并保存在仓库之外。证据必须使用 `~/.oac` 下新的绝对路径;其中保存公共响应和检查名称,不保存设置。运行器拒绝写入含有三个已提供凭据中任意一个的证据。

```bash
python services/core/tests/qualify_public_native.py \
--settings "$HOME/.oac/qualification/codex-none.json" \
--foreign-key-file "$HOME/.oac/qualification/foreign-project.key" \
--suite none \
--evidence "$HOME/.oac/qualification/codex-none-result.json"
```

| 套件 | 操作 | 放置方式 |
| --- | --- | --- |
| `none` | 创建重试、外部历史拒绝、两个原生文本 Turn、历史回忆、SSE 顺序,以及 SDK/原始 Item 和 Turn 一致性 | `none` |
| `pending-actions` | 查询和重连待处理调用、成功/错误结果、取消、精确目标拒绝、重试和持久化 Item | 声明支持函数工具的任意放置方式 |
| `functions` | SDK handler、成功/错误、原生文件和 Artifact 字节、继续执行、待处理调用取消和租户隔离 | 工作区 |
| `images` | 初始和活动图像、图像结果、重试/原子拒绝、原生文件/Artifact、隔离和继续执行 | 声明支持图像和函数的工作区 |
| `structured` | 保存和内联 schema、函数辅助原生文件、精确 JSON/SSE、取消和文本覆盖 | 声明支持结构化输出和函数的工作区 |

对于 `self_hosted`,选择自定义绝对 `workspace_directory`。运行器打印每个新 Session ID 后,使用该 Session 的公共安装命令连接独立的隔离机器或容器;运行器最多等待五分钟。多个 Session 不得共享工作区。使用现有 Environment setup、包和能力断言单独验证准备语义。独立的 `official_environment_files_native.py` 检查接受两个已连接的 self-hosted Session,通过 Environment owner 验证 Files.list 排序、分页和隔离。

默认验证热继续执行,并将冷恢复记录为 `unverified`。验证冷继续执行时,额外传入指向所拥有安装的绝对目录的 `--compose-directory`,以及指定其精确项目名称的 `--compose-project`。运行器仅重启该项目的 `agent-host`,确认容器启动时间已改变,然后执行相同的历史断言。这不能证明 Core 重启或 sandbox 检查点恢复。`pending-actions` 套件重连的是公共客户端,而非 agent-host 进程,因此拒绝这些重启选项。仅在[声明和覆盖台账](./index.md#known-gaps) 支持时选择冷恢复;不支持的恢复仍是缺口,不能把跳过的检查记为成功。API 拒绝会使所选套件失败。

使用锁定的 SDK 运行 `python -m unittest discover -s services/core/tests -p qualify_public_native_test.py`,可在不调用模型的情况下检查凭据处理和所拥有的重启边界。现有确定性 Core 集成测试仍负责 schema 验证、原子准入、持久化回执和拒绝语义。真实模型结果仅证明所选套件、协议、Harness 和放置方式。Provider 生命周期、原生身份、凭据隔离、延迟发现和未选择的套件需要独立证据;仅通过 view 测试不能证明公共调用路径。

## 原生安装器参与 {#native-installer-participation}

适配器从自身包中的 `installation.go` 提供 `agent.Installation`:已注册 kind 和激活环境。agent host 使用该声明激活打包的 Harness。适配器负责原生布局;必须在 Linux agent host 上验证打包内容和执行。原生内容缺失或不兼容时必须失败;绝不会在 Turn 期间自行安装。自托管安装器不携带 Harness 或 Node.js。
Expand Down
27 changes: 12 additions & 15 deletions services/core/tests/official_environment_files_native.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
"""Opt-in live check; stdin supplies engine, base and two tenants with session_id and token_file/token_env.

Optional directory_reader is "local" (default, a local workspace binding) or, for
claude_sdk only, "claude_sdk_adapter" for a daemon without that binding.
"""
"""Opt-in live check; stdin supplies engine, base and two tenants with session_id and token_file/token_env."""

import importlib.metadata
import json
Expand Down Expand Up @@ -52,11 +48,13 @@ def generate_files(client, session_id, label):
workspace = PurePosixPath(session.environment.workspace_directory)
assert workspace.is_absolute(), "Absolute workspace required"
assert client.beta.agents.environments.retrieve(environment_id).status == "connected", "Connect the Environment first"
directory = str(workspace / ("files-list-" + label + "-" + uuid.uuid4().hex))
name = "files-list-" + label + "-" + uuid.uuid4().hex
directory = str(workspace / name)
public_directory = "/workspace/" + name
contents = {"A.txt": "A\n", "a-b.txt": "three\n", "a.txt": "fourteen-bytes\n", "z.txt": "last\n"}
expected = {directory + "/" + name: len(content.encode()) for name, content in contents.items()}
expected = {public_directory + "/" + name: len(content.encode()) for name, content in contents.items()}
sibling = directory + "-sibling"
sibling_expected = {sibling + "/one.txt": 3, sibling + "/two.txt": 3}
sibling_expected = {public_directory + "-sibling/one.txt": 3, public_directory + "-sibling/two.txt": 3}
command = "mkdir -- " + shlex.quote(directory) + " " + shlex.quote(sibling)
for name, content in contents.items():
command += " && printf %s " + shlex.quote(content) + " > " + shlex.quote(directory + "/" + name)
Expand All @@ -74,17 +72,16 @@ def generate_files(client, session_id, label):
assert turns[0].status not in ("failed", "cancelled"), "File generation Turn failed"
if turns[0].status == "completed" and sessions.retrieve(session_id).status == "idle":
return {"session_id": session_id, "environment_id": environment_id, "turn_id": turns[0].id,
"directory": directory, "expected": expected,
"sibling_directory": sibling, "sibling_expected": sibling_expected}
"directory": public_directory, "expected": expected,
"sibling_directory": public_directory + "-sibling", "sibling_expected": sibling_expected}
time.sleep(0.2)
raise AssertionError("File generation Turn did not complete")


def main():
settings = json.load(sys.stdin)
assert settings["engine"] in ("codex", "claude_sdk"), "Select one qualified native engine"
reader = settings.get("directory_reader", "local")
assert reader == "local" or (reader == "claude_sdk_adapter" and settings["engine"] == "claude_sdk"), "Unsupported directory reader"
assert settings["engine"] in ("codex", "claude_sdk", "mcode"), "Select one qualified native engine"
assert "directory_reader" not in settings, "Files are served by the Environment owner"
assert len(settings["tenants"]) == 2, "Two independent tenant Sessions are required"
pin = json.loads((Path(__file__).resolve().parents[3] / "contracts/agents-api/upstream.json").read_text())
distribution = importlib.metadata.distribution("openai")
Expand All @@ -107,12 +104,12 @@ def main():
client, http, fixture["environment_id"], fixture["sibling_directory"], fixture["sibling_expected"])
fixture["wire_rows"] = verify_file_list_rows(client, http, fixture["environment_id"], {
"directory": fixture["directory"], "missing": fixture["directory"] + "-missing",
"file": next(iter(fixture["expected"]))}, empty_pages=reader == "local")
"file": next(iter(fixture["expected"]))}, empty_pages=True)
verify_file_tenant_isolation(client, clients[1 - index], http, fixture["environment_id"],
fixture["directory"], page, fixture["expected"] | fixture["sibling_expected"])
assert [turn.to_dict() for turn in client.beta.agents.sessions.turns.list(fixture["session_id"])] == before, "Files.list changed Turns"
fixture["cross_tenant_denied"] = True
proof = {"engine": settings["engine"], "directory_reader": reader, "sdk_version": distribution.version, "sdk_commit": pin["commit"],
proof = {"engine": settings["engine"], "sdk_version": distribution.version, "sdk_commit": pin["commit"],
"scope": "Public input-generated flat files, SDK/raw listing, sorting, pagination and two-tenant isolation",
"fixtures": generated, "unverified": UNVERIFIED}
serialized = json.dumps(proof, indent=2)
Expand Down
Loading