From 64004bf8a2bdb40df3094e08b49f43e9c4070356 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 20:17:33 +0000 Subject: [PATCH] Fix stdio MCP and token limit review findings --- .../daemon/internal/agent/codex/mcp_config.go | 5 ++-- .../features/system/ModelProviderDialog.tsx | 4 +-- contracts/agents-api/execution-tools.md | 6 ++-- contracts/agents-api/index.md | 2 +- contracts/agents-api/v1/model_execution.go | 2 +- contracts/agents-api/zh/execution-tools.md | 8 ++--- contracts/agents-api/zh/index.md | 4 +-- .../harnessconfig/claudesdk/declaration.go | 16 ++++++++++ .../claudesdk/declaration_test.go | 12 ++++++++ internal/harnessconfig/harness.go | 2 ++ internal/modelprovider/config.go | 8 ++++- .../src/execution-configuration-projection.ts | 7 ++++- packages/agents-client/src/index.ts | 1 + .../agents-client/src/protocol-types.test.ts | 6 ++-- packages/agents-client/src/types.ts | 29 ++++++------------- packages/claude-sdk-adapter/src/mcp.ts | 6 +++- .../tests/mcp_workspace.test.mjs | 9 +++++- .../execution/environment_placement.go | 4 ++- .../internal/execution/harness_admission.go | 13 ++++++--- services/core/internal/execution/mcp.go | 5 +++- 20 files changed, 101 insertions(+), 48 deletions(-) diff --git a/apps/daemon/internal/agent/codex/mcp_config.go b/apps/daemon/internal/agent/codex/mcp_config.go index 2fcc05120..27133314c 100644 --- a/apps/daemon/internal/agent/codex/mcp_config.go +++ b/apps/daemon/internal/agent/codex/mcp_config.go @@ -122,7 +122,8 @@ func writeMCPHeaderMap(b *strings.Builder, field string, values map[string]strin // tomlQuoteString returns a TOML basic-string literal (double-quoted) // with the documented escape set applied — \" \\ \n \r \t plus -// \uXXXX for control chars. Matches the TOML 1.0 spec for basic strings. +// \uXXXX for the other control characters, DEL included. Matches the TOML 1.0 +// spec for basic strings. func tomlQuoteString(s string) string { var b strings.Builder b.WriteByte('"') @@ -139,7 +140,7 @@ func tomlQuoteString(s string) string { case '\t': b.WriteString(`\t`) default: - if r < 0x20 { + if r < 0x20 || r == 0x7f { fmt.Fprintf(&b, `\u%04X`, r) } else { b.WriteRune(r) diff --git a/apps/web/src/features/system/ModelProviderDialog.tsx b/apps/web/src/features/system/ModelProviderDialog.tsx index 6ff1a410c..b62a876ff 100644 --- a/apps/web/src/features/system/ModelProviderDialog.tsx +++ b/apps/web/src/features/system/ModelProviderDialog.tsx @@ -1,4 +1,4 @@ -import { AgentCoreError, type CoreHarness, type CoreHarnessKind, type ModelProviderInput } from "@oac/agents-client"; +import { AgentCoreError, type CoreHarness, type CoreHarnessKind, type ModelProviderInput, tokenLimitsFit } from "@oac/agents-client"; import { useId, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ConsoleSelect } from "../../components/console-select"; @@ -87,7 +87,7 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: { const limitProblem = (limit: ReturnType) => (limit.problem === "whole" ? t("models.form.wholeNumber") : limit.problem === "large" ? t("models.form.tooLarge") : null); const outputProblem = limitProblem(output); // An undersized window is the field the administrator must fix. - const contextProblem = limitProblem(context) ?? (!outputProblem && output.value !== undefined && context.value !== undefined && output.value > context.value ? t("models.form.needsContext") : null); + const contextProblem = limitProblem(context) ?? (!outputProblem && !tokenLimitsFit(context.value, output.value) ? t("models.form.needsContext") : null); const ready = harness !== null && !busy && url !== "" && !urlProblem && apiKey.trim() !== "" && model.trim() !== "" && nativeConfig !== null && !configTooLarge && !protocolProblem && !contextProblem && !outputProblem; function clearNativeConfig() { diff --git a/contracts/agents-api/execution-tools.md b/contracts/agents-api/execution-tools.md index 36f4d62b0..bc955df4e 100644 --- a/contracts/agents-api/execution-tools.md +++ b/contracts/agents-api/execution-tools.md @@ -100,8 +100,8 @@ Execution carries the resolved values to the Harness and validates them against A `stdio` server carries `command`, `args`, `cwd` and `env_vars`, runs as a process of the Environment and needs a workspace placement (`openai_hosted` or `self_hosted`); on `environment: none` admission returns a 400. Its `connection_origin` defaults to and must be `environment`, and it takes no `credential_id`. A nonempty Session `env` is rejected with "Inline MCP env is not supported; declare env_vars and set the values in the Environment."; `command` is nonempty, `cwd` is an absolute path, `env_vars` holds variable names and no part contains NUL. `env_vars` names variables of the Environment, whose values never reach Core; as for [Plugin MCP](./environments.md#plugin-mcp), the start fails when the Environment does not set one. The Runtime must advertise `mcp_http_tools`, which covers every public MCP server. - Codex writes `command`, `args`, `cwd` and `env_vars` to its `[mcp_servers]` table and verifies them with the rest of the declared configuration before any native connection. -- Claude Code starts the server with `command` and `args` in the directory of its own process, which is the workspace directory, so the Runtime rejects any other `cwd`. Core rejects non-null `allowed_tools` for a stdio server, because the adapter can hide unselected tools only on HTTP servers. +- Claude Code starts the server with `command` and `args` in the directory of its own process, which is the workspace directory, so admission rejects any other `cwd` with "The configured engine starts MCP stdio servers in the workspace directory; set cwd to it." Core rejects non-null `allowed_tools` for a stdio server, because the adapter can hide unselected tools only on HTTP servers. - MiniMax Code starts session stdio servers in its private home directory, which no `cwd` can name, so admission rejects every stdio server with "The configured engine cannot start MCP stdio servers in the declared cwd." -- Codex passes `env_vars` to its restricted native environment. Claude Code starts the server with its whole process environment, so it inherits more than `env_vars` lists. +- Codex passes `env_vars` to its restricted native environment. Claude Code starts the server with its whole process environment, as it starts Bash, so the server also sees the model provider key and the Vault bearer tokens of other MCP servers, and a variable the adapter sets, such as `HOME`, keeps the adapter's value. -Codex trusts the workspace natively, so its project configuration loads on start and cold resume, and undeclared servers, apps and plugins keep their native behavior. Before any native connection that carries a Core credential, it verifies that each declared server keeps its URL or command, bearer reference and headers, and fails the start or resume with an error otherwise. It rejects reserved native labels and stored native MCP credentials. Claude accepts labels of ASCII letters, digits, underscore and hyphen except `functions`, any tool name (the native client normalizes it, and the adapter rejects normalized collisions), and requires connected servers with static inventories. MiniMax Code serves public functions as the MCP server `functions`, so an Agent with functions cannot use that label for a public or Plugin server. Anonymous Claude requests send a blank Authorization header to suppress native OAuth injection. Native OAuth login is not supported. +Codex trusts the workspace natively, so its project configuration loads on start and cold resume, and undeclared servers, apps and plugins keep their native behavior. Before any native connection that carries a Core credential, it verifies that each declared server keeps its URL or command, bearer reference and headers, and fails the start or resume with an error otherwise. It rejects reserved native labels and stored native MCP credentials. Claude accepts labels of ASCII letters, digits, underscore and hyphen except `functions`, any tool name (the native client normalizes it, and the adapter rejects normalized collisions), and requires static inventories. As locally, Claude runs on without the tools of an optional server that did not connect; a required one fails the start. MiniMax Code serves public functions as the MCP server `functions`, so an Agent with functions cannot use that label for a public or Plugin server. Anonymous Claude requests send a blank Authorization header to suppress native OAuth injection. Native OAuth login is not supported. diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index 2db7a58d8..e6447d1c2 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -78,7 +78,7 @@ Each item is Core's deliberate or native behavior where the official service beh - Codex maps programmatic tool calling to its native `code_mode`, whose model-written scripts call the other tools; the feature is under development upstream. - A Session's `agent.tools` omits `tool_search` declarations. - Core rejects an inline `authorization` on a Session MCP transport, an `Authorization` entry in `headers` and a nonempty stdio `env`: credentials come only from Vaults (`credential_id`) and environment values only from the Environment (`env_vars`) ([MCP](./execution-tools.md#mcp)). -- A `stdio` MCP server needs a workspace placement and the origin `environment`. Claude Code starts the server with its whole process environment, which holds more than `env_vars` lists, in its workspace directory, so `cwd` must equal it, and it needs `allowed_tools` null. MiniMax Code starts the server in its private home directory, so it rejects every `stdio` server ([MCP](./execution-tools.md#mcp)). +- A `stdio` MCP server needs a workspace placement and the origin `environment`. Claude Code starts the server in its workspace directory, so `cwd` must equal it, with its whole process environment, which also holds the model provider key and other servers' Vault bearer tokens, and it needs `allowed_tools` null. MiniMax Code starts the server in its private home directory, so it rejects every `stdio` server ([MCP](./execution-tools.md#mcp)). - Claude rejects an MCP `server_url` or `headers` value containing `${`, because the native client expands `${VAR}` in both from a process environment that holds Core-held credentials. - Deleting a Session right after an events 202 returns 409, because Core admits the Turn in the same transaction; the official service returned 200. diff --git a/contracts/agents-api/v1/model_execution.go b/contracts/agents-api/v1/model_execution.go index 8cc80a5a2..e193af040 100644 --- a/contracts/agents-api/v1/model_execution.go +++ b/contracts/agents-api/v1/model_execution.go @@ -51,7 +51,7 @@ func (p *ModelProviderInput) validate(registry harnessconfig.Registry) error { if strings.TrimSpace(p.APIKey) == "" || len(p.APIKey) > 16384 || strings.ContainsAny(p.APIKey, "\x00\r\n") { return &ModelProviderError{Code: "model_provider_api_key_invalid", Param: "api_key", message: "invalid model provider API key"} } - if p.ContextWindow < 0 || p.MaxOutputTokens < 0 || (p.ContextWindow > 0 && p.MaxOutputTokens > p.ContextWindow) { + if !modelprovider.TokenLimitsValid(p.ContextWindow, p.MaxOutputTokens) { param := "max_output_tokens" if p.ContextWindow < 0 { param = "context_window" diff --git a/contracts/agents-api/zh/execution-tools.md b/contracts/agents-api/zh/execution-tools.md index ee74aaa43..8b5430064 100644 --- a/contracts/agents-api/zh/execution-tools.md +++ b/contracts/agents-api/zh/execution-tools.md @@ -1,7 +1,7 @@ --- title: "执行工具" source: contracts/agents-api/execution-tools.md -source_hash: 6956ecfe568c551e3161eb1fc982d412741846eaa56341ad58ec6511df229df8 +source_hash: d88acdb94153f3974e66adbdb9443706ae47adef280cae71b03fdf896bdfe579 --- Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在 `text.format` 中声明输出 schema。本契约说明 Core 如何验证声明、哪些内容跨越 Runtime 边界,以及调用方如何恢复待执行操作。[Harness 能力](harness-capabilities.md)列出各 Harness 在不同部署位置支持的操作。原生工作区工具和 Environment Plugin MCP 属于 [Environment](environments.md#skills-plugins-and-environment-mcp)。 @@ -102,8 +102,8 @@ Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoadi `stdio` 服务器携带 `command`、`args`、`cwd` 和 `env_vars`,作为 Environment 的进程运行,需要工作区部署位置(`openai_hosted` 或 `self_hosted`);在 `environment: none` 上,准入返回 400。其 `connection_origin` 默认且只能为 `environment`,不带 `credential_id`。非空的 Session `env` 被拒绝,消息为 "Inline MCP env is not supported; declare env_vars and set the values in the Environment.";`command` 非空,`cwd` 为绝对路径,`env_vars` 只含变量名,且任何部分都不含 NUL。`env_vars` 指定 Environment 中的变量名,其值不会到达 Core;与 [Plugin MCP](environments.md#plugin-mcp) 相同,Environment 未设置其中某个变量时启动失败。Runtime 必须声明 `mcp_http_tools`,它涵盖所有公开 MCP 服务器。 - Codex 把 `command`、`args`、`cwd` 和 `env_vars` 写入其 `[mcp_servers]` 表,并在任何原生连接之前与其余已声明配置一并验证。 -- Claude Code 以 `command` 和 `args` 在其自身进程所在目录(即工作区目录)启动服务器,因此 Runtime 拒绝其他 `cwd`。Core 拒绝 stdio 服务器的非 null `allowed_tools`,因为适配器只能在 HTTP 服务器上隐藏未选工具。 +- Claude Code 以 `command` 和 `args` 在其自身进程所在目录(即工作区目录)启动服务器,因此准入以 "The configured engine starts MCP stdio servers in the workspace directory; set cwd to it." 拒绝其他 `cwd`。Core 拒绝 stdio 服务器的非 null `allowed_tools`,因为适配器只能在 HTTP 服务器上隐藏未选工具。 - MiniMax Code 在其私有主目录中启动会话 stdio 服务器,任何 `cwd` 都无法指向该目录,因此准入拒绝所有 stdio 服务器,消息为 "The configured engine cannot start MCP stdio servers in the declared cwd."。 -- Codex 把 `env_vars` 交给其受限的原生环境。Claude Code 以整个进程环境启动服务器,因此继承的变量多于 `env_vars` 所列。 +- Codex 把 `env_vars` 交给其受限的原生环境。Claude Code 像启动 Bash 一样以整个进程环境启动服务器,因此服务器也能读到模型提供方密钥和其他 MCP 服务器的 Vault bearer 令牌,适配器设置的变量(如 `HOME`)保持适配器的值。 -Codex 以原生方式信任工作区,因此工作区的项目配置在启动和冷恢复时加载,未声明的服务器、apps 和 plugins 保持原生行为。在任何携带 Core 凭据的原生连接之前,它验证每个已声明服务器保持其 URL 或命令、bearer 引用和请求头,否则启动或恢复以错误失败。它拒绝原生保留标签和已存储原生 MCP 凭据。Claude 接受 ASCII 字母、数字、下划线和连字符组成的标签,但不允许 `functions`;接受任意工具名称(原生客户端会将其规范化,适配器拒绝规范化后的冲突),并要求已连接服务器具有静态工具清单。MiniMax Code 以 MCP 服务器 `functions` 提供公共函数,因此声明了函数的 Agent 不能让公共或 Plugin 服务器使用该标签。匿名 Claude 请求发送空 Authorization 头以阻止原生 OAuth 注入。不支持原生 OAuth 登录。 +Codex 以原生方式信任工作区,因此工作区的项目配置在启动和冷恢复时加载,未声明的服务器、apps 和 plugins 保持原生行为。在任何携带 Core 凭据的原生连接之前,它验证每个已声明服务器保持其 URL 或命令、bearer 引用和请求头,否则启动或恢复以错误失败。它拒绝原生保留标签和已存储原生 MCP 凭据。Claude 接受 ASCII 字母、数字、下划线和连字符组成的标签,但不允许 `functions`;接受任意工具名称(原生客户端会将其规范化,适配器拒绝规范化后的冲突),并要求静态工具清单。与本地一致,可选服务器未连接时 Claude 不带其工具继续运行;必需服务器未连接则启动失败。MiniMax Code 以 MCP 服务器 `functions` 提供公共函数,因此声明了函数的 Agent 不能让公共或 Plugin 服务器使用该标签。匿名 Claude 请求发送空 Authorization 头以阻止原生 OAuth 注入。不支持原生 OAuth 登录。 diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index d0ecae444..4d984de38 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "OpenAI 兼容性" source: contracts/agents-api/index.md -source_hash: 1387a015bb94dc885cc6b9b2c9f6a55c9837438a59e86ce9ff371e161ace1499 +source_hash: fd77d3617bd8a124382a8d7e8c95adb824fc0da73c31fce2ff475c920e3b0626 --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -81,7 +81,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - Codex 把程序化工具调用映射到原生 `code_mode`,由模型编写的脚本调用其他工具;该功能在上游仍处于开发中。 - Session 的 `agent.tools` 会省略 `tool_search` 声明。 - Core 拒绝 Session MCP 传输中的内联 `authorization`、`headers` 中的 `Authorization` 条目以及非空的 stdio `env`:凭据只来自 Vault(`credential_id`),环境值只来自 Environment(`env_vars`)([MCP](execution-tools.md#mcp))。 -- `stdio` MCP 服务器需要工作区部署位置和 `environment` 来源。Claude Code 以其整个进程环境(多于 `env_vars` 所列)在其工作区目录中启动服务器,因此 `cwd` 必须与之相同,且 `allowed_tools` 须为 null。MiniMax Code 在其私有主目录中启动服务器,因此拒绝所有 `stdio` 服务器([MCP](execution-tools.md#mcp))。 +- `stdio` MCP 服务器需要工作区部署位置和 `environment` 来源。Claude Code 在其工作区目录中启动服务器,因此 `cwd` 必须与之相同;服务器继承整个进程环境,其中也有模型提供方密钥和其他服务器的 Vault bearer 令牌;且 `allowed_tools` 须为 null。MiniMax Code 在其私有主目录中启动服务器,因此拒绝所有 `stdio` 服务器([MCP](execution-tools.md#mcp))。 - Claude 会拒绝包含 `${` 的 MCP `server_url` 或 `headers` 值,因为原生客户端会从持有 Core 所持凭据的进程环境中展开这两处的 `${VAR}`。 - 在 events 202 之后立即删除 Session 会返回 409,因为 Core 会在同一事务中准入该 Turn;官方服务返回 200。 diff --git a/internal/harnessconfig/claudesdk/declaration.go b/internal/harnessconfig/claudesdk/declaration.go index 3be9e59f0..736f65b37 100644 --- a/internal/harnessconfig/claudesdk/declaration.go +++ b/internal/harnessconfig/claudesdk/declaration.go @@ -3,8 +3,10 @@ package claudesdk import ( "encoding/base64" + "encoding/json" "errors" "math" + "path/filepath" "regexp" "strings" @@ -84,6 +86,20 @@ func validateConfiguration(admission harnessconfig.Admission) error { return err } } + for _, raw := range admission.Tools { + var tool struct { + Type string `json:"type"` + Transport struct { + Type string `json:"type"` + Cwd string `json:"cwd"` + } `json:"transport"` + } + // Native starts stdio servers in the workspace and has no cwd setting. + if json.Unmarshal(raw, &tool) == nil && tool.Type == "mcp" && tool.Transport.Type == "stdio" && admission.WorkspaceDirectory != "" && + filepath.Clean(tool.Transport.Cwd) != filepath.Clean(admission.WorkspaceDirectory) { + return errors.New("The configured engine starts MCP stdio servers in the workspace directory; set cwd to it.") + } + } return nil } diff --git a/internal/harnessconfig/claudesdk/declaration_test.go b/internal/harnessconfig/claudesdk/declaration_test.go index ae532bb63..62f8b1a86 100644 --- a/internal/harnessconfig/claudesdk/declaration_test.go +++ b/internal/harnessconfig/claudesdk/declaration_test.go @@ -51,3 +51,15 @@ func TestDeclarationRejectsMCPEnvironmentExpansion(t *testing.T) { t.Fatal("URL with ${ admitted") } } + +func TestDeclarationRequiresWorkspaceStdioCwd(t *testing.T) { + stdio := func(cwd string) []json.RawMessage { + return []json.RawMessage{json.RawMessage(`{"type":"mcp","transport":{"type":"stdio","command":"x","cwd":"` + cwd + `"}}`)} + } + if err := Declaration.ValidateConfiguration(harnessconfig.Admission{WorkspaceDirectory: "/workspace", Tools: stdio("/workspace/")}); err != nil { + t.Fatal(err) + } + if err := Declaration.ValidateConfiguration(harnessconfig.Admission{WorkspaceDirectory: "/workspace", Tools: stdio("/srv")}); err == nil { + t.Fatal("stdio cwd outside the workspace admitted") + } +} diff --git a/internal/harnessconfig/harness.go b/internal/harnessconfig/harness.go index 18d3ecec8..17d0d2a98 100644 --- a/internal/harnessconfig/harness.go +++ b/internal/harnessconfig/harness.go @@ -309,6 +309,8 @@ type Admission struct { TextSchema json.RawMessage MultiAgent bool Tools []json.RawMessage + // WorkspaceDirectory is the Environment's workspace, empty without one. + WorkspaceDirectory string } // ValidateMCPOrigins preserves outbound authority independently of Harness names. diff --git a/internal/modelprovider/config.go b/internal/modelprovider/config.go index 7a8e34e10..95d10fcfd 100644 --- a/internal/modelprovider/config.go +++ b/internal/modelprovider/config.go @@ -70,8 +70,14 @@ func (p Provider) Validate() error { if strings.TrimSpace(p.APIKey) == "" || len(p.APIKey) > 16384 || strings.ContainsAny(p.APIKey, "\x00\r\n") { return ErrConfiguration } - if p.ContextWindow < 0 || p.MaxOutputTokens < 0 || (p.ContextWindow > 0 && p.MaxOutputTokens > p.ContextWindow) { + if !TokenLimitsValid(p.ContextWindow, p.MaxOutputTokens) { return ErrConfiguration } return nil } + +// TokenLimitsValid holds the token-limit rule: unset is zero, and an output +// limit never exceeds a set context window. +func TokenLimitsValid(contextWindow, maxOutputTokens int32) bool { + return contextWindow >= 0 && maxOutputTokens >= 0 && (contextWindow == 0 || maxOutputTokens <= contextWindow) +} diff --git a/packages/agents-client/src/execution-configuration-projection.ts b/packages/agents-client/src/execution-configuration-projection.ts index 7a5cf65e4..b78e69a3b 100644 --- a/packages/agents-client/src/execution-configuration-projection.ts +++ b/packages/agents-client/src/execution-configuration-projection.ts @@ -33,6 +33,11 @@ function safeBaseURL(value: string): boolean { return !authority.includes("@") && host !== "" && query === "" && fragment === ""; } +/** Core's token-limit rule: an output limit never exceeds a set context window. */ +export function tokenLimitsFit(contextWindow?: number, maxOutputTokens?: number): boolean { + return !contextWindow || (maxOutputTokens ?? 0) <= contextWindow; +} + /** The safe provider view shared by frozen Session configuration and saved Agent reads. */ export function safeProvider(value: unknown, invalid: Invalid): ModelProviderView { if (!isRecord(value) || !onlyFields(value, modelProviderViewFields) || @@ -40,7 +45,7 @@ export function safeProvider(value: unknown, invalid: Invalid): ModelProviderVie typeof value.base_url !== "string" || typeof value.api_key_configured !== "boolean" || (value.context_window !== undefined && !isNonnegativeInteger(value.context_window)) || (value.max_output_tokens !== undefined && !isNonnegativeInteger(value.max_output_tokens)) || - (Number(value.context_window ?? 0) > 0 && Number(value.max_output_tokens ?? 0) > Number(value.context_window))) return invalid(); + !tokenLimitsFit(value.context_window as number | undefined, value.max_output_tokens as number | undefined)) return invalid(); if (!safeBaseURL(value.base_url)) return invalid(); return { protocol: value.protocol as ModelProviderView["protocol"], base_url: value.base_url, api_key_configured: value.api_key_configured, diff --git a/packages/agents-client/src/index.ts b/packages/agents-client/src/index.ts index c2ca28622..1bd385590 100644 --- a/packages/agents-client/src/index.ts +++ b/packages/agents-client/src/index.ts @@ -10,6 +10,7 @@ export * from "./core-metrics"; export type { CoreClientOptions } from "./core-request"; export { isEnvironmentTemplateName, isRecognizedEnvironmentTemplate } from "./environment-template-projection"; export { isOpenAIHostedSessionEnvironment } from "./session-environment-projection"; +export { tokenLimitsFit } from "./execution-configuration-projection"; export { compareSkillVersionNumbers, isSkillId, isSkillUploadPath, isSkillVersionId, isSkillVersionNumber } from "./skill-projection"; export { AdminClient } from "./admin-client"; // Skill, SkillVersion and SessionArtifact come from ./types; the admin projections use the same shapes. diff --git a/packages/agents-client/src/protocol-types.test.ts b/packages/agents-client/src/protocol-types.test.ts index 39cc30384..a720a95bd 100644 --- a/packages/agents-client/src/protocol-types.test.ts +++ b/packages/agents-client/src/protocol-types.test.ts @@ -12,7 +12,7 @@ import turnResources from "./fixtures/parsar-0438880a/turn-resources.json"; import toolProfiles from "./fixtures/parsar-2b34ea46/tool-profiles.json"; import type { AnonymousHttpMcpToolInput, - ServiceHttpMcpToolInput, + McpToolInput, AgentsCoreSelection, SavedAgentCoreInput, SavedAgentCore, @@ -159,8 +159,8 @@ describe("Parsar 2b34ea46 bounded tool profiles", () => { }); it("accepts the minimal pinned MCP tool whose omitted origin Core saves as service", () => { - expectTypeOf().toEqualTypeOf<"service" | null | undefined>(); - const minimal: ServiceHttpMcpToolInput = { + expectTypeOf().toEqualTypeOf<"service" | "environment" | null | undefined>(); + const minimal: McpToolInput = { type: "mcp", server_label: "docs", transport: { type: "http", server_url: "https://mcp.example/tools" }, diff --git a/packages/agents-client/src/types.ts b/packages/agents-client/src/types.ts index 7a2c37b41..93f06d8e0 100644 --- a/packages/agents-client/src/types.ts +++ b/packages/agents-client/src/types.ts @@ -7,7 +7,7 @@ import type { EnvironmentParamSelfHosted, EnvironmentResourceSelfHosted, EnvironmentStatusResource, FunctionCallStatusResource, HostedEnvironmentFileParam, InputContentParam, InputContentParamInputText, InputMessageParam, ListOrderParam, McpOauthRefreshResource, MultiAgentConfigCurrentParam, MultiAgentConfigResource, NetworkAccessResource, - NetworkPolicyResource, PersistedAgentToolConfigParamFunction, PersistedAgentToolConfigParamProgrammaticToolCalling, + NetworkPolicyResource, PersistedAgentToolConfigParamFunction, PersistedAgentToolConfigParamMcp, PersistedAgentToolConfigParamProgrammaticToolCalling, PersistedAgentToolConfigParamToolSearch, PersistedAgentToolConfigParamWebSearch, ReasoningEffortResource, ReasoningParam, ReasoningResource, ReasoningSummaryResource, RotateVaultCredentialAuthParamStaticBearer, RotateVaultCredentialParams, ServiceTierResource, SessionEnvironmentStateResource, SessionEnvironmentStatusResource, @@ -58,25 +58,14 @@ export type MultiAgentInput = MultiAgentConfigCurrentParam; export type FunctionToolInput = PersistedAgentToolConfigParamFunction; -/** The service-origin HTTP MCP profile accepted by current Core. */ -export interface ServiceHttpMcpToolInput { - type: "mcp"; - server_label: string; - transport: { - type: "http"; - server_url: string; - }; - /** null or omitted permits every advertised tool; [] permits none. */ - allowed_tools?: string[] | null; - /** null or omitted is saved as "service", as the official service does. */ - connection_origin?: "service" | null; - /** Saving a reference does not authorize it; Session vault_ids must attach its owner. */ - credential_id?: string | null; - required?: boolean; -} +/** + * The persisted MCP profile, without inline secrets. Omitted `connection_origin` is saved + * as "service"; a `credential_id` reference is authorized only by a Session vault that owns it. + */ +export type McpToolInput = PersistedAgentToolConfigParamMcp; /** Anonymous is an explicit subset that cannot name a stored Credential. */ -export type AnonymousHttpMcpToolInput = Omit & { +export type AnonymousHttpMcpToolInput = Omit & { credential_id?: null; }; @@ -96,13 +85,13 @@ export type WebSearchToolInput = PersistedAgentToolConfigParamWebSearch; // The tool profiles Core saves, narrowed from the schema's tool params. export type SavedAgentToolInput = | FunctionToolInput - | ServiceHttpMcpToolInput + | McpToolInput | ToolSearchInput | ProgrammaticToolCallingInput | WebSearchToolInput; // Session function tools load eagerly. export type SessionFunctionToolInput = Omit & { defer_loading?: false }; -export type ConfigurableAgentToolInput = SessionFunctionToolInput | ServiceHttpMcpToolInput; +export type ConfigurableAgentToolInput = SessionFunctionToolInput | McpToolInput; export type VaultStatus = VaultStatusParam; diff --git a/packages/claude-sdk-adapter/src/mcp.ts b/packages/claude-sdk-adapter/src/mcp.ts index ab502581a..3567190c0 100644 --- a/packages/claude-sdk-adapter/src/mcp.ts +++ b/packages/claude-sdk-adapter/src/mcp.ts @@ -160,8 +160,12 @@ export class MCPProfile { if (exact) throw new Error("native MCP server unavailable or undeclared"); continue; } - if (status.status !== "connected") throw new Error("native MCP server unavailable or undeclared"); matched++; + // Native runs on without an optional server that did not connect. + if (status.status !== "connected") { + if ("required" in server && server.required) throw new Error("required native MCP server unavailable"); + continue; + } for (const tool of status.tools ?? []) { const native = nativeToolName(server.server_label, tool.name); if (nativeNames.has(native)) throw new Error("ambiguous native MCP identity"); diff --git a/packages/claude-sdk-adapter/tests/mcp_workspace.test.mjs b/packages/claude-sdk-adapter/tests/mcp_workspace.test.mjs index ac71b2018..939d60528 100644 --- a/packages/claude-sdk-adapter/tests/mcp_workspace.test.mjs +++ b/packages/claude-sdk-adapter/tests/mcp_workspace.test.mjs @@ -77,7 +77,6 @@ test("workspace inventory tolerates native tools and servers, but not unavailabl mcp.close(); for (const [tools, servers] of [ [[...nativePreset, native], [{ ...statuses[0], tools: [{ name: "echo.v1" }, { name: "echo_v1" }] }]], - [[...nativePreset, native], [{ ...statuses[0], status: "pending" }]], [nativePreset, []], ]) { const declared = new MCPProfile([stdio], []); @@ -87,6 +86,14 @@ test("workspace inventory tolerates native tools and servers, but not unavailabl const exact = new MCPProfile([stdio], []); assert.throws(() => exact.verify([...nativePreset, native], statuses, "session")); exact.close(); + // An optional server that did not connect contributes no tools; a required one fails. + const optional = new MCPProfile([stdio], []); + optional.verify(nativePreset, [{ ...statuses[0], status: "failed" }], "session", false); + assert.equal(optional.identities.size, 0); + optional.close(); + const required = new MCPProfile([{ ...stdio, required: true }], []); + assert.throws(() => required.verify(nativePreset, [{ ...statuses[0], status: "pending" }], "session", false), /required/); + required.close(); }); test("workspace bearer references reach native HTTP without leaking values into the request", t => { diff --git a/services/core/internal/execution/environment_placement.go b/services/core/internal/execution/environment_placement.go index b30c5363b..ed1bcbf95 100644 --- a/services/core/internal/execution/environment_placement.go +++ b/services/core/internal/execution/environment_placement.go @@ -12,6 +12,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) +const hostedWorkspaceDirectory = "/workspace" + type environmentPlacement struct { Plugins []agentplugin.Metadata `json:"plugins,omitempty"` Skills []environmentconfig.SkillMetadata `json:"skills,omitempty"` @@ -55,7 +57,7 @@ func parseEnvironmentPlacement(configuration json.RawMessage) (environmentPlacem if local.WorkspaceDirectory != "" || agentcapabilities.ValidateDirectories(local.CapabilityDirectories) != nil { return placement, sessions.ErrInvalidInput } - placement.WorkspaceDirectory = "/workspace" + placement.WorkspaceDirectory = hostedWorkspaceDirectory case "self_hosted": if !validSelfHostedPlacement(placement) { return placement, sessions.ErrInvalidInput diff --git a/services/core/internal/execution/harness_admission.go b/services/core/internal/execution/harness_admission.go index 1f077350c..f88bca4b1 100644 --- a/services/core/internal/execution/harness_admission.go +++ b/services/core/internal/execution/harness_admission.go @@ -20,11 +20,16 @@ func admissionError(err error) error { // declarations validate. func admissionOf(snapshot Snapshot) harnessconfig.Admission { agent := snapshot.Agent + workspace := snapshot.Environment.WorkspaceDirectory + if snapshot.Environment.Type == "openai_hosted" { + workspace = hostedWorkspaceDirectory + } return harnessconfig.Admission{ - TextFormat: agent.Text.Format.Type, - TextSchema: agent.Text.Format.Schema, - MultiAgent: agent.MultiAgent.Enabled, - Tools: agent.Tools, + TextFormat: agent.Text.Format.Type, + TextSchema: agent.Text.Format.Schema, + MultiAgent: agent.MultiAgent.Enabled, + Tools: agent.Tools, + WorkspaceDirectory: workspace, } } diff --git a/services/core/internal/execution/mcp.go b/services/core/internal/execution/mcp.go index 6cf51518a..360bb48ab 100644 --- a/services/core/internal/execution/mcp.go +++ b/services/core/internal/execution/mcp.go @@ -89,10 +89,13 @@ func executionTools(raw []json.RawMessage) (executionToolSet, error) { server := proto.MCPHTTPServer{ConnectionOrigin: tool.ConnectionOrigin, ServerLabel: tool.ServerLabel, AllowedTools: tool.AllowedTools, Required: tool.Required} switch transport := tool.Transport; transport.Type { case "stdio": - if tool.ConnectionOrigin != "environment" || transport.Command == "" || transport.Args == nil || transport.EnvVars == nil || tool.CredentialID != nil { + if tool.ConnectionOrigin != "environment" || transport.Args == nil || transport.EnvVars == nil || tool.CredentialID != nil { return executionToolSet{}, errors.New("unsupported execution MCP configuration") } server.Stdio = &proto.MCPStdio{Command: transport.Command, Args: *transport.Args, Cwd: transport.Cwd, EnvVars: *transport.EnvVars} + if !server.Stdio.Valid() { + return executionToolSet{}, errors.New("unsupported execution MCP configuration") + } case "http": u, err := url.Parse(transport.ServerURL) if err != nil || u.Hostname() == "" || (u.Scheme != "http" && u.Scheme != "https") || u.User != nil || u.Fragment != "" {