From 98510acd5b1950fbf64ad780fd85470cc2edbf32 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 04:01:35 +0000 Subject: [PATCH] Let Claude Code run live web search --- .../claudesdk/execution_controls_test.go | 19 ++++++++++++++++++- .../internal/agent/claudesdk/options.go | 5 ++++- .../agent/claudesdk/preparation_test.go | 2 +- contracts/agents-api/execution-tools.md | 2 +- contracts/agents-api/harness-capabilities.md | 2 +- contracts/agents-api/zh/execution-tools.md | 4 ++-- .../agents-api/zh/harness-capabilities.md | 4 ++-- docs/api/public-agent-api.md | 2 +- docs/zh/api/public-agent-api.md | 4 ++-- .../harnessconfig/claudesdk/declaration.go | 17 +++++++++++------ .../execution/harness_admission_test.go | 12 ++++++++++++ 11 files changed, 55 insertions(+), 18 deletions(-) diff --git a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go index 1cf817d6c..e7ed164ab 100644 --- a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go +++ b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go @@ -44,11 +44,28 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) { } } +func TestLiveWebSearchKeepsNativeToolsOnlyOnAWorkspace(t *testing.T) { + config := workspaceFixture(t) + request := workspaceRequest() + ordinary, _, err := prepare(config, request) + if err != nil { + t.Fatal(err) + } + request.ExecutionControls = &proto.ExecutionControls{WebSearch: &proto.WebSearchControl{Mode: "live", ContextSize: "medium"}, TextVerbosity: "medium"} + live, _, err := prepare(config, request) + if err != nil || !reflect.DeepEqual(ordinary, live) { + t.Fatal("live search changed the native web tools", err) + } + config.Workspace = nil + if _, _, err := prepare(config, request); err == nil || !strings.Contains(err.Error(), "live web search requires a dedicated workspace") { + t.Fatal("live search without a workspace was accepted", err) + } +} + func TestExecutionControlsRejectUnsupportedProfilesBeforeLaunch(t *testing.T) { enabled := true cases := map[string]proto.ExecutionControls{ "cached-search": {WebSearch: &proto.WebSearchControl{Mode: "cached", ContextSize: "medium"}}, - "live-search": {WebSearch: &proto.WebSearchControl{Mode: "live", ContextSize: "medium"}}, "enabled-ptc": {ProgrammaticToolCalling: &enabled}, "low-verbosity": {TextVerbosity: "low"}, "high-verbosity": {TextVerbosity: "high"}, diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index b542f8950..a52b7f2b2 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -82,8 +82,11 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR if err := validateMCP(req); err != nil { return startRequest{}, nil, err } - // Without a search control Claude Code keeps its native web tools. + // Without a search control Claude Code keeps its native web tools, which only a workspace runs. if controls := req.ExecutionControls; controls != nil && controls.WebSearch != nil { + if controls.WebSearch.Mode == "live" && config.Workspace == nil { + return fail("live web search requires a dedicated workspace") + } start.DisableWebSearch = controls.WebSearch.Mode == "disabled" } if req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil { diff --git a/apps/daemon/internal/agent/claudesdk/preparation_test.go b/apps/daemon/internal/agent/claudesdk/preparation_test.go index 2d88c6024..8040a0b4d 100644 --- a/apps/daemon/internal/agent/claudesdk/preparation_test.go +++ b/apps/daemon/internal/agent/claudesdk/preparation_test.go @@ -117,7 +117,7 @@ func TestPreparationRejectsInputAndUnavailableProfilesBeforeLaunch(t *testing.T) case "mcp": req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "remote", ServerURL: "https://example.test/mcp"}} case "controls": - req.ExecutionControls = &proto.ExecutionControls{WebSearch: &proto.WebSearchControl{Mode: "live", ContextSize: "medium"}} + req.ExecutionControls = &proto.ExecutionControls{WebSearch: &proto.WebSearchControl{Mode: "cached", ContextSize: "medium"}} } if _, err := NewExecutorFactory(config)(t.Context(), req); err == nil { t.Fatal("invalid preparation was accepted") diff --git a/contracts/agents-api/execution-tools.md b/contracts/agents-api/execution-tools.md index d71a107f7..17e613bd8 100644 --- a/contracts/agents-api/execution-tools.md +++ b/contracts/agents-api/execution-tools.md @@ -67,7 +67,7 @@ Execution carries the resolved values to the Harness and validates them against | Harness | Web search | Programmatic tool calling | | --- | --- | --- | | Codex | `disabled`, `cached` and `live`, with `context_size`, `allowed_domains` and `location`, as native `web_search` and `tools.web_search` settings | `enabled: true` turns on native `code_mode`; `false` turns off `code_mode`, `code_mode_only` and `code_mode_prewarm` and checks native managed requirements before a thread starts or resumes | -| Claude Code | `disabled` only. On a workspace placement the native `claude_code` tool set runs without ask-user, scheduling, workflow, background and `ListAgents` tools, and `disabled` removes `WebSearch` and `WebFetch`; `environment: none` keeps no built-in tools | `false` only | +| Claude Code | `disabled` and `live`, with `context_size: "medium"` and no `allowed_domains` or `location`. On a workspace placement the native `claude_code` tool set runs without ask-user, scheduling, workflow, background and `ListAgents` tools; `disabled` removes `WebSearch` and `WebFetch`, and `live` keeps them. `environment: none` keeps no built-in tools and rejects `live` | `false` only | | MiniMax Code | `disabled` only, through the native `features.webSearch=false` because the restricted profile has no search tool | `false` only | ## HTTP MCP diff --git a/contracts/agents-api/harness-capabilities.md b/contracts/agents-api/harness-capabilities.md index 26f473ab7..ed3e0595c 100644 --- a/contracts/agents-api/harness-capabilities.md +++ b/contracts/agents-api/harness-capabilities.md @@ -38,7 +38,7 @@ Native model parameters and provider protocols per Harness are in [model executi | [Structured output](./execution-tools.md#structured-output) | Rejected | Verified on all placements | Rejected | | [Deferred function discovery](./execution-tools.md#deferred-function-discovery) | Rejected | Verified: `none`, self-hosted; admitted: hosted | Rejected | | [Disabled web search and programmatic tool calling](./execution-tools.md#web-search-and-programmatic-tool-calling) | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | -| Web search `cached` or `live` and programmatic tool calling `enabled` | Admitted on all placements | Rejected | Rejected | +| Web search `cached` or `live` and programmatic tool calling `enabled` | Admitted on all placements | `live` admitted on hosted and self-hosted, rejected on `none`; `cached` and `enabled` rejected | Rejected | | [Service-origin HTTP MCP](./environments.md#public-mcp-connection-origin) | Verified: `none`; rejected elsewhere | Verified: `none`; rejected elsewhere | Rejected | | [Environment-origin HTTP MCP](./environments.md#public-mcp-connection-origin) | Verified: hosted, self-hosted | Verified: hosted, self-hosted | Verified: hosted, self-hosted; `allowed_tools` null and `required` false only | | [HTTP MCP bearer credentials](./execution-tools.md#http-mcp) | Verified | Verified | Verified | diff --git a/contracts/agents-api/zh/execution-tools.md b/contracts/agents-api/zh/execution-tools.md index d4a280795..b55b2bc58 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: ae8c47681587ccd0e7084c883be63a1f7064c1e780a5516536736943d8d45e08 +source_hash: ad7fb570c29fc3d346ad406e794b15c7bdabd667634b2ac0e32506e52d964aaa --- Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在 `text.format` 中声明输出 schema。本契约说明 Core 如何验证声明、哪些内容跨越 Runtime 边界,以及调用方如何恢复待执行操作。[Harness 能力](harness-capabilities.md)列出各 Harness 在不同部署位置支持的操作。原生工作区工具和 Environment Plugin MCP 属于 [Environment](environments.md#skills-plugins-and-environment-mcp)。 @@ -69,7 +69,7 @@ Core 发送 `PromptRequestPayload.ToolSearch` 和每个 `FunctionTool.DeferLoadi | Harness | Web 搜索 | 程序化工具调用 | | --- | --- | --- | | Codex | `disabled`、`cached` 和 `live`,以及 `context_size`、`allowed_domains` 和 `location`,对应原生 `web_search` 与 `tools.web_search` 设置 | `enabled: true` 开启原生 `code_mode`;`false` 关闭 `code_mode`、`code_mode_only` 和 `code_mode_prewarm`,并在启动或恢复线程前检查原生受管要求 | -| Claude Code | 仅 `disabled`。在工作区位置运行原生 `claude_code` 工具集,但移除向用户提问、定时、工作流、后台和 `ListAgents` 工具,`disabled` 会移除 `WebSearch` 和 `WebFetch`;`environment: none` 不保留内置工具 | 仅 `false` | +| Claude Code | `disabled` 和 `live`,需使用 `context_size: "medium"`,且不支持 `allowed_domains` 或 `location`。在工作区位置运行原生 `claude_code` 工具集,但移除向用户提问、定时、工作流、后台和 `ListAgents` 工具;`disabled` 会移除 `WebSearch` 和 `WebFetch`,`live` 保留它们。`environment: none` 不保留内置工具,并拒绝 `live` | 仅 `false` | | MiniMax Code | 仅 `disabled`,受限配置没有搜索工具,通过原生 `features.webSearch=false` 实现 | 仅 `false` | ## HTTP MCP {#http-mcp} diff --git a/contracts/agents-api/zh/harness-capabilities.md b/contracts/agents-api/zh/harness-capabilities.md index 9ff421073..77c4a4520 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: ebe62fa0536333b51a80233883039ed732a6ce7232030cd90e8f8854dfde9859 +source_hash: c76b7f62749647860c2623546574eadf28552aa4e525dad072dfc637091bb4b0 --- 本页列出每个 Harness 在每种部署位置支持的能力。Core 根据 `internal/harnessconfig//declaration.go` 中 Harness 的声明决定准入,运行 Session 的 Runtime 也必须声明操作。所链接契约定义各操作;[Harness 接入](harness-onboarding.md#qualify-the-adapter)说明资格验证方法。 @@ -40,7 +40,7 @@ source_hash: ebe62fa0536333b51a80233883039ed732a6ce7232030cd90e8f8854dfde9859 | [结构化输出](execution-tools.md#structured-output) | 已拒绝 | 所有位置已验证 | 已拒绝 | | [延迟函数发现](execution-tools.md#deferred-function-discovery) | 已拒绝 | 已验证:`none`、自托管;已准入:托管 | 已拒绝 | | [禁用 Web 搜索与程序化工具调用](execution-tools.md#web-search-and-programmatic-tool-calling) | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | -| Web 搜索 `cached` 或 `live`,以及程序化工具调用 `enabled` | 所有位置已准入 | 已拒绝 | 已拒绝 | +| Web 搜索 `cached` 或 `live`,以及程序化工具调用 `enabled` | 所有位置已准入 | `live` 在托管和自托管已准入,在 `none` 已拒绝;`cached` 和 `enabled` 已拒绝 | 已拒绝 | | [服务端来源 HTTP MCP](environments.md#public-mcp-connection-origin) | 已验证:`none`;其他位置拒绝 | 已验证:`none`;其他位置拒绝 | 已拒绝 | | [Environment 来源 HTTP MCP](environments.md#public-mcp-connection-origin) | 已验证:托管、自托管 | 已验证:托管、自托管 | 已验证:托管、自托管;仅 `allowed_tools` null、`required` false | | [HTTP MCP bearer 凭据](execution-tools.md#http-mcp) | 已验证 | 已验证 | 已验证 | diff --git a/docs/api/public-agent-api.md b/docs/api/public-agent-api.md index 7380bf9b7..8bda176b3 100644 --- a/docs/api/public-agent-api.md +++ b/docs/api/public-agent-api.md @@ -206,7 +206,7 @@ agents "/agents" -d '{ - **Update** changes only the fields you send. `metadata` replaces all pairs; `null` clears `name` or `instructions`. - **Delete** keeps existing Sessions. -- **Tools** are functions, MCP servers, `tool_search` (Claude) and `web_search` with `mode: "disabled"`. Support depends on Harness and Environment; see [Execution tools](../../contracts/agents-api/execution-tools.md). +- **Tools** are functions, MCP servers, `tool_search` (Claude) and `web_search`. Support depends on Harness and Environment; see [Execution tools](../../contracts/agents-api/execution-tools.md). ## Sessions diff --git a/docs/zh/api/public-agent-api.md b/docs/zh/api/public-agent-api.md index 7f42c7b65..8f535efdf 100644 --- a/docs/zh/api/public-agent-api.md +++ b/docs/zh/api/public-agent-api.md @@ -1,7 +1,7 @@ --- title: "Agents API 指南" source: docs/api/public-agent-api.md -source_hash: ad2aea1d04536a7b12079a14f9926fc91b5202d394c1cd132d6c0a004a489451 +source_hash: 926187936e4c0aa8ac5298a1156463a15e893add8df2e4665bbc813204e4a6cd --- Core 在 `/v1` 提供 OpenAI Agents API。本指南用官方 OpenAI SDK 和普通 HTTP 两种方式演示每项常见任务,并说明 Core 与 OpenAI 的差异。如果尚未启动过 Session,请先阅读[运行第一个 Session](../getting-started/quickstart.md)。 @@ -208,7 +208,7 @@ agents "/agents" -d '{ - **更新**只会更改你发送的字段。`metadata` 会替换所有键值对;`null` 会清除 `name` 或 `instructions`。 - **删除**不会影响现有 Session。 -- **工具**包括函数、MCP 服务器、`tool_search`(Claude)以及设置了 `mode: "disabled"` 的 `web_search`。支持情况取决于 Harness 和 Environment;请参阅 [执行工具](../../../contracts/agents-api/zh/execution-tools.md)。 +- **工具**包括函数、MCP 服务器、`tool_search`(Claude)以及 `web_search`。支持情况取决于 Harness 和 Environment;请参阅 [执行工具](../../../contracts/agents-api/zh/execution-tools.md)。 ## Sessions {#sessions} diff --git a/internal/harnessconfig/claudesdk/declaration.go b/internal/harnessconfig/claudesdk/declaration.go index b155f8192..bfdc298c5 100644 --- a/internal/harnessconfig/claudesdk/declaration.go +++ b/internal/harnessconfig/claudesdk/declaration.go @@ -51,13 +51,13 @@ var Declaration = harnessconfig.Declaration{ MCPHTTPRequired: proto.CapabilitySupported, MCPHTTPBearerAuth: proto.CapabilitySupported, }, - // The SDK takes effort low to max. It has no verbosity, summary, service - // tier, search mode or search setting, and enabling programmatic tool calling - // is not wired. Native search has no cached mode or settings, so disabled is - // the only selectable mode and an absent control keeps native search; medium - // is the official default verbosity and needs no setting. + // The SDK takes effort low to max. It has no verbosity, summary or service + // tier, and enabling programmatic tool calling is not wired. Native WebSearch + // and WebFetch are live and have no cached mode or settings: disabled removes + // them, while live and an absent control keep them. Medium is the official + // default verbosity and needs no setting. Execution: harnessconfig.ExecutionOptions{ - WebSearchModes: []string{"disabled"}, + WebSearchModes: []string{"disabled", "live"}, ReasoningEfforts: []string{"low", "medium", "high", "xhigh", "max"}, ProgrammaticToolCalling: []bool{false}, TextVerbosities: []string{"medium"}, @@ -107,10 +107,15 @@ func validateConfiguration(admission harnessconfig.Admission) error { for _, raw := range admission.Tools { var tool struct { Type string `json:"type"` + Mode string `json:"mode"` } if json.Unmarshal(raw, &tool) != nil { return harnessconfig.ErrInvalidInput } + // environment:none runs no native tools, so live search has no home there. + 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") } diff --git a/services/core/internal/execution/harness_admission_test.go b/services/core/internal/execution/harness_admission_test.go index 7f5fcdb25..da29a1087 100644 --- a/services/core/internal/execution/harness_admission_test.go +++ b/services/core/internal/execution/harness_admission_test.go @@ -85,6 +85,18 @@ func TestClaudeHostedToolsRequireSeparateSupport(t *testing.T) { } } +func TestClaudeLiveSearchRequiresAWorkspace(t *testing.T) { + for _, mode := range []string{"disabled", "cached", "live"} { + for _, placement := range []string{"none", "openai_hosted"} { + raw := json.RawMessage(`{"agent":{"model":"fixture","tools":[{"type":"web_search","mode":"` + mode + `","context_size":"medium"}]},"environment":{"type":"` + placement + `"}}`) + want := mode == "disabled" || (mode == "live" && placement != "none") + if err := (Policy{}).ValidateSessionConfiguration("claude_sdk", raw); (err == nil) != want { + t.Fatalf("%s/%s: %v", mode, placement, err) + } + } + } +} + func TestValidatorPrecedencePreservesErrors(t *testing.T) { snapshot := Snapshot{Agent: v1.Agent{Tools: []json.RawMessage{json.RawMessage(`{"type":"unknown"}`)}}} _, decodeErr := executionTools(snapshot.Agent.Tools)