From e8ece99adcdd59b720330086058b1dacd12f726e Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Tue, 29 Sep 2026 17:52:51 +0000 Subject: [PATCH 1/2] refactor: declare public harness registrations once --- CONTRIBUTING.md | 11 ++ Makefile | 10 +- apps/docs/content/docs/configure.mdx | 83 +++-------- apps/docs/content/docs/development.mdx | 2 +- apps/docs/content/docs/harness-onboarding.mdx | 96 +++++------- apps/docs/content/guide-sources.json | 12 +- apps/docs/openapi/core-api.yaml | 4 +- apps/docs/openapi/public-api.yaml | 6 +- apps/docs/openapi/sources.json | 8 +- .../internal/cli/agent_registration.go | 9 +- apps/parsar-daemon/internal/cli/claude_sdk.go | 4 +- apps/web/src/i18n/locales/en/sessions.ts | 4 +- apps/web/src/i18n/locales/zh-CN/sessions.ts | 4 +- apps/web/src/lib/harness-labels.ts | 4 +- contracts/agents-api/core.openapi.yaml | 4 +- contracts/agents-api/harness-catalog.md | 19 +++ contracts/agents-api/harness-onboarding.md | 96 +++++------- contracts/agents-api/harness-selection.md | 66 ++------- contracts/agents-api/openapi.yaml | 6 +- contracts/agents-api/v1/core_extension.go | 11 +- .../agents-api/v1/harness_catalog_test.go | 23 +++ .../agents-api/v1/saved_core_extension.go | 4 +- docs/configuration.md | 83 +++-------- docs/development.md | 2 +- internal/harnessconfig/builtin/catalog.json | 5 + internal/harnessconfig/builtin/registry.go | 22 ++- .../agents-client/src/admin-projection.ts | 3 +- packages/agents-client/src/client.ts | 3 +- packages/agents-client/src/harness-catalog.ts | 9 ++ packages/agents-client/src/index.ts | 1 + packages/agents-client/src/types.ts | 3 +- scripts/generate-harness-catalog.py | 137 ++++++++++++++++++ scripts/generate-harness-catalog.test.py | 91 ++++++++++++ scripts/patch-agents-openapi.py | 43 +++++- .../internal/api/harness_model_providers.go | 12 +- .../internal/api/saved_core_input.go | 3 +- .../internal/engine/catalog_generated.go | 8 + .../agents-api/internal/engine/profile.go | 6 - 38 files changed, 555 insertions(+), 362 deletions(-) create mode 100644 contracts/agents-api/harness-catalog.md create mode 100644 contracts/agents-api/v1/harness_catalog_test.go create mode 100644 internal/harnessconfig/builtin/catalog.json create mode 100644 packages/agents-client/src/harness-catalog.ts create mode 100644 scripts/generate-harness-catalog.py create mode 100644 scripts/generate-harness-catalog.test.py create mode 100644 services/agents-api/internal/engine/catalog_generated.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 300e827dc..ba3ac314b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,7 @@ copy, when a contract changes. | Runtime messages, Executor/Turn lifetimes, receipts and failure ownership | [Core–Runtime protocol](docs/runtime-protocol.md) and `internal/agentdaemon/proto` | | Environment ownership and capability preparation (Skills, Plugins, MCP, `packages.system`) | [Environments](contracts/agents-api/environments.md) | | Adding a Harness (steps) | [Harness onboarding](contracts/agents-api/harness-onboarding.md), `apps/parsar-daemon/internal/agent/harness.go` and `internal/harnessconfig/harness.go` | +| Built-in Harness identifiers, configuration/profile bindings and display names | `internal/harnessconfig/builtin/catalog.json` and its [generated reference](contracts/agents-api/harness-catalog.md) | | Harness qualification and acceptance | [Harness integration](contracts/agents-api/harnesses.md) | | Harness selection and Agent defaults | [Harness selection](contracts/agents-api/harness-selection.md) | | Adding a Sandbox Provider | [Sandbox Provider guide](docs/sandbox-provider.md) and `services/agents-api/internal/sandbox/sandbox_provider.go` | @@ -46,6 +47,9 @@ copy, when a contract changes. are evidence, not current instructions or authority to restore a retired implementation. Keep task chronology and rollout reports out of contributor rules. - Keep current integration guidance separate from historical qualification evidence. +- A guide may summarize a workflow but must link to the owning contract for + versions, accepted values, precedence and lifecycle rules. Do not maintain + another normative copy. Generated references are projections, not new owners. ## Repository boundary @@ -320,6 +324,13 @@ production provider-switch guard. ### 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; + `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. + - `make sqlc-generate` owns only `services/agents-api/internal/db/sqlc` (sqlc v1.29.0). Do not rewrite landed migrations. - The public protocol schema is `contracts/agents-api/openapi.yaml`. Preserve diff --git a/Makefile b/Makefile index 16bf3ecb1..7396e4bdf 100644 --- a/Makefile +++ b/Makefile @@ -8,9 +8,17 @@ SWAG_VERSION ?= v1.16.4 help: @printf '%s\n' 'make build-agents-api Build standalone Core commands' 'make build-daemon Build the execution daemon' 'make check Run Core, persistence and runtime checks' 'See README.md for runtime prerequisites and deployment.' -check: check-docs check-names check-distribution check-database check-sqlc check-go check-microsandbox-provider check-agents-api check-claude-sdk check-web check-example check-mcode-harness +check: check-harness-catalog check-docs check-names check-distribution check-database check-sqlc check-go check-microsandbox-provider check-agents-api check-claude-sdk check-web check-example check-mcode-harness @printf 'OpenAgentCore checks passed.\n' +.PHONY: generate-harness-catalog check-harness-catalog +generate-harness-catalog: + python3 scripts/generate-harness-catalog.py + +check-harness-catalog: + python3 scripts/generate-harness-catalog.py --check + python3 scripts/generate-harness-catalog.test.py + .PHONY: check-names check-names: python3 scripts/check-names.test.py diff --git a/apps/docs/content/docs/configure.mdx b/apps/docs/content/docs/configure.mdx index e65a8159c..464faf819 100644 --- a/apps/docs/content/docs/configure.mdx +++ b/apps/docs/content/docs/configure.mdx @@ -149,52 +149,20 @@ Which harnesses are enabled, and the default one, are process settings ### Sandbox deployment -Exactly one provider serves the deployment: Docker, microsandbox or E2B. The -deployment specification holds the per-sandbox resources and, for Docker and -microsandbox, the Runtime release. Web's setup proposes the Standard size from -[`standard-sizes.json`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/apps/web/src/features/sandbox/standard-sizes.json) and -derives Small and Large from it; the installer applies the bundle's copy of the same -file. The database owns the saved values. - -| Setting | Standard | Meaning | -| --- | --- | --- | -| `resources.cpus` | 2 | Whole vCPUs per sandbox, 1 through 255 | -| `resources.memory_mib` | Docker 2048, microsandbox 4096 | MiB per sandbox, 512 through 1048576 | -| `resources.root_disk_mib`, `environment_disk_mib` | microsandbox 8192 each | At least 1024 MiB; microsandbox only | -| `runtime` | The bundle's Runtime release | Source revision, exact image ID and manifest, and Runtime and firmware hashes; never a mutable tag | -| Idle suspension, snapshot retention | 300 and 86400 seconds | microsandbox only; fixed | - -E2B takes no `runtime`, and Web sends no resources for it: Core adopts the CPU and -memory of the ready template build `template-id:build-uuid`, and supplied values must -match it. For an E2B-compatible service, enter both its HTTPS API origin and -sandbox data-plane domain in the setup wizard. Leaving both blank uses official -E2B. The API host must be the data-plane domain or one of its subdomains. -Changing either address requires draining the deployment in maintenance. -Docker has no separate disk quota. - -Changing backend type or E2B team requires an explicit reset and a new setup. -Same-team E2B template, resource and key changes apply online through the Core API: -new allocations use the new generation, while existing sandboxes retain their -original specification. Omit the key to preserve it; explicitly submitting a key, -even the same value, verifies the replacement and advances the generation. Keep the -old key valid until the update succeeds. Initial setup requires a team-owned template. -If Core cannot verify the committed ownership anchor, reset the deployment before -setting it up again. - -Docker and microsandbox size/Runtime edits advance the target generation online. -They require neither zero held resources nor node retirement or reenrollment. -Version 2 nodes prepare the target independently, while existing allocations and -suspended VMs retain their original generation. A qualified older serving generation -can still accept new Sessions when it has capacity, including while the target is -preparing or has failed. Target rollout and serving readiness are separate facts. -Use **System** → **Sandbox backend** → **Change resources** to edit the target; -**Nodes** owns node management and readiness. - -Runtime generation coexistence and ownership-scoped garbage collection operate -within the installed node release. Existing Sessions retain their placement and history. -See the [nodes guide](/hosted-providers#change-the-sandbox-backend-or-size), -[operator reference](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-reset) -and [deployment contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/sandbox-deployment.md). +Configure the backend and sandbox size in **System** → **Sandbox backend**. +**Change resources** edits the deployment target; **Nodes** manages enrolled +machines and their readiness. + +The [deployment contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/sandbox-deployment.md) owns +provider-specific fields, limits, Runtime identity and online change/reset rules. +The [node protocol](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/node-generation-protocol.md) owns +generation preparation and wire compatibility. This page does not maintain +another version table or set of provider rules. + +Web proposes sizes from +[`standard-sizes.json`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/apps/web/src/features/sandbox/standard-sizes.json); +the installer uses the bundle's copy of that source. For operator steps, see +[changing the sandbox backend or size](/hosted-providers#change-the-sandbox-backend-or-size). ### Node capacity @@ -210,23 +178,14 @@ node's own files can't change its capacity, size or Runtime. ### Default models -Each harness has at most one default model configuration: - -- `model`: the provider's model ID; -- `model_provider`: the same bundle as `x_agents_core.model_provider` (`protocol`, - HTTPS `base_url`, write-only `api_key`, and for MiniMax Code `context_window` and - `max_output_tokens`); -- `harness_config`: optional native model parameters, `{}` by default. - -Core encrypts the key with `secrets/credential.key` and never returns it. Set it in -Web or with `PUT /core/v1/harnesses/{harness}/model-configuration`. - -Which Sessions use the default, and in what order it applies, is in -[Which model provider a Session uses](/user-guide#which-model-provider-a-session-uses). -Full rules: [model execution](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/model-execution.md#deployment-defaults). +Set a default in **System** → **Default model**, or use +`PUT /core/v1/harnesses/{harness}/model-configuration`. Core encrypts provider keys +with `secrets/credential.key` and does not return them. -The operator file `AGENTS_API_EXECUTION_OPTIONS_FILE` is retired; see -[installation version policy](/troubleshooting#installation-version-policy). +[Model execution](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/model-execution.md#deployment-defaults) +owns the request fields, replacement rules and applicable sources. The +[user guide](/user-guide#which-model-provider-a-session-uses) explains how to +choose a source for a Session. ## Secrets and identity diff --git a/apps/docs/content/docs/development.mdx b/apps/docs/content/docs/development.mdx index ba09f0475..7173802e1 100644 --- a/apps/docs/content/docs/development.mdx +++ b/apps/docs/content/docs/development.mdx @@ -122,7 +122,7 @@ only helps you pick the right one. Implement the shared `ExecutorFactory`, `Executor` and `Turn` interfaces in [`agent/harness.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/apps/parsar-daemon/internal/agent/harness.go), register -the adapter and add a service profile. Follow the numbered steps in +the adapter and add its profile/configuration entry to the shared catalog. Follow the numbered steps in [Harness onboarding](/harness-onboarding); qualification evidence belongs in [Harness integration](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/harnesses.md). diff --git a/apps/docs/content/docs/harness-onboarding.mdx b/apps/docs/content/docs/harness-onboarding.mdx index a46a55d4a..c9f83bdf5 100644 --- a/apps/docs/content/docs/harness-onboarding.mdx +++ b/apps/docs/content/docs/harness-onboarding.mdx @@ -17,12 +17,6 @@ Start from two entry points: execution lifecycle, optional interfaces and registration methods. -The code entry point is -[`agent/harness.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/apps/parsar-daemon/internal/agent/harness.go). It -declares the required lifecycle, the separate optional interfaces and the -registration methods. The pinned public Agents API in [upstream.json](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/upstream.json) -is separate from this internal adapter contract. - ## Ownership ```text @@ -72,7 +66,7 @@ model communication configuration, not Turn scheduling or native process ownersh process, credential/configuration and local workspace helpers. 3. **Register the kind in the Runtime** in `apps/parsar-daemon/internal/cli`. See [Register the adapter](#register-the-adapter). -4. **Add the Core engine identifier and service profile.** See +4. **Add the service profile and one catalog entry.** See [Add the engine to Core](#add-the-engine-to-core). 5. **Package native prerequisites.** Add a Runtime image under `services/agents-api/deploy/` and, optionally, @@ -166,48 +160,22 @@ canonical interface entry point. Its required lifecycle is `ExecutorFactory`, remain separate; their result types and error values stay in the corresponding operation files in the same package. All use the existing neutral protocol types. -The factory prepares a fixed configuration without sending model input. Executor -owns the native process or connection, native Session, capability configuration and -cleanup resources. If preparation fails while cleanup remains unconfirmed, return -that non-nil Executor together with the error so Runtime retains its cleanup owner. -`StartTurn` creates a fresh Turn wrapper and event stream; -normal completion retains the Executor. Never reset a completed Turn object or -restart the process merely to implement the next Turn. - -A nil Turn from `StartTurn` guarantees no input was submitted and no output writer -was retained. Runtime closes the output channel in that case. Once input may have -been submitted, return a non-nil Turn even with an error: that Turn owns exactly -one output-channel close, and uncertain input must not be replayed. - -`Cancel` addresses that exact Turn (see -[Cancellation ownership](#cancellation-ownership)). `AwaitSettlement` confirms that native events, -inputs, function results, interactions, child work and output writes have settled. -Only then may `Reusable` be true. False requires a reason and confirmed Executor -close before replacement. An error means settlement is unconfirmed. A caller -cancelling its wait cannot discard resource ownership or redirect a late callback -to the next Turn. Close must retain its exact cleanup target after failure so the -Runtime can retry serially. Successful resource close alone does not prove that a -cancelled input was applied or that its original outcome is known. - -The Runtime owns admission handles, idle deadlines and resource limits. Adapters -own native translation. For example, a Codex adapter retains its app-server and -thread, a Claude adapter retains one streaming Query, and a MiniMax adapter retains -its ACP connection and native Session. Core sees the same Executor and Turn -semantics in each case. +The [Core–Runtime lifecycle contract](/runtime-protocol#executor-and-turn-lifetimes) +owns preparation failure, partial StartTurn results, output closure, settlement, +reuse and cleanup. Implement those obligations through the interfaces above. +Native callbacks and resources stay inside the adapter; Runtime owns admission, +idle expiry and replacement. -### Cancellation ownership +For example, a Codex adapter retains its app-server and thread, a Claude adapter +retains one streaming Query, and a MiniMax adapter retains its ACP connection and +native Session. Their implementations expose the same Executor and Turn contract. -Cancellation is declared on the base `agent.Session` interface, which every -`Turn` embeds. The adapter's Turn implements `Cancel` for that exact Turn and -never retargets it to a successor; `Executor` has no `Cancel`. Runtime dispatch -decides when to call it (for example on `prompt_cancel` or shutdown). +### Cancellation ownership -`Cancel` alone does not transfer resource ownership. `AwaitSettlement` confirms -the Turn settled, and `Executor.Close` retires native resources. Permission and -user-choice responses are the optional `PermissionResponder` and -`UserChoiceResponder` interfaces; implement them only when the adapter emits -those interactions. Unsupported responses receive a negative receipt. The router -keeps its interaction routing and retry ownership. +Implement cancellation on the exact Turn through `agent.Session`. Follow the +[lifecycle and settlement rules](/runtime-protocol#executor-and-turn-lifetimes); +the adapter must supply native completion evidence to the shared Runtime. +Permission and user-choice responses use the optional interfaces below. ## Events, inputs and optional capabilities @@ -268,14 +236,25 @@ never shipped as a real engine. ## Add the engine to Core -Core recognizes engine identifiers explicitly. Add the new identifier to: +Core recognizes the [built-in Harness registrations](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/harness-catalog.md). +Add one entry to `internal/harnessconfig/builtin/catalog.json` with: + +- the public `kind` and display `label`; +- the model `configuration` package under `internal/harnessconfig`; +- the `profile` constructor under `services/agents-api/internal/engine`. -| File | Purpose | -| --- | --- | -| [`internal/engine/profile.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/engine/profile.go) | The static service profile catalog | -| [`contracts/agents-api/v1/core_extension.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/v1/core_extension.go) | Accepted `x_agents_core.harness` values | -| [`internal/api/saved_core_input.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/api/saved_core_input.go) | Saved Agent extension validation | -| [`internal/harnessconfig/builtin/registry.go`](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/internal/harnessconfig/builtin/registry.go) | Built-in Harness configuration | +Implement the profile constructor, then run `make generate-harness-catalog`. +This generates the model configuration registry, Core profile catalog, client +identifiers/display names and registration reference. Public input validators read +the generated registry. `make openapi` derives Harness enums from the same authored +catalog; do not add handwritten enums to DTO tags or route annotations. +`make check-harness-catalog` rejects stale projections. + +Runtime registration uses `builtin.Configuration(kind)` for public Harnesses and +separately registers native factories, probes and installed capability evidence. +The catalog cannot declare a machine's availability. Adapter discovery and +packaging still require their own implementation and qualification; no dynamic +plugin loader is introduced. The profile is pure: it declares supported placements, public configuration and result limits and required Runtime controls, using existing public/protocol @@ -293,14 +272,9 @@ mutable global registration or compatibility fallback. ## Engine selection -| Setting | Scope | Effect | -| --- | --- | --- | -| `OAC_DEFAULT_HARNESS` | Core deployment | Default engine for new Sessions; `codex` when unset | -| `OAC_HARNESSES` | Core deployment | Comma-separated extra engines to enable, without requiring a managed Provider | -| `agent.x_agents_core.harness` | Saved Agent or inline Session Agent | Explicitly selects an enabled engine; never falls back to another | - -Existing Sessions keep their engine. Do not add another selector; the -[harness selection extension](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/harness-selection.md) owns the field's semantics. +The [Harness selection contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/harness-selection.md) owns the public selector, +deployment enablement, defaults and immutable Session binding. This guide adds no +second selector or fallback rule. ## Required versus optional operations diff --git a/apps/docs/content/guide-sources.json b/apps/docs/content/guide-sources.json index 599bc1930..b83979ad7 100644 --- a/apps/docs/content/guide-sources.json +++ b/apps/docs/content/guide-sources.json @@ -6,7 +6,7 @@ "docs/web/architecture.md": "7d5daf7745d2a25bf1113eea48512b085137f4f806c3fa0b6b16af63f7b18c4e", "docs/getting-started/install.md": "84b21002136acd4f006392685b7a071323336e87c1aac6e3c23ca56acb40833c", "docs/getting-started/install-options.md": "e03487bb6d978f84c596467028e25fd99fdc21cf2060af24ceb68df885084798", - "docs/configuration.md": "680719abd401cf643c59c74c2840955fe7a64f17ca93e1e1ba80e40e473e675e", + "docs/configuration.md": "7dc0031145bb5811bf22cab15270b511b1c43b1bd1ee2b71175d3bc848751bd6", "docs/web/core-connection.md": "861c75c32676fd17b84eb356b263096703af5750c1ceb71bb339e84607fffc56", "docs/getting-started/quickstart.md": "b989682ac2e58d59a794118fd371b37d1ea64c957d3512ff53739458a95d0f9a", "docs/api/README.md": "dbe3f172a997ee2fd3d2e5765d382b4fb7cf7c50f1dd17212ab9646f6959d63e", @@ -24,8 +24,8 @@ "docs/getting-started/operations.md": "4069e89deed7ef619f28cc8d9779055669246eabe322089113ac3a786c3cb2d5", "docs/user-guide.md": "190bea5bfe23b71db3d9437065ee270e07226a89a6e98c668853e5c7f7555529", "docs/assets/development-architecture.png": "24e6d0145d4f16ad70b07b6bc643808a6455aaf6398434d199cf74def200fca6", - "docs/development.md": "e875acfce070e1b38186d67d774f802861417b0a4a3600c0b2d3653042c5d1d6", - "contracts/agents-api/harness-onboarding.md": "18d3ccfe989e2caa57f220430157c6b73d87f5fc8e31aba8cada0b7ab9653a8c", + "docs/development.md": "f5c340253036a2cabc43e22aecdf70522d90280d6511e7649278ae93712ab8b6", + "contracts/agents-api/harness-onboarding.md": "234906652d381f4920d2245a795ad7f12f2f66577018db4dd5d545697d4f111b", "docs/runtime-bootstrap.md": "0d49aed73b298039e04453e6f465b0e925fb2227fa35d4206820bb3acbd8df39", "docs/runtime-protocol.md": "0e968e509236369dd4e840b94d9083372830c45b695ae3a71a56eba5bae01347", "docs/sandbox-provider.md": "3def50dfeb69e5160124cb9c4577e107e6ca666b40e6da32843a1c33b2b3515b", @@ -37,7 +37,7 @@ "content/docs/execution-model.mdx": "50133f3911c5ad801868e695ad52651e83809b80cbc887ee0bca25769ad32065", "content/docs/install.mdx": "6c3ec01d1b80cbce35d58e3f141b0fa832d6294a2ecc07c3b286ecc56ba66919", "content/docs/install-options.mdx": "aaafc209293a905d5b433511149aa1f0cc422bd31d5bb5b6cd7eec0d532ab8eb", - "content/docs/configure.mdx": "48682bc016a27331d6ac59defa33dbabd7696fd039eb4f3e0c63650b0970696d", + "content/docs/configure.mdx": "02d1eee789646fdf65ed2ec48b6fe954c81107d6ff5891536ec6ac2df0a8c4dc", "content/docs/bootstrap-projects-keys.mdx": "91a6f62cbe41737adfce9e8ec56e6dc3e2fb0ec8f6b677576f941cdfb378dc47", "content/docs/quickstart.mdx": "d0ab9537ab68f6dd3109353e937a29c5d58ec3894b56a52873c9a1d79dfa60a6", "content/docs/public-api.mdx": "cd59f4a661f897d1a9dc6ff08a09f7b7504c355288bbf022eff9b70500a9865c", @@ -55,8 +55,8 @@ "content/docs/troubleshooting.mdx": "bccd725d2389a80b66caf1a1da80532bd68dd3d470bf851799d0114f84e0af25", "content/docs/user-guide.mdx": "7c099b2d787ce3d7876889c7cefd621840ec083f267eadf278214a83f11ba274", "public/images/source/docs/assets/development-architecture.png": "24e6d0145d4f16ad70b07b6bc643808a6455aaf6398434d199cf74def200fca6", - "content/docs/development.mdx": "3238057db9027bb823dfc4fb6f98d3196da8567e9de2a23db8c3bd70fe0a5a7b", - "content/docs/harness-onboarding.mdx": "e7a172793d81e0dee89398ea34351617c66435a13f511f7fb29df3b6a8967fbf", + "content/docs/development.mdx": "2e5249ddfca571d264e93fd1e0e390a4bd5b0b623bda100cd02f2982929850bb", + "content/docs/harness-onboarding.mdx": "d9c9dcbd339ba9b278b133816c1d905bdbd849f44e2ff27882090cd39002e1c8", "content/docs/runtime-bootstrap.mdx": "58982955811c9ad46a9fc04d8d2aef5a762fc9d25d5833f88aa75ee3fc46523f", "content/docs/runtime-protocol.mdx": "75e7c85a2085cccedf453e8ee751316ba2ee22ed0559e4018ee82df1af805f48", "content/docs/sandbox-provider.mdx": "5472ccc9dc965c48716b6d73ff74263b4373bab6b41a8ae30040b6181d2de5c0" diff --git a/apps/docs/openapi/core-api.yaml b/apps/docs/openapi/core-api.yaml index 24f16f2c7..bb96577ca 100644 --- a/apps/docs/openapi/core-api.yaml +++ b/apps/docs/openapi/core-api.yaml @@ -5942,8 +5942,8 @@ components: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: @@ -7073,8 +7073,8 @@ components: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: diff --git a/apps/docs/openapi/public-api.yaml b/apps/docs/openapi/public-api.yaml index 4e3267064..0128e1282 100644 --- a/apps/docs/openapi/public-api.yaml +++ b/apps/docs/openapi/public-api.yaml @@ -3519,8 +3519,8 @@ components: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: @@ -4666,8 +4666,8 @@ components: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: @@ -4679,8 +4679,8 @@ components: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: diff --git a/apps/docs/openapi/sources.json b/apps/docs/openapi/sources.json index d5b1fcfd6..a61e16574 100644 --- a/apps/docs/openapi/sources.json +++ b/apps/docs/openapi/sources.json @@ -1,7 +1,7 @@ { "sources": { - "contracts/agents-api/openapi.yaml": "c68be89861971f10f01e129a3fbed1b18ad177516959acdce15339863ce65de9", - "contracts/agents-api/core.openapi.yaml": "5f17f3049a0892888bf4abe919b791b2df45c161b5fa4ee6e27efae2145c7d01", + "contracts/agents-api/openapi.yaml": "57c1b19119010613e80cf926ecc897d2011d8729c9fc6807dbc890d589839ddd", + "contracts/agents-api/core.openapi.yaml": "4458f46aae4aa708c5add12459f5d8b13a04190be14f200000446414c2ee67d5", "contracts/agents-api/runtime.openapi.yaml": "505286d5eacf94a14f9527fcf4e7beb4fba4f792681be26ba966254cbf49b4aa" }, "outputs": { @@ -18,7 +18,7 @@ "content/docs/api-reference/skills.mdx": "774ea05a7a8250fe5af5164388b98dafca99d56dc2eae686e8f628acb4e7c10b", "content/docs/api-reference/vaults.mdx": "d80eade1c2f8100030866abbb0fb6b7a09c94250a859c97cdf1abce32fdfdf1e", "content/docs/api-reference/credentials.mdx": "f129a51f74a3c7021c016600518e2350023f8d29571666ac5d64b7584cd7c7d0", - "openapi/public-api.yaml": "5439ed8aa9109dea4c07718302d6612d600a78f110068ccd3fcd8c6f89eb2b61", + "openapi/public-api.yaml": "a188f3cf63a391b3389a1d9abcb529f7f33651a621fe5a2c18f4c21aea4c523f", "content/docs/api-reference/meta.json": "56bf1dc145ccc8adde38c5f956f1a06f18ef18b646122250d01a294c516f8f83", "content/docs/api-reference/index.mdx": "9fd6b1c54149874d2999235067c101326f9e8a2e707a70b6672c81eb3ebfe63f", "content/docs/api-reference/core/core-administration.mdx": "5fc3ada51d2190a141f13738151e4d513390cc3bd66ce6f5013994bda68569b0", @@ -41,7 +41,7 @@ "content/docs/api-reference/core/vaults.mdx": "12d0aac96cd4a3472ef4eaa287f7945f78862017956e79bfc3ed0ef8e772e78a", "content/docs/api-reference/core/credentials.mdx": "27d586b9a8fd738b9947802f32c80458195370252886bc26abedccf2125ee981", "content/docs/api-reference/core/sandbox-manager.mdx": "45e65afffb25e5b6ee2ab0853d7a84dee164e0d44ea384675fbd7ffcbd6c1a58", - "openapi/core-api.yaml": "b673d2da3c0e444a307e6c70069426ca4a3c4c8f0d74c45ff8b25c662ec47b13", + "openapi/core-api.yaml": "724a8d38bdc19b52867430dbc404960576cc8cd429f68c5064b50e2fddd9131c", "content/docs/api-reference/core/meta.json": "d0aef1f54c4cfc30b4e1d974119ae1cdce1c87d5ecba723afbeb4599119482a7", "content/docs/api-reference/core/index.mdx": "57cf9ea947ef45e79575c25a0a3d4e5e856dde6d14698ccc2c51b86201c249e9", "content/docs/api-reference/machine/native-installation.mdx": "421a858685b79fdcf8d7eff308c5768d1b62385c3988c6089f380afe80aaed69", diff --git a/apps/parsar-daemon/internal/cli/agent_registration.go b/apps/parsar-daemon/internal/cli/agent_registration.go index 47a2c8ef8..1cd70ecef 100644 --- a/apps/parsar-daemon/internal/cli/agent_registration.go +++ b/apps/parsar-daemon/internal/cli/agent_registration.go @@ -1,8 +1,7 @@ package cli import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig" -import codexconfiguration "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/codex" -import mcodeconfiguration "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/mcode" +import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/builtin" import ( "context" @@ -25,7 +24,7 @@ func registerAgentKinds(registry *agent.Registry, agentCLIs agentCLIDiscovery, s registerProduct(agentCLIs.ClaudeCode, harnessconfig.Configuration{}, withSkillUploadServer(withCapabilityDownloads(claudecode.Factory, serverURL), serverURL)) registerProduct(agentCLIs.OpenCode, harnessconfig.Configuration{}, withSkillUploadServer(withCapabilityDownloads(opencodeagent.Factory, serverURL), serverURL)) - registerProduct(agentCLIs.Codex, codexconfiguration.Configuration(), withSkillUploadServer(withCapabilityDownloads(codex.Factory, serverURL), serverURL)) + registerProduct(agentCLIs.Codex, builtin.Configuration("codex"), withSkillUploadServer(withCapabilityDownloads(codex.Factory, serverURL), serverURL)) if agentCLIs.Codex.Available { registry.RegisterExecutor("codex", withExecutorCapabilities(codex.NewExecutorFactory(), serverURL)) } @@ -40,10 +39,10 @@ func registerAgentKinds(registry *agent.Registry, agentCLIs agentCLIDiscovery, s } registerProduct(agentCLIs.Pi, harnessconfig.Configuration{}, withSkillUploadServer(withCapabilityDownloads(pi.Factory, serverURL), serverURL)) if agentCLIs.MCodeWorkspace != nil { - registry.RegisterKind(agentCLIs.MCode, mcodeconfiguration.Configuration(), mcode.Factory) + registry.RegisterKind(agentCLIs.MCode, builtin.Configuration("mcode"), mcode.Factory) registry.RegisterPreparation("mcode", true, mcode.NewPreparationFactory(*agentCLIs.MCodeWorkspace)) } else { - registerProduct(agentCLIs.MCode, mcodeconfiguration.Configuration(), withSkillUploadServer(withCapabilityDownloads(mcode.Factory, serverURL), serverURL)) + registerProduct(agentCLIs.MCode, builtin.Configuration("mcode"), withSkillUploadServer(withCapabilityDownloads(mcode.Factory, serverURL), serverURL)) } if agentCLIs.MCode.Available { registry.RegisterExecutor("mcode", withExecutorCapabilities(mcode.NewExecutorFactory(agentCLIs.MCodeWorkspace), serverURL)) diff --git a/apps/parsar-daemon/internal/cli/claude_sdk.go b/apps/parsar-daemon/internal/cli/claude_sdk.go index ac146a472..304b294d2 100644 --- a/apps/parsar-daemon/internal/cli/claude_sdk.go +++ b/apps/parsar-daemon/internal/cli/claude_sdk.go @@ -1,6 +1,6 @@ package cli -import harnessconfiguration "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/claudesdk" +import "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/builtin" import ( "context" @@ -126,7 +126,7 @@ func registerClaudeSDK(registry *agent.Registry, discovery *claudeSDKDiscovery) return nil, fmt.Errorf("claude_sdk: configured runtime is unavailable") } } - registry.RegisterKind(discovery.Info, harnessconfiguration.Configuration(), factory) + registry.RegisterKind(discovery.Info, builtin.Configuration("claude_sdk"), factory) if discovery.Info.Available { registry.RegisterExecutor("claude_sdk", claudesdk.NewExecutorFactory(discovery.Config)) } diff --git a/apps/web/src/i18n/locales/en/sessions.ts b/apps/web/src/i18n/locales/en/sessions.ts index 21b0d5886..19686565e 100644 --- a/apps/web/src/i18n/locales/en/sessions.ts +++ b/apps/web/src/i18n/locales/en/sessions.ts @@ -1,3 +1,5 @@ +import { coreHarnessNames } from "@agents-core-web/agents-client"; + /** * Session log and one Session's read-only history (Monitor › Session log). * `validation` and `actions.errors` serve the retired Session-creation and @@ -15,7 +17,7 @@ export const sessions = { }, sessionStatus: { all: "All", in_progress: "Running", requires_action: "Waiting for caller", failed: "Failed", idle: "Idle", other: "Other" }, environment: { none: "No environment", self_hosted: "Self-hosted", openai_hosted: "Hosted sandbox", other: "Other" }, - harness: { claude_sdk: "Claude Code", codex: "Codex", mcode: "MiniMax Code" }, + harness: coreHarnessNames, usage: { input: "Input", output: "Output", total: "Total", cached: "Cached", reasoning: "Reasoning" }, log: { title: "Session log", diff --git a/apps/web/src/i18n/locales/zh-CN/sessions.ts b/apps/web/src/i18n/locales/zh-CN/sessions.ts index 0f2cae2b9..18cd5046f 100644 --- a/apps/web/src/i18n/locales/zh-CN/sessions.ts +++ b/apps/web/src/i18n/locales/zh-CN/sessions.ts @@ -1,3 +1,5 @@ +import { coreHarnessNames } from "@agents-core-web/agents-client"; + import { sessions as english } from "../en/sessions"; export const sessions = { @@ -12,7 +14,7 @@ export const sessions = { }, sessionStatus: { all: "全部", in_progress: "运行中", requires_action: "等待调用方", failed: "失败", idle: "空闲", other: "其他" }, environment: { none: "无环境", self_hosted: "自托管", openai_hosted: "托管沙箱", other: "其他" }, - harness: { claude_sdk: "Claude Code", codex: "Codex", mcode: "MiniMax Code" }, + harness: coreHarnessNames, usage: { input: "输入", output: "输出", total: "总计", cached: "缓存", reasoning: "推理" }, log: { title: "Session 日志", diff --git a/apps/web/src/lib/harness-labels.ts b/apps/web/src/lib/harness-labels.ts index a81dd8c9e..668b4d4c1 100644 --- a/apps/web/src/lib/harness-labels.ts +++ b/apps/web/src/lib/harness-labels.ts @@ -1,7 +1,7 @@ -import type { CoreHarnessKind, ModelProviderView } from "@agents-core-web/agents-client"; +import type { ModelProviderView } from "@agents-core-web/agents-client"; /** Harnesses by their product names, the same in every language. */ -export const harnessNames: Record = { claude_sdk: "Claude Code", codex: "Codex", mcode: "MiniMax Code" }; +export { coreHarnessNames as harnessNames } from "@agents-core-web/agents-client"; /** Provider protocols by their product names, the same in every language. */ export const protocolNames: Record = { anthropic: "Anthropic Messages", responses: "OpenAI Responses", chat_completions: "OpenAI Chat Completions" }; diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index cc79791c7..7381ff4d7 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -1621,8 +1621,8 @@ definitions: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: @@ -2702,8 +2702,8 @@ definitions: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: diff --git a/contracts/agents-api/harness-catalog.md b/contracts/agents-api/harness-catalog.md new file mode 100644 index 000000000..842296853 --- /dev/null +++ b/contracts/agents-api/harness-catalog.md @@ -0,0 +1,19 @@ + +# Built-in Harness registrations + +The authored registration list is +[`catalog.json`](../../internal/harnessconfig/builtin/catalog.json). +Change it and run `make generate-harness-catalog`; generated projections are checked +by `make check-harness-catalog`. + +| Identifier | Display name | Model configuration declaration | Core qualification constructor | +| --- | --- | --- | --- | +| `claude_sdk` | Claude Code | `claudesdk.Configuration` | `claudeProfile` | +| `codex` | Codex | `codex.Configuration` | `codexProfile` | +| `mcode` | MiniMax Code | `mcode.Configuration` | `mcodeProfile` | + +Configuration declarations live in `internal/harnessconfig/`; +qualification constructors live in `services/agents-api/internal/engine`. +These registrations describe the build. Deployment enablement, qualified +operations and a connected Runtime's actual availability remain separate checks. +See [Harness onboarding](harness-onboarding.md) for adapter and packaging steps. diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 3157d4c9c..ab8185c5d 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -14,12 +14,6 @@ Start from two entry points: execution lifecycle, optional interfaces and registration methods. -The code entry point is -[`agent/harness.go`](../../apps/parsar-daemon/internal/agent/harness.go). It -declares the required lifecycle, the separate optional interfaces and the -registration methods. The pinned public Agents API in [upstream.json](upstream.json) -is separate from this internal adapter contract. - ## Ownership ```text @@ -69,7 +63,7 @@ model communication configuration, not Turn scheduling or native process ownersh process, credential/configuration and local workspace helpers. 3. **Register the kind in the Runtime** in `apps/parsar-daemon/internal/cli`. See [Register the adapter](#register-the-adapter). -4. **Add the Core engine identifier and service profile.** See +4. **Add the service profile and one catalog entry.** See [Add the engine to Core](#add-the-engine-to-core). 5. **Package native prerequisites.** Add a Runtime image under `services/agents-api/deploy/` and, optionally, @@ -163,48 +157,22 @@ canonical interface entry point. Its required lifecycle is `ExecutorFactory`, remain separate; their result types and error values stay in the corresponding operation files in the same package. All use the existing neutral protocol types. -The factory prepares a fixed configuration without sending model input. Executor -owns the native process or connection, native Session, capability configuration and -cleanup resources. If preparation fails while cleanup remains unconfirmed, return -that non-nil Executor together with the error so Runtime retains its cleanup owner. -`StartTurn` creates a fresh Turn wrapper and event stream; -normal completion retains the Executor. Never reset a completed Turn object or -restart the process merely to implement the next Turn. - -A nil Turn from `StartTurn` guarantees no input was submitted and no output writer -was retained. Runtime closes the output channel in that case. Once input may have -been submitted, return a non-nil Turn even with an error: that Turn owns exactly -one output-channel close, and uncertain input must not be replayed. - -`Cancel` addresses that exact Turn (see -[Cancellation ownership](#cancellation-ownership)). `AwaitSettlement` confirms that native events, -inputs, function results, interactions, child work and output writes have settled. -Only then may `Reusable` be true. False requires a reason and confirmed Executor -close before replacement. An error means settlement is unconfirmed. A caller -cancelling its wait cannot discard resource ownership or redirect a late callback -to the next Turn. Close must retain its exact cleanup target after failure so the -Runtime can retry serially. Successful resource close alone does not prove that a -cancelled input was applied or that its original outcome is known. - -The Runtime owns admission handles, idle deadlines and resource limits. Adapters -own native translation. For example, a Codex adapter retains its app-server and -thread, a Claude adapter retains one streaming Query, and a MiniMax adapter retains -its ACP connection and native Session. Core sees the same Executor and Turn -semantics in each case. +The [Core–Runtime lifecycle contract](../../docs/runtime-protocol.md#executor-and-turn-lifetimes) +owns preparation failure, partial StartTurn results, output closure, settlement, +reuse and cleanup. Implement those obligations through the interfaces above. +Native callbacks and resources stay inside the adapter; Runtime owns admission, +idle expiry and replacement. -### Cancellation ownership +For example, a Codex adapter retains its app-server and thread, a Claude adapter +retains one streaming Query, and a MiniMax adapter retains its ACP connection and +native Session. Their implementations expose the same Executor and Turn contract. -Cancellation is declared on the base `agent.Session` interface, which every -`Turn` embeds. The adapter's Turn implements `Cancel` for that exact Turn and -never retargets it to a successor; `Executor` has no `Cancel`. Runtime dispatch -decides when to call it (for example on `prompt_cancel` or shutdown). +### Cancellation ownership -`Cancel` alone does not transfer resource ownership. `AwaitSettlement` confirms -the Turn settled, and `Executor.Close` retires native resources. Permission and -user-choice responses are the optional `PermissionResponder` and -`UserChoiceResponder` interfaces; implement them only when the adapter emits -those interactions. Unsupported responses receive a negative receipt. The router -keeps its interaction routing and retry ownership. +Implement cancellation on the exact Turn through `agent.Session`. Follow the +[lifecycle and settlement rules](../../docs/runtime-protocol.md#executor-and-turn-lifetimes); +the adapter must supply native completion evidence to the shared Runtime. +Permission and user-choice responses use the optional interfaces below. ## Events, inputs and optional capabilities @@ -265,14 +233,25 @@ never shipped as a real engine. ## Add the engine to Core -Core recognizes engine identifiers explicitly. Add the new identifier to: +Core recognizes the [built-in Harness registrations](harness-catalog.md). +Add one entry to `internal/harnessconfig/builtin/catalog.json` with: + +- the public `kind` and display `label`; +- the model `configuration` package under `internal/harnessconfig`; +- the `profile` constructor under `services/agents-api/internal/engine`. -| File | Purpose | -| --- | --- | -| [`internal/engine/profile.go`](../../services/agents-api/internal/engine/profile.go) | The static service profile catalog | -| [`contracts/agents-api/v1/core_extension.go`](v1/core_extension.go) | Accepted `x_agents_core.harness` values | -| [`internal/api/saved_core_input.go`](../../services/agents-api/internal/api/saved_core_input.go) | Saved Agent extension validation | -| [`internal/harnessconfig/builtin/registry.go`](../../internal/harnessconfig/builtin/registry.go) | Built-in Harness configuration | +Implement the profile constructor, then run `make generate-harness-catalog`. +This generates the model configuration registry, Core profile catalog, client +identifiers/display names and registration reference. Public input validators read +the generated registry. `make openapi` derives Harness enums from the same authored +catalog; do not add handwritten enums to DTO tags or route annotations. +`make check-harness-catalog` rejects stale projections. + +Runtime registration uses `builtin.Configuration(kind)` for public Harnesses and +separately registers native factories, probes and installed capability evidence. +The catalog cannot declare a machine's availability. Adapter discovery and +packaging still require their own implementation and qualification; no dynamic +plugin loader is introduced. The profile is pure: it declares supported placements, public configuration and result limits and required Runtime controls, using existing public/protocol @@ -290,14 +269,9 @@ mutable global registration or compatibility fallback. ## Engine selection -| Setting | Scope | Effect | -| --- | --- | --- | -| `OAC_DEFAULT_HARNESS` | Core deployment | Default engine for new Sessions; `codex` when unset | -| `OAC_HARNESSES` | Core deployment | Comma-separated extra engines to enable, without requiring a managed Provider | -| `agent.x_agents_core.harness` | Saved Agent or inline Session Agent | Explicitly selects an enabled engine; never falls back to another | - -Existing Sessions keep their engine. Do not add another selector; the -[harness selection extension](harness-selection.md) owns the field's semantics. +The [Harness selection contract](harness-selection.md) owns the public selector, +deployment enablement, defaults and immutable Session binding. This guide adds no +second selector or fallback rule. ## Required versus optional operations diff --git a/contracts/agents-api/harness-selection.md b/contracts/agents-api/harness-selection.md index abef34fc2..395d77a57 100644 --- a/contracts/agents-api/harness-selection.md +++ b/contracts/agents-api/harness-selection.md @@ -8,8 +8,8 @@ Agent configuration. This is a Core extension, not an upstream field. {"x_agents_core":{"harness":"claude_sdk"}} ``` -Supported identifiers are `codex`, `claude_sdk` (Claude Code), and `mcode` -(MiniMax Code). Unknown identifiers, empty objects and unknown nested fields are +Supported identifiers come from the [generated Harness catalog](harness-catalog.md). +Unknown identifiers, empty objects and unknown nested fields are rejected. Omission inherits a saved Agent value, or uses `OAC_DEFAULT_HARNESS` for an inline Agent. An explicit null clears the saved selection or replaces it for one Session, restoring deployment-default selection. Agent updates preserve omitted @@ -40,49 +40,15 @@ adds comma-separated deployment-supported engines, for example remains enabled; unknown names fail startup. This setting does not install a harness or qualify a native deployment. -Hosted placement uses one provider per deployment, saved in PostgreSQL. Web or -`POST /core/v1/sandbox/deployment` with the Core key selects `docker`, -`microsandbox` or `e2b`, the per-sandbox resources and one immutable Runtime -release (an E2B template build for E2B); see the -[deployment configuration](sandbox-deployment.md). Core needs a stable -`OAC_INSTALLATION_ID` for this; the installer generates it. Docker -and microsandbox sandboxes run on enrolled nodes, whose files hold only host paths -and an installed copy of that selection. The former -`AGENTS_API_MANAGED_RUNTIMES_FILE` is rejected at startup. +Harness selection is independent of Environment provisioning. Provider selection, +node generations and reset behavior are owned by +[Sandbox deployment](sandbox-deployment.md); node wire compatibility is owned by +[the node protocol](node-generation-protocol.md). Enabling a Harness does not +install or qualify it on an existing Runtime. -There is no engine-to-provider routing or mixed-provider configuration. Enabling -another harness does not choose another image or backend; the selected Runtime -image must contain and qualify each enabled harness. Capability checks still -apply. - -Same-provider node resource/Runtime edits advance the target online without an -execution drain or reenrollment. Version 2 nodes prepare independently; retained -allocations keep their generation, and a qualified older serving generation can -still accept new Sessions. E2B same-team changes also apply online with immutable -per-allocation generations. Changing provider requires explicit durable reset, -confirmed cleanup and a new setup at the resulting generation. Existing Sessions -never move providers. See the [deployment procedure](../../services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-reset). - -Each engine has at most one deployment default model provider, stored encrypted in -PostgreSQL and managed with the Core key in Web or through -`/core/v1/harnesses/{harness}/model-configuration`; `GET /core/v1/harnesses` lists every -engine with its enabled and default flags and its safe provider view. No engine -inherits another engine's credentials. New `openai_hosted` and `none` Sessions that -resolve no Session or Agent bundle freeze their engine's default; `self_hosted` -Sessions never use it. Saved Agent provider credentials use separately encrypted -defaults. Public reads return only safe fields and a configured flag; credentials -never enter metadata or ordinary effective responses. A Session may instead provide -the [write-only model execution extension](model-execution.md); its frozen -configuration takes precedence. See -[model execution](model-execution.md#deployment-defaults) for precedence and where -each source applies. The former `AGENTS_API_EXECUTION_OPTIONS_FILE` is rejected at -startup. - -Model names remain explicit `agent.model` values. The selected native adapter uses -its configured provider and rejects unsupported model settings without changing -model identity. Core applies its existing engine/environment/tool/verbosity rules -before creating a Session; provider model availability is checked during native -startup/execution. There is no invented cross-provider model-name catalog. +Model/provider defaults, source precedence and credential handling are owned by +[Model execution](model-execution.md#saved-defaults-and-precedence). They do not +introduce another Harness selector. Current profile limits remain in the [engine coverage table](README.md#public-engine-profiles). @@ -91,13 +57,5 @@ Current profile limits remain in the [engine coverage table](README.md#public-en Core accepts the optional `agent.x_agents_core.harness` extension through the saved Agent and inline Session configuration paths. Define the extension once in `contracts/agents-api/v1`; never use metadata or a competing top-level selector. -Resolve saved overrides before selecting the existing Session engine, and apply -that engine's execution policy before persistence. Omitted selection preserves -the deployment default; explicit unavailable selection fails without fallback. -Effective extension reads use the persisted engine; Sessions without the extension -retain the official Agent response shape. Null and retry behavior are described above. - -Hosted provider selection belongs to deployment configuration and is independent -of the engine. Session creation fixes a node through automatic placement; retained allocations keep that node and provider identity. Runtime images must satisfy their existing qualification rules. -Transient model options are partitioned by engine and must not expose another -engine's credentials. Do not infer an engine from a model name or template. +Use the catalog-derived validators for identifiers and the selection semantics +above. Do not maintain another accepted-name list in handlers or schemas. diff --git a/contracts/agents-api/openapi.yaml b/contracts/agents-api/openapi.yaml index 6e0e23607..ea334ef7d 100644 --- a/contracts/agents-api/openapi.yaml +++ b/contracts/agents-api/openapi.yaml @@ -90,8 +90,8 @@ definitions: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: @@ -1167,8 +1167,8 @@ definitions: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: @@ -1180,8 +1180,8 @@ definitions: properties: harness: enum: - - codex - claude_sdk + - codex - mcode type: string harness_config: diff --git a/contracts/agents-api/v1/core_extension.go b/contracts/agents-api/v1/core_extension.go index 77cbb0514..0da78ce94 100644 --- a/contracts/agents-api/v1/core_extension.go +++ b/contracts/agents-api/v1/core_extension.go @@ -3,11 +3,12 @@ package v1 import ( "encoding/json" "fmt" + "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/builtin" ) // AgentsCore selects an existing Core harness independently of model identity. type AgentsCore struct { - Harness string `json:"harness,omitempty" enums:"codex,claude_sdk,mcode"` + Harness string `json:"harness,omitempty"` HarnessConfig json.RawMessage `json:"harness_config,omitempty" swaggertype:"object"` } @@ -18,10 +19,8 @@ func (x *AgentsCore) Validate() error { if x.Harness == "" && len(x.HarnessConfig) == 0 { return fmt.Errorf("x_agents_core requires harness or harness_config") } - switch x.Harness { - case "", "codex", "claude_sdk", "mcode": - return ValidateHarnessConfig(x.Harness, x.HarnessConfig) - default: - return fmt.Errorf("x_agents_core.harness must be codex, claude_sdk or mcode") + if x.Harness != "" && !builtin.Contains(x.Harness) { + return fmt.Errorf("x_agents_core.harness must select a registered harness") } + return ValidateHarnessConfig(x.Harness, x.HarnessConfig) } diff --git a/contracts/agents-api/v1/harness_catalog_test.go b/contracts/agents-api/v1/harness_catalog_test.go new file mode 100644 index 000000000..b26bf4f17 --- /dev/null +++ b/contracts/agents-api/v1/harness_catalog_test.go @@ -0,0 +1,23 @@ +package v1 + +import ( + "testing" + + "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/builtin" +) + +func TestRegisteredHarnessesSharePublicValidation(t *testing.T) { + for _, kind := range builtin.Kinds() { + if err := (&AgentsCore{Harness: kind}).Validate(); err != nil { + t.Fatalf("inline %s: %v", kind, err) + } + if err := (&SavedAgentCoreInput{Harness: kind}).Validate(); err != nil { + t.Fatalf("saved %s: %v", kind, err) + } + } + for _, kind := range []string{"unregistered", "Codex", " codex"} { + if (&AgentsCore{Harness: kind}).Validate() == nil || (&SavedAgentCoreInput{Harness: kind}).Validate() == nil { + t.Fatalf("unregistered Harness %q accepted", kind) + } + } +} diff --git a/contracts/agents-api/v1/saved_core_extension.go b/contracts/agents-api/v1/saved_core_extension.go index 2708fefba..fea0cecbf 100644 --- a/contracts/agents-api/v1/saved_core_extension.go +++ b/contracts/agents-api/v1/saved_core_extension.go @@ -12,14 +12,14 @@ import ( // is replaced as a whole; its API key is write-only. type SavedAgentCoreInput struct { HarnessConfig json.RawMessage `json:"harness_config,omitempty" swaggertype:"object"` - Harness string `json:"harness,omitempty" enums:"codex,claude_sdk,mcode"` + Harness string `json:"harness,omitempty"` ModelProvider *ModelProviderInput `json:"model_provider,omitempty" extensions:"x-nullable"` } // SavedAgentCore is the non-confidential representation of saved defaults. type SavedAgentCore struct { HarnessConfig json.RawMessage `json:"harness_config,omitempty" swaggertype:"object"` - Harness string `json:"harness,omitempty" enums:"codex,claude_sdk,mcode"` + Harness string `json:"harness,omitempty"` ModelProvider *ModelProviderView `json:"model_provider,omitempty"` } diff --git a/docs/configuration.md b/docs/configuration.md index 94634da3d..a0aaa69a8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -146,52 +146,20 @@ Which harnesses are enabled, and the default one, are process settings ### Sandbox deployment -Exactly one provider serves the deployment: Docker, microsandbox or E2B. The -deployment specification holds the per-sandbox resources and, for Docker and -microsandbox, the Runtime release. Web's setup proposes the Standard size from -[`standard-sizes.json`](../apps/web/src/features/sandbox/standard-sizes.json) and -derives Small and Large from it; the installer applies the bundle's copy of the same -file. The database owns the saved values. - -| Setting | Standard | Meaning | -| --- | --- | --- | -| `resources.cpus` | 2 | Whole vCPUs per sandbox, 1 through 255 | -| `resources.memory_mib` | Docker 2048, microsandbox 4096 | MiB per sandbox, 512 through 1048576 | -| `resources.root_disk_mib`, `environment_disk_mib` | microsandbox 8192 each | At least 1024 MiB; microsandbox only | -| `runtime` | The bundle's Runtime release | Source revision, exact image ID and manifest, and Runtime and firmware hashes; never a mutable tag | -| Idle suspension, snapshot retention | 300 and 86400 seconds | microsandbox only; fixed | - -E2B takes no `runtime`, and Web sends no resources for it: Core adopts the CPU and -memory of the ready template build `template-id:build-uuid`, and supplied values must -match it. For an E2B-compatible service, enter both its HTTPS API origin and -sandbox data-plane domain in the setup wizard. Leaving both blank uses official -E2B. The API host must be the data-plane domain or one of its subdomains. -Changing either address requires draining the deployment in maintenance. -Docker has no separate disk quota. - -Changing backend type or E2B team requires an explicit reset and a new setup. -Same-team E2B template, resource and key changes apply online through the Core API: -new allocations use the new generation, while existing sandboxes retain their -original specification. Omit the key to preserve it; explicitly submitting a key, -even the same value, verifies the replacement and advances the generation. Keep the -old key valid until the update succeeds. Initial setup requires a team-owned template. -If Core cannot verify the committed ownership anchor, reset the deployment before -setting it up again. - -Docker and microsandbox size/Runtime edits advance the target generation online. -They require neither zero held resources nor node retirement or reenrollment. -Version 2 nodes prepare the target independently, while existing allocations and -suspended VMs retain their original generation. A qualified older serving generation -can still accept new Sessions when it has capacity, including while the target is -preparing or has failed. Target rollout and serving readiness are separate facts. -Use **System** → **Sandbox backend** → **Change resources** to edit the target; -**Nodes** owns node management and readiness. - -Runtime generation coexistence and ownership-scoped garbage collection operate -within the installed node release. Existing Sessions retain their placement and history. -See the [nodes guide](getting-started/nodes.md#change-the-sandbox-backend-or-size), -[operator reference](../services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-reset) -and [deployment contract](../contracts/agents-api/sandbox-deployment.md). +Configure the backend and sandbox size in **System** → **Sandbox backend**. +**Change resources** edits the deployment target; **Nodes** manages enrolled +machines and their readiness. + +The [deployment contract](../contracts/agents-api/sandbox-deployment.md) owns +provider-specific fields, limits, Runtime identity and online change/reset rules. +The [node protocol](../contracts/agents-api/node-generation-protocol.md) owns +generation preparation and wire compatibility. This page does not maintain +another version table or set of provider rules. + +Web proposes sizes from +[`standard-sizes.json`](../apps/web/src/features/sandbox/standard-sizes.json); +the installer uses the bundle's copy of that source. For operator steps, see +[changing the sandbox backend or size](getting-started/nodes.md#change-the-sandbox-backend-or-size). ### Node capacity @@ -207,23 +175,14 @@ node's own files can't change its capacity, size or Runtime. ### Default models -Each harness has at most one default model configuration: - -- `model`: the provider's model ID; -- `model_provider`: the same bundle as `x_agents_core.model_provider` (`protocol`, - HTTPS `base_url`, write-only `api_key`, and for MiniMax Code `context_window` and - `max_output_tokens`); -- `harness_config`: optional native model parameters, `{}` by default. - -Core encrypts the key with `secrets/credential.key` and never returns it. Set it in -Web or with `PUT /core/v1/harnesses/{harness}/model-configuration`. - -Which Sessions use the default, and in what order it applies, is in -[Which model provider a Session uses](user-guide.md#which-model-provider-a-session-uses). -Full rules: [model execution](../contracts/agents-api/model-execution.md#deployment-defaults). +Set a default in **System** → **Default model**, or use +`PUT /core/v1/harnesses/{harness}/model-configuration`. Core encrypts provider keys +with `secrets/credential.key` and does not return them. -The operator file `AGENTS_API_EXECUTION_OPTIONS_FILE` is retired; see -[installation version policy](getting-started/operations.md#installation-version-policy). +[Model execution](../contracts/agents-api/model-execution.md#deployment-defaults) +owns the request fields, replacement rules and applicable sources. The +[user guide](user-guide.md#which-model-provider-a-session-uses) explains how to +choose a source for a Session. ## Secrets and identity diff --git a/docs/development.md b/docs/development.md index 9a744d834..d793fcdc7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -119,7 +119,7 @@ only helps you pick the right one. Implement the shared `ExecutorFactory`, `Executor` and `Turn` interfaces in [`agent/harness.go`](../apps/parsar-daemon/internal/agent/harness.go), register -the adapter and add a service profile. Follow the numbered steps in +the adapter and add its profile/configuration entry to the shared catalog. Follow the numbered steps in [Harness onboarding](../contracts/agents-api/harness-onboarding.md); qualification evidence belongs in [Harness integration](../contracts/agents-api/harnesses.md). diff --git a/internal/harnessconfig/builtin/catalog.json b/internal/harnessconfig/builtin/catalog.json new file mode 100644 index 000000000..9328ac7d7 --- /dev/null +++ b/internal/harnessconfig/builtin/catalog.json @@ -0,0 +1,5 @@ +[ + {"kind": "claude_sdk", "label": "Claude Code", "configuration": "claudesdk", "profile": "claudeProfile"}, + {"kind": "codex", "label": "Codex", "configuration": "codex", "profile": "codexProfile"}, + {"kind": "mcode", "label": "MiniMax Code", "configuration": "mcode", "profile": "mcodeProfile"} +] diff --git a/internal/harnessconfig/builtin/registry.go b/internal/harnessconfig/builtin/registry.go index 27de5d0b3..197931649 100644 --- a/internal/harnessconfig/builtin/registry.go +++ b/internal/harnessconfig/builtin/registry.go @@ -1,5 +1,4 @@ -// Package builtin composes adapter-owned provider validation rules for Core. -// It contains no native launch configuration or public discovery contract. +// Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT. package builtin import ( @@ -10,9 +9,26 @@ import ( ) var registry = harnessconfig.NewRegistry(map[string]harnessconfig.Configuration{ - "codex": codex.Configuration(), "claude_sdk": claudesdk.Configuration(), + "codex": codex.Configuration(), "mcode": mcode.Configuration(), }) func Registry() harnessconfig.Registry { return registry } + +// Configuration binds a registered public Harness to its authored declaration. +func Configuration(kind string) harnessconfig.Configuration { + configuration, ok := registry.Lookup(kind) + if !ok { + panic("unregistered public Harness configuration") + } + return configuration +} + +// Kinds returns the public identifiers in this build, not Runtime availability. +func Kinds() []string { return []string{"claude_sdk", "codex", "mcode"} } + +func Contains(kind string) bool { + _, ok := registry.Lookup(kind) + return ok +} diff --git a/packages/agents-client/src/admin-projection.ts b/packages/agents-client/src/admin-projection.ts index 398db09bb..a6f3a5577 100644 --- a/packages/agents-client/src/admin-projection.ts +++ b/packages/agents-client/src/admin-projection.ts @@ -1,3 +1,4 @@ +import { coreHarnessKinds } from "./harness-catalog"; import { AgentCoreError, projectRuntimeObservation, projectSavedAgentConfiguration } from "./client"; import { projectTokenUsage } from "./usage-projection"; import { safeProvider } from "./execution-configuration-projection"; @@ -224,7 +225,7 @@ export function projectIssuedExecutorCredential(value: unknown, keyId: string, e } const providerObservationErrors = new Set(["authentication_error", "connection_failed", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "resource_not_found", "request_timeout", "invalid_request"]); -const harnessKinds = new Set(["claude_sdk", "codex", "mcode"]); +const harnessKinds = new Set(coreHarnessKinds); /** A deployment default model provider: exactly the safe view, never `api_key`. */ export function projectHarnessModelConfiguration(value: unknown, harness?: CoreHarnessKind): HarnessModelConfiguration { if (!isRecord(value) || !onlyFields(value, new Set(["object", "harness", "updated_at", "last_used_at", "last_error_code", "last_error_at", "model_provider", "model", "harness_config"]))) return invalidAdminResponse(); diff --git a/packages/agents-client/src/client.ts b/packages/agents-client/src/client.ts index 8f1eaa8f5..c7298d75b 100644 --- a/packages/agents-client/src/client.ts +++ b/packages/agents-client/src/client.ts @@ -1,3 +1,4 @@ +import { coreHarnessKinds } from "./harness-catalog"; import { projectEnvironmentInstallation } from "./installation-projection"; import { exactFields, onlyFields, isRecord, hasOwn, canonicalUuid, isNonnegativeInteger, sameResourceId } from "./response-projection"; import { projectTokenUsage } from "./usage-projection"; @@ -179,7 +180,7 @@ function trimTrailingSlash(value: string): string { return value.replace(/\/+$/, ""); } -const harnessKinds = new Set(["claude_sdk", "codex", "mcode"]); +const harnessKinds = new Set(coreHarnessKinds); function isHarnessKind(value: unknown): value is CoreHarnessKind { return typeof value === "string" && harnessKinds.has(value as CoreHarnessKind); diff --git a/packages/agents-client/src/harness-catalog.ts b/packages/agents-client/src/harness-catalog.ts new file mode 100644 index 000000000..2594b6442 --- /dev/null +++ b/packages/agents-client/src/harness-catalog.ts @@ -0,0 +1,9 @@ +// Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT. + +export const coreHarnessKinds = ["claude_sdk", "codex", "mcode"] as const; +export type CoreHarnessKind = (typeof coreHarnessKinds)[number]; +export const coreHarnessNames: Record = { + "claude_sdk": "Claude Code", + "codex": "Codex", + "mcode": "MiniMax Code", +}; diff --git a/packages/agents-client/src/index.ts b/packages/agents-client/src/index.ts index af03182c7..c65bc9604 100644 --- a/packages/agents-client/src/index.ts +++ b/packages/agents-client/src/index.ts @@ -1,3 +1,4 @@ +export { coreHarnessKinds, coreHarnessNames } from "./harness-catalog"; export { AgentCoreError, CreationStreamRetryError, createIdempotencyKey, isSessionDeletionConflict, OpenAIAgentsClient } from "./client"; export type { OpenAIAgentsClientOptions, CoreErrorDetail, CoreErrorDetails } from "./client"; export { createSSEDecoder } from "./sse"; diff --git a/packages/agents-client/src/types.ts b/packages/agents-client/src/types.ts index ea05eba89..ad21d1ff4 100644 --- a/packages/agents-client/src/types.ts +++ b/packages/agents-client/src/types.ts @@ -1,3 +1,4 @@ +import type { CoreHarnessKind } from "./harness-catalog"; export type PageOrder = "asc" | "desc"; export interface ListPage { @@ -1191,7 +1192,7 @@ export interface RuntimeHistory { token_usage: RuntimeHistoryTokenUsagePoint[]; } -export type CoreHarnessKind = "claude_sdk" | "codex" | "mcode"; +export type { CoreHarnessKind } from "./harness-catalog"; /** A complete replacement bundle. API keys are write-only. */ export interface ModelProviderInput { diff --git a/scripts/generate-harness-catalog.py b/scripts/generate-harness-catalog.py new file mode 100644 index 000000000..335a65346 --- /dev/null +++ b/scripts/generate-harness-catalog.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Generate static Harness registrations; native execution stays in adapters.""" + +import argparse +import json +from pathlib import Path +import re +import subprocess + +ROOT = Path(__file__).resolve().parent.parent +CATALOG = Path("internal/harnessconfig/builtin/catalog.json") +MODULE = "github.com/MiniMax-AI-Dev/parsar" +HEADER = "// Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT.\n" + + +def load_catalog(path): + entries = json.loads(path.read_text()) + if not isinstance(entries, list) or not entries: + raise ValueError("Harness catalog must be a nonempty list") + kinds = set() + for entry in entries: + if not isinstance(entry, dict) or set(entry) != {"kind", "label", "configuration", "profile"}: + 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-Za-z0-9]*", entry["profile"]): + raise ValueError("Invalid profile constructor") + if not entry["label"].strip() or any(c in entry["label"] for c in "\n\r\t|"): + raise ValueError("Invalid Harness label") + kinds.add(entry["kind"]) + return sorted(entries, key=lambda entry: entry["kind"]) + + +def go(source): + return subprocess.run(["gofmt"], input=HEADER + source, text=True, + check=True, capture_output=True).stdout + + +def render(entries): + packages = sorted({entry["configuration"] for entry in entries}) + imports = "\n".join(f'\t"{MODULE}/internal/harnessconfig/{package}"' for package in packages) + configurations = "\n".join( + f'\t{json.dumps(entry["kind"])}: {entry["configuration"]}.Configuration(),' + for entry in entries) + kinds = ", ".join(json.dumps(entry["kind"]) for entry in entries) + profiles = "\n".join( + f'\t{json.dumps(entry["kind"])}: {entry["profile"]}(),' for entry in entries) + labels = "\n".join(f' {json.dumps(entry["kind"])}: {json.dumps(entry["label"])},' for entry in entries) + tick = chr(96) + rows = "\n".join( + f'| {tick}{e["kind"]}{tick} | {e["label"]} | {tick}{e["configuration"]}.Configuration{tick} | {tick}{e["profile"]}{tick} |' + for e in entries) + return { + Path("internal/harnessconfig/builtin/registry.go"): go(f'''package builtin + +import ( + "{MODULE}/internal/harnessconfig" +{imports} +) + +var registry = harnessconfig.NewRegistry(map[string]harnessconfig.Configuration{{ +{configurations} +}}) + +func Registry() harnessconfig.Registry {{ return registry }} + +// Configuration binds a registered public Harness to its authored declaration. +func Configuration(kind string) harnessconfig.Configuration {{ + configuration, ok := registry.Lookup(kind) + if !ok {{ panic("unregistered public Harness configuration") }} + return configuration +}} + +// Kinds returns the public identifiers in this build, not Runtime availability. +func Kinds() []string {{ return []string{{{kinds}}} }} + +func Contains(kind string) bool {{ + _, ok := registry.Lookup(kind) + return ok +}} +'''), + Path("services/agents-api/internal/engine/catalog_generated.go"): go(f'''package engine + +var qualified = NewCatalog(map[string]Profile{{ +{profiles} +}}) +'''), + Path("packages/agents-client/src/harness-catalog.ts"): HEADER + f''' +export const coreHarnessKinds = [{kinds}] as const; +export type CoreHarnessKind = (typeof coreHarnessKinds)[number]; +export const coreHarnessNames: Record = {{ +{labels} +}}; +''', + Path("contracts/agents-api/harness-catalog.md"): f''' +# Built-in Harness registrations + +The authored registration list is +[{tick}catalog.json{tick}](../../internal/harnessconfig/builtin/catalog.json). +Change it and run {tick}make generate-harness-catalog{tick}; generated projections are checked +by {tick}make check-harness-catalog{tick}. + +| Identifier | Display name | Model configuration declaration | Core qualification constructor | +| --- | --- | --- | --- | +{rows} + +Configuration declarations live in {tick}internal/harnessconfig/{tick}; +qualification constructors live in {tick}services/agents-api/internal/engine{tick}. +These registrations describe the build. Deployment enablement, qualified +operations and a connected Runtime's actual availability remain separate checks. +See [Harness onboarding](harness-onboarding.md) for adapter and packaging steps. +''', + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + stale = [] + for relative, content in render(load_catalog(ROOT / CATALOG)).items(): + path = ROOT / relative + if args.check: + if not path.exists() or path.read_text() != content: + stale.append(str(relative)) + else: + path.write_text(content) + if stale: + raise SystemExit("Stale Harness catalog projections; run make generate-harness-catalog:\n" + "\n".join(stale)) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-harness-catalog.test.py b/scripts/generate-harness-catalog.test.py new file mode 100644 index 000000000..cc921ca05 --- /dev/null +++ b/scripts/generate-harness-catalog.test.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Verify catalog extension and generated contract freshness without live models.""" + +import importlib.util +import json +from pathlib import Path +import tempfile +import re +import unittest + + +def module(name, file): + spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(file)) + result = importlib.util.module_from_spec(spec) + spec.loader.exec_module(result) + return result + + +catalog = module("catalog", "generate-harness-catalog.py") +schema = module("schema", "patch-agents-openapi.py") + + +class HarnessCatalogTests(unittest.TestCase): + def test_new_registration_reaches_all_projections(self): + entries = [{"kind": "example", "label": "Example", + "configuration": "example", "profile": "exampleProfile"}] + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "catalog.json" + path.write_text(json.dumps(entries)) + generated = catalog.render(catalog.load_catalog(path)) + for content in generated.values(): + self.assertIn("example", content) + for old in ("codex", "claude_sdk", "mcode"): + self.assertNotIn(old, content) + self.assertIn('"example": example.Configuration()', generated[Path("internal/harnessconfig/builtin/registry.go")]) + self.assertIn('"example": exampleProfile()', generated[Path("services/agents-api/internal/engine/catalog_generated.go")]) + + def test_checked_in_openapi_enums_match_catalog(self): + expected = [entry["kind"] for entry in catalog.load_catalog(catalog.ROOT / catalog.CATALOG)] + surfaces = { + "openapi.yaml": [("v1.AgentsCore", "harness"), ("v1.SavedAgentCoreInput", "harness"), + ("v1.SavedAgentCore", "harness")], + "core.openapi.yaml": [("api.CoreHarness", "id"), ("api.HarnessModelConfiguration", "harness"), + ("v1.AgentsCore", "harness"), ("v1.SavedAgentCore", "harness")], + } + for surface, fields in surfaces.items(): + text = (catalog.ROOT / "contracts/agents-api" / surface).read_text() + for definition, member in fields: + with self.subTest(surface=surface, definition=definition): + body = re.search(r"^ " + re.escape(definition) + r":\n(.*?)(?=^ [^ ]|\Z)", text, re.M | re.S) + self.assertIsNotNone(body, "missing schema; run make openapi") + field = re.search(r"^ " + member + r":\n(.*?)(?=^ [^ ]|^ [^ ]|\Z)", body.group(1), re.M | re.S) + self.assertIsNotNone(field) + self.assertEqual(re.findall(r"^ +- ([a-z][a-z0-9_]*)$", field.group(1), re.M), expected, + "stale Harness enum; run make openapi") + + def test_invalid_or_duplicate_registration_is_rejected(self): + entry = {"kind": "example", "label": "Example", + "configuration": "example", "profile": "exampleProfile"} + candidates = [[], [entry, entry], [{**entry, "kind": "bad/kind"}], + [{**entry, "configuration": "../example"}], + [{**entry, "profile": "bad()"}], + [{**entry, "unknown": True}]] + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "catalog.json" + for value in candidates: + path.write_text(json.dumps(value)) + with self.subTest(value=value), self.assertRaises(ValueError): + catalog.load_catalog(path) + + def test_openapi_projection_keeps_every_harness_constraint(self): + document = "definitions:\n" + fields = [("v1.AgentsCore", "harness"), ("v1.SavedAgentCoreInput", "harness"), + ("v1.SavedAgentCore", "harness"), ("api.CoreHarness", "id"), + ("api.HarnessModelConfiguration", "harness")] + for definition, field in fields: + document += f" {definition}:\n properties:\n {field}:\n type: string\n" + document += "paths:\n" + for method in ("get", "put", "delete"): + document += f" /{method}:\n parameters:\n - description: Harness\n in: path\n name: harness\n type: string\n" + result = schema.harness_enums(document) + kinds = catalog.load_catalog(catalog.ROOT / catalog.CATALOG) + for entry in kinds: + self.assertEqual(result.count(" - " + entry["kind"] + "\n"), 8) + self.assertIn("type: string", result) + with self.assertRaises(ValueError): + schema.harness_enums(document.replace("name: harness", "name: other")) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/patch-agents-openapi.py b/scripts/patch-agents-openapi.py index 7064da65c..def3b983d 100644 --- a/scripts/patch-agents-openapi.py +++ b/scripts/patch-agents-openapi.py @@ -3,6 +3,8 @@ from pathlib import Path import sys +import importlib.util +import re PROVIDER_TYPE_SCHEMA = """ provider_type: @@ -14,12 +16,51 @@ """ + +def harness_enums(document): + # Read the authoring catalog, never another independently maintained enum. + spec = importlib.util.spec_from_file_location( + "harness_catalog", Path(__file__).with_name("generate-harness-catalog.py")) + catalog = importlib.util.module_from_spec(spec) + spec.loader.exec_module(catalog) + kinds = [entry["kind"] for entry in catalog.load_catalog(catalog.ROOT / catalog.CATALOG)] + for definition, member in [ + ("v1.AgentsCore", "harness"), + ("v1.SavedAgentCoreInput", "harness"), + ("v1.SavedAgentCore", "harness"), + ("api.CoreHarness", "id"), + ("api.HarnessModelConfiguration", "harness"), + ]: + pattern = re.compile(r"(^ " + re.escape(definition) + r":\n)(.*?)(?=^ [^ ]|\Z)", re.M | re.S) + match = pattern.search(document) + if match is None: + raise ValueError("Missing Harness schema: " + definition) + body = match.group(2) + marker = " " + member + ":\n" + if body.count(marker) != 1: + raise ValueError("Missing Harness field: " + definition + "." + member) + values = " enum:\n" + "".join(" - " + kind + "\n" for kind in kinds) + replacement = match.group(1) + body.replace(marker, marker + values) + document = document[:match.start()] + replacement + document[match.end():] + + def parameter(match): + indent = match.group(1) + return (indent + "enum:\n" + "".join(indent + "- " + kind + "\n" for kind in kinds) + + match.group(0)) + + document, count = re.subn( + r"^( +)in: path\n\1name: harness\n", parameter, document, flags=re.M) + if count != 3: + raise ValueError("Expected the three Harness model-configuration path parameters") + return document + + def main() -> None: if len(sys.argv) != 2: raise SystemExit("usage: patch-agents-openapi.py OPENAPI_YAML") path = Path(sys.argv[1]) - document = path.read_text(encoding="utf-8") + document = harness_enums(path.read_text(encoding="utf-8")) series_start = " v1.RuntimeHistorySeries:\n" series_end = "\n v1.RuntimeHistoryTime:\n" if document.count(series_start) != 1 or document.count(series_end) != 1: diff --git a/services/agents-api/internal/api/harness_model_providers.go b/services/agents-api/internal/api/harness_model_providers.go index 76596c489..51c132287 100644 --- a/services/agents-api/internal/api/harness_model_providers.go +++ b/services/agents-api/internal/api/harness_model_providers.go @@ -25,7 +25,7 @@ type DeploymentModelProviderStore interface { // never contains the API key, only whether one is configured. type HarnessModelConfiguration struct { Object string `json:"object" enums:"core.model_configuration" binding:"required"` - Harness string `json:"harness" enums:"claude_sdk,codex,mcode" binding:"required"` + Harness string `json:"harness" binding:"required"` v1.ModelConfigurationView LastUsedAt *time.Time `json:"last_used_at" format:"date-time" extensions:"x-nullable" binding:"required"` LastErrorCode *string `json:"last_error_code" extensions:"x-nullable" binding:"required" enums:"authentication_error,connection_failed,rate_limit_exceeded,usage_limit_exceeded,server_overloaded,server_error,resource_not_found,request_timeout,invalid_request"` @@ -39,7 +39,7 @@ type HarnessModelConfiguration struct { type CoreHarness struct { ModelConfigurationSupport v1.ModelConfigurationSupport `json:"model_configuration_support" binding:"required"` Object string `json:"object" enums:"core.harness" binding:"required"` - ID string `json:"id" enums:"claude_sdk,codex,mcode" binding:"required"` + ID string `json:"id" binding:"required"` Enabled bool `json:"enabled" binding:"required"` Default bool `json:"default" binding:"required"` ModelConfiguration *HarnessModelConfiguration `json:"model_configuration" extensions:"x-nullable" binding:"required"` @@ -116,7 +116,7 @@ func (h *Handler) listHarnesses(w http.ResponseWriter, r *http.Request, s Deploy // @Tags Deployment Model Providers // @Produce json // @Security DeploymentAdminAuth -// @Param harness path string true "Harness" Enums(claude_sdk,codex,mcode) +// @Param harness path string true "Harness" // @Success 200 {object} api.HarnessModelConfiguration // @Failure 401,404,500 {object} CoreErrorResponse // @Router /core/v1/harnesses/{harness}/model-configuration [get] @@ -157,12 +157,12 @@ func requiredModelProviderShape() shape { } // @Summary Replace a harness's deployment default model provider -// @Description Core key only. The body is the complete x_agents_core.model_configuration bundle, including the write-only api_key; there is no partial update and bundles are never merged. The provider is validated for this harness: an HTTPS base_url without credentials, query or fragment, an upstream protocol (responses, anthropic or chat_completions), automatically adapted by Runtime to the selected harness and, for mcode, positive context_window and max_output_tokens. New openai_hosted and none Sessions resolve omitted model settings from this default and freeze the resolved configuration into their encrypted snapshot; existing Sessions never change. self_hosted Sessions never use it. The key is encrypted and never returned. Each write records an administrator audit entry without the key and resets last_used_at, last_error_code and last_error_at to null, including identical writes. +// @Description Core key only. Replaces one complete deployment model configuration, including its write-only provider key. Validates through the selected Harness declaration and freezes the resolved configuration for new Sessions; existing Sessions are unchanged. See contracts/agents-api/model-execution.md#deployment-defaults for fields, source precedence and observation rules. // @Tags Deployment Model Providers // @Accept json // @Produce json // @Security DeploymentAdminAuth -// @Param harness path string true "Harness" Enums(claude_sdk,codex,mcode) +// @Param harness path string true "Harness" // @Param body body v1.ModelConfigurationInput true "Complete model provider bundle" // @Success 200 {object} api.HarnessModelConfiguration // @Failure 400,401,404,413,500,503 {object} CoreErrorResponse @@ -202,7 +202,7 @@ func (h *Handler) setHarnessModelConfiguration(w http.ResponseWriter, r *http.Re // @Description Core key only. Idempotent; each successful request is audited. Sessions that already froze the default keep it. Afterwards new openai_hosted Sessions for this harness need a Session or Agent bundle. // @Tags Deployment Model Providers // @Security DeploymentAdminAuth -// @Param harness path string true "Harness" Enums(claude_sdk,codex,mcode) +// @Param harness path string true "Harness" // @Success 204 // @Failure 401,404,500 {object} CoreErrorResponse // @Router /core/v1/harnesses/{harness}/model-configuration [delete] diff --git a/services/agents-api/internal/api/saved_core_input.go b/services/agents-api/internal/api/saved_core_input.go index 41b285aaa..f09b62220 100644 --- a/services/agents-api/internal/api/saved_core_input.go +++ b/services/agents-api/internal/api/saved_core_input.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "errors" + "github.com/MiniMax-AI-Dev/parsar/internal/harnessconfig/builtin" ) // Validate presence before decoding: a supplied empty/null harness is invalid, @@ -15,7 +16,7 @@ func validateSavedCoreInput(raw []byte) error { return nil } coreShape := shape{kind: objectValue, nullable: true, members: []member{ - {"harness", shape{kind: enumValue, values: []string{"codex", "claude_sdk", "mcode"}}}, + {"harness", shape{kind: enumValue, values: builtin.Kinds()}}, {"model_provider", nullableModelProviderShape()}, {"harness_config", shape{kind: openObject}}, }} diff --git a/services/agents-api/internal/engine/catalog_generated.go b/services/agents-api/internal/engine/catalog_generated.go new file mode 100644 index 000000000..20dab6a6f --- /dev/null +++ b/services/agents-api/internal/engine/catalog_generated.go @@ -0,0 +1,8 @@ +// Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT. +package engine + +var qualified = NewCatalog(map[string]Profile{ + "claude_sdk": claudeProfile(), + "codex": codexProfile(), + "mcode": mcodeProfile(), +}) diff --git a/services/agents-api/internal/engine/profile.go b/services/agents-api/internal/engine/profile.go index e65f9ff75..0ea10ca40 100644 --- a/services/agents-api/internal/engine/profile.go +++ b/services/agents-api/internal/engine/profile.go @@ -67,9 +67,3 @@ func (c Catalog) Kinds() []string { slices.Sort(kinds) return kinds } - -var qualified = NewCatalog(map[string]Profile{ - "codex": codexProfile(), - "claude_sdk": claudeProfile(), - "mcode": mcodeProfile(), -}) From df0bf16e1aa190e55fa7429c84add54e02e98c4b Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Tue, 29 Sep 2026 17:57:45 +0000 Subject: [PATCH 2/2] docs: refresh deployment model API reference --- .../core/deployment-model-providers.mdx | 20 ++++++------------- apps/docs/openapi/core-api.yaml | 2 +- apps/docs/openapi/sources.json | 6 +++--- contracts/agents-api/core.openapi.yaml | 2 +- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/apps/docs/content/docs/api-reference/core/deployment-model-providers.mdx b/apps/docs/content/docs/api-reference/core/deployment-model-providers.mdx index abffbdde1..0ead2b602 100644 --- a/apps/docs/content/docs/api-reference/core/deployment-model-providers.mdx +++ b/apps/docs/content/docs/api-reference/core/deployment-model-providers.mdx @@ -30,20 +30,12 @@ _openapi: openai_hosted Sessions for this harness need a Session or Agent bundle. - content: >- - Core key only. The body is the complete - x_agents_core.model_configuration bundle, including the write-only - api_key; there is no partial update and bundles are never merged. The - provider is validated for this harness: an HTTPS base_url without - credentials, query or fragment, an upstream protocol (responses, - anthropic or chat_completions), automatically adapted by Runtime to - the selected harness and, for mcode, positive context_window and - max_output_tokens. New openai_hosted and none Sessions resolve omitted - model settings from this default and freeze the resolved configuration - into their encrypted snapshot; existing Sessions never change. - self_hosted Sessions never use it. The key is encrypted and never - returned. Each write records an administrator audit entry without the - key and resets last_used_at, last_error_code and last_error_at to - null, including identical writes. + Core key only. Replaces one complete deployment model configuration, + including its write-only provider key. Validates through the selected + Harness declaration and freezes the resolved configuration for new + Sessions; existing Sessions are unchanged. See + contracts/agents-api/model-execution.md#deployment-defaults for + fields, source precedence and observation rules. --- {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} diff --git a/apps/docs/openapi/core-api.yaml b/apps/docs/openapi/core-api.yaml index bb96577ca..94179a5b2 100644 --- a/apps/docs/openapi/core-api.yaml +++ b/apps/docs/openapi/core-api.yaml @@ -195,7 +195,7 @@ paths: tags: - Deployment Model Providers put: - description: 'Core key only. The body is the complete x_agents_core.model_configuration bundle, including the write-only api_key; there is no partial update and bundles are never merged. The provider is validated for this harness: an HTTPS base_url without credentials, query or fragment, an upstream protocol (responses, anthropic or chat_completions), automatically adapted by Runtime to the selected harness and, for mcode, positive context_window and max_output_tokens. New openai_hosted and none Sessions resolve omitted model settings from this default and freeze the resolved configuration into their encrypted snapshot; existing Sessions never change. self_hosted Sessions never use it. The key is encrypted and never returned. Each write records an administrator audit entry without the key and resets last_used_at, last_error_code and last_error_at to null, including identical writes.' + description: Core key only. Replaces one complete deployment model configuration, including its write-only provider key. Validates through the selected Harness declaration and freezes the resolved configuration for new Sessions; existing Sessions are unchanged. See contracts/agents-api/model-execution.md#deployment-defaults for fields, source precedence and observation rules. parameters: - schema: type: string diff --git a/apps/docs/openapi/sources.json b/apps/docs/openapi/sources.json index a61e16574..8a85d8a47 100644 --- a/apps/docs/openapi/sources.json +++ b/apps/docs/openapi/sources.json @@ -1,7 +1,7 @@ { "sources": { "contracts/agents-api/openapi.yaml": "57c1b19119010613e80cf926ecc897d2011d8729c9fc6807dbc890d589839ddd", - "contracts/agents-api/core.openapi.yaml": "4458f46aae4aa708c5add12459f5d8b13a04190be14f200000446414c2ee67d5", + "contracts/agents-api/core.openapi.yaml": "f1b9278c3a4b1cb55127cf9471583d00f1ab13b6ceb64f8693a01c4cf03674eb", "contracts/agents-api/runtime.openapi.yaml": "505286d5eacf94a14f9527fcf4e7beb4fba4f792681be26ba966254cbf49b4aa" }, "outputs": { @@ -22,7 +22,7 @@ "content/docs/api-reference/meta.json": "56bf1dc145ccc8adde38c5f956f1a06f18ef18b646122250d01a294c516f8f83", "content/docs/api-reference/index.mdx": "9fd6b1c54149874d2999235067c101326f9e8a2e707a70b6672c81eb3ebfe63f", "content/docs/api-reference/core/core-administration.mdx": "5fc3ada51d2190a141f13738151e4d513390cc3bd66ce6f5013994bda68569b0", - "content/docs/api-reference/core/deployment-model-providers.mdx": "bf207c1f14f1ce0a9efaa778cdb8ae77b5cc932ec25cc264f3f3dc08d4ce0591", + "content/docs/api-reference/core/deployment-model-providers.mdx": "fb15ed2075e256a3fe0222641f95c48c962563325668817ac7cb2a940d9b125d", "content/docs/api-reference/core/administrator-projects.mdx": "8fd687bb2a7119fa16e938d7c71710c2816afb9cf959eb6849ef5e6300c5419b", "content/docs/api-reference/core/agents.mdx": "b6c089fbea4b07bc4249e7c7ca5699d47f1512a4fc2d8ee01cb26910ae541a84", "content/docs/api-reference/core/environment-templates.mdx": "05907f35d7f02d66af9da8b1efaa3765badb4cdf7b94d0f6c3ab0413a2cfa1a9", @@ -41,7 +41,7 @@ "content/docs/api-reference/core/vaults.mdx": "12d0aac96cd4a3472ef4eaa287f7945f78862017956e79bfc3ed0ef8e772e78a", "content/docs/api-reference/core/credentials.mdx": "27d586b9a8fd738b9947802f32c80458195370252886bc26abedccf2125ee981", "content/docs/api-reference/core/sandbox-manager.mdx": "45e65afffb25e5b6ee2ab0853d7a84dee164e0d44ea384675fbd7ffcbd6c1a58", - "openapi/core-api.yaml": "724a8d38bdc19b52867430dbc404960576cc8cd429f68c5064b50e2fddd9131c", + "openapi/core-api.yaml": "dc8e80ada4ef8bd93635cab2c5d337b9d7661991a6baa1f69dc4258ebe434ef9", "content/docs/api-reference/core/meta.json": "d0aef1f54c4cfc30b4e1d974119ae1cdce1c87d5ecba723afbeb4599119482a7", "content/docs/api-reference/core/index.mdx": "57cf9ea947ef45e79575c25a0a3d4e5e856dde6d14698ccc2c51b86201c249e9", "content/docs/api-reference/machine/native-installation.mdx": "421a858685b79fdcf8d7eff308c5768d1b62385c3988c6089f380afe80aaed69", diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index 7381ff4d7..decee3473 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -3607,7 +3607,7 @@ paths: put: consumes: - application/json - description: 'Core key only. The body is the complete x_agents_core.model_configuration bundle, including the write-only api_key; there is no partial update and bundles are never merged. The provider is validated for this harness: an HTTPS base_url without credentials, query or fragment, an upstream protocol (responses, anthropic or chat_completions), automatically adapted by Runtime to the selected harness and, for mcode, positive context_window and max_output_tokens. New openai_hosted and none Sessions resolve omitted model settings from this default and freeze the resolved configuration into their encrypted snapshot; existing Sessions never change. self_hosted Sessions never use it. The key is encrypted and never returned. Each write records an administrator audit entry without the key and resets last_used_at, last_error_code and last_error_at to null, including identical writes.' + description: Core key only. Replaces one complete deployment model configuration, including its write-only provider key. Validates through the selected Harness declaration and freezes the resolved configuration for new Sessions; existing Sessions are unchanged. See contracts/agents-api/model-execution.md#deployment-defaults for fields, source precedence and observation rules. parameters: - description: Harness enum: