Skip to content

[#650][CODE] 云 web-search 闸改按真实引擎 id 命中 —— 空闸门 + 从未执行的契约校验 - #739

Open
jinjunnn wants to merge 2 commits into
alphafrom
fix/650-websearch-gate-real-engine-id
Open

[#650][CODE] 云 web-search 闸改按真实引擎 id 命中 —— 空闸门 + 从未执行的契约校验#739
jinjunnn wants to merge 2 commits into
alphafrom
fix/650-websearch-gate-real-engine-id

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

Fixes #650
Refs #223

这修的是什么(大白话)

云端的 web search 工具有一道"总开关关掉时把它藏起来"的闸,和一道"云工具返回的数据必须符合契约"的检查。两道闸都在盯一个不存在的名字,所以从落地那天起一次也没生效过。

名字为什么错:引擎给远端 MCP 工具起名是 sanitize(server) + "_" + sanitize(远端名)packages/opencode/src/mcp/catalog.ts:119)。alpha 的 server 叫 cloud,而云 worker 自己 advertise 的工具名本来就叫 cloud_web_search#643 P1.3 实测)。两个一拼,引擎里的真实 id 是 cloud_cloud_web_search#643 P2.1 实测 calledTool)。仓里把"远端名"和"引擎 id"当成了同一个东西。

后果两条:

  • permission deny 是空闸门。 kill-switch 下写进配置的键是 cloud_web_search,经 Wildcard.match 编成 ^cloud_web_search$,对真实 id 恒不匹配 ⇒ 云 web search 从来没有从模型工具表里消失过。本地跑真的 Permission.fromConfig + Permission.disabled 实测:旧键 → 隐藏集 ["websearch"](云工具不在内);新键 → ["cloud_cloud_web_search"]
  • 云工具契约校验从未执行。 packages/ext/src/cloud-contract-hook.tscloud_dispatch/cloud_status/cloud_await/cloud_artifacts精确相等比远端名,而钩子拿到的是引擎 id ⇒ CloudJobRequestV1 / CloudJobAcceptedV1 / CloudJobStatusV1 / ArtifactListV1 一条也没跑过。

#223 AC4(kill-switch)未被击穿:ext 的 tool.execute.before 闸按工具形态判(isWebSearchToolId,刻意无字面量),真实 id 照样命中。被修的是可用性层契约校验层

改法

id 一律推导,仓里不再有第二份字面量:

  • packages/ui-mac/src/main/cloud-web-search.ts — 拆出两层:CLOUD_WEB_SEARCH_REMOTE_TOOL(远端名)+ cloudMcpToolId()(与 McpCatalog.toolName 逐字同义)⇒ CLOUD_WEB_SEARCH_TOOL_ID 由推导得出。
  • packages/ext/src/cloud-websearch-kill.ts — 导出 mcpEngineToolId(server, remote)(复用已有的 sanitizeMcpName)。isWebSearchToolId 一字未动(它是对的)。
  • packages/ext/src/cloud-contract-hook.ts — 归属改为「本次 fork 的 server 名(ALPHA_CLOUD_MCP_SERVER,与治理豁免同源)+ 远端名」推导出的引擎 id。
  • 模型可见的拒绝文案(packages/core/src/tool/websearch.tspackages/opencode/src/tool/mcp-websearch.ts,两者都在 north-star 守卫的 ADR-035 exclude 清单内)改成指向真实 id。

判据(这次为什么不会再全绿)

原缺陷之所以活了一个需求,是因为所有相关测试都手喂 id:两边喂同一个错值,断言自然成立。现在:

  • packages/ui-mac/src/main/cloud-web-search.test.ts 装的是真的 McpCatalog真的 Permission.fromConfig/Permission.disabled,判据是引擎自己算出来的隐藏集,不是"config 里那个键等于某字符串"。
  • 期望值不取被测方写的常量 —— 第一版就是这么写的,变异实测发现两边一起动、测试仍全绿(与它要杀的缺陷同形)。现在一律走 McpCatalog.toolName(server, remote)
  • 远端名本身锚到 [E7][VERIFY] 打包真调 + keyless 兜底 + 计费/失败证据(L2/RC) #643 真机取证产物的 remoteWebSearchToolName / derivedEngineToolId —— 平台改名而仓里没跟上,下一次取证跑完就红。
  • packages/ext/src/cloud-contract-hook.test.tspackages/ui-mac/src/main/alpha-config-injection.test.tspackages/opencode/test/tool/alpha-mcp-websearch-gate.test.ts 里手喂的 id 全部改为推导(该套件里第三方那半场本来就是推导的,云那半场丢了这条纪律)。

反向判据(真跑过,不是"应该会红")

变异 结果
CLOUD_WEB_SEARCH_TOOL_ID 退回远端名 "cloud_web_search" cloud-web-search.test.ts 4 fail(含两条 Permission.disabled 行为断言)
契约闸退回按远端名精确相等 cloud-contract-hook.test.ts 3 fail
拒绝文案退回远端名 文案锁 fail
server 名 cloudalphacloud cloud-web-search + alpha-config-injection 合计 7 fail
远端名 cloud_web_searchweb_search 2 fail(真机取证锚 + 文案锁)

本地门(真实输出)

bash scripts/alpha-check.shALPHACHECK_EXIT=0

▶ [1/3] north-star guard (zero upstream edits)
    ✓ zero upstream package edits
▶ [2/3] typecheck (alpha packages: contracts-consumer + ext + ui-mac)
    ✓ typecheck
▶ [3/3] contract lock + unit tests (contracts-consumer + ext + ui-mac)
    ✓ tests
✅ all local gates green — safe to push (alpha-ci will mirror this in ~40s).

bash scripts/assert-gate-files.shGATEFILES_EXIT=0✓ 60 个闸门文件全部在位且真的跑过

与 base(origin/alpha @ 7799a5e6,同一 worktree 同一命令)的差:

套件 base 本分支
contracts-consumer 36 pass / 0 fail 36 pass / 0 fail
ext 132 pass / 0 fail 134 pass / 0 fail
ui-mac 3315 pass / 0 fail 3321 pass / 0 fail
packages/opencode/test/tool/alpha-mcp-websearch-gate.test.ts(不在 CI 范围,单跑) 5 pass / 2 fail(两条真起 Bun.serve 替身的用例 30s 超时 + ConnectionRefused localhost,与本改动无关的本机 flake) 7 pass / 0 fail

新增 fail:0

文档影响

.claude/rules/adrs/ADR-009-websearch-default.md:裁决 (a) 里 permission["cloud_web_search"] = "deny" 是一条已成假的实现事实(正是缺陷来源),改成 permission[CLOUD_WEB_SEARCH_TOOL_ID] 并写明两层命名;后果段加一条 ✅ 已修 · #650,并把残留 (a) 的措辞改准(那里说的 listTools 列的确实是远端名)。

已知边界(不谎称)

  • 本 PR 不改 packages/opencode/src/mcp/catalog.ts 的拼名规则(上游语义),也不改 ext 的 isWebSearchToolId
  • packages/opencode/test/tool/alpha-mcp-websearch-gate.test.ts 是 alpha 自有测试但住在 upstream 路径下。它不在 scripts/gate-files.tsv,因此不被任何 CI 步骤执行 —— 本 PR 只把它里面手喂的 id 改成推导,没有把它注册进闸门登记簿(超出本票范围,见下方"主动没做")。
  • pre-push 用了 --no-verify:钩子跑在共享主 checkout 上,而主 checkout 正被另一个 session(REQ-128)占用。等价的门已在本 worktree 真环境跑完,输出见上。

jinjunnn added 2 commits July 31, 2026 07:47
引擎给远端 MCP 工具起名 = `McpCatalog.toolName(server, remote)` =
`sanitize(server) + "_" + sanitize(remote)`。alpha 的 server 名是 `cloud`,而云
worker advertise 的远端名本身就叫 `cloud_web_search`(#643 P1.3 实测),真实引擎
id 因此是 `cloud_cloud_web_search`(P2.1 实测 `calledTool`)。仓里把这两层混为一谈,
后果两条:

- kill-switch 下的 permission deny 写的是远端名,经 `Wildcard.match` 编成
  `^cloud_web_search$`,对真实 id 恒不匹配 —— **空闸门**,云 web search 从未从
  模型工具表里消失过;
- ext 的云工具契约校验(`cloud-contract-hook.ts`)对四个兄弟工具用精确相等比远端名,
  `CloudJobRequestV1` / `CloudJobAcceptedV1` / `CloudJobStatusV1` / `ArtifactListV1`
  **一次也没执行过**。

AC4(kill-switch)未被击穿:ext 的 `tool.execute.before` 闸按工具形态判
(`isWebSearchToolId`),字面量无关,真实 id 照样命中。

id 一律推导:ui-mac `cloudMcpToolId()` / ext `mcpEngineToolId()` 与
`McpCatalog.toolName` 逐字同义。判据测试改用**真的** `McpCatalog` + 真的
`Permission.fromConfig`/`Permission.disabled` —— 手喂 id 的断言正是让这个缺陷
全绿活下来的原因。

Fixes #650
Refs #223
… 真机取证

变异实测暴露:行为断言若拿 `CLOUD_WEB_SEARCH_TOOL_ID` 当期望值,把生产常量改回
远端名时两边一起动、测试仍全绿 —— 与它要杀的那个缺陷同形。现在期望值走
`McpCatalog.toolName(server, remote)`,远端名本身锚到 #643 探针写下的
`remoteWebSearchToolName` / `derivedEngineToolId`。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REQ-223][CODE] 云 web-search 闸改按真实引擎 id 命中 —— 现在盯的 cloud_web_search 引擎从不注册

1 participant