-
Notifications
You must be signed in to change notification settings - Fork 9
feat(grok): grok-build-cli 启动前预检非特权 userns(Ubuntu 24.04+ 上它必然失败,但没有任何提示) #752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| import { afterEach, describe, expect, it } from "bun:test"; | ||
| import { afterEach, describe, expect, it, test } from "bun:test"; | ||
| import { chmodSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "fs"; | ||
| import { tmpdir } from "os"; | ||
| import { join } from "path"; | ||
| import { assertGrokCliFeatures, assertGrokCliVersion, buildGrokCliArgs, normalizeGrokCliTools, runGrokCliTurn } from "./grok-build-cli"; | ||
| import { assertGrokCliFeatures, assertGrokCliVersion, assertUnprivilegedUserNsUsable, buildGrokCliArgs, normalizeGrokCliTools, runGrokCliTurn } from "./grok-build-cli"; | ||
| import { buildGrokChildEnv } from "./grok-child-env"; | ||
|
|
||
| const roots: string[] = []; | ||
|
|
@@ -359,3 +359,43 @@ describe("runGrokCliTurn", () => { | |
| expect(Date.now() - started).toBeLessThan(2_000); | ||
| }); | ||
| }); | ||
|
|
||
| describe("assertUnprivilegedUserNsUsable (#grok userns preflight)", () => { | ||
| test("passes when the probe succeeds", () => { | ||
|
Comment on lines
+363
to
+364
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These four cases extend the AGENTS.md reference: AGENTS.md:L7-L8 Useful? React with 👍 / 👎. |
||
| let seen: { bin: string; args: string[] } | null = null; | ||
| expect(() => assertUnprivilegedUserNsUsable("unshare", (bin, args) => { | ||
| seen = { bin, args }; | ||
| return { ok: true, stderr: "" }; | ||
| })).not.toThrow(); | ||
| // 探针必须是**真实那个操作**,不是读 sysctl 之类的代理值。 | ||
| expect(seen).toEqual({ bin: "unshare", args: ["--user", "--map-root-user", "/bin/true"] }); | ||
| }); | ||
|
|
||
| test("throws with the real stderr and an actionable next step when uid_map is refused", () => { | ||
| // 这段 stderr 是 2026-08-13 在 Ubuntu 24.04.3 上实测到的原文。 | ||
| const real = "unshare: write failed /proc/self/uid_map: Operation not permitted"; | ||
| let msg = ""; | ||
| try { | ||
| assertUnprivilegedUserNsUsable("unshare", () => ({ ok: false, stderr: real })); | ||
| } catch (e: any) { msg = String(e.message); } | ||
| expect(msg).toContain("write failed /proc/self/uid_map"); | ||
| // 必须指出可执行的出路,而不是只报「失败了」。 | ||
| expect(msg).toContain("grok-build-acp"); | ||
| // 必须给出自查命令。 | ||
| expect(msg).toContain("sysctl kernel.apparmor_restrict_unprivileged_userns"); | ||
| // 🔴 不能把「放宽 sysctl」说成推荐做法 —— 那是削弱全机安全边界的运维决策。 | ||
| expect(msg).toContain("operator decision"); | ||
| }); | ||
|
|
||
| test("still throws when the probe fails with no stderr at all", () => { | ||
| // 兜底:拿不到 stderr 也必须 fail-closed,不能因为「没有证据」就放行。 | ||
| expect(() => assertUnprivilegedUserNsUsable("unshare", () => ({ ok: false, stderr: "" }))) | ||
| .toThrow(/refuses unprivileged user-namespace uid_map writes/); | ||
| }); | ||
|
|
||
| test("honours a custom unshare binary path", () => { | ||
| let bin = ""; | ||
| assertUnprivilegedUserNsUsable("/opt/util-linux/bin/unshare", (b) => { bin = b; return { ok: true, stderr: "" }; }); | ||
| expect(bin).toBe("/opt/util-linux/bin/unshare"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,6 +103,51 @@ export function assertGrokCliVersion(version: string): void { | |
| } | ||
|
|
||
| /** Translate the node profile's Claude-style names to Grok's internal IDs. */ | ||
| /** grok-build-cli 每个 turn 都在 `unshare --user --map-root-user` 下跑。 | ||
| * 调用方已经挡掉了非 Linux,但**「是 Linux」不等于「非特权 userns 可用」**: | ||
| * Ubuntu 24.04+ 默认 `kernel.apparmor_restrict_unprivileged_userns=1`, | ||
| * 此时写 /proc/self/uid_map 会被拒。 | ||
| * | ||
| * 实测(2026-08-13,Ubuntu 24.04.3): | ||
| * unshare --user --map-root-user … /bin/true | ||
| * → rc=1 "unshare: write failed /proc/self/uid_map: Operation not permitted" | ||
| * unshare --user /bin/true | ||
| * → rc=0 ← 命名空间本身能建,被拒的**只是 uid_map 那一步** | ||
| * | ||
| * 没有这道预检,失败会推迟到第一个 turn,并以内核层的 errno 出现 —— | ||
| * 读到的人会去查内核/权限,而不是「这个 runtime 在这台机上用不了」。 | ||
| * | ||
| * 🔴 判据是**真跑一次那个操作**,不是读 sysctl。sysctl 只是一个代理值: | ||
| * 发行版、容器、seccomp、LSM 都可能让两者不一致,而真正决定成败的是操作本身。 | ||
| * | ||
| * `run` 可注入,便于测试;默认用 execFileSync。 */ | ||
| export function assertUnprivilegedUserNsUsable( | ||
| unshareBinary: string, | ||
| run?: (bin: string, args: string[]) => { ok: boolean; stderr: string }, | ||
| ): 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the parent contains config Useful? React with 👍 / 👎. |
||
| return { ok: true, stderr: "" }; | ||
| } catch (e: any) { | ||
| return { ok: false, stderr: String(e?.stderr ?? e?.message ?? e) }; | ||
| } | ||
| }); | ||
| const probe = exec(unshareBinary, ["--user", "--map-root-user", "/bin/true"]); | ||
| if (probe.ok) return; | ||
| throw new Error( | ||
| "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" | ||
|
Comment on lines
+139
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When 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" | ||
|
Comment on lines
+144
to
+145
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new operational gate and its recommendation to switch runtimes are absent from the authoritative AGENTS.md reference: AGENTS.md:L24-L24 Useful? React with 👍 / 👎. |
||
| + " (Relaxing the sysctl weakens a host-wide security boundary; that is an operator decision,\n" | ||
| + " not something this node should require.)", | ||
| ); | ||
| } | ||
|
|
||
| export function normalizeGrokCliTools(tools: readonly string[]): string[] { | ||
| const mapped: string[] = []; | ||
| const unknown: string[] = []; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a headless
grok-build-clinode on a host that blocks this operation, this call is reached only fromprocessWithGrokCli, after startup has already calledregister()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 👍 / 👎.