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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/daemon/internal/agent/codex/mcp_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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('"')
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/system/ModelProviderDialog.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -87,7 +87,7 @@ export function ModelProviderDialog({ harness, onClose, onSaved, onReread }: {
const limitProblem = (limit: ReturnType<typeof tokenLimit>) => (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() {
Expand Down
6 changes: 3 additions & 3 deletions contracts/agents-api/execution-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion contracts/agents-api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/v1/model_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions contracts/agents-api/zh/execution-tools.md
Original file line number Diff line number Diff line change
@@ -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)。
Expand Down Expand Up @@ -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 登录。
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "OpenAI 兼容性"
source: contracts/agents-api/index.md
source_hash: 1387a015bb94dc885cc6b9b2c9f6a55c9837438a59e86ce9ff371e161ace1499
source_hash: fd77d3617bd8a124382a8d7e8c95adb824fc0da73c31fce2ff475c920e3b0626
---

Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。
Expand Down Expand Up @@ -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。

Expand Down
16 changes: 16 additions & 0 deletions internal/harnessconfig/claudesdk/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package claudesdk

import (
"encoding/base64"
"encoding/json"
"errors"
"math"
"path/filepath"
"regexp"
"strings"

Expand Down Expand Up @@ -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
}

Expand Down
12 changes: 12 additions & 0 deletions internal/harnessconfig/claudesdk/declaration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
2 changes: 2 additions & 0 deletions internal/harnessconfig/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion internal/modelprovider/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading
Loading