diff --git a/apps/daemon/internal/agent/claudesdk/combination_live_linux_test.go b/apps/daemon/internal/agent/claudesdk/combination_live_linux_test.go new file mode 100644 index 000000000..d1ceba357 --- /dev/null +++ b/apps/daemon/internal/agent/claudesdk/combination_live_linux_test.go @@ -0,0 +1,140 @@ +//go:build linux + +package claudesdk + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/google/uuid" +) + +// TestLiveClaudeSDKNativeCombinations proves structured output beside multi_agent, +// a function tool and deferred tool search on the real SDK and provider. +func TestLiveClaudeSDKNativeCombinations(t *testing.T) { + entrypoint, keyFile, proofRoot := os.Getenv("OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT"), os.Getenv("OAC_TEST_CLAUDE_SDK_MINIMAX_KEY_FILE"), os.Getenv("OAC_TEST_CLAUDE_SDK_PROOF_DIR") + endpoint, model := os.Getenv("OAC_TEST_CLAUDE_SDK_BASE_URL"), os.Getenv("OAC_TEST_CLAUDE_SDK_MODEL") + if entrypoint == "" || keyFile == "" || endpoint == "" || model == "" || !filepath.IsAbs(proofRoot) { + t.Skip("real SDK/provider acceptance requires an entrypoint, a private key file, an endpoint, a model and an absolute proof directory") + } + key, err := os.ReadFile(keyFile) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(proofRoot, 0o700); err != nil { + t.Fatal(err) + } + root, err := os.MkdirTemp(proofRoot, "claude-combination-") + if err != nil { + t.Fatal(err) + } + t.Setenv("OAC_RUNTIME_HOME", root) + config := Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{ + "ANTHROPIC_BASE_URL=" + endpoint, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), + "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=", + "ANTHROPIC_DEFAULT_SONNET_MODEL=" + model, "ANTHROPIC_DEFAULT_OPUS_MODEL=" + model, "ANTHROPIC_DEFAULT_HAIKU_MODEL=" + model, + }} + schema := json.RawMessage(`{"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"],"additionalProperties":false}`) + lookup := func(deferred bool) []proto.FunctionTool { + return []proto.FunctionTool{{Name: "lookup", Description: "Return the synthetic verification value.", Parameters: json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`), DeferLoading: deferred}} + } + // A stateless JSON-RPC MCP server with one tool. + mcpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + var rpc struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + } + _ = json.Unmarshal(body, &rpc) + var result string + switch rpc.Method { + case "initialize": + result = `{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"proof","version":"0"}}` + case "tools/list": + result = `{"tools":[{"name":"echo","description":"Return the synthetic MCP value.","inputSchema":{"type":"object","properties":{}}}]}` + case "tools/call": + result = `{"content":[{"type":"text","text":"synthetic-mcp-value"}]}` + default: + w.WriteHeader(http.StatusAccepted) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":` + string(rpc.ID) + `,"result":` + result + `}`)) + })) + defer mcpServer.Close() + mcp := func(req *proto.PromptRequestPayload) { + req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "proof", ServerURL: mcpServer.URL}} + } + cases := map[string]func(*proto.PromptRequestPayload){ + "multi_agent": func(req *proto.PromptRequestPayload) { + req.DisableSubagents, req.ObserveSubagentIdentities = false, true + }, + "mcp": mcp, + "mcp_tool_search": func(req *proto.PromptRequestPayload) { + mcp(req) + req.FunctionTools, req.ToolSearch = lookup(true), true + }, + "function": func(req *proto.PromptRequestPayload) { req.FunctionTools = lookup(false) }, + "tool_search": func(req *proto.PromptRequestPayload) { + req.FunctionTools, req.ToolSearch = lookup(true), true + }, + } + for name, configure := range cases { + t.Run(name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + req := proto.PromptRequestPayload{RunID: uuid.NewString(), ObserveMessages: true, DisableExecutionEnvironment: true, DisableSubagents: true, + Input: proto.TextInput("Call the lookup tool if one is available and put its value in answer. Otherwise answer with the word ready."), + ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}, + AgentOptions: map[string]any{"model": model, "system_prompt": "Reply only through the structured output."}} + configure(&req) + out := make(chan proto.Envelope, 64) + running, err := startSingleTurn(ctx, config, req, out) + if err != nil { + t.Fatal(err) + } + defer running.Cancel(context.Background()) + s := running.(*session) + var done, called bool + var content, failure string + for event := range out { + switch event.Type { + case proto.TypeFunctionCall: + var call proto.FunctionCallPayload + if err := event.DecodePayload(&call); err != nil { + t.Fatal(err) + } + called = true + text := "synthetic-verification-value" + if err := s.SubmitFunctionResult(ctx, proto.FunctionResultPayload{CallID: call.CallID, DeliveryID: uuid.NewString(), Success: true, Content: []proto.InputContent{{Type: "input_text", Text: &text}}}); err != nil { + t.Fatal(err) + } + case proto.TypeError: + var payload proto.ErrorPayload + _ = json.Unmarshal(event.Payload, &payload) + failure = payload.Error + case proto.TypeDone: + var payload proto.DonePayload + _ = json.Unmarshal(event.Payload, &payload) + done, content = true, payload.Content + } + } + var answer struct { + Answer string `json:"answer"` + } + if !done || failure != "" || json.Unmarshal([]byte(content), &answer) != nil || answer.Answer == "" { + t.Fatalf("structured output failed: done=%v failure=%q content=%q", done, failure, content) + } + t.Logf("%s: function_called=%v answer=%q", name, called, answer.Answer) + }) + } +} diff --git a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go index e7ed164ab..ff1332c34 100644 --- a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go +++ b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go @@ -141,13 +141,14 @@ func TestStructuredOutputConfigurationReachesNativeUnchanged(t *testing.T) { t.Fatal("lossy schema accepted") } request.ExecutionControls.OutputFormat.Schema = schema - request.DisableSubagents = false - if _, _, err := prepare(config, request); err == nil { - t.Fatal("unqualified subagent combination accepted") + request.DisableExecutionEnvironment = true + request.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp"}} + if start, _, err := prepare(config, request); err != nil || start.OutputFormat == nil { + t.Fatal("native structured output was narrowed by MCP", err) } } -func TestToolDiscoveryPreservesFrozenFunctionsAndRejectsOtherProfiles(t *testing.T) { +func TestToolDiscoveryPreservesFrozenFunctionsBesideOtherProfiles(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} @@ -159,11 +160,12 @@ func TestToolDiscoveryPreservesFrozenFunctionsAndRejectsOtherProfiles(t *testing if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions, request.FunctionTools) { t.Fatal("function discovery changed native definitions", err) } - request.DisableSubagents = false - if _, _, err := prepare(config, request); err == nil { - t.Fatal("unqualified combination admitted") + request.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: json.RawMessage(`{"type":"object"}`)}} + request.ObserveMessages = true + if start, _, err := prepare(config, request); err != nil || !start.ToolSearch || start.OutputFormat == nil { + t.Fatal("native tool search was narrowed by structured output", err) } - request.DisableSubagents = true + request.ExecutionControls = nil request.ToolSearch = false if _, _, err := prepare(config, request); err == nil { t.Fatal("deferred definitions became eager") diff --git a/apps/daemon/internal/agent/claudesdk/mcp.go b/apps/daemon/internal/agent/claudesdk/mcp.go index 31e9d44ef..726b6da61 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp.go +++ b/apps/daemon/internal/agent/claudesdk/mcp.go @@ -15,7 +15,6 @@ import ( ) var mcpLabel = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) -var mcpTool = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`) func validateMCP(req proto.PromptRequestPayload) error { if req.MCPHTTPServers == nil { @@ -41,13 +40,6 @@ func validateMCPServers(servers []proto.MCPHTTPServer) error { return fmt.Errorf("claudesdk: unsupported HTTPS MCP bearer credential") } labels[server.ServerLabel] = true - if server.AllowedTools != nil { - for _, name := range *server.AllowedTools { - if !mcpTool.MatchString(name) { - return fmt.Errorf("claudesdk: unsupported MCP tool allowlist") - } - } - } } return nil } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_test.go b/apps/daemon/internal/agent/claudesdk/mcp_test.go index d44e4748c..f219d101d 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_test.go @@ -47,7 +47,7 @@ func TestHTTPMCPDeclaration(t *testing.T) { req.DisableExecutionEnvironment = false } start, _, err := prepare(config, req) - valid := mode == "unrestricted" || mode == "selected" || mode == "empty" || mode == "nil-slice" || mode == "auth" || mode == "required" || mode == "query" + valid := mode == "unrestricted" || mode == "selected" || mode == "empty" || mode == "nil-slice" || mode == "auth" || mode == "required" || mode == "query" || mode == "wildcard" if (err == nil) != valid { t.Fatalf("unexpected admission: %v", err) } diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index a52b7f2b2..ec793668e 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -73,12 +73,7 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR if err := req.ValidateToolSearch(true); err != nil { return startRequest{}, nil, err } - if req.ToolSearch { - 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 - } + start.ToolSearch = req.ToolSearch if err := validateMCP(req); err != nil { return startRequest{}, nil, err } @@ -91,8 +86,8 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR } if req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil { format := req.ExecutionControls.OutputFormat - if format.Type != "json_schema" || !req.ObserveMessages || !req.DisableSubagents || req.MCPHTTPServers != nil || (req.LocalEnvironment != nil && (len(req.LocalEnvironment.MCP) != 0 || len(req.LocalEnvironment.Skills) != 0)) { - return fail("structured output requires the qualified message-observing single-agent function profile") + if format.Type != "json_schema" || !req.ObserveMessages { + return fail("structured output requires message observation") } if err := proto.ValidateBinary64Schema(format.Schema); err != nil { return startRequest{}, nil, err diff --git a/contracts/agents-api/execution-tools.md b/contracts/agents-api/execution-tools.md index e65ee0ecb..bb9392d01 100644 --- a/contracts/agents-api/execution-tools.md +++ b/contracts/agents-api/execution-tools.md @@ -41,7 +41,7 @@ The Worker fails previously claimed work without replay after execution loss; qu Core carries the schema in `ExecutionControls.OutputFormat` and requires structured-output support in the Harness declaration plus the Runtime's `structured_output` and message-observation capabilities, only for requests that use the option. Frozen schemas reach preparation before input and apply to initial and resumed execution; Start cannot replace them. -The Claude adapter passes `outputFormat` to the pinned SDK and allows its native `StructuredOutput` terminal tool, which is internal and never an extra caller function. A matching live root tool result and an attributed successful SDK result confirm the output. The adapter publishes the native `result.result` string unchanged as a completed `final_answer` message with the native tool-use ID; parent assistant prose keeps its own ID. Unvalidated retries and cancelled candidates never become the answer, and the adapter never serializes `structured_output` back to JSON. The stream follows the official message sequence with the whole text in one `output_text.delta`. The bridge advertises the operation only when it reports `structured_output`, and a workspace Runtime also needs `workspace_structured_output`. +The Claude adapter passes `outputFormat` to the pinned SDK and allows its native `StructuredOutput` terminal tool, which is internal and never an extra caller function. Natively the tool is added beside Subagents, Skills, MCP and ToolSearch, and the adapter's exact tool inventory counts it. A matching live root tool result and an attributed successful SDK result confirm the output. The adapter publishes the native `result.result` string unchanged as a completed `final_answer` message with the native tool-use ID; parent assistant prose keeps its own ID. Unvalidated retries and cancelled candidates never become the answer, and the adapter never serializes `structured_output` back to JSON. The stream follows the official message sequence with the whole text in one `output_text.delta`. The bridge advertises the operation only when it reports `structured_output`, and a workspace Runtime also needs `workspace_structured_output`. The Codex adapter sends the schema unchanged as the native `outputSchema` on every root Turn, including Turns after resume or recovery, and publishes the native final agent message text unchanged. Codex reads JSON numbers losslessly, so Session admission applies no number or root-type limit beyond the common rules. A steered input joins the running Turn under its schema, and Subagent threads do not receive the schema. Codex passes the schema to the model provider as a strict Responses `text.format`; a provider that cannot run it returns its own error unchanged, and a provider that ignores it can return text that does not match the schema. @@ -49,7 +49,7 @@ The Codex adapter sends the schema unchanged as the native `outputSchema` on eve A `tool_search` tool has only `type`; Responses-only execution fields are rejected. Function `defer_loading` marks which definitions load lazily. Discovery requires both: `tool_search` without a deferred function, or deferred functions without `tool_search`, is rejected. The saved-Agent tool union keeps `tool_search`; the pinned Session response union omits it, so Session and SSE resources project it out while the frozen configuration keeps it. The pinned Items union has no tool-search Item, and Core invents none. -Core sends `PromptRequestPayload.ToolSearch` and each `FunctionTool.DeferLoading`, and requires `tool_search` support in both the Harness declaration and the Runtime. Native search and lazy schema loading belong to the adapter. The Claude adapter's MCP server marks eager definitions `anthropic/alwaysLoad:true` and deferred ones false, and enables native ToolSearch; the function profile allows only declared callbacks and ToolSearch besides the workspace's native tools. A workspace Runtime derives `tool_search` from the bridge's `workspace_tool_search` feature. The native Harness owns model and provider policy; known conflicting modes and beta settings reject in the adapter, and the SDK gives no reliable pre-input signal that deferral took effect after an opaque policy change. +Core sends `PromptRequestPayload.ToolSearch` and each `FunctionTool.DeferLoading`, and requires `tool_search` support in both the Harness declaration and the Runtime. Native search and lazy schema loading belong to the adapter. The Claude adapter's MCP server marks eager definitions `anthropic/alwaysLoad:true` and deferred ones false, and sets `ENABLE_TOOL_SEARCH=true`; the `none` profile allows declared callbacks, declared MCP tools and ToolSearch besides the workspace's native tools. A workspace Runtime derives `tool_search` from the bridge's `workspace_tool_search` feature. The native Harness owns model, provider and beta-setting policy; the adapter rejects none of them, and the SDK gives no reliable pre-input signal that deferral took effect. ## Web search and programmatic tool calling @@ -92,4 +92,4 @@ Execution carries the resolved values to the Harness and validates them against - Bearer authentication uses an attached static or OAuth Vault credential. [Vault credentials](./vaults.md) owns selection, and [MCP credential authority](./environments.md#public-mcp-connection-origin) owns the frozen Runtime binding. Authenticated execution requires `mcp_http_bearer_auth`. - The Runtime must advertise `mcp_http_tools`. The native Harness owns discovery, calls and results; public `mcp_call` Items use the original server and tool names and keep the observed native result. -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`, tool names that may also contain dots, and requires connected servers with static inventories. 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 connected servers with static inventories. 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/harness-capabilities.md b/contracts/agents-api/harness-capabilities.md index 040ff5cab..e8c3fb349 100644 --- a/contracts/agents-api/harness-capabilities.md +++ b/contracts/agents-api/harness-capabilities.md @@ -47,7 +47,7 @@ Native model parameters and provider protocols per Harness are in [model executi | [Subagents](./subagents.md) | Verified: hosted; admitted: `none`, self-hosted | Verified: hosted; admitted: `none`, self-hosted | Verified: hosted; admitted: `none`, self-hosted | | Subagents together with functions or HTTP MCP | Rejected | Rejected | Rejected | -Claude structured output requires a single Agent, medium verbosity and no Skills, Plugins, capability directories, MCP or `tool_search`. Claude deferred discovery requires a single Agent, only function tools besides `tool_search` and the disabled controls, no Skills, Plugins or capability directories and no structured output. Claude on a workspace placement needs the packaged bridge features for each operation (`workspace_functions`, `workspace_structured_output`, `workspace_tool_search`, `workspace_mcp_http`). +Claude structured output requires medium verbosity and combines with `multi_agent`, Skills, Plugins, capability directories, MCP and `tool_search`. Claude deferred discovery combines with MCP, Skills, Plugins, capability directories and structured output. The Subagent profile does not carry function tools, so deferred discovery with `multi_agent` stays rejected. Claude Code decides whether `tool_search` takes effect for the configured model, provider and beta settings. Claude on a workspace placement needs the packaged bridge features for each operation (`workspace_functions`, `workspace_structured_output`, `workspace_tool_search`, `workspace_mcp_http`). ## Environment preparation diff --git a/contracts/agents-api/zh/execution-tools.md b/contracts/agents-api/zh/execution-tools.md index 20f92190e..71268b466 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: b7a4e9408422a272c0c8695f264d1ee9be9b85695b4622a9751c7ead4d3c218c +source_hash: fd0abcd2ab5d65d4eb63662d42a2562b8870926069b0255d4de0e014b3cbea06 --- Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在 `text.format` 中声明输出 schema。本契约说明 Core 如何验证声明、哪些内容跨越 Runtime 边界,以及调用方如何恢复待执行操作。[Harness 能力](harness-capabilities.md)列出各 Harness 在不同部署位置支持的操作。原生工作区工具和 Environment Plugin MCP 属于 [Environment](environments.md#skills-plugins-and-environment-mcp)。 @@ -43,7 +43,7 @@ SSE 仅提供实时事件。重启或流丢失后,读取 Session 的 `required Core 在 `ExecutionControls.OutputFormat` 中携带 schema,仅对使用该选项的请求要求 Harness 声明支持结构化输出,并要求 Runtime 具有 `structured_output` 和消息观察能力。冻结的 schema 在输入之前送达准备阶段,适用于初次和恢复执行;Start 不能替换它。 -Claude 适配器将 `outputFormat` 传给固定版本 SDK,并允许原生 `StructuredOutput` 终态工具;该工具属于内部,不是额外的调用方函数。匹配的实时根工具结果和已归属的成功 SDK 结果确认输出。适配器将原生 `result.result` 字符串原样发布为已完成的 `final_answer` 消息,使用原生 tool-use ID;父 assistant 文本保留自己的 ID。未验证重试和已取消候选不会成为答案,适配器不会把 `structured_output` 重新序列化为 JSON。流遵循官方消息顺序,将整段文本放入一个 `output_text.delta`。桥接层仅在报告 `structured_output` 时声明该操作,工作区 Runtime 还需要 `workspace_structured_output`。 +Claude 适配器将 `outputFormat` 传给固定版本 SDK,并允许原生 `StructuredOutput` 终态工具;该工具属于内部,不是额外的调用方函数。原生会在 Subagent、Skill、MCP 和 ToolSearch 之外添加该工具,适配器的精确工具清单会计入它。匹配的实时根工具结果和已归属的成功 SDK 结果确认输出。适配器将原生 `result.result` 字符串原样发布为已完成的 `final_answer` 消息,使用原生 tool-use ID;父 assistant 文本保留自己的 ID。未验证重试和已取消候选不会成为答案,适配器不会把 `structured_output` 重新序列化为 JSON。流遵循官方消息顺序,将整段文本放入一个 `output_text.delta`。桥接层仅在报告 `structured_output` 时声明该操作,工作区 Runtime 还需要 `workspace_structured_output`。 Codex 适配器在每个根 Turn(包括 resume 或恢复后的 Turn)上将 schema 原样作为原生 `outputSchema` 发送,并原样发布原生最终 agent 消息文本。Codex 无损读取 JSON 数字,因此 Session 准入除通用规则外不附加数字或根类型限制。引导(steer)输入并入正在运行的 Turn 并沿用其 schema,Subagent 线程不接收该 schema。Codex 将 schema 作为 strict 的 Responses `text.format` 传给模型提供商;无法运行它的提供商原样返回自己的错误,忽略它的提供商可能返回与 schema 不符的文本。 @@ -51,7 +51,7 @@ Codex 适配器在每个根 Turn(包括 resume 或恢复后的 Turn)上将 s `tool_search` 工具仅包含 `type`;仅适用于 Responses 的执行字段被拒绝。函数 `defer_loading` 标记延迟加载的定义。发现要求两者同时存在:没有延迟函数的 `tool_search`,或没有 `tool_search` 的延迟函数均被拒绝。已保存 Agent 的工具联合类型保留 `tool_search`;固定版本 Session 响应联合类型省略它,因此 Session 与 SSE 资源投影移除它,冻结配置仍保留它。固定版本 Item 联合类型没有 tool-search Item,Core 不自行创建。 -Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoading`,并要求 Harness 声明和 Runtime 均支持 `tool_search`。原生搜索与 schema 延迟加载属于适配器。Claude 适配器的 MCP 服务器将立即加载定义标记为 `anthropic/alwaysLoad:true`,延迟定义标记为 false,并启用原生 ToolSearch;函数配置仅允许所声明回调、ToolSearch 与工作区的原生工具。工作区 Runtime 从桥接层的 `workspace_tool_search` 功能推导 `tool_search`。原生 Harness 管理模型与提供商策略;已知冲突模式和 beta 设置在适配器中拒绝,不透明策略改变后,SDK 不提供可靠的输入前信号来确认延迟加载是否生效。 +Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoading`,并要求 Harness 声明和 Runtime 均支持 `tool_search`。原生搜索与 schema 延迟加载属于适配器。Claude 适配器的 MCP 服务器将立即加载定义标记为 `anthropic/alwaysLoad:true`,延迟定义标记为 false,并设置 `ENABLE_TOOL_SEARCH=true`;`none` 配置除工作区的原生工具外,允许所声明回调、所声明 MCP 工具和 ToolSearch。工作区 Runtime 从桥接层的 `workspace_tool_search` 功能推导 `tool_search`。原生 Harness 管理模型、提供商与 beta 设置策略;适配器不拒绝其中任何一项,SDK 也不提供可靠的输入前信号来确认延迟加载是否生效。 ## Web 搜索与程序化工具调用 {#web-search-and-programmatic-tool-calling} @@ -94,4 +94,4 @@ Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoadi - Bearer 认证使用附加的静态或 OAuth Vault 凭据。[Vault 凭据](vaults.md)定义选择规则,[MCP 凭据权限](environments.md#public-mcp-connection-origin)定义冻结的 Runtime 绑定。认证执行要求 `mcp_http_bearer_auth`。 - Runtime 必须声明 `mcp_http_tools`。原生 Harness 管理发现、调用和结果;公开 `mcp_call` Item 使用原始服务器与工具名称,保留观察到的原生结果。 -Codex 以原生方式信任工作区,因此工作区的项目配置在启动和冷恢复时加载,未声明的服务器、apps 和 plugins 保持原生行为。在任何携带 Core 凭据的原生连接之前,它验证每个已声明服务器保持其 URL 或命令、bearer 引用和请求头,否则启动或恢复以错误失败。它拒绝原生保留标签和已存储原生 MCP 凭据。Claude 接受 ASCII 字母、数字、下划线和连字符组成的标签,但不允许 `functions`;工具名称还可包含点,并要求已连接服务器具有静态工具清单。匿名 Claude 请求发送空 Authorization 头以阻止原生 OAuth 注入。不支持原生 OAuth 登录。 +Codex 以原生方式信任工作区,因此工作区的项目配置在启动和冷恢复时加载,未声明的服务器、apps 和 plugins 保持原生行为。在任何携带 Core 凭据的原生连接之前,它验证每个已声明服务器保持其 URL 或命令、bearer 引用和请求头,否则启动或恢复以错误失败。它拒绝原生保留标签和已存储原生 MCP 凭据。Claude 接受 ASCII 字母、数字、下划线和连字符组成的标签,但不允许 `functions`;接受任意工具名称(原生客户端会将其规范化,适配器拒绝规范化后的冲突),并要求已连接服务器具有静态工具清单。匿名 Claude 请求发送空 Authorization 头以阻止原生 OAuth 注入。不支持原生 OAuth 登录。 diff --git a/contracts/agents-api/zh/harness-capabilities.md b/contracts/agents-api/zh/harness-capabilities.md index 24e8158c4..0893c2628 100644 --- a/contracts/agents-api/zh/harness-capabilities.md +++ b/contracts/agents-api/zh/harness-capabilities.md @@ -1,7 +1,7 @@ --- title: "Harness 能力" source: contracts/agents-api/harness-capabilities.md -source_hash: c31a323311129a5d005cec014003634c0e4e30ed824798cdda3ee10f2902efd9 +source_hash: 4cea6f45872bd29a38f800f58801a4d271142612fb853a023952323895d6eed3 --- 本页列出每个 Harness 在每种部署位置支持的能力。Core 根据 `internal/harnessconfig//declaration.go` 中 Harness 的声明决定准入,运行 Session 的 Runtime 也必须声明操作。所链接契约定义各操作;[Harness 接入](harness-onboarding.md#qualify-the-adapter)说明资格验证方法。 @@ -49,7 +49,7 @@ source_hash: c31a323311129a5d005cec014003634c0e4e30ed824798cdda3ee10f2902efd9 | [Subagent](subagents.md) | 已验证:托管;已准入:`none`、自托管 | 已验证:托管;已准入:`none`、自托管 | 已验证:托管;已准入:`none`、自托管 | | Subagent 与函数或 HTTP MCP 同时使用 | 已拒绝 | 已拒绝 | 已拒绝 | -Claude 结构化输出要求单 Agent、medium verbosity,且无 Skill、Plugin、能力目录、MCP 或 `tool_search`。Claude 延迟发现要求单 Agent,除 `tool_search` 与禁用控制项外仅函数工具,无 Skill、Plugin 或能力目录,也无结构化输出。Claude 在工作区位置需要每种操作的打包桥接功能(`workspace_functions`、`workspace_structured_output`、`workspace_tool_search`、`workspace_mcp_http`)。 +Claude 结构化输出要求 medium verbosity,可与 `multi_agent`、Skill、Plugin、能力目录、MCP 和 `tool_search` 组合。Claude 延迟发现可与 MCP、Skill、Plugin、能力目录和结构化输出组合。Subagent 配置不携带函数工具,因此延迟发现与 `multi_agent` 的组合仍被拒绝。`tool_search` 是否对所配置的模型、提供商和 beta 设置生效,由 Claude Code 决定。Claude 在工作区位置需要每种操作的打包桥接功能(`workspace_functions`、`workspace_structured_output`、`workspace_tool_search`、`workspace_mcp_http`)。 ## Environment 准备 {#environment-preparation} diff --git a/internal/harnessconfig/claudesdk/declaration.go b/internal/harnessconfig/claudesdk/declaration.go index b4260573f..c018b560d 100644 --- a/internal/harnessconfig/claudesdk/declaration.go +++ b/internal/harnessconfig/claudesdk/declaration.go @@ -72,10 +72,7 @@ var Declaration = harnessconfig.Declaration{ ValidateFunctionResult: validateFunctionResult, } -var ( - mcpLabel = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) - mcpTool = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`) -) +var mcpLabel = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) // validateConfiguration holds the packaged adapter's execution limits on a saved // Agent and Environment, not saved-Agent schema rules. @@ -88,24 +85,9 @@ func validateConfiguration(admission harnessconfig.Admission) error { if err := proto.ValidateBinary64Schema(admission.TextSchema); err != nil { return err } - if admission.MultiAgent { - return errors.New("Structured output requires a single Agent.") - } - if environment.Extensions { - return errors.New("Structured output with environment Skills or Plugins is not supported.") - } - for _, raw := range admission.Tools { - var tool struct { - Type string `json:"type"` - } - if json.Unmarshal(raw, &tool) != nil || (tool.Type != "function" && tool.Type != "web_search" && tool.Type != "programmatic_tool_calling") { - return harnessconfig.ErrInvalidInput - } - } } else if admission.TextFormat != "" && admission.TextFormat != "text" { return harnessconfig.ErrInvalidInput } - search, otherTools := false, false for _, raw := range admission.Tools { var tool struct { Type string `json:"type"` @@ -118,11 +100,6 @@ func validateConfiguration(admission harnessconfig.Admission) error { if tool.Type == "web_search" && tool.Mode == "live" && environment.Type == "none" { return errors.New("Live web search requires a managed or self-hosted execution Environment.") } - 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 && (admission.MultiAgent || otherTools || environment.Extensions || admission.TextFormat == "json_schema") { - return errors.New("Tool discovery requires a single-agent function profile without Skills or Plugins.") } return harnessconfig.RejectSubagentTools(admission, "function", "mcp") } @@ -134,13 +111,6 @@ func validateTools(tools []proto.FunctionTool, servers []proto.MCPHTTPServer) er if !mcpLabel.MatchString(server.ServerLabel) || server.ServerLabel == "functions" || strings.Contains(server.ServerURL, "#") { return errors.New("The configured engine does not support this HTTP MCP declaration.") } - if server.AllowedTools != nil { - for _, name := range *server.AllowedTools { - if !mcpTool.MatchString(name) { - return errors.New("The configured engine does not support this MCP tool name.") - } - } - } } for _, tool := range tools { var schema struct { diff --git a/internal/harnessconfig/claudesdk/declaration_test.go b/internal/harnessconfig/claudesdk/declaration_test.go new file mode 100644 index 000000000..cd995502f --- /dev/null +++ b/internal/harnessconfig/claudesdk/declaration_test.go @@ -0,0 +1,42 @@ +package claudesdk + +import ( + "encoding/json" + "testing" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig" +) + +func TestDeclarationAdmitsNativeToolCombinations(t *testing.T) { + tools := func(types ...string) []json.RawMessage { + var raw []json.RawMessage + for _, kind := range types { + raw = append(raw, json.RawMessage(`{"type":"`+kind+`"}`)) + } + return raw + } + for name, admission := range map[string]harnessconfig.Admission{ + "structured output with multi_agent": {MultiAgent: true, TextFormat: "json_schema"}, + "structured output with mcp": {TextFormat: "json_schema", Tools: tools("mcp")}, + "structured output with tool_search": {TextFormat: "json_schema", Tools: tools("function", "tool_search")}, + "tool_search with mcp": {Tools: tools("function", "tool_search", "mcp")}, + } { + if admission.Environment == nil { + admission.Environment = &harnessconfig.AdmissionEnvironment{Type: "none"} + } + admission.Model = "model" + admission.TextSchema = json.RawMessage(`{"type":"object"}`) + if err := Declaration.ValidateConfiguration(admission); err != nil { + t.Errorf("%s: %v", name, err) + } + } +} + +func TestDeclarationAdmitsAnyMCPToolName(t *testing.T) { + allowed := []string{"a/b", "tool name"} + err := Declaration.ValidateTools(nil, []proto.MCPHTTPServer{{ServerLabel: "docs", ServerURL: "https://example.test/mcp", AllowedTools: &allowed}}) + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/harnessconfig/harness.go b/internal/harnessconfig/harness.go index 7a01bbb84..3397e00ee 100644 --- a/internal/harnessconfig/harness.go +++ b/internal/harnessconfig/harness.go @@ -328,11 +328,9 @@ type Admission struct { Environment *AdmissionEnvironment } -// AdmissionEnvironment carries the placement and whether the Environment adds -// Skills, Plugins or capability directories. +// AdmissionEnvironment carries the placement. type AdmissionEnvironment struct { - Type string - Extensions bool + Type string } // ValidateMCPOrigins preserves outbound authority independently of Harness names. diff --git a/packages/claude-sdk-adapter/README.md b/packages/claude-sdk-adapter/README.md index 8d96e797b..0e34b18ed 100644 --- a/packages/claude-sdk-adapter/README.md +++ b/packages/claude-sdk-adapter/README.md @@ -54,13 +54,13 @@ Private workspace execution requires the packaged `workspace_command_observation The private adapter accepts typed anonymous HTTP and static-bearer HTTPS MCP declarations on the trusted `environment:none` harness host. The packaged readiness report must include `mcp_http_tools`; discovery advertises that feature only when present, and execution rechecks the installed bundle before dispatching an MCP request. An unchanged SDK version alone cannot qualify an older bridge. Authenticated private requests also require the packaged `mcp_http_bearer_auth` feature at discovery and dispatch. The daemon generates a separate environment reference for each server and launch; only those references enter the bridge request and native SDK configuration. The native HTTP client expands them from its owned process environment. Literal bearers must never enter SDK MCP headers because that configuration enters argv. Readiness probes receive no per-request bearer environment. Token validation is shared with the Codex adapter; credential storage remains an opaque-string contract. Public MCP admission, Vault credential selection and their failure rules belong to [public MCP connection origin](../../contracts/agents-api/environments.md#public-mcp-connection-origin) and [credential selection](../../contracts/agents-api/vaults.md#credential-selection-in-a-session). -Before the native query starts, the adapter lists each server with a nonempty `allowed_tools` subset over Streamable HTTP with the native configuration's URL and bearer header, and passes the native names of the unselected tools in `disallowedTools`; a listing failure fails the query without retry. The PreToolUse hook denies any unselected call as a backstop. Null selects all tools from a declared server; an empty list selects none. Host functions compose with those selections. Native server status supplies original tool identities; map their normalized native aliases while preserving the original names in observations. Native status deduplicates aliases, so it does not prove a complete original server inventory. A native PreToolUse hook waits for inventory verification before admitting root calls and denies unverified, mismatched or cancelled calls. Inventory verification is not a barrier before the model request. Anonymous HTTP declarations explicitly set an empty Authorization header to disable native OAuth and automatic credential injection. Preserve that header; do not erase native history or credentials to enforce this boundary. Servers that reject a blank Authorization header, normalized name collisions and inventory changes during a query require separate validation; this profile covers static inventories. Private SDK status/control objects can contain expanded authentication headers. Read only connection and tool identity fields; never retain, log or publish raw status/configuration or control responses. Diagnostic projections must whitelist safe fields; filtering actual model or tool output does not fix a leak. The adapter profile requires connected servers, reserves the `functions` label, accepts alphanumeric/underscore/hyphen server labels and alphanumeric/underscore/hyphen/dot selected tool names, and excludes remote environments. Required startup is separately qualified by `mcp_http_required`. All HTTP MCP queries use native SDK startup and an empty input iterator to confirm initialization hooks. Required declarations additionally check connected server status before the initial prompt is released exactly once. Pending, failed, missing or ambiguous required status rejects before input; native startup timeouts are retained without an adapter retry loop. Normal system/init still verifies Session identity before input readiness/tool authority, with the complete inventory on `none` and the presence of every tool Core declared on a workspace. Optional servers without a subset keep their inventory checks without a pre-input connection requirement. A Runtime must advertise the concrete required-initialization capability; there is no fallback to another execution path. +Before the native query starts, the adapter lists each server with a nonempty `allowed_tools` subset over Streamable HTTP with the native configuration's URL and bearer header, and passes the native names of the unselected tools in `disallowedTools`; a listing failure fails the query without retry. The PreToolUse hook denies any unselected call as a backstop. Null selects all tools from a declared server; an empty list selects none. Host functions compose with those selections. Native server status supplies original tool identities; map their normalized native aliases while preserving the original names in observations. Native status deduplicates aliases, so it does not prove a complete original server inventory. A native PreToolUse hook waits for inventory verification before admitting root calls and denies unverified, mismatched or cancelled calls. Inventory verification is not a barrier before the model request. Anonymous HTTP declarations explicitly set an empty Authorization header to disable native OAuth and automatic credential injection. Preserve that header; do not erase native history or credentials to enforce this boundary. Servers that reject a blank Authorization header, normalized name collisions and inventory changes during a query require separate validation; this profile covers static inventories. Private SDK status/control objects can contain expanded authentication headers. Read only connection and tool identity fields; never retain, log or publish raw status/configuration or control responses. Diagnostic projections must whitelist safe fields; filtering actual model or tool output does not fix a leak. The adapter profile requires connected servers, reserves the `functions` label, accepts alphanumeric/underscore/hyphen server labels and any selected tool name, which the native client normalizes, and excludes remote environments. Required startup is separately qualified by `mcp_http_required`. All HTTP MCP queries use native SDK startup and an empty input iterator to confirm initialization hooks. Required declarations additionally check connected server status before the initial prompt is released exactly once. Pending, failed, missing or ambiguous required status rejects before input; native startup timeouts are retained without an adapter retry loop. Normal system/init still verifies Session identity before input readiness/tool authority, with the complete inventory on `none` and the presence of every tool Core declared on a workspace. Optional servers without a subset keep their inventory checks without a pre-input connection requirement. A Runtime must advertise the concrete required-initialization capability; there is no fallback to another execution path. Root assistant tool calls and live root user results produce the neutral MCP observations. Correlate actual Session/call identities; exclude replay, synthetic and subagent work and keep host function receipts separate. Preserve the exact native `tool_use_result` when one result is unambiguous, otherwise the per-call result content. Native errors remain observed native errors. The SDK can replace annotated MCP content with rendered structuredContent and flatten MCP errors; these observations do not claim original MCP envelope fidelity or hosted output parity. Do not reconstruct lost fields or infer output from model prose. Unfinished observed calls become incomplete on shutdown, without claiming that remote tool effects were cancelled. Rich content, native truncation and asynchronous MCP task results remain unverified. ### Deferred function discovery -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 workspace/function features. Qualification, combination limits and model-policy limitations are owned by [Deferred function discovery](../../contracts/agents-api/execution-tools.md#deferred-function-discovery). +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 workspace/function features. Combinations are described under [Deferred function discovery](../../contracts/agents-api/execution-tools.md#deferred-function-discovery). ### Registration and state @@ -74,7 +74,7 @@ The SDK descriptor advertises the validated daemon subset, including durable Tur Core owns public admission for Claude: [harness selection](../../contracts/agents-api/model-execution.md#harness-selection) chooses the engine for each Session; one [engine policy](../../contracts/agents-api/harness-onboarding.md#declare-the-harness) serves API admission, device selection and the final claim; the [supported operations](../../contracts/agents-api/harness-capabilities.md) table records Claude's medium-only verbosity and object-root function schemas; [function result images](../../contracts/agents-api/message-content.md#function-results) defines which placements accept image results; and [deployment defaults](../../contracts/agents-api/model-execution.md#deployment-defaults) define the model provider Core freezes for a Session. The adapter receives that provider as the adapter-owned `model_provider`, never in public Session configuration. Without one, a `none` host uses the daemon's own provider environment. The adapter alone selects the provider environment and removes credentials from native tool environments. -The `none` profile accepts only text, explicit model/system instructions, managed state, exact native resume and declared functions with ordered text or successful inline PNG, JPEG, GIF or WebP results, and the HTTP MCP subset described above. It rejects unsupported request options and disables built-in tools and undeclared MCP discovery. `DisableExecutionEnvironment` and `DisableSubagents` are accepted assertions about the single-Agent restrictive profile. Omission does not enable built-in tools. Explicit Subagent observation enables only the native delegation tools described under [Subagents](#subagents). Single-Agent new and resumed queries use the SDK's empty built-in tool set, explicit function MCP configuration and allowlist, strict MCP configuration and empty user/project/local setting sources. Without HTTP MCP declarations, native initialization and real provider request inventories must contain only the declared host functions. Managed operator policy may further restrict execution; it must not widen the profile. The profile limits model tool access; it does not isolate native state files or filesystem access by an explicitly supplied host function. The private factory accepts the execution controls in the [Claude declaration](../../internal/harnessconfig/claudesdk/declaration.go): disabled search, medium text verbosity and reasoning effort, which maps to the SDK's `effort` option. Search remains excluded by the native tool inventory; medium retains the SDK's default text generation, without adding instructions or changing caller input. The pinned SDK has no verbosity, summary or service-tier option. An undeclared value fails before native setup; omitting the block keeps the same restrictive profile. Use the SDK's history lookup before explicit resume; never fall back to a new Session. Native files remain device-affine under a caller-selected managed runtime directory. The launch configuration supplies trusted provider environment; request options cannot supply environment variables or business write authority. Omitted, null and empty `system_prompt` map to empty SDK instructions only at this adapter boundary; null model values and unsupported options remain rejected. +The `none` profile accepts only text, explicit model/system instructions, managed state, exact native resume and declared functions with ordered text or successful inline PNG, JPEG, GIF or WebP results, and the HTTP MCP subset described above. It rejects unsupported request options and disables built-in tools and undeclared MCP discovery. `DisableExecutionEnvironment` and `DisableSubagents` are accepted assertions about the single-Agent restrictive profile. Omission does not enable built-in tools. Explicit Subagent observation enables only the native delegation tools described under [Subagents](#subagents). Single-Agent new and resumed queries use the SDK's empty built-in tool set, explicit function MCP configuration and allowlist, strict MCP configuration and empty user/project/local setting sources. Without HTTP MCP declarations, native initialization and real provider request inventories must contain only the declared host functions plus the native ToolSearch and StructuredOutput tools when the request selects them. Managed operator policy may further restrict execution; it must not widen the profile. The profile limits model tool access; it does not isolate native state files or filesystem access by an explicitly supplied host function. The private factory accepts the execution controls in the [Claude declaration](../../internal/harnessconfig/claudesdk/declaration.go): disabled search, medium text verbosity and reasoning effort, which maps to the SDK's `effort` option. Search remains excluded by the native tool inventory; medium retains the SDK's default text generation, without adding instructions or changing caller input. The pinned SDK has no verbosity, summary or service-tier option. An undeclared value fails before native setup; omitting the block keeps the same restrictive profile. Use the SDK's history lookup before explicit resume; never fall back to a new Session. Native files remain device-affine under a caller-selected managed runtime directory. The launch configuration supplies trusted provider environment; request options cannot supply environment variables or business write authority. Omitted, null and empty `system_prompt` map to empty SDK instructions only at this adapter boundary; null model values and unsupported options remain rejected. ### Function server and results diff --git a/packages/claude-sdk-adapter/src/adapter.ts b/packages/claude-sdk-adapter/src/adapter.ts index 0a710f68a..b26039c1b 100644 --- a/packages/claude-sdk-adapter/src/adapter.ts +++ b/packages/claude-sdk-adapter/src/adapter.ts @@ -1,7 +1,6 @@ import { NativeFailure } from "./native_failure.js"; import { ExecutorTurns, type ExecutorEvent } from "./executor_protocol.js"; import { StructuredOutput } from "./structured_output.js"; -import { toolSearchEnvironment } from "./tool_search.js"; import { Subagents } from "./subagents.js"; import type { Fact } from "./subagent_history.js"; import { WorkspaceDirectories, type WorkspaceDirectoryEvent } from "./workspace_directories.js"; @@ -47,7 +46,7 @@ export async function execute(request: Start | Prepare | ExecutorPrepare, emit: let classifiedFailure: { engine_error_code: string; session_id: string; result_id: string } | undefined; const definitions = (request.functions ?? []).map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.parameters, deferLoading: tool.defer_loading })); const names = definitions.map(tool => `mcp__functions__${tool.name}`); - const allowed = [...names, ...(request.tool_search ? ["ToolSearch"] : [])]; + const search = request.tool_search ? ["ToolSearch"] : []; 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; @@ -117,7 +116,8 @@ export async function execute(request: Start | Prepare | ExecutorPrepare, emit: // Environment none keeps no workspace tools and approves only Core's function and MCP tools. The host's // settings, memory files and MCP servers belong to no Session. ...(workspace ? {} : { - tools: subagents ? ["Agent", "SendMessage"] : request.tool_search ? ["ToolSearch"] : [], allowedTools: profile?.allowed ?? allowed, + tools: [...(subagents ? ["Agent", "SendMessage"] : []), ...search], + allowedTools: [...(profile?.allowed ?? names), ...search], settingSources: [], strictMcpConfig: true, canUseTool: async () => ({ behavior: "deny" as const, message: "Tools are unavailable in this execution profile." }), }), @@ -136,7 +136,8 @@ export async function execute(request: Start | Prepare | ExecutorPrepare, emit: return child; }, }; - if (request.tool_search) options.env = toolSearchEnvironment(options.env ?? process.env, request.model); + // ENABLE_TOOL_SEARCH=true is the official mapping of tool_search; native owns discovery and provider policy. + if (request.tool_search) options.env = { ...(options.env ?? process.env), ENABLE_TOOL_SEARCH: "true" }; 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))); @@ -200,14 +201,15 @@ export async function execute(request: Start | Prepare | ExecutorPrepare, emit: if (message.type === "system" && message.subtype === "init") { nativeID = message.session_id; if (!nativeID || (request.resume && nativeID !== request.resume)) throw new Error("unexpected native session"); - if (profile) profile.verify(message.tools, await stream.mcpServerStatus(), nativeID, !workspace); + // Native decides whether ToolSearch and StructuredOutput join the inventory; a missing structured call fails the result. + const tools = message.tools.filter(name => name !== "ToolSearch" && name !== "StructuredOutput"); + if (profile) profile.verify(tools, await stream.mcpServerStatus(), nativeID, !workspace); if (workspace) { // Native tools and settings may add servers and tools, but every tool and Skill Core declared must still be present. - const declared = [...names, ...(workspace.skillNames.length ? ["Skill"] : []), ...(request.tool_search ? ["ToolSearch"] : []), - ...(structured ? ["StructuredOutput"] : []), ...(subagents ? ["Task", "SendMessage"] : [])]; + const declared = [...names, ...(workspace.skillNames.length ? ["Skill"] : []), ...(subagents ? ["Task", "SendMessage"] : [])]; if (declared.some(name => !message.tools.includes(name)) || workspace.skillNames.some(name => !message.skills?.includes(name)) || definitions.length && !message.mcp_servers.some(server => server.name === "functions" && server.status === "connected")) throw new Error("unexpected native configuration"); - } else if (!profile && (message.tools.length !== allowed.length + (subagents ? 2 : 0) + (structured ? 1 : 0) || message.tools.some(name => ![...allowed, ...(subagents ? ["Task", "SendMessage"] : []), ...(structured ? ["StructuredOutput"] : [])].includes(name)) || + } else if (!profile && (tools.length !== names.length + (subagents ? 2 : 0) || tools.some(name => ![...names, ...(subagents ? ["Task", "SendMessage"] : [])].includes(name)) || message.mcp_servers.length !== (definitions.length ? 1 : 0) || message.mcp_servers.some(server => server.name !== "functions" || server.status !== "connected"))) { throw new Error("unexpected native configuration"); diff --git a/packages/claude-sdk-adapter/src/mcp.ts b/packages/claude-sdk-adapter/src/mcp.ts index 0d6a07109..1eb85de63 100644 --- a/packages/claude-sdk-adapter/src/mcp.ts +++ b/packages/claude-sdk-adapter/src/mcp.ts @@ -31,7 +31,7 @@ export function parseHTTPServers(value: unknown): HTTPServer[] | undefined { server.server_label === "functions" || labels.has(server.server_label) || typeof server.server_url !== "string" || (server.allowed_tools !== null && (!Array.isArray(server.allowed_tools) || - server.allowed_tools.some((name: unknown) => typeof name !== "string" || !/^[a-zA-Z0-9_.-]+$/.test(name))))) { + server.allowed_tools.some((name: unknown) => typeof name !== "string")))) { throw new Error("invalid_request"); } const url = new URL(server.server_url); diff --git a/packages/claude-sdk-adapter/src/request.ts b/packages/claude-sdk-adapter/src/request.ts index e39e5e97b..3a6e2ee7a 100644 --- a/packages/claude-sdk-adapter/src/request.ts +++ b/packages/claude-sdk-adapter/src/request.ts @@ -53,8 +53,7 @@ export function parseRequest(line: string): Start | Prepare | ExecutorPrepare { !tool.parameters || tool.parameters.type !== "object"))) throw new Error("invalid_request"); 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.mcp_http_servers !== undefined || request.output_format))) throw new Error("invalid_request"); + (!!request.tool_search !== deferred)) 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 || @@ -63,15 +62,13 @@ export function parseRequest(line: string): Start | Prepare | ExecutorPrepare { if (request.output_format !== undefined) { const format = request.output_format as Start["output_format"]; if (!format || format.type !== "json_schema" || Object.keys(format).some(key => !["type", "schema"].includes(key)) || - !format.schema || format.schema.type !== "object" || !request.observe_messages || request.subagents || - request.mcp_http_servers !== undefined) throw new Error("invalid_request"); + !format.schema || format.schema.type !== "object" || !request.observe_messages) throw new Error("invalid_request"); } if (request.type === "start") requestInput(request.input); parseNativeModelOptions(request.native_model_options); 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 || 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/tool_search.ts b/packages/claude-sdk-adapter/src/tool_search.ts deleted file mode 100644 index d95d4f6ff..000000000 --- a/packages/claude-sdk-adapter/src/tool_search.ts +++ /dev/null @@ -1,12 +0,0 @@ -// The native harness owns discovery, schema loading and its model/provider policy. -// Reject known conflicting modes, without copying its dynamic policy evaluator. -export function toolSearchEnvironment(env: NodeJS.ProcessEnv, model: string): NodeJS.ProcessEnv { - const enabled = (value: string | undefined) => !!value && !["0", "false"].includes(value.toLowerCase()); - if (["CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY", - "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"].some(key => enabled(env[key])) || - (env.ENABLE_TOOL_SEARCH && env.ENABLE_TOOL_SEARCH !== "true") || - ["claude-3-haiku", "claude-3-5-haiku"].some(name => model.toLowerCase().includes(name))) { - throw new Error("unqualified native tool search configuration"); - } - return { ...env, ENABLE_TOOL_SEARCH: "true" }; -} diff --git a/packages/claude-sdk-adapter/tests/mcp.test.mjs b/packages/claude-sdk-adapter/tests/mcp.test.mjs index 6029ba41b..75fcffe27 100644 --- a/packages/claude-sdk-adapter/tests/mcp.test.mjs +++ b/packages/claude-sdk-adapter/tests/mcp.test.mjs @@ -27,7 +27,7 @@ test("native selection composes unrestricted, selected and empty servers with ho test("invalid remote declarations and wildcard injection fail at the bridge boundary", () => { const start = { type: "start", input: [{ content: [{ type: "input_text", text: "hello" }] }], model: "model", system_prompt: "", cwd: "/tmp" }; - for (const value of [null, {}, [declaration(["*"])], [{ ...declaration(null), server_label: "functions" }], + for (const value of [null, {}, [{ ...declaration(null), server_label: "functions" }], [{ ...declaration(null), server_url: "https://user:secret@example.invalid/mcp" }], [{ ...declaration(null), server_url: "https://example.invalid/mcp#fragment" }], [{ ...declaration(null), required: "true" }], [{ ...declaration(null), required: null }], [declaration(null), declaration([])]]) { @@ -37,6 +37,10 @@ test("invalid remote declarations and wildcard injection fail at the bridge boun [declaration(null), { ...declaration([]), server_label: "empty" }]); const withQuery = { ...declaration(null), server_url: "https://example.invalid/mcp?tenant=a" }; assert.deepEqual(parseStart(JSON.stringify({ ...start, mcp_http_servers: [withQuery] })).mcp_http_servers, [withQuery]); + // Native normalization turns any tool name into a literal identity, so a wildcard cannot widen the allowlist. + const wildcard = declaration(["*", "a/b"]); + assert.deepEqual(parseStart(JSON.stringify({ ...start, mcp_http_servers: [wildcard] })).mcp_http_servers, [wildcard]); + assert.deepEqual(new MCPProfile([wildcard], []).allowed, [native("_"), native("a_b")]); }); test("native tool spelling preserves original identity and rejects ambiguous aliases", () => { diff --git a/packages/claude-sdk-adapter/tests/mcp_workspace.test.mjs b/packages/claude-sdk-adapter/tests/mcp_workspace.test.mjs index b0e04b9c3..1d74fe9ea 100644 --- a/packages/claude-sdk-adapter/tests/mcp_workspace.test.mjs +++ b/packages/claude-sdk-adapter/tests/mcp_workspace.test.mjs @@ -31,8 +31,8 @@ function fixture(t, declarations = [stdio]) { test("installed MCP projection uses the common Runtime launcher", t => { const { request } = fixture(t); assert.deepEqual(parseStart(JSON.stringify(request)), request); - assert.throws(() => parseStart(JSON.stringify({ ...request, observe_messages: true, - output_format: { type: "json_schema", schema: { type: "object" } } })), /invalid_request/); + const structured = { ...request, observe_messages: true, output_format: { type: "json_schema", schema: { type: "object" } } }; + assert.deepEqual(parseStart(JSON.stringify(structured)), structured); assert.equal(immediateInput(request), undefined); assert.deepEqual(parseEnvironmentMCP([stdio]), [stdio]); for (const value of [[stdio, stdio], [{ ...stdio, command: "relative" }], [{ ...stdio, env: { TOKEN: "secret" } }], diff --git a/packages/claude-sdk-adapter/tests/structured_output.test.mjs b/packages/claude-sdk-adapter/tests/structured_output.test.mjs index 878c9fcd9..eea3b3e62 100644 --- a/packages/claude-sdk-adapter/tests/structured_output.test.mjs +++ b/packages/claude-sdk-adapter/tests/structured_output.test.mjs @@ -21,8 +21,9 @@ test('unrelated or failed results cannot publish a candidate',()=>{ const o=new StructuredOutput();o.consume(call('id'),'session');o.consume(receipt('id'),'session');assert.throws(()=>o.complete(r)); } }); -test('output configuration is restricted to the qualified profile',()=>{ +test('output configuration requires message observation and combines with agents and MCP',()=>{ const r={type:'start',input: [{ content: [{ type: "input_text", text: 'hello' }] }],model:'model',system_prompt:'',cwd:'/tmp',observe_messages:true,output_format:{type:'json_schema',schema:{type:'object'}}}; assert.deepEqual(parseStart(JSON.stringify(r)),r); - for(const change of [{observe_messages:false},{subagents:{max_concurrent:1}},{mcp_http_servers:[]},{output_format:{type:'text',schema:{}}}]) assert.throws(()=>parseStart(JSON.stringify({...r,...change}))); + for(const change of [{subagents:{max_concurrent:1}},{mcp_http_servers:[]}]) assert.deepEqual(parseStart(JSON.stringify({...r,...change})),{...r,...change}); + for(const change of [{observe_messages:false},{output_format:{type:'text',schema:{}}}]) assert.throws(()=>parseStart(JSON.stringify({...r,...change}))); }); diff --git a/packages/claude-sdk-adapter/tests/tool_search.test.mjs b/packages/claude-sdk-adapter/tests/tool_search.test.mjs index 7e184f247..4d952a042 100644 --- a/packages/claude-sdk-adapter/tests/tool_search.test.mjs +++ b/packages/claude-sdk-adapter/tests/tool_search.test.mjs @@ -1,26 +1,19 @@ import assert from "node:assert/strict"; import test from "node:test"; import { parseStart } from "../dist/request.js"; -import { toolSearchEnvironment } from "../dist/tool_search.js"; const request = () => ({type:"start", input:[{content:[{type:"input_text",text:"lookup"}]}], model:"model", system_prompt:"", cwd:process.cwd(), tool_search:true, functions:[{name:"lookup",description:"Lookup",parameters:{type:"object"},defer_loading:true}]}); -test("discovery preserves intent and rejects unqualified native combinations", () => { +test("discovery preserves intent and rejects a mismatch with deferred functions", () => { assert.deepEqual(parseStart(JSON.stringify(request())), request()); - for (const patch of [{tool_search:false}, {tool_search:null}, {output_format:{type:"json_schema",schema:{type:"object"}}}, - {mcp_http_servers:[]}, {subagents:{max_concurrent:2}}, {functions:[{...request().functions[0],defer_loading:false}]}]) { + for (const patch of [{tool_search:false}, {tool_search:null}, {functions:[{...request().functions[0],defer_loading:false}]}]) { assert.throws(() => parseStart(JSON.stringify({...request(),...patch})), /invalid_request/); } }); -test("native search uses explicit mode without overriding conflicting operator settings", () => { - const env = {ANTHROPIC_BASE_URL:"https://example.test/anthropic", RETAIN:"yes"}; - assert.deepEqual(toolSearchEnvironment(env,"model"), {...env, ENABLE_TOOL_SEARCH:"true"}); - assert.equal(env.ENABLE_TOOL_SEARCH,undefined); - for (const patch of [{ENABLE_TOOL_SEARCH:"false"},{ENABLE_TOOL_SEARCH:"force"},{ENABLE_TOOL_SEARCH:"auto"}, - {CLAUDE_CODE_USE_BEDROCK:"1"},{CLAUDE_CODE_USE_VERTEX:"1"},{CLAUDE_CODE_USE_FOUNDRY:"1"},{CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS:"true"}]) { - assert.throws(() => toolSearchEnvironment({...env,...patch},"model"), /unqualified/); - } - assert.throws(() => toolSearchEnvironment(env,"claude-3-5-haiku-latest"), /unqualified/); +test("discovery combines with structured output and MCP", () => { + const combined = {...request(), observe_messages:true, output_format:{type:"json_schema",schema:{type:"object"}}, + mcp_http_servers:[{server_label:"docs",server_url:"https://example.test/mcp",allowed_tools:null}]}; + assert.deepEqual(parseStart(JSON.stringify(combined)), combined); }); diff --git a/packages/claude-sdk-adapter/tests/workspace_execution.test.mjs b/packages/claude-sdk-adapter/tests/workspace_execution.test.mjs index ca4cb7608..07a45deed 100644 --- a/packages/claude-sdk-adapter/tests/workspace_execution.test.mjs +++ b/packages/claude-sdk-adapter/tests/workspace_execution.test.mjs @@ -26,8 +26,7 @@ const workspace = { home: dirs.home, state: dirs.state, scratch: dirs.scratch, skills: [{ metadata: { type: "inline", name: "fixture", description: "Fixture." }, relative_root: "pkg/skill", package_root: "pkg" }] } : {}) }; const request = { type: "start", input: [{ content: [{ type: "input_text", text: "fixture" }] }], model: "fixture", system_prompt: mode === "instructions" ? "Be brief." : "", cwd: dirs.workspace, workspace, ...(mode.startsWith("resume") ? { resume: "native" } : {}), - ...(mode.startsWith("functions") || mode === "search-missing" ? { functions: [{ name: "lookup", description: "Lookup", parameters: { type: "object" }, defer_loading: mode === "search-missing" }] } : {}), - ...(mode === "search-missing" ? { tool_search: true } : {}) }; + ...(mode.startsWith("functions") ? { functions: [{ name: "lookup", description: "Lookup", parameters: { type: "object" }, defer_loading: false }] } : {}) }; process.env.HOME = dirs.home; process.env.CLAUDE_CONFIG_DIR = dirs.state; process.env.ANTHROPIC_API_KEY = "fixture-secret"; @@ -59,7 +58,7 @@ globalThis.queryFixture = ({prompt, options}) => { return { close() { child.stdin.end(); }, async *[Symbol.asyncIterator]() { const first = (await prompt[Symbol.asyncIterator]().next()).value; yield { type: "system", subtype: "init", session_id: "native", mcp_servers: request.functions ? [{ name: "functions", status: "connected" }] : [], - tools: ["Bash", "Read", "Edit", ...(mode === "functions" || mode === "search-missing" ? ["mcp__functions__lookup"] : []), ...(mode.startsWith("skills") ? ["Skill"] : [])], + tools: ["Bash", "Read", "Edit", ...(mode === "functions" ? ["mcp__functions__lookup"] : []), ...(mode.startsWith("skills") ? ["Skill"] : [])], // A project's settings can disable Core's Skill plugin while the Skill tool stays. skills: mode === "skills" ? ["environment-skills-0:fixture"] : [] }; if (mode.startsWith("commands")) { @@ -100,14 +99,14 @@ try { assert.deepEqual(observations.at(-1).observation,{kind:"command",status:"incomplete",command:"printf 'observed'"}); assert.deepEqual(events.at(-1),{type:"error",code:mode === "commands-cancel"?"cancelled":"execution_failed"}); } - } else if (mode === "functions-missing" || mode === "search-missing" || mode === "skills-missing") assert.deepEqual(events.at(-1), {type:"error",code:"execution_failed"}); + } else if (mode === "functions-missing" || mode === "skills-missing") assert.deepEqual(events.at(-1), {type:"error",code:"execution_failed"}); else assert.equal(events.at(-1).type, "result"); } } } finally { rmSync(root, {recursive:true,force:true}); } `; -for (const mode of ["fresh", "instructions", "resume-existing", "resume-missing", "resume-mismatch", "commands-success", "commands-cancel", "commands-missing", "functions", "functions-missing", "search-missing", "skills", "skills-missing"]) { +for (const mode of ["fresh", "instructions", "resume-existing", "resume-missing", "resume-mismatch", "commands-success", "commands-cancel", "commands-missing", "functions", "functions-missing", "skills", "skills-missing"]) { test(`workspace execution boundary: ${mode}`, { timeout: 15000 }, () => { const child = spawnSync(process.execPath, ["--input-type=module", "-e", fixture, mode], { encoding: "utf8", timeout: 10000 }); assert.equal(child.status, 0, child.stderr || child.error?.message); diff --git a/services/core/internal/api/claude_admission_test.go b/services/core/internal/api/claude_admission_test.go index 683af40e1..bb467664a 100644 --- a/services/core/internal/api/claude_admission_test.go +++ b/services/core/internal/api/claude_admission_test.go @@ -23,8 +23,6 @@ func TestClaudeSessionConfigurationAdmission(t *testing.T) { {"schema", `,"text":{"format":{"type":"json_schema","schema":{"type":"object","properties":{"value":{"type":"string"}}}}}`, true}, {"lossy schema", `,"text":{"format":{"type":"json_schema","schema":{"type":"object","const":9007199254740993}}}`, false}, {"array schema", `,"text":{"format":{"type":"json_schema","schema":{"type":"array"}}}`, false}, - {"schema and MCP", `,"text":{"format":{"type":"json_schema","schema":{"type":"object"}}},"tools":[` + publicMCP + `]`, false}, - {"schema and subagents", `,"text":{"format":{"type":"json_schema","schema":{"type":"object"}}},"multi_agent":{"enabled":true}`, false}, {"medium", `,"text":{"verbosity":"medium"}`, true}, {"low", `,"text":{"verbosity":"low"}`, false}, {"high", `,"text":{"verbosity":"high"}`, false}, @@ -38,7 +36,6 @@ func TestClaudeSessionConfigurationAdmission(t *testing.T) { {"MCP required", `,"tools":[` + strings.TrimSuffix(publicMCP, "}") + `,"required":true}]`, true}, {"MCP reserved label", `,"tools":[` + strings.Replace(publicMCP, `"records"`, `"functions"`, 1) + `]`, false}, {"MCP invalid label", `,"tools":[` + strings.Replace(publicMCP, `"records"`, `"records.v1"`, 1) + `]`, false}, - {"MCP wildcard name", `,"tools":[` + strings.TrimSuffix(publicMCP, "}") + `,"allowed_tools":["*"]}]`, false}, {"MCP empty fragment", `,"tools":[` + strings.Replace(publicMCP, `/tools"`, `/tools#"`, 1) + `]`, false}, } { t.Run(fmt.Sprintf("%s/stream=%t/input=%s", test.name, stream, input), func(t *testing.T) { diff --git a/services/core/internal/api/hosted_structured_test.go b/services/core/internal/api/hosted_structured_test.go deleted file mode 100644 index 16fe09fed..000000000 --- a/services/core/internal/api/hosted_structured_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package api - -import ( - "encoding/json" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" -) - -func TestHostedStructuredConfigurationQualification(t *testing.T) { - format := `"text":{"format":{"type":"json_schema","schema":{"type":"object"}}}` - for _, fields := range []string{ - `"skills":[` + string(skillInput(t, "Use native tools.")) + `]`, - `"plugins":[` + string(pluginInput(t)) + `]`, - `"capability_directories":["/workspace/capabilities"]`, - } { - template, err := decodeTemplateInput(json.RawMessage(`{` + fields + `}`)) - if err != nil { - t.Fatal(err) - } - lookup := &templateLookup{network: "enabled", skills: template.Setup.Skills, - plugins: template.Setup.Plugins, directories: template.Setup.CapabilityDirectories} - for _, environment := range []string{ - `{"type":"openai_hosted",` + fields + `}`, - `{"type":"openai_hosted","environment_template_id":"saved"}`, - `{"type":"openai_hosted","environment_template_id":"saved","skills":[],"plugins":[],"capability_directories":[]}`, - } { - var decoded decodedSessionRequest - if err := json.Unmarshal([]byte(`{"agent":{"model":"model",`+format+`},"environment":`+environment+`}`), &decoded); err != nil { - t.Fatal(err) - } - input, err := decoded.validated() - if err != nil { - t.Fatal(err) - } - if input.templateID != "" { - if err := applyTemplateEnvironment(&input, lookup.resolved()); err != nil { - t.Fatal(err) - } - } - configuration, err := resolve(input, "tenant", "key", nil) - if err != nil { - t.Fatal(err) - } - wantAccepted := len(input.Environment.Skills)+len(input.Environment.Plugins)+len(input.Environment.CapabilityDirectories) == 0 - if err := (execution.Policy{}).ValidateSessionConfiguration("claude_sdk", configuration); (err == nil) != wantAccepted { - t.Fatal("incorrect inline/template qualification", environment, err) - } - if !wantAccepted { - input.Agent.Text.Format = json.RawMessage(`{"type":"text"}`) - configuration, err = resolve(input, "tenant", "key", nil) - if err != nil { - t.Fatal(err) - } - if err := (execution.Policy{}).ValidateSessionConfiguration("claude_sdk", configuration); err != nil { - t.Fatal("ordinary workspace profile changed", err) - } - } - } - } -} diff --git a/services/core/internal/execution/environment_capability_profiles_test.go b/services/core/internal/execution/environment_capability_profiles_test.go index 96fef1b16..ad8c9fe39 100644 --- a/services/core/internal/execution/environment_capability_profiles_test.go +++ b/services/core/internal/execution/environment_capability_profiles_test.go @@ -25,8 +25,9 @@ func TestQualifiedClaudeCapabilitiesDoNotDependOnEnvironmentSource(t *testing.T) } 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) + // Structured output is native beside Subagents; function tools are not carried by the Subagent profile. + if err := (Policy{}).ValidateSessionConfiguration("claude_sdk", raw); (err == nil) != (feature == "structured") { + t.Fatalf("%s with Subagents: %v", feature, err) } } }) diff --git a/services/core/internal/execution/harness_admission.go b/services/core/internal/execution/harness_admission.go index 10d2e0d21..9ec1aa343 100644 --- a/services/core/internal/execution/harness_admission.go +++ b/services/core/internal/execution/harness_admission.go @@ -23,10 +23,7 @@ func admissionEnvironment(environment *v1.Environment) *harnessconfig.AdmissionE if environment == nil { return nil } - return &harnessconfig.AdmissionEnvironment{ - Type: environment.Type, - Extensions: len(environment.Skills) != 0 || len(environment.Plugins) != 0 || len(environment.CapabilityDirectories) != 0, - } + return &harnessconfig.AdmissionEnvironment{Type: environment.Type} } func admissionOf(snapshot Snapshot) harnessconfig.Admission {