From 64b8b98c4c565e19c570995f0bd7636666d399b9 Mon Sep 17 00:00:00 2001 From: jinjunnn Date: Mon, 27 Jul 2026 04:06:26 -0400 Subject: [PATCH] =?UTF-8?q?docs(adr):=20#642=20ADR-009=20=E8=A3=81?= =?UTF-8?q?=E5=86=B3=20(c)=20=E6=94=B6=E5=8F=A3=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E3=80=8C=E7=A6=81=E7=94=A8=20provider=20=E6=A3=98=E8=BD=AE?= =?UTF-8?q?=E3=80=8D=E6=98=AF=E5=81=87=E9=97=B8=E9=97=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #642 的前提「ADR-009 仍是 #639 之前的版本」不成立:PR#639 自己就改了这份 ADR (+88 行,R3–R7 全部收口在内),AC 要求的六项记载与裁决 (a)/(b)/(d) 均已在册。 逐条核对后剩下的是两处真缺口。 一、裁决 (c) 声明的闸门不存在,且它守的也不是这条路。 「以禁用 provider 棘轮(断言打包端 provider 目录不含 opencode)守死」——按 forbidden-provider 与 provider 目录集合两个方向普查全仓测试,零命中。它是被写进 ADR 与 E7 基线的一处假闸门:声明了保证,没有任何可执行判据钉住它。 即便补上,它守的也是错的层。注入面把用户自配 provider id 无条件并回硬白名单: alpha-models.ts:115-117 遍历 readUserProviderIds()(ext-config.ts:784,取 opencode.jsonc 的 provider 键,零过滤)后 enabled.push(id)。用户写一条 {"provider":{"opencode":{…}}},opencode 即进 enabled_providers, webSearchEnabled(ProviderV2.ID.opencode, …)(registry.ts:58)恒真 —— 与 4 个 keyless flag 是否被 force-0 无关。「恒为死码」是错的。 主权判决不受影响,因为保证本来就不在注册闸上:执行面撞裁决 (b) 的传输层最终闸 (ALPHA_LOCAL_WEBSEARCH_DENY,mcp-websearch.ts 的 call() 与 core 的 callMcp() 各自第一句),调用被拒且零出网。与已登记的残留同类 —— 能力真关,可见性 cosmetic。 裁决:不补棘轮(守错层),不过滤该 id(登出/BYOK 自带 Zen key 是决策 A 明示允许的 形态,过滤会误伤);(c) 从「死码 + 棘轮」降级为「注册面可达、执行面被传输闸兜住」, 按现状诚实登记。「无需为它 patch 上游 registry.ts」这一半仍成立。 二、frontmatter amended 停在 2026-07-25,而正文已含 07-26 的 R3–R7 收口段。 E7 基线按其既有的「勘破更正」体例加同一个块,并声明本稿凡出现该表述之处一律以 该块为准(§3a、票 3、票 3b、票 5 共 7 处),不逐处改写。 本地闸全绿:3077 pass / 0 fail、docs 链接闸绿。 Fixes #642 Co-Authored-By: Claude Opus 5 (1M context) --- .../rules/adrs/ADR-009-websearch-default.md | 10 ++++++++-- ...2026-07-22-e7-cloud-web-search-baseline.md | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.claude/rules/adrs/ADR-009-websearch-default.md b/.claude/rules/adrs/ADR-009-websearch-default.md index 182f462d5c35..8ee1d65acc12 100644 --- a/.claude/rules/adrs/ADR-009-websearch-default.md +++ b/.claude/rules/adrs/ADR-009-websearch-default.md @@ -3,7 +3,7 @@ id: ADR-009 title: web search 默认策略 —— 登出/BYOK keyless 放开;登录态云优先权威 + 逃生开关跨本地/云 status: amended date: 2026-06-18 -amended: 2026-07-25 +amended: 2026-07-27 related: [ADR-002, ADR-005, ADR-006, ADR-018, ADR-029, ADR-035] supersedes_premise: "2026-06-18 的『桌面默认对所有 provider 放开 keyless websearch』现被登录态门控收窄为登出/BYOK 兜底" --- @@ -90,7 +90,12 @@ opencode `websearch`(Exa/Parallel)默认只给官方 `opencode`(Zen)provider(`pa - **修法**:归属改存在**每个 `AlphaExt` 实例自己的闭包**里,在该实例**全部配置合并完成之后**(项目 `alpha.jsonc` 合并之后)才算,并显式传给该实例的 `tool.execute.before`;模块侧不再留任何可变状态,判决函数的默认实参恒为 `UNVERIFIED_MCP_OWNERSHIP`(没传 = fail-closed),合并中途抛错同样倒向它。 - **同轮修掉的 Minor**:分类判据 ②(`search` + 具名搜索引擎词)此前实现按**词元集合同时出现**判,而本 ADR 与代码注释都写的是**相邻** —— 两张皮,且集合判会误杀 `brave_translate_and_search` / `internet_archive_search` 一类第三方工具(与 AC4 反向)。**改实现**为真正的相邻判定,与宣称对齐。 - **证据**:`packages/ext/src/cloud-websearch-kill.test.ts` 的「跨实例隔离」组(两个并存 directory 的**真** `AlphaExt` 实例,同一个被缓存的模块 import 两次,证明 B 的 foreign server 改不动 A 的判决;修复前该用例复现误拒)与「R7 回归」相邻用例。 -- **(c) `providerID==="opencode"` Zen 分支打包端不可达**:产品不发布 `opencode`/Zen provider,`webSearchEnabled` 的该 OR 分支恒为死码。以**禁用 provider 棘轮**(forbidden-provider ratchet:断言打包端 provider 目录不含 `opencode`)守死,**无需**为它 patch 上游闸。 +- **(c) `providerID==="opencode"` Zen 分支打包端不可达**(原始表述,2026-07-22):产品不发布 `opencode`/Zen provider,`webSearchEnabled` 的该 OR 分支恒为死码。以**禁用 provider 棘轮**(forbidden-provider ratchet:断言打包端 provider 目录不含 `opencode`)守死,**无需**为它 patch 上游闸。 + **2026-07-27 收口(#642;上一段的「以禁用 provider 棘轮守死」**不成立**,作废)**:两处都不成立。 + - **那道棘轮不存在**。全仓没有任何断言「打包端 provider 目录不含 `opencode`」的测试(按 forbidden-provider / 目录集合两个方向普查,零命中)。它是被写进 ADR 的一道**假闸门** —— 声明了保证,但没有任何可执行判据钉住它,守卫在 #639 之后仍然只靠这句话。 + - **即使补上那道棘轮,它守的也不是这条路**。`enabled_providers` 是硬白名单,但注入面把**用户自配的 provider id 无条件并回白名单**:`alpha-models.ts:115-117` 遍历 `readUserProviderIds()`(`ext-config.ts:784`,把 `opencode.jsonc` 的 `provider` 键**原样**取出、零过滤)`enabled.push(id)`。用户写一条 `{"provider":{"opencode":{…}}}`,`opencode` 就进了白名单,`webSearchEnabled(ProviderV2.ID.opencode, …)`(`registry.ts:58`)于是恒真 —— 与 4 个 keyless flag 是否被 force-0 无关。**「恒为死码」是错的**,该 OR 分支在用户面可达。 + - **但主权判决不受影响 —— 因为保证本来就不在注册闸上**。这条路只让本地 `websearch` 工具**注册**;真正的最终闸在裁决 (b) 的传输层(`ALPHA_LOCAL_WEBSEARCH_DENY`,`mcp-websearch.ts` 的 `call()` 与 `packages/core/src/tool/websearch.ts` 的 `callMcp()` 各自第一句),它不查 provider、不查 permission ruleset。主权态下该工具可注册、可见,**调用一律被拒且零出网**。这与 (b) 已登记的残留同一类:**能力真关,可见性是 cosmetic**。 + - **裁决**:不补那道棘轮(它守的是错的层,补了也只是把假闸门换成一道守不住东西的真测试),也**不**过滤用户自配的 `opencode` id —— 登出/BYOK 态下用户自带 Zen key 是决策 A 明示允许的形态,过滤它会误伤。**改为诚实登记**:(c) 从「死码 + 棘轮」降级为「注册面可达、执行面被 (b) 的传输闸兜住」。仍然**无需**为它 patch 上游 `registry.ts`,原结论的这一半成立。 - **(d) host-tool 的真实失败集(2026-07-25 更正:402 与余额门**已上线**)**:本条 2026-07-22 的原始表述——「`POST /v1/tools/web_search` 无 `accountPreauth`、计费仅事后 settle,故该路径无 402/余额面」——**今天是错的**,照它实现会漏掉真实失败态。`alpha-platform#37` 的 web_search 切片已落地(`packages/gateway/src/worker.ts` @ `2fd1984`):路由已登记进 `BILLABLE_ROUTES`(`:92`,`reservePolicy: "fixed-web-search-unit"`)且由启动断言 `assertRegisteredBillableRoutes(app.routes)`(`:952`)双向核对,ADR-018「未登记的收费入口不得运行」对 `web_search` 已满足。`webSearchHandler`(`:849-871`)今天的失败集: - `401` 无 auth; - `403` **`error.code = "action_forbidden"`**(`:853-854`,`model.invoke` 未授权)—— 原稿写的 `scope_forbidden` 是**另一个 worker**(`packages/gateway/src/server.ts:59`)的码,不在这条链上;另有 `403 job_not_enforceable`(`:864`,per-job 预算不可强制); @@ -113,6 +118,7 @@ opencode `websearch`(Exa/Parallel)默认只给官方 `opencode`(Zen)provider(`pa **2026-07-25 之前本条只有 permission 注入,声明「跨本地+云关掉」是超前的**:R3 判定云侧那一半当时可被后置授权顶掉(见裁决 (a) 的 2026-07-26 收口段)。 **R4 的「disarmed 不可重开」也不成立**:`/mcp/:name/connect` 能把 `enabled:false` 无条件翻开,见裁决 (a) 的五次收口段。 - ⚠️ **残留(a)**:云工具 catalog `listTools` 仍列 `cloud_web_search`,仅模型工具集不含——待引擎 remote 逐工具 deny。kill-switch 下调用一律被 ext 钩子拒(可见性是 cosmetic);**但**平台侧仍会接受并计费一个绕过引擎工具循环、直发的 `web_search` 调用 —— 服务端按 kill-switch 拒绝需要平台改动,不在本仓。 +- ⚠️ **残留(c)(2026-07-27 更正,#642;原「以禁用 provider 棘轮守死」是假闸门,作废)**:`providerID==="opencode"` 分支**不是死码**,且全仓没有任何棘轮守着它。用户在 `opencode.jsonc` 自配一个 id 为 `opencode` 的 provider,注入面就把它无条件并回 `enabled_providers`(`alpha-models.ts:115-117` ← `readUserProviderIds()`),`webSearchEnabled` 于是恒真,主权态下本地 `websearch` 工具照常**注册**。**能力仍关**:执行面撞裁决 (b) 的传输层最终闸(`ALPHA_LOCAL_WEBSEARCH_DENY`),调用被拒且零出网。与残留(b)同类 —— 可见性是 cosmetic。不补棘轮、不过滤该 id(登出/BYOK 态自带 Zen key 是决策 A 明示允许的形态),按现状登记。 - ✅ **残留(b)已收口(2026-07-25 二轮 + 2026-07-26 三轮,#223)**:`export OPENCODE_EXPERIMENTAL=1` 的 env 层绕过由两层堵死 —— permission 注入(把工具从模型工具表滤掉)+ **工具自身的最终闸**(`ALPHA_LOCAL_WEBSEARCH_DENY`)。**只有第二层是主权保证**:R2 动态复现,单靠 permission 注入会被 agent wildcard / 持久 session permission / approved 三条后置规则覆盖(见裁决 (b) 的二次收口段)。 **三轮更正**:二轮把最终闸只放进 legacy 一份副本,而同进程还挂着第二份 V2 Core 注册 —— 「已收口」当时**不成立**。现在两份都带闸,并加了防第四份副本的普查闸(见裁决 (b) 的三次收口段)。不碰 umbrella、不改上游 registry;新增上游接管**一个同类叶子**([[ADR-035]] §1)。缩小后的残留:被绕过的规则集下工具名仍可见,调用一律被拒。 **四轮更正**:三轮把源码普查网称为「类闸」并据此声称按类闭合 —— R4 用「算出注册名 + 复用既有传输」的可执行构造证明其**不成立**,已作废。闸已下沉到两条传输出口,普查网降为纵深。不再声称穷尽:自带全新出口且端点也算出来的副本仍拦不住,见裁决 (b) 的四次收口段。**零新增上游接管**(本轮改的三个源文件都已在 [[ADR-035]] 的 exclude 清单内)。 diff --git a/docs/design/2026-07-22-e7-cloud-web-search-baseline.md b/docs/design/2026-07-22-e7-cloud-web-search-baseline.md index c31187c01e92..a554f19ba0e8 100644 --- a/docs/design/2026-07-22-e7-cloud-web-search-baseline.md +++ b/docs/design/2026-07-22-e7-cloud-web-search-baseline.md @@ -156,6 +156,26 @@ review_after: 2027-01-16 > 棘轮**(forbidden-provider ratchet:断言打包端 provider 目录不含 `opencode`)守死,**无需** > 为它给 `webSearchEnabled` 打 patch。 +> **勘破更正(2026-07-27,alpha-code#642)**:上一块的两句都不成立,**本稿凡出现 +> 「`providerID==="opencode"` 打包端不可达 / 恒为死码 / 以禁用 provider 棘轮守死」之处 +> (§3a、票 3、票 3b、票 5),一律以本块为准**。 +> +> - **那道棘轮从未存在**。按 forbidden-provider 与「provider 目录集合」两个方向普查全仓测试, +> 零命中。它是一处**假闸门**:ADR 与本稿都声明了保证,却没有任何可执行判据钉住它。 +> - **该分支在用户面可达,不是死码**。注入面把用户自配的 provider id **无条件**并回硬白名单—— +> `alpha-models.ts:115-117` 遍历 `readUserProviderIds()`(`ext-config.ts:784`,取 +> `opencode.jsonc` 的 `provider` 键,零过滤)后 `enabled.push(id)`。用户写一条 +> `{"provider":{"opencode":{…}}}`,`opencode` 即进 `enabled_providers`, +> `webSearchEnabled(ProviderV2.ID.opencode, …)` 恒真,**与 4 个 keyless flag 是否被 force-0 +> 无关**。所以它确实是一条复活面,只是复活的是**注册**,不是能力。 +> - **不变量不受影响,因为保证本来就不在注册闸上**。执行面撞 §3a 收口后的传输层最终闸 +> (`ALPHA_LOCAL_WEBSEARCH_DENY`,`mcp-websearch.ts` 的 `call()` 与 +> `packages/core/src/tool/websearch.ts` 的 `callMcp()` 各自第一句),调用被拒且零出网。 +> 与「被后置 allow 顶掉的 permission」同类残留:**能力真关,可见性是 cosmetic**。 +> - **裁决(照搬进 [[ADR-009]] 裁决 (c))**:不补棘轮(它守的是错的层),也不过滤该 id +> (登出/BYOK 态自带 Zen key 是决策 A 明示允许的形态,过滤会误伤);(c) 从「死码 + 棘轮」 +> 降级为「注册面可达、执行面被传输闸兜住」。「无需为它 patch 上游 `registry.ts`」这一半仍成立。 + **被否决**: - **重排/改名远端工具**:远端工具名与顺序归 platform 所有;要生效须 platform 回声 → 红旗,否决。