feat(grok): grok-build-cli 启动前预检非特权 userns(Ubuntu 24.04+ 上它必然失败,但没有任何提示) - #752
Conversation
grok-build-cli 每个 turn 都在 `unshare --user --map-root-user` 下跑。
启动路径上只有 `process.platform !== "linux"` 这一道判断,而
**「是 Linux」不等于「非特权 userns 可用」**。
Ubuntu 24.04.3 实测(本机,2026-08-13):
unshare --user --map-root-user --keep-caps --pid --fork … /bin/true
→ rc=1 "unshare: write failed /proc/self/uid_map: Operation not permitted"
unshare --user /bin/true
→ rc=0 ← 命名空间本身能建,被拒的只是 uid_map 那一步
kernel.apparmor_restrict_unprivileged_userns = 1
没有预检时,失败推迟到第一个 turn,并以内核层 errno 出现 ——
读到的人会去查内核/权限,而不是「这个 runtime 在这台机上用不了」。
新增 assertUnprivilegedUserNsUsable(),在 unshare 二进制解析之后立刻挡下,
报错里给出:真实 stderr 首行、自查命令、以及**可执行的出路(改用 grok-build-acp)**。
🔴 探针是**真跑一次那个操作**,不是读 sysctl。sysctl 只是代理值,
发行版/容器/seccomp/LSM 都可能让两者不一致,决定成败的是操作本身。
测试里专门有一条断言探针参数就是 ["--user","--map-root-user","/bin/true"]。
🔴 报错刻意**不把「放宽 sysctl」写成推荐做法** —— 那是削弱全机安全边界的运维决策,
不该由一个节点的需求来驱动。测试里有一条断言这句措辞在。
验证:
bun test grok-build-cli.test.ts 25 pass / 0 fail(新增 4 条)
npm run build rc=0;dist/cli.js 里 uid_map 命中 1、grok-build-acp 命中 6
mutation A 闸门恒通过 2 fail(命中两条断言抛错的)
mutation B 探针改读 sysctl 代理值 2 fail(命中断言探针参数的两条)
还原 25 pass / 0 fail
⚠️ mutation A 第一次注入 diff 非空却全绿 —— 注入点落在了函数签名里而不是函数体。
**diff 非空只证明文件变了,不证明被测行为被改变了。** 第二次肉眼确认注入点后才咬住。
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2405cb6855
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ): void { | ||
| const exec = run ?? ((bin: string, args: string[]) => { | ||
| try { | ||
| require("child_process").execFileSync(bin, args, { stdio: ["ignore", "ignore", "pipe"], timeout: 10_000 }); |
There was a problem hiding this comment.
Sanitize the environment passed to the userns probe
When the parent contains config envRef values or ambient cloud credentials, this execFileSync call inherits the complete process.env, exposing those secrets to the executable selected through UNSHARE_BINARY or PATH. The actual turn launcher deliberately receives the allowlisted childEnv instead, so the new preflight bypasses that boundary; pass a projected helper environment to the probe as well.
Useful? React with 👍 / 👎.
| "grok-build-cli cannot start: this machine refuses unprivileged user-namespace uid_map writes" | ||
| + (probe.stderr.trim() ? ` (${probe.stderr.trim().split("\n")[0]})` : "") | ||
| + ".\n" | ||
| + " Ubuntu 24.04+ ships kernel.apparmor_restrict_unprivileged_userns=1, which blocks the\n" | ||
| + " `unshare --map-root-user` this runtime uses for every turn.\n" |
There was a problem hiding this comment.
Preserve non-uid_map probe failures
When unshare is missing, too old, times out, or fails for a container/seccomp reason, probe.ok is also false, but this path unconditionally reports a refused uid_map write and recommends inspecting Ubuntu's AppArmor sysctl. That makes the new diagnostic misleading precisely for the non-sysctl cases the comment says the real probe should distinguish; report a general probe failure unless the captured error actually identifies the mapping restriction.
Useful? React with 👍 / 👎.
| // Ubuntu 24.04+ 默认禁写 uid_map,而这个 runtime 每个 turn 都依赖它。 | ||
| // 在这里挡下来,才能给出「换 grok-build-acp」这种可执行建议; | ||
| // 否则失败推迟到第一个 turn,以内核层 errno 出现,把人引去查权限。 | ||
| assertUnprivilegedUserNsUsable(unshareBinary); |
There was a problem hiding this comment.
Run the userns preflight before registering the node
For a headless grok-build-cli node on a host that blocks this operation, this call is reached only from processWithGrokCli, after startup has already called register() and begun draining the inbox. The node therefore still advertises itself as healthy and discovers the hard prerequisite only after accepting its first task—the delayed-failure behavior this change is intended to eliminate; run or cache this gate in the headless startup path before registration.
Useful? React with 👍 / 👎.
| describe("assertUnprivilegedUserNsUsable (#grok userns preflight)", () => { | ||
| test("passes when the probe succeeds", () => { |
There was a problem hiding this comment.
Record the new tests' Docker result
These four cases extend the grok-build-cli.test.ts suite selected by Docker test219, but docs/tests/report-test219.txt remains bound to source commit 026937d0... and contains only the previous 21 cases, while this commit records only a direct bun test run. Consequently there is no repository artifact showing that the changed suite and build passed in the required isolated environment; run test219 through Docker and commit its updated report.
AGENTS.md reference: AGENTS.md:L7-L8
Useful? React with 👍 / 👎.
| + " Check with: sysctl kernel.apparmor_restrict_unprivileged_userns\n" | ||
| + " Preferred fix: use the `grok-build-acp` runtime instead — it does not need user namespaces.\n" |
There was a problem hiding this comment.
Document the headless userns prerequisite and fallback
This new operational gate and its recommendation to switch runtimes are absent from the authoritative docs/grok-build-cli-preview.md: its Requirements section lists only Linux/procfs, its headless quick start gives no userns compatibility check, and Common errors does not explain this failure or how to change and verify the runtime. A rebuild on the affected Ubuntu hosts therefore cannot determine from the repository whether the headless service is viable or follow a reviewed migration/rollback procedure; update the runbook in the same change.
AGENTS.md reference: AGENTS.md:L24-L24
Useful? React with 👍 / 👎.
|
补一条我当时不知道的限制,与本 PR 给出的建议直接相关。 本 PR 的报错文案建议 Ubuntu 24.04+ 用户「改用 而 不影响本 PR 的正确性(预检本身是对的,交互式使用下建议也有效), |
|
更正我上一条评论。 我上一条说「
走的是单节点路径,是可用的。我上次测试同时传了 本 PR 给的建议成立,不需要修改。 仍然成立的相关问题是另一件事: |
grok-build-cli每个 turn 都在unshare --user --map-root-user下跑。启动路径上只有
process.platform !== "linux"这一道判断 ——但**「是 Linux」不等于「非特权 userns 可用」**。
本机实测(Ubuntu 24.04.3,2026-08-13)
对照那一条很重要:它把结论从「这台机不支持 user namespace」收窄到
「这台机不允许非特权进程写 uid_map」—— 而后者正是
--map-root-user需要的。没有预检时,失败推迟到第一个 turn,以内核层 errno 出现。
读到的人会去查内核和权限,而不是意识到「这个 runtime 在这台机上根本用不了」。
这与刚修的 Bun 前置(#744)、opencode 的 cwd(#739)是同一形状:
硬环境前置 + 无预检 + 推迟到运行时的底层报错。
改动
assertUnprivilegedUserNsUsable(),在 unshare 二进制解析之后立刻挡下。报错给三样东西:真实 stderr 首行、自查命令、以及可执行的出路 —— 改用
grok-build-acp(它不需要 user namespace)。两个刻意的设计,都有测试钉住
🔴 探针是真跑一次那个操作,不是读 sysctl。 sysctl 只是代理值;
发行版、容器、seccomp、LSM 都可能让代理值与实际能力不一致,决定成败的是操作本身。
有一条测试断言探针参数就是
["--user", "--map-root-user", "/bin/true"]。🔴 报错不把「放宽 sysctl」写成推荐做法。 那是削弱全机安全边界的运维决策,
不该由一个节点的需求来驱动。有一条测试断言
operator decision这句措辞在。验证
mutation A 第一次注入时 diff 非空(6 行)却 25/0 全绿。
原因:我的正则
assertUnprivilegedUserNsUsable\([^)]*\)在参数里(bin: string, args: string[]) => …的第一个)处就截断了,return;落在了函数签名里而不是函数体里。diff 非空只证明文件变了,不证明被测行为被改变了。
第二次改成锚定函数体内的具体语句、并肉眼确认注入后的代码,才真正咬住。