fix(websearch): 登录之后本地搜网仍留在模型工具表里 —— 账户信号只决定云腿在不在 - #1442
Merged
Merged
Conversation
owner 2026-09-23 裁决推翻 ADR-009 B1「一登录就只走云端」:本地 keyless websearch 不花钱,是云腿 402 / 网络故障时唯一的退路,任何账户态都必须留着。 账户信号(平台代付)从此只决定**云腿在不在**。 - `applyWebSearchSovereignty()` 不再读代付判据,连 `userDataPath` 参数一起去掉 —— 代付判据住在那底下的密钥文件里,参数没了就没人能顺手读回来。 - `applyWebSearchDenies()` 的本地腿 deny 只看 kill-switch;`platformPays` 刻意 不被读,并由一条反回归断言钉住「代付一个 deny 都不产生」。 - 两份 `LOCAL_WEBSEARCH_DENIED_MESSAGE` 改成说 kill-switch 的实话:那句话现在 只在 kill-switch 下可达,而那一态云工具同样不在表里,原来的指路必然是错的。 - 新增四条用例把基线 §九 第 1 条的**推论**跑成事实:真 fork 的 env → 真 `RuntimeFlags` → 真 `webSearchEnabled` → 真 `injectAlphaConfig` → 真 `Permission.disabled`,登录代付态两条腿同时在表里;kill-switch 反例臂证明 这套手段测得出已知的坏。 kill-switch 半场、云腿、出网围栏一个字未改。 Refs #1414 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jinjunnn
pushed a commit
that referenced
this pull request
Sep 24, 2026
模型开场时被告知「你有搜网这个能力」,而它手里到底有没有这个工具是另一套逻辑决定的。 `#1414` 勘破 §6 测出全仓有四处在判「能不能搜网」,其中 `buildAlphaCapabilities` 把结论 **直接写进系统提示**,而它读的是 `ALPHA_WEBSEARCH_DISABLE` + `OPENCODE_ENABLE_EXA`, 与真闸(`applyWebSearchDenies` 的 deny 判决 + 云 MCP 定义的形状)**不同源** —— 于是可以 「提示说有、工具表里其实没有」,模型要么去调一个不存在的工具,要么凭印象编答案。 - `alpha-identity.ts`:`buildAlphaCapabilities` 的入参从「env 派生的三个布尔」换成 「两条腿各自的**工具表在场性**」。它不再自己判,只把在场性折成提示事实;本模块必须保持 零 import(ext 的登记簿直接 import 它),所以判据只能住在调用点。 - `alpha-config-injection.ts`:调用点改成**消费真闸自己的判决** —— 拿一个空壳 config 让 `applyWebSearchDenies` 用同一组入参判一次再读判决,不手抄它的条件。云腿在场性读新增的 `cloudMcpEnabled`(与写 `config.mcp` 那个三分支同一组输入)。顺带把同一函数里 26 行外 重算一遍的 `Boolean(process.env.ALPHA_WEBSEARCH_DISABLE)` 换成已有的 `killSwitch` 常量。 - 新增四格闸门 `websearch-prompt-tool-parity.test.ts`(基线 §三 S4 / ADR-046 不变量的执行者), 登记进 `scripts/gate-files.tsv`(精确条数 10)。四格 = 登出/BYOK · 登录有额度 · 登录无额度 · kill-switch;keyless flag 当自由变量跑两遍,所以本闸对 `#1411` 前后两种 base 都成立。 代付态那一格按 ADR-046 的目标态钉死(`#1411`/PR `#1442` 已合:代付一个 local deny 都不产生 ⇒ 两条腿同时在模型工具表里);撤掉 `#1411` 的变异实测让这一格当场两臂全红。 `caps.websearch` 的取值与改动前逐格相同(`!kill && (keyless || platformPays)`,可代数化简 验证),所以这是一次同源化重构,不改任何账户态下的用户可见行为。 Refs #1414 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jinjunnn
pushed a commit
that referenced
this pull request
Sep 24, 2026
模型开场时被告知「你有搜网这个能力」,而它手里到底有没有这个工具是另一套逻辑决定的。 `#1414` 勘破 §6 测出全仓有四处在判「能不能搜网」,其中 `buildAlphaCapabilities` 把结论 **直接写进系统提示**,而它读的是 `ALPHA_WEBSEARCH_DISABLE` + `OPENCODE_ENABLE_EXA`, 与真闸(`applyWebSearchDenies` 的 deny 判决 + 云 MCP 定义的形状)**不同源** —— 于是可以 「提示说有、工具表里其实没有」,模型要么去调一个不存在的工具,要么凭印象编答案。 - `alpha-identity.ts`:`buildAlphaCapabilities` 的入参从「env 派生的三个布尔」换成 「两条腿各自的**工具表在场性**」。它不再自己判,只把在场性折成提示事实;本模块必须保持 零 import(ext 的登记簿直接 import 它),所以判据只能住在调用点。 - `alpha-config-injection.ts`:调用点改成**消费真闸自己的判决** —— 拿一个空壳 config 让 `applyWebSearchDenies` 用同一组入参判一次再读判决,不手抄它的条件。云腿在场性读新增的 `cloudMcpEnabled`(与写 `config.mcp` 那个三分支同一组输入)。顺带把同一函数里 26 行外 重算一遍的 `Boolean(process.env.ALPHA_WEBSEARCH_DISABLE)` 换成已有的 `killSwitch` 常量。 - 新增四格闸门 `websearch-prompt-tool-parity.test.ts`(基线 §三 S4 / ADR-046 不变量的执行者), 登记进 `scripts/gate-files.tsv`(精确条数 10)。四格 = 登出/BYOK · 登录有额度 · 登录无额度 · kill-switch;keyless flag 当自由变量跑两遍,所以本闸对 `#1411` 前后两种 base 都成立。 代付态那一格按 ADR-046 的目标态钉死(`#1411`/PR `#1442` 已合:代付一个 local deny 都不产生 ⇒ 两条腿同时在模型工具表里);撤掉 `#1411` 的变异实测让这一格当场两臂全红。 `caps.websearch` 的取值与改动前逐格相同(`!kill && (keyless || platformPays)`,可代数化简 验证),所以这是一次同源化重构,不改任何账户态下的用户可见行为。 Refs #1414 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jinjunnn
added a commit
that referenced
this pull request
Sep 24, 2026
* feat(#1431): 让系统提示里的「能搜网」消费真闸的判决,而不是自己再判一遍 模型开场时被告知「你有搜网这个能力」,而它手里到底有没有这个工具是另一套逻辑决定的。 `#1414` 勘破 §6 测出全仓有四处在判「能不能搜网」,其中 `buildAlphaCapabilities` 把结论 **直接写进系统提示**,而它读的是 `ALPHA_WEBSEARCH_DISABLE` + `OPENCODE_ENABLE_EXA`, 与真闸(`applyWebSearchDenies` 的 deny 判决 + 云 MCP 定义的形状)**不同源** —— 于是可以 「提示说有、工具表里其实没有」,模型要么去调一个不存在的工具,要么凭印象编答案。 - `alpha-identity.ts`:`buildAlphaCapabilities` 的入参从「env 派生的三个布尔」换成 「两条腿各自的**工具表在场性**」。它不再自己判,只把在场性折成提示事实;本模块必须保持 零 import(ext 的登记簿直接 import 它),所以判据只能住在调用点。 - `alpha-config-injection.ts`:调用点改成**消费真闸自己的判决** —— 拿一个空壳 config 让 `applyWebSearchDenies` 用同一组入参判一次再读判决,不手抄它的条件。云腿在场性读新增的 `cloudMcpEnabled`(与写 `config.mcp` 那个三分支同一组输入)。顺带把同一函数里 26 行外 重算一遍的 `Boolean(process.env.ALPHA_WEBSEARCH_DISABLE)` 换成已有的 `killSwitch` 常量。 - 新增四格闸门 `websearch-prompt-tool-parity.test.ts`(基线 §三 S4 / ADR-046 不变量的执行者), 登记进 `scripts/gate-files.tsv`(精确条数 10)。四格 = 登出/BYOK · 登录有额度 · 登录无额度 · kill-switch;keyless flag 当自由变量跑两遍,所以本闸对 `#1411` 前后两种 base 都成立。 代付态那一格按 ADR-046 的目标态钉死(`#1411`/PR `#1442` 已合:代付一个 local deny 都不产生 ⇒ 两条腿同时在模型工具表里);撤掉 `#1411` 的变异实测让这一格当场两臂全红。 `caps.websearch` 的取值与改动前逐格相同(`!kill && (keyless || platformPays)`,可代数化简 验证),所以这是一次同源化重构,不改任何账户态下的用户可见行为。 Refs #1414 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#1431): 按棘轮指示人手抬 tree 基线到实测值(59813 → 59854) `scripts/assert-module-size.sh` 量出本票让 packages/ui-mac/src/main 非测试源码 +41 行 (全部是说明注释;先把两处注释收紧过一轮,+53 → +41)。基线只能靠人手 diff 抬高,评审看得见。 起点 = 干净 origin/alpha@78e751461 实测 59813,零遗留 ⇒ 这 41 行全部是本票的。基线与别人的 理由整份取 alpha 那一份,只重放本票这一段。树下两条 file 行(ext-install-planner.ts / ext-transaction.ts)本票一行未动、实测均 = 基线,所以按 `#1438` 新加的交叉判据只抬 tree 这一行 (该判据自己的闸 17 pass / 0 fail)。 Refs #1414 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: jinjunnn <slmbaovanetti99@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1411
Refs #1414
owner 2026-09-23 裁决:本地和云端的搜网都必须有效,账户信号只决定「云腿在不在」,不决定「本地腿在不在」。 本地那条不花钱,是云腿 402 / 网络故障 / 平台故障时所有人的兜底,任何账户态都必须留着。
#1415(PR#1427,已合)已经让登出 / BYOK 态的本地 websearch 真的能出网。本 PR 只做剩下那一格:登录代付态下本地腿此前被主权闸有意关掉,并且还明确告诉模型「去用云端那个」。方案基线:
docs/design/2026-09-23-1414-web-tools-account-routing-baseline.md§二 / §七 CODE-1(owner 2026-09-24 批准)。改了什么(生产面 4 个文件)
packages/ui-mac/src/main/server.tsapplyWebSearchSovereignty()不再读「平台代付」判据;连userDataPath形参一起去掉 —— 代付判据住在那底下的密钥文件里,参数没了就没人能顺手把它读回来。三个调用点同步更新,hasSecretFile随之从该文件的 import 里消失。packages/ui-mac/src/main/cloud-web-search.tsapplyWebSearchDenies()的本地腿 deny 只看 kill-switch;state.platformPays在那一行刻意不被读。诊断串里的三元(kill switch/platform pays)收敛成固定的kill switch—— kill-switch 态的输出逐字未变。packages/core/src/tool/websearch.tsLOCAL_WEBSEARCH_DENIED_MESSAGE改写。packages/opencode/src/tool/mcp-websearch.tspackages/opencode/src/tool/websearch.ts只是从这里转出,两个包之间没有可共用的依赖边,所以两份各写一份)。云腿的任何一处、kill-switch 半场、出网围栏:一个字没改。
为什么文案必须跟着改(基线 S5)
改完之后
ALPHA_LOCAL_WEBSEARCH_DENY只由 kill-switch 置位,而 kill-switch 同时关掉云腿。于是那句「Usecloud_cloud_web_searchif it is present」在唯一还能到达它的状态下必然是错的 —— 它把模型指向一个同一时刻也不在工具表里的工具。新文案说 kill-switch 的实话,并明说没有替代;保留「do not retry」。为什么
WebSearchSovereignty.platformPays字段留着它仍然是「云腿在不在」那一轴的真值,注入面在别处消费它(云 MCP 定义的形状)。更重要的是:
cloud-web-search.test.ts里{ killSwitch: false, platformPays: true }那条臂正是钉住「代付一个 deny 都不产生」的反回归断言 —— 删掉字段就写不出那条断言。字段上写了一行注释说明它不得参与本地腿判定。退出条件 ①:登录代付态下两个工具同时在模型工具表里
基线 §九 第 1 条把这件事标为「从代码结构推出来的推论,不是跑出来的」。本 PR 把它跑成事实。
新增
packages/ui-mac/src/main/server.test.ts的一组用例,一条规则都不手写,四个供数方全是生产自己的:applyWebSearchSovereignty()+createSidecarEnv()白名单 —— 经真spawnLocalServerfork 出来的那份 env;RuntimeFlags(真ConfigProvider解析①给出的那份 env,不是手写env[k] === "1");webSearchEnabled()—— 本地腿的注册闸(tool/registry.ts的tools()用的就是它);injectAlphaConfig()→ 引擎的真Permission.fromConfig+Permission.disabled—— 可见性闸(session/llm/request.ts的resolveTools()用的就是它)。实测输出(这条链在最终树上跑出来的真实工具表):
(B 的 env 里
ALPHA_LOCAL_WEBSEARCH_DENY与ALPHA_CLOUD_WEBSEARCH_DENY都不存在 —— 这正是本票要的。)手段的自证(先证明它测得出已知的坏):C 那一格是反例臂,同一条链在 kill-switch 下两条腿一个都不在表里;它同时覆盖②(flag 被 force-off)与④(permission deny)。另外把生产代码改回旧语义跑了一次变异实验(
applyWebSearchDenies恢复|| state.platformPays,applyWebSearchSovereignty恢复登录 force-off):四个测试文件 16 条当场转红,含上面这条端到端用例;还原后 67/67 回绿。诚实边界,写在用例注释里:
installCloudMcp(),它读不到{file:}凭证时会响亮不装 —— 那半格一律按「已注册」处理,再由 permission deny 兜住,两条路径结论相同。退出条件 ②:kill-switch 态的断言逐字未变
没有动过任何一条 kill-switch 断言。具体地:
cloud-web-search.test.ts的kill switch removes both web search tools…、kill-switch:引擎真的把云 web search 滤出模型工具表…、kill-switch:alpha 注入的 agent 也压得住…三条断言体一字未动;server.test.ts的disable overrides a shell-exported %s flag in every auth state、disable wins over all shell-exported keyless flags even when cloud is registered、the sovereignty verdict reaches the sidecar so the tool itself can refuse、turning the kill switch back off clears the cloud verdict on the next fork一字未动;the kill switch ships a cloud verdict to the sidecar; platform-pays does not只改了它的 platform-pays 那一行(ALPHA_LOCAL_WEBSEARCH_DENY从toBe("1")变成toBeUndefined()),kill-switch 那两行未动;packages/ext的 208 条(含cloud-websearch-kill.test.ts43 条)全绿且一字未改。applyWebSearchDenies在 kill-switch 态写出的两条诊断串与改前逐字节相同(三元收敛成的那个分支就是原来的"kill switch")。退出条件 ③:
cloud-web-search.test.ts的platformPays臂改成新语义 —— 为什么platform pays denies the local websearch tool and leaves the cloud tool authoritativepermission.websearch === "deny"platform pays denies nothing,断言permission与三个 agent 一个字都没被写、诊断为空no injected agent may re-allow a denied web search tool{killSwitch:false, platformPays:true}an agent wildcard cannot re-allow a denied web search tool代付但无 kill-switch:云工具是权威通道,引擎不许把它藏起来Permission.disabled(...)==[websearch]== [],并补一条 agent 级的同判据Permission.fromConfig/disabled,期望值仍从远端名经McpCatalog.toolName推导两份拒绝文案指向的是引擎真实 idUse cloud_cloud_web_search if it is presentdo not retry/Answer without web search and say so/kill switch;并新增一条「两份副本逐字相同」的漂移锁基线漏掉的一格(如实登记)
基线 §1.1(d) 只点名了
cloud-web-search.test.ts的 5 处,并写「子票要改的就是它们,不是别的」。实测另有 3 个文件 10 条同样转红,全部是同一条边的落点:packages/ui-mac/src/main/server.test.ts8 条(#621组 6 条 +#223 Major 3组 2 条)packages/ui-mac/src/main/alpha-config-injection.test.ts1 条(真实生产 composition 下的代付臂)packages/ui-mac/src/main/config-injection-throat.test.ts1 条(代付态顶层permission键整个不再写出 ⇒ 「九个顶层键」变八个)处置:凡「被测机制没变、只是唯一能驱动它的开关从登录换成 kill-switch」的,一律保留断言体、只换驱动态(并在注释里写明换了什么、以前是什么)。
websearch-copies.test.ts如基线所料没有变红(12/12),它查的是副本集合,不是选路。本机门的真实输出
分支已 rebase 到
origin/alpha(基线随#1413/#1412/#1424/#1430前移;git merge-tree无冲突,rebase 后 typecheck 与全量单测都重跑过 —— 上游动过alpha-config-injection.ts并新增了第四个 alpha agentalpha-ask,它自带websearch: "allow",与本票的逐 agent 压平兼容)。bash scripts/north-star-guard.shrc=0·✓ zero upstream package editsbun run typecheckrc=0· 33/33 tasksbun run --cwd packages/alpha-contracts-consumer check:vendorrc=0· 40 artifacts verifiedbash scripts/bun-test-floor.sh 15 packages/alpha-contracts-consumerrc=0· 60 条(下界 15)bash scripts/bun-test-floor.sh 100 packages/extrc=0· 208 条(下界 100)ALPHA_KNOWN_FAILS_FILE=scripts/known-fails.tsv bash scripts/bun-test-floor.sh 3000 packages/ui-mac srcrc=0· 5362 pass / 0 fail(385 files, 308s)bash scripts/bun-test-app.shrc=0· 5 个 alpha 判据文件 56 条逐文件精确匹配bash scripts/assert-gate-files.shrc=0· 221 个闸门文件全部在位且条数与登记精确一致bash scripts/assert-seed-assets.shrc=0bash scripts/assert-module-size.shsrc/main59762);阈值那一半也无点名packages/coretest/alpha-websearch-sovereignty.test.tspackages/opencodetest/tool/{alpha-websearch-failure,alpha-mcp-websearch-gate}.test.tsgit push(未用--no-verify)PUSH_RC=0·local gates passed;git ls-remote读回 sha 与本地 HEAD 相同与 base fail-set 的差:0 条新增红。 本次全量是
0 fail,所以「新增红」这一集合是空的,不依赖任何归因。pass 数的差是+4(orchestrator 交的 base fail-set 是 rebase 前8c8867c90上的 5321 pass / 0 fail;rebase 带进来的三条上游 lane 补了其余):bun test src/main/server.test.ts --test-name-pattern "模型工具表"⇒ 4 pass / 0 fail,正是本 PR 新增的那一组。[9/14](worktree bootstrap 探针)[10/14](分支保护真源)[11/14](BYOK 目录)[13/14](漂移度量)在两次 push 里按脚本自己的三档语义各有「未验证 / 已测量」的自陈,与本 PR 改动无关;第一次 push 时[14/14]曾响过,处置见下。手段自证(不是「跑过了都绿」)
packages/opencode/src/tool/registry.ts提交一行(它在 ADR-041 收编白名单里)⇒ 仍rc=0(正确);换成不在白名单的packages/llm/src/index.ts⇒rc=1并逐字点名该文件。两个探针提交都已git reset --hard掉。applyWebSearchDenies恢复|| state.platformPays,applyWebSearchSovereignty恢复登录 force-off)⇒ 四个测试文件 16 条当场转红,含那条端到端工具表用例;还原后 67/67 回绿。electron/{dist,path.txt}软链主 checkout、alpha_fence.node在本树现编),所以process-fence-apply.test.ts/network-egress-fence.test.ts不是定点假红 —— base 与 after 的全量都是0 fail。[14/14]模块体积棘轮:它第一次响了,处置写在这里第一次 push 时
[14/14]点名两行:ext-install-planner.ts3539 > 基线 3525(我没碰过那个文件,git show 8c8867c90:… | wc -l= 3539,主干上就已超基线,后来由#1430/#1434在主干上补齐)、以及packages/ui-mac/src/main59091 > 基线 59075(+16 全是我的,注释行)。处置选的是拆小而不是抬基线:把两处新注释压紧(每一条事实都留着,只是更短),
src/main回到 59075 = 基线。理由 —— 16 行散文不值得抬一条全仓共享的基线,而且那一行同期有别的 lane 也要改,抬它等于替他们制造冲突。rebase 之后主干已把 tree 基线抬到 59762,本 PR 的净行数为 0,三行全部 = 基线。我主动没做的事
#1432)。本 PR 不碰.claude/rules/adrs/,docs gate 因此是 no-op。#1431)。顺带交一条勘破:alpha-config-injection.ts的能力判据是process.env.OPENCODE_ENABLE_EXA !== "0"(单 flag),而本地腿真正的注册闸是引擎的enableExa = OPENCODE_EXPERIMENTAL || OPENCODE_ENABLE_EXA || OPENCODE_EXPERIMENTAL_EXA加enableParallel的两 flag OR —— 两者在「用户显式 exportOPENCODE_ENABLE_EXA=0且 export 了另外三个之一」时分叉。既有缺陷,本 PR 既没引入也没修;在登录代付态它今天不会被用户看见(identity 的websearch能力是keyless || cloud,云腿在场就为真),分叉只在登出 / BYOK 态可观察。packages/ext/src/cloud-websearch-kill.ts的webSearchToolDenial()对非治理 server 的 web search 工具判localWebSearchDenied(env) || cloudWebSearchDenied(env)。本 PR 之后ALPHA_LOCAL_WEBSEARCH_DENY只由 kill-switch 置位 ⇒ 登录态下用户自带的第三方 web search MCP(如exa_web_search_exa)不再被拦。方向上与「两条腿都活着」一致(它同样 keyless、不花平台的钱),但它是 ADR-009 B1 的另一处落点,而基线 D1 只说了「本地腿」。建议 CODE-3 的新 ADR 显式登记。同一文件的拒绝理由串仍写「the platform pays for search, or the web search kill switch is set」——「the platform pays」那半句现在不可达。它在 kill-switch / 云腿半场,票面明令不碰。
packages/ui-mac/src/main/network-egress-registry.ts里#1415那段注释仍写「登出 / BYOK 态下websearch是广告给模型的」;现在它在所有非 kill-switch 态都广告给模型。纯注释事实过时,不影响行为(放行集合本来就无条件),未改。bun run lint有 1 个 error(packages/web的Invalid tsconfig: Cannot find type definition file for 'bun')+ 9480 warnings,全是既有噪声、与本 PR 改动的文件无关;scripts/alpha-check.sh不跑 lint,它不是合并闸。未修。packages/ui-mac/tsconfig.json排除*.test.ts⇒ 本 PR 改的 6 个测试文件不过 typecheck。已用 grep + 实跑两条轴交叉验证,不靠编译器。🤖 Generated with Claude Code