diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7931da11..d10fc9ae0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,7 +100,7 @@ jobs: path: | ~/.npm/_cacache ~/.oac/cache/microsandbox-v0.7.2-linux-x86_64.tar.gz - key: release-downloads-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('scripts/prepare-release-runtimes.sh', 'packages/mcode-harness/source.json', 'scripts/core-distribution-manifest.py') }} + key: release-downloads-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('scripts/prepare-release-runtimes.sh', 'internal/harnessconfig/builtin/catalog.json', 'packages/mcode-harness/source.json', 'scripts/core-distribution-manifest.py') }} restore-keys: | release-downloads-v1-${{ runner.os }}-${{ runner.arch }}- - name: Check release metadata and prepare pinned harnesses diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b90b9b1b..0edffe628 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ This guide owns how to work in the repository: documentation ownership, the repo | Core service setup, tests and generation | [Core service guide](services/core/README.md) | | Core implementation constraints beyond the public contracts | [Implementation constraints](services/core/IMPLEMENTATION.md) | | Environment ownership, preparation, Skills, Plugins, packages and MCP bindings | [Environments](contracts/agents-api/environments.md) | -| Built-in Harness identifiers and display names | `internal/harnessconfig/builtin/catalog.json` and its [generated reference](contracts/agents-api/harness-catalog.md) | +| Built-in Harness identifiers, display names and version pins | `internal/harnessconfig/builtin/catalog.json` and its [generated reference](contracts/agents-api/harness-catalog.md) | | Harness registration, support declaration and acceptance | [Harness onboarding](contracts/agents-api/harness-onboarding.md) | | Harness selection, model providers and native parameters | [Model execution](contracts/agents-api/model-execution.md) | | Provider registration and lifecycle | [Sandbox Provider guide](docs/sandbox-provider.md) | @@ -113,7 +113,7 @@ The role needs `CREATE DATABASE`: tests of database-wide state, such as the exec ### Contract and schema rules -- `internal/harnessconfig/builtin/catalog.json` is the single authored public Harness registration list. `make generate-harness-catalog` generates Go configuration/profile registration, client identifiers/names and the reference, and projects the model-provider protocol names of `internal/modelprovider/config.go` to the client; `make openapi` derives the matching enums. `make check-harness-catalog` verifies freshness in the full gate. Native configuration rules stay in their adapter declarations; Core qualification and Runtime availability stay separate. +- `internal/harnessconfig/builtin/catalog.json` owns public Harness registration and native version pins. `make generate-harness-catalog` generates Go configuration and agent-host registration, version pin projections, client identifiers/names and the reference, and projects the model-provider protocol names of `internal/modelprovider/config.go` to the client; `make openapi` derives the matching enums. [Harness onboarding](contracts/agents-api/harness-onboarding.md#native-version-pins) owns native package and source projection rules. `make check-harness-catalog` verifies freshness in the full gate. Native configuration rules stay in their adapter declarations; Core qualification and Runtime availability stay separate. - `make sqlc-generate` owns only `services/core/internal/db/sqlc` (sqlc v1.29.0). Do not rewrite landed migrations. - `make check-runtime-contract` is the focused Core–Runtime contract entry point; see [Contract verification](docs/runtime-protocol.md#contract-verification). It also runs through `check-go` and `check-core`. diff --git a/apps/daemon/internal/agent/codex/recovery.go b/apps/daemon/internal/agent/codex/recovery.go index 79bd4a760..91fd2d6bc 100644 --- a/apps/daemon/internal/agent/codex/recovery.go +++ b/apps/daemon/internal/agent/codex/recovery.go @@ -6,10 +6,12 @@ import ( "errors" "path/filepath" "strings" + + configuration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/codex" ) func SupportsNativeSessionRecovery(version string) bool { - return strings.TrimSpace(version) == "codex-cli 0.153.4" + return strings.TrimSpace(version) == "codex-cli "+configuration.NativeVersion } func (s *Session) recoverRoot(plan SessionPlan) (string, error) { diff --git a/apps/daemon/internal/agent/mcode/declaration.go b/apps/daemon/internal/agent/mcode/declaration.go index e7e9082bd..017883a7b 100644 --- a/apps/daemon/internal/agent/mcode/declaration.go +++ b/apps/daemon/internal/agent/mcode/declaration.go @@ -26,7 +26,7 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, i defer cancel() version, err := check(ctx, "") if err != nil { - fmt.Fprintf(options.Stderr, "oac-daemon: mcode unavailable: %v\n Install: npm install -g @minimax-ai/code@0.4.12\n", err) + fmt.Fprintf(options.Stderr, "oac-daemon: mcode unavailable: %v\n", err) return runtime } runtime.Info.Available, runtime.Info.Version = true, version diff --git a/apps/daemon/internal/agent/mcode/version.go b/apps/daemon/internal/agent/mcode/version.go index 95c476007..1c414086b 100644 --- a/apps/daemon/internal/agent/mcode/version.go +++ b/apps/daemon/internal/agent/mcode/version.go @@ -7,11 +7,12 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/binpath" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/versionprobe" + configuration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/mcode" ) var ErrCLINotFound = errors.New("mcode CLI not found") -const SupportedVersion = "0.4.12" +const SupportedVersion = configuration.NativeVersion func defaultBinary() string { return binpath.MCode() } diff --git a/apps/daemon/internal/cli/agent_host_linux.go b/apps/daemon/internal/cli/agent_host_linux.go index 7efe75922..c7aae8327 100644 --- a/apps/daemon/internal/cli/agent_host_linux.go +++ b/apps/daemon/internal/cli/agent_host_linux.go @@ -19,9 +19,6 @@ import ( "golang.org/x/sys/unix" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/claudesdk" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/codex" - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/mcode" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agenthost" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/daemonize" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/dispatch" @@ -50,8 +47,6 @@ const ( // agentHostUIDs is the range the agent host runs its Executors as. var agentHostUIDs = agenthost.UIDRange{First: 70000, Count: 4096} -var harnessDeclarations = []agent.Declaration{codex.Declaration, mcode.Declaration, claudesdk.Declaration} - func runAgentHost(rc *runContext, args []string) error { return serveAgentHost(context.Background(), rc, args, harnessDeclarations) } @@ -99,7 +94,7 @@ func serveAgentHost(parent context.Context, rc *runContext, args []string, decla ctx, cancel := daemonize.NotifyContext(parent) defer cancel() - env, err := agent.ManifestEnvironment(agentHostManifest, claudesdk.Installation(), codex.Installation(), mcode.Installation()) + env, err := agent.ManifestEnvironment(agentHostManifest, harnessInstallations...) if err != nil { return fmt.Errorf("agent-host: %w", err) } diff --git a/apps/daemon/internal/cli/harness_catalog_linux.go b/apps/daemon/internal/cli/harness_catalog_linux.go new file mode 100644 index 000000000..452416108 --- /dev/null +++ b/apps/daemon/internal/cli/harness_catalog_linux.go @@ -0,0 +1,14 @@ +//go:build linux + +// Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT. +package cli + +import ( + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/claudesdk" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/codex" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/mcode" +) + +var harnessDeclarations = []agent.Declaration{claudesdk.Declaration, codex.Declaration, mcode.Declaration} +var harnessInstallations = []agent.Installation{claudesdk.Installation(), codex.Installation(), mcode.Installation()} diff --git a/contracts/agents-api/harness-catalog.md b/contracts/agents-api/harness-catalog.md index 96548b11d..70bd43116 100644 --- a/contracts/agents-api/harness-catalog.md +++ b/contracts/agents-api/harness-catalog.md @@ -1,12 +1,12 @@ [//]: # (Generated by scripts/generate-harness-catalog.py; DO NOT EDIT.) # Built-in Harness registrations -The authored registration list is [`catalog.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json). Change it and run `make generate-harness-catalog`; `make check-harness-catalog` checks the generated projections. +The authored registration list and Harness version pins are [`catalog.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json). Change it and run `make generate-harness-catalog`; `make check-harness-catalog` checks the generated projections. -| Identifier | Display name | Declaration | -| --- | --- | --- | -| `claude_sdk` | Claude Code | `claudesdk.Configuration` | -| `codex` | Codex | `codex.Configuration` | -| `mcode` | MiniMax Code | `mcode.Configuration` | +| Identifier | Display name | Version | Declaration | +| --- | --- | --- | --- | +| `claude_sdk` | Claude Code | `0.3.269` | `claudesdk.Configuration` | +| `codex` | Codex | `0.153.4` | `codex.Configuration` | +| `mcode` | MiniMax Code | `0.4.12` | `mcode.Configuration` | Each declaration in `internal/harnessconfig/` states the Harness's model providers and its support, the only source Core and the Runtime admit a selection against. These registrations describe the build. Which Harnesses a deployment enables is the `core.harnesses` [process setting](../../docs/configuration.md#settings), and a connected Runtime reports its own availability. [Harness onboarding](./harness-onboarding.md) describes the adapter and packaging steps. diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 95d8be692..2f1187c3a 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -30,16 +30,16 @@ Runtime: Executor preparation, reuse, idle expiry, recovery | Adapter | Native configuration, resources, API calls, event translation and restrictions | `apps/daemon/internal/agent/` | | Harness | Native model and tool loop and history | Pinned SDK or executable | | Declaration | The Harness's support, against which Core and the Runtime admit each selection | `internal/harnessconfig/` | -| Registration | Adapter declarations, installed views and the installation's narrowed support | `apps/daemon/internal/agent//declaration.go`; static list in `apps/daemon/internal/cli/agent_host_linux.go` | +| Registration | Adapter declarations, installed views and the installation's narrowed support | `apps/daemon/internal/agent//declaration.go`; catalog-generated list in `apps/daemon/internal/cli/harness_catalog_linux.go` | An Environment supplies execution resources. Managed E2B, Docker and microsandbox machines and application-owned machines differ in provisioning and connection; each serves the sandbox to the Linux agent host, which runs every Harness in a [view](#run-in-an-agent-host-view) of it under this same contract. Native factories receive capabilities only after the Runtime has loaded the bound installed snapshot ([capability preparation](./environments.md#runtime-capability-preparation)). Model providers supply model communication settings, not Turn scheduling or native process ownership. ## Steps -1. **Pin the native source.** Record the upstream package version and source revision and document the native entry point next to the adapter. +1. **Pin the native source.** Add the upstream package version to the [Harness catalog](./harness-catalog.md), keep any source revision in one authored owner ([native version pins](#native-version-pins)), and document the native entry point next to the adapter. 2. **Implement the adapter** in `apps/daemon/internal/agent/`: a view whose `ViewExecutorFactory` prepares an `Executor`, and a `Turn` ([required interfaces](#required-adapter-interfaces), [lifetimes](#executor-and-turn-lifetimes), [view](#run-in-an-agent-host-view)). Reuse the shared process, credential and configuration helpers. -3. **Declare its support and register it.** Declare the support in `internal/harnessconfig/` with one catalog entry ([declare support](#declare-support)), then declare the kind in the adapter and add it to the agent host's static list in `apps/daemon/internal/cli/agent_host_linux.go` ([register the adapter](#register-the-adapter)). -4. **Package native prerequisites.** Supply the adapter's installation and add the Harness to the agent-host image ([native installer participation](#native-installer-participation)). +3. **Declare its support and register it.** Declare the support in `internal/harnessconfig/` with one catalog entry ([declare support](#declare-support)), then export the adapter declaration for generated agent-host registration ([register the adapter](#register-the-adapter)). +4. **Package native prerequisites.** Supply the adapter's installation and add the Harness to the agent-host image ([native Harness packaging](#native-harness-packaging)). 5. **Enable and select the engine** with the `core.harnesses` setting and [Harness selection](./model-execution.md#harness-selection). 6. **Qualify it** ([qualify the adapter](#qualify-the-adapter)) and record each native difference in the [coverage ledger](./index.md). @@ -147,7 +147,7 @@ A Harness that supports the Subagent reads implements the [neutral observation c ## Register the adapter -Registration is static and requires a build. Export one `agent.Declaration` from `apps/daemon/internal/agent//declaration.go`, then add it to `harnessDeclarations` in [`cli/agent_host_linux.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_host_linux.go). The declaration contains the kind with the capabilities of the shared model `Configuration`'s declaration, that `Configuration` and a `Discover` function. Discovery receives the diagnostic writers, owns native configuration and availability checks, and returns the installed `agent.Runtime` with its descriptor and view declaration. Return nil when the adapter is not configured; return an unavailable descriptor without a view when configured prerequisites fail. Keep version gates and view-selection conditions inside the adapter; they only clear support. +Registration is static and requires a build. Export one `agent.Declaration` from `apps/daemon/internal/agent//declaration.go`. `make generate-harness-catalog` generates the agent host's declaration and `Installation()` lists in `apps/daemon/internal/cli/harness_catalog_linux.go` from each catalog entry's `configuration` package. The declaration contains the kind with the capabilities of the shared model `Configuration`'s declaration, that `Configuration` and a `Discover` function. Discovery receives the diagnostic writers, owns native configuration and availability checks, and returns the installed `agent.Runtime` with its descriptor and view declaration. Return nil when the adapter is not configured; return an unavailable descriptor without a view when configured prerequisites fail. Keep version gates and view-selection conditions inside the adapter; they only clear support. [`cli/agent_host_linux.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_host_linux.go) registers each discovered Runtime that has a view with `RegisterKind` and `RegisterView`. The agent host then registers each such kind for dispatch through `Registry.Register`, which composes the declaration with the Environments it serves, and installs its own Executor factory with `RegisterExecutor`; that factory builds the Session's view and calls the view's `ViewExecutorFactory`. @@ -167,7 +167,7 @@ The runnable test-only example [`testdata/onboarding/main.go`](https://github.co ## Declare support -Core recognizes the [built-in Harness registrations](./harness-catalog.md). Add one entry to `internal/harnessconfig/builtin/catalog.json` with the public `kind`, the display `label` and the model `configuration` package under `internal/harnessconfig`, then run `make generate-harness-catalog`. It generates the model configuration registry, client identifiers and display names and the registration reference; public input validators read the generated registry. `make openapi` derives Harness enums from the same catalog, so do not add handwritten enums to DTO tags or route annotations. `make check-harness-catalog` rejects stale projections. +Core recognizes the [built-in Harness registrations](./harness-catalog.md). Add one entry to `internal/harnessconfig/builtin/catalog.json` with the public `kind`, the display `label`, the pinned upstream `version` and the model `configuration` package under `internal/harnessconfig`, then run `make generate-harness-catalog`. It generates the model configuration registry, agent-host declaration and installation lists, version pin projections, client identifiers and display names and the registration reference; public input validators read the generated registry. `make openapi` derives Harness enums from the same catalog, so do not add handwritten enums to DTO tags or route annotations. `make check-harness-catalog` rejects stale projections. The `Declaration` of `Configuration()` in `internal/harnessconfig/` is the Harness's support: a `proto.Declaration` with its `AgentKindCapabilities`, its message, image, MCP and output-schema limits, and in `Conflicts` the feature pairs it supports alone but not together. It states the adapter's maximum support and is the only source: Core reads it through `builtin.Registry()`, and the adapter's Runtime descriptor starts from it. Discovery and the Environment owner only clear support, and Core rejects a heartbeat that widens it. Declare only real differences between Harnesses; a rule that holds for every Harness is a common check in `proto.ValidateSelection`. @@ -236,9 +236,17 @@ Warm continuation is the default and records cold recovery as `unverified`. To q 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 +## Native version pins -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. +`internal/harnessconfig/builtin/catalog.json` owns each built-in Harness's upstream version. Build scripts read that catalog; adapter constants and package version fields are generated projections. Update the catalog and run `make generate-harness-catalog` before building, then use `make check-harness-catalog` to verify freshness. A version change requires [native qualification](#qualify-the-adapter). + +For Claude, `version` pins the official Agent SDK dependency in `packages/claude-sdk-adapter/package.json`; its pnpm lock must resolve that exact version and installs stay frozen. Update the lock through pnpm when changing the pin. Native Claude Code's version comes from the installed SDK's `claudeCodeVersion` metadata and is checked against its runtime report. Do not author a separate native Claude Code pin. + +For MiniMax, `packages/mcode-harness/source.json` owns the upstream repository and source revision; its `version` is projected from the catalog. The companion artifact carries `source.json`, so its source validation and provenance work independently of the checkout. + +## Native Harness packaging + +An adapter supplies `agent.Installation` from `installation.go` in its own package: its registered `AgentKind` and activation `Environment`. The generated registration passes these declarations to `agent.ManifestEnvironment`, which activates the packaged Harness from the image manifest. 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. `deploy/distribution/AgentHost.Dockerfile` installs each Harness in its own directory and lists it in the image's manifest, `/opt/oac/harnesses.json`. `agent.ManifestEnvironment` activates it through `Installation.Environment`. Add each new Harness to that image and manifest; use the shared [image build](../../docs/maintainers.md#runtime-images-and-helpers) and [view qualification](#qualify-the-view) workflow. diff --git a/contracts/agents-api/zh/harness-catalog.md b/contracts/agents-api/zh/harness-catalog.md index b0652c3eb..196a98d41 100644 --- a/contracts/agents-api/zh/harness-catalog.md +++ b/contracts/agents-api/zh/harness-catalog.md @@ -1,12 +1,12 @@ [//]: # (Generated by scripts/generate-harness-catalog.py; DO NOT EDIT.) # 内置 Harness 注册 {#built-in-harness-registrations} -注册列表的源文件是 [`catalog.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json)。修改后运行 `make generate-harness-catalog`;`make check-harness-catalog` 检查生成结果。 +注册列表和 Harness 版本固定值的源文件是 [`catalog.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json)。修改后运行 `make generate-harness-catalog`;`make check-harness-catalog` 检查生成结果。 -| 标识符 | 显示名称 | 声明 | -| --- | --- | --- | -| `claude_sdk` | Claude Code | `claudesdk.Configuration` | -| `codex` | Codex | `codex.Configuration` | -| `mcode` | MiniMax Code | `mcode.Configuration` | +| 标识符 | 显示名称 | 版本 | 声明 | +| --- | --- | --- | --- | +| `claude_sdk` | Claude Code | `0.3.269` | `claudesdk.Configuration` | +| `codex` | Codex | `0.153.4` | `codex.Configuration` | +| `mcode` | MiniMax Code | `0.4.12` | `mcode.Configuration` | `internal/harnessconfig/` 中的声明给出 Harness 的模型提供方及其支持范围,是 Core 和 Runtime 准入选择的唯一依据。这些注册描述当前构建。部署启用的 Harness 由 `core.harnesses` [进程设置](../../../docs/zh/configuration.md#settings)决定,连接的 Runtime 报告自身可用性。[Harness 接入](./harness-onboarding.md)介绍适配器和打包步骤。 diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index 7f6af512d..eea03629f 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "添加 Harness" source: contracts/agents-api/harness-onboarding.md -source_hash: 8bf3becb666da1ba0e1f6470bb468eba8d2352e63c2bddda78be6511414ed98a +source_hash: aace6aceea7229f9950eeca914b8e1de131973b49d48258649d4e596ec8054b1 --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、支持声明和验收。 @@ -32,16 +32,16 @@ Runtime: Executor preparation, reuse, idle expiry, recovery | Adapter | 原生配置、资源、API 调用、事件转换和限制 | `apps/daemon/internal/agent/` | | Harness | 原生模型和工具循环以及历史记录 | 锁定版本的 SDK 或可执行文件 | | 声明 | Harness 的支持范围,Core 和 Runtime 据此准入每个选择 | `internal/harnessconfig/` | -| 注册 | 适配器声明、已安装视图和该安装收窄后的支持 | `apps/daemon/internal/agent//declaration.go`;`apps/daemon/internal/cli/agent_host_linux.go` 中的静态列表 | +| 注册 | 适配器声明、已安装视图和该安装收窄后的支持 | `apps/daemon/internal/agent//declaration.go`;`apps/daemon/internal/cli/harness_catalog_linux.go` 中由目录生成的列表 | Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以及应用自有机器在预配和连接方式上有所不同;它们都把沙箱提供给 Linux agent host,由 agent host 在沙箱的[视图](#run-in-an-agent-host-view)中按同一契约运行每个 Harness。只有在 Runtime 加载绑定的已安装快照后,原生工厂才会收到能力([capability preparation](environments.md#runtime-capability-preparation))。模型 Provider 提供模型通信设置,而不负责 Turn 调度或原生进程所有权。 ## 步骤 {#steps} -1. **锁定原生来源。** 记录上游包版本和源修订版本,并在适配器旁记录原生入口点。 +1. **锁定原生来源。** 将上游包版本加入 [Harness 目录](harness-catalog.md),将源修订版本保留在唯一编写来源中([原生版本固定](#native-version-pins)),并在适配器旁记录原生入口点。 2. **实现适配器**,位置为 `apps/daemon/internal/agent/`:实现一个视图,其 `ViewExecutorFactory` 准备 `Executor`,以及 `Turn`([required interfaces](#required-adapter-interfaces)、[lifetimes](#executor-and-turn-lifetimes)、[view](#run-in-an-agent-host-view))。复用共享的进程、凭据和配置辅助函数。 -3. **声明支持并注册。** 在 `internal/harnessconfig/` 中声明支持并添加一个目录条目([declare support](#declare-support)),然后在适配器中声明 kind,并将其添加到 `apps/daemon/internal/cli/agent_host_linux.go` 中 agent host 的静态列表([register the adapter](#register-the-adapter))。 -4. **打包原生先决条件。** 提供适配器的安装描述,并将 Harness 加入 agent-host 镜像([native installer participation](#native-installer-participation))。 +3. **声明支持并注册。** 在 `internal/harnessconfig/` 中声明支持并添加一个目录条目([declare support](#declare-support)),然后导出适配器声明,供生成的 agent-host 注册列表使用([register the adapter](#register-the-adapter))。 +4. **打包原生先决条件。** 提供适配器的安装描述,并将 Harness 加入 agent-host 镜像([原生 Harness 打包](#native-harness-packaging))。 5. **启用并选择引擎**,通过 `core.harnesses` 设置和 [Harness selection](model-execution.md#harness-selection) 完成。 6. **认定其资格**([qualify the adapter](#qualify-the-adapter)),并将每项原生差异记录到[覆盖台账](index.md)。 @@ -149,7 +149,7 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 ## 注册适配器 {#register-the-adapter} -注册是静态的,并且需要构建。从 `apps/daemon/internal/agent//declaration.go` 导出一个 `agent.Declaration`,然后将其添加到 [`cli/agent_host_linux.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_host_linux.go) 的 `harnessDeclarations` 中。声明包含 kind 及共享模型 `Configuration` 的声明中的能力、该 `Configuration` 和 `Discover` 函数。发现过程接收诊断写入器,负责原生配置和可用性检查,并返回已安装的 `agent.Runtime` 及其描述符和视图声明。未配置适配器时返回 nil;已配置的前置条件失败时,返回不带视图的不可用描述符。将版本门控和视图选择条件保留在适配器内部;它们只能清除支持。 +注册是静态的,并且需要构建。从 `apps/daemon/internal/agent//declaration.go` 导出一个 `agent.Declaration`。`make generate-harness-catalog` 根据各目录条目的 `configuration` 包,在 `apps/daemon/internal/cli/harness_catalog_linux.go` 中生成 agent host 的声明和 `Installation()` 列表。声明包含 kind 及共享模型 `Configuration` 的声明中的能力、该 `Configuration` 和 `Discover` 函数。发现过程接收诊断写入器,负责原生配置和可用性检查,并返回已安装的 `agent.Runtime` 及其描述符和视图声明。未配置适配器时返回 nil;已配置的前置条件失败时,返回不带视图的不可用描述符。将版本门控和视图选择条件保留在适配器内部;它们只能清除支持。 [`cli/agent_host_linux.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/cli/agent_host_linux.go) 以 `RegisterKind` 和 `RegisterView` 注册每个带视图的已发现 Runtime。随后 agent host 通过 `Registry.Register` 为 dispatch 注册每个这样的 kind,该方法把声明与 agent host 提供的 Environment 组合,再以 `RegisterExecutor` 安装 agent host 自己的 Executor 工厂;该工厂构建 Session 的视图,并调用视图的 `ViewExecutorFactory`。 @@ -169,7 +169,7 @@ MCP、公共函数、延迟函数发现、结构化输出、图像输入、详 ## 声明支持 {#declare-support} -Core 会识别[内置 Harness 注册项](harness-catalog.md)。向 `internal/harnessconfig/builtin/catalog.json` 添加一个条目,包含公共 `kind`、显示 `label` 和 `internal/harnessconfig` 下的模型 `configuration` 包,然后运行 `make generate-harness-catalog`。它会生成模型配置 registry、客户端标识符和显示名称以及注册参考;公共输入验证器读取生成的 registry。`make openapi` 从同一目录派生 Harness 枚举,因此不要在 DTO 标签或路由注解中添加手写枚举。`make check-harness-catalog` 会拒绝过时的投影。 +Core 会识别[内置 Harness 注册项](harness-catalog.md)。向 `internal/harnessconfig/builtin/catalog.json` 添加一个条目,包含公共 `kind`、显示 `label`、固定的上游 `version` 和 `internal/harnessconfig` 下的模型 `configuration` 包,然后运行 `make generate-harness-catalog`。它会生成模型配置 registry、agent-host 声明和安装列表、版本固定值投影、客户端标识符和显示名称以及注册参考;公共输入验证器读取生成的 registry。`make openapi` 从同一目录派生 Harness 枚举,因此不要在 DTO 标签或路由注解中添加手写枚举。`make check-harness-catalog` 会拒绝过时的投影。 `internal/harnessconfig/` 中 `Configuration()` 的 `Declaration` 就是 Harness 的支持范围:一个 `proto.Declaration`,包含其 `AgentKindCapabilities`、消息、图像、MCP 和输出 schema 限制,以及 `Conflicts` 中它能单独支持但不能同时支持的功能对。它说明适配器的最大支持范围,并且是唯一来源:Core 通过 `builtin.Registry()` 读取它,适配器的 Runtime 描述符也从它开始。发现过程和 Environment owner 只能清除支持,Core 拒绝扩大该声明的心跳。只声明 Harness 之间的真实差异;对每个 Harness 都成立的规则属于 `proto.ValidateSelection` 中的通用检查。 @@ -238,9 +238,17 @@ python services/core/tests/qualify_public_native.py \ 使用锁定的 SDK 运行 `python -m unittest discover -s services/core/tests -p qualify_public_native_test.py`,可在不调用模型的情况下检查凭据处理和所拥有的重启边界。现有确定性 Core 集成测试仍负责 schema 验证、原子准入、持久化回执和拒绝语义。真实模型结果仅证明所选套件、协议、Harness 和放置方式。Provider 生命周期、原生身份、凭据隔离、延迟发现和未选择的套件需要独立证据;仅通过 view 测试不能证明公共调用路径。 -## 原生安装器参与 {#native-installer-participation} +## 原生版本固定 {#native-version-pins} -适配器从自身包中的 `installation.go` 提供 `agent.Installation`:已注册 kind 和激活环境。agent host 使用该声明激活打包的 Harness。适配器负责原生布局;必须在 Linux agent host 上验证打包内容和执行。原生内容缺失或不兼容时必须失败;绝不会在 Turn 期间自行安装。自托管安装器不携带 Harness 或 Node.js。 +`internal/harnessconfig/builtin/catalog.json` 拥有每个内置 Harness 的上游版本。构建脚本读取该目录;适配器常量和包版本字段是生成的投影。更新目录并运行 `make generate-harness-catalog` 后再构建,然后使用 `make check-harness-catalog` 验证投影是否最新。版本变更需要[原生验收](#qualify-the-adapter)。 + +对于 Claude,`version` 固定 `packages/claude-sdk-adapter/package.json` 中的官方 Agent SDK 依赖;其 pnpm 锁文件必须解析到该精确版本,安装保持 frozen 模式。更改固定版本时通过 pnpm 更新锁文件。原生 Claude Code 的版本来自已安装 SDK 的 `claudeCodeVersion` 元数据,并与其运行时报告进行核验。不要单独编写原生 Claude Code 版本固定值。 + +对于 MiniMax,`packages/mcode-harness/source.json` 拥有上游仓库和源修订版本;其 `version` 从目录投影而来。配套程序构件携带 `source.json`,因此其源验证和来源记录可以独立于检出目录运行。 + +## 原生 Harness 打包 {#native-harness-packaging} + +适配器从自身包中的 `installation.go` 提供 `agent.Installation`:已注册的 `AgentKind` 和激活 `Environment`。生成的注册列表将这些声明传给 `agent.ManifestEnvironment`,由它根据镜像清单激活打包的 Harness。适配器负责原生布局;必须在 Linux agent host 上验证打包内容和执行。原生内容缺失或不兼容时必须失败;绝不会在 Turn 期间自行安装。自托管安装器不携带 Harness 或 Node.js。 `deploy/distribution/AgentHost.Dockerfile` 把每个 Harness 安装在各自的目录中,并列入镜像清单 `/opt/oac/harnesses.json`。`agent.ManifestEnvironment` 通过 `Installation.Environment` 激活它。将每个新增 Harness 加入该镜像和清单,并使用共享的[镜像构建](../../../docs/zh/maintainers.md#runtime-images-and-helpers)与[视图验收](#qualify-the-view)流程。 diff --git a/deploy/distribution/AgentHost.Dockerfile b/deploy/distribution/AgentHost.Dockerfile index 314f8665a..4c6b2959c 100644 --- a/deploy/distribution/AgentHost.Dockerfile +++ b/deploy/distribution/AgentHost.Dockerfile @@ -23,12 +23,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* COPY --chmod=0555 oac-daemon oac-process-shim /opt/oac/bin/ COPY --chmod=0555 codex/codex codex/codex-code-mode-host /opt/oac/harnesses/codex/bin/ +COPY codex/package.json /opt/oac/harnesses/codex/package.json COPY claude/claude-sdk /opt/oac/harnesses/claude_sdk COPY mcode/mcode-harness /opt/oac/harnesses/mcode COPY < "$context/upstream/.oac-source-revision" cp "$package/"*.mjs "$package/"*.ts "$package/"*.json "$context/" -test "$(node "$native/cli.js" --version)" = 0.4.12 +test "$(node "$native/cli.js" --version)" = "$version" ( cd "$context" MCODE_SOURCE="$context/upstream" node patch-native.mjs diff --git a/scripts/build-mcode-runtime.sh b/scripts/build-mcode-runtime.sh index 85a48aa99..c019e89b5 100644 --- a/scripts/build-mcode-runtime.sh +++ b/scripts/build-mcode-runtime.sh @@ -17,7 +17,8 @@ for file in launch.mjs bridge.mjs check.mjs tool-executor.mjs subagent-snapshot. exit 1 fi done -test "$(node "$native/cli.js" --version)" = 0.4.12 +version="$(python3 -c 'import json,sys; print(next(entry["version"] for entry in json.load(open(sys.argv[1])) if entry["kind"] == "mcode"))' "$repo_root/internal/harnessconfig/builtin/catalog.json")" +test "$(node "$native/cli.js" --version)" = "$version" mkdir -p "$runtime_root/cache/oac-runtime-builds" context="$(mktemp -d "$runtime_root/cache/oac-runtime-builds/mcode.XXXXXX")" trap 'rm -rf "$context"' EXIT diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index e07e9334c..5288fcb58 100644 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -109,7 +109,7 @@ IMAGE_INPUTS = ("scripts/build-core", "scripts/build-e2b-provider", "deploy/distribution/", "services/core/tools/e2b-provider/", "services/core/deploy/e2b/") # Generated outputs retain freshness checks even when the file is documentation. -GENERATED_OUTPUTS = {"contracts/agents-api/harness-catalog.md", "contracts/agents-api/zh/harness-catalog.md", "packages/agents-client/src/harness-catalog.ts"} +GENERATED_OUTPUTS = {"apps/daemon/internal/cli/harness_catalog_linux.go", "contracts/agents-api/harness-catalog.md", "contracts/agents-api/zh/harness-catalog.md", "packages/agents-client/src/harness-catalog.ts"} def full(reason): diff --git a/scripts/ci_plan_test.py b/scripts/ci_plan_test.py index 67a25f77d..22866ed61 100644 --- a/scripts/ci_plan_test.py +++ b/scripts/ci_plan_test.py @@ -52,7 +52,7 @@ def test_generated_outputs_keep_freshness_checks(self): spec = importlib.util.spec_from_file_location("catalog_generator", Path(__file__).with_name("generate-harness-catalog.py")) generator = importlib.util.module_from_spec(spec) spec.loader.exec_module(generator) - with patch.object(generator, "go", side_effect=lambda source: source): + with patch.object(generator, "go", side_effect=lambda source, **kwargs: source): outputs = generator.render(generator.load_catalog(generator.ROOT / generator.CATALOG)) for path in [*map(str, outputs), "scripts/acceptance/harness_catalog.py"]: with self.subTest(path=path): diff --git a/scripts/core-distribution-manifest.test.py b/scripts/core-distribution-manifest.test.py index bdcd5d03a..4273e356f 100644 --- a/scripts/core-distribution-manifest.test.py +++ b/scripts/core-distribution-manifest.test.py @@ -138,6 +138,39 @@ def test_mcode_payload_rejects_stale_companion_at_the_same_version(self): self.assertEqual((output / "mcode-harness" / name).read_bytes(), current) (companion / name).write_bytes(current) + def test_codex_payload_uses_catalog_pin_and_preserves_package_identity(self): + repository = self.stage / "repository" + scripts = repository / "scripts" + scripts.mkdir(parents=True) + builder = pathlib.Path(__file__).with_name("build-codex-runtime.sh") + (scripts / builder.name).write_bytes(builder.read_bytes()) + catalog = repository / "internal/harnessconfig/builtin/catalog.json" + catalog.parent.mkdir(parents=True) + catalog.write_text(json.dumps([{"kind": "codex", "version": "9.8.7"}])) + package = self.stage / "package" + native = package / "vendor/x86_64-unknown-linux-musl/bin" + native.mkdir(parents=True) + for executable in ("codex", "codex-code-mode-host"): + (native / executable).write_text("#!/bin/sh\nexit 0\n") + (native / executable).chmod(0o755) + manifest = {"name": "@openai/codex", "version": "9.8.7-linux-x64"} + (package / "package.json").write_text(json.dumps(manifest)) + output = self.stage / "payload" + environment = {**os.environ, "OAC_DEV_HOME": str(self.stage / "dev"), + "CODEX_CLI_DIR": str(package), "AGENTS_RUNTIME_BUILD_DIR": str(output)} + command = ["bash", str(scripts / builder.name)] + result = subprocess.run(command, env=environment, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads((output / "package.json").read_text()), manifest) + for invalid in ({**manifest, "version": "9.8.6-linux-x64"}, + {**manifest, "name": "unofficial-codex"}): + with self.subTest(package=invalid): + (package / "package.json").write_text(json.dumps(invalid)) + result = subprocess.run(command, env=environment, capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn("Expected pinned official Linux x64 package", result.stderr) + self.assertEqual(json.loads((output / "package.json").read_text()), manifest) + def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) diff --git a/scripts/generate-harness-catalog.py b/scripts/generate-harness-catalog.py index bcb45fe75..e29ecb9b5 100644 --- a/scripts/generate-harness-catalog.py +++ b/scripts/generate-harness-catalog.py @@ -19,23 +19,27 @@ def load_catalog(path): if not isinstance(entries, list) or not entries: raise ValueError("Harness catalog must be a nonempty list") kinds = set() + configurations = set() for entry in entries: - if not isinstance(entry, dict) or set(entry) != {"kind", "label", "configuration"}: + if not isinstance(entry, dict) or set(entry) != {"kind", "label", "configuration", "version"}: raise ValueError("Invalid Harness catalog fields") if any(not isinstance(value, str) for value in entry.values()): raise ValueError("Harness catalog values must be strings") if not re.fullmatch(r"[a-z][a-z0-9_]*", entry["kind"]) or entry["kind"] in kinds: raise ValueError("Invalid or duplicate Harness kind") - if not re.fullmatch(r"[a-z][a-z0-9]*", entry["configuration"]): - raise ValueError("Invalid configuration package") + if not re.fullmatch(r"[a-z][a-z0-9]*", entry["configuration"]) or entry["configuration"] in configurations: + raise ValueError("Invalid or duplicate configuration package") if not entry["label"].strip() or any(c in entry["label"] for c in "\n\r\t|"): raise ValueError("Invalid Harness label") + if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", entry["version"]): + raise ValueError("Invalid pinned Harness version") kinds.add(entry["kind"]) + configurations.add(entry["configuration"]) return sorted(entries, key=lambda entry: entry["kind"]) -def go(source): - return subprocess.run(["gofmt"], input=HEADER + source, text=True, +def go(source, build=""): + return subprocess.run(["gofmt"], input=build + HEADER + source, text=True, check=True, capture_output=True).stdout @@ -65,12 +69,25 @@ def render(entries): configurations = "\n".join( f'\t{json.dumps(entry["kind"])}: {entry["configuration"]}.Configuration(),' for entry in entries) + declarations = ", ".join(f'{entry["configuration"]}.Declaration' for entry in entries) + installations = ", ".join(f'{entry["configuration"]}.Installation()' for entry in entries) + adapter_imports = "\n".join(f'\t"{MODULE}/apps/daemon/internal/agent/{package}"' for package in packages) kinds = ", ".join(json.dumps(entry["kind"]) for entry in entries) tick = chr(96) rows = "\n".join( - f'| {tick}{e["kind"]}{tick} | {e["label"]} | {tick}{e["configuration"]}.Configuration{tick} |' + f'| {tick}{e["kind"]}{tick} | {e["label"]} | {tick}{e["version"]}{tick} | {tick}{e["configuration"]}.Configuration{tick} |' for e in entries) - return { + result = { + Path("apps/daemon/internal/cli/harness_catalog_linux.go"): go(f'''package cli + +import ( + "{MODULE}/apps/daemon/internal/agent" +{adapter_imports} +) + +var harnessDeclarations = []agent.Declaration{{{declarations}}} +var harnessInstallations = []agent.Installation{{{installations}}} +''', build="//go:build linux\n\n"), Path("internal/harnessconfig/builtin/registry.go"): go(f'''package builtin import ( @@ -102,10 +119,10 @@ def render(entries): Path("contracts/agents-api/harness-catalog.md"): f'''[//]: # (Generated by scripts/generate-harness-catalog.py; DO NOT EDIT.) # Built-in Harness registrations -The authored registration list is [{tick}catalog.json{tick}](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json). Change it and run {tick}make generate-harness-catalog{tick}; {tick}make check-harness-catalog{tick} checks the generated projections. +The authored registration list and Harness version pins are [{tick}catalog.json{tick}](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json). Change it and run {tick}make generate-harness-catalog{tick}; {tick}make check-harness-catalog{tick} checks the generated projections. -| Identifier | Display name | Declaration | -| --- | --- | --- | +| Identifier | Display name | Version | Declaration | +| --- | --- | --- | --- | {rows} Each declaration in {tick}internal/harnessconfig/{tick} states the Harness's model providers and its support, the only source Core and the Runtime admit a selection against. These registrations describe the build. Which Harnesses a deployment enables is the {tick}core.harnesses{tick} [process setting](../../docs/configuration.md#settings), and a connected Runtime reports its own availability. [Harness onboarding](./harness-onboarding.md) describes the adapter and packaging steps. @@ -113,16 +130,46 @@ def render(entries): Path("contracts/agents-api/zh/harness-catalog.md"): f'''[//]: # (Generated by scripts/generate-harness-catalog.py; DO NOT EDIT.) # 内置 Harness 注册 {{#built-in-harness-registrations}} -注册列表的源文件是 [{tick}catalog.json{tick}](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json)。修改后运行 {tick}make generate-harness-catalog{tick};{tick}make check-harness-catalog{tick} 检查生成结果。 +注册列表和 Harness 版本固定值的源文件是 [{tick}catalog.json{tick}](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/builtin/catalog.json)。修改后运行 {tick}make generate-harness-catalog{tick};{tick}make check-harness-catalog{tick} 检查生成结果。 -| 标识符 | 显示名称 | 声明 | -| --- | --- | --- | +| 标识符 | 显示名称 | 版本 | 声明 | +| --- | --- | --- | --- | {rows} {tick}internal/harnessconfig/{tick} 中的声明给出 Harness 的模型提供方及其支持范围,是 Core 和 Runtime 准入选择的唯一依据。这些注册描述当前构建。部署启用的 Harness 由 {tick}core.harnesses{tick} [进程设置](../../../docs/zh/configuration.md#settings)决定,连接的 Runtime 报告自身可用性。[Harness 接入](./harness-onboarding.md)介绍适配器和打包步骤。 ''', } + # Only these adapters gate support against a Go version constant. Claude + # validates the SDK dependency in its packaged manifest instead. + for entry in entries: + if entry["kind"] not in ("codex", "mcode"): + continue + package = entry["configuration"] + result[Path(f"internal/harnessconfig/{package}/version.go")] = go( + f'package {package}\n\n// NativeVersion is the packaged native Harness version.\nconst NativeVersion = {json.dumps(entry["version"])}\n') + return result + + +def render_manifests(entries, root): + versions = {entry["kind"]: entry["version"] for entry in entries} + result = {} + # Keep package-manager and standalone artifact manifests in their native + # formats. Only the Harness version is a projection; other fields retain + # their existing owners. + for kind, relative in (("claude_sdk", "packages/claude-sdk-adapter/package.json"), + ("mcode", "packages/mcode-harness/source.json")): + if kind not in versions: + continue + path = Path(relative) + manifest = json.loads((root / path).read_text()) + if kind == "claude_sdk": + manifest["dependencies"]["@anthropic-ai/claude-agent-sdk"] = versions[kind] + else: + manifest["version"] = versions[kind] + result[path] = json.dumps(manifest, indent=2) + "\n" + return result + def main(): parser = argparse.ArgumentParser(description=__doc__) @@ -139,7 +186,7 @@ def projection(relative, content): else: path.write_text(content) - for relative, content in render(entries).items(): + for relative, content in (render(entries) | render_manifests(entries, ROOT)).items(): projection(relative, content) # Refresh the Go registration before loading the adapter declarations. In check # mode a stale registration must fail before its declarations can be projected. diff --git a/scripts/generate-harness-catalog.test.py b/scripts/generate-harness-catalog.test.py index 6e78313cc..9a414a5db 100644 --- a/scripts/generate-harness-catalog.test.py +++ b/scripts/generate-harness-catalog.test.py @@ -2,12 +2,12 @@ """Verify catalog extension and generated contract freshness without live models.""" import importlib.util -import copy import json from pathlib import Path import tempfile import re import unittest +from unittest.mock import patch def module(name, file): @@ -30,7 +30,7 @@ def test_installer_projection_includes_every_declaration(self): self.assertEqual(namespace["PROVIDERS"], providers) def test_new_registration_reaches_all_projections(self): - entries = [{"kind": "example", "label": "Example", "configuration": "example"}] + entries = [{"kind": "example", "label": "Example", "configuration": "example", "version": "1.2.3"}] with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "catalog.json" path.write_text(json.dumps(entries)) @@ -42,6 +42,55 @@ def test_new_registration_reaches_all_projections(self): for old in ("codex", "claude_sdk", "mcode"): self.assertNotIn(old, content) self.assertIn('"example": example.Configuration()', generated[Path("internal/harnessconfig/builtin/registry.go")]) + cli = generated[Path("apps/daemon/internal/cli/harness_catalog_linux.go")] + self.assertIn("example.Declaration", cli) + self.assertIn("example.Installation()", cli) + self.assertTrue(cli.startswith("//go:build linux\n")) + + def test_go_version_gates_use_catalog_pins(self): + entries = catalog.load_catalog(catalog.ROOT / catalog.CATALOG) + generated = catalog.render(entries) + for entry in entries: + path = Path(f"internal/harnessconfig/{entry['configuration']}/version.go") + if entry["kind"] == "claude_sdk": + self.assertNotIn(path, generated) + else: + self.assertIn(f'const NativeVersion = "{entry["version"]}"', generated[path]) + + def test_manifest_projection_preserves_other_owners_and_detects_stale_pins(self): + entries = catalog.load_catalog(catalog.ROOT / catalog.CATALOG) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + originals = {} + for kind, relative in (("claude_sdk", "packages/claude-sdk-adapter/package.json"), + ("mcode", "packages/mcode-harness/source.json")): + original = json.loads((catalog.ROOT / relative).read_text()) + originals[relative] = original + stale = json.loads(json.dumps(original)) + if kind == "claude_sdk": + stale["dependencies"]["@anthropic-ai/claude-agent-sdk"] = "0.0.1" + else: + stale["version"] = "0.0.1" + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(stale, indent=2) + "\n") + projected = catalog.render_manifests(entries, root) + for relative, content in projected.items(): + expected = originals[str(relative)] + self.assertEqual(json.loads(content), expected) + self.assertNotEqual(content, (root / relative).read_text()) + # Check mode rejects these stale manifests before invoking Go. + catalog_path = root / catalog.CATALOG + catalog_path.parent.mkdir(parents=True) + catalog_path.write_text(json.dumps(entries)) + with patch.object(catalog, "ROOT", root), patch.object(catalog, "render", return_value={}), \ + patch.object(catalog.subprocess, "run") as run, patch("sys.argv", ["catalog", "--check"]): + with self.assertRaisesRegex(SystemExit, "package.json.*|source.json"): + catalog.main() + run.assert_not_called() + for relative, content in projected.items(): + (root / relative).write_text(content) + self.assertEqual(projected, catalog.render_manifests(entries, root)) def test_checked_in_openapi_enums_match_catalog(self): expected = [entry["kind"] for entry in catalog.load_catalog(catalog.ROOT / catalog.CATALOG)] @@ -67,10 +116,11 @@ def test_checked_in_openapi_enums_match_catalog(self): "stale Harness enum; run make openapi") def test_invalid_or_duplicate_registration_is_rejected(self): - entry = {"kind": "example", "label": "Example", "configuration": "example"} - candidates = [[], [entry, entry], [{**entry, "kind": "bad/kind"}], + entry = {"kind": "example", "label": "Example", "configuration": "example", "version": "1.2.3"} + candidates = [[], [entry, entry], [entry, {**entry, "kind": "another"}], [{**entry, "kind": "bad/kind"}], [{**entry, "configuration": "../example"}], - [{**entry, "unknown": True}]] + [{**entry, "unknown": True}], [{**entry, "version": "^1.2.3"}], + [{**entry, "version": "1.2.3-linux-x64"}], [{**entry, "version": 123}]] with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "catalog.json" for value in candidates: diff --git a/scripts/prepare-release-runtimes.sh b/scripts/prepare-release-runtimes.sh index 93ae5b041..8659df5e6 100644 --- a/scripts/prepare-release-runtimes.sh +++ b/scripts/prepare-release-runtimes.sh @@ -3,6 +3,8 @@ set -euo pipefail # Prepare pinned upstream inputs once, then reuse the existing Runtime builders. repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +catalog="$repo_root/internal/harnessconfig/builtin/catalog.json" +codex_version="$(python3 -c 'import json,sys; print(next(entry["version"] for entry in json.load(open(sys.argv[1])) if entry["kind"] == "codex"))' "$catalog")" release_root="$HOME/.oac/build/release-inputs" if [[ -e "$release_root" ]]; then printf 'Release input directory already exists; use a fresh build host\n' >&2 @@ -10,13 +12,13 @@ if [[ -e "$release_root" ]]; then fi mkdir -p "$release_root/codex" "$release_root/mcode-native" cd "$release_root/codex" -npm pack --ignore-scripts --silent @openai/codex@0.153.4-linux-x64 > package-name.txt +npm pack --ignore-scripts --silent "@openai/codex@$codex_version-linux-x64" > package-name.txt tar -xzf "$(cat package-name.txt)" pin="$repo_root/packages/mcode-harness/source.json" source_repository="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["repository"])' "$pin")" source_revision="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["revision"])' "$pin")" -source_version="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$pin")" +source_version="$(python3 -c 'import json,sys; print(next(entry["version"] for entry in json.load(open(sys.argv[1])) if entry["kind"] == "mcode"))' "$catalog")" git init --quiet "$release_root/mcode-source" git -C "$release_root/mcode-source" remote add origin "$source_repository" git -C "$release_root/mcode-source" fetch --depth 1 origin "$source_revision"