diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5afb1130c..aca18c9a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,7 @@ copy, when a contract changes. | 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) | +| Effective MCP bindings and credential authority | [Environment MCP](contracts/agents-api/environments.md#skills-plugins-and-environment-mcp) and `apps/parsar-daemon/internal/agent/mcp_binding.go` | | 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` | @@ -273,6 +274,8 @@ broader compatibility target is complete from one merged batch. - Split growing files at an existing ownership boundary instead of adding unrelated responsibilities. - Use `internal/obs/log` for logs. Keep credentials out of source and logs. + Harness profiles must not copy Runtime tool environment values; see the + [environment contract](contracts/agents-api/environments.md#explicit-local-tool-environment). - Require absolute user-supplied working directories. - Keep test artifacts under `~/.oac/` and build output under `${OAC_DEV_HOME:-$HOME/.oac}`. Runtime state uses `${OAC_RUNTIME_HOME:-$HOME/.oac}`. diff --git a/README.md b/README.md index fe83c0b9d..87460c0bd 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ the [architecture guide](docs/architecture.md). | Build an application on the API | [Quickstart](docs/getting-started/quickstart.md), then the [Agents API guide](docs/api/public-agent-api.md) | | See a complete application | [Examples](docs/examples.md) | | Run agents on my own machine | [Self-hosted execution](docs/getting-started/self-hosted.md) | +| Check verified Runtime capabilities and limits | [Capability qualification](contracts/agents-api/environment-capabilities-qualification.md) | | Understand the design | [Architecture](docs/architecture.md) | | Add a sandbox, harness or other component | [Developer guide](docs/development.md) | diff --git a/README.zh-CN.md b/README.zh-CN.md index 207534c67..57da12978 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -68,6 +68,7 @@ Core 对外提供两组 API: | 基于 API 开发应用 | [快速开始](docs/getting-started/quickstart.md),然后看 [Agents API 指南](docs/api/public-agent-api.md) | | 看一个完整的应用 | [示例](docs/examples.md) | | 在自己的机器上运行 Agent | [自托管执行](docs/getting-started/self-hosted.md) | +| 查看 Runtime 能力和验收范围 | [能力验收记录](contracts/agents-api/environment-capabilities-qualification.md) | | 了解设计 | [架构说明](docs/architecture.md) | | 接入新的沙箱、Harness 或其他组件 | [开发指南](docs/development.md) | diff --git a/apps/docs/content/docs/agents-and-tools.mdx b/apps/docs/content/docs/agents-and-tools.mdx index a8e622256..0cd017cd3 100644 --- a/apps/docs/content/docs/agents-and-tools.mdx +++ b/apps/docs/content/docs/agents-and-tools.mdx @@ -22,14 +22,14 @@ and Environment Plugin MCP have separate inventories and qualification. | Operation | Implemented behavior and real qualification | Unsupported or unverified boundary | | --- | --- | --- | -| Initial message input, `sessions.create` | String input and ordered user-message arrays share atomic admission. Codex/Claude text and inline image execution: M1/M2; ordinary MiniMax text: M1/P1. [Input contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/message-input.md), [initial parser](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/api/session_initial_input.go). | Whitespace-only text is admitted verbatim for Codex and rejected at admission for Claude SDK and MiniMax Code ([text content](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/message-input.md#text-content)). Empty parts beside text (SES-08) and local size-limit parity need upstream evidence. Images are only qualified for inline PNG/JPEG on `none` and Core-managed Docker `openai_hosted`; MiniMax, self-hosted images and remote URLs remain gaps. | +| Initial message input, `sessions.create` | String input and ordered user-message arrays share atomic admission. Codex/Claude text and inline image execution: M1/M2; ordinary MiniMax text: M1/P1. [Input contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/message-input.md), [initial parser](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/api/session_initial_input.go). | Whitespace-only text is admitted verbatim for Codex and rejected at admission for Claude SDK and MiniMax Code ([text content](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/message-input.md#text-content)). Empty parts beside text (SES-08) and local size-limit parity need upstream evidence. Inline PNG/JPEG is qualified on `none`, Core-managed Docker `openai_hosted` and `self_hosted`; MiniMax images and remote URLs remain gaps. | | Prepared and active messages, `sessions.events.create` | Ordered `input_text`/`input_image` arrays retain original content and distinct public user Items. HTTP 202 confirms persistence; native receipts establish application. Initial/prepared/active Docker PNG/JPEG: M2; active PNG on `none`: M1. [Shared admission](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/api/inputs.go). | Codex flattens native messages with blank-line separators. Claude can fold or queue native turns; it does not promise Codex's same-native-turn behavior. Public durability does not prove native consumption. Claude SDK and MiniMax Code reject messages without an image or non-whitespace text at admission (`unsupported_or_invalid_configuration`); Codex delivers them unchanged. | -| Structured output, `agent.text.format` | Save/inherit/freeze `{type:json_schema,schema:...}`. Claude SDK object-root, single Agent, medium verbosity, ordinary functions returning text: S1 (`none`) and S2 (Docker, including prepared/active input and Files/Artifacts). Native final text is retained unchanged; its stream follows the official message sequence with the whole text in one `output_text.delta` ([item serialization](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/history-events-usage.md#item-serialization-2026-09-23)). [Contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/structured-output.md), [profile](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/engine/claude.go). | An explicit non-object root type is a protocol error for every harness ([validation](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/official-semantics-alignment.md#agent-configuration-validation--september-23)). Codex/MiniMax, schemas without an object root, schema numbers changed by binary64, self-hosted, Skills/Plugins, MCP, Subagent and discovery combinations reject execution. No output repair, coercion or extra model loop. Arbitrary schema dialects are unverified. | +| Structured output, `agent.text.format` | Save/inherit/freeze `{type:json_schema,schema:...}`. Claude SDK object-root, single Agent, medium verbosity, ordinary functions returning text: S1 (`none`) and S2 (Docker, including prepared/active input and Files/Artifacts), plus [self-hosted execution and cold continuation](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/environment-capabilities-qualification.md). Native final text is retained unchanged; its stream follows the official message sequence with the whole text in one `output_text.delta` ([item serialization](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/history-events-usage.md#item-serialization-2026-09-23)). [Contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/structured-output.md), [profile](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/engine/claude.go). | An explicit non-object root type is a protocol error for every harness ([validation](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/official-semantics-alignment.md#agent-configuration-validation--september-23)). Codex/MiniMax, schemas without an object root, schema numbers changed by binary64, Skills/Plugins, MCP, Subagent and discovery combinations reject execution. No output repair, coercion or extra model loop. Arbitrary schema dialects are unverified. | | Function configuration, saved/inline Agents | Required name/description/schema; `defer_loading` defaults false. Protocol errors, repeated names and explicit non-object root types reject with the official fields ([validation](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/official-semantics-alignment.md#agent-configuration-validation--september-23)). Saved references resolve into an immutable Session snapshot. Codex/Claude real calls: F1/F2/M2/S1/S2. [Parser](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/api/function_configuration.go), [saved tools](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/api/saved_tools.go). | MiniMax public functions reject. Claude requires an explicit object root. Local nonblank/512-byte name and 64-definition Session bounds are compatibility gaps. Saving configuration alone does not qualify execution. | | Function-result admission, `events.create` | Required `turn_id`, `call_id`, `success`; optional nullable `error` and `output`. Output is string or ordered text/image content. Scoped atomic batches retain field presence, original content and retry identity in storage; public result Items and events always carry `output` and `error`, null when not submitted ([item serialization](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/history-events-usage.md#item-serialization-2026-09-23)). Same result retries are accepted; changed results and results after cancellation return 409 `conflict_error`, including after terminal state. In the caller's Session, an unknown call or a call of another Turn returns 400 `invalid_request_error` without changing the pending action; missing and foreign Sessions stay 404 ([error rows](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/official-semantics-alignment.md#session-input-conflicts-and-result-targets--september-23)). F1/F2/M2 plus [controlled SDK/raw checks](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/tests/official_function_inputs.py). [Parser](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/api/function_inputs.go), [Store](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/store/function_inputs.go). | Admission is separate from application and public Item publication. Invalid or unqualified content cannot consume a pending call. Core messages omit the call and executor IDs that official messages name; hosted defaults and publication timing remain unverified. | | Function text results and native application | Codex waits for a matching live root dynamic-tool completion; Claude waits for a matching live root native tool result. Text/error results, retry/conflict, cancellation and cold continuation: F1/F2. [Receipt contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/function-result-images.md). | Transport writes alone do not confirm application. Confirmation is not provider consumption, crash recovery or exactly-once external effects. No automatic result replay. | -| Function image results | Successful ordered inline PNG/JPEG with text, large PNG and image-only JPEG: Codex/Claude `none` F1/F2; Docker M2. Public Items retain submitted bytes. Codex receipt regression: F2. | Claude rejects failed images and remote references before persistence; native resizing may change its image bytes. MiniMax functions and self-hosted image workflows are unqualified. Other Codex image/error/reference combinations cannot be inferred from successful-inline evidence. | -| Deferred function discovery | Type-only `tool_search` plus mixed eager/deferred functions: Claude SDK 0.3.269/native 2.1.269, Kimi K3, single Agent, medium, `none`, text results; text and PNG input D1. Native provider observations establish lazy schema loading. [Contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/tool-search.md). | A repeated `tool_search` is a protocol error. Codex/MiniMax discovery, search-only/missing-search, workspace, MCP, structured-output and Subagent combinations remain gaps. Opaque native policy changes lack a reliable pre-input deferral signal. Saved tools include `tool_search`; the pinned Session response union excludes it. Exact hosted projection is unverified. | +| Function image results | Successful ordered inline PNG/JPEG with text, large PNG and image-only JPEG: Codex/Claude `none` F1/F2; Docker M2. Public Items retain submitted bytes. Codex receipt regression: F2. | Claude rejects failed images and remote references before persistence; native resizing may change its image bytes. Self-hosted Claude image results use the same native path; see the current [qualification record](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/environment-capabilities-qualification.md). MiniMax functions remain unqualified. Other Codex image/error/reference combinations cannot be inferred from successful-inline evidence. | +| Deferred function discovery | Type-only `tool_search` plus mixed eager/deferred functions: Claude SDK 0.3.269/native 2.1.269, Kimi K3, single Agent, medium, `none`, text results; text and PNG input D1. Native provider observations establish lazy schema loading for D1. [Self-hosted workspace callback, continuation and cancellation](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/environment-capabilities-qualification.md) use the same native path; they do not add model-request observer evidence. [Contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/tool-search.md). | A repeated `tool_search` is a protocol error. Codex/MiniMax discovery, search-only/missing-search, workspace with Skills/Plugins, MCP, structured-output and Subagent combinations remain gaps. Opaque native policy changes lack a reliable pre-input deferral signal. Saved tools include `tool_search`; the pinned Session response union excludes it. Exact hosted projection is unverified. | | Explicit disabled search/PTC | Saved and inline `web_search.mode=disabled` and `programmatic_tool_calling.enabled=false`; shared native controls on initial and cold execution. All three harnesses on `none`: P1, including native inventory/control evidence. [Contract](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/tool-policy.md), [parser](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/api/disabled_tools.go). | Unsupported explicit enablement rejects at Session admission; saved Agents keep every pinned search mode as resource data (TV-05), and Sessions from such Agents reject unless they replace the tools. A repeated `web_search` is a protocol error. Omission retains approved native behavior, which does not establish official default-on PTC parity. Enabled search and default/error parity remain gaps; unrelated native utilities are not implicitly removed. | | Agent service-origin MCP | Implemented Codex/Claude HTTP `none` profiles, anonymous/static bearer, scoped Vault selection and native `mcp_call` Items. [Configuration and qualification limits](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/README.md#http-mcp-execution), [profiles](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/services/agents-api/internal/engine/profile.go). | This closure batch does not requalify MCP/provider combinations. MiniMax, self-hosted/hosted service-origin MCP, OAuth, nonempty inline headers/metadata and other transports/origins reject. Claude requires a static connected inventory; original MCP-envelope fidelity and continuing server health are unverified. | | Environment Plugin MCP/native tools | Separate [Docker Plugin transport matrix](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/environment-templates.md#environment-origin-mcp-plugins) and [V1 deployment evidence](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/user-managed-runtime-v1.md). Native tools stay within the existing colocated Runtime. | Plugin MCP is not Agent service-origin MCP or a public function action. No new optional cross-product is qualified here. Native utility inventories need not be identical. | diff --git a/apps/docs/content/docs/api-reference/sessions.mdx b/apps/docs/content/docs/api-reference/sessions.mdx index 6d2ef1732..011a38b27 100644 --- a/apps/docs/content/docs/api-reference/sessions.mdx +++ b/apps/docs/content/docs/api-reference/sessions.mdx @@ -80,50 +80,50 @@ _openapi: limits keep unsupported_or_invalid_configuration. Hosted network policy rejections return invalid_request_error with a null param. Initial input accepts a string or ordered user-message array. Codex - and Claude SDK on none and qualified openai_hosted also accept inline - PNG/JPEG image content; other image combinations and remote URLs are - unsupported. None initial input atomically starts a Turn; self_hosted - initial input is reserved while returning its Environment connection - target, with execution deferred to native readiness and Session - failure on initial timeout. Initial input is required for none and for - streamed creation outside self_hosted. Omitted/null input remains - valid for non-streaming hosted and self_hosted creation. With - stream=true, returns live Session events starting with the committed - creation snapshot and closes right after the first agent.session.idle - recorded when a Turn ends or an input reservation stops being pending, - or any agent.session.failed, without sending later events. A creation - that admitted nothing closes after the snapshot; a settlement that - records no event closes after events up to the cursor read with a - settled Session projection. Required actions keep it open; disconnect - does not cancel execution. The GET events stream remains live-only. - New Sessions retain their authenticated creator; all creation retries - require the same typed subject, including across key rotation. - Saved-Agent retries and inline requests using Vault attachments or - credential references retain caller intent independently of later - resource changes; new hosted inline requests also freeze caller intent - before deployment defaults resolve; unrelated non-hosted inline - retries preserve resolved/default equivalences, and their resolved - hash leaves out any deployment default. Provider keys enter retry - hashes only as fingerprints keyed by the credential key. Unknown - historical creators reject retries; known creators without recorded - intent retain resolved-snapshot retry rules. These conflict policies - are local and not verified hosted parity. A same-key stream=true retry - of an existing creation returns 201 with no events and closes at once; - retry with stream=false or use the GET events stream to recover. - Claude SDK on none and Core-managed Docker openai_hosted supports - qualified object-root json_schema output with medium verbosity, - single-Agent execution and ordinary functions. Hosted execution reuses - native workspace tools and Files/Artifacts; Skills, Plugins, - capability directories, HTTP MCP, Subagent and tool_search - combinations remain unqualified, including inherited template - contents. Other non-text initial input remains unsupported. Basic - Codex and Claude SDK openai_hosted creation requires an explicitly - configured managed provider. The Claude workspace profile supports - non-deferred function tools with text or successful inline PNG/JPEG - results alongside native workspace tools; HTTP MCP remains - unsupported. Idle Sessions provision automatically; initial - provisioning has no caller connection action. Network defaults to - enabled; disabled and restricted policies reject before compute + and Claude SDK on none and qualified managed or self_hosted workspace + profiles also accept inline PNG/JPEG image content; other image + combinations and remote URLs are unsupported. None initial input + atomically starts a Turn; self_hosted initial input is reserved while + returning its Environment connection target, with execution deferred + to native readiness and Session failure on initial timeout. Initial + input is required for none and for streamed creation outside + self_hosted. Omitted/null input remains valid for non-streaming hosted + and self_hosted creation. With stream=true, returns live Session + events starting with the committed creation snapshot and closes right + after the first agent.session.idle recorded when a Turn ends or an + input reservation stops being pending, or any agent.session.failed, + without sending later events. A creation that admitted nothing closes + after the snapshot; a settlement that records no event closes after + events up to the cursor read with a settled Session projection. + Required actions keep it open; disconnect does not cancel execution. + The GET events stream remains live-only. New Sessions retain their + authenticated creator; all creation retries require the same typed + subject, including across key rotation. Saved-Agent retries and inline + requests using Vault attachments or credential references retain + caller intent independently of later resource changes; new hosted + inline requests also freeze caller intent before deployment defaults + resolve; unrelated non-hosted inline retries preserve resolved/default + equivalences, and their resolved hash leaves out any deployment + default. Provider keys enter retry hashes only as fingerprints keyed + by the credential key. Unknown historical creators reject retries; + known creators without recorded intent retain resolved-snapshot retry + rules. These conflict policies are local and not verified hosted + parity. A same-key stream=true retry of an existing creation returns + 201 with no events and closes at once; retry with stream=false or use + the GET events stream to recover. Claude SDK on none, Core-managed + Docker openai_hosted and self_hosted supports qualified object-root + json_schema output with medium verbosity, single-Agent execution and + ordinary functions. Hosted execution reuses native workspace tools and + Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, + Subagent and tool_search combinations remain unqualified, including + inherited template contents. Other non-text initial input remains + unsupported. Basic Codex and Claude SDK openai_hosted creation + requires an explicitly configured managed provider. The Claude + workspace profile supports non-deferred function tools with text or + successful inline PNG/JPEG results alongside native workspace tools; + HTTP MCP remains unsupported. Idle Sessions provision automatically; + initial provisioning has no caller connection action. Network defaults + to enabled; disabled and restricted policies reject before compute allocation because the current Runtime cannot enforce them. The x_agents_core.environment extension accepts common preparation fields for either hosted or self-hosted placement: environment_template_id, @@ -155,15 +155,15 @@ _openapi: selectors resolve the default version. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in - the qualified single-agent Claude environment:none function profile, - including qualified inline image messages and text results. Explicit - web_search mode disabled and programmatic_tool_calling enabled false - use frozen common Runtime controls. Enabled forms, including those - saved on an Agent, remain unqualified and reject before any write - unless the Session replaces tools. Omitted programmatic configuration - preserves native behavior, a documented difference from the official - default-on behavior. Other combinations remain unqualified; see the - operation coverage. + the qualified single-agent Claude function profile on none or a + managed/user-owned workspace, including qualified inline image + messages and text results. Explicit web_search mode disabled and + programmatic_tool_calling enabled false use frozen common Runtime + controls. Enabled forms, including those saved on an Agent, remain + unqualified and reject before any write unless the Session replaces + tools. Omitted programmatic configuration preserves native behavior, a + documented difference from the official default-on behavior. Other + combinations remain unqualified; see the operation coverage. - content: >- Returns supported none, self_hosted and basic openai_hosted Session environments. Self-hosted pending input can require a caller @@ -195,12 +195,12 @@ _openapi: An empty events array is a resource-authorized no-op; it creates no execution retry identity, Turn, Item or input receipt. For environment none, atomically accepts text messages, cancellation and function - results. Messages steer active work or start a queued Turn. The - supported self_hosted profile accepts text-only messages; qualified - Codex and Claude SDK openai_hosted profiles also accept inline - PNG/JPEG. Under the Session lock, matching retries retain their - original target; new active messages append to the current Turn, while - idle messages reserve work and wait up to the original five-minute + results. Messages steer active work or start a queued Turn. Qualified + Codex and Claude SDK workspace profiles accept text and inline + PNG/JPEG messages, independently of managed or self_hosted ownership. + Under the Session lock, matching retries retain their original target; + new active messages append to the current Turn, while idle messages + reserve work and wait up to the original five-minute connection/admission deadline. Return 202 only after durable admission, without claiming native application; active messages create no Turn or reservation. Cancellation-only prepared-environment batches @@ -230,15 +230,15 @@ _openapi: cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text - results and, on none and qualified openai_hosted, successful inline - PNG/JPEG results, preserving ordered content; error images and remote - references reject before admission. Native image resizing may change - bytes. Runtime image-result support is checked only for image-bearing - delivery. Codex and Claude SDK on none and qualified openai_hosted - accept ordered inline PNG/JPEG image messages. Self-hosted profiles - and other engines remain text-only; remote image URLs are unsupported. - Image references are retained unchanged without service-side - downloads. + results and, on none and qualified workspace profiles, successful + inline PNG/JPEG results, preserving ordered content; error images and + remote references reject before admission. Native image resizing may + change bytes. Runtime image-result support is checked only for + image-bearing delivery. Codex and Claude SDK on none and qualified + managed or self_hosted workspace profiles accept ordered inline + PNG/JPEG image messages. Other engines remain text-only; remote image + URLs are unsupported. Image references are retained unchanged without + service-side downloads. --- {/* 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/content/docs/environments-and-files.mdx b/apps/docs/content/docs/environments-and-files.mdx index 770b25532..361abedf7 100644 --- a/apps/docs/content/docs/environments-and-files.mdx +++ b/apps/docs/content/docs/environments-and-files.mdx @@ -641,17 +641,32 @@ initialized values override the selected declaration's variables. There is no Python sandbox launcher, mount policy, shell prefix or separate credential sandbox. Process groups and Windows Jobs own cancellation and descendant cleanup only. Claude composes MCP identity and observation with the same workspace profile; -MiniMax keeps its existing supported transport qualification. A public capability +MiniMax uses the same effective bindings for stdio and HTTP. A public capability advertisement must still match the installed engine's actual supported transport. +Runtime resolves public HTTP declarations and installed Plugin MCP through +`agent.ResolveMCPBindings` before adapter projection. Each transient binding +retains its connection origin, transport, nullable tool allowlist, required flag, +credential authority and installed stdio identity. Bindings are never persisted +or logged. Duplicate identities and unavailable selected credentials reject. +A service-origin request cannot silently become an Environment-origin connection; +the existing public HTTP MCP profile remains service-origin `environment:none`. +This internal consolidation does not qualify public `connection_origin=environment`. + Environment-origin literal HTTP headers remain rejected for the pinned Claude -client because its interpolation and cross-origin forwarding change their meaning. -MiniMax accepts environment stdio only. Its adapter reads the existing Session-private -native runtime-name registry for exact first-frame identities and cross-checks -completed native results. Reuse existing observation and cancellation settlement; -never fabricate a delayed start event, guess normalized identities or add a registry -of our own. Its unqualified HTTP transport remains rejected. -These mechanisms alone do not establish public MCP support or full compatibility. +and MiniMax clients because their cross-origin forwarding cannot preserve header +authority. MiniMax supports installed stdio and HTTP servers with anonymous or +explicit user-selected HTTPS bearer authentication. ACP HTTP declarations remain +Session-local native memory; tokens do not enter native configuration files or +process arguments. Required initialization and tool allowlists are not exposed +through the Plugin manifest, and public MiniMax MCP remains unqualified. +MiniMax reads the existing Session-private native runtime-name registry for exact +first-frame identities and cross-checks completed native results for both transports. +Reuse existing observation and cancellation settlement; never fabricate a delayed +start event, guess normalized identities or add a registry of our own. + +See [capability qualification](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/environment-capabilities-qualification.md) for +real model evidence and the remaining public-origin and image gaps. ## Contract inventory @@ -784,7 +799,8 @@ explicit Session `env` keys override the base. Runtime does not rewrite the sour file or inherit unrelated ambient credentials. Setup, capability resolution and Harness execution read the same prepared snapshot. Reconnect preserves that snapshot even if the operator edits the source; a new installation for a new -Session reads the current source. A missing or invalid explicitly configured file +Session reads the current source. Harness profiles may reference the Runtime-owned +environment file, but must not persist copies of its values. A missing or invalid explicitly configured file fails preparation. [Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/environments.md) diff --git a/apps/docs/content/docs/harness-onboarding.mdx b/apps/docs/content/docs/harness-onboarding.mdx index c9f83bdf5..372b0007a 100644 --- a/apps/docs/content/docs/harness-onboarding.mdx +++ b/apps/docs/content/docs/harness-onboarding.mdx @@ -298,7 +298,7 @@ Initial requests, `Executor.StartTurn` and steering consume the same ordered `proto.MessageInput`. Text-only adapters use `TextOnly()` to reject images without discarding content. Image adapters translate each part natively and acknowledge an active batch only after all its messages are applied. Register -`MessageImages` and qualify `MessageImagePlacements` separately; see the +Runtime `MessageImages` and qualify the engine profile’s `MessageImages` separately; see the [message-input contract and real acceptance](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/message-input.md). Hosted workspace execution additionally requires verified preparation, workspace diff --git a/apps/docs/content/docs/runtime-protocol.mdx b/apps/docs/content/docs/runtime-protocol.mdx index e601f2494..b784b9287 100644 --- a/apps/docs/content/docs/runtime-protocol.mdx +++ b/apps/docs/content/docs/runtime-protocol.mdx @@ -243,13 +243,13 @@ input list and uses blank-line separators between messages; this does not preser independent native user-message boundaries. The independently packaged Claude bridge uses protocol 3 for a prepared Executor and separately identified Turns; readiness rejects other protocol versions. -Image-bearing messages require a qualified profile/placement before persistence +Image-bearing messages require a qualified operation profile before persistence and image support from the selected Runtime before native delivery. These checks apply to that operation only; ordinary text retains offline queueing. Initial, prepared and active paths use the same content and preserve receipt ownership. The qualified public profile is inline PNG/JPEG on Codex/Claude `none` and -Core-managed Docker `openai_hosted`. Self-hosted/user-managed images, MiniMax -images and remote URLs remain explicit implementation gaps. Hosted images reuse +Core-managed Docker `openai_hosted` and user-managed `self_hosted`. MiniMax +images and remote URLs remain explicit implementation gaps. Workspace images reuse the existing preparation, active-input and workspace authority; they do not add a downloader, a mount or a separate execution lifecycle. Core does not fetch or transform media. See [message input coverage](https://github.com/MiniMax-AI/parsar-core/blob/f6d258735fc601c521dd990e6f9e1ed261f4ef2d/contracts/agents-api/message-input.md). diff --git a/apps/docs/content/guide-sources.json b/apps/docs/content/guide-sources.json index 855a7536f..3bb814ba3 100644 --- a/apps/docs/content/guide-sources.json +++ b/apps/docs/content/guide-sources.json @@ -10,10 +10,10 @@ "docs/web/core-connection.md": "861c75c32676fd17b84eb356b263096703af5750c1ceb71bb339e84607fffc56", "docs/getting-started/quickstart.md": "b989682ac2e58d59a794118fd371b37d1ea64c957d3512ff53739458a95d0f9a", "docs/api/README.md": "dbe3f172a997ee2fd3d2e5765d382b4fb7cf7c50f1dd17212ab9646f6959d63e", - "contracts/agents-api/execution-tools.md": "e0b61f6c0c236c362186c5f6d0ad69a16dd8a8d9afabe1329a1fe22e5f4a71e9", + "contracts/agents-api/execution-tools.md": "8cc0dbe207e8e80ac104bf482c37ea297cd64250b51d288ed4baf553756424d2", "docs/api/public-agent-api.md": "00979732412a971013e8f01b4c74820ff51aafdded6b0f105d25a78af86a6627", "docs/examples.md": "0e1bdaeff51c9c36779f817be31ea9816b7d8cb2290cf8350d3c4801f436f4f9", - "contracts/agents-api/environments.md": "d5fc16c904f751bd13e2af35c9e43c92ca39c6b86401f85cf7f9ee4093c156c9", + "contracts/agents-api/environments.md": "424dac4bfd63418afc314896dd6a3e4c5c4375dd0a8bb7579920c5dd9743da22", "docs/getting-started/nodes.md": "c1ad18c445990ea696625011cc3cfa0725779beefd379064d278656801d5578e", "docs/getting-started/self-hosted.md": "5ade597e09cbfa2e321f341693b47730b3696fe7bd4d6834eafbe6b279a55e6b", "docs/self-hosted-native.md": "b3cf736f88792e6925c50b81a4c33eba6b9ee86e195f9ed4e2187db7bed71d88", @@ -25,9 +25,9 @@ "docs/user-guide.md": "190bea5bfe23b71db3d9437065ee270e07226a89a6e98c668853e5c7f7555529", "docs/assets/development-architecture.png": "24e6d0145d4f16ad70b07b6bc643808a6455aaf6398434d199cf74def200fca6", "docs/development.md": "f5c340253036a2cabc43e22aecdf70522d90280d6511e7649278ae93712ab8b6", - "contracts/agents-api/harness-onboarding.md": "234906652d381f4920d2245a795ad7f12f2f66577018db4dd5d545697d4f111b", + "contracts/agents-api/harness-onboarding.md": "874a80dc1ffc5e97b2783bea3f6b217893f997b38bb8180a29f09c6dbf75e622", "docs/runtime-bootstrap.md": "0d49aed73b298039e04453e6f465b0e925fb2227fa35d4206820bb3acbd8df39", - "docs/runtime-protocol.md": "79f6bf1597eb0c82b41074fd21a6617225ffb7d11572c156e1d56c4e8741f322", + "docs/runtime-protocol.md": "e8aa4cf862b5f63a4138cfeb25196a6bdb5c40a2e389e828b464585415dc6c45", "docs/sandbox-provider.md": "4d47b234a457f874f3ff7e61a7f5da6fc8b75b0c6df0ce0ce9ead1c07afdfb3e", "apps/docs/scripts/guides.json": "3c3768fb94fd3d42464d4ce8c55724b9ba8360133c7cc19d513d8ec83a8e034f" }, @@ -41,10 +41,10 @@ "content/docs/bootstrap-projects-keys.mdx": "91a6f62cbe41737adfce9e8ec56e6dc3e2fb0ec8f6b677576f941cdfb378dc47", "content/docs/quickstart.mdx": "d0ab9537ab68f6dd3109353e937a29c5d58ec3894b56a52873c9a1d79dfa60a6", "content/docs/public-api.mdx": "cd59f4a661f897d1a9dc6ff08a09f7b7504c355288bbf022eff9b70500a9865c", - "content/docs/agents-and-tools.mdx": "b8a85fe92e19a71b7ad7c9357ce7ad0595d50c9190edb748ca834df615b056cb", + "content/docs/agents-and-tools.mdx": "44dfde4e3b3906b30323c2e75a89650ae4837210c7ba425be2266edf87ce8ff8", "content/docs/sessions.mdx": "bb63d799ed90038836d652f3f866d0309822425114c6b2aa36654b8a991947f6", "content/docs/examples.mdx": "587061e65ab2e841d14980539ba94e2216d4e8135c948a852b9a8bb0359c85d7", - "content/docs/environments-and-files.mdx": "352ca1f2b0ab1b0853b890be694629bb47399ba6bc88905f68feaa3bfe401949", + "content/docs/environments-and-files.mdx": "6d8b2a5d5e95e3a5eff47ce93f0239c7cc38d4678bac53c58f5c8df016c23dc5", "content/docs/hosted-providers.mdx": "85696d88b76f1233db3d8db3266bb0ab4d297d795a44ede13cf34e5169524c68", "content/docs/self-hosted-execution.mdx": "eeed4c6b3646927ccc3c7ac2d20b2c0f9c8a65e42d734310fe3959e016324e57", "content/docs/self-hosted-native.mdx": "671451580dfb4f5c134221991f68292a4f992008174d23190b1da3742046571f", @@ -56,9 +56,9 @@ "content/docs/user-guide.mdx": "7c099b2d787ce3d7876889c7cefd621840ec083f267eadf278214a83f11ba274", "public/images/source/docs/assets/development-architecture.png": "24e6d0145d4f16ad70b07b6bc643808a6455aaf6398434d199cf74def200fca6", "content/docs/development.mdx": "2e5249ddfca571d264e93fd1e0e390a4bd5b0b623bda100cd02f2982929850bb", - "content/docs/harness-onboarding.mdx": "d9c9dcbd339ba9b278b133816c1d905bdbd849f44e2ff27882090cd39002e1c8", + "content/docs/harness-onboarding.mdx": "17626e9f6256ddfffc95fd81dfa8d9c712d9ff16792ea5ef54bfe8c3ce1a2a9f", "content/docs/runtime-bootstrap.mdx": "58982955811c9ad46a9fc04d8d2aef5a762fc9d25d5833f88aa75ee3fc46523f", - "content/docs/runtime-protocol.mdx": "fed94a899e953c486d8560a21a2eb15a8c41c173d020e21b57cd207c08c272b7", + "content/docs/runtime-protocol.mdx": "d6b818edf2a0e0c4f2d3878f75805043c6dba3b09c6563f80faa4cecd05d40c7", "content/docs/sandbox-provider.mdx": "9db833fe9ff3f30a65451f80d7251348695a08e7830f9e95871739a710529818" } } diff --git a/apps/docs/openapi/public-api.yaml b/apps/docs/openapi/public-api.yaml index bc8e62cfe..9f8111cc0 100644 --- a/apps/docs/openapi/public-api.yaml +++ b/apps/docs/openapi/public-api.yaml @@ -864,7 +864,7 @@ paths: tags: - Sessions post: - description: 'The optional Core model_provider bundle resolves from the Session override, saved Agent defaults, then, for openai_hosted and none, the deployment default of the resolved harness; self_hosted never uses the deployment default and none accepts only it. openai_hosted and self_hosted Sessions that resolve no bundle return 400 model_provider_required with param x_agents_core.model_provider before any write. Core encrypts and freezes the resolved bundle; later Agent or deployment default edits and same-key retries cannot change it. Keys are never returned. Supports inline configuration or a tenant-owned saved agent_id with per-Session field replacements. Execution supports model/instructions, text verbosity, non-deferred function tools, adapter-qualified multi_agent with persisted Subagent reads, implicit reasoning, service tier auto and environment type none, subject to the configured engine. Codex additionally supports HTTP MCP with service origin (omitted or null on HTTP transport is saved as service), native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer or OAuth credential for the exact HTTPS URL, while null/omission selects a unique match or remains anonymous. Session reads, lists and event snapshots show that implicitly selected credential ID in a null or omitted credential_id, also after the credential is deleted; anonymous selections stay null and the stored caller intent is unchanged. After the input requirement and before any write, a credential_id without vault_ids, one outside the attached Vaults (one message for missing, foreign and unattached IDs) or one for another server_url returns 400 invalid_request_error, and several implicit matches return 409 conflict_error. Missing decryption configuration fails dispatch without anonymous fallback. Required initialization uses native startup before the first native Turn, including cold resume, and requires a separately advertised capability; exact hosted creation timing and error parity remain unverified. Other MCP origins and native OAuth login remain unsupported. The self_hosted profile uses a qualified native harness, a clean absolute workspace_directory and optional absolute local capability_directories prepared by Runtime, with optional non-deferred function tools and HTTP MCP using service origin, optionally authenticated by the attached Vault rules. Remote MCP and remote Bearer authentication each require separately advertised combination support; old peers cannot receive unsupported work. Omitted/null capability_directories use the empty-list default; self_hosted requires configured execution plus executor registry. Claude SDK currently requires medium verbosity and object-root function schemas. It supports anonymous or attached static-bearer service-origin HTTP MCP on none with boolean required and separately advertised MCP/bearer/required runtime support. Required servers must be connected before the first native input is released; pending or failed startup rejects execution. The shared Vault selection and immutable binding rules apply; unsupported native labels/tool names reject before persistence. An attached Vault with no matching credential may remain anonymous; missing keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null; non-string values and limit violations return invalid_request_error with a metadata or metadata. param. The inline agent uses the Agent create configuration validation with agent.-prefixed params, reported before the input requirement and saved-Agent lookup; saved configurations with conflicting tools or schema roots reject admission with the same errors, and execution limits keep unsupported_or_invalid_configuration. Hosted network policy rejections return invalid_request_error with a null param. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none and qualified openai_hosted also accept inline PNG/JPEG image content; other image combinations and remote URLs are unsupported. None initial input atomically starts a Turn; self_hosted initial input is reserved while returning its Environment connection target, with execution deferred to native readiness and Session failure on initial timeout. Initial input is required for none and for streamed creation outside self_hosted. Omitted/null input remains valid for non-streaming hosted and self_hosted creation. With stream=true, returns live Session events starting with the committed creation snapshot and closes right after the first agent.session.idle recorded when a Turn ends or an input reservation stops being pending, or any agent.session.failed, without sending later events. A creation that admitted nothing closes after the snapshot; a settlement that records no event closes after events up to the cursor read with a settled Session projection. Required actions keep it open; disconnect does not cancel execution. The GET events stream remains live-only. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key rotation. Saved-Agent retries and inline requests using Vault attachments or credential references retain caller intent independently of later resource changes; new hosted inline requests also freeze caller intent before deployment defaults resolve; unrelated non-hosted inline retries preserve resolved/default equivalences, and their resolved hash leaves out any deployment default. Provider keys enter retry hashes only as fingerprints keyed by the credential key. Unknown historical creators reject retries; known creators without recorded intent retain resolved-snapshot retry rules. These conflict policies are local and not verified hosted parity. A same-key stream=true retry of an existing creation returns 201 with no events and closes at once; retry with stream=false or use the GET events stream to recover. Claude SDK on none and Core-managed Docker openai_hosted supports qualified object-root json_schema output with medium verbosity, single-Agent execution and ordinary functions. Hosted execution reuses native workspace tools and Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, Subagent and tool_search combinations remain unqualified, including inherited template contents. Other non-text initial input remains unsupported. Basic Codex and Claude SDK openai_hosted creation requires an explicitly configured managed provider. The Claude workspace profile supports non-deferred function tools with text or successful inline PNG/JPEG results alongside native workspace tools; HTTP MCP remains unsupported. Idle Sessions provision automatically; initial provisioning has no caller connection action. Network defaults to enabled; disabled and restricted policies reject before compute allocation because the current Runtime cannot enforce them. The x_agents_core.environment extension accepts common preparation fields for either hosted or self-hosted placement: environment_template_id, files, env, packages, setup_commands, skills, plugins and capability_directories. Duplicate fields in environment and the extension reject. Confidential env, npm/Python packages and ordered setup commands use the same Environment-owned initialization lifecycle; compute allocation does not own preparation. Unknown side effects are not replayed after disconnect or restart. System dependencies must be preinstalled in the sandbox image or template, or on the host machine; packages.system is rejected. Initial inline and tenant-owned file_id files freeze encrypted bytes before provisioning, then install through the common Core lifecycle before native execution or live Files access. With a template reference, omitted/null files, env, packages and setup_commands inherit. Non-null files and command lists replace; env overlays by key; each package manager inherits on omission/null and otherwise replaces its list. Empty lists clear their selected field. Tenant-owned environment_template_id references inherit omitted/null network and allow only narrowing overrides. Inline hosted network:null retains the enabled default; updating a Template with network:null resets its saved policy to enabled. Core freezes effective configuration; template updates/deletion do not alter Session snapshots or same-intent creation retries. Inline or tenant-owned skill_reference Skills share initialization. Templates preserve default/latest/explicit selectors; Session creation freezes concrete metadata and encrypted content atomically. Skill, Plugin and capability-directory list omission/null inherit; a non-null list replaces, including empty-list clearing. Omitted/null Skill version selectors resolve the default version. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified single-agent Claude environment:none function profile, including qualified inline image messages and text results. Explicit web_search mode disabled and programmatic_tool_calling enabled false use frozen common Runtime controls. Enabled forms, including those saved on an Agent, remain unqualified and reject before any write unless the Session replaces tools. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage.' + description: 'The optional Core model_provider bundle resolves from the Session override, saved Agent defaults, then, for openai_hosted and none, the deployment default of the resolved harness; self_hosted never uses the deployment default and none accepts only it. openai_hosted and self_hosted Sessions that resolve no bundle return 400 model_provider_required with param x_agents_core.model_provider before any write. Core encrypts and freezes the resolved bundle; later Agent or deployment default edits and same-key retries cannot change it. Keys are never returned. Supports inline configuration or a tenant-owned saved agent_id with per-Session field replacements. Execution supports model/instructions, text verbosity, non-deferred function tools, adapter-qualified multi_agent with persisted Subagent reads, implicit reasoning, service tier auto and environment type none, subject to the configured engine. Codex additionally supports HTTP MCP with service origin (omitted or null on HTTP transport is saved as service), native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer or OAuth credential for the exact HTTPS URL, while null/omission selects a unique match or remains anonymous. Session reads, lists and event snapshots show that implicitly selected credential ID in a null or omitted credential_id, also after the credential is deleted; anonymous selections stay null and the stored caller intent is unchanged. After the input requirement and before any write, a credential_id without vault_ids, one outside the attached Vaults (one message for missing, foreign and unattached IDs) or one for another server_url returns 400 invalid_request_error, and several implicit matches return 409 conflict_error. Missing decryption configuration fails dispatch without anonymous fallback. Required initialization uses native startup before the first native Turn, including cold resume, and requires a separately advertised capability; exact hosted creation timing and error parity remain unverified. Other MCP origins and native OAuth login remain unsupported. The self_hosted profile uses a qualified native harness, a clean absolute workspace_directory and optional absolute local capability_directories prepared by Runtime, with optional non-deferred function tools and HTTP MCP using service origin, optionally authenticated by the attached Vault rules. Remote MCP and remote Bearer authentication each require separately advertised combination support; old peers cannot receive unsupported work. Omitted/null capability_directories use the empty-list default; self_hosted requires configured execution plus executor registry. Claude SDK currently requires medium verbosity and object-root function schemas. It supports anonymous or attached static-bearer service-origin HTTP MCP on none with boolean required and separately advertised MCP/bearer/required runtime support. Required servers must be connected before the first native input is released; pending or failed startup rejects execution. The shared Vault selection and immutable binding rules apply; unsupported native labels/tool names reject before persistence. An attached Vault with no matching credential may remain anonymous; missing keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null; non-string values and limit violations return invalid_request_error with a metadata or metadata. param. The inline agent uses the Agent create configuration validation with agent.-prefixed params, reported before the input requirement and saved-Agent lookup; saved configurations with conflicting tools or schema roots reject admission with the same errors, and execution limits keep unsupported_or_invalid_configuration. Hosted network policy rejections return invalid_request_error with a null param. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none and qualified managed or self_hosted workspace profiles also accept inline PNG/JPEG image content; other image combinations and remote URLs are unsupported. None initial input atomically starts a Turn; self_hosted initial input is reserved while returning its Environment connection target, with execution deferred to native readiness and Session failure on initial timeout. Initial input is required for none and for streamed creation outside self_hosted. Omitted/null input remains valid for non-streaming hosted and self_hosted creation. With stream=true, returns live Session events starting with the committed creation snapshot and closes right after the first agent.session.idle recorded when a Turn ends or an input reservation stops being pending, or any agent.session.failed, without sending later events. A creation that admitted nothing closes after the snapshot; a settlement that records no event closes after events up to the cursor read with a settled Session projection. Required actions keep it open; disconnect does not cancel execution. The GET events stream remains live-only. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key rotation. Saved-Agent retries and inline requests using Vault attachments or credential references retain caller intent independently of later resource changes; new hosted inline requests also freeze caller intent before deployment defaults resolve; unrelated non-hosted inline retries preserve resolved/default equivalences, and their resolved hash leaves out any deployment default. Provider keys enter retry hashes only as fingerprints keyed by the credential key. Unknown historical creators reject retries; known creators without recorded intent retain resolved-snapshot retry rules. These conflict policies are local and not verified hosted parity. A same-key stream=true retry of an existing creation returns 201 with no events and closes at once; retry with stream=false or use the GET events stream to recover. Claude SDK on none, Core-managed Docker openai_hosted and self_hosted supports qualified object-root json_schema output with medium verbosity, single-Agent execution and ordinary functions. Hosted execution reuses native workspace tools and Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, Subagent and tool_search combinations remain unqualified, including inherited template contents. Other non-text initial input remains unsupported. Basic Codex and Claude SDK openai_hosted creation requires an explicitly configured managed provider. The Claude workspace profile supports non-deferred function tools with text or successful inline PNG/JPEG results alongside native workspace tools; HTTP MCP remains unsupported. Idle Sessions provision automatically; initial provisioning has no caller connection action. Network defaults to enabled; disabled and restricted policies reject before compute allocation because the current Runtime cannot enforce them. The x_agents_core.environment extension accepts common preparation fields for either hosted or self-hosted placement: environment_template_id, files, env, packages, setup_commands, skills, plugins and capability_directories. Duplicate fields in environment and the extension reject. Confidential env, npm/Python packages and ordered setup commands use the same Environment-owned initialization lifecycle; compute allocation does not own preparation. Unknown side effects are not replayed after disconnect or restart. System dependencies must be preinstalled in the sandbox image or template, or on the host machine; packages.system is rejected. Initial inline and tenant-owned file_id files freeze encrypted bytes before provisioning, then install through the common Core lifecycle before native execution or live Files access. With a template reference, omitted/null files, env, packages and setup_commands inherit. Non-null files and command lists replace; env overlays by key; each package manager inherits on omission/null and otherwise replaces its list. Empty lists clear their selected field. Tenant-owned environment_template_id references inherit omitted/null network and allow only narrowing overrides. Inline hosted network:null retains the enabled default; updating a Template with network:null resets its saved policy to enabled. Core freezes effective configuration; template updates/deletion do not alter Session snapshots or same-intent creation retries. Inline or tenant-owned skill_reference Skills share initialization. Templates preserve default/latest/explicit selectors; Session creation freezes concrete metadata and encrypted content atomically. Skill, Plugin and capability-directory list omission/null inherit; a non-null list replaces, including empty-list clearing. Omitted/null Skill version selectors resolve the default version. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified single-agent Claude function profile on none or a managed/user-owned workspace, including qualified inline image messages and text results. Explicit web_search mode disabled and programmatic_tool_calling enabled false use frozen common Runtime controls. Enabled forms, including those saved on an Agent, remain unqualified and reject before any write unless the Session replaces tools. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage.' parameters: - schema: type: string @@ -1476,7 +1476,7 @@ paths: tags: - Events post: - description: An empty events array is a resource-authorized no-op; it creates no execution retry identity, Turn, Item or input receipt. For environment none, atomically accepts text messages, cancellation and function results. Messages steer active work or start a queued Turn. The supported self_hosted profile accepts text-only messages; qualified Codex and Claude SDK openai_hosted profiles also accept inline PNG/JPEG. Under the Session lock, matching retries retain their original target; new active messages append to the current Turn, while idle messages reserve work and wait up to the original five-minute connection/admission deadline. Return 202 only after durable admission, without claiming native application; active messages create no Turn or reservation. Cancellation-only prepared-environment batches use existing durable cancellation admission and return 202 without waiting for native exit; a new cancellation conflicts while a pre-Turn reservation is pending. Homogeneous tool_result-only prepared-environment batches reuse existing scoped result admission and application receipts without creating a Turn or bypassing a pending reservation. Mixed prepared-environment batches remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled errors. New input on a Session whose hosted Environment failed to provision returns the observed 409 conflict_error "the hosted environment failed to provision"; input already waiting when it fails and expired Environments keep the local 409 environment_unavailable. Input the Session cannot accept in its current state, such as a result after cancellation or a batch while earlier input is pending, and a result that differs from the call's saved result return 409 with type and code conflict_error; reusing an Idempotency-Key with a different batch returns the local 409 idempotency_conflict. Inside an owned Session, a result for an unknown call or for a call of another Turn returns 400 invalid_request_error and changes nothing; missing and foreign Sessions return 404. Losing execution ownership returns 503. The response write deadline accommodates the admission window for either prepared Environment, independently of new-hosted-admission and executor URL settings. Disconnecting the waiting HTTP request does not cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text results and, on none and qualified openai_hosted, successful inline PNG/JPEG results, preserving ordered content; error images and remote references reject before admission. Native image resizing may change bytes. Runtime image-result support is checked only for image-bearing delivery. Codex and Claude SDK on none and qualified openai_hosted accept ordered inline PNG/JPEG image messages. Self-hosted profiles and other engines remain text-only; remote image URLs are unsupported. Image references are retained unchanged without service-side downloads. + description: An empty events array is a resource-authorized no-op; it creates no execution retry identity, Turn, Item or input receipt. For environment none, atomically accepts text messages, cancellation and function results. Messages steer active work or start a queued Turn. Qualified Codex and Claude SDK workspace profiles accept text and inline PNG/JPEG messages, independently of managed or self_hosted ownership. Under the Session lock, matching retries retain their original target; new active messages append to the current Turn, while idle messages reserve work and wait up to the original five-minute connection/admission deadline. Return 202 only after durable admission, without claiming native application; active messages create no Turn or reservation. Cancellation-only prepared-environment batches use existing durable cancellation admission and return 202 without waiting for native exit; a new cancellation conflicts while a pre-Turn reservation is pending. Homogeneous tool_result-only prepared-environment batches reuse existing scoped result admission and application receipts without creating a Turn or bypassing a pending reservation. Mixed prepared-environment batches remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled errors. New input on a Session whose hosted Environment failed to provision returns the observed 409 conflict_error "the hosted environment failed to provision"; input already waiting when it fails and expired Environments keep the local 409 environment_unavailable. Input the Session cannot accept in its current state, such as a result after cancellation or a batch while earlier input is pending, and a result that differs from the call's saved result return 409 with type and code conflict_error; reusing an Idempotency-Key with a different batch returns the local 409 idempotency_conflict. Inside an owned Session, a result for an unknown call or for a call of another Turn returns 400 invalid_request_error and changes nothing; missing and foreign Sessions return 404. Losing execution ownership returns 503. The response write deadline accommodates the admission window for either prepared Environment, independently of new-hosted-admission and executor URL settings. Disconnecting the waiting HTTP request does not cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text results and, on none and qualified workspace profiles, successful inline PNG/JPEG results, preserving ordered content; error images and remote references reject before admission. Native image resizing may change bytes. Runtime image-result support is checked only for image-bearing delivery. Codex and Claude SDK on none and qualified managed or self_hosted workspace profiles accept ordered inline PNG/JPEG image messages. Other engines remain text-only; remote image URLs are unsupported. Image references are retained unchanged without service-side downloads. parameters: - schema: type: string diff --git a/apps/docs/openapi/sources.json b/apps/docs/openapi/sources.json index d51f7dfb0..e5ba3e4f7 100644 --- a/apps/docs/openapi/sources.json +++ b/apps/docs/openapi/sources.json @@ -1,6 +1,6 @@ { "sources": { - "contracts/agents-api/openapi.yaml": "270b39eb456c78fd2fff7ca43dee5ae879cd593dec59fc5516a550afcd2df397", + "contracts/agents-api/openapi.yaml": "42ae2585b0abe60860de99a6958b639a495a0d9b7c3d0142b199a427fa77b5a5", "contracts/agents-api/core.openapi.yaml": "f1b9278c3a4b1cb55127cf9471583d00f1ab13b6ceb64f8693a01c4cf03674eb", "contracts/agents-api/runtime.openapi.yaml": "505286d5eacf94a14f9527fcf4e7beb4fba4f792681be26ba966254cbf49b4aa" }, @@ -8,7 +8,7 @@ "content/docs/api-reference/agents.mdx": "1f7697959e9c52c9d24fbe70e116e807f49912b9a27ac638b86782e2f326af44", "content/docs/api-reference/environments.mdx": "6a9e08a67479a745983c45b9711137617c13fdf7599a64533997b00a26ac7282", "content/docs/api-reference/environment-templates.mdx": "68df9276e858e525e15bb88af624fc8adee8bcdcc03a788faaff56b560fd038c", - "content/docs/api-reference/sessions.mdx": "91ba66daf03c61cd5abd0d2a2494dff331f6685537cb7783518920be21f3582c", + "content/docs/api-reference/sessions.mdx": "68a839427c3ff380644fd79e7876fd8fac7a6792179eb3cb9ed376f5953e6e46", "content/docs/api-reference/artifacts.mdx": "8df8d008126b49b1df88f28a676c4b237ccffc45a470b35ec30eab2daf1c0f7f", "content/docs/api-reference/events.mdx": "fa509794f3c208c0cdcdaa68615c26b7e400ea6c892922a4e0a6a833e42fe5dd", "content/docs/api-reference/items.mdx": "fa4d248ac5623beb3f00a4ebb5a6c4ff477865ebee351e0c543a970a7ec3654d", @@ -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": "dd6f34a59e4dc16aef3e0a48cce44c65b62eb053e8dbbc355fc49d74f9c3c741", + "openapi/public-api.yaml": "3cdee54523822cd19766771dae4c13a64d6e318ca8e9d5fabe8339e975c8ac25", "content/docs/api-reference/meta.json": "56bf1dc145ccc8adde38c5f956f1a06f18ef18b646122250d01a294c516f8f83", "content/docs/api-reference/index.mdx": "9fd6b1c54149874d2999235067c101326f9e8a2e707a70b6672c81eb3ebfe63f", "content/docs/api-reference/core/core-administration.mdx": "5fc3ada51d2190a141f13738151e4d513390cc3bd66ce6f5013994bda68569b0", diff --git a/apps/parsar-daemon/internal/agent/claudesdk/mcp_environment.go b/apps/parsar-daemon/internal/agent/claudesdk/mcp_environment.go index 5b8004e28..082941cb4 100644 --- a/apps/parsar-daemon/internal/agent/claudesdk/mcp_environment.go +++ b/apps/parsar-daemon/internal/agent/claudesdk/mcp_environment.go @@ -3,6 +3,7 @@ package claudesdk import ( "fmt" + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" ) @@ -15,45 +16,34 @@ type environmentMCPServer struct { Args []string `json:"args,omitempty"` } -func prepareEnvironmentMCP(environment *proto.LocalEnvironment) ([]environmentMCPServer, []string, error) { - if environment == nil || len(environment.MCP) == 0 { - return nil, nil, nil - } - if environment.NetworkAccess != "enabled" { - return nil, nil, fmt.Errorf("claudesdk: environment MCP requires enabled network") +func prepareRuntimeMCP(req proto.PromptRequestPayload) ([]environmentMCPServer, []string, error) { + bindings, err := agent.ResolveMCPBindings(req) + if err != nil { + return nil, nil, err } var servers []environmentMCPServer var env []string - labels := map[string]bool{} - for _, item := range environment.MCP { - declaration := item.Server - if !mcpLabel.MatchString(declaration.Name) || declaration.Name == "functions" || labels[declaration.Name] { - return nil, nil, fmt.Errorf("claudesdk: unsupported environment MCP identity") + for _, binding := range bindings { + if !mcpLabel.MatchString(binding.ServerLabel) || binding.ServerLabel == "functions" { + return nil, nil, fmt.Errorf("claudesdk: unsupported MCP identity") + } + if binding.Stdio != nil { + command, args := localworkspace.MCPStdioCommand(*binding.Stdio) + servers = append(servers, environmentMCPServer{mcpHTTPServer: mcpHTTPServer{ServerLabel: binding.ServerLabel}, Command: command, Args: args}) + continue + } + // Native header interpolation and redirect behavior cannot preserve literal + // custom-header authority. Reject this unqualified combination explicitly. + if len(binding.HTTPHeaders) != 0 { + return nil, nil, fmt.Errorf("claudesdk: literal MCP HTTP headers are not supported") } - labels[declaration.Name] = true - switch declaration.Type { - case "stdio": - command, args := localworkspace.MCPStdioCommand(item) - servers = append(servers, environmentMCPServer{mcpHTTPServer: mcpHTTPServer{ServerLabel: declaration.Name}, Command: command, Args: args}) - case "http": - // The pinned native client expands literal headers again and forwards - // custom headers across origins. Do not reinterpret their public meaning. - if len(declaration.HTTPHeaders) != 0 { - return nil, nil, fmt.Errorf("claudesdk: environment MCP literal HTTP headers are not supported") - } - if declaration.BearerTokenEnvVar != "" && item.BearerToken == nil { - return nil, nil, fmt.Errorf("claudesdk: environment MCP credential unavailable") - } - http := []proto.MCPHTTPServer{{ServerLabel: declaration.Name, ServerURL: declaration.URL, BearerToken: item.BearerToken}} - if err := validateMCPServers(http); err != nil { - return nil, nil, err - } - projected, credentials := prepareMCPHTTP(&http) - servers = append(servers, environmentMCPServer{mcpHTTPServer: (*projected)[0]}) - env = append(env, credentials...) - default: - return nil, nil, fmt.Errorf("claudesdk: unsupported environment MCP transport") + declarations := []proto.MCPHTTPServer{{ServerLabel: binding.ServerLabel, ServerURL: binding.ServerURL, AllowedTools: binding.AllowedTools, Required: binding.Required, BearerToken: binding.BearerToken}} + if err := validateMCPServers(declarations); err != nil { + return nil, nil, err } + projected, credentials := prepareMCPHTTP(&declarations) + servers = append(servers, environmentMCPServer{mcpHTTPServer: (*projected)[0]}) + env = append(env, credentials...) } return servers, env, nil } diff --git a/apps/parsar-daemon/internal/agent/claudesdk/mcp_environment_test.go b/apps/parsar-daemon/internal/agent/claudesdk/mcp_environment_test.go index 6cd293b29..0d6c23ce4 100644 --- a/apps/parsar-daemon/internal/agent/claudesdk/mcp_environment_test.go +++ b/apps/parsar-daemon/internal/agent/claudesdk/mcp_environment_test.go @@ -66,7 +66,7 @@ func TestEnvironmentMCPRejectsUnqualifiedCombinations(t *testing.T) { } { environment := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}}}} mutate(environment) - if _, _, err := prepareEnvironmentMCP(environment); err == nil { + if _, _, err := prepareRuntimeMCP(proto.PromptRequestPayload{LocalEnvironment: environment}); err == nil { t.Fatal("unsupported declaration accepted") } } diff --git a/apps/parsar-daemon/internal/agent/claudesdk/options.go b/apps/parsar-daemon/internal/agent/claudesdk/options.go index 5b8a1f71e..51dd4b9d6 100644 --- a/apps/parsar-daemon/internal/agent/claudesdk/options.go +++ b/apps/parsar-daemon/internal/agent/claudesdk/options.go @@ -71,7 +71,7 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR return startRequest{}, nil, err } if req.ToolSearch { - if config.Workspace != nil || req.LocalEnvironment != nil || req.MCPHTTPServers != nil || !req.DisableSubagents || (req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil) { + if (req.LocalEnvironment != nil && (len(req.LocalEnvironment.Skills) != 0 || len(req.LocalEnvironment.MCP) != 0)) || req.MCPHTTPServers != nil || !req.DisableSubagents || (req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil) { return fail("tool discovery requires the single-agent text/function profile") } start.ToolSearch = true @@ -187,8 +187,17 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR } env := withProvider(append(append([]string{}, os.Environ()...), config.Env...), provider) env = append(env, "CLAUDE_CONFIG_DIR="+config.StateDir, "TMPDIR="+filepath.Join(config.StateDir, "tmp"), "DISABLE_TELEMETRY=1", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1") - var mcpEnv []string - start.MCPHTTPServers, mcpEnv = prepareMCPHTTP(req.MCPHTTPServers) + projectedMCP, mcpEnv, err := prepareRuntimeMCP(req) + if err != nil { + return startRequest{}, nil, err + } + if req.MCPHTTPServers != nil { + servers := make([]mcpHTTPServer, 0, len(projectedMCP)) + for _, server := range projectedMCP { + servers = append(servers, server.mcpHTTPServer) + } + start.MCPHTTPServers = &servers + } env = append(env, mcpEnv...) return start, env, nil } diff --git a/apps/parsar-daemon/internal/agent/claudesdk/readiness.go b/apps/parsar-daemon/internal/agent/claudesdk/readiness.go index a04e2c0dc..1179f551c 100644 --- a/apps/parsar-daemon/internal/agent/claudesdk/readiness.go +++ b/apps/parsar-daemon/internal/agent/claudesdk/readiness.go @@ -37,6 +37,10 @@ func (info RuntimeInfo) SupportsToolSearch() bool { return slices.Contains(info.Features, "tool_search") } +func (info RuntimeInfo) SupportsWorkspaceToolSearch() bool { + return info.SupportsWorkspaceFunctions() && info.SupportsToolSearch() && slices.Contains(info.Features, "workspace_tool_search") +} + func (info RuntimeInfo) SupportsStructuredOutput() bool { return slices.Contains(info.Features, "structured_output") } diff --git a/apps/parsar-daemon/internal/agent/claudesdk/readiness_test.go b/apps/parsar-daemon/internal/agent/claudesdk/readiness_test.go index 2e4825cc0..27c9f3ceb 100644 --- a/apps/parsar-daemon/internal/agent/claudesdk/readiness_test.go +++ b/apps/parsar-daemon/internal/agent/claudesdk/readiness_test.go @@ -149,3 +149,17 @@ func runReadinessHelper() { os.Exit(3) } } + +func TestWorkspaceToolSearchRequiresNativeWorkspaceFeature(t *testing.T) { + features := []string{"tool_search", "workspace_tool_search", "workspace_functions", "workspace_tools", "workspace_prepare", "workspace_command_observations", "local_runtime_v2"} + if !(RuntimeInfo{Features: features}).SupportsWorkspaceToolSearch() { + t.Fatal("complete workspace discovery contract rejected") + } + for omitted := range features { + candidate := append([]string{}, features[:omitted]...) + candidate = append(candidate, features[omitted+1:]...) + if (RuntimeInfo{Features: candidate}).SupportsWorkspaceToolSearch() { + t.Fatal("incomplete native discovery contract advertised") + } + } +} diff --git a/apps/parsar-daemon/internal/agent/claudesdk/workspace.go b/apps/parsar-daemon/internal/agent/claudesdk/workspace.go index 07ebad94a..336893cde 100644 --- a/apps/parsar-daemon/internal/agent/claudesdk/workspace.go +++ b/apps/parsar-daemon/internal/agent/claudesdk/workspace.go @@ -64,7 +64,7 @@ func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspace profile.Skills = req.LocalEnvironment.Skills profile.CapabilityRoot = req.LocalEnvironment.CapabilityRoot } - servers, credentials, err := prepareEnvironmentMCP(req.LocalEnvironment) + servers, credentials, err := prepareRuntimeMCP(req) if err != nil { return nil, nil, err } diff --git a/apps/parsar-daemon/internal/agent/codex/mcp_environment.go b/apps/parsar-daemon/internal/agent/codex/mcp_environment.go deleted file mode 100644 index 43cd55d23..000000000 --- a/apps/parsar-daemon/internal/agent/codex/mcp_environment.go +++ /dev/null @@ -1,62 +0,0 @@ -package codex - -import ( - "crypto/rand" - "errors" - "strings" - - "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" - "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" -) - -// mergeEnvironmentMCP translates only qualified Runtime declarations. Credential -// references enter the config; values enter the private native env after probes. -func mergeEnvironmentMCP(servers map[string]mcpServerConfig, local *proto.LocalEnvironment) (map[string]mcpServerConfig, []string, error) { - if local == nil || len(local.MCP) == 0 { - return servers, nil, nil - } - if local.NetworkAccess != "enabled" { - return nil, nil, errors.New("codex: environment MCP requires enabled network") - } - if servers == nil { - servers = map[string]mcpServerConfig{} - } - var env []string - for _, item := range local.MCP { - declaration := item.Server - name := declaration.Name - if name == "" || strings.TrimSpace(name) != name || name == "codex_apps" { - return nil, nil, errors.New("codex: unsupported environment MCP identity") - } - if _, exists := servers[name]; exists { - return nil, nil, errors.New("codex: ambiguous environment MCP identity") - } - server := mcpServerConfig{Name: name, ApproveTools: true} - switch declaration.Type { - case "stdio": - server.Command, server.Args = localworkspace.MCPStdioCommand(item) - case "http": - server.URL = declaration.URL - if item.BearerToken != nil { - if !strings.HasPrefix(server.URL, "https://") || !agent.ValidMCPHTTPBearerToken(*item.BearerToken) { - return nil, nil, errors.New("codex: unsupported environment MCP bearer") - } - server.BearerTokenEnvVar = "OAC_RUNTIME_MCP_BEARER_" + rand.Text() - env = append(env, server.BearerTokenEnvVar+"="+*item.BearerToken) - } - server.EnvHTTPHeaders = map[string]string{} - for key, value := range declaration.HTTPHeaders { - // The public value stays literal. This native-only env reference - // keeps its bytes out of generated configuration and argv. - reference := "OAC_RUNTIME_MCP_HEADER_" + rand.Text() - server.EnvHTTPHeaders[key] = reference - env = append(env, reference+"="+value) - } - default: - return nil, nil, errors.New("codex: unsupported environment MCP transport") - } - servers[name] = server - } - return servers, env, nil -} diff --git a/apps/parsar-daemon/internal/agent/codex/mcp_environment_test.go b/apps/parsar-daemon/internal/agent/codex/mcp_environment_test.go index d583078ca..0ab768579 100644 --- a/apps/parsar-daemon/internal/agent/codex/mcp_environment_test.go +++ b/apps/parsar-daemon/internal/agent/codex/mcp_environment_test.go @@ -17,7 +17,7 @@ func TestEnvironmentMCPProjectsIsolatedStdioAndPrivateHTTPReferences(t *testing. {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/0", Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: "must-not-be-native-command", Args: []string{"private-argument"}}}, {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/1", BearerToken: &token, Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.com/mcp", HTTPHeaders: map[string]string{"X-Key": "literal-${DO_NOT_EXPAND}"}}}, }} - servers, env, err := mergeEnvironmentMCP(nil, local) + servers, env, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local}) if err != nil || len(servers) != 2 || len(env) != 2 { t.Fatal("environment declarations were not projected", err) } @@ -52,17 +52,17 @@ func TestEnvironmentMCPProjectsIsolatedStdioAndPrivateHTTPReferences(t *testing. func TestEnvironmentMCPRejectsUnqualifiedNetworkAndCredentialChanges(t *testing.T) { for _, access := range []string{"", "restricted", "disabled"} { local := &proto.LocalEnvironment{NetworkAccess: access, MCP: []proto.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "local", Type: "stdio"}}}} - if _, _, err := mergeEnvironmentMCP(nil, local); err == nil { + if _, _, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local}); err == nil { t.Errorf("unqualified MCP network accepted: %s", access) } } token := "user-token" local := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{{BearerToken: &token, Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "http://example.com/mcp"}}}} - if _, _, err := mergeEnvironmentMCP(nil, local); err == nil { + if _, _, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local}); err == nil { t.Fatal("plaintext bearer accepted") } local.MCP[0].Server.URL = "https://example.com/mcp" - if _, _, err := mergeEnvironmentMCP(map[string]mcpServerConfig{"remote": {}}, local); err == nil { + if _, _, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local, MCPHTTPServers: &[]proto.MCPHTTPServer{{ServerLabel: "remote", ServerURL: "https://example.com/mcp"}}}); err == nil { t.Fatal("service and environment identity collision accepted") } } diff --git a/apps/parsar-daemon/internal/agent/codex/mcp_http.go b/apps/parsar-daemon/internal/agent/codex/mcp_http.go index 86daead44..7a17a3634 100644 --- a/apps/parsar-daemon/internal/agent/codex/mcp_http.go +++ b/apps/parsar-daemon/internal/agent/codex/mcp_http.go @@ -1,55 +1,53 @@ package codex import ( + "crypto/rand" "errors" - "net/url" "os" "path/filepath" "slices" "strings" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" ) -// A non-nil public declaration owns the complete MCP profile, including an empty -// declaration. Product requests without this field retain their existing options. -func publicMCPHTTPServers(req proto.PromptRequestPayload) (map[string]mcpServerConfig, error) { - if req.MCPHTTPServers == nil { - return nil, nil +// One projection consumes both public and installed Runtime bindings. A non-nil +// empty public declaration still owns the complete native MCP configuration. +func runtimeMCPServers(req proto.PromptRequestPayload) (map[string]mcpServerConfig, []string, error) { + bindings, err := agent.ResolveMCPBindings(req) + if err != nil { + return nil, nil, err } - if !req.DisableExecutionEnvironment { - return nil, errors.New("codex: public HTTP MCP requires environment:none") + if bindings == nil { + return nil, nil, nil } - servers := make(map[string]mcpServerConfig, len(*req.MCPHTTPServers)) - for _, declaration := range *req.MCPHTTPServers { - name := declaration.ServerLabel - if name == "" || strings.TrimSpace(name) != name || name == "codex_apps" { - return nil, errors.New("codex: unsupported public MCP server label") + servers := make(map[string]mcpServerConfig, len(bindings)) + var env []string + for _, binding := range bindings { + if binding.ServerLabel == "codex_apps" { + return nil, nil, errors.New("codex: reserved MCP server label") } - if _, exists := servers[name]; exists { - return nil, errors.New("codex: duplicate public MCP server label") + server := mcpServerConfig{Name: binding.ServerLabel, URL: binding.ServerURL, Required: binding.Required, EnabledTools: binding.AllowedTools, ApproveTools: binding.ConnectionOrigin == "environment"} + if binding.Stdio != nil { + server.Command, server.Args = localworkspace.MCPStdioCommand(*binding.Stdio) } - endpoint, err := url.Parse(declaration.ServerURL) - if err != nil || (endpoint.Scheme != "http" && endpoint.Scheme != "https") || endpoint.Hostname() == "" || endpoint.User != nil || endpoint.RawQuery != "" || endpoint.ForceQuery || endpoint.Fragment != "" || endpoint.Opaque != "" { - return nil, errors.New("codex: unsupported public MCP server URL") + if binding.BearerToken != nil { + server.BearerTokenEnvVar = "OAC_RUNTIME_MCP_BEARER_" + rand.Text() + env = append(env, server.BearerTokenEnvVar+"="+*binding.BearerToken) } - if declaration.BearerToken != nil && (endpoint.Scheme != "https" || !agent.ValidMCPHTTPBearerToken(*declaration.BearerToken)) { - return nil, errors.New("codex: unsupported HTTPS MCP bearer credential") - } - server := mcpServerConfig{Name: name, URL: declaration.ServerURL, Required: declaration.Required} - if declaration.AllowedTools != nil { - tools := slices.Clone(*declaration.AllowedTools) - for _, tool := range tools { - if tool == "" || strings.TrimSpace(tool) != tool { - return nil, errors.New("codex: invalid public MCP tool allowlist") - } + if len(binding.HTTPHeaders) > 0 { + server.EnvHTTPHeaders = map[string]string{} + for name, value := range binding.HTTPHeaders { + reference := "OAC_RUNTIME_MCP_HEADER_" + rand.Text() + server.EnvHTTPHeaders[name] = reference + env = append(env, reference+"="+value) } - server.EnabledTools = &tools } - servers[name] = server + servers[server.Name] = server } - return servers, nil + return servers, env, nil } func configureMCP(plan *SessionPlan, servers map[string]mcpServerConfig) error { diff --git a/apps/parsar-daemon/internal/agent/codex/mcp_http_bearer.go b/apps/parsar-daemon/internal/agent/codex/mcp_http_bearer.go deleted file mode 100644 index 11c382519..000000000 --- a/apps/parsar-daemon/internal/agent/codex/mcp_http_bearer.go +++ /dev/null @@ -1,26 +0,0 @@ -package codex - -import ( - "crypto/rand" - - "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" -) - -// Only the daemon-generated reference enters native configuration. The returned -// secrets are added to the app-server environment after other launch probes. -func prepareMCPHTTPBearer(servers map[string]mcpServerConfig, declarations *[]proto.MCPHTTPServer) []string { - if declarations == nil { - return nil - } - var env []string - for _, declaration := range *declarations { - if declaration.BearerToken == nil { - continue - } - server := servers[declaration.ServerLabel] - server.BearerTokenEnvVar = "OAC_RUNTIME_MCP_BEARER_" + rand.Text() - servers[declaration.ServerLabel] = server - env = append(env, server.BearerTokenEnvVar+"="+*declaration.BearerToken) - } - return env -} diff --git a/apps/parsar-daemon/internal/agent/codex/mcp_http_bearer_test.go b/apps/parsar-daemon/internal/agent/codex/mcp_http_bearer_test.go index 46e6a5cb5..bd1fd711a 100644 --- a/apps/parsar-daemon/internal/agent/codex/mcp_http_bearer_test.go +++ b/apps/parsar-daemon/internal/agent/codex/mcp_http_bearer_test.go @@ -58,7 +58,7 @@ func TestMCPHTTPBearerRejectsInvalidTokensWithoutPersistence(t *testing.T) { for _, token := range []string{"", "=", " has-space", "has-space ", "has space", "line\r\ninjection", "nul\x00byte", "opaque中文", "middle=padding", "punctuation:invalid"} { servers := []proto.MCPHTTPServer{{ServerLabel: "tools", ServerURL: "https://tools.example/mcp", BearerToken: &token}} req := proto.PromptRequestPayload{AgentStateKey: "invalid-bearer", DisableExecutionEnvironment: true, MCPHTTPServers: &servers} - if _, _, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()); err == nil || err.Error() != "codex: unsupported HTTPS MCP bearer credential" { + if _, _, err := prepareSessionPlan(t.Context(), req, defaultSessionConfig()); err == nil || err.Error() != "invalid HTTPS MCP bearer credential" { t.Fatal("invalid bearer value accepted or unsafe error returned") } } diff --git a/apps/parsar-daemon/internal/agent/codex/mcp_http_preflight_test.go b/apps/parsar-daemon/internal/agent/codex/mcp_http_preflight_test.go index 785cef5b2..e92b4d7ed 100644 --- a/apps/parsar-daemon/internal/agent/codex/mcp_http_preflight_test.go +++ b/apps/parsar-daemon/internal/agent/codex/mcp_http_preflight_test.go @@ -141,7 +141,7 @@ func TestPublicMCPHTTPPreparationChecksBeforeNewAndResumedThread(t *testing.T) { if err := os.WriteFile(filepath.Join(root, "unknown-status"), []byte("unknown"), 0o600); err != nil { t.Fatal(err) } - declarations, err := publicMCPHTTPServers(req) + declarations, _, err := runtimeMCPServers(req) if err != nil { t.Fatal(err) } diff --git a/apps/parsar-daemon/internal/agent/codex/mcp_http_test.go b/apps/parsar-daemon/internal/agent/codex/mcp_http_test.go index 10ad40014..0a60d7a54 100644 --- a/apps/parsar-daemon/internal/agent/codex/mcp_http_test.go +++ b/apps/parsar-daemon/internal/agent/codex/mcp_http_test.go @@ -85,7 +85,7 @@ func TestPublicMCPHTTPRejectsInvalidProfileAndStoredCredentials(t *testing.T) { for _, req := range []proto.PromptRequestPayload{ {MCPHTTPServers: &valid}, } { - if _, err := publicMCPHTTPServers(req); err == nil { + if _, _, err := runtimeMCPServers(req); err == nil { t.Fatal("non-service profile accepted") } } @@ -96,7 +96,7 @@ func TestPublicMCPHTTPRejectsInvalidProfileAndStoredCredentials(t *testing.T) { {ServerLabel: "docs", ServerURL: "file:///tmp/mcp"}, } { servers := []proto.MCPHTTPServer{server} - if _, err := publicMCPHTTPServers(proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &servers}); err == nil || strings.Contains(err.Error(), "synthetic-secret") { + if _, _, err := runtimeMCPServers(proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &servers}); err == nil || strings.Contains(err.Error(), "synthetic-secret") { t.Fatal("unsupported configuration was accepted or exposed", err) } } @@ -151,11 +151,11 @@ func TestPublicMCPBearerRequiresHTTPS(t *testing.T) { token := "synthetic-private-token" servers := []proto.MCPHTTPServer{{ServerLabel: "tools", ServerURL: "http://tools.example/mcp", BearerToken: &token}} req.MCPHTTPServers = &servers - if _, err := publicMCPHTTPServers(req); err == nil || strings.Contains(err.Error(), token) { + if _, _, err := runtimeMCPServers(req); err == nil || strings.Contains(err.Error(), token) { t.Fatal("plaintext bearer accepted or exposed") } servers[0].ServerURL = "https://tools.example/mcp" - if _, err := publicMCPHTTPServers(req); err != nil { + if _, _, err := runtimeMCPServers(req); err != nil { t.Fatal("HTTPS bearer declaration rejected", err) } } diff --git a/apps/parsar-daemon/internal/agent/codex/mcp_required_test.go b/apps/parsar-daemon/internal/agent/codex/mcp_required_test.go index a7f04bf27..5eb978b5e 100644 --- a/apps/parsar-daemon/internal/agent/codex/mcp_required_test.go +++ b/apps/parsar-daemon/internal/agent/codex/mcp_required_test.go @@ -26,7 +26,7 @@ func TestRequiredMCPWaitsForNativeThreadAndNeverRestartsFailedResume(t *testing. req.AgentSessionID = "fixture-native-thread" method = "thread/resume" } - declarations, err := publicMCPHTTPServers(req) + declarations, _, err := runtimeMCPServers(req) if err != nil { t.Fatal(err) } diff --git a/apps/parsar-daemon/internal/agent/codex/session_plan.go b/apps/parsar-daemon/internal/agent/codex/session_plan.go index 4dad48a69..8591f34b8 100644 --- a/apps/parsar-daemon/internal/agent/codex/session_plan.go +++ b/apps/parsar-daemon/internal/agent/codex/session_plan.go @@ -17,7 +17,7 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg if err != nil { return SessionPlan{}, nil, err } - mcpServers, err := publicMCPHTTPServers(req) + mcpServers, mcpEnv, err := runtimeMCPServers(req) if err != nil { return SessionPlan{}, nil, err } @@ -53,12 +53,6 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg if req.DisableSubagents { disableSubagents(&plan) } - mcpBearerEnv := prepareMCPHTTPBearer(mcpServers, req.MCPHTTPServers) - mcpServers, environmentMCPEnv, err := mergeEnvironmentMCP(mcpServers, req.LocalEnvironment) - if err != nil { - plan.Cleanup() - return SessionPlan{}, nil, err - } if mcpServers != nil { if err := configureMCP(&plan, mcpServers); err != nil { plan.Cleanup() @@ -96,8 +90,7 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg return SessionPlan{}, nil, err } - plan.Env = append(plan.Env, mcpBearerEnv...) - plan.Env = append(plan.Env, environmentMCPEnv...) + plan.Env = append(plan.Env, mcpEnv...) return plan, skillRoots, nil } diff --git a/apps/parsar-daemon/internal/agent/mcode/environment_mcp.go b/apps/parsar-daemon/internal/agent/mcode/environment_mcp.go index 7fc33a999..78a91d3c5 100644 --- a/apps/parsar-daemon/internal/agent/mcode/environment_mcp.go +++ b/apps/parsar-daemon/internal/agent/mcode/environment_mcp.go @@ -2,37 +2,52 @@ package mcode import ( "fmt" - "strings" + "net/url" + + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent" "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" ) -func environmentMCP(local *proto.LocalEnvironment) ([]map[string]any, error) { - if local == nil || len(local.MCP) == 0 { - return nil, nil - } - if local.NetworkAccess != "enabled" { - return nil, fmt.Errorf("mcode: environment MCP requires enabled network") +func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, error) { + bindings, err := agent.ResolveMCPBindings(req) + if err != nil { + return nil, err } - servers := make([]map[string]any, 0, len(local.MCP)) - names := map[string]bool{"oac_workspace": true} - for _, item := range local.MCP { - name := item.Server.Name - if name == "" || strings.TrimSpace(name) != name || names[name] { - return nil, fmt.Errorf("mcode: ambiguous or unsupported environment MCP identity") + var servers []map[string]any + for _, binding := range bindings { + if binding.ServerLabel == "oac_workspace" || binding.ConnectionOrigin != "environment" || binding.AllowedTools != nil || binding.Required { + return nil, fmt.Errorf("mcode: unsupported MCP binding") } - names[name] = true - if item.Server.Type == "http" { - return nil, fmt.Errorf("mcode: environment HTTP MCP is not qualified") + if binding.Transport == "http" { + server, err := environmentHTTPMCP(binding) + if err != nil { + return nil, err + } + servers = append(servers, server) + } else { + command, args := localworkspace.MCPStdioCommand(*binding.Stdio) + servers = append(servers, map[string]any{"name": binding.ServerLabel, "command": command, "args": args, "env": []map[string]string{}}) } - if item.Server.Type != "stdio" || item.BearerToken != nil { - return nil, fmt.Errorf("mcode: unsupported environment MCP transport") - } - command, args := localworkspace.MCPStdioCommand(item) - // The fixed launcher resolves the declaration and selected user variables - // from the installed snapshot in the shared Runtime launcher. - servers = append(servers, map[string]any{"name": name, "command": command, "args": args, "env": []map[string]string{}}) } return servers, nil } + +// ACP session servers remain in native memory; credentials never enter argv or +// persisted native configuration. Custom headers are not qualified because the +// pinned native HTTP client can forward them across redirect origins. +func environmentHTTPMCP(item agent.MCPBinding) (map[string]any, error) { + endpoint, err := url.Parse(item.ServerURL) + if err != nil || (endpoint.Scheme != "http" && endpoint.Scheme != "https") || endpoint.Hostname() == "" || endpoint.User != nil || endpoint.Fragment != "" || endpoint.RawQuery != "" || endpoint.ForceQuery || endpoint.Opaque != "" || len(item.HTTPHeaders) != 0 { + return nil, fmt.Errorf("mcode: unsupported environment HTTP MCP declaration") + } + headers := []map[string]string{} + if item.BearerToken != nil { + if endpoint.Scheme != "https" || !agent.ValidMCPHTTPBearerToken(*item.BearerToken) { + return nil, fmt.Errorf("mcode: unsupported environment MCP bearer credential") + } + headers = append(headers, map[string]string{"name": "Authorization", "value": "Bearer " + *item.BearerToken}) + } + return map[string]any{"name": item.ServerLabel, "type": "http", "url": item.ServerURL, "headers": headers}, nil +} diff --git a/apps/parsar-daemon/internal/agent/mcode/environment_mcp_test.go b/apps/parsar-daemon/internal/agent/mcode/environment_mcp_test.go index 455e5ec3c..aca2db8ba 100644 --- a/apps/parsar-daemon/internal/agent/mcode/environment_mcp_test.go +++ b/apps/parsar-daemon/internal/agent/mcode/environment_mcp_test.go @@ -72,15 +72,22 @@ func TestEnvironmentMCPUsesFixedLauncherForNewAndLoadedSessions(t *testing.T) { } func TestEnvironmentMCPRejectsUnqualifiedAuthorityBeforePreparation(t *testing.T) { - for _, name := range []string{"http", "http-bearer", "restricted", "disabled", "duplicate", "reserved"} { + for _, name := range []string{"http-headers", "http-bearer-insecure", "http-bearer-missing", "restricted", "disabled", "duplicate", "reserved"} { t.Run(name, func(t *testing.T) { c, req, _ := workspaceFixture(t) c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" req.LocalEnvironment.MCP = []proto.EnvironmentMCP{environmentMCPFixture()} switch name { - case "http", "http-bearer": + case "http-headers", "http-bearer-insecure", "http-bearer-missing": req.LocalEnvironment.MCP[0].Server = agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"} - if name == "http-bearer" { + if name == "http-headers" { + req.LocalEnvironment.MCP[0].Server.HTTPHeaders = map[string]string{"X-Private": "secret"} + } + if name == "http-bearer-missing" { + req.LocalEnvironment.MCP[0].Server.BearerTokenEnvVar = "SELECTED_TOKEN" + } + if name == "http-bearer-insecure" { + req.LocalEnvironment.MCP[0].Server.URL = "http://example.invalid/mcp" token := "confidential-http-token" req.LocalEnvironment.MCP[0].BearerToken = &token } @@ -172,3 +179,56 @@ func mcpRegistryEntry(server, segment, tool, toolSegment string) map[string]any key, _ := json.Marshal([]string{"configured", server}) return map[string]any{"key": string(key), "raw": server, "segment": segment, "tools": []map[string]string{{"raw": tool, "segment": toolSegment}}} } + +func TestEnvironmentHTTPMCPUsesEphemeralACPConfiguration(t *testing.T) { + for _, authenticated := range []bool{false, true} { + c, req, record := workspaceFixture(t) + c.Network, req.LocalEnvironment.NetworkAccess = "enabled", "enabled" + item := proto.EnvironmentMCP{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}} + const token = "private-mcp-canary" + if authenticated { + value := token + item.BearerToken = &value + } + req.LocalEnvironment.MCP = []proto.EnvironmentMCP{item} + resource, err := NewPreparationFactory(c)(t.Context(), req) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = resource.Close() }) + raw, err := os.ReadFile(record + ".session") + if err != nil { + t.Fatal(err) + } + var params struct { + MCP []struct { + Name, Type, URL string + Headers []map[string]string + } `json:"mcpServers"` + } + if json.Unmarshal(raw, ¶ms) != nil || len(params.MCP) != 2 { + t.Fatal("HTTP MCP displaced workspace tools") + } + server := params.MCP[1] + if server.Name != "remote" || server.Type != "http" || server.URL != item.Server.URL || server.Headers == nil { + t.Fatal("invalid native HTTP projection") + } + if authenticated { + if !reflect.DeepEqual(server.Headers, []map[string]string{{"name": "Authorization", "value": "Bearer " + token}}) { + t.Fatal("credential missing from ACP transport") + } + } else if len(server.Headers) != 0 { + t.Fatal("anonymous MCP inherited credentials") + } + dataDir := resource.(*prepared).session.opts.DataDir + for _, name := range []string{"config.yaml", "mcp.json", "workspace-profile.json"} { + body, err := os.ReadFile(filepath.Join(dataDir, name)) + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + if strings.Contains(string(body), token) { + t.Fatal("MCP credential persisted in native configuration") + } + } + } +} diff --git a/apps/parsar-daemon/internal/agent/mcode/mcp_observations.go b/apps/parsar-daemon/internal/agent/mcode/mcp_observations.go index f8a6b06ea..f309dea72 100644 --- a/apps/parsar-daemon/internal/agent/mcode/mcp_observations.go +++ b/apps/parsar-daemon/internal/agent/mcode/mcp_observations.go @@ -43,7 +43,7 @@ func (s *Session) environmentMCPIdentity(name string) (*mcpToolIdentity, error) var key []string declared := 0 for _, item := range s.req.LocalEnvironment.MCP { - if item.Server.Name == server.Raw && item.Server.Type == "stdio" { + if item.Server.Name == server.Raw && (item.Server.Type == "stdio" || item.Server.Type == "http") { declared++ } } diff --git a/apps/parsar-daemon/internal/agent/mcode/tool_environment_test.go b/apps/parsar-daemon/internal/agent/mcode/tool_environment_test.go index 420cdf980..ac8b3b8c9 100644 --- a/apps/parsar-daemon/internal/agent/mcode/tool_environment_test.go +++ b/apps/parsar-daemon/internal/agent/mcode/tool_environment_test.go @@ -1,39 +1,130 @@ package mcode import ( + "bytes" "encoding/json" + "io/fs" "os" "path/filepath" "testing" + + "github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace" + "github.com/MiniMax-AI-Dev/parsar/internal/agentcapabilities" + "github.com/google/uuid" ) -func TestSelfHostedToolEnvironment(t *testing.T) { +func TestWorkspaceCredentialsRemainInRuntimeSnapshotAcrossReconnect(t *testing.T) { config, req, _ := workspaceFixture(t) - file := filepath.Join(t.TempDir(), "tool-env.json") - if err := os.WriteFile(file, []byte(`{"USER_VALUE":"ready"}`), 0600); err != nil { - t.Fatal(err) + source := filepath.Join(t.TempDir(), "operator.json") + write := func(path, body string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0600); err != nil { + t.Fatal(err) + } } - t.Setenv("OAC_RUNTIME_TOOL_ENV_FILE", file) - opts, err := prepareWorkspaceOptions(t.Context(), config, req) - if err != nil { + write(source, `{"MCP_TEST_TOKEN":"fixture-bearer-canary","TOOL_SECRET":"fixture-tool-canary"}`) + initialization := t.TempDir() + t.Setenv("OAC_RUNTIME_INITIALIZATION_DIRECTORY", initialization) + t.Setenv("OAC_RUNTIME_TOOL_ENV_FILE", source) + plugin := t.TempDir() + write(filepath.Join(plugin, ".codex-plugin", "plugin.json"), `{"name":"remote","description":"Credential fixture","mcpServers":"./.mcp.json"}`) + write(filepath.Join(plugin, ".mcp.json"), `{"mcpServers":{"remote":{"type":"http","url":"https://example.invalid/mcp","bearer_token_env_var":"MCP_TEST_TOKEN"}}}`) + environment, session := uuid.NewString(), uuid.NewString() + t.Setenv("OAC_RUNTIME_ENVIRONMENT_ID", environment) + t.Setenv("OAC_RUNTIME_SESSION_ID", session) + t.Setenv("OAC_RUNTIME_WORKSPACE", config.Directory) + t.Setenv("OAC_RUNTIME_CAPABILITY_DIRECTORY", t.TempDir()) + t.Setenv("OAC_RUNTIME_NETWORK_ACCESS", "enabled") + t.Setenv("OAC_RUNTIME_ALLOWED_DOMAINS", "") + req.WorkDir, req.AgentStateKey = "", "agents-api-"+session + req.LocalEnvironment.ID, req.LocalEnvironment.WorkspaceDirectory = environment, config.Directory + req.LocalEnvironment.Capabilities = true + req.LocalEnvironment.CapabilitySources = &agentcapabilities.Input{Directories: []string{plugin}} + for _, resume := range []bool{false, true} { + if resume { + req.AgentSessionID = "native-1" + write(source, `{"MCP_TEST_TOKEN":"changed","TOOL_SECRET":"changed"}`) + } + binding, err := localworkspace.Load() + if err != nil { + t.Fatal(err) + } + configured, err := binding.Configure(req) + if err != nil { + t.Fatal(err) + } + prepared, err := binding.Prepare(t.Context(), configured) + if err != nil { + t.Fatal(err) + } + opts, err := prepareWorkspaceOptions(t.Context(), config, prepared) + if err != nil { + t.Fatal(err) + } + raw, err := os.ReadFile(filepath.Join(opts.DataDir, "workspace-profile.json")) + if err != nil { + t.Fatal(err) + } + var profile struct { + ToolEnvFile string `json:"toolEnvFile"` + } + if err := json.Unmarshal(raw, &profile); err != nil { + t.Fatal(err) + } + if profile.ToolEnvFile != filepath.Join(initialization, "tool-env.json") { + t.Fatal("native profile did not retain the Runtime snapshot reference") + } + // Assert the actual env-selected HTTP credential, not a manually injected token. + headers, ok := opts.MCP[1]["headers"].([]map[string]string) + if !ok || len(headers) != 1 || headers[0]["name"] != "Authorization" || headers[0]["value"] != "Bearer fixture-bearer-canary" { + t.Fatal("frozen MCP credential was lost or replaced") + } + if err := filepath.WalkDir(opts.DataDir, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() { + return nil + } + body, err := os.ReadFile(path) + if err != nil { + return err + } + if bytes.Contains(body, []byte("fixture-bearer-canary")) || bytes.Contains(body, []byte("fixture-tool-canary")) { + t.Fatal("tool credential copied into native persisted state") + } + return nil + }); err != nil { + t.Fatal(err) + } + } + if err := os.Remove(filepath.Join(initialization, "tool-env.json")); err != nil { t.Fatal(err) } - raw, err := os.ReadFile(filepath.Join(opts.DataDir, "workspace-profile.json")) + binding, err := localworkspace.Load() if err != nil { t.Fatal(err) } - var profile struct { - ToolEnv map[string]string `json:"toolEnv"` - } - if err := json.Unmarshal(raw, &profile); err != nil { + configured, err := binding.Configure(req) + if err != nil { t.Fatal(err) } - if profile.ToolEnv["USER_VALUE"] != "ready" { - t.Fatal("self-hosted tool configuration was not applied") + if _, err := binding.Prepare(t.Context(), configured); err == nil { + t.Fatal("missing frozen credential source was recreated or fell back to anonymous") } +} + +func TestWorkspaceRejectsInvalidToolEnvironment(t *testing.T) { + config, req, _ := workspaceFixture(t) + file := filepath.Join(t.TempDir(), "tool-env.json") if err := os.WriteFile(file, []byte(`[]`), 0600); err != nil { t.Fatal(err) } + t.Setenv("OAC_RUNTIME_TOOL_ENV_FILE", file) + t.Setenv("OAC_RUNTIME_INITIALIZATION_DIRECTORY", t.TempDir()) if _, err := prepareWorkspaceOptions(t.Context(), config, req); err == nil { t.Fatal("invalid explicit tool configuration was ignored") } diff --git a/apps/parsar-daemon/internal/agent/mcode/workspace.go b/apps/parsar-daemon/internal/agent/mcode/workspace.go index d61d5dfb2..d2d53cfce 100644 --- a/apps/parsar-daemon/internal/agent/mcode/workspace.go +++ b/apps/parsar-daemon/internal/agent/mcode/workspace.go @@ -57,7 +57,7 @@ func prepareWorkspaceOptions(ctx context.Context, c WorkspaceConfig, req proto.P if !req.StrictResume || req.LocalEnvironment == nil || req.WorkDir != c.Directory || req.DisableExecutionEnvironment || !(agentnetwork.Policy{Access: c.Network, AllowedDomains: c.AllowedDomains}).Equal(agentnetwork.Policy{Access: req.LocalEnvironment.NetworkAccess, AllowedDomains: req.LocalEnvironment.AllowedDomains}) || req.WorkspaceReadOnly { return launchOptions{}, fmt.Errorf("mcode: execution does not match the dedicated workspace") } - servers, err := environmentMCP(req.LocalEnvironment) + servers, err := runtimeMCP(req) if err != nil { return launchOptions{}, err } @@ -122,11 +122,13 @@ func prepareWorkspaceOptions(ctx context.Context, c WorkspaceConfig, req proto.P profile["workspace"] = c.Directory { - values, err := localworkspace.ReadOptionalToolEnvironment() + file, err := localworkspace.ToolEnvironmentFile() if err != nil { return opts, err } - profile["toolEnv"] = values + if file != "" { + profile["toolEnvFile"] = file + } } raw, err = json.Marshal(profile) diff --git a/apps/parsar-daemon/internal/agent/mcp_binding.go b/apps/parsar-daemon/internal/agent/mcp_binding.go new file mode 100644 index 000000000..4072eb33e --- /dev/null +++ b/apps/parsar-daemon/internal/agent/mcp_binding.go @@ -0,0 +1,110 @@ +package agent + +import ( + "errors" + "maps" + "net/url" + "slices" + "strings" + + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" +) + +// MCPBinding is the Runtime's transient effective declaration. Adapters project +// it into their native configuration without resolving credentials or packages. +// Never serialize or log bindings: headers and bearer values may be confidential. +type MCPBinding struct { + ServerLabel string + ConnectionOrigin string + CredentialAuthority string + Transport string + ServerURL string + AllowedTools *[]string + Required bool + BearerToken *string + HTTPHeaders map[string]string + // Stdio retains the installed package identity for the fixed Runtime launcher. + Stdio *proto.EnvironmentMCP +} + +// ResolveMCPBindings combines public declarations with the frozen installation. +// The current public profile is service-origin HTTP; a workspace cannot move +// that connection or its credential authority onto the Environment implicitly. +func ResolveMCPBindings(req proto.PromptRequestPayload) ([]MCPBinding, error) { + var bindings []MCPBinding + if req.MCPHTTPServers != nil { + if !req.DisableExecutionEnvironment { + return nil, errors.New("service-origin MCP requires a service execution host") + } + bindings = make([]MCPBinding, 0, len(*req.MCPHTTPServers)) + for _, server := range *req.MCPHTTPServers { + item := MCPBinding{ServerLabel: server.ServerLabel, ConnectionOrigin: "service", CredentialAuthority: "none", Transport: "http", ServerURL: server.ServerURL, Required: server.Required, BearerToken: server.BearerToken} + if server.AllowedTools != nil { + names := append([]string{}, (*server.AllowedTools)...) + item.AllowedTools = &names + } + if server.BearerToken != nil { + item.CredentialAuthority = "project_vault" + } + bindings = append(bindings, item) + } + } + if local := req.LocalEnvironment; local != nil && len(local.MCP) > 0 { + if req.DisableExecutionEnvironment || local.NetworkAccess != "enabled" { + return nil, errors.New("environment MCP requires an enabled workspace network") + } + for _, installed := range local.MCP { + declaration := installed.Server + item := MCPBinding{ServerLabel: declaration.Name, ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: declaration.Type, ServerURL: declaration.URL, BearerToken: installed.BearerToken, HTTPHeaders: maps.Clone(declaration.HTTPHeaders)} + if declaration.BearerTokenEnvVar != "" && installed.BearerToken == nil { + return nil, errors.New("environment MCP credential unavailable") + } + if installed.BearerToken != nil || len(declaration.HTTPHeaders) > 0 || len(declaration.EnvVars) > 0 { + item.CredentialAuthority = "environment_configuration" + } + if declaration.Type == "stdio" { + value := installed + value.Server.Args = slices.Clone(installed.Server.Args) + value.Server.EnvVars = slices.Clone(installed.Server.EnvVars) + item.Stdio = &value + } + bindings = append(bindings, item) + } + } + names := map[string]bool{} + for i := range bindings { + item := &bindings[i] + if item.ServerLabel == "" || strings.TrimSpace(item.ServerLabel) != item.ServerLabel || names[item.ServerLabel] { + return nil, errors.New("ambiguous MCP identity") + } + names[item.ServerLabel] = true + if item.BearerToken != nil { + value := *item.BearerToken + item.BearerToken = &value + } + switch item.Transport { + case "stdio": + if item.Stdio == nil || item.BearerToken != nil || len(item.HTTPHeaders) > 0 || item.ServerURL != "" { + return nil, errors.New("invalid installed MCP transport") + } + case "http": + endpoint, err := url.Parse(item.ServerURL) + if err != nil || endpoint.Hostname() == "" || (endpoint.Scheme != "https" && endpoint.Scheme != "http") || endpoint.User != nil || endpoint.Fragment != "" || endpoint.Opaque != "" || (item.ConnectionOrigin == "service" && (endpoint.RawQuery != "" || endpoint.ForceQuery)) { + return nil, errors.New("invalid MCP endpoint") + } + if item.BearerToken != nil && (endpoint.Scheme != "https" || !ValidMCPHTTPBearerToken(*item.BearerToken)) { + return nil, errors.New("invalid HTTPS MCP bearer credential") + } + if item.AllowedTools != nil { + for _, name := range *item.AllowedTools { + if name == "" || strings.TrimSpace(name) != name { + return nil, errors.New("invalid MCP tool allowlist") + } + } + } + default: + return nil, errors.New("unsupported MCP transport") + } + } + return bindings, nil +} diff --git a/apps/parsar-daemon/internal/agent/mcp_binding_test.go b/apps/parsar-daemon/internal/agent/mcp_binding_test.go new file mode 100644 index 000000000..e4cf7c89d --- /dev/null +++ b/apps/parsar-daemon/internal/agent/mcp_binding_test.go @@ -0,0 +1,72 @@ +package agent + +import ( + "testing" + + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto" + "github.com/MiniMax-AI-Dev/parsar/internal/agentplugin" +) + +func TestMCPBindingsPreserveOriginAuthorityAndPolicy(t *testing.T) { + token := "explicit-token" + empty := []string{} + for _, allow := range []*[]string{nil, &empty} { + public := []proto.MCPHTTPServer{{ServerLabel: "remote", ServerURL: "https://example.test/mcp", AllowedTools: allow, Required: true, BearerToken: &token}} + got, err := ResolveMCPBindings(proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &public}) + if err != nil || len(got) != 1 { + t.Fatal("public binding unavailable", err) + } + b := got[0] + if b.ConnectionOrigin != "service" || b.CredentialAuthority != "project_vault" || b.Transport != "http" || !b.Required || (b.AllowedTools == nil) != (allow == nil) || *b.BearerToken != token { + t.Fatal("public policy or authority changed") + } + *b.BearerToken = "mutated" + if token != "explicit-token" { + t.Fatal("binding retained mutable credential pointer") + } + } + local := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{ + {Server: agentplugin.MCPServer{Name: "stdio", Type: "stdio", Command: "node", Args: []string{"tool.js"}}, PackageRoot: "plugins/proof", InstallationRoot: "/private/installed"}, + {Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp", HTTPHeaders: map[string]string{"X-Selected": "literal"}}, BearerToken: &token}, + }} + got, err := ResolveMCPBindings(proto.PromptRequestPayload{LocalEnvironment: local}) + if err != nil || len(got) != 2 { + t.Fatal("installed bindings unavailable", err) + } + if got[0].ConnectionOrigin != "environment" || got[0].CredentialAuthority != "none" || got[0].Stdio.PackageRoot != "plugins/proof" || got[0].Required || got[0].AllowedTools != nil || got[1].CredentialAuthority != "environment_configuration" { + t.Fatal("installed identity or authority changed") + } + got[0].Stdio.Server.Args[0] = "mutated" + got[1].HTTPHeaders["X-Selected"] = "mutated" + if local.MCP[0].Server.Args[0] != "tool.js" || local.MCP[1].Server.HTTPHeaders["X-Selected"] != "literal" { + t.Fatal("projection changed frozen installation") + } +} + +func TestMCPBindingsDistinguishAbsentAndEmptyProfile(t *testing.T) { + got, err := ResolveMCPBindings(proto.PromptRequestPayload{}) + if err != nil || got != nil { + t.Fatal("absent profile changed") + } + empty := []proto.MCPHTTPServer{} + got, err = ResolveMCPBindings(proto.PromptRequestPayload{DisableExecutionEnvironment: true, MCPHTTPServers: &empty}) + if err != nil || got == nil || len(got) != 0 { + t.Fatal("explicit empty profile lost ownership") + } +} + +func TestMCPBindingsRejectRelocationAndAmbiguousInstallation(t *testing.T) { + public := []proto.MCPHTTPServer{{ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} + local := &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp"}}}} + for _, req := range []proto.PromptRequestPayload{ + {MCPHTTPServers: &public, LocalEnvironment: local}, + {DisableExecutionEnvironment: true, LocalEnvironment: local}, + {LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: append(local.MCP, local.MCP[0])}}, + {LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "disabled", MCP: local.MCP}}, + {LocalEnvironment: &proto.LocalEnvironment{NetworkAccess: "enabled", MCP: []proto.EnvironmentMCP{{Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.test/mcp", BearerTokenEnvVar: "MISSING"}}}}}, + } { + if _, err := ResolveMCPBindings(req); err == nil { + t.Fatal("unsupported authority accepted") + } + } +} diff --git a/apps/parsar-daemon/internal/cli/claude_sdk.go b/apps/parsar-daemon/internal/cli/claude_sdk.go index 304b294d2..8fcb330e4 100644 --- a/apps/parsar-daemon/internal/cli/claude_sdk.go +++ b/apps/parsar-daemon/internal/cli/claude_sdk.go @@ -99,7 +99,10 @@ func discoverClaudeSDK(parent context.Context, rc *runContext, profile string, c out.Info.Available, out.Info.Version = true, info.SDK out.Info.Capabilities.MessageImages = info.SupportsMessageImages() out.Info.Capabilities.FunctionResultImages = info.SupportsFunctionResultImages() - out.Info.Capabilities.ToolSearch = out.Config.Workspace == nil && info.SupportsToolSearch() + out.Info.Capabilities.ToolSearch = info.SupportsToolSearch() + if out.Config.Workspace != nil { + out.Info.Capabilities.ToolSearch = info.SupportsWorkspaceToolSearch() + } out.Info.Capabilities.StructuredOutput = info.SupportsStructuredOutput() if out.Config.Workspace != nil { out.Info.Capabilities.StructuredOutput = info.SupportsWorkspaceStructuredOutput() diff --git a/apps/parsar-daemon/internal/cli/connect.go b/apps/parsar-daemon/internal/cli/connect.go index 15be6f650..037ee086a 100644 --- a/apps/parsar-daemon/internal/cli/connect.go +++ b/apps/parsar-daemon/internal/cli/connect.go @@ -30,7 +30,8 @@ const ( bootstrapTimeout = 10 * time.Second - killTimeout = 3 * time.Second + // Allow the native process grace period and subsequent owner/pipe cleanup. + stopTimeout = 10 * time.Second connectInlineURLEnv = "OAC_RUNTIME_DAEMON_CONNECT_URL" connectInlineTokenEnv = "OAC_RUNTIME_DAEMON_CONNECT_TOKEN" @@ -276,7 +277,7 @@ func spawnBackground(ctx context.Context, rc *runContext, profile string, argv [ } if err := ctx.Err(); err != nil { - if stopErr := daemonize.StopPIDFile(pidPath, killTimeout); stopErr != nil { + if stopErr := daemonize.StopPIDFile(pidPath, stopTimeout); stopErr != nil { return fmt.Errorf("connect: interrupted startup cleanup: %w", stopErr) } return err diff --git a/apps/parsar-daemon/internal/cli/stop.go b/apps/parsar-daemon/internal/cli/stop.go index e58f32301..eb20c5d99 100644 --- a/apps/parsar-daemon/internal/cli/stop.go +++ b/apps/parsar-daemon/internal/cli/stop.go @@ -42,7 +42,7 @@ func runStop(ctx *runContext, args []string) error { } fmt.Fprintf(ctx.stdout, "oac-daemon: requesting stop for pid=%d\n", pid) - if err := daemonize.StopPIDFile(pidPath, killTimeout); err != nil { + if err := daemonize.StopPIDFile(pidPath, stopTimeout); err != nil { return fmt.Errorf("stop: signal: %w", err) } fmt.Fprintln(ctx.stdout, "oac-daemon: stopped") diff --git a/apps/parsar-daemon/internal/localworkspace/initialization.go b/apps/parsar-daemon/internal/localworkspace/initialization.go index c974336a4..e4240761a 100644 --- a/apps/parsar-daemon/internal/localworkspace/initialization.go +++ b/apps/parsar-daemon/internal/localworkspace/initialization.go @@ -70,14 +70,30 @@ func validToolEnvironment(values map[string]string) bool { // ReadOptionalToolEnvironment permits a Runtime without explicit user variables. func ReadOptionalToolEnvironment() (map[string]string, error) { + values, _, err := readOptionalToolEnvironment() + return values, err +} + +// ToolEnvironmentFile returns the validated Runtime-owned source without copying +// its values into a Harness profile. An unconfigured environment has no file. +func ToolEnvironmentFile() (string, error) { + _, path, err := readOptionalToolEnvironment() + return path, err +} + +func readOptionalToolEnvironment() (map[string]string, string, error) { path, err := toolEnvironmentPath() if err != nil { - return nil, err + return nil, "", err } if _, err = os.Stat(path); errors.Is(err, os.ErrNotExist) && os.Getenv("OAC_RUNTIME_TOOL_ENV_FILE") == "" { - return map[string]string{}, nil + return map[string]string{}, "", nil + } + values, err := readToolEnvironmentFile(path) + if err != nil { + return nil, "", err } - return ReadToolEnvironment() + return values, path, nil } // The prepared snapshot takes precedence over the operator's source file. diff --git a/contracts/agents-api/README.md b/contracts/agents-api/README.md index 6ad43c2fb..0d07353b9 100644 --- a/contracts/agents-api/README.md +++ b/contracts/agents-api/README.md @@ -737,8 +737,8 @@ adds type-only `tool_search` for its qualified profile. Other discovery combinat the native 64-definition cap and nonblank names of at most 512 bytes remain compatibility gaps; repeated names and explicit non-object root types reject as officially. Claude SDK additionally requires an explicit object root. It accepts text and -successful inline PNG/JPEG function results on `none` and Docker `openai_hosted`; -failed images, unqualified placements and remote references +successful inline PNG/JPEG function results on `none`, Docker `openai_hosted` and `self_hosted`; +failed images and remote references remain gaps. See [function image coverage](function-result-images.md). Codex internal Goal/Skills/user-input/discovery semantics need upstream evidence; their presence alone does not prove a tool-set mismatch. diff --git a/contracts/agents-api/environment-capabilities-qualification.md b/contracts/agents-api/environment-capabilities-qualification.md new file mode 100644 index 000000000..ae7209e4c --- /dev/null +++ b/contracts/agents-api/environment-capabilities-qualification.md @@ -0,0 +1,104 @@ +# Environment capability qualification, September 30, 2026 + +This record covers the existing Linux Runtime path. It does not qualify every +Harness, model, operating system or infrastructure provider. Current rules remain +in [Environments](environments.md), [structured output](structured-output.md) and +[deferred function discovery](tool-search.md). + +## Verified public workflows + +Real requests used the pinned official Python SDK against a dedicated Core and +PostgreSQL, the official native daemon installer, and MiniMax-M2.7 or Kimi K3. No test model +or replacement model/tool loop was used. Remote evidence is under +`zju_a100_2:~/.oac/acceptance/environment-capabilities-20260930`. + +| Workflow | Evidence | Outcome | +| --- | --- | --- | +| Self-hosted Claude structured output | `claude-structured`, Session `2a70e3ca-b67c-4222-9c3a-2fa9b7db2b9d` | Native file read followed by schema-constrained proof; another completed Turn after a fresh daemon process resumed the same Session. | +| Self-hosted Claude deferred function discovery | `claude-discovery-qualified`, Session `7801d632-697f-419d-8531-848a60d9f1a8` | Native history records `ToolSearch` and the declared deferred callback. The callback used the random schema argument and the answer contained a fresh result supplied only by the client. Cold continuation completed another callback; cancellation during a third pending callback cancelled the Turn and settled the function call. | +| MiniMax installed HTTP MCP | `minimax-http-common` | Anonymous and explicit HTTPS bearer servers both returned fresh proofs, including after daemon restart. Cancelling an actual waiting HTTP tool call cancelled its Turn. The original scan covered only `config.yaml` and `mcp.json`; later review found an extra tool-environment copy in `workspace-profile.json`, so that scan did not establish secret-free native state. | + +The HTTP fixture implements MCP, not model responses. It verifies that the +anonymous server does not receive the other server's Authorization header and +that the authenticated server receives its selected token. It does not establish +cross-origin redirect qualification for arbitrary custom headers; those remain +rejected for MiniMax and Claude. + +The structured-output continuation and the first discovery result were recovered +through durable public reads. The initial test scripts made assumptions about an +initial SSE snapshot or optional assistant phase, so their failed script records +are retained separately. They are not presented as clean end-to-end SSE evidence. +The recovered results and native history verify the actual completed work; inputs +were not replayed to obtain those results. + +## MCP credential persistence follow-up + +Independent review found that MiniMax's workspace profile copied the complete +tool environment, including selected bearer values. The adapter now persists only +the validated Runtime-owned snapshot path; the tool launcher reads it when starting +tools. Missing or malformed snapshots fail explicitly without parser contents in +errors. No historical profile or credential migration is provided. + +A newly built daemon and MiniMax companion passed `minimax-http-private-env`: +anonymous and selected HTTPS bearer calls, cold continuation and cancellation of +an active HTTP call. After each Turn, the test scanned every native MiniMax state +file for the fresh token. A final check confirmed a real workspace profile with +no inline `toolEnv`, a snapshot reference outside native state, and no token in +any native state file. The earlier narrower scan above is not reused as evidence +for this guarantee. The Runtime's authorized private snapshot intentionally +retains the environment values required by tools. + +## Funded visual-model validation + +After restoring Kimi K3 credit, real pinned official-client requests used the +same Core/Runtime and native adapters on Linux user-managed machines: + +| Workflow | Evidence | Verified behavior | +| --- | --- | --- | +| Claude image messages | `claude-image-funded`, Session `be8c8251-5c07-4c39-b71c-3ac74139bbb7` | Randomized PNG colors, a second PNG after daemon restart, and JPEG on the rebuilt final Core. | +| Codex image messages | `codex-image-funded`, Session `eed03095-b3e9-4c97-8e23-3343287c797d` | Randomized PNG colors, a second PNG after daemon restart, and JPEG on the rebuilt final Core. | +| Claude function image results | `claude-function-image-funded`, Session `fc0f373d-d5a2-41bb-9061-7c0da3997387` | PNG supplied only in the callback; remote/failed image rejection leaves the call pending; identical result retry succeeds, conflicting retry rejects, and Items retain submitted content. Cold continuation recalls the prior image, pending-call cancellation settles, and final-Core JPEG succeeds. | + +The image follow-up changes operation admission, not native encoding, credential +selection, preparation or execution ownership. It removes the Environment-source +image gate for Codex and Claude; Runtime image support is still checked before +delivery. MiniMax image input remains rejected. This record adds no new managed +provider run, macOS/Windows live model run, or Codex function-result image +qualification. No production deployment was updated; only the owned acceptance +Core was restarted, preserving its database and Sessions. + +## Runtime boundaries + +All three adapters consume the same transient effective MCP binding resolver. +Unit and adapter tests retain nil versus empty tool allowlists, required flags, +service versus Environment origin, credential authority, reserved/duplicate +identities, fixed installed stdio launchers, secret-free generated configuration, +and existing failure/cancellation settlement. Public service-origin HTTP remains +restricted to its existing service execution host. This change does not add a +Core proxy or claim public `connection_origin=environment` support. + +The daemon CLI now waits up to ten seconds for confirmed shutdown, allowing the +native process owner's three-second grace period and subsequent pipe/owner +cleanup. A real MiniMax HTTP Session reproduced the old three-second CLI timeout; +its process exited shortly afterward. The new timeout passed cold restart and +cancellation. Genuine timeout still fails and retains process ownership records. + +## Limits and retained failures + +- The first Kimi attempt returned HTTP 429/quota and was cancelled. After the + account was funded, separate image workflows below used real visual requests; + the failed quota attempt remains separate evidence. +- An earlier ToolSearch bundle did not advertise workspace discovery. Its input + remained pending without a Turn; public cancellation returned 409. That Runtime + was stopped and the records retained. The corrected bundle advertises discovery + only with the explicit workspace/function features. This does not claim a fix + for the general pending-input cancellation gap. +- These model runs use Linux user-managed environments. The common managed + preparation flow has separate evidence in + [preparation qualification](environment-preparation-qualification.md); this + record does not add a new Docker/E2B/microsandbox model run. +- No Claude installation was performed on the Mac. Native platform CI, including + Windows, is required for the final PR. Windows manual acceptance remains absent. +- Public MiniMax MCP, Plugin tool allowlists/required flags, custom HTTP headers, + workspace discovery combined with Skills/Plugins/MCP, and MiniMax image + execution are not qualified by this work. diff --git a/contracts/agents-api/environments.md b/contracts/agents-api/environments.md index 0b3f47e86..9308ae026 100644 --- a/contracts/agents-api/environments.md +++ b/contracts/agents-api/environments.md @@ -638,17 +638,32 @@ initialized values override the selected declaration's variables. There is no Python sandbox launcher, mount policy, shell prefix or separate credential sandbox. Process groups and Windows Jobs own cancellation and descendant cleanup only. Claude composes MCP identity and observation with the same workspace profile; -MiniMax keeps its existing supported transport qualification. A public capability +MiniMax uses the same effective bindings for stdio and HTTP. A public capability advertisement must still match the installed engine's actual supported transport. +Runtime resolves public HTTP declarations and installed Plugin MCP through +`agent.ResolveMCPBindings` before adapter projection. Each transient binding +retains its connection origin, transport, nullable tool allowlist, required flag, +credential authority and installed stdio identity. Bindings are never persisted +or logged. Duplicate identities and unavailable selected credentials reject. +A service-origin request cannot silently become an Environment-origin connection; +the existing public HTTP MCP profile remains service-origin `environment:none`. +This internal consolidation does not qualify public `connection_origin=environment`. + Environment-origin literal HTTP headers remain rejected for the pinned Claude -client because its interpolation and cross-origin forwarding change their meaning. -MiniMax accepts environment stdio only. Its adapter reads the existing Session-private -native runtime-name registry for exact first-frame identities and cross-checks -completed native results. Reuse existing observation and cancellation settlement; -never fabricate a delayed start event, guess normalized identities or add a registry -of our own. Its unqualified HTTP transport remains rejected. -These mechanisms alone do not establish public MCP support or full compatibility. +and MiniMax clients because their cross-origin forwarding cannot preserve header +authority. MiniMax supports installed stdio and HTTP servers with anonymous or +explicit user-selected HTTPS bearer authentication. ACP HTTP declarations remain +Session-local native memory; tokens do not enter native configuration files or +process arguments. Required initialization and tool allowlists are not exposed +through the Plugin manifest, and public MiniMax MCP remains unqualified. +MiniMax reads the existing Session-private native runtime-name registry for exact +first-frame identities and cross-checks completed native results for both transports. +Reuse existing observation and cancellation settlement; never fabricate a delayed +start event, guess normalized identities or add a registry of our own. + +See [capability qualification](environment-capabilities-qualification.md) for +real model evidence and the remaining public-origin and image gaps. ## Contract inventory @@ -781,5 +796,6 @@ explicit Session `env` keys override the base. Runtime does not rewrite the sour file or inherit unrelated ambient credentials. Setup, capability resolution and Harness execution read the same prepared snapshot. Reconnect preserves that snapshot even if the operator edits the source; a new installation for a new -Session reads the current source. A missing or invalid explicitly configured file +Session reads the current source. Harness profiles may reference the Runtime-owned +environment file, but must not persist copies of its values. A missing or invalid explicitly configured file fails preparation. diff --git a/contracts/agents-api/execution-tools.md b/contracts/agents-api/execution-tools.md index 13604208f..1448f3c86 100644 --- a/contracts/agents-api/execution-tools.md +++ b/contracts/agents-api/execution-tools.md @@ -19,14 +19,14 @@ and Environment Plugin MCP have separate inventories and qualification. | Operation | Implemented behavior and real qualification | Unsupported or unverified boundary | | --- | --- | --- | -| Initial message input, `sessions.create` | String input and ordered user-message arrays share atomic admission. Codex/Claude text and inline image execution: M1/M2; ordinary MiniMax text: M1/P1. [Input contract](message-input.md), [initial parser](../../services/agents-api/internal/api/session_initial_input.go). | Whitespace-only text is admitted verbatim for Codex and rejected at admission for Claude SDK and MiniMax Code ([text content](message-input.md#text-content)). Empty parts beside text (SES-08) and local size-limit parity need upstream evidence. Images are only qualified for inline PNG/JPEG on `none` and Core-managed Docker `openai_hosted`; MiniMax, self-hosted images and remote URLs remain gaps. | +| Initial message input, `sessions.create` | String input and ordered user-message arrays share atomic admission. Codex/Claude text and inline image execution: M1/M2; ordinary MiniMax text: M1/P1. [Input contract](message-input.md), [initial parser](../../services/agents-api/internal/api/session_initial_input.go). | Whitespace-only text is admitted verbatim for Codex and rejected at admission for Claude SDK and MiniMax Code ([text content](message-input.md#text-content)). Empty parts beside text (SES-08) and local size-limit parity need upstream evidence. Inline PNG/JPEG is qualified on `none`, Core-managed Docker `openai_hosted` and `self_hosted`; MiniMax images and remote URLs remain gaps. | | Prepared and active messages, `sessions.events.create` | Ordered `input_text`/`input_image` arrays retain original content and distinct public user Items. HTTP 202 confirms persistence; native receipts establish application. Initial/prepared/active Docker PNG/JPEG: M2; active PNG on `none`: M1. [Shared admission](../../services/agents-api/internal/api/inputs.go). | Codex flattens native messages with blank-line separators. Claude can fold or queue native turns; it does not promise Codex's same-native-turn behavior. Public durability does not prove native consumption. Claude SDK and MiniMax Code reject messages without an image or non-whitespace text at admission (`unsupported_or_invalid_configuration`); Codex delivers them unchanged. | -| Structured output, `agent.text.format` | Save/inherit/freeze `{type:json_schema,schema:...}`. Claude SDK object-root, single Agent, medium verbosity, ordinary functions returning text: S1 (`none`) and S2 (Docker, including prepared/active input and Files/Artifacts). Native final text is retained unchanged; its stream follows the official message sequence with the whole text in one `output_text.delta` ([item serialization](history-events-usage.md#item-serialization-2026-09-23)). [Contract](structured-output.md), [profile](../../services/agents-api/internal/engine/claude.go). | An explicit non-object root type is a protocol error for every harness ([validation](official-semantics-alignment.md#agent-configuration-validation--september-23)). Codex/MiniMax, schemas without an object root, schema numbers changed by binary64, self-hosted, Skills/Plugins, MCP, Subagent and discovery combinations reject execution. No output repair, coercion or extra model loop. Arbitrary schema dialects are unverified. | +| Structured output, `agent.text.format` | Save/inherit/freeze `{type:json_schema,schema:...}`. Claude SDK object-root, single Agent, medium verbosity, ordinary functions returning text: S1 (`none`) and S2 (Docker, including prepared/active input and Files/Artifacts), plus [self-hosted execution and cold continuation](environment-capabilities-qualification.md). Native final text is retained unchanged; its stream follows the official message sequence with the whole text in one `output_text.delta` ([item serialization](history-events-usage.md#item-serialization-2026-09-23)). [Contract](structured-output.md), [profile](../../services/agents-api/internal/engine/claude.go). | An explicit non-object root type is a protocol error for every harness ([validation](official-semantics-alignment.md#agent-configuration-validation--september-23)). Codex/MiniMax, schemas without an object root, schema numbers changed by binary64, Skills/Plugins, MCP, Subagent and discovery combinations reject execution. No output repair, coercion or extra model loop. Arbitrary schema dialects are unverified. | | Function configuration, saved/inline Agents | Required name/description/schema; `defer_loading` defaults false. Protocol errors, repeated names and explicit non-object root types reject with the official fields ([validation](official-semantics-alignment.md#agent-configuration-validation--september-23)). Saved references resolve into an immutable Session snapshot. Codex/Claude real calls: F1/F2/M2/S1/S2. [Parser](../../services/agents-api/internal/api/function_configuration.go), [saved tools](../../services/agents-api/internal/api/saved_tools.go). | MiniMax public functions reject. Claude requires an explicit object root. Local nonblank/512-byte name and 64-definition Session bounds are compatibility gaps. Saving configuration alone does not qualify execution. | | Function-result admission, `events.create` | Required `turn_id`, `call_id`, `success`; optional nullable `error` and `output`. Output is string or ordered text/image content. Scoped atomic batches retain field presence, original content and retry identity in storage; public result Items and events always carry `output` and `error`, null when not submitted ([item serialization](history-events-usage.md#item-serialization-2026-09-23)). Same result retries are accepted; changed results and results after cancellation return 409 `conflict_error`, including after terminal state. In the caller's Session, an unknown call or a call of another Turn returns 400 `invalid_request_error` without changing the pending action; missing and foreign Sessions stay 404 ([error rows](official-semantics-alignment.md#session-input-conflicts-and-result-targets--september-23)). F1/F2/M2 plus [controlled SDK/raw checks](../../services/agents-api/tests/official_function_inputs.py). [Parser](../../services/agents-api/internal/api/function_inputs.go), [Store](../../services/agents-api/internal/store/function_inputs.go). | Admission is separate from application and public Item publication. Invalid or unqualified content cannot consume a pending call. Core messages omit the call and executor IDs that official messages name; hosted defaults and publication timing remain unverified. | | Function text results and native application | Codex waits for a matching live root dynamic-tool completion; Claude waits for a matching live root native tool result. Text/error results, retry/conflict, cancellation and cold continuation: F1/F2. [Receipt contract](function-result-images.md). | Transport writes alone do not confirm application. Confirmation is not provider consumption, crash recovery or exactly-once external effects. No automatic result replay. | -| Function image results | Successful ordered inline PNG/JPEG with text, large PNG and image-only JPEG: Codex/Claude `none` F1/F2; Docker M2. Public Items retain submitted bytes. Codex receipt regression: F2. | Claude rejects failed images and remote references before persistence; native resizing may change its image bytes. MiniMax functions and self-hosted image workflows are unqualified. Other Codex image/error/reference combinations cannot be inferred from successful-inline evidence. | -| Deferred function discovery | Type-only `tool_search` plus mixed eager/deferred functions: Claude SDK 0.3.269/native 2.1.269, Kimi K3, single Agent, medium, `none`, text results; text and PNG input D1. Native provider observations establish lazy schema loading. [Contract](tool-search.md). | A repeated `tool_search` is a protocol error. Codex/MiniMax discovery, search-only/missing-search, workspace, MCP, structured-output and Subagent combinations remain gaps. Opaque native policy changes lack a reliable pre-input deferral signal. Saved tools include `tool_search`; the pinned Session response union excludes it. Exact hosted projection is unverified. | +| Function image results | Successful ordered inline PNG/JPEG with text, large PNG and image-only JPEG: Codex/Claude `none` F1/F2; Docker M2. Public Items retain submitted bytes. Codex receipt regression: F2. | Claude rejects failed images and remote references before persistence; native resizing may change its image bytes. Self-hosted Claude image results use the same native path; see the current [qualification record](environment-capabilities-qualification.md). MiniMax functions remain unqualified. Other Codex image/error/reference combinations cannot be inferred from successful-inline evidence. | +| Deferred function discovery | Type-only `tool_search` plus mixed eager/deferred functions: Claude SDK 0.3.269/native 2.1.269, Kimi K3, single Agent, medium, `none`, text results; text and PNG input D1. Native provider observations establish lazy schema loading for D1. [Self-hosted workspace callback, continuation and cancellation](environment-capabilities-qualification.md) use the same native path; they do not add model-request observer evidence. [Contract](tool-search.md). | A repeated `tool_search` is a protocol error. Codex/MiniMax discovery, search-only/missing-search, workspace with Skills/Plugins, MCP, structured-output and Subagent combinations remain gaps. Opaque native policy changes lack a reliable pre-input deferral signal. Saved tools include `tool_search`; the pinned Session response union excludes it. Exact hosted projection is unverified. | | Explicit disabled search/PTC | Saved and inline `web_search.mode=disabled` and `programmatic_tool_calling.enabled=false`; shared native controls on initial and cold execution. All three harnesses on `none`: P1, including native inventory/control evidence. [Contract](tool-policy.md), [parser](../../services/agents-api/internal/api/disabled_tools.go). | Unsupported explicit enablement rejects at Session admission; saved Agents keep every pinned search mode as resource data (TV-05), and Sessions from such Agents reject unless they replace the tools. A repeated `web_search` is a protocol error. Omission retains approved native behavior, which does not establish official default-on PTC parity. Enabled search and default/error parity remain gaps; unrelated native utilities are not implicitly removed. | | Agent service-origin MCP | Implemented Codex/Claude HTTP `none` profiles, anonymous/static bearer, scoped Vault selection and native `mcp_call` Items. [Configuration and qualification limits](../../services/agents-api/README.md#http-mcp-execution), [profiles](../../services/agents-api/internal/engine/profile.go). | This closure batch does not requalify MCP/provider combinations. MiniMax, self-hosted/hosted service-origin MCP, OAuth, nonempty inline headers/metadata and other transports/origins reject. Claude requires a static connected inventory; original MCP-envelope fidelity and continuing server health are unverified. | | Environment Plugin MCP/native tools | Separate [Docker Plugin transport matrix](environment-templates.md#environment-origin-mcp-plugins) and [V1 deployment evidence](user-managed-runtime-v1.md). Native tools stay within the existing colocated Runtime. | Plugin MCP is not Agent service-origin MCP or a public function action. No new optional cross-product is qualified here. Native utility inventories need not be identical. | diff --git a/contracts/agents-api/function-result-images.md b/contracts/agents-api/function-result-images.md index ff98e51a0..699d10e75 100644 --- a/contracts/agents-api/function-result-images.md +++ b/contracts/agents-api/function-result-images.md @@ -62,6 +62,11 @@ error/remote result directly on an outstanding call before accepting a valid image on that same call; no mixed-message rejection substitutes for this check. Run evidence is retained outside the repository. This -coverage does not qualify self-hosted/user-managed image results, all native image limits, provider +coverage does not qualify all native image limits, provider parity, arbitrary managed output rewrites, crash recovery or full Agents API compatibility. No downloader, image converter or second tool loop belongs in Core. + +User-managed Linux image execution follows the same native workspace path. See +[current qualification](environment-capabilities-qualification.md) for the tested +Harnesses, formats, continuation and remaining boundaries. Historical evidence +above retains its original deployment scope. diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index ab8185c5d..2848ed2b4 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -295,7 +295,7 @@ Initial requests, `Executor.StartTurn` and steering consume the same ordered `proto.MessageInput`. Text-only adapters use `TextOnly()` to reject images without discarding content. Image adapters translate each part natively and acknowledge an active batch only after all its messages are applied. Register -`MessageImages` and qualify `MessageImagePlacements` separately; see the +Runtime `MessageImages` and qualify the engine profile’s `MessageImages` separately; see the [message-input contract and real acceptance](message-input.md). Hosted workspace execution additionally requires verified preparation, workspace diff --git a/contracts/agents-api/harnesses.md b/contracts/agents-api/harnesses.md index 1c346e3e3..a2ac5efe3 100644 --- a/contracts/agents-api/harnesses.md +++ b/contracts/agents-api/harnesses.md @@ -70,7 +70,7 @@ contracts. | V1 `self_hosted` daemon enrollment at `/workspace` | [Qualified deployment scope](user-managed-runtime-v1.md) | [Qualified deployment scope](user-managed-runtime-v1.md) | [Qualified deployment scope](user-managed-runtime-v1.md) | | Deferred function discovery | Unqualified; explicit rejection | [Single-agent text/function profile](tool-search.md) | Gap; see [tool search](tool-search.md) | | Structured output | Unqualified; explicit rejection | [Qualified single-agent function profile](structured-output.md) | Gap; see [structured output](structured-output.md) | -| Message images | Inline PNG/JPEG on `none` and Docker `openai_hosted` | Inline PNG/JPEG on `none` and Docker `openai_hosted` | Unsupported; see [message input](message-input.md) | +| Message images | Inline PNG/JPEG on `none`, Docker `openai_hosted` and `self_hosted` | Inline PNG/JPEG on `none`, Docker `openai_hosted` and `self_hosted` | Unsupported; see [message input](message-input.md) | | Explicit reasoning | Shared service gap | Shared service gap | Shared service gap | | Six Subagent reads | [Qualified scope](subagents.md) | [Qualified scope](subagents.md) | [Qualified scope](subagents.md) | diff --git a/contracts/agents-api/message-input.md b/contracts/agents-api/message-input.md index eb0eee56c..076daba7c 100644 --- a/contracts/agents-api/message-input.md +++ b/contracts/agents-api/message-input.md @@ -52,7 +52,7 @@ text ([Web architecture](../../docs/web/architecture.md)). Other clients' text i never trimmed. Harness profiles declare whether whitespace-only text is qualified, through the -same engine profile that declares image placements. Only Codex is qualified; it +same engine profile that declares image support. Only Codex is qualified; it delivers such text unchanged and completed whitespace-only Turns in live acceptance. Claude SDK is not qualified: the bridge and Anthropic-compatible providers reject text without a non-whitespace character. MiniMax Code is not @@ -89,7 +89,7 @@ only after every message in its batch is consumed. Its native 64-message bound i checked before any part of a batch enters the iterator. Public profile qualification and Runtime advertisement are separate. Admission -checks the registered image profile for this placement; device selection and +checks the registered operation-specific image profile; device selection and delivery check actual image support. Text-only operations retain existing offline admission. Adding an adapter must implement the shared contract and qualify the public operation, without adding engine-name branches to Core. @@ -168,7 +168,7 @@ native receipt qualification. ## Remaining gaps -Self-hosted/user-managed image workflows, MiniMax Code image input, remote HTTP(S) image URLs, +MiniMax Code image input, remote HTTP(S) image URLs, other media types and full upstream error/default semantics remain unqualified. MiniMax's fixed ACP advertises `image:false`; its adapter rejects images. These are implementation gaps, not changes to the official protocol. JPEG parsing/conversion @@ -178,3 +178,8 @@ parts beside text (SES-08), local payload limits and native batch-size parity ne upstream evidence. Function-result image support has its own [coverage record](function-result-images.md). No full protocol compatibility or support for arbitrary vision-model/provider combinations is claimed. + +User-managed Linux image execution follows the same native workspace path. See +[current qualification](environment-capabilities-qualification.md) for the tested +Harnesses, formats, continuation and remaining boundaries. Historical evidence +above retains its original deployment scope. diff --git a/contracts/agents-api/openapi.yaml b/contracts/agents-api/openapi.yaml index b3d283ae6..f53cc5999 100644 --- a/contracts/agents-api/openapi.yaml +++ b/contracts/agents-api/openapi.yaml @@ -3038,44 +3038,45 @@ paths: limits keep unsupported_or_invalid_configuration. Hosted network policy rejections return invalid_request_error with a null param. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none and qualified - openai_hosted also accept inline PNG/JPEG image content; other image combinations - and remote URLs are unsupported. None initial input atomically starts a Turn; - self_hosted initial input is reserved while returning its Environment connection - target, with execution deferred to native readiness and Session failure on - initial timeout. Initial input is required for none and for streamed creation - outside self_hosted. Omitted/null input remains valid for non-streaming hosted - and self_hosted creation. With stream=true, returns live Session events starting - with the committed creation snapshot and closes right after the first agent.session.idle - recorded when a Turn ends or an input reservation stops being pending, or - any agent.session.failed, without sending later events. A creation that admitted - nothing closes after the snapshot; a settlement that records no event closes - after events up to the cursor read with a settled Session projection. Required - actions keep it open; disconnect does not cancel execution. The GET events - stream remains live-only. New Sessions retain their authenticated creator; - all creation retries require the same typed subject, including across key - rotation. Saved-Agent retries and inline requests using Vault attachments - or credential references retain caller intent independently of later resource - changes; new hosted inline requests also freeze caller intent before deployment - defaults resolve; unrelated non-hosted inline retries preserve resolved/default - equivalences, and their resolved hash leaves out any deployment default. Provider - keys enter retry hashes only as fingerprints keyed by the credential key. - Unknown historical creators reject retries; known creators without recorded - intent retain resolved-snapshot retry rules. These conflict policies are local - and not verified hosted parity. A same-key stream=true retry of an existing - creation returns 201 with no events and closes at once; retry with stream=false - or use the GET events stream to recover. Claude SDK on none and Core-managed - Docker openai_hosted supports qualified object-root json_schema output with - medium verbosity, single-Agent execution and ordinary functions. Hosted execution - reuses native workspace tools and Files/Artifacts; Skills, Plugins, capability - directories, HTTP MCP, Subagent and tool_search combinations remain unqualified, - including inherited template contents. Other non-text initial input remains - unsupported. Basic Codex and Claude SDK openai_hosted creation requires an - explicitly configured managed provider. The Claude workspace profile supports - non-deferred function tools with text or successful inline PNG/JPEG results - alongside native workspace tools; HTTP MCP remains unsupported. Idle Sessions - provision automatically; initial provisioning has no caller connection action. - Network defaults to enabled; disabled and restricted policies reject before - compute allocation because the current Runtime cannot enforce them. The x_agents_core.environment + managed or self_hosted workspace profiles also accept inline PNG/JPEG image + content; other image combinations and remote URLs are unsupported. None initial + input atomically starts a Turn; self_hosted initial input is reserved while + returning its Environment connection target, with execution deferred to native + readiness and Session failure on initial timeout. Initial input is required + for none and for streamed creation outside self_hosted. Omitted/null input + remains valid for non-streaming hosted and self_hosted creation. With stream=true, + returns live Session events starting with the committed creation snapshot + and closes right after the first agent.session.idle recorded when a Turn ends + or an input reservation stops being pending, or any agent.session.failed, + without sending later events. A creation that admitted nothing closes after + the snapshot; a settlement that records no event closes after events up to + the cursor read with a settled Session projection. Required actions keep it + open; disconnect does not cancel execution. The GET events stream remains + live-only. New Sessions retain their authenticated creator; all creation retries + require the same typed subject, including across key rotation. Saved-Agent + retries and inline requests using Vault attachments or credential references + retain caller intent independently of later resource changes; new hosted inline + requests also freeze caller intent before deployment defaults resolve; unrelated + non-hosted inline retries preserve resolved/default equivalences, and their + resolved hash leaves out any deployment default. Provider keys enter retry + hashes only as fingerprints keyed by the credential key. Unknown historical + creators reject retries; known creators without recorded intent retain resolved-snapshot + retry rules. These conflict policies are local and not verified hosted parity. + A same-key stream=true retry of an existing creation returns 201 with no events + and closes at once; retry with stream=false or use the GET events stream to + recover. Claude SDK on none, Core-managed Docker openai_hosted and self_hosted + supports qualified object-root json_schema output with medium verbosity, single-Agent + execution and ordinary functions. Hosted execution reuses native workspace + tools and Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, + Subagent and tool_search combinations remain unqualified, including inherited + template contents. Other non-text initial input remains unsupported. Basic + Codex and Claude SDK openai_hosted creation requires an explicitly configured + managed provider. The Claude workspace profile supports non-deferred function + tools with text or successful inline PNG/JPEG results alongside native workspace + tools; HTTP MCP remains unsupported. Idle Sessions provision automatically; + initial provisioning has no caller connection action. Network defaults to + enabled; disabled and restricted policies reject before compute allocation + because the current Runtime cannot enforce them. The x_agents_core.environment extension accepts common preparation fields for either hosted or self-hosted placement: environment_template_id, files, env, packages, setup_commands, skills, plugins and capability_directories. Duplicate fields in environment @@ -3102,13 +3103,14 @@ paths: version selectors resolve the default version. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified - single-agent Claude environment:none function profile, including qualified - inline image messages and text results. Explicit web_search mode disabled - and programmatic_tool_calling enabled false use frozen common Runtime controls. - Enabled forms, including those saved on an Agent, remain unqualified and reject - before any write unless the Session replaces tools. Omitted programmatic configuration - preserves native behavior, a documented difference from the official default-on - behavior. Other combinations remain unqualified; see the operation coverage.' + single-agent Claude function profile on none or a managed/user-owned workspace, + including qualified inline image messages and text results. Explicit web_search + mode disabled and programmatic_tool_calling enabled false use frozen common + Runtime controls. Enabled forms, including those saved on an Agent, remain + unqualified and reject before any write unless the Session replaces tools. + Omitted programmatic configuration preserves native behavior, a documented + difference from the official default-on behavior. Other combinations remain + unqualified; see the operation coverage.' parameters: - description: agents=v1 in: header @@ -3613,9 +3615,9 @@ paths: description: An empty events array is a resource-authorized no-op; it creates no execution retry identity, Turn, Item or input receipt. For environment none, atomically accepts text messages, cancellation and function results. - Messages steer active work or start a queued Turn. The supported self_hosted - profile accepts text-only messages; qualified Codex and Claude SDK openai_hosted - profiles also accept inline PNG/JPEG. Under the Session lock, matching retries + Messages steer active work or start a queued Turn. Qualified Codex and Claude + SDK workspace profiles accept text and inline PNG/JPEG messages, independently + of managed or self_hosted ownership. Under the Session lock, matching retries retain their original target; new active messages append to the current Turn, while idle messages reserve work and wait up to the original five-minute connection/admission deadline. Return 202 only after durable admission, without claiming native @@ -3642,13 +3644,13 @@ paths: cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text results and, on none and qualified - openai_hosted, successful inline PNG/JPEG results, preserving ordered content; - error images and remote references reject before admission. Native image resizing - may change bytes. Runtime image-result support is checked only for image-bearing - delivery. Codex and Claude SDK on none and qualified openai_hosted accept - ordered inline PNG/JPEG image messages. Self-hosted profiles and other engines - remain text-only; remote image URLs are unsupported. Image references are - retained unchanged without service-side downloads. + workspace profiles, successful inline PNG/JPEG results, preserving ordered + content; error images and remote references reject before admission. Native + image resizing may change bytes. Runtime image-result support is checked only + for image-bearing delivery. Codex and Claude SDK on none and qualified managed + or self_hosted workspace profiles accept ordered inline PNG/JPEG image messages. + Other engines remain text-only; remote image URLs are unsupported. Image references + are retained unchanged without service-side downloads. parameters: - description: agents=v1 in: header diff --git a/contracts/agents-api/structured-output.md b/contracts/agents-api/structured-output.md index 530f1ea62..403b87ce2 100644 --- a/contracts/agents-api/structured-output.md +++ b/contracts/agents-api/structured-output.md @@ -8,14 +8,15 @@ the existing Agent/Session configuration resolution and immutable snapshot. ## Qualified execution profile Claude SDK supports object-root schemas with `environment:none` or Core-managed -Docker `openai_hosted`, medium verbosity, `multi_agent.enabled=false` and optional +Docker `openai_hosted` or user-managed `self_hosted`, medium verbosity, `multi_agent.enabled=false` and optional ordinary function tools returning text. Hosted execution uses the existing native workspace tools, preparation, Files and Artifacts. The native SDK remains responsible for its model/tool loop and schema validation. Codex and MiniMax -structured-output execution, self-hosted/E2B execution, Skills/Plugins/capability +structured-output execution, Skills/Plugins/capability directories (including inherited template contents), HTTP MCP, Subagent/tool-search combinations and schemas without an explicit object root are unqualified and -explicitly rejected. These are implementation gaps, not a redefinition of the +explicitly rejected. E2B uses the common execution path but has not been +requalified in this change. These are implementation gaps, not a redefinition of the official protocol. An explicit non-object root type is an official protocol error on save and Session creation for every harness ([validation](official-semantics-alignment.md#agent-configuration-validation--september-23)). @@ -68,7 +69,7 @@ preparation checks that bundle before native launch. These remain adapter readin features, not new Core lifecycle or public protocol variants. The current qualified path is Claude SDK, `environment:none` or Core-managed -Docker `openai_hosted`, medium verbosity, single Agent, with optional ordinary +Docker `openai_hosted` or `self_hosted`, medium verbosity, single Agent, with optional ordinary function tools and text results. The workspace uses its existing preparation and bypass execution profile with the SDK's configured `StructuredOutput` tool added to inventory and permission checks. Frozen schemas reach preparation before the @@ -119,3 +120,6 @@ of complete protocol compatibility. The retained Codex provider/tool-chain failure is not reopened by Claude's native qualification. Its next attempt needs a concrete changed prerequisite; do not weaken assertions, rewrite model output or repeatedly sample until one run passes. + +Self-hosted Claude structured output and cold continuation use the same workspace +adapter as managed execution. See [current qualification](environment-capabilities-qualification.md). diff --git a/contracts/agents-api/tool-search.md b/contracts/agents-api/tool-search.md index c9e0fa9e1..2d1a8c8b0 100644 --- a/contracts/agents-api/tool-search.md +++ b/contracts/agents-api/tool-search.md @@ -17,16 +17,18 @@ loading belong to the harness adapter. Core retains the frozen definitions and existing public function calls, results and application receipts. The pinned Items union contains no tool-search Item; do not invent one. -The first implementation is Claude SDK 0.3.269 / native 2.1.269, single Agent, -`environment:none`, medium verbosity, object-root function schemas and text results. +The implementation is Claude SDK 0.3.269 / native 2.1.269, single Agent, +`environment:none` or a managed/user-owned workspace, medium verbosity, object-root +function schemas and text results. Workspace tool discovery excludes Skills, +Plugins and local capability directories; its ordinary native workspace tools remain available. It supports a mixture of eager and deferred application functions with text or previously qualified inline PNG/JPEG message inputs. The native MCP server marks eager definitions `anthropic/alwaysLoad:true`; deferred definitions use -false and the adapter explicitly enables native ToolSearch. Only declared callbacks -and ToolSearch are allowed. No search index, callback protocol, provider proxy or +false and the adapter explicitly enables native ToolSearch. The function profile allows only declared callbacks and ToolSearch in addition +to the selected workspace tools. No search index, callback protocol, provider proxy or model loop is added to production. -Search-only, missing-search, workspace, HTTP MCP, structured-output and Subagent +Search-only, missing-search, HTTP MCP, structured-output and Subagent combinations remain unqualified. A repeated `tool_search` is an official protocol error on saved and inline configuration. They are implementation/verification gaps, not claimed upstream restrictions. Codex and MiniMax discovery remain gaps. @@ -42,20 +44,12 @@ model/tool loop. Additional harnesses implement the same intent in their adapter The pinned Session AgentTool response union excludes the tool_search input member; project it out of Session/SSE resources while preserving saved and frozen input. -The bounded implementation targets Claude's single-agent `environment:none` -function profile, including qualified inline message images. The adapter explicitly enables native ToolSearch and sets -per-function MCP `anthropic/alwaysLoad` from the requested deferral flag. Ordinary -functions stay eager. Existing function callbacks, application receipts, cancellation -and cold continuation remain the only execution/result lifecycle. Runtime discovery -advertises this operation only with an installed bridge supporting `tool_search`. - -Known conflicting native provider modes and search/beta settings reject in the -adapter. The maintained native harness owns dynamic model/provider eligibility; -its SDK exposes no reliable pre-input receipt proving effective deferral after a -policy change. Do not represent tool inventory or an operator allowlist as that -proof. Record exact real model/provider evidence and this detection gap separately. -Search-only, missing-search, workspace, MCP and Subagent combinations remain -unqualified; a repeated `tool_search` is a protocol error. See [the operation coverage](tool-search.md). +Workspace discovery uses the same native ToolSearch and callback path. The +installed bridge must additionally advertise `workspace_tool_search` and the +complete local Runtime/function contract. The daemon derives its ordinary +`tool_search` capability from these verified bridge features; Core does not branch +on environment ownership. Existing callback receipts, cancellation and native cold +continuation retain their semantics. See [current workspace qualification](environment-capabilities-qualification.md). ## Evidence and limitations diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 480765419..c40367646 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -240,13 +240,13 @@ input list and uses blank-line separators between messages; this does not preser independent native user-message boundaries. The independently packaged Claude bridge uses protocol 3 for a prepared Executor and separately identified Turns; readiness rejects other protocol versions. -Image-bearing messages require a qualified profile/placement before persistence +Image-bearing messages require a qualified operation profile before persistence and image support from the selected Runtime before native delivery. These checks apply to that operation only; ordinary text retains offline queueing. Initial, prepared and active paths use the same content and preserve receipt ownership. The qualified public profile is inline PNG/JPEG on Codex/Claude `none` and -Core-managed Docker `openai_hosted`. Self-hosted/user-managed images, MiniMax -images and remote URLs remain explicit implementation gaps. Hosted images reuse +Core-managed Docker `openai_hosted` and user-managed `self_hosted`. MiniMax +images and remote URLs remain explicit implementation gaps. Workspace images reuse the existing preparation, active-input and workspace authority; they do not add a downloader, a mount or a separate execution lifecycle. Core does not fetch or transform media. See [message input coverage](../contracts/agents-api/message-input.md). diff --git a/packages/claude-sdk-adapter/README.md b/packages/claude-sdk-adapter/README.md index a2735681b..0b32bcd37 100644 --- a/packages/claude-sdk-adapter/README.md +++ b/packages/claude-sdk-adapter/README.md @@ -442,3 +442,9 @@ or treat local readiness as provider authentication, public capability acceptanc or filesystem isolation. The native installer reuses this readiness check after copying its release components. + +Workspace deferred-function discovery uses native ToolSearch alongside the normal +workspace tool profile. Its readiness feature is `workspace_tool_search`, in +addition to `tool_search` and the existing workspace/function features. Qualification, +combination limits and model-policy limitations are owned by +[Deferred function discovery](../../contracts/agents-api/tool-search.md). diff --git a/packages/claude-sdk-adapter/src/adapter.ts b/packages/claude-sdk-adapter/src/adapter.ts index d62e2753c..0cdf3e4be 100644 --- a/packages/claude-sdk-adapter/src/adapter.ts +++ b/packages/claude-sdk-adapter/src/adapter.ts @@ -49,7 +49,7 @@ export async function execute(request: Start | Prepare | ExecutorPrepare, emit: const declarations = request.workspace?.mcp ?? request.mcp_http_servers; const profile = declarations === undefined ? undefined : new MCPProfile(declarations, names); const subagents = request.subagents ? new Subagents(request.cwd, request.subagents.max_concurrent, request.resume) : undefined; - const workspace = request.workspace === undefined ? undefined : new WorkspaceProfile(request.cwd, request.workspace, names, profile, subagents, !!request.output_format); + const workspace = request.workspace === undefined ? undefined : new WorkspaceProfile(request.cwd, request.workspace, names, profile, subagents, !!request.output_format, !!request.tool_search); let commands = workspace ? new CommandObserver() : undefined; if (request.type === "prepare" && !workspace) throw new Error("invalid_request"); if (workspace && "mcp_http_servers" in request) throw new Error("invalid_request"); @@ -89,7 +89,7 @@ export async function execute(request: Start | Prepare | ExecutorPrepare, emit: ...(request.native_model_options?.effort !== undefined ? { effort: request.native_model_options.effort } : {}), ...(request.native_model_options?.thinking !== undefined ? { thinking: request.native_model_options.thinking } : {}), cwd: request.cwd, - env: request.tool_search ? toolSearchEnvironment(process.env, request.model) : workspace?.options.env ?? { ...process.env, ...(subagents ? { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" } : {}) }, + env: workspace?.options.env ?? { ...process.env, ...(subagents ? { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" } : {}) }, model: request.model, ...(request.output_format ? { outputFormat: request.output_format } : {}), systemPrompt: request.system_prompt, @@ -121,6 +121,7 @@ export async function execute(request: Start | Prepare | ExecutorPrepare, emit: return child; }, }; + if (request.tool_search) options.env = toolSearchEnvironment(options.env ?? process.env, request.model); if(turns) { for(const matchers of Object.values(options.hooks ?? {})) for(const matcher of matchers ?? []) { matcher.hooks=matcher.hooks.map(hook=>(...args)=>turns.track(()=>hook(...args))); diff --git a/packages/claude-sdk-adapter/src/request.ts b/packages/claude-sdk-adapter/src/request.ts index dc96325a1..9cdd460ed 100644 --- a/packages/claude-sdk-adapter/src/request.ts +++ b/packages/claude-sdk-adapter/src/request.ts @@ -52,7 +52,7 @@ export function parseRequest(line: string): Start | Prepare | ExecutorPrepare { const deferred = (request.functions as Start["functions"])?.some(tool => tool.defer_loading) ?? false; if ((request.tool_search !== undefined && typeof request.tool_search !== "boolean") || (!!request.tool_search !== deferred) || - (request.tool_search && (request.subagents || request.workspace || request.mcp_http_servers !== undefined || request.output_format))) throw new Error("invalid_request"); + (request.tool_search && (request.subagents || request.mcp_http_servers !== undefined || request.output_format))) throw new Error("invalid_request"); if (request.subagents !== undefined) { const value = request.subagents as Record; if (!value || typeof value !== "object" || Object.keys(value).length !== 1 || !Number.isSafeInteger(value.max_concurrent) || (value.max_concurrent as number) < 1 || @@ -69,7 +69,7 @@ export function parseRequest(line: string): Start | Prepare | ExecutorPrepare { parseHTTPServers(request.mcp_http_servers); const workspace = parseWorkspace(request.workspace, request.cwd); if (request.subagents && workspace?.mcp?.length) throw new Error("invalid_request"); - if (request.output_format && (workspace?.mcp?.length || workspace?.skills?.length)) throw new Error("invalid_request"); + if ((request.output_format || request.tool_search) && (workspace?.mcp?.length || workspace?.skills?.length)) throw new Error("invalid_request"); if (request.require_history && !workspace) throw new Error("invalid_request"); if ((workspace && "mcp_http_servers" in request) || (request.type === "prepare" && !workspace)) throw new Error("invalid_request"); diff --git a/packages/claude-sdk-adapter/src/runtime_check.ts b/packages/claude-sdk-adapter/src/runtime_check.ts index 9a2ad1d94..3571f1dfc 100644 --- a/packages/claude-sdk-adapter/src/runtime_check.ts +++ b/packages/claude-sdk-adapter/src/runtime_check.ts @@ -45,7 +45,7 @@ try { assert.equal(smoke.error, undefined, "bridge_unavailable"); assert.equal(smoke.status, 0, "bridge_unavailable"); assert.deepEqual(JSON.parse(smoke.stdout), { type: "error", code: "invalid_request" }); - process.stdout.write(JSON.stringify({ type: "runtime_ready", protocol: 3, features: ["executor_reuse", ...(["linux", "darwin", "win32"].includes(process.platform) ? ["workspace_directory", "local_runtime_v2", "workspace_functions", "workspace_structured_output"] : []), "message_images", "function_result_images", "tool_search", "structured_output", "subagent_resources", "mcp_http_tools", "mcp_http_bearer_auth", "mcp_http_required", "workspace_tools", "workspace_prepare", "workspace_read", "workspace_command_observations"], node: process.versions.node, sdk: sdk.version, mcp: mcp.version, native: nativeVersion }) + "\n"); + process.stdout.write(JSON.stringify({ type: "runtime_ready", protocol: 3, features: ["executor_reuse", ...(["linux", "darwin", "win32"].includes(process.platform) ? ["workspace_directory", "local_runtime_v2", "workspace_functions", "workspace_structured_output", "workspace_tool_search"] : []), "message_images", "function_result_images", "tool_search", "structured_output", "subagent_resources", "mcp_http_tools", "mcp_http_bearer_auth", "mcp_http_required", "workspace_tools", "workspace_prepare", "workspace_read", "workspace_command_observations"], node: process.versions.node, sdk: sdk.version, mcp: mcp.version, native: nativeVersion }) + "\n"); } catch { // Native diagnostics can include operator environment; never forward them. process.stdout.write(JSON.stringify({ type: "runtime_unavailable" }) + "\n"); diff --git a/packages/claude-sdk-adapter/src/workspace.ts b/packages/claude-sdk-adapter/src/workspace.ts index 3aa66a38f..f26d85424 100644 --- a/packages/claude-sdk-adapter/src/workspace.ts +++ b/packages/claude-sdk-adapter/src/workspace.ts @@ -66,7 +66,7 @@ export class WorkspaceProfile { readonly options: Options; private readonly skillNames: readonly string[]; - constructor(private readonly cwd: string, private readonly config: Workspace, private readonly functions: readonly string[] = [], private readonly mcp?: MCPProfile, private readonly subagents?: Subagents, private readonly structuredOutput = false) { + constructor(private readonly cwd: string, private readonly config: Workspace, private readonly functions: readonly string[] = [], private readonly mcp?: MCPProfile, private readonly subagents?: Subagents, private readonly structuredOutput = false, private readonly toolSearch = false) { config = parseWorkspace(config, cwd)!; // SDK history lookup reads the bridge environment, independently of query.env. if (process.env.HOME !== config.home || process.env.CLAUDE_CONFIG_DIR !== config.state || @@ -94,8 +94,8 @@ export class WorkspaceProfile { this.skillNames = skills?.names ?? []; const skillTools = skills ? ["Skill"] : []; this.options = { - env, tools: [...nativeTools, ...skillTools, ...(subagents ? ["Agent", "SendMessage"] : [])], - ...(skills ? { plugins: skills.paths.map(path => ({ type: "local" as const, path, skipMcpDiscovery: true })) } : {}), allowedTools: mcp?.allowed ?? [...functions], mcpServers: {}, strictMcpConfig: true, + env, tools: [...nativeTools, ...(toolSearch ? ["ToolSearch"] : []), ...skillTools, ...(subagents ? ["Agent", "SendMessage"] : [])], + ...(skills ? { plugins: skills.paths.map(path => ({ type: "local" as const, path, skipMcpDiscovery: true })) } : {}), allowedTools: mcp?.allowed ?? [...functions, ...(toolSearch ? ["ToolSearch"] : [])], mcpServers: {}, strictMcpConfig: true, // The existing callback authorizes tools without CLI permission bypass, // which the native CLI refuses for root accounts. settingSources: [], permissionMode: "default", persistSession: true, @@ -111,7 +111,7 @@ export class WorkspaceProfile { this.mcp.verify(tools, servers as Parameters[1], sessionID, [...nativeTools, ...(this.skillNames.length ? ["Skill"] : []), ...(this.subagents ? ["Task", "SendMessage"] : [])]); return; } - const expected = [...nativeTools, ...(this.structuredOutput ? ["StructuredOutput"] : []), ...this.functions, ...(this.skillNames.length ? ["Skill"] : []), ...(this.subagents ? ["Task", "SendMessage"] : [])]; + const expected = [...nativeTools, ...(this.toolSearch ? ["ToolSearch"] : []), ...(this.structuredOutput ? ["StructuredOutput"] : []), ...this.functions, ...(this.skillNames.length ? ["Skill"] : []), ...(this.subagents ? ["Task", "SendMessage"] : [])]; if (servers.length !== (this.functions.length ? 1 : 0) || servers.some(server => server.name !== "functions" || server.status !== "connected") || tools.length !== expected.length || new Set(tools).size !== tools.length || @@ -155,6 +155,7 @@ export class WorkspaceProfile { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const input = value as Record; if (this.structuredOutput && name === "StructuredOutput") return true; + if (this.toolSearch && name === "ToolSearch") return true; if (this.functions.includes(name) || this.mcp?.permits(name)) return true; if (name === "Skill") return this.skillName(input.skill) !== undefined; if (name === "Bash") return typeof input.command === "string" && !!input.command.trim() && diff --git a/packages/claude-sdk-adapter/tests/workspace.test.mjs b/packages/claude-sdk-adapter/tests/workspace.test.mjs index bdb471fb2..750d578ba 100644 --- a/packages/claude-sdk-adapter/tests/workspace.test.mjs +++ b/packages/claude-sdk-adapter/tests/workspace.test.mjs @@ -200,3 +200,24 @@ test("host tools can access paths outside the workspace", async t => { assert.equal((await profile.canUseTool("Read",{file_path},{signal:new AbortController().signal})).behavior,"allow"); } }); + + +test("workspace discovery preserves native file tools and the function boundary", async t => { + const { dirs, config, request } = fixture(t); + const functions = [{name:"lookup", description:"Lookup", parameters:{type:"object"}, defer_loading:true}]; + const configured = {...request, tool_search:true, functions}; + assert.deepEqual(parseStart(JSON.stringify(configured)), configured); + const profile = new WorkspaceProfile(dirs.workspace, config, ["mcp__functions__lookup"], undefined, undefined, false, true); + assert.deepEqual(profile.options.tools, ["Bash", "Read", "Edit", "ToolSearch"]); + assert.deepEqual(profile.options.allowedTools, ["mcp__functions__lookup", "ToolSearch"]); + const tools = [...profile.options.tools, "mcp__functions__lookup"]; + profile.verify(tools, [{name:"functions",status:"connected"}]); + assert.throws(() => profile.verify(tools.filter(name => name !== "ToolSearch"), [{name:"functions",status:"connected"}])); + const ordinary = new WorkspaceProfile(dirs.workspace, config); + const context = {signal:new AbortController().signal, toolUseID:"search", requestId:"request"}; + assert.equal((await profile.canUseTool("ToolSearch", {query:"lookup"}, context)).behavior,"allow"); + assert.equal((await ordinary.canUseTool("ToolSearch", {query:"lookup"}, context)).behavior,"deny"); + assert.equal((await profile.canUseTool("mcp__functions__undeclared", {}, context)).behavior,"deny"); + assert.equal((await profile.canUseTool("ToolSearch", {}, {...context, agentID:"child"})).behavior,"deny"); + assert.equal((await profile.canUseTool("ToolSearch", {}, {...context, signal:AbortSignal.abort()})).behavior,"deny"); +}); diff --git a/packages/mcode-harness/launch.mjs b/packages/mcode-harness/launch.mjs index d07c45649..3400808ff 100644 --- a/packages/mcode-harness/launch.mjs +++ b/packages/mcode-harness/launch.mjs @@ -14,9 +14,21 @@ if (!Array.isArray(domains) || (profile.network==='restricted' ? domains.length<1 || domains.length>100 || domains.some(host=>typeof host!=='string'||!hostname.test(host)||host.trim()!==host||isIP(host)!==0) : domains.length!==0)) throw new Error('Invalid network domains'); const baseEnv = {...process.env}; -if (profile.toolEnv !== undefined) { - if (!profile.toolEnv || typeof profile.toolEnv !== 'object' || Array.isArray(profile.toolEnv) || Object.values(profile.toolEnv).some(value=>typeof value!=='string')) throw new Error('Invalid tool environment'); - Object.assign(baseEnv,profile.toolEnv); +if (Object.hasOwn(profile, 'toolEnv')) throw new Error('Inline tool environment is unsupported'); +if (profile.toolEnvFile !== undefined) { + // Resolve values only at tool launch, from the Runtime-owned frozen snapshot. + // Never include parser diagnostics: malformed input can contain credentials. + let values; + try { + const path = profile.toolEnvFile; + if (typeof path !== 'string' || !isAbsolute(path) || normalize(path) !== path) throw new Error(); + const raw = readFileSync(path); + if (raw.length > 1024 * 1024) throw new Error(); + values = JSON.parse(raw.toString('utf8')); + if (!values || typeof values !== 'object' || Array.isArray(values) || + Object.entries(values).some(([key, value]) => !key || /[=\x00\r\n]/.test(key) || typeof value !== 'string' || value.includes('\0'))) throw new Error(); + } catch { throw new Error('Runtime tool environment unavailable'); } + Object.assign(baseEnv, values); } if (profile.capabilityRoot && (typeof profile.capabilityRoot !== 'string' || !isAbsolute(profile.capabilityRoot) || normalize(profile.capabilityRoot) !== profile.capabilityRoot || profile.capabilityRoot === '/' || /[\\\x00-\x1f\x7f]/.test(profile.capabilityRoot))) throw new Error('Invalid capability root'); if (profile.skills && !profile.capabilityRoot) throw new Error('Missing capability root'); diff --git a/packages/mcode-harness/launch.test.mjs b/packages/mcode-harness/launch.test.mjs index 647c8fcf3..a2aa3859c 100644 --- a/packages/mcode-harness/launch.test.mjs +++ b/packages/mcode-harness/launch.test.mjs @@ -13,8 +13,16 @@ test('Runtime directly executes host worker with bound cwd and initialized envir const launcher=join(root,'launch.mjs');await copyFile(new URL('./launch.mjs',import.meta.url),launcher); await writeFile(join(root,'dist','worker.mjs'),`import {readFileSync} from 'node:fs';const request=JSON.parse(readFileSync(0,'utf8'));console.log(JSON.stringify({tool_name:request.tool,text:JSON.stringify({cwd:process.cwd(),value:process.env.INITIALIZED}),content:[]}));`); const profile=join(root,'profile.json'); - await writeFile(profile,JSON.stringify({workspace,scratch:join(root,'scratch'),network:'enabled',toolEnv:{INITIALIZED:'ordinary'}})); + const environment=join(root,'runtime-env.json'); + await writeFile(environment,JSON.stringify({INITIALIZED:'ordinary'}),{mode:0o600}); + await writeFile(profile,JSON.stringify({workspace,scratch:join(root,'scratch'),network:'enabled',toolEnvFile:environment})); const executor=new ToolExecutor(profile,launcher);t.after(()=>executor.close()); const result=await executor.execute('bash',{}); assert.deepEqual(JSON.parse(result.text),{cwd:workspace,value:'ordinary'}); + for (const body of ['{\"SECRET\":\"fixture-canary\",', '[]', '{\"KEY\":123}']) { + await writeFile(environment,body); + await assert.rejects(executor.execute('bash',{})); + } + await rm(environment); + await assert.rejects(executor.execute('bash',{})); }); diff --git a/scripts/check-claude-sdk-runtime.mjs b/scripts/check-claude-sdk-runtime.mjs index 8452b0f60..37327baab 100644 --- a/scripts/check-claude-sdk-runtime.mjs +++ b/scripts/check-claude-sdk-runtime.mjs @@ -23,7 +23,7 @@ assert.equal(probe.status, 0, "Exported runtime is unavailable"); const report = JSON.parse(probe.stdout); assert.equal(report.type, "runtime_ready"); assert.equal(report.protocol, 3); -assert.deepEqual(report.features, ["executor_reuse", ...(["linux", "darwin", "win32"].includes(process.platform) ? ["workspace_directory", "local_runtime_v2", "workspace_functions", "workspace_structured_output"] : []), "message_images", "function_result_images", "tool_search", "structured_output", "subagent_resources", "mcp_http_tools", "mcp_http_bearer_auth", "mcp_http_required", "workspace_tools", "workspace_prepare", "workspace_read", "workspace_command_observations"]); +assert.deepEqual(report.features, ["executor_reuse", ...(["linux", "darwin", "win32"].includes(process.platform) ? ["workspace_directory", "local_runtime_v2", "workspace_functions", "workspace_structured_output", "workspace_tool_search"] : []), "message_images", "function_result_images", "tool_search", "structured_output", "subagent_resources", "mcp_http_tools", "mcp_http_bearer_auth", "mcp_http_required", "workspace_tools", "workspace_prepare", "workspace_read", "workspace_command_observations"]); assert.equal(report.sdk, source.dependencies["@anthropic-ai/claude-agent-sdk"]); assert.equal(report.mcp, source.dependencies["@modelcontextprotocol/sdk"]); console.log(`Verified exported SDK ${report.sdk}, MCP ${report.mcp}, ${report.native}`); diff --git a/scripts/name-allowlist.json b/scripts/name-allowlist.json index c249fe4f2..afe14e326 100644 --- a/scripts/name-allowlist.json +++ b/scripts/name-allowlist.json @@ -547,22 +547,22 @@ { "path": "apps/web/src/features/sessions/items/ItemRenderers.tsx", "regex": "parseParsarApplyPatch|Parsar apply_patch", - "reason": "The parser identifies the existing product-origin apply_patch event format; renaming private parser symbols is deferred by §9." + "reason": "The parser identifies the existing product-origin apply_patch event format; renaming private parser symbols is deferred by \u00a79." }, { "path": "apps/web/src/features/sessions/items/apply-patch.ts", "regex": "parseParsarApplyPatch|Parsar apply_patch", - "reason": "The parser identifies the existing product-origin apply_patch event format; renaming private parser symbols is deferred by §9." + "reason": "The parser identifies the existing product-origin apply_patch event format; renaming private parser symbols is deferred by \u00a79." }, { "path": "apps/web/src/features/sessions/items/apply-patch.test.tsx", "regex": "parseParsarApplyPatch|Parsar apply_patch", - "reason": "The parser identifies the existing product-origin apply_patch event format; renaming private parser symbols is deferred by §9." + "reason": "The parser identifies the existing product-origin apply_patch event format; renaming private parser symbols is deferred by \u00a79." }, { "path": "apps/web/src/features/sessions/trace/TraceView.tsx", "regex": "parseParsarApplyPatch|Parsar apply_patch", - "reason": "The parser identifies the existing product-origin apply_patch event format; renaming private parser symbols is deferred by §9." + "reason": "The parser identifies the existing product-origin apply_patch event format; renaming private parser symbols is deferred by \u00a79." }, { "path": "apps/web/src/features/sandbox/deployment-specification.test.ts", @@ -878,5 +878,10 @@ "path": "apps/parsar-daemon/internal/contracttest/wire_test.go", "regex": "agents-api-session", "reason": "The controlled Runtime fixture uses the existing Core Session state-key prefix required by execution admission." + }, + { + "path": "apps/parsar-daemon/internal/agent/mcode/tool_environment_test.go", + "regex": "agents-api-", + "reason": "The regression fixture uses the existing persisted Session state-key namespace required by Runtime binding validation." } ] diff --git a/services/agents-api/internal/api/handler.go b/services/agents-api/internal/api/handler.go index 5b2c5df5d..dc3c51a26 100644 --- a/services/agents-api/internal/api/handler.go +++ b/services/agents-api/internal/api/handler.go @@ -164,7 +164,7 @@ func (h *Handler) routes() *chi.Mux { // createSession atomically reserves or admits initial text with the Session. // @Summary Create an execution Session -// @Description The optional Core model_provider bundle resolves from the Session override, saved Agent defaults, then, for openai_hosted and none, the deployment default of the resolved harness; self_hosted never uses the deployment default and none accepts only it. openai_hosted and self_hosted Sessions that resolve no bundle return 400 model_provider_required with param x_agents_core.model_provider before any write. Core encrypts and freezes the resolved bundle; later Agent or deployment default edits and same-key retries cannot change it. Keys are never returned. Supports inline configuration or a tenant-owned saved agent_id with per-Session field replacements. Execution supports model/instructions, text verbosity, non-deferred function tools, adapter-qualified multi_agent with persisted Subagent reads, implicit reasoning, service tier auto and environment type none, subject to the configured engine. Codex additionally supports HTTP MCP with service origin (omitted or null on HTTP transport is saved as service), native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer or OAuth credential for the exact HTTPS URL, while null/omission selects a unique match or remains anonymous. Session reads, lists and event snapshots show that implicitly selected credential ID in a null or omitted credential_id, also after the credential is deleted; anonymous selections stay null and the stored caller intent is unchanged. After the input requirement and before any write, a credential_id without vault_ids, one outside the attached Vaults (one message for missing, foreign and unattached IDs) or one for another server_url returns 400 invalid_request_error, and several implicit matches return 409 conflict_error. Missing decryption configuration fails dispatch without anonymous fallback. Required initialization uses native startup before the first native Turn, including cold resume, and requires a separately advertised capability; exact hosted creation timing and error parity remain unverified. Other MCP origins and native OAuth login remain unsupported. The self_hosted profile uses a qualified native harness, a clean absolute workspace_directory and optional absolute local capability_directories prepared by Runtime, with optional non-deferred function tools and HTTP MCP using service origin, optionally authenticated by the attached Vault rules. Remote MCP and remote Bearer authentication each require separately advertised combination support; old peers cannot receive unsupported work. Omitted/null capability_directories use the empty-list default; self_hosted requires configured execution plus executor registry. Claude SDK currently requires medium verbosity and object-root function schemas. It supports anonymous or attached static-bearer service-origin HTTP MCP on none with boolean required and separately advertised MCP/bearer/required runtime support. Required servers must be connected before the first native input is released; pending or failed startup rejects execution. The shared Vault selection and immutable binding rules apply; unsupported native labels/tool names reject before persistence. An attached Vault with no matching credential may remain anonymous; missing keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null; non-string values and limit violations return invalid_request_error with a metadata or metadata. param. The inline agent uses the Agent create configuration validation with agent.-prefixed params, reported before the input requirement and saved-Agent lookup; saved configurations with conflicting tools or schema roots reject admission with the same errors, and execution limits keep unsupported_or_invalid_configuration. Hosted network policy rejections return invalid_request_error with a null param. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none and qualified openai_hosted also accept inline PNG/JPEG image content; other image combinations and remote URLs are unsupported. None initial input atomically starts a Turn; self_hosted initial input is reserved while returning its Environment connection target, with execution deferred to native readiness and Session failure on initial timeout. Initial input is required for none and for streamed creation outside self_hosted. Omitted/null input remains valid for non-streaming hosted and self_hosted creation. With stream=true, returns live Session events starting with the committed creation snapshot and closes right after the first agent.session.idle recorded when a Turn ends or an input reservation stops being pending, or any agent.session.failed, without sending later events. A creation that admitted nothing closes after the snapshot; a settlement that records no event closes after events up to the cursor read with a settled Session projection. Required actions keep it open; disconnect does not cancel execution. The GET events stream remains live-only. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key rotation. Saved-Agent retries and inline requests using Vault attachments or credential references retain caller intent independently of later resource changes; new hosted inline requests also freeze caller intent before deployment defaults resolve; unrelated non-hosted inline retries preserve resolved/default equivalences, and their resolved hash leaves out any deployment default. Provider keys enter retry hashes only as fingerprints keyed by the credential key. Unknown historical creators reject retries; known creators without recorded intent retain resolved-snapshot retry rules. These conflict policies are local and not verified hosted parity. A same-key stream=true retry of an existing creation returns 201 with no events and closes at once; retry with stream=false or use the GET events stream to recover. Claude SDK on none and Core-managed Docker openai_hosted supports qualified object-root json_schema output with medium verbosity, single-Agent execution and ordinary functions. Hosted execution reuses native workspace tools and Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, Subagent and tool_search combinations remain unqualified, including inherited template contents. Other non-text initial input remains unsupported. Basic Codex and Claude SDK openai_hosted creation requires an explicitly configured managed provider. The Claude workspace profile supports non-deferred function tools with text or successful inline PNG/JPEG results alongside native workspace tools; HTTP MCP remains unsupported. Idle Sessions provision automatically; initial provisioning has no caller connection action. Network defaults to enabled; disabled and restricted policies reject before compute allocation because the current Runtime cannot enforce them. The x_agents_core.environment extension accepts common preparation fields for either hosted or self-hosted placement: environment_template_id, files, env, packages, setup_commands, skills, plugins and capability_directories. Duplicate fields in environment and the extension reject. Confidential env, npm/Python packages and ordered setup commands use the same Environment-owned initialization lifecycle; compute allocation does not own preparation. Unknown side effects are not replayed after disconnect or restart. System dependencies must be preinstalled in the sandbox image or template, or on the host machine; packages.system is rejected. Initial inline and tenant-owned file_id files freeze encrypted bytes before provisioning, then install through the common Core lifecycle before native execution or live Files access. With a template reference, omitted/null files, env, packages and setup_commands inherit. Non-null files and command lists replace; env overlays by key; each package manager inherits on omission/null and otherwise replaces its list. Empty lists clear their selected field. Tenant-owned environment_template_id references inherit omitted/null network and allow only narrowing overrides. Inline hosted network:null retains the enabled default; updating a Template with network:null resets its saved policy to enabled. Core freezes effective configuration; template updates/deletion do not alter Session snapshots or same-intent creation retries. Inline or tenant-owned skill_reference Skills share initialization. Templates preserve default/latest/explicit selectors; Session creation freezes concrete metadata and encrypted content atomically. Skill, Plugin and capability-directory list omission/null inherit; a non-null list replaces, including empty-list clearing. Omitted/null Skill version selectors resolve the default version. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified single-agent Claude environment:none function profile, including qualified inline image messages and text results. Explicit web_search mode disabled and programmatic_tool_calling enabled false use frozen common Runtime controls. Enabled forms, including those saved on an Agent, remain unqualified and reject before any write unless the Session replaces tools. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage. +// @Description The optional Core model_provider bundle resolves from the Session override, saved Agent defaults, then, for openai_hosted and none, the deployment default of the resolved harness; self_hosted never uses the deployment default and none accepts only it. openai_hosted and self_hosted Sessions that resolve no bundle return 400 model_provider_required with param x_agents_core.model_provider before any write. Core encrypts and freezes the resolved bundle; later Agent or deployment default edits and same-key retries cannot change it. Keys are never returned. Supports inline configuration or a tenant-owned saved agent_id with per-Session field replacements. Execution supports model/instructions, text verbosity, non-deferred function tools, adapter-qualified multi_agent with persisted Subagent reads, implicit reasoning, service tier auto and environment type none, subject to the configured engine. Codex additionally supports HTTP MCP with service origin (omitted or null on HTTP transport is saved as service), native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer or OAuth credential for the exact HTTPS URL, while null/omission selects a unique match or remains anonymous. Session reads, lists and event snapshots show that implicitly selected credential ID in a null or omitted credential_id, also after the credential is deleted; anonymous selections stay null and the stored caller intent is unchanged. After the input requirement and before any write, a credential_id without vault_ids, one outside the attached Vaults (one message for missing, foreign and unattached IDs) or one for another server_url returns 400 invalid_request_error, and several implicit matches return 409 conflict_error. Missing decryption configuration fails dispatch without anonymous fallback. Required initialization uses native startup before the first native Turn, including cold resume, and requires a separately advertised capability; exact hosted creation timing and error parity remain unverified. Other MCP origins and native OAuth login remain unsupported. The self_hosted profile uses a qualified native harness, a clean absolute workspace_directory and optional absolute local capability_directories prepared by Runtime, with optional non-deferred function tools and HTTP MCP using service origin, optionally authenticated by the attached Vault rules. Remote MCP and remote Bearer authentication each require separately advertised combination support; old peers cannot receive unsupported work. Omitted/null capability_directories use the empty-list default; self_hosted requires configured execution plus executor registry. Claude SDK currently requires medium verbosity and object-root function schemas. It supports anonymous or attached static-bearer service-origin HTTP MCP on none with boolean required and separately advertised MCP/bearer/required runtime support. Required servers must be connected before the first native input is released; pending or failed startup rejects execution. The shared Vault selection and immutable binding rules apply; unsupported native labels/tool names reject before persistence. An attached Vault with no matching credential may remain anonymous; missing keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null; non-string values and limit violations return invalid_request_error with a metadata or metadata. param. The inline agent uses the Agent create configuration validation with agent.-prefixed params, reported before the input requirement and saved-Agent lookup; saved configurations with conflicting tools or schema roots reject admission with the same errors, and execution limits keep unsupported_or_invalid_configuration. Hosted network policy rejections return invalid_request_error with a null param. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none and qualified managed or self_hosted workspace profiles also accept inline PNG/JPEG image content; other image combinations and remote URLs are unsupported. None initial input atomically starts a Turn; self_hosted initial input is reserved while returning its Environment connection target, with execution deferred to native readiness and Session failure on initial timeout. Initial input is required for none and for streamed creation outside self_hosted. Omitted/null input remains valid for non-streaming hosted and self_hosted creation. With stream=true, returns live Session events starting with the committed creation snapshot and closes right after the first agent.session.idle recorded when a Turn ends or an input reservation stops being pending, or any agent.session.failed, without sending later events. A creation that admitted nothing closes after the snapshot; a settlement that records no event closes after events up to the cursor read with a settled Session projection. Required actions keep it open; disconnect does not cancel execution. The GET events stream remains live-only. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key rotation. Saved-Agent retries and inline requests using Vault attachments or credential references retain caller intent independently of later resource changes; new hosted inline requests also freeze caller intent before deployment defaults resolve; unrelated non-hosted inline retries preserve resolved/default equivalences, and their resolved hash leaves out any deployment default. Provider keys enter retry hashes only as fingerprints keyed by the credential key. Unknown historical creators reject retries; known creators without recorded intent retain resolved-snapshot retry rules. These conflict policies are local and not verified hosted parity. A same-key stream=true retry of an existing creation returns 201 with no events and closes at once; retry with stream=false or use the GET events stream to recover. Claude SDK on none, Core-managed Docker openai_hosted and self_hosted supports qualified object-root json_schema output with medium verbosity, single-Agent execution and ordinary functions. Hosted execution reuses native workspace tools and Files/Artifacts; Skills, Plugins, capability directories, HTTP MCP, Subagent and tool_search combinations remain unqualified, including inherited template contents. Other non-text initial input remains unsupported. Basic Codex and Claude SDK openai_hosted creation requires an explicitly configured managed provider. The Claude workspace profile supports non-deferred function tools with text or successful inline PNG/JPEG results alongside native workspace tools; HTTP MCP remains unsupported. Idle Sessions provision automatically; initial provisioning has no caller connection action. Network defaults to enabled; disabled and restricted policies reject before compute allocation because the current Runtime cannot enforce them. The x_agents_core.environment extension accepts common preparation fields for either hosted or self-hosted placement: environment_template_id, files, env, packages, setup_commands, skills, plugins and capability_directories. Duplicate fields in environment and the extension reject. Confidential env, npm/Python packages and ordered setup commands use the same Environment-owned initialization lifecycle; compute allocation does not own preparation. Unknown side effects are not replayed after disconnect or restart. System dependencies must be preinstalled in the sandbox image or template, or on the host machine; packages.system is rejected. Initial inline and tenant-owned file_id files freeze encrypted bytes before provisioning, then install through the common Core lifecycle before native execution or live Files access. With a template reference, omitted/null files, env, packages and setup_commands inherit. Non-null files and command lists replace; env overlays by key; each package manager inherits on omission/null and otherwise replaces its list. Empty lists clear their selected field. Tenant-owned environment_template_id references inherit omitted/null network and allow only narrowing overrides. Inline hosted network:null retains the enabled default; updating a Template with network:null resets its saved policy to enabled. Core freezes effective configuration; template updates/deletion do not alter Session snapshots or same-intent creation retries. Inline or tenant-owned skill_reference Skills share initialization. Templates preserve default/latest/explicit selectors; Session creation freezes concrete metadata and encrypted content atomically. Skill, Plugin and capability-directory list omission/null inherit; a non-null list replaces, including empty-list clearing. Omitted/null Skill version selectors resolve the default version. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified single-agent Claude function profile on none or a managed/user-owned workspace, including qualified inline image messages and text results. Explicit web_search mode disabled and programmatic_tool_calling enabled false use frozen common Runtime controls. Enabled forms, including those saved on an Agent, remain unqualified and reject before any write unless the Session replaces tools. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage. // @Tags Sessions // @Accept json // @Produce json,text/event-stream diff --git a/services/agents-api/internal/api/inputs.go b/services/agents-api/internal/api/inputs.go index c600a31c0..4dc98a6cc 100644 --- a/services/agents-api/internal/api/inputs.go +++ b/services/agents-api/internal/api/inputs.go @@ -24,7 +24,7 @@ type Option func(*Handler) func WithExecution(s InputSubmitter) Option { return func(h *Handler) { h.inputs = s } } // @Summary Submit Session input events -// @Description An empty events array is a resource-authorized no-op; it creates no execution retry identity, Turn, Item or input receipt. For environment none, atomically accepts text messages, cancellation and function results. Messages steer active work or start a queued Turn. The supported self_hosted profile accepts text-only messages; qualified Codex and Claude SDK openai_hosted profiles also accept inline PNG/JPEG. Under the Session lock, matching retries retain their original target; new active messages append to the current Turn, while idle messages reserve work and wait up to the original five-minute connection/admission deadline. Return 202 only after durable admission, without claiming native application; active messages create no Turn or reservation. Cancellation-only prepared-environment batches use existing durable cancellation admission and return 202 without waiting for native exit; a new cancellation conflicts while a pre-Turn reservation is pending. Homogeneous tool_result-only prepared-environment batches reuse existing scoped result admission and application receipts without creating a Turn or bypassing a pending reservation. Mixed prepared-environment batches remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled errors. New input on a Session whose hosted Environment failed to provision returns the observed 409 conflict_error "the hosted environment failed to provision"; input already waiting when it fails and expired Environments keep the local 409 environment_unavailable. Input the Session cannot accept in its current state, such as a result after cancellation or a batch while earlier input is pending, and a result that differs from the call's saved result return 409 with type and code conflict_error; reusing an Idempotency-Key with a different batch returns the local 409 idempotency_conflict. Inside an owned Session, a result for an unknown call or for a call of another Turn returns 400 invalid_request_error and changes nothing; missing and foreign Sessions return 404. Losing execution ownership returns 503. The response write deadline accommodates the admission window for either prepared Environment, independently of new-hosted-admission and executor URL settings. Disconnecting the waiting HTTP request does not cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text results and, on none and qualified openai_hosted, successful inline PNG/JPEG results, preserving ordered content; error images and remote references reject before admission. Native image resizing may change bytes. Runtime image-result support is checked only for image-bearing delivery. Codex and Claude SDK on none and qualified openai_hosted accept ordered inline PNG/JPEG image messages. Self-hosted profiles and other engines remain text-only; remote image URLs are unsupported. Image references are retained unchanged without service-side downloads. +// @Description An empty events array is a resource-authorized no-op; it creates no execution retry identity, Turn, Item or input receipt. For environment none, atomically accepts text messages, cancellation and function results. Messages steer active work or start a queued Turn. Qualified Codex and Claude SDK workspace profiles accept text and inline PNG/JPEG messages, independently of managed or self_hosted ownership. Under the Session lock, matching retries retain their original target; new active messages append to the current Turn, while idle messages reserve work and wait up to the original five-minute connection/admission deadline. Return 202 only after durable admission, without claiming native application; active messages create no Turn or reservation. Cancellation-only prepared-environment batches use existing durable cancellation admission and return 202 without waiting for native exit; a new cancellation conflicts while a pre-Turn reservation is pending. Homogeneous tool_result-only prepared-environment batches reuse existing scoped result admission and application receipts without creating a Turn or bypassing a pending reservation. Mixed prepared-environment batches remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled errors. New input on a Session whose hosted Environment failed to provision returns the observed 409 conflict_error "the hosted environment failed to provision"; input already waiting when it fails and expired Environments keep the local 409 environment_unavailable. Input the Session cannot accept in its current state, such as a result after cancellation or a batch while earlier input is pending, and a result that differs from the call's saved result return 409 with type and code conflict_error; reusing an Idempotency-Key with a different batch returns the local 409 idempotency_conflict. Inside an owned Session, a result for an unknown call or for a call of another Turn returns 400 invalid_request_error and changes nothing; missing and foreign Sessions return 404. Losing execution ownership returns 503. The response write deadline accommodates the admission window for either prepared Environment, independently of new-hosted-admission and executor URL settings. Disconnecting the waiting HTTP request does not cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text results and, on none and qualified workspace profiles, successful inline PNG/JPEG results, preserving ordered content; error images and remote references reject before admission. Native image resizing may change bytes. Runtime image-result support is checked only for image-bearing delivery. Codex and Claude SDK on none and qualified managed or self_hosted workspace profiles accept ordered inline PNG/JPEG image messages. Other engines remain text-only; remote image URLs are unsupported. Image references are retained unchanged without service-side downloads. // @Tags Sessions // @Accept json // @Security BearerAuth diff --git a/services/agents-api/internal/db/queries/sandbox_reset.sql b/services/agents-api/internal/db/queries/sandbox_reset.sql index 9fe0be78f..2ca1e9af6 100644 --- a/services/agents-api/internal/db/queries/sandbox_reset.sql +++ b/services/agents-api/internal/db/queries/sandbox_reset.sql @@ -22,7 +22,6 @@ UPDATE runtime_deployment SET provider_kind = '', backend_fingerprint = '', mode specification = '{}', idle_seconds = 0, retention_seconds = 0, e2b_template = '', e2b_credential = NULL, e2b_api_url = '', e2b_domain = '', e2b_template_build_status = NULL, e2b_template_cpus = NULL, e2b_template_memory_mib = NULL, e2b_template_root_disk_mib = NULL, - e2b_api_url = '', e2b_domain = '', generation = generation + 1, owner_epoch = owner_epoch + 1, admission_paused = false, reset_clear = NULL, reset_requested_at = NULL, reset_deadline_at = NULL, reset_forced_at = NULL, reset_audit = NULL, diff --git a/services/agents-api/internal/db/sqlc/sandbox_reset.sql.go b/services/agents-api/internal/db/sqlc/sandbox_reset.sql.go index e0bee807b..2f63a4933 100644 --- a/services/agents-api/internal/db/sqlc/sandbox_reset.sql.go +++ b/services/agents-api/internal/db/sqlc/sandbox_reset.sql.go @@ -28,7 +28,6 @@ UPDATE runtime_deployment SET provider_kind = '', backend_fingerprint = '', mode specification = '{}', idle_seconds = 0, retention_seconds = 0, e2b_template = '', e2b_credential = NULL, e2b_api_url = '', e2b_domain = '', e2b_template_build_status = NULL, e2b_template_cpus = NULL, e2b_template_memory_mib = NULL, e2b_template_root_disk_mib = NULL, - e2b_api_url = '', e2b_domain = '', generation = generation + 1, owner_epoch = owner_epoch + 1, admission_paused = false, reset_clear = NULL, reset_requested_at = NULL, reset_deadline_at = NULL, reset_forced_at = NULL, reset_audit = NULL, diff --git a/services/agents-api/internal/engine/claude.go b/services/agents-api/internal/engine/claude.go index 4614f8fd8..876eea4ff 100644 --- a/services/agents-api/internal/engine/claude.go +++ b/services/agents-api/internal/engine/claude.go @@ -16,13 +16,13 @@ func claudeProfile() Profile { ProgrammaticToolCallingDisable: true, StructuredOutput: true, ToolSearch: true, - MessageImagePlacements: []string{"none", "openai_hosted"}, + MessageImages: true, Placements: []string{"none", "openai_hosted", "self_hosted"}, MCPBearer: true, ValidateConfiguration: validateClaudeConfiguration, ValidateTools: validateClaudeTools, - ValidateFunctionResult: func(placement string, result proto.FunctionResultPayload) error { + ValidateFunctionResult: func(_ string, result proto.FunctionResultPayload) error { for _, part := range result.Content { - if part.Type == "input_image" && (!result.Success || (placement != "none" && placement != "openai_hosted")) { + if part.Type == "input_image" && !result.Success { return ErrInvalidInput } } @@ -48,8 +48,8 @@ func validateClaudeConfiguration(agent v1.Agent, environment *v1.Environment, ha if err := proto.ValidateBinary64Schema(agent.Text.Format.Schema); err != nil { return err } - if (environment.Type != "none" && environment.Type != "openai_hosted") || agent.MultiAgent.Enabled { - return errors.New("Structured output requires a qualified single-agent placement.") + if agent.MultiAgent.Enabled { + return errors.New("Structured output requires a single Agent.") } if len(environment.Skills) != 0 || len(environment.Plugins) != 0 || len(environment.CapabilityDirectories) != 0 { return errors.New("Structured output with environment Skills or Plugins is not qualified.") @@ -76,8 +76,8 @@ func validateClaudeConfiguration(agent v1.Agent, environment *v1.Environment, ha search = search || tool.Type == "tool_search" otherTools = otherTools || (tool.Type != "function" && tool.Type != "tool_search" && tool.Type != "web_search" && tool.Type != "programmatic_tool_calling") } - if search && (environment.Type != "none" || agent.MultiAgent.Enabled || otherTools || agent.Text.Format.Type == "json_schema") { - return errors.New("Tool discovery currently requires a single-agent environment:none function profile.") + if search && (agent.MultiAgent.Enabled || otherTools || len(environment.Skills) != 0 || len(environment.Plugins) != 0 || len(environment.CapabilityDirectories) != 0 || agent.Text.Format.Type == "json_schema") { + return errors.New("Tool discovery requires a single-agent function profile without Skills or Plugins.") } return rejectSubagentTools(agent, "function", "mcp") } diff --git a/services/agents-api/internal/engine/codex.go b/services/agents-api/internal/engine/codex.go index 9093445af..6e767264d 100644 --- a/services/agents-api/internal/engine/codex.go +++ b/services/agents-api/internal/engine/codex.go @@ -11,7 +11,7 @@ func codexProfile() Profile { return Profile{ ProgrammaticToolCallingDisable: true, Placements: []string{"none", "self_hosted", "openai_hosted"}, - MessageImagePlacements: []string{"none", "openai_hosted"}, + MessageImages: true, WhitespaceOnlyText: true, WebSearchControl: true, TextVerbosity: true, MCPBearer: true, ValidateConfiguration: func(agent v1.Agent, _ *v1.Environment, _ bool) error { diff --git a/services/agents-api/internal/engine/profile.go b/services/agents-api/internal/engine/profile.go index 0ea10ca40..1e9ad9fb2 100644 --- a/services/agents-api/internal/engine/profile.go +++ b/services/agents-api/internal/engine/profile.go @@ -18,7 +18,7 @@ type Profile struct { WebSearchControl, TextVerbosity, MCPBearer bool StructuredOutput bool ToolSearch bool - MessageImagePlacements []string + MessageImages bool ValidateConfiguration func(agent v1.Agent, environment *v1.Environment, hasDaemon bool) error ValidateTools func(environment *v1.Environment, hasDaemon bool, functions []proto.FunctionTool, mcp []proto.MCPHTTPServer) error ValidateFunctionResult func(placement string, result proto.FunctionResultPayload) error @@ -42,7 +42,6 @@ func NewCatalog(profiles map[string]Profile) Catalog { c := Catalog{profiles: make(map[string]Profile, len(profiles))} for kind, profile := range profiles { profile.Placements = slices.Clone(profile.Placements) - profile.MessageImagePlacements = slices.Clone(profile.MessageImagePlacements) c.profiles[kind] = profile } return c @@ -54,7 +53,6 @@ func (c Catalog) Lookup(kind string) (Profile, bool) { } profile, ok := c.profiles[kind] profile.Placements = slices.Clone(profile.Placements) - profile.MessageImagePlacements = slices.Clone(profile.MessageImagePlacements) return profile, ok } diff --git a/services/agents-api/internal/execution/environment_capability_profiles_test.go b/services/agents-api/internal/execution/environment_capability_profiles_test.go new file mode 100644 index 000000000..96fef1b16 --- /dev/null +++ b/services/agents-api/internal/execution/environment_capability_profiles_test.go @@ -0,0 +1,34 @@ +package execution + +import ( + "encoding/json" + "testing" +) + +func TestQualifiedClaudeCapabilitiesDoNotDependOnEnvironmentSource(t *testing.T) { + for _, placement := range []string{"none", "openai_hosted", "self_hosted"} { + t.Run(placement, func(t *testing.T) { + for _, feature := range []string{"structured", "discovery"} { + agent := map[string]any{"model": "fixture", "multi_agent": map[string]bool{"enabled": false}} + if feature == "structured" { + agent["text"] = map[string]any{"format": map[string]any{"type": "json_schema", "name": "answer", "strict": true, "schema": map[string]any{"type": "object", "properties": map[string]any{}, "additionalProperties": false}}} + } else { + agent["tools"] = []any{map[string]any{"type": "tool_search"}, map[string]any{"type": "function", "name": "lookup", "parameters": map[string]any{"type": "object"}, "defer_loading": true}} + } + environment := map[string]any{"type": placement} + if placement == "self_hosted" { + environment["workspace_directory"] = "/workspace" + } + raw, _ := json.Marshal(map[string]any{"agent": agent, "environment": environment}) + if err := (Policy{}).ValidateSessionConfiguration("claude_sdk", raw); err != nil { + t.Fatalf("%s: %v", feature, err) + } + agent["multi_agent"] = map[string]bool{"enabled": true} + raw, _ = json.Marshal(map[string]any{"agent": agent, "environment": environment}) + if err := (Policy{}).ValidateSessionConfiguration("claude_sdk", raw); err == nil { + t.Fatalf("%s with Subagents accepted", feature) + } + } + }) + } +} diff --git a/services/agents-api/internal/execution/function_images_test.go b/services/agents-api/internal/execution/function_images_test.go index 6512c9b15..7d0956b96 100644 --- a/services/agents-api/internal/execution/function_images_test.go +++ b/services/agents-api/internal/execution/function_images_test.go @@ -35,8 +35,8 @@ func TestFunctionImageAdmission(t *testing.T) { } for _, placement := range []string{"self_hosted"} { url := image - if err := profile.ValidateFunctionResult(placement, proto.FunctionResultPayload{Success: true, Content: []proto.InputContent{{Type: "input_image", ImageURL: &url}}}); !errors.Is(err, engine.ErrInvalidInput) { - t.Fatal("unqualified image placement", placement, err) + if err := profile.ValidateFunctionResult(placement, proto.FunctionResultPayload{Success: true, Content: []proto.InputContent{{Type: "input_image", ImageURL: &url}}}); err != nil { + t.Fatal("qualified image rejected", placement, err) } if err := profile.ValidateFunctionResult(placement, proto.FunctionResultPayload{Success: true, Content: proto.TextInput("text")[0].Content}); err != nil { t.Fatal("workspace text regressed", err) diff --git a/services/agents-api/internal/execution/message_support.go b/services/agents-api/internal/execution/message_support.go index 5e8413ad4..7a855464b 100644 --- a/services/agents-api/internal/execution/message_support.go +++ b/services/agents-api/internal/execution/message_support.go @@ -2,7 +2,6 @@ package execution import ( "errors" - "slices" "strings" "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/gateway" @@ -47,11 +46,11 @@ func blankTextRune(r rune) bool { return r >= '\u2000' && r <= '\u200a' } -func validateMessageImageProfile(profile engine.Profile, placement string, input proto.MessageInput) error { +func validateMessageImageProfile(profile engine.Profile, _ string, input proto.MessageInput) error { if !input.HasImages() { return nil } - if !slices.Contains(profile.MessageImagePlacements, placement) || input.ValidateInlineImages() != nil { + if !profile.MessageImages || input.ValidateInlineImages() != nil { return store.ErrInvalidInput } return nil diff --git a/services/agents-api/internal/execution/message_support_test.go b/services/agents-api/internal/execution/message_support_test.go index 072ad22df..72e01609c 100644 --- a/services/agents-api/internal/execution/message_support_test.go +++ b/services/agents-api/internal/execution/message_support_test.go @@ -13,12 +13,12 @@ import ( func TestMessageImageQualificationIsOperationSpecific(t *testing.T) { url := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXioAAAAASUVORK5CYII=" input := proto.MessageInput{{Content: []proto.InputContent{{Type: "input_image", ImageURL: &url}}}} - profile := engine.Profile{MessageImagePlacements: []string{"none"}} + profile := engine.Profile{MessageImages: true} if err := validateMessageImageProfile(profile, "none", input); err != nil { t.Fatal(err) } - if err := validateMessageImageProfile(profile, "self_hosted", input); !errors.Is(err, store.ErrInvalidInput) { - t.Fatal("unqualified placement accepted", err) + if err := validateMessageImageProfile(profile, "self_hosted", input); err != nil { + t.Fatal("qualified user machine rejected", err) } if err := validateMessageImageProfile(engine.Profile{}, "none", input); !errors.Is(err, store.ErrInvalidInput) { t.Fatal("unqualified profile accepted", err) diff --git a/services/agents-api/internal/execution/workspace_images_test.go b/services/agents-api/internal/execution/workspace_images_test.go index d6f749738..9ea797f9a 100644 --- a/services/agents-api/internal/execution/workspace_images_test.go +++ b/services/agents-api/internal/execution/workspace_images_test.go @@ -9,7 +9,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" ) -func TestHostedImageQualification(t *testing.T) { +func TestImageQualificationIsIndependentOfEnvironmentSource(t *testing.T) { image := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXioAAAAASUVORK5CYII=" message := store.Input{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"inspect"},{"type":"input_image","image_url":"` + image + `"}]}]}`)} result := store.Input{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":[{"type":"input_image","image_url":"` + image + `"}]}}`)} @@ -17,7 +17,7 @@ func TestHostedImageQualification(t *testing.T) { profile, _ := (engine.Catalog{}).Lookup(kind) for _, placement := range []string{"none", "openai_hosted", "self_hosted"} { err := validateProfileInputs(profile, placement, []store.Input{message}) - want := kind != "mcode" && placement != "self_hosted" + want := kind != "mcode" if (err == nil) != want { t.Fatalf("%s/%s: %v", kind, placement, err) } diff --git a/services/agents-api/internal/store/message_image_admission_test.go b/services/agents-api/internal/store/message_image_admission_test.go index 660bdb066..bb2af240b 100644 --- a/services/agents-api/internal/store/message_image_admission_test.go +++ b/services/agents-api/internal/store/message_image_admission_test.go @@ -25,7 +25,7 @@ func TestUnqualifiedImageAdmissionIsAtomic(t *testing.T) { // A registered text-only profile must stay closed regardless of the // adapters currently qualified by the built-in catalog. profile, _ := (engine.Catalog{}).Lookup("codex") - profile.MessageImagePlacements = nil + profile.MessageImages = false h.d.Policy = execution.Policy{Engines: engine.NewCatalog(map[string]engine.Profile{"codex": profile})} worker, err := execution.StartWorker(t.Context(), h.d) if err != nil {