Skip to content

feat(grok): grok-build-cli 启动前预检非特权 userns(Ubuntu 24.04+ 上它必然失败,但没有任何提示) - #752

Merged
vansin merged 1 commit into
mainfrom
feat/grok-cli-userns-preflight
Aug 12, 2026
Merged

feat(grok): grok-build-cli 启动前预检非特权 userns(Ubuntu 24.04+ 上它必然失败,但没有任何提示)#752
vansin merged 1 commit into
mainfrom
feat/grok-cli-userns-preflight

Conversation

@vansin

@vansin vansin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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 --kill-child=SIGKILL --mount-proc /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

对照那一条很重要:它把结论从「这台机不支持 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 这句措辞在。

验证

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   ← 产物是 minify 的,验存在不验可读

mutation A  闸门恒通过(等于没加这道闸)      2 fail ← 命中两条断言抛错的
mutation B  探针改读 sysctl 代理值            2 fail ← 命中两条断言探针参数的
还原                                          25 pass / 0 fail

⚠️ 一个我自己踩了又爬出来的坑,写在这里给后来人

mutation A 第一次注入时 diff 非空(6 行)却 25/0 全绿
原因:我的正则 assertUnprivilegedUserNsUsable\([^)]*\) 在参数里
(bin: string, args: string[]) => … 的第一个 ) 处就截断了,
return; 落在了函数签名里而不是函数体里。

diff 非空只证明文件变了,不证明被测行为被改变了。
第二次改成锚定函数体内的具体语句、并肉眼确认注入后的代码,才真正咬住。

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 非空只证明文件变了,不证明被测行为被改变了。** 第二次肉眼确认注入点后才咬住。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +139 to +143
"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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread agent-node/src/cli.ts
// Ubuntu 24.04+ 默认禁写 uid_map,而这个 runtime 每个 turn 都依赖它。
// 在这里挡下来,才能给出「换 grok-build-acp」这种可执行建议;
// 否则失败推迟到第一个 turn,以内核层 errno 出现,把人引去查权限。
assertUnprivilegedUserNsUsable(unshareBinary);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +363 to +364
describe("assertUnprivilegedUserNsUsable (#grok userns preflight)", () => {
test("passes when the probe succeeds", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +144 to +145
+ " 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@vansin
vansin merged commit b6f7913 into main Aug 12, 2026
8 checks passed
@vansin
vansin deleted the feat/grok-cli-userns-preflight branch August 12, 2026 20:28
@vansin

vansin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

补一条我当时不知道的限制,与本 PR 给出的建议直接相关。

本 PR 的报错文案建议 Ubuntu 24.04+ 用户「改用 grok-build-acp」。这个建议在交互式使用下成立,
但我刚穷举了 cli.ts:3470VENDORS 表 —— 它只能产出三种 runtime:

claude-agent-sdk (9) / claude-code-cli (2) / codex-sdk (1)
grok-build-acp  命中 0

anet node create --batch 的 runtime 来自这张表。所以
grok-build-acp 无法通过 --batch 非交互创建 —— 在脚本化 / CI 场景下,
我给的这条出路和原路一样走不通。

不影响本 PR 的正确性(预检本身是对的,交互式使用下建议也有效),
建议的完整性有缺口,已记在 #765

@vansin

vansin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

更正我上一条评论。

我上一条说「grok-build-acp 无法非交互创建,所以本 PR 给的出路在 CI/脚本场景下走不通」。
这个说法下重了,收回。

cli.ts:3886 的 usage 明确支持 --runtime grok-build-acp,而 createCommand() 只有在
--batch 时才进那个只认 preset 的向导(cli.ts:3880)。所以

anet node create <name> --runtime grok-build-acp      # 不加 --batch

走的是单节点路径,是可用的。我上次测试同时传了 --runtime--batch,
--batch 赢了,我把「这组参数走不通」误当成了「这个运行时不可自动化」。

本 PR 给的建议成立,不需要修改。

仍然成立的相关问题是另一件事:--batch静默忽略 --runtime,
且提示把人指向对该运行时永远无解的 --preset —— 已记在 #765,与本 PR 无关。

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.

2 participants