fix(grok): preserve safe TUI composer navigation - #867
Conversation
|
Draft,不合,只留 review。这个 PR 改的是 🔴 一、那条身份比对,在两个分支下分别是恒假和恒真改后的判据里有这一条: && requestId === `tool:${input.requestTool}`其中两个值都来自同一个 event: const requestId = lifecyclePermissionIdentity(event); // :3286
const eventTool = lifecyclePermissionTool(event); // :2122
… isGrokPreviewAutomaticResolution({ requestTool: eventTool, … })而 if (requestId /* event.request_id | event.requestId */) return `id:${requestId}`;
return toolName ? `tool:${toolName}` : null;⇒ 分两种情况:
也就是说:这一条在能通过的那个分支里,比较的是同一个 event 里的同一个字段和它自己。它贡献 0。 🔴 而它看起来像是在做「把这次 resolution 和当初那个 request 对上」。 这是一条读起来像身份校验、实际不做校验的断言 —— 它的危险不在于错,在于它占住了那个位置,让后来的人以为那里有一道校验。 二、被它替换掉的那条,是真的身份绑定改之前: - && input.activeRequestId === `tool:${input.requestTool}`
- && requestId === input.activeRequestId而当时 ⇒ 旧版第二个合取项 新版把它换成了 🔴 计数不能回答「这次 resolution 属于哪一个 request」。 而这个 PR 的动机恰恰是并发/重叠的 request(测试名: 我理解旧版单个 三、
|
…1004) * fix: fail closed on broken Grok CommHub MCP * test: exercise Grok CommHub MCP readiness handshake * test: add isolated Grok MCP readiness gate * test: exercise the real Grok MCP doctor * docs: report Grok vendor MCP readiness * test: gate Grok readiness on product startup path * test: keep Grok gate dependency layers source-independent * test: anchor Grok readiness to runtime events * docs: record exact Grok MCP readiness evidence * fix(grok): 把 #825 的 MCP 就绪 fail-closed 落到 main(rebase + 补两处今天新门抓到的) #825(`fix(grok): fail closed when CommHub MCP is not ready`)从 2026-08-15 起是 draft,落后 main 120 个提交,上次 CI 只跑了 10 个 check(今天一个 PR 跑 13–23 个)。 它是 grok 栈 `#825 → #826 → #830 → {#836,#867}` 的**唯一出口**,它不落地, 上面四条谁都到不了 main。 **没有 force-push 那条共享 draft 分支** —— 本 PR 从我自己的分支发,内容是它那 9 个 补丁 rebase 到 `origin/main`(`rc=0`,0 冲突)再加下面两处修补。 ## 今天新合的两道门抓到了它两处 上一轮我测过「#825 引入新失败 0」,那句话当时是真的 —— 但那两道门是**今天晚些时候** 才合进 main 的,门槛抬高之后它就红了: 1. `test-suite-registration`(#1003) `tests/test813-grok-mcp-readiness/` 是新增套件,没有任何 CI 会跑它。 🔴 这条特别值得修而不是豁免:**#825 的目的就是「MCP 没就绪时 fail closed」, 而验证这件事的套件如果不进 CI,fail-closed 这个保证就没有任何东西持续守着。** 照 test831 的同构做法接进 qa.yml:两处 `paths` + build/run 两个 step。⚠️ 它的 Dockerfile 收的 build-arg 叫 `SOURCE_COMMIT`(不是 831 的 `TEST831_SOURCE_COMMIT`),容器里由 `ENV TEST813_SOURCE_COMMIT` 承接 —— 照抄 831 的参数名会静默拿不到值。 2. `doc-symbol-pins`(#1002) `docs/message-lifecycle.md` 把 `shouldSkipMessage` 钉在 `cli.ts#L4639`, 而 #825 给 cli.ts 加了 54 行,真值现在是 **4662**。 文本 `cli.ts:4639` 和 URL `#L4639` 是同一句话的两副面孔,两处都改 —— 只改 URL 会留下渲染出来给人读的那个错数字。 ## 验证 rebase 到 origin/main rc=0,0 冲突,9 个补丁 test-suite-registration rc=0 suites=198 registered=34 orphans=164 new=0 doc-symbol-pins rc=0 l1-paths-sync / qa-trigger-coverage / workflow-structure / docs-integrity / no-escaped-comments 全部 rc=0 `registered` 从 33 变 34、`orphans` 仍是 164 —— 新套件是接进 CI 了,不是塞进基线蒙混。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(test813): 一条从写下起就是空的变异 —— 接进 CI 的第一次运行就把它照出来了 把 test813 接进 CI(本 PR 上一个提交)之后,它在 GitHub runner 上第一次运行就红: MCP_READINESS_PASS tools=commhub_get_all_status,commhub_send_message,commhub_send_task,commhub_upload_file FAIL: mutation survived: upload-tool-removed MCP_READINESS_PASS tools=…(同一份列表,commhub_upload_file 仍在) 不是产品没拦住,是**变异根本没发生**: sed -i '/^[[:space:]]*"commhub_upload_file",[[:space:]]*$/d' agent-network/src/node-server.ts 那个模式要求整行只有 `"commhub_upload_file",`,而真实那行是 ` name: "commhub_upload_file",`(对象字面量的字段)。实测命中 **0** —— 在 origin/main 上也是 0,**它从写下那天起就没匹配过任何一行**。 套件此前不在任何 CI 里(#861 说的 164 个孤儿之一),所以没人见过它红。 删整行会破坏对象字面量语法,改成改名: sed -i 's/name: "commhub_upload_file"/name: "commhub_upload_file_MUT"/' … 干跑验证:文件确实变了(`243: name: "commhub_upload_file_MUT",`),已还原。 ## 顺带堵住这一类,不只这一条 给 `expect_red` 加空变异防护:跑完变异命令后,若两个源文件**都**没被改动就直接判红, 并说清楚是「sed 模式和源码对不上」而不是「产品没拦住」。 这两种结论指向完全不同的下一步 —— **去改产品 vs 去改测试** —— 而它们在没有这道防护 时打印出来是同一句 `mutation survived`。 同一个套件里另外三条 sed 都逐条查过,各自命中 1(其中打 fake-grok.mjs 那条还自带 `grep -Fxc … -eq 1` 基数守卫),只有这一条是空的。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: t <internlmorg@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(grok): add strict repo-read copresence profile * docs(tests): record restacked Grok repo-read evidence * feat(grok): 把 #826 的 strict repo-read 共存档位落到 main grok 栈 `#825 → #826 → #830 → {#836,#867}` 的第二层。#825 已由 #1004 落地 (main `89a6164e`),所以这一层现在可以直接对 main 落。 **没有 force-push #826 那条共享 draft 分支**,本 PR 从我自己的分支发。 ## rebase 方式:只取它自己那两个提交 直接 `git rebase origin/main` **会冲突** —— 冲在 `436b4bdb fix: fail closed on broken Grok CommHub MCP`,那是 **#825 自己的提交**,已经以 squash 形式在 main 上了,而 #826 的历史里还带着它。 正确做法是把它自己那部分接到 main 上: git rebase --onto origin/main <#825-head> <#826-head> rc=0 它自己的两个提交: 4496835 feat(grok): add strict repo-read copresence profile 1149676 docs(tests): record restacked Grok repo-read evidence 结果 12 files changed, 284 insertions(+), 36 deletions(-)。 ## 今天这套门抓到一处 `doc-symbol-pins`:`docs/message-lifecycle.md` 把 `shouldSkipMessage` 钉在 `cli.ts#L4662`,而本层又往 `cli.ts` 加了行,真值变成 **4669**。文本和 URL 两处都改。 🔴 **这是同一条 pin 今晚第二次漂**(#1004 刚把它从 4639 改到 4662)。 往 `cli.ts` 这种热点文件钉**行号**,几乎每个碰它的 PR 都会把它顶漂 —— 改成只钉符号、不钉行号才是根治,但那是另一条改动,不在这里顺手做。 其余全绿:test-suite-registration(suites=198 registered=34 orphans=164 new=0)/ doc-source-pins / no-escaped-comments / no-memory-slugs / home-path-baseline / public-script-safety / l1-paths-sync / copresence-profile-pin。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: t <internlmorg@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
预跑今天这套门时,#867 这一层红了三条。三条都是真的,逐条修,没有走豁免。 ## 🔴 1. 能力边界回归(copresence-profile-pin) ::error file=agent-node/src/cli.ts,line=76::static import of grok-copresence/policy|runtime. ESM static imports are hoisted, so its top level runs BEFORE the profile pin at line 570 #867 加了一条**静态** import: import { retireStoppedGrokCopresenceRuntime } from "./runtime/grok-copresence/runtime-retirement"; ESM 静态 import 是提升的 —— 被导入模块的顶层会在 cli.ts 第一条语句之前执行,于是 `policy.ts` 顶层那句 `readPinnedGrokCopresenceCapabilityProfile()` 读到的是 **ambient 环境变量**,而不是启动时从节点配置钉下来的档位。**能力档位就此可以被环境变量绕过。** 改成动态 import,放在调用点、启动钉档位之后,与本文件里另外两处 grok-copresence 的取法一致:`static_imports 1 → 0,dynamic_imports 2 → 3`。 这道门(#937 建的)第一次抓到真东西,而它抓到的是一条**在 draft 里躺了三天**的回归。 ## 2. 公开仓里的真人 home 路径(home-path-baseline) 新文件 `docs/tests/report-grok-copresence-fatal-recovery-7914755a.txt` 有 3 处 `/home/<真人名>/`。承重的是 `.commhub/runtime-commdog-…` 那部分,前缀不是, 所以按门给的第一条修法换成 `~/`,证据一字不损,**不抬基线**。 ## 3. 行号 pin 第三次漂(doc-symbol-pins) `docs/message-lifecycle.md` 的 `shouldSkipMessage`:本层又动了 `cli.ts`(含上面第 1 条 的改动),`4669 → 4695`。文本和 URL 两处都改。 (这是它今晚第三次漂:4639 → 4662 → 4669 → 4695。我曾提议「去掉行号根治」, **那句已撤回** —— 实测去掉 `#L` 会让这条 pin 整个掉出那道门的正则: `扫到 15→14 / 判定 5→4 / rc 1→0`,是删覆盖不是修问题。) ## 复跑 home-path-baseline / copresence-profile-pin / doc-symbol-pins / test-suite-registration / doc-source-pins / no-escaped-comments / no-memory-slugs / l1-paths-sync 全部 rc=0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(grok): preserve safe TUI composer navigation * test(grok): record safe navigation evidence * fix(grok): recover after fatal copresence boundary * docs(test): record communication dog recovery evidence * fix(grok): correlate batched automatic permissions * docs(test): record batched approval recovery * fix(grok): 落地前修掉 #867 的三处 —— 其中一处是能力边界回归 预跑今天这套门时,#867 这一层红了三条。三条都是真的,逐条修,没有走豁免。 ## 🔴 1. 能力边界回归(copresence-profile-pin) ::error file=agent-node/src/cli.ts,line=76::static import of grok-copresence/policy|runtime. ESM static imports are hoisted, so its top level runs BEFORE the profile pin at line 570 #867 加了一条**静态** import: import { retireStoppedGrokCopresenceRuntime } from "./runtime/grok-copresence/runtime-retirement"; ESM 静态 import 是提升的 —— 被导入模块的顶层会在 cli.ts 第一条语句之前执行,于是 `policy.ts` 顶层那句 `readPinnedGrokCopresenceCapabilityProfile()` 读到的是 **ambient 环境变量**,而不是启动时从节点配置钉下来的档位。**能力档位就此可以被环境变量绕过。** 改成动态 import,放在调用点、启动钉档位之后,与本文件里另外两处 grok-copresence 的取法一致:`static_imports 1 → 0,dynamic_imports 2 → 3`。 这道门(#937 建的)第一次抓到真东西,而它抓到的是一条**在 draft 里躺了三天**的回归。 ## 2. 公开仓里的真人 home 路径(home-path-baseline) 新文件 `docs/tests/report-grok-copresence-fatal-recovery-7914755a.txt` 有 3 处 `/home/<真人名>/`。承重的是 `.commhub/runtime-commdog-…` 那部分,前缀不是, 所以按门给的第一条修法换成 `~/`,证据一字不损,**不抬基线**。 ## 3. 行号 pin 第三次漂(doc-symbol-pins) `docs/message-lifecycle.md` 的 `shouldSkipMessage`:本层又动了 `cli.ts`(含上面第 1 条 的改动),`4669 → 4695`。文本和 URL 两处都改。 (这是它今晚第三次漂:4639 → 4662 → 4669 → 4695。我曾提议「去掉行号根治」, **那句已撤回** —— 实测去掉 `#L` 会让这条 pin 整个掉出那道门的正则: `扫到 15→14 / 判定 5→4 / rc 1→0`,是删覆盖不是修问题。) ## 复跑 home-path-baseline / copresence-profile-pin / doc-symbol-pins / test-suite-registration / doc-source-pins / no-escaped-comments / no-memory-slugs / l1-paths-sync 全部 rc=0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(grok): 摘掉 `7914755a` —— 它让 test813 的 canonical-Bun 产品路径起不来 CI 上 `doc source-pin floor (Docker)` 这个 job 红了(它同时跑 test813): MUTATION_RED upload-tool-removed ← 我在 #1004 修好的那条空变异,现在真的会红了 MUTATION_RED stale-three-tool-doctor PRODUCT_PATH_NEGATIVE_PASS registration=absent session=unchanged FAIL: canonical-Bun product path did not reach TUI readiness GrokCopresenceFailure: grok copresence pre-spawn audit failed: grok copresence refuses any MCP server except its runtime-owned commhub stdio server failureCode: 'spawn_audit' ## 为什么 `7914755a fix(grok): bind MCP audit to resolved Bun path` 把审计从 commhubRecord.target !== "bun" 改成把**解析后的绝对 bun 路径**传进去、两侧 `resolve()` 后再比。而生成那份 MCP 配置的 `resolveGrokCommhubMcpCommand()` 用的是 **`realpathSync()`(解析符号链接)**, 审计侧用的是 **`resolve()`(只归一化路径,不解析符号链接)**。容器里 `bun` 是符号链接时, 两侧就对不上 —— 于是 pre-spawn 审计把自己生成的那份配置也拒了。 ## 处理:只摘这一个提交,其余照落 git rebase --onto <该提交>^ <该提交> <分支> 剩下 7 个提交,`runtime.ts` 那处比较回到 main 现在的写法(`target !== "bun"`)。 **#883 那条 P0 点名的 `b830403b` 和 #881 的修复 `a9df8dba` 都在保留的这 7 个里**, 本次落地的核心价值不受影响。 ## 不在这里顺手修 `7914755a` 的理由 它要解决的问题是真的(裸 `"bun"` 比较挡不住指向别处的 bun)。但把它改对需要决定 **审计与生成两侧统一用哪一种解析**(`realpath` vs `resolve`),那是安全审计语义, 要有自己的见红证据;而且它已经被 #836 的核对牵扯进来(`fc221fc5` 是它的前身)。 **留作单独一条,不塞进这条含 P0 的 PR。** ## 顺带 摘掉这 6 行之后 `shouldSkipMessage` 的行号 pin 从 4695 变回 **4691**,已改(文本+URL)。 八道门在本地全 rc=0。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: t <t@t> Co-authored-by: t <internlmorg@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
这条的内容已落到 main(#1010,
|
| 文件 | 新增行 | 在 main 上找不到 |
|---|---|---|
agent-node/src/runtime/grok-copresence/runtime.ts |
136 | 2 |
agent-node/src/runtime/grok-copresence/runtime.test.ts |
120 | 5 |
agent-node/src/runtime/grok-copresence/runtime-retirement.ts |
27 | 0 |
agent-node/src/runtime/grok-copresence/runtime-retirement.test.ts |
58 | 0 |
agent-node/src/cli.ts |
19 | 5 |
docs/tests/report-grok-copresence-safe-navigation-a9df8dba.txt |
356 | 0 |
docs/tests/report-grok-copresence-fatal-recovery-7914755a.txt |
102 | 3 |
| 其余 3 个文件 | — | 0 |
每一处「找不到」都是我有意为之的,逐条列出来:
cli.ts (5)
import { retireStoppedGrokCopresenceRuntime } from "./runtime/grok-copresence/runtime-retirement";
→ 改成了动态 import(见下「能力边界回归」)
assertGrokCopresenceApprovalOwnership( / inspection, / auditRuntime.grokCliHome.home, / commhubMcpCommand,
→ 来自被摘掉的 7914755a
runtime.ts (2)
expectedCommhubCommand = "bun",
|| resolve(String(commhubRecord.target || "")) !== resolve(expectedCommhubCommand)
→ 来自被摘掉的 7914755a
report-…-fatal-recovery (3)
previous runtime: /home/vansin/.commhub/… ×3
→ 按 home-path-baseline 那道门的要求脱敏成 ~/,证据部分一字未动
没有一处无法解释的丢失。
落地时改了三处,两处是它自带的问题
1. 🔴 能力边界回归(copresence-profile-pin 抓到)
本分支加了一条静态 import:
import { retireStoppedGrokCopresenceRuntime } from "./runtime/grok-copresence/runtime-retirement";ESM 静态 import 是提升的 —— 被导入模块的顶层在 cli.ts 第一条语句之前执行,
于是 policy.ts:22 的 readPinnedGrokCopresenceCapabilityProfile() 读到的是
ambient 环境变量,而不是启动时从节点配置钉下来的档位。能力档位就此可被环境变量绕过。
改成动态 import 放在调用点(钉档位之后):static_imports 1 → 0,dynamic_imports 2 → 3。
顺带查「同样的危害还有没有别的路径」,发现那道门本身有个可绕过的缺口
(传递静态 import),已带实测假绿开在 #1009,未在本次顺手改。
2. 摘掉 7914755a(CI 上真红)
doc source-pin floor (Docker) 红了:
FAIL: canonical-Bun product path did not reach TUI readiness
GrokCopresenceFailure: grok copresence pre-spawn audit failed:
grok copresence refuses any MCP server except its runtime-owned commhub stdio server
failureCode: 'spawn_audit'
根因:生成配置那侧用 realpathSync()(解析符号链接),7914755a 改后的审计侧用
resolve()(不解析)。容器里 bun 是符号链接 ⇒ 节点拒绝了自己刚生成的配置。
外科式只摘这一个提交,其余 7 个照落。它要解决的问题是真的(裸 "bun" 比较挡不住
指向别处的 bun),已带证据开在 #1011。
3. 公开仓真人 home 路径 + 行号 pin
见上表。
这条修了什么(已按内容核过,不是看标题猜的)
- [grok-copresence] 按一次方向键,这一行就永远发不出去了(且提示语谎称是斜杠命令) #881(方向键让这行永远发不出去):把导航拆成
history/viewport,
只有history触发 blocked 警告,方向键改为写穿。main 上已可见。 - [grok-copresence][P0] 空 composer 上的回车永久死锁节点(通信狗实例,产品内无恢复路径) #883(P0) 正文点名的
b830403b fix(grok): correlate batched automatic permissions
与bf0d3134 fix(grok): recover after fatal copresence boundary都已落 main。
⚠️ 但合进 main ≠ 上线 —— 在跑的节点仍需发新 preview 并重启才会拿到,已在 [grok-copresence][P0] 空 composer 上的回车永久死锁节点(通信狗实例,产品内无恢复路径) #883 写明。
What changed
runtime_closedforeverRoot cause
The live
通信狗incidents had three layers. Its original x-search profile denied a repository tool that Grok nevertheless auto-resolved; the bridge correctly failed closed, but agent-node cached that terminal runtime. The repo-read pilot then exposed an obsolete literal-bunaudit comparison. After those were fixed, the real Grok event stream emitted three automatic permission requests first (search_tool,list_dir,search_tool) and resolved them later as (search_tool,search_tool,list_dir). A scalar pending-permission slot misclassified this valid batched lifecycle as an approval-boundary violation, after which six queued tasks failed within seconds.Frozen coordinates
b830403b82a97a98ec004e59c86eb294ddfd177119af299adea89cb6296a5d3971b574ad88e2c423sha256:261ade0fd44147370df56a7377f409c840f3e65a07aa472174e7761184b45ecb5c0d5cbcfb0350af1eb27e78de1ccd5902c5ba22be732df415fe26851c269a45This Draft remains stacked on the existing Grok co-presence work. It does not publish npm or merge into the base branch.
Validation
duplicate preview automatic permission request84 pass / 0 fail / 3 files1288 pass / 0 fail / 92 files; named mutation red;RESULT: PASS通信狗with0:nodeand1:tui; it reached TUI input-ready, CommHub registration, and SSE, and Hub reports idle with zero pending/in-flight tasksFull evidence:
docs/tests/report-grok-copresence-fatal-recovery-7914755a.txt.Honest limits
/modelremains intentionally blocked: the current FIFO protocol cannot yet prove when Grok's interactive model picker has returned to the composer. Persisted model changes still require a controlled config update and restart.