Skip to content

fix(e7): 云搜索主权闸下沉传输层与插件钩子 —— 修冷启动时序、失败诚实化、kill-switch 真能关 - #639

Merged
jinjunnn merged 11 commits into
alphafrom
wt/223
Jul 26, 2026
Merged

fix(e7): 云搜索主权闸下沉传输层与插件钩子 —— 修冷启动时序、失败诚实化、kill-switch 真能关#639
jinjunnn merged 11 commits into
alphafrom
wt/223

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

三片

片 1 冷启动时序(Fixes #621 —— server.ts 的 force-off 判定依赖 ALPHA_CLOUD_MCP_URL,而它由 initAuthEnv() 在 whenReady 之后写入,晚于 preferAppEnv()。于是冷启动登录用户的 force-off 恒假,反而把 OPENCODE_ENABLE_EXA 设成 "1" → sidecar fork 时本地 keyless 与云工具双活,AC1「登录代付态云优先」根本不成立。判决改为每次 fork 前重算(与既有 syncSecretFiles 同纪律),并做成双向(登出 respawn 还原 keyless)。原单测在调用前手工预置该 env,恰好掩盖了真实时序。

片 2 失败诚实化(Fixes #489 —— 删掉「空/nullish 结果当成功串」的伪成功路径,Effect.orDie 换成可辨的 typed failure;失败集覆盖 401/403 action_forbidden/400/402/502 与任何非 2xx;structuredContent 进 schema,合法零命中不再被误判为失败,200 带结构化 error 不再被当成功;body 读取与 headers 同一 timeout 且硬限 2 MiB(初版单个 3 MiB chunk 实收 3,145,728 字节)。

片 3 主权闸 —— 最终判决下沉到传输层(mcp-websearch.tscall() 与 core 的 callMcp(),两者都在 ADR-035 收编面内),并把 core 的 callMcp 导出,使「复用传输」成为新增副本的正确写法(复用即受闸)。云工具的 kill-switch 钳在 ext 插件钩子 tool.execute.before 首句(alpha 自有包,零上游接管),该钩子不查任何 permission ruleset,因此后置的 agent wildcard / 持久 session permission / approved 一概够不着。云 server 的注册绑定到真实装载握手(ARM+DEF 成对,且 DEF 里的 URL 身份不可由 MCP 名称伪造),ext 未确认时完整定义根本不进配置,并用一条中和条目(loopback 端口 1,不做 DNS、必然 ECONNREFUSED)压掉 global/项目/managed 各源继承来的同名定义。

审计

Codex 对抗审计 8 轮到 MERGE。被推翻的实现者自陈包括:「permission deny 即 kill-switch 保证」(可被三条后置规则顶掉)、「V2 Core 的 websearch 永不挂载」(实为第二份副本,引擎 v1/v2 断层第三次现形)、「ext 未装载即 fail-closed」(OPENCODE_PURE 可让插件整体跳过而路径仍在)、「传输层是共同执行边界」(通用 Remote MCP 是第三条出口)、「继承来的同名条目一并抹掉」(只清了 OPENCODE_CONFIG_CONTENT)、「所有权快照按实例隔离」(实为模块级,跨项目串扰)。

诚实登记的边界:本票保证 ① alpha 治理的云 server(按定义身份识别)② alpha 自有本地 websearch(传输层闸);用户自配的第三方 web-search MCP 不在保证内,对它是尽力识别(Unicode 名、同名 add 替换、managed/MDM 通道均已具名登记)。ADR-009/035 的宣称已按此收窄。

闸门(本地实跑,已 rebase 到 a50322e0f)

  • unit:ui-mac 3062 pass / 0 fail;ext 132/0;opencode test/tool+test/mcp 462/0
  • north-star ✓(exclude 表 CI 与本地逐条一致,23 条);docs gate ✓;git diff --check 干净
  • 全程 30+ 次变异验证,每条闸门先自行实施绕过确认会红;跨仓依赖 alpha-platform#105 已开票

顺带

scripts/alpha-check.sh 自 ADR-033 起从未补 exclude 表,本地守卫一直假红、与 CI 不是 1:1 —— 本 PR 一并补齐。

Fixes #489
Fixes #621

jinjunnn and others added 11 commits July 26, 2026 09:01
force-off 的判据 ALPHA_CLOUD_MCP_URL 由 applyAuthEnv() 写入,而它经 initAuthEnv() 在
whenReady 之后才跑,晚于 preferAppEnv()。冷启动的登录用户在 preferAppEnv 里恒判「登出」,
force-off 恒不触发,反把 OPENCODE_ENABLE_EXA 设成 "1" —— fork 出去的 sidecar 里本地
keyless 与 cloud_web_search 双活,ADR-009 B1「云优先、只剩一个 web search 工具」被破。

修法不动 preferAppEnv 在 index.ts 的位置(它必须早于 fork,要供 PATH 等),而是把闸抽成
导出的 applyWebSearchSovereignty(),在 spawnLocalServer() 里 syncSecretFiles 之后、fork
之前**每次**重算 —— 与 A6 密钥文件同纪律,两个判据(URL + 密钥文件)此刻才双双成立。

闸做成幂等的双向函数:登录/kill-switch 覆盖写 "0";登出/BYOK 还原 keyless 基线 + 桌面默认。
只单向 force-off 会让一次登录把 keyless 哑到重启为止 —— 该洞今天不显形,只因闸从未真正触发。

测试改走真实顺序(preferAppEnv → 写 auth env → 断言 fork 时的 env),不再手工预置判据;
去掉 force-off 调用后 3 条断言转红。

Fixes #621
打包端真正挂载的 web search 是 packages/opencode/src/tool/websearch.ts。它此前有两处失守:
`output: result ?? "No search results found…"` 把空/坏响应伪装成成功串(模型无从分辨「零命中」
与「响应坏了」),`.pipe(Effect.orDie)` 把一切错误塌成匿名 defect(无类别、无状态、工具崩溃)。

改为单一可辨类型 WebSearchFailure,失败集与 alpha-platform `POST /v1/tools/web_search` 契约
对齐:401 / 403(带 error.code,action_forbidden 与 job_not_enforceable 可区分)/ 400 /
402(accountPreauth 拒绝、per-job 超预算)/ 502 / 其余非 2xx 一律 unexpected_status;另加
timeout / transport / provider_error(HTTP 200 但 MCP isError)/ empty_result / invalid_response。
不再用 filterStatusOk —— 它把每个非 2xx 压成同一个 StatusError,状态与 body 都拿不回来。
传输层 defect 经 catchCause 收进同一类型,不再是未处理 defect。

websearch.ts 侧把它转成 canonical 的 ToolFailure 再 die:legacy 工具链跨 Promise 边界后,
AI SDK 与 native adapter 两条路都能把它结算成模型可见的 tool error,消息带类别+状态+上游 body。
云失败不静默切回 keyless,空结果不再当成功。

刻意不动 mcp/catalog.ts 与 tool/code-mode.ts:云路径在 isError 时已经带 body 抛错(已 loud),
为了加一层分类前缀去扩大上游收编面不划算。

Fixes #489
#489 此前卡在 north-star 守卫:packages/opencode 在 UPSTREAM_PATHS 内,守卫用
--diff-filter=DMR,改既有文件必红。owner 裁决走 ADR-029 L3 文件级 exclude(ADR-033 先例)。

ADR-035 按 ADR-029 §3 逐要件登记:被接管表面(websearch.ts / mcp-websearch.ts + 随源测试)、
L0–L2 为何不够用(tool.execute.after 只在成功路径触发;失败语义是控制流不是文本变换;
alpha 至今没有 L2 patch 施加机制,为两个低 churn 叶子新建一台机器不划算)、守卫形态、
回退方案、放弃白嫖范围声明(单向门,含上游安全修复)。云路径的 catalog.ts / code-mode.ts
已 loud,明确不收编 —— 自限写进 ADR,后续要收须自己的 ADR。

顺带修一处既有漂移:scripts/alpha-check.sh **完全没有** exclude 清单、UPSTREAM_PATHS 也停在
ADR-033 之前的五个包,即本地守卫自 ADR-033 起对 16 个被接管/生成文件恒报假红,与 CI 早已不是
1:1。恒红的门等于没有门,故与 alpha-ci.yml 逐条对齐。

Fixes #489
ADR-009 §裁决(d) 与 E7 基线 §1/§3b/票2/票3c/票5/票6 里「host-tool 无 402/余额面」「web_search
仍 settle-only 未入册」「403 = scope_forbidden」三处表述今天都是错的,照办会漏掉真实失败态。

平台侧实况(alpha-platform packages/gateway/src/worker.ts @ 2fd1984):/v1/tools/web_search
已入 BILLABLE_ROUTES(:92)且由启动断言 assertRegisteredBillableRoutes(app.routes)(:952)
与真实 router 双向核对(ADR-018 对 web_search 已满足);handler(:849-871)有 accountPreauth
(:867-871 → 402)与 per-job 预算 precall(:863 → 402);403 的码是 action_forbidden(:853-854)
——scope_forbidden 属另一个 worker(server.ts:59),不在这条链上。

同时登记:#621 冷启动时序缺陷(force-off 判据晚于 preferAppEnv,云优先在冷启动路径上从未成立)
与 ADR-035 的两文件 L3 收编;把 ADR-009 的「未竟 · #489 暂缓」条目换成已交付。
Codex 对抗审计判 NOT-MERGE(1 Blocker + 5 Major + 1 Minor,多条动态复现)。逐条闭合:

Blocker(umbrella 绕过):`OPENCODE_EXPERIMENTAL=1` 让上游 `enableExa` 恒真,主权闸把四个
keyless flag 写 "0" 也关不掉本地 `websearch` —— 代付态本地+云双活,kill-switch 下云暗而本地
仍活。走 ADR-009 裁决 (b) 路 (i):注入面对本地工具 ID 也写 permission deny,并压平 alpha 自己
注入的三个 agent 的 `websearch: "allow"`(引擎 agent 级规则并在全局之后,不压平则全局 deny
对它们无效)。零改 umbrella、零改上游 registry。

Major 2(平台 402 到不了 payment_required):事实是平台 cloud MCP 薄壳 `text(body, !r.ok)`
丢弃 HTTP 状态,且 gateway 的两条 402 body 不带 `error.code` —— 不改平台就无法在客户端分类。
不改 alpha-platform 仓,改为:①ADR-035「HTTP 状态在 body 透传」的错误表述作废,ADR-009 与
E7 基线同步标明「可分类范围 = 本地 Exa/Parallel 直连链路」;②新增走**真实 McpCatalog.convertTool
链路**的测试把「loud + 原 body 完整,但无状态、无分类」钉成回归基线(含反向断言);
③开 alpha-platform#105 要求透传状态 + 补 402 error.code。上一轮把平台响应直接喂本地 call()
的测试是假接线证明,已删除。

Major 3(baseline 取点):基线改在登录 shell env 合入之后截取。此前 Finder 首启 + 用户
`export OPENCODE_ENABLE_PARALLEL=1` 时基线为空,登出「还原」反而删掉用户真值并默认开 Exa。
测试换成真实登录 shell 探测(SHELL 指向真脚本、spawnSync 真跑),不再用 SHELL=nu 跳过导入。

Major 4(secret 同步失败仍 fork):改 fail closed —— 同步失败拒绝本次 fork,不再 catch 后继续
(旧 token 文件会让主权闸误判 platformPays,新 sidecar 带作废 token 注册云工具)。

Major 5(零命中/错误判定):`structuredContent` 进 schema —— 「content:[] + results:[]」是
合法零命中成功;200 且负载为结构化 `{error:…}`(未置 isError)判 provider_error 并带 code;
200 的 HTML/非 JSON 错误页在 empty_result 里附上原 body。

Major 6(传输有界):headers + body 读取放进同一个 timeout,body 边读边计数、2MiB 处停手。

Minor 7(整文件 exclude):上游 `test/tool/websearch.test.ts` 还原受守(整文件 exclude 会连带
放行其中 `registry.ts` webSearchEnabled 的断言),新增断言落 alpha 自有的
`test/tool/alpha-websearch-failure.test.ts`(新增文件不触发 --diff-filter=DMR,无需 exclude)。
两处 exclude 表同步减一条并保持逐条一致,ADR-035 登记。
R2 判 NOT-MERGE 的三条未闭合项。

Blocker 1(主权 deny 可被覆盖):R2 动态探针实测 configOnly=deny,但加入
session allow 或 approved 后均变 allow。三条绕过——① 别的 config 源后加载的
agent 写 "*": "allow"(agent.ts:293 并在全局之后 + evaluate 取 findLast);
② PromptInput.tools.websearch=true 被 setPermission 持久化进 session
(prompt.ts:1060),tools.ts:87 再并在 agent 之后,respawn 也带得回来;
③ approved 排在整个 ruleset 之后(permission/index.ts:73)。

结论:permission 注入不是主权保证。最终规则改放在**工具自身** ——
applyWebSearchSovereignty 每次 fork 前把判决写进 ALPHA_LOCAL_WEBSEARCH_DENY
(sidecar-env 白名单放行),ADR-035 已接管的 tool/websearch.ts 在 execute
首行读它并以 ToolFailure 拒绝;它不查 ruleset,故没有任何 permission 规则能
覆盖。这是最窄的解:零新增上游接管(该文件已在 exclude 清单内),不动
permission/index.ts 的求值序,也不动 registry.ts 的注册闸。注入面的 deny 保留
但降级为可用性(把工具从模型工具表滤掉),并顺手修好 agent wildcard 压平
(以前只改已有的精确键,且不钉末位)。三条绕过各有一条反向测试,走真实
Tool.init → execute 路径:先断言该绕过在 permission 层真的判 allow,再断言
工具仍拒绝且传输层零命中。

Major 6(读取不硬限):实现先整块 push 再判越界,单个 3 MiB chunk 实收
3,145,728 字节而声明上限 2,097,152。最后一块改为只保留剩余可读字节,
补一条按字节精确断言的测试。

Major 5(结构化 error 的 code 到不了模型面):message 以前只在有 HTTP status
时才拼 code,而工具边界只把 message 交给模型。改为 status/code 各自独立出现,
断言从最终 ToolFailure 面取。

文档:ADR-009 / ADR-035 里「主权已穷尽」「2 MiB 处停手」「provider error 带
code」三处与事实不符的已完成声明全部更正;并登记云链路反向断言的到期条件
(硬编码现状 fixture,alpha-platform#105 落地后不会自动变红,届时须改成
status/code 的正向断言)。
Blocker 1(V2 Core 副本旁路):R3 勘破打包 sidecar 的 HttpApi 同时挂载 V2 Session
路由与 Location 服务,core 的 BuiltInTools 里是第二份已挂载的同名 websearch 注册
——它不读 ALPHA_LOCAL_WEBSEARCH_DENY,走自己的 PermissionV2 后直接调 Exa/Parallel。
R2 的「最终闸」只覆盖了 legacy 一份。

按类修而非按实例修:
- packages/core/src/tool/websearch.ts 的 execute 首行加同一条闸(ADR-035 §1 按同一
  裁决追加接管这一个同类叶子,两处 exclude 表逐条同步)。更窄的解逐条勘探否掉:
  V2 结算不触发任何 plugin hook;BuiltInTools 是静态 deps,删掉等于永久去掉工具而
  判决是每次 fork 动态重算;V2 permission 与 V1 同病。
- 新增普查闸 packages/ui-mac/src/main/websearch-copies.test.ts:钉住「全仓注册为
  websearch 的源文件集合」与「全仓直接引用 Exa/Parallel 端点的源文件集合」,并要求
  每一份注册都读同一个主权信号、闸排在任何 permission 交互/出网调用之前。第四份
  副本出现即红(已用模拟副本验证)。
- 真实 V2 链路回归 packages/core/test/alpha-websearch-sovereignty.test.ts:真
  ToolRegistry.materialize() + settle();含「显式 allow 的 ruleset 顶不掉」与
  「permission 层根本没被咨询」。

云工具 kill-switch(R3 判门控):先验证过的更窄方案被否——ConfigMCPV1.Remote 只有
整 server 的 enabled,没有 per-tool 过滤,「注册期就不提供这个工具」在引擎侧不存在;
整 server 关会误伤兄弟工具。最终闸因此落在 alpha 自有的 ext 插件
(packages/ext/src/cloud-websearch-kill.ts),由 tool.execute.before 钩子首行调用:
普通 MCP(session/tools.ts)与 code-mode(tool/code-mode.ts)两条链都在 ctx.ask
之前触发它,而 Plugin.trigger 用 Effect.promise 调 hook 且不捕获,抛出即终止调用。
钩子不查任何 permission ruleset,故后置 agent/session allow 与 approved 都够不着。
判决经 ALPHA_CLOUD_WEBSEARCH_DENY 过河(main 每次 fork 前两个方向都写,白名单放行),
只在 kill-switch 时置位——代付态云工具是权威通道。零上游接管。
fail-closed:kill-switch 且 ext 未装载时索性不注册整个 cloud server 并 loud,宁可
连兄弟云工具一起损失,也不放一个活的 web_search 出去。

文档:ADR-035 §背景 2 的「core 的 v2 builtin websearch 永不挂载」作废更正(它是
Blocker 1 的源头);ADR-009 裁决 (a) 里「permission deny 即 kill-switch 保证」、
裁决 (b) 二次收口的「能力真关」、后果里「跨本地+云关掉」三处与运行时事实冲突的
声明全部按本轮真实结论改写,并诚实登记未闭合残留(远端 catalog 仍 advertise;
闸够不着绕过引擎工具循环直发 MCP 的客户端;平台侧仍会接受并计费直发调用)。
同时登记:engine 侧两个回归文件跑在 alpha 合并闸之外,机制事实已固化在 ui-mac/ext
的测试里。

验证:north-star 守卫绿(exclude 拿掉即红);typecheck 错误集与 base 逐字相同
(196 条,全是冻结前端 packages/app 的既有假红);ui-mac 2883→2892 pass、ext
90→99 pass、contracts-consumer 19 pass,0 fail;core alpha 回归 9 pass、
opencode alpha 失败面 38 pass;docs gate 绿;git diff --check 干净。
六条单点回退变异全部验证变红后还原。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R4 判 NOT-MERGE 的两条,逐条闭合。

## Blocker 1:源码普查网可绕,闸落到共同执行边界

R4 的可执行反例:`const id = ["web","search"].join("")` + `Tool.define(id, …)`
复用既有 `McpWebSearch.call` —— 注册名算出来(Net A 看不见)、传输已白名单
(Net B 看不见新 URL)、新叶子不读主权信号。实测 netA=false, netB=false。
「两个已知实例加闸 + 源码盘点」是实例修补,不是类级规则。

闸因此下沉到**共同的执行边界**:本地 keyless web search 的两条出网出口,
两条都已在 ADR-035 接管面内,现在都在第一句读 ALPHA_LOCAL_WEBSEARCH_DENY:

- packages/opencode/src/tool/mcp-websearch.ts 的 `call()`(legacy 唯一传输)
  —— 新增 sovereignty_denied 失败类别,其 message 即「别重试」原文;
  主权信号的**声明点**下沉到此,叶子改为转出,免两处漂移。
- packages/core/src/tool/websearch.ts 的 `callMcp()`(V2 Core 那份的传输)
  —— 导出,使「复用传输」成为新副本的正确写法;拒绝时直接 ToolFailure,
  因为 ToolRegistry.settle 只把 LLM.ToolFailure 结算成模型可见 error,
  于是连一句错误映射都没写的副本也拿得到正确拒绝。

叶子 execute 首行那道闸保留为纵深;两张源码普查网保留但降为纵深(抓自带全新
HTTP 出口的副本),不再声称穷尽。

## 云 kill-switch:ext 缺席判断绑到「确认装载 + 钩子已注册」

alpha-config-injection.ts 原来只看 extPluginPath 存不存在 —— 路径存在不等于
插件装载。三条路径仍在而钩子不存在的真实链路:OPENCODE_PURE=true(经
sidecar-env 前缀规则进 sidecar → runtime-flags 判 pure → plugin/index 整个
跳过外部插件)、bundle import 失败、AlphaExt 初始化失败(后两者 log-and-continue)。

改为握手:kill-switch 下注入面只写一个 enabled:false 的 cloud server 并置位
ALPHA_CLOUD_MCP_ARM;打开它的是 ext 自己 config 钩子里的 armCloudMcp()。
该钩子能执行 ⇔ 插件已返回 hooks ⇔ 同一对象上的 tool.execute.before 闸已注册。
arm 通道刻意不进 sidecar-env 白名单,外部 shell 伪造进不来。

## 变异验证(实跑)

- R4 那段构造作为变异种真跑:opencode 与 core 各一组,断言传输层拒 + 零出网 +
  模型面拿到主权拒绝原文,各带一条闸不置位时确实出网的正向对照。
- ext 缺席三态各跑一次(真 AlphaExt 动态 import + 真失败模块),断言云 server
  停在 enabled:false;OPENCODE_PURE 的解析由真 RuntimeFlags 断言。

ADR-009 / ADR-035 中被本轮推翻的表述(「ext 未装载即 fail-closed」按路径判、
「普查网 = 类闸/按类闭合」)已改成与实现事实一致,并登记不再声称穷尽。
零新增上游接管:三个源文件都已在 ADR-035 的 exclude 清单内。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blocker(通用 Remote MCP 是第三条现存出口):配置里直接声明任意 remote MCP
(`{"mcp":{"exa":{"type":"remote","url":"https://mcp.exa.ai/mcp"}}}`)会产生
`exa_web_search_exa`,普通模式与 code-mode 都直接 `client.callTool()`,R4 那两道
传输闸一概不经过。收口 = 把本地判决接到两条链**共同的钳制点**:ext 的
`tool.execute.before`。`ALPHA_LOCAL_WEBSEARCH_DENY` 置位时,任何 MCP server 上的
web-search 形态一律拒;唯一例外是 alpha 自己注册的云 server(名字经新的
`ALPHA_CLOUD_MCP_SERVER` 过河),代付态它是权威通道、只有 kill-switch 关它。
传输层两道闸保留为本地 `websearch` 工具的纵深。
> 产品语义(已在 ADR-009 登记并标「归 owner 复核」):这等于主权态下连用户自己
> 配置的 web-search MCP 一起关。按「关」实现。

Major(R4 增量回归:`enabled:false` 不是不可重开):`MCP.connect()` 无条件把配置
复制成 `enabled:true`,该能力公开为 `/mcp/:name/connect` 且产品 UI 真的在调 ——
ext 缺席时用户点一下就能把那份含完整 URL/header 的 server 热连起来。改成
kill-switch 下**完整定义根本不进配置**:注入面只经 `ALPHA_CLOUD_MCP_ARM` +
`ALPHA_CLOUD_MCP_DEF` 托管,由 ext 的 `installCloudMcp()` 在 config 钩子里装进去
(`{file:}` 引用由它自己解析,读不到密钥即 fail-closed 不装);继承来的同名条目
一并抹掉。ext 缺席 ⇒ `/mcp/cloud/connect` 直接 NotFound,无物可复活。

Minor(`ALPHA_ENV_ALLOWLIST_EXTRA` 可放行 ARM):R4 的「shell 进不来」不成立 ——
三个握手变量都不是 credential-shaped,逃生阀点名即放行。文档与测试改成真判据:
可信度来自「injectAlphaConfig 每次 fork 的每条分支都覆盖或删除它们」+「ARM 与 DEF
必须成对」,两条测试各钉一半。

变异验证(真实配置,非替身):
- `packages/opencode/test/tool/alpha-mcp-websearch-gate.test.ts` —— 真 streamable-HTTP
  MCP server + 真 `mcp` 配置段 + 真装载 `packages/ext/src/plugin.ts` 本体;普通模式与
  code-mode(真 `CodeModeTool` → `invokeChildTool`)各跑一次,断言被拒 + 远端零
  `tools/call`,各带一条闸不置位时真的出网的正向对照。
- `packages/opencode/test/mcp/alpha-cloud-mcp-revival.test.ts` —— 真 MCP lifecycle +
  真 HttpApi 路由;正向对照先证明 R4 形态确实能被 `/connect` 热连,再断言 R5 形态
  404 + 远端零请求;`add` 只带 enabled:true 走不通(400)。

诚实登记的残留:`POST /mcp` 的 `add` 自带完整定义能新装第三方 MCP(拦它等于拦任意
第三方 MCP,需收编 handlers/mcp.ts / mcp/index.ts,不在本票);ext 缺席时第三方
web-search MCP 无拦截点;判据认 `web[_]search` 词根,不谎称穷尽。

零上游改动(两个 opencode 测试是新增文件)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R6 判 NOT-MERGE 两条,本轮各自闭合并同步收窄 ADR 宣称。

Blocker —— 按名字分类既漏又可伪造:
- 分类扩宽但**降级为尽力而为**。判据从 `web_?search` 单词根扩为「去分隔符后含
  websearch/searchweb」∪「search 与 web/internet/online 或一个具名搜索引擎词相邻」,
  收进 R6 点名的 search_web / web-search / websearchTool / brave-search;非 ASCII 名
  经 McpCatalog.sanitize 后只剩下划线,**按名字识别不可能穷尽**,该漏以回归登记。
- 治理豁免改绑**端点身份**。ALPHA_CLOUD_MCP_DEF 里 alpha 本次 fork 自己写的定义 url
  是不可伪造真源;ext 的 config 钩子用 recordMcpOwnership() 拿引擎已合并完成的配置
  逐个核对(名字 = 点名的那个 ∧ type=remote ∧ url 逐字相同),工具归属的任何歧义
  一律 fail-closed。cloud_attacker / cloud_web+search / 运行时新装 server 全部拿不到
  豁免。DEF 因此在代付两条分支都置位(ARM 仍只在 kill-switch 分支,成对不变式不变)。

Major —— 「继承来的同名条目一并抹掉」不成立:
- R5 只删继承的 OPENCODE_CONFIG_CONTENT 那一份,而 mergeDeep 里缺键不会删除 global /
  OPENCODE_CONFIG / 项目 / managed 先前来源的定义。改为写 WITHHELD_CLOUD_MCP 中和条目
  (type=remote + http://127.0.0.1:1/… + enabled:false),靠 later-wins 标量覆盖逐字段
  压过继承定义;ext 确认装载后由 installCloudMcp() 整条替换。
- injectMcpDefaultDeny 里写死的 "cloud" 治理名删除 —— 名字不是治理凭据;治理集改为
  「alpha.jsonc 里真有的 ∪ 本轮注入面真放进去的」。

证据(全部真实链路):
- ext 单测新增 R6 六个绕过构造 + 端点身份核验组;
- alpha-mcp-websearch-gate.test.ts 加真配置 + 真 ext 装载的 cloud_attacker 回归;
- 新增 alpha-cloud-mcp-multisource.test.ts:真 Config.Service 多源加载(真
  Global.Path.config / Flag.OPENCODE_CONFIG / 项目文件 / managed 目录),四来源各带
  正向对照,另有真 MCP lifecycle + 真 HttpApi 的「中和后连不上、远端零请求」。

诚实登记(未闭合,已写进 ADR 与测试):managed 目录与 MDM 托管偏好排在
OPENCODE_CONFIG_CONTENT 之后能覆盖回去(root/管理员通道);同名 POST /mcp add 替换
已连客户端后 ext 分辨不出来(上游不暴露活的 server 定义)。

ADR-009 / ADR-035 宣称同步收窄:本票保证 = alpha 治理的云 server(按定义身份识别)+
alpha 自有的本地 websearch 工具;用户自配的第三方 web-search MCP **不在**保证内,
owner 复核标记保留但不再写成已实现。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R7 判 NOT-MERGE 的唯一 Major:MCP 治理归属快照存在 ext 的模块级 `let`,
而插件模块由 Bun 动态 import 缓存(一个引擎进程一份),Plugin/MCP 状态却
按 directory 建实例 —— 后跑的实例覆盖先跑的。实测:实例 A 记录正常 `cloud`
后 `cloud_web_search` 放行,实例 B 只多记一个 foreign `cloud_web`,A 的同一
个权威云工具立刻被误拒(跨项目串扰,违反本票对治理云 server 的保证与 AC4)。

- 归属改存**每个 AlphaExt 实例自己的闭包**,在该实例全部配置合并完成之后
  (项目 alpha.jsonc 合并之后、config 钩子的 finally 里)才算,并显式传给
  该实例的 tool.execute.before;模块侧删掉 `recorded` 与 recordMcpOwnership,
  只留纯函数 computeMcpOwnership,判决函数默认实参恒为
  UNVERIFIED_MCP_OWNERSHIP(没传 = 豁免不给);合并中途抛错同样倒向它。
- 隔离回归:两个并存 directory 的**真** AlphaExt 实例(同一个被缓存的模块
  import 两次),证明 B 的 foreign server 改不动 A 的判决,两个方向都验。
  修复前该用例复现误拒。
- out-of-round Minor:分类判据 ②(search + 具名搜索引擎词)实现按「词元集合
  同时出现」判,而注释与 ADR 写的都是「相邻」—— 改**实现**为真正的相邻判定,
  `brave_translate_and_search` / `internet_archive_search` 不再被误杀(AC4 方向)。
- 措辞改准:中和条目压的是**连接控制字段**(type/url/enabled/oauth),不是
  「逐字段完整覆盖」—— 真实 mergeDeep 探针显示继承的 headers/timeout 会留下;
  URL 已是不可用 loopback,故判决不变,只改宣称。ADR-009 同步登记七次收口。
@jinjunnn
jinjunnn merged commit d341b20 into alpha Jul 26, 2026
1 check passed
@jinjunnn
jinjunnn deleted the wt/223 branch July 26, 2026 13:05
jinjunnn added a commit that referenced this pull request Jul 27, 2026
#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: jinjunnn <slmbaovanetti99@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
jinjunnn added a commit that referenced this pull request Jul 27, 2026
…648)

* fix(e7): readBoundedBody 读全响应体 —— 修 runForEachWhile 只读首个 chunk 的静默截断

`Stream.runForEachWhile` 只消费第一个 chunk 就停,即使谓词恒返回 true。三种读法实测:
runForEachWhile 1 chunk / 4,090 字节;runForEach 3 chunks / 18,063 字节;response.text
18,034 字符。唯一的生产调用方 `call()` 于是拿到截断的 JSON,登出态 web search 真调报
「invalid response … Unterminated string in JSON」——上游返回的其实是成功结果。

引入点 commit d341b20(PR #639,落地 #489 的有界读取要求);该 commit 之前是
`yield* response.text`,读全、无此缺陷。为了修 #223 R2 Major 6 的 DoS 上限,把「读全」
这条正确性弄丢了,并且发了版。

修法与 `tool/read.ts:146` 同构:`runForEach` 读全 + tagged error 在触限那一刻中止上游流。
不能天真换成裸 `runForEach` —— 那会读完整条流再截,把 R2 的 DoS 洞放回来。
`MAX_BODY_BYTES` 仍是与块大小无关的硬限,`truncated` 语义与后缀不变。

类级防复发(本仓第二次踩同一个 API,第一次的教训只写在 read.ts:143 的注释里,
对正在写另一个文件的人不可见 —— 注释不是闸门):
新增 `packages/ui-mac/src/main/stream-read-hygiene.test.ts`,全仓源码禁 `Stream.runForEachWhile`,
并自带覆盖面自检与自我变异,避免成为空闸门。

行为闸落在 packages/opencode/test,而该目录以前一条都没进过 alpha CI(那份文件抬头早已
把这条盲区写下来)。补一个 CI 步骤只跑 alpha 自有的那份 web search 闸门测试。

Fixes #647

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(e7): 两条防复发闸都是假的 —— 枚举补全 + CI 断言真跑了用例

codex 第 1 轮 Major ①/②。修复本身(readBoundedBody)未动。

① stream-read-hygiene.test.ts —— 空闸门 + 覆盖面自检假绿
手写 walker 只进 `packages/<一级目录>/src`,却表述为「全仓生产源码」。实际漏掉
packages/console/*、packages/stats/*、packages/sdk/js 三个嵌套 workspace 共 353 个生产
文件;在 packages/stats/server/src/ingest.ts 写违规代码可完全绕过,而当时的自检
(扫到 >200 文件 + 两个事故锚命中)仍然全绿 —— 它只证明「扫到了一些文件」,不证明「没漏」。

改为 `git ls-files --cached --others --exclude-standard`(可证明完备,且覆盖尚未 add 的
新文件)。覆盖面自检改成「漏扫可观测」:钉住根 package.json 的 workspace 通配符清单
(新增 workspace 根即红)+ 断言每个嵌套 workspace 根都真有文件进入集合 + 断言
packages/stats/server/src/ingest.ts 这个具体绕过点在集合里。抬头措辞改成守住什么写什么,
并显式列出守不住什么。

② alpha-ci.yml —— 零用例默认放行
`bun test` 对「文件被清空 / 用例条件注册成零条」打印 `Ran 0 tests` 并**退出 0**(已实测)。
新步骤改为解析实际通过数并钉下界 40(当前 45),低于即红;bun 非零退出仍单独判红。

* fix(e7): 闸门文件被删除不再静默放行 —— 用例数下界收口成共用脚本

第三种同类假绿:`bun test src` 对「测试文件被删除」默认放行。删掉 stream-read-hygiene.test.ts
后 ui-mac 跑 3077 条全绿、CI 全绿,类级闸就此消失且不自愈。刚用一整轮确立「一个能被静默删除
的闸门不是闸门」,不能同时交付一个能被静默删除的闸门。

把「断言实际执行的用例数」抽成 scripts/bun-test-floor.sh,三处共用同一个机制而不是三份拷贝
(拷贝会各自漂移,那是下一次漏掉的成因)。

两层下界,各管各的 —— 单靠整套 suite 的地板抓不到「少了一个文件」:
  · ui-mac 整套 suite 地板 3000(当前 3085):抓灾难性丢失(加载失败/成片删除/runner 配错)。
    刻意留大余量 —— 会随正常增删漂移的阈值早晚被调松或删掉,那又是一个自毁的闸门。
    实测:删掉类级闸后本步 3077 条**仍然绿**,所以它抓不到单文件删除,不能靠它。
  · 类级闸单独点名,下界 6(当前 8,留出登记项增减的余量):这一层才抓单文件删除。
    实测:文件删除 → bun exit 1 → 红;文件清空 → bun exit 0 但 0 条 → 下界判红;放回 → 8 条绿。

* fix(e7): 整类收口 —— contracts-consumer / ext 两步同样是零用例默认放行

自查第四种时发现:我上一条回复答了「无」,是错的。`test` job 里还有两步只看退出码 ——
`bun test (contracts consumer fixtures)` 与 `bun test (ext)` —— 与刚修的三步是**同一类**。

实测(把 ext 全部 10 个测试文件清空):
  裸 `bun test`        → `Ran 0 tests across 10 files`,**exit 0**,原步骤全绿
  带下界的新步骤       → exit 1
两步一并接到 scripts/bun-test-floor.sh(下界 15 / 100,当前 19 / 132)。

只修被点名的实例、留着同类的其余实例,正是本 PR 在讲的那个故事。机制已经有了,整类一起收。

同时放宽脚本参数检查:允许只给 floor + workdir(整包跑,等价于原来的裸 `bun test`),
不再强制第三个过滤参数。

* fix(e7): 闸门文件全量类扫 —— 21 个逐个点名 + 登记簿完备性闸

上一轮我在 ui-mac 上实测出「整包地板抓不到删掉一个 N 条用例的文件」,却只把这条规律应用了
一次:ext 只加了地板 100,而 ext 排除 REQ-062 drift lock 后还剩 106 条 —— 删掉那个闸门文件
CI 静默全绿。同一个类,修了一个实例。这一轮做全量。

闸门文件的判据(写进 scripts/gate-files.tsv 抬头):删掉它就移除某条**具体保证**,而不只是
减少覆盖率;操作性signature = 它断言的是自己模块之外的东西(仓库源码文本、vendored/生成的
fixture、上游文件、钉住的清单),于是没有别的测试会因为它消失而变红。

跨五个 CI 步骤扫全,登记 21 个:契约哈希锁、REQ-062 prompt drift lock、云 websearch
kill-switch、REQ-100 skill 注入门、seed 快照守卫、REQ-012 上游 DOM 锚点、ADR-027 seam 存活、
对比度/接管件/cards/permission 挂载/settings surface/reduced-motion/route authority 六个棘轮、
alpha-web 契约 fixture、契约横幅接线、#647 类级禁令与 web search 行为闸。
逐个删除实测:21/21 删除即红、放回即绿。

三件配套:
· scripts/gate-files.tsv —— 登记簿,每行带下界与「这道闸保证什么」。
· scripts/assert-gate-files.sh —— 逐个跑 + 逐个判下界;登记簿被清空(<15 条)也判红。
· packages/ui-mac/src/main/gate-file-registry.test.ts —— **完备性闸**:凡文件名命中仓库
  闸门命名习惯(ratchet/lock/anchors/contract/copies/snapshot/drift/hygiene/rebrand/seam/
  coexistence/census)的测试文件,必须要么登记、要么在 NOT_GATES 里写明为什么不是。
  这一条才是「别再只应用一次」的机制本身;它自己也在登记簿里。实测新增未登记文件即红。

分层判据写进了 tsv 抬头、脚本抬头与 workflow 注释三处,并各自附上实测数字
(ui-mac 3077≥3000 全绿、ext 106≥100 全绿),免得下一个人再以为地板就够了。

* fix(e7): 委派规则 —— 主判据被委派出去时受托方也必须在册(登记簿 21→30)

owner 裁决 route-deep-link-consumer.test.ts 必须登记,理由是我自己的判据:
route-authority-ratchet 抬头写着「THE PRIMARY JUDGEMENT IS NOT IN THIS FILE」,那么删掉受托方
保证就没了,棘轮只守住「源码里有那段文字」—— 按本仓定义就是假闸门。而我判它不登记的理由是
「不命中命名词」,那正是问题所在:按名字枚举与当初只扫一级目录的 walker 是同一种缺陷。

做法(选显式列 + 发现绊线,不选自动解析抬头):
· 权威 = 登记簿新增 `delegates_to` 列,**默认拒**:每行必须显式填,没有委派写 `-`,留空即红;
  且每个受托方必须也在登记簿里。自然语言的委派说法太多(「真实判据见」「行为层闸门在」
  「移至」「由 X 断言」…),按措辞枚举与按文件名枚举是同一种缺陷,不能当权威。
· 发现绊线 = 闸门文件正文里出现的**任何**其它测试文件都必须已被分类(已登记 / NOT_GATES /
  REFERENCED_BUT_UNREGISTERED 三选一)。不理解语义,只要求「提到了就表态」。

用绊线核完 21 个抬头,**另外找出 7 处**未登记的委派关系(一层二层都没发现):
  websearch-copies      → core/alpha-websearch-sovereignty(运行时半场,此前不在任何 CI)
                        → sidecar-env、alpha-config-injection(「真实判据见」)
  gen-skill-paths       → ext-receipt-v2(「decoder 强度用例移至」)
  takeover-coexistence  → permission-single-surface(「行为层闸门在」)
  permission-mount      → permission-single-surface
  route-authority       → route-deep-link-consumer、route-upstream-shape
二阶闭包再找出 2 处:alpha-config-injection → sidecar-ready-message(纯文本锚曾被变异绕过)、
alpha-models。逐层登记到不动点。

登记簿 21 → 30 条(含首次纳入 CI 的 packages/core 那份主权闸运行时证据)。
删除实测 30/30 全 PASS。委派规则绕过实测:受托方未登记 → 两条断言各自转红;
delegates_to 留空 → 测试与 shell runner 两侧都转红。

未登记但已表态的 4 处写进 REFERENCED_BUT_UNREGISTERED,各带理由与跟踪票:
三个 opencode 云 MCP/网关测试归 #649 评估(owner 已指示不在本 PR 纳入),
两个上游 websearch 测试由 north-star 守卫保护、alpha 刻意不接管。

---------

Co-authored-by: jinjunnn <slmbaovanetti99@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
jinjunnn pushed a commit that referenced this pull request Jul 28, 2026
#223 AC5「packaged 真调用与 keyless fallback 均有证据」的取证需要 owner 本人的登录态,
只有取证之前的一切可以先做完。本 PR 交付那一切:

- 打包应用重打并真机启动验(alpha@94a76b669;装机版 app.asar 的 sha256 钉进探针,
  在错的构建上跑出来的绿会被第一条判据挡住)。
- 一个两相位探针:默认 = 登录态,`--keyless` = 登出态,**各自对错误的登录态 fail-closed**。
  故意不做自动识别 —— 会自己切相位的探针永远报不出「未登录,无法取证」。
  每项自带运行前定好的判据,输出机器可读 JSON;造不出来的失败态记 `not-producible` + 理由,
  不记成绿。探针零密钥:用的每个凭证都是打包应用自己写进 alpha-secrets 的,写盘前经 redact。
- 证据骨架:逐项判据表(登录态那半留空待 owner)、复现步骤、边界。

准备阶段已实跑出两件事:

1. **未登录自检**:登录态相位 blocked + exit 2,零证据产出(results/logged-in-*.json)。
2. **一个阻断项**:登出态 keyless 真调在本构建上是坏的。`readBoundedBody` 用的
   `Stream.runForEachWhile` 在谓词恒真时仍只读第一个 chunk(实测 4,090 / 18,063 字节),
   于是任何多 chunk 的搜索响应被截断 → `invalid_response`。#639 之前该处是 `response.text`,
   属那一刀带进来的回归;本仓 read.ts:143-145 早已记过同一个 API 的雷。
   修复归一张 CODE 票,不在本 VERIFY 票里动源码。

另记两项待裁决:402 的采集范围(Issue 正文与基线票 6 的 2026-07-25 更正相反),
以及云工具在引擎里的真实 id 可能是 `cloud_cloud_web_search`(MCP catalog 会拼 server 名前缀)。

Refs #643
jinjunnn added a commit that referenced this pull request Jul 28, 2026
* docs(verification): #643 E7 打包版取证准备 —— 探针 + 证据骨架 + 一个阻断项

#223 AC5「packaged 真调用与 keyless fallback 均有证据」的取证需要 owner 本人的登录态,
只有取证之前的一切可以先做完。本 PR 交付那一切:

- 打包应用重打并真机启动验(alpha@94a76b669;装机版 app.asar 的 sha256 钉进探针,
  在错的构建上跑出来的绿会被第一条判据挡住)。
- 一个两相位探针:默认 = 登录态,`--keyless` = 登出态,**各自对错误的登录态 fail-closed**。
  故意不做自动识别 —— 会自己切相位的探针永远报不出「未登录,无法取证」。
  每项自带运行前定好的判据,输出机器可读 JSON;造不出来的失败态记 `not-producible` + 理由,
  不记成绿。探针零密钥:用的每个凭证都是打包应用自己写进 alpha-secrets 的,写盘前经 redact。
- 证据骨架:逐项判据表(登录态那半留空待 owner)、复现步骤、边界。

准备阶段已实跑出两件事:

1. **未登录自检**:登录态相位 blocked + exit 2,零证据产出(results/logged-in-*.json)。
2. **一个阻断项**:登出态 keyless 真调在本构建上是坏的。`readBoundedBody` 用的
   `Stream.runForEachWhile` 在谓词恒真时仍只读第一个 chunk(实测 4,090 / 18,063 字节),
   于是任何多 chunk 的搜索响应被截断 → `invalid_response`。#639 之前该处是 `response.text`,
   属那一刀带进来的回归;本仓 read.ts:143-145 早已记过同一个 API 的雷。
   修复归一张 CODE 票,不在本 VERIFY 票里动源码。

另记两项待裁决:402 的采集范围(Issue 正文与基线票 6 的 2026-07-25 更正相反),
以及云工具在引擎里的真实 id 可能是 `cloud_cloud_web_search`(MCP catalog 会拼 server 名前缀)。

Refs #643

* docs(verification): #643 重打包到 e578e00 —— keyless 阻断项解除,登出态相位 10/10 绿

上一份装机产物(`94a76b669` / `8706d0c4…`)早于 #648 的 `readBoundedBody` 修复,
K1.5(登出态 keyless 真调)在其上必红 —— #643 的 AC2 不可能转绿。在当前 alpha
HEAD `e578e00ae` 上重跑 `ship:mac` 并重新装机,证据目录的指纹与 `probe.ts` 的
`PINNED_ASAR_SHA256` / `PINNED_COMMIT` 同步换成新产物
(`60589c59c58e44ac0daede93fc7397a8a04365f5345eac4312e205a0d8f48e44`)。

真机启动已验:main / renderer / GPU / NetworkService / NodeService 五进程齐,
CDP 列出 `oc://renderer/index.html` page target(窗口真开),
`server ready { url: 'http://127.0.0.1:50360' }`,crash.log 无崩溃。
本仓两个打包坑(source-only contracts-consumer 留原始 .ts、eager ajv `new Function`
撞 renderer CSP)均未复现。

阻断项解除,用两条独立判据确认「打包版里真的生效」而非「源码合了就假定」:
1. 解包 app.asar,`out/main/chunks/node-*.js` 的 `McpWebSearch.readBoundedBody`
   确为 `Stream.runForEach` + `catchTag("BodyCapReached")`;
2. 真机跑 `--keyless`:K1.5 `completed`,拿到完整多 chunk 的真实搜索结果,exit 0、
   10/10 必需项通过。

两条反向 fail-closed 自检也在新产物上复验:登出态跑登录态相位 → blocked + exit 2;
登录态跑 `--keyless` → blocked + exit 2。两者都只记前置、零证据产出。

未改动源码,未改动 `docs/verification/` 下任何其它目录;旧产物的原始记录
(`*20260727T0952*.json`)保留不动。登录态那一半仍待 owner 本人执行。

Refs #643

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(verification): #643 402 按裁决改为「采」+ MISMATCH 判读方法

owner 2026-07-27 在 #643 上裁定 402「采」(多采一项没有害处,漏采要再叫 owner 一次),
但上一轮探针仍按 Issue 正文走 out-of-scope —— 裁决没落进代码。本次补上。

**P3.7 从「跳过」改成真打的判据**,且不预设结论:发一次带真 bearer 的
`POST /v1/tools/web_search`,按运行前定好的规则判读 ——
402 ⇒ pass;200 ⇒ `not-producible`(账户被预授权通过,这本身就是「今天产生不了」的
可观测证据,带实测 balanceFen/walletUsedFen/plan 作理由);其它状态 ⇒ fail(LOUD)。
不记成绿,也不静默跳过 —— 与 P3.4/P3.5/P3.6 同一套处置。

可产生性已按平台源码判定(alpha-platform `packages/gateway/src/worker.ts`):
- 臂 A(per-job 预算耗尽)**桌面端不可达** —— 需 `auth.via === "job"` 的 job token
  (`lib/tenant-auth.ts:114-123`);桌面端拿的是 route-purpose 绑定 JWT,jobId 恒空 ⇒
  `perJobPrecall` 直接 pass/enforced:false。与 P3.4 同源。
- 臂 B(`accountPreauth` 拒绝)只在账户余额+会员额度双空时触发;估价是路由常量,
  客户端无调价杠杆。把账户打空是破坏性计费变更,且会连带打红同轮的 P2.1/P2.2/P2.3 ——
  402 证据与 AC1/计费证据不可能在同一账户形态下同时取到。**owner 无需为 402 做额外操作。**
映射本身由 L1 覆盖:`packages/opencode/test/tool/alpha-websearch-failure.test.ts`。

`#643` 正文冲突已消:out-of-scope 与退出条件两处按裁决改成与基线票 6 的 2026-07-25
更正一致(须含 402,产生不了则记 not-producible)。

README §7 补上 **`MISMATCH` 的判读方法** —— 此前只记录不解读,等于把判断留给一个没有
上下文的人。现在写清:出现 MISMATCH 意味着所有按 `cloud_web_search` 字面量下的闸
**当前是空闸门**(盯着一个引擎从不注册的工具名),并逐个列出要重核的闸(permission deny /
ext `tool.execute.before` / catalog 拼名)、下错了各会怎样、以及判据必须是「用真实 id
跑一遍确认真的命中」而不是 grep 到字面量。

证据同步重跑,使committed 证据由 committed 探针产出:
登出态相位在新产物上独立两遍均 10/10(K1.5 不是侥幸);两条反向 fail-closed 自检复验通过。

本地闸门:north-star UPSTREAM 命中 0;docs gate 11 个相对链接全解析。
未改源码,未改 `docs/verification/` 其它目录,未动应用登录态(保持登出)。

Refs #643

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(verification): #643 owner 登录态取证结果(打包版 e578e00)

owner 亲自登录后跑完登录态相位。23 项判据:AC2(登出态 keyless)已在前一轮通过;
本轮登录态暴露六条必需红,其中两条是真缺陷、两条是探针判据缺陷:

真缺陷
- 云 MCP tools/call 恒 forbidden:cloud_web_search 要 purpose=model.invoke,
  而桌面云通道令牌 ALPHA_CLOUD_TOKEN 恒为 cloud.dispatch(alpha-platform#131)
- /mcp 传输层零鉴权,tools/list 与账户无关 ⇒ P1.3 那条 PASS 是空绿(alpha-platform#132)

探针判据缺陷(alpha-code#651)
- P1.5 断的是「每个 agent 配置键有 deny」这个机制,而有效判决本来就全是 deny
- P1.2 断言观测点错:引擎加载时已解析 {file:},/config 天然看不到 file ref
- P3.8 判据构造上不可满足(工具已被 Permission.disabled 过滤,模型拿不到)

另:引擎侧真实工具 id 实测为 cloud_cloud_web_search(双前缀),按字面量
cloud_web_search 下的 permission 闸因此是空闸门(alpha-code#650)。

#643 不能关:AC1 待 #131 修复并手动部署后重取,AC3 待 #651 收敛判据。

---------

Co-authored-by: jinjunnn <slmbaovanetti99@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant