Skip to content

fix(grok): 把 #867 落到 main —— 含 #883(P0) 点名的修复,并修掉它自带的一处能力边界回归 - #1010

Merged
vansin merged 8 commits into
mainfrom
fix/867-grok-safe-navigation-landed
Aug 18, 2026
Merged

fix(grok): 把 #867 落到 main —— 含 #883(P0) 点名的修复,并修掉它自带的一处能力边界回归#1010
vansin merged 8 commits into
mainfrom
fix/867-grok-safe-navigation-landed

Conversation

@vansin

@vansin vansin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

grok 栈 #825 → #826 → #830 → {#836, #867}第四层。前三层已落地并关闭:
#825#100489a6164e)、#826#1006c85dca96)、#830#1008526163cd)。

没有 force-push #867 那条共享 draft 分支 —— 本 PR 从我自己的分支发。

🔴 为什么这一层要紧:#883(P0)点名的修复就在里面

#883 正文原话:

修复 已完成并验证:170 pass / 0 fail,移植到生产源码线 b830403b
是否上线 🔴 没有。在跑的节点仍是 agent-node 2.5.0-preview.31,修复未部署

b830403b 正是本层 7 个提交之一:

a9df8dba fix(grok): preserve safe TUI composer navigation
ccf9953e test(grok): record safe navigation evidence
bf0d3134 fix(grok): recover after fatal copresence boundary        ← 对应「产品内无恢复路径」
7914755a fix(grok): bind MCP audit to resolved Bun path
e5c593e2 docs(test): record communication dog recovery evidence
b830403b fix(grok): correlate batched automatic permissions        ← #883 点名的那个
19af299a docs(test): record batched approval recovery

⚠️ 合进 main ≠ 上线。 #883 那条 P0 要真正解除,还需要发一个带修复的 preview 版本
并重启那些共存节点 —— 那超出我的授权范围,已在 #883 上写明并留给 Vincent 拍板。

它同时是 #881 的修复(按内容核过,不是看标题猜的)

#881 的根因是「方向键把 composer 标成污染,回车被就地清掉,提示语却谎称是斜杠命令」。
本层对 runtime.ts 的改动:

-          const navigationLength = knownComposerNavigationLength(remainder);
+          const navigation = knownComposerNavigation(remainder);
+            if (navigation.kind === "history") {
+              this.warnBlockedPermissionModeChange("composer history navigation");
+            if (navigation.kind === "viewport") {
+              this.writeHumanBytes(remainder.subarray(0, navigation.length));

把导航拆成 history / viewport 两类,只有 history 触发那条 blocked 警告,
方向键(viewport)改为写穿

🔴 落地前预跑,抓到三处,全修,无豁免

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

原分支加了一条静态 import:

import { retireStoppedGrokCopresenceRuntime } from "./runtime/grok-copresence/runtime-retirement";

ESM 静态 import 是提升的 —— 被导入模块的顶层在 cli.ts 第一条语句之前执行,于是
policy.ts:22readPinnedGrokCopresenceCapabilityProfile() 读到的是 ambient
环境变量
,而不是启动时从节点配置钉下来的档位。能力档位就此可被环境变量绕过。

改成动态 import、放在调用点(钉档位之后),与本文件另外两处取法一致:

pin_line=569 static_imports=0 dynamic_imports=3 type_imports=1
every policy/runtime import is dynamic and lands after the boot-time profile pin.

顺带:追查「同样的危害还有没有别的路径」时发现这道门本身有个可绕过的缺口
(传递静态 import),已带实测假绿开在 #1009不在本 PR 里顺手改

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.mdshouldSkipMessage4669 → 4695。文本和 URL 两处都改。
(今晚第三次:4639 → 4662 → 4669 → 4695。曾提议「去掉行号根治」,已撤回 ——
实测去掉 #L 会让这条 pin 整个掉出那道门的正则,是删覆盖不是修问题。)

当前 main526163cd)上复跑,八道全 rc=0。

栈还剩最后一层

#836 —— 只有 1 个提交(fc221fc5),用 cherry-pick;range rebase 会撞它分支上那 48 个
已合进 main 的提交。

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

t and others added 5 commits August 18, 2026 23:16
预跑今天这套门时,#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>
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>
@vansin
vansin force-pushed the fix/867-grok-safe-navigation-landed branch from 07decf1 to f0d2f40 Compare August 18, 2026 15:16
@vansin
vansin merged commit f120844 into main Aug 18, 2026
26 checks passed
@vansin
vansin deleted the fix/867-grok-safe-navigation-landed branch August 18, 2026 15:28
vansin added a commit that referenced this pull request Aug 18, 2026
## 我造成了什么

- **#1004**(`89a6164e`,今天)把生成侧从 `'command = "bun"'`(硬编码)换成
  `resolveGrokCommhubMcpCommand()` 的返回值 —— **realpath 之后的绝对路径**;
- **#1010**(`f1208449`,今天)我把 `7914755a fix(grok): bind MCP audit to resolved Bun path`
  **外科式摘掉了**,而那正是同一次改动的**审计侧**。

⇒ main 因此停在一对不匹配的半边:**生成侧写绝对路径,审计侧要求裸 `"bun"`。**

我当时摘它的理由(「它让产品拒绝自己生成的配置」)**方向反了**:真正会拒绝的是**缺了它**。
我读到的那次 CI 红来自**夹具**,不来自产品 —— 见下。

## 真 grok 报什么(实测,不是推断)

容器里装钉死的 `0.2.93`、`--network none` 离线跑(宿主那份是 `1.0.5`,且其符号链接被
63 个在跑节点共用,没有动):

    容器内 grok --version → grok 0.2.93 (f00f96316d)
    config.toml:  command = "/usr/local/bin/fake-bun"
    $ grok inspect --json
    "mcpServers": [{ "name":"commhub", "transport":"stdio",
                     "target":"/usr/local/bin/fake-bun",
                     "source":{"type":"configToml","path":"/tmp/gh/config.toml"} }]

⇒ **`target` 逐字等于 config.toml 的 `command`。** 而生成侧那个解析器只可能返回
realpath 之后的绝对路径或抛错 ⇒ **拿裸 `"bun"` 去比永远不成立。**

(顺带:同一件事三条命令三种形状。`mcp doctor --json` 顶层是 `sources`/`servers`,
它的 `target` 是 **command + args 空格拼串**;`mcp list --json` 的字段叫 `command`。
拿错一条就会得出错的结论。)

## 两件必须一起落,分开落任一半就是我今天犯的错

**① 审计侧**(`agent-node/src/runtime/grok-copresence/runtime.ts`)

    -    || commhubRecord.target !== "bun"
    +    || resolve(String(commhubRecord.target || "")) !== resolve(expectedCommhubCommand)

新增第三个参数 `expectedCommhubCommand`(默认 `"bun"` 仅为兼容尚未传参的调用点),
`agent-node/src/cli.ts` 的生产调用点显式传 `commhubMcpCommand`。

**② 夹具**(`tests/test225-grok-preview-package-live/fake-grok.mjs`)

    -      target: "bun",
    +      target: inspectTarget,     // ← 从它自己声称的 configToml 里读 command

不修 ②,①会再一次「看起来让 test813 红」,而红的仍然是夹具。
**原来那个写死的 `"bun"` 不是「夹具不够真」,是它让一个真实的不一致在 CI 里永远看不见** ——
审计要 `"bun"`、夹具就报 `"bun"`,两者恒相符,而产品写的是绝对路径。#1016 就是这么被藏住的。

## 见红 / 见绿

`agent-node/src/runtime/grok-copresence/runtime.test.ts` 加三条,第三条就是 #1016 那个不一致:

    target=绝对路径, expected=同一个绝对路径      → 不抛
    target=绝对路径, expected=另一个绝对路径      → 抛 "runtime-owned commhub"
    target=裸 "bun", expected=绝对路径            → 抛(#1016 本身)

    bun test src/runtime/grok-copresence/runtime.test.ts     54 pass 0 fail
    把审计退回 main 的写法(target !== "bun")再跑          53 pass 1 fail   ← 见红
    还原                                                     54 pass 0 fail

原有 5 条断言仍传 2 个参数、夹具 target 是 `"bun"` ⇒ 默认值让它们照旧通过,向后兼容。

## 没做的

`bun build` 通过(276 modules),但**它不做类型检查**,所以那只证明能打包。
本机没有可用的 tsc(`npx tsc@5` / `typescript@5.9` 都取不到),**类型检查交给 CI**。
test813 的 Docker 套件也交给 CI 跑 —— 那是这次改动真正的验收点。

顺带:`docs/message-lifecycle.md` 的 `shouldSkipMessage` 行号 4691 → 4695(本次给 cli.ts 加了 3 行)。

Co-authored-by: t <internlmorg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
vansin added a commit that referenced this pull request Aug 18, 2026
## 事实

    4639 → 4662 → 4669 → 4691 → 4695 → 4713 → 4725 → 4728

八个值,对应 #987 / #1004 / #1006 / #1010 / #1017 / #1007 / #1023 / #1039。
规律:**任何往 `agent-node/src/cli.ts` 第 4600 行以上插代码的 PR 都会让它漂**,
那个 PR 就要多跑一轮 CI。这是每次都付的成本,不是偶发。

## 为什么现在才能这么修

`#1024` 里我提过「改成不可变 SHA」,但**当时实测证明它不成立** ——
`check-doc-symbol-pins.py` 的正则捕获了 ref 却从没用过那个捕获组,
SHA 和 `main` 一视同仁,一律拿工作树解析。单变量 A/B 两边逐字相同。

`#1025` 让脚本认了 ref(SHA pin 用 `git show <ref>:<path>` 取那个版本判)。
**这条路是那之后才通的。** 所以本 PR 是 #1025 的下游,不是 #1024 那个被推翻的提案的重提。

## 改动

    - ✅ v1.4.0;当前在 [cli.ts:4725 `shouldSkipMessage`](.../blob/main/agent-node/src/cli.ts#L4725)
    + ✅ v1.4.0;截至 [`04f37800` 的 cli.ts:4725 `shouldSkipMessage`](.../blob/04f37800/agent-node/src/cli.ts#L4725)

**「当前在」也一起改成「截至 `<sha>`」** —— 这不是措辞洁癖:
钉到某个 commit 之后,「当前」这个词就不再被那个链接支持了。
只改链接不改这句话,等于用一个更稳的 pin 去支撑一句更弱的真话。

## 验证:两个方向都见证过,而且**判定数始终是 5**

    未漂时                                rc=0  扫到 15  判定 5  跳过 10  漂移 0
    往 cli.ts 插 3 行(过去 8 次红的场景)  rc=0  扫到 15  判定 5  跳过 10  漂移 0   ← 不再红
    把 pin 的行号改成 100(错的)          rc=1  扫到 15  判定 5  跳过 10  漂移 1   ← 门仍然咬得动
    还原                                  rc=0

**第三行是本 PR 最该看的**:如果这个改法让门不再对错误 pin 判红,那就是把对象移出了取集 ——
「让门变绿」而不是「把问题修好」。它没有:判定数一次都没掉,错的 pin 照样红。

`docs-site` 取集同时核过:扫到 28 / 判定 8 / 跳过 20 / 漂移 0(#1025 之后它才在取集里)。

## 一个操作上的插曲,记一下

我第一次建 worktree 时目录已存在,`git worktree add` 失败 —— **但后面的 `cd` 成功进了那个陈旧目录**,
于是我在一份完全不相干的树上跑了检查,得到 `cli.ts:836` 和 `rc=2`。
**命令失败了,后续步骤照跑。** 后来加了 `cd ... || exit` 并先打印 HEAD 核对才发现。

Co-authored-by: t <internlmorg@gmail.com>
Co-authored-by: Claude Opus 5 <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

Development

Successfully merging this pull request may close these issues.

1 participant