From 551f5961507cc27ada6962460676d8d0aff0dc8b Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 04:46:18 +0200 Subject: [PATCH 01/26] feat: pin Grok CLI 1.0.34 lean worker contract and single config renderer --- scripts/liveGrokBrokerSession.ts | 16 +-- src/contracts/grokWorkerContract.ts | 42 ++++++++ src/contracts/runtimeContractManifest.ts | 33 ++++++ src/runtime/grokBrokerModelPolicy.ts | 32 ++++++ src/runtime/grokBrokerWorkerConfig.test.ts | 65 +++++++++++- src/runtime/grokBrokerWorkerConfig.ts | 115 +++++++++++++++++++-- 6 files changed, 284 insertions(+), 19 deletions(-) create mode 100644 src/contracts/grokWorkerContract.ts create mode 100644 src/runtime/grokBrokerModelPolicy.ts diff --git a/scripts/liveGrokBrokerSession.ts b/scripts/liveGrokBrokerSession.ts index 092bb9b..0e0663e 100644 --- a/scripts/liveGrokBrokerSession.ts +++ b/scripts/liveGrokBrokerSession.ts @@ -9,7 +9,8 @@ import { terminateChild, trackCliChild } from "../src/pi/cliProcess.ts"; import { decodeGrokHeadlessTurn } from "../src/pi/grokHeadlessResult.ts"; import { readGrokBrokerCredential } from "../src/runtime/grokBrokerCredentialReader.ts"; import { startGrokBrokerProxy } from "../src/runtime/grokBrokerProxy.ts"; -import { renderGrokBrokerWorkerConfig } from "../src/runtime/grokBrokerWorkerConfig.ts"; +import { DEFAULT_GROK_BROKER_MODEL_POLICY } from "../src/runtime/grokBrokerModelPolicy.ts"; +import { renderGrokBrokerWorkerArgs, renderGrokBrokerWorkerConfigWith } from "../src/runtime/grokBrokerWorkerConfig.ts"; // Explicit live auth/transport check, not the Linux native worker/isolation E2E. // Read the operator credential only in this process; never stage or rotate it. @@ -40,15 +41,16 @@ try { const helper = path.join(home, "auth-helper"); await writeFile(helper, `#!/bin/sh\nprintf '{"access_token":"${capability}","expires_in":600}\\n'\n`, { mode: 0o700 }); // No MCP tools are needed for this exact-reply authentication probe. - await writeFile(path.join(home, "config.toml"), renderGrokBrokerWorkerConfig(helper, proxy.port).split("[mcp_servers.daimon]")[0]); + await writeFile(path.join(home, "config.toml"), renderGrokBrokerWorkerConfigWith(DEFAULT_GROK_BROKER_MODEL_POLICY, { helperPath: helper, proxyPort: proxy.port, mcpUrl: "http://127.0.0.1:43124/mcp" }).split("[mcp_servers.daimon]")[0]); const prompt = path.join(home, "prompt.txt"); await writeFile(prompt, `Reply exactly ${sentinel}. Do not use tools.`); stage = `model turn ${round}`; - const child = trackCliChild(spawn("grok", [ - "--sandbox", "strict", "--prompt-file", prompt, "--no-memory", "--no-subagents", - "--disable-web-search", "--max-turns", "1", "--permission-mode", "dontAsk", - "--model", "daimon-broker-grok", "--output-format", "streaming-messages-json", - ], { + // The proxy only forwards the lean worker request shape (pinned client + // version, exact tool set, declared effort), so the probe uses the same + // argv as the native launcher with the built-in strict profile. + const args = [...renderGrokBrokerWorkerArgs(prompt, home)].map((value) => value === "daimon-strict" ? "strict" : value); + args[args.indexOf("--max-turns") + 1] = "1"; + const child = trackCliChild(spawn("grok", args, { cwd: home, detached: process.platform !== "win32", env: { PATH: process.env.PATH, HOME: home, GROK_HOME: home, LANG: "C", LC_ALL: "C", TZ: "UTC" }, stdio: ["ignore", "pipe", "pipe"], diff --git a/src/contracts/grokWorkerContract.ts b/src/contracts/grokWorkerContract.ts new file mode 100644 index 0000000..c0cc666 --- /dev/null +++ b/src/contracts/grokWorkerContract.ts @@ -0,0 +1,42 @@ +/** + * Fixed operating contract every broker-launched Grok worker receives through + * `--system-prompt-override`. + * + * The override replaces Grok's ~12k-token coding-agent system prompt and stops + * cwd `AGENTS.md` injection; the agent's identity and instructions still arrive + * in the prompt file. It is compiled into the native launcher byte-for-byte + * (`DBL_GROK_SYSTEM_PROMPT`), pinned by sha256 in the runtime contract + * manifest, and must stay ASCII without quotes or backslashes so the C literal + * needs no escaping. + * + * Daimon's per-wake tools reach Grok as deferred MCP tools named + * `daimon__` (server `[mcp_servers.daimon]`). Naming them lets `use_tool` + * run directly and saves one `search_tool` round trip per tool (P0: 3 → 2 + * requests). + */ +export const DAIMON_GROK_SYSTEM_PROMPT = [ + "You are a headless Daimon agent; no human is present.", + "Your identity, instructions and wake event are in the user prompt.", + "Daimon tools are MCP tools on server daimon: call a known one directly with use_tool (tool_name daimon__moltnet_read, daimon__moltnet_send, daimon__memory_search, daimon__memory_register, or another daimon__ name you were given); use search_tool only for a name you do not know.", + "If a tool result says output was saved to a file, read that path with read_file.", + "If a tool fails, do not retry it in a loop: stop and report the failure.", + "Your final answer is a private note to the runtime: one line, or empty." +].join(" "); + +/** Closed declared-model vocabulary; defaults are `grok-4.6` at `low`. */ +export const GROK_BROKER_MODELS = Object.freeze(["grok-4.6", "grok-4.5", "grok-build"] as const); +export const GROK_BROKER_REASONING_EFFORTS = Object.freeze(["low", "medium", "high"] as const); + +/** `--tools` input ids. These are NOT the model-visible names (see below). */ +export const GROK_WORKER_TOOL_IDS = Object.freeze(["run_terminal_cmd", "read_file", "grep", "list_dir", "search_tool", "use_tool"] as const); + +/** + * The exact tool names a lean worker request body must carry. + * + * Grok 1.0.34 fails open on an unmappable `--tools` entry and ships all 19 + * tools, so the proxy compares every upstream body against this set. + */ +export const GROK_WORKER_VISIBLE_TOOLS = Object.freeze(["grep", "list_dir", "read_file", "run_terminal_command", "search_tool", "use_tool"] as const); + +/** `--max-turns` backstop compiled into the launcher; per-wake ceilings belong to the broker. */ +export const GROK_WORKER_MAX_TURNS = 48 as const; diff --git a/src/contracts/runtimeContractManifest.ts b/src/contracts/runtimeContractManifest.ts index b462d54..561e781 100644 --- a/src/contracts/runtimeContractManifest.ts +++ b/src/contracts/runtimeContractManifest.ts @@ -1,4 +1,5 @@ import { WORK_AVAILABILITY_SCHEMA, WORK_BLOCKED_SCHEMA } from "./attentionContract.js"; +import { GROK_BROKER_MODELS, GROK_BROKER_REASONING_EFFORTS, GROK_WORKER_MAX_TURNS, GROK_WORKER_TOOL_IDS, GROK_WORKER_VISIBLE_TOOLS } from "./grokWorkerContract.js"; import { ORGANIZATION_RUNTIME_CONFIG_SCHEMA, ORGANIZATION_RUNTIME_CONFIG_V2_SCHEMA, @@ -34,6 +35,38 @@ export const GROK_ENGINE_BROKER = { providerProxy: { host: "127.0.0.1", port: 43_123 }, mcpFacade: { host: "127.0.0.1", port: 43_124, path: "/mcp" }, identities: { organizationUid: 2_000, brokerUid: 2_100, firstWorkerUid: 2_200 }, + grokCliVersion: "1.0.34", + grokCliBuild: "3736acbc8658", + grokCliArtifacts: { + arm64: { url: "https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-1.0.34-linux-aarch64", sha256: "39ab87666877d64ef3a40aa60fbe0c3b6a6acd7001b78fe60e2c76bb6cfc4a94", bytes: 136_090_504 }, + x64: { url: "https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-1.0.34-linux-x86_64", sha256: "be5905e107d2b8b5f3c142d21ecfe4c8fd32a913d2fd551b788707930c4dc80d", bytes: 163_035_648 } + }, + worker: { + modelId: "daimon-broker-grok", + models: GROK_BROKER_MODELS, + reasoningEfforts: GROK_BROKER_REASONING_EFFORTS, + defaultModel: "grok-4.6", + defaultReasoningEffort: "low", + toolIds: GROK_WORKER_TOOL_IDS, + visibleTools: GROK_WORKER_VISIBLE_TOOLS, + maxTurns: GROK_WORKER_MAX_TURNS, + systemPromptSha256: "2c31c0085a54a4efbf9c0cf0b8124c56e47f38691b7f0c7fa233a74abaa8ddf8", + // sha256 of `renderGrokBrokerWorkerConfig({ model, reasoningEffort })`, the only accepted config.toml bytes. + configSha256: { + "grok-4.6": { low: "e09c127363094a7cad560586d89e994a9e6117b9c548feaffb1b5b01705cf363", medium: "b90807d3c73651a1a3f0bf89eacb0c73360e7cfc34d10e0185a381a27b40a718", high: "045171e44ba44b09522589ba85770f7c09fb8cab3e3f76fbb88cb534dcc01018" }, + "grok-4.5": { low: "848df2f71d0a88cf1185728cd6e0b7e15238b3a7536bf1538467f8c4868b0647", medium: "3e84795e834cf7b5857c24070d9f91b951c9c5f806823dd29c92cf88635a9318", high: "792f90bb7ae5154d6e002419f5b308e2ea975fc55ae7c324952f928329238f93" }, + "grok-build": { low: "1be3438799f9023b6acdaec991c139133bc791877f86a8b139129ef4b7b8386c", medium: "cadef8a2fcca778b515fcc10bfa8ab2ed8425fa46f37dc3a64095b477beb914a", high: "cb3ef9f71eefa913517dc775e5d72c49cbf718cf6c43ccc33d55b22401ef2e2f" } + }, + // Worker `GROK_HOME` layout the broker attests before every turn. The home and + // its `sessions/` directory are root-owned, worker-group writable and sticky so + // Grok can create its own state but never replace a root-owned file. + home: { + directory: { uid: 0, group: "worker", mode: 0o1771 }, + sessionsDirectory: { relativePath: "sessions", uid: 0, group: "worker", mode: 0o1771 }, + readOnlyFiles: { names: ["config.toml", "managed_config.toml", "requirements.toml", "sandbox.toml", "trusted_folders.toml"], uid: 0, gid: 0, mode: 0o444 }, + sandboxEvents: { relativePath: "sessions/sandbox-events.jsonl", owner: "worker", group: "broker", mode: 0o640 } + } + }, bounds: { promptBytes: 65_536, capabilityBytes: 4_096, capabilityBundleBytes: 8_196, outputBytes: 65_536 }, artifacts: { sourceSha256: "bdcab1e12dcc531ed8e56f890263ca23a9ee7bac468191dd598e143df4ff8c58", diff --git a/src/runtime/grokBrokerModelPolicy.ts b/src/runtime/grokBrokerModelPolicy.ts new file mode 100644 index 0000000..266a1d7 --- /dev/null +++ b/src/runtime/grokBrokerModelPolicy.ts @@ -0,0 +1,32 @@ +/** + * The closed model and reasoning-effort vocabulary a Grok broker worker may be + * declared with. + * + * Both halves of one declaration are consumed from this single parser: the + * worker `config.toml` renderer (`grokBrokerWorkerConfig.ts`) writes them into + * the worker's only custom model, and the provider proxy + * (`grokBrokerProxyRequest.ts`) refuses any request body that does not carry + * exactly them. Nothing is inherited: Grok 1.0.34 silently drops an effort for + * a model that does not declare effort support, and its embedded catalog + * default for `grok-4.6` is `high`. + */ +import { GROK_BROKER_MODELS, GROK_BROKER_REASONING_EFFORTS } from "../contracts/grokWorkerContract.js"; + +export { GROK_BROKER_MODELS, GROK_BROKER_REASONING_EFFORTS }; +export type GrokBrokerModel = (typeof GROK_BROKER_MODELS)[number]; +export type GrokBrokerReasoningEffort = (typeof GROK_BROKER_REASONING_EFFORTS)[number]; +export type GrokBrokerModelPolicy = Readonly<{ model: GrokBrokerModel; reasoningEffort: GrokBrokerReasoningEffort }>; + +export const DEFAULT_GROK_BROKER_MODEL_POLICY: GrokBrokerModelPolicy = Object.freeze({ model: "grok-4.6", reasoningEffort: "low" }); + +export function parseGrokBrokerModelPolicy(value: unknown = {}): GrokBrokerModelPolicy { + if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError("invalid Grok broker model policy"); + const input = value as Record; + if (Object.keys(input).some((key) => key !== "model" && key !== "reasoningEffort")) throw new TypeError("invalid Grok broker model policy"); + const model = input.model ?? DEFAULT_GROK_BROKER_MODEL_POLICY.model; + const reasoningEffort = input.reasoningEffort ?? DEFAULT_GROK_BROKER_MODEL_POLICY.reasoningEffort; + if (!(GROK_BROKER_MODELS as readonly unknown[]).includes(model) || !(GROK_BROKER_REASONING_EFFORTS as readonly unknown[]).includes(reasoningEffort)) { + throw new TypeError("invalid Grok broker model policy"); + } + return Object.freeze({ model: model as GrokBrokerModel, reasoningEffort: reasoningEffort as GrokBrokerReasoningEffort }); +} diff --git a/src/runtime/grokBrokerWorkerConfig.test.ts b/src/runtime/grokBrokerWorkerConfig.test.ts index 0be5567..af63d80 100644 --- a/src/runtime/grokBrokerWorkerConfig.test.ts +++ b/src/runtime/grokBrokerWorkerConfig.test.ts @@ -1,8 +1,63 @@ import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; import test from "node:test"; -import { renderGrokBrokerWorkerArgs, renderGrokBrokerWorkerConfig } from "./grokBrokerWorkerConfig.js"; -test("worker config uses only named in-memory auth and fixed loopback proxy", () => { - const config = renderGrokBrokerWorkerConfig("/opt/daimon/bin/grok-broker-auth", 43123); - assert.match(config, /auth_provider\.daimon/u); assert.match(config, /args = \["--auth-provider"\]/u);assert.match(config, /127\.0\.0\.1:43123/u);assert.match(config,/127\.0\.0\.1:43124\/mcp/u);assert.match(config,/DAIMON_MCP_CAPABILITY/u); assert.doesNotMatch(config, /access_token|refresh_token|auth\.json/u); - const args = renderGrokBrokerWorkerArgs("/run/worker/prompt", "/workspace"); assert.equal(args.includes("--prompt-file"), true); assert.equal(args.includes("--single"), false); + +import { GROK_ENGINE_BROKER } from "../contracts/runtimeContractManifest.js"; +import { GROK_BROKER_MODELS, GROK_BROKER_REASONING_EFFORTS } from "./grokBrokerModelPolicy.js"; +import { GROK_1_0_34_BUNDLED_SKILLS, grokBrokerWorkerConfigSha256, renderGrokBrokerWorkerArgs, renderGrokBrokerWorkerConfig, renderGrokBrokerWorkerConfigWith } from "./grokBrokerWorkerConfig.js"; + +const section = (config: string, header: string): string => { + const start = config.indexOf(`${header}\n`); + assert.notEqual(start, -1, `missing ${header}`); + const end = config.indexOf("\n[", start + header.length); + return config.slice(start, end === -1 ? undefined : end); +}; + +test("worker config uses only named in-memory auth, the fixed loopback proxy, and the capability-scoped MCP facade", () => { + const config = renderGrokBrokerWorkerConfig(); + assert.match(section(config, "[auth_provider.daimon]"), /command = "\/opt\/daimon\/bin\/daimon-engine-broker"\nargs = \["--auth-provider"\]/u); + assert.match(section(config, "[model.daimon-broker-grok]"), /base_url = "http:\/\/127\.0\.0\.1:43123\/v1"\nauth_provider = "daimon"/u); + assert.equal(section(config, "[mcp_servers.daimon]"), '[mcp_servers.daimon]\nurl = "http://127.0.0.1:43124/mcp"\nbearer_token_env_var = "DAIMON_MCP_CAPABILITY"\n'); + assert.doesNotMatch(config, /access_token|refresh_token|auth\.json/u); +}); + +test("worker config disables every bundled 1.0.34 skill, workflows, and the per-turn session title request", () => { + const config = renderGrokBrokerWorkerConfig(); + assert.equal(GROK_1_0_34_BUNDLED_SKILLS.length, 25); + assert.equal(section(config, "[skills]"), `[skills]\ndisabled = [${GROK_1_0_34_BUNDLED_SKILLS.map((name) => JSON.stringify(name)).join(", ")}]\n`); + assert.equal(section(config, "[workflows]"), "[workflows]\nenabled = false\n"); + assert.match(section(config, "[models]"), /\nsession_summary = "daimon-session-title-disabled"\n/u); + assert.equal(section(config, "[model.daimon-session-title-disabled]"), '[model.daimon-session-title-disabled]\nmodel = "disabled"\nbase_url = "http://127.0.0.1:9/v1"\napi_key = "session-title-disabled"\nmax_retries = 0\nhidden = true\n'); + for (const toggle of ["title_refresh", "telemetry", "session_recap", "turn_summary", "backend_tools", "ask_user_question"]) assert.match(section(config, "[features]"), new RegExp(`\\n${toggle} = false\\n`, "u")); + assert.match(section(config, "[cli]"), /auto_update = false\nuse_leader = false/u); +}); + +test("the declared model and effort reach the worker's only model as its sole allowed effort", () => { + const config = renderGrokBrokerWorkerConfig({ model: "grok-build", reasoningEffort: "medium" }); + assert.match(section(config, "[model.daimon-broker-grok]"), /\nmodel = "grok-build"\n/u); + assert.match(section(config, "[models]"), /\ndefault = "daimon-broker-grok"\ndefault_reasoning_effort = "medium"\n/u); + assert.equal(section(config, "[[model.daimon-broker-grok.reasoning_efforts]]"), '[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = "medium"\nlabel = "Medium"\ndefault = true\n'); + assert.equal(config.match(/reasoning_efforts\]\]/gu)?.length, 1); + assert.match(renderGrokBrokerWorkerConfig(), /\nmodel = "grok-4\.6"\n[\s\S]*value = "low"/u); + for (const invalid of [{ model: "grok-3" }, { reasoningEffort: "xhigh" }, { model: "grok-4.6", reasoningEffort: "low", extra: true }]) { + assert.throws(() => renderGrokBrokerWorkerConfig(invalid as never), /model policy/u); + } +}); + +test("the manifest pins the sha256 of every renderable worker config", () => { + for (const model of GROK_BROKER_MODELS) { + for (const reasoningEffort of GROK_BROKER_REASONING_EFFORTS) { + const digest = createHash("sha256").update(renderGrokBrokerWorkerConfig({ model, reasoningEffort })).digest("hex"); + assert.equal(grokBrokerWorkerConfigSha256({ model, reasoningEffort }), digest); + assert.equal(GROK_ENGINE_BROKER.worker.configSha256[model][reasoningEffort], digest, `${model}/${reasoningEffort}`); + } + } +}); + +test("the probe-only renderer refuses non-loopback endpoints and injected helper paths", () => { + const policy = { model: "grok-4.6", reasoningEffort: "low" } as const; + assert.throws(() => renderGrokBrokerWorkerConfigWith(policy, { helperPath: "/x\"\n[evil]", proxyPort: 1, mcpUrl: "http://127.0.0.1:1/mcp" }), /invalid/u); + assert.throws(() => renderGrokBrokerWorkerConfigWith(policy, { helperPath: "/x", proxyPort: 1, mcpUrl: "http://example.com/mcp" }), /invalid/u); + const args = renderGrokBrokerWorkerArgs("/run/worker/prompt", "/workspace"); + assert.equal(args.includes("--prompt-file"), true); assert.equal(args.includes("--single"), false); }); diff --git a/src/runtime/grokBrokerWorkerConfig.ts b/src/runtime/grokBrokerWorkerConfig.ts index 4223490..da08c87 100644 --- a/src/runtime/grokBrokerWorkerConfig.ts +++ b/src/runtime/grokBrokerWorkerConfig.ts @@ -1,17 +1,118 @@ +import { createHash } from "node:crypto"; import path from "node:path"; -export function renderGrokBrokerWorkerConfig(helperPath: string, proxyPort: number): string { - if (!path.posix.isAbsolute(helperPath) || /[\r\n"']/u.test(helperPath) || !Number.isInteger(proxyPort) || proxyPort < 1 || proxyPort > 65_535) throw new TypeError("invalid Grok broker worker configuration"); +import { GROK_ENGINE_BROKER } from "../contracts/runtimeContractManifest.js"; +import { DAIMON_GROK_SYSTEM_PROMPT, GROK_WORKER_MAX_TURNS, GROK_WORKER_TOOL_IDS } from "../contracts/grokWorkerContract.js"; +import { parseGrokBrokerModelPolicy, type GrokBrokerModelPolicy } from "./grokBrokerModelPolicy.js"; + +export { DAIMON_GROK_SYSTEM_PROMPT, GROK_WORKER_MAX_TURNS, GROK_WORKER_TOOL_IDS, GROK_WORKER_VISIBLE_TOOLS } from "../contracts/grokWorkerContract.js"; + +/** + * Bundled skills shipped by Grok CLI 1.0.34. Names are version-specific: + * `[skills] disabled` removed all ~2.1k skill tokens in the P0 matrix, and a + * CLI bump must re-derive this list rather than inherit it. + */ +export const GROK_1_0_34_BUNDLED_SKILLS = Object.freeze([ + "build-with-ai", "code-review", "create-skill", "create-workflow", "design", "docx", "execute-plan", + "game-animation-frames", "game-asset-core", "game-character-consistency", "game-tilesets", "game-ui-icons", + "imagine", "implement", "learn", "long-running-background-tasks", "pdf", "pptx", "pr-babysit", + "resume-claude", "resume-codex", "resume-cursor", "review", "skill-design-principles", "statusline" +] as const); + +/** The worker's only model id; the argv selects it and the proxy never sees another. */ +export const GROK_BROKER_WORKER_MODEL_ID = "daimon-broker-grok" as const; + +/** + * Grok 1.0.34 sends a `session_title` model request before every headless + * turn, and no config key or environment variable disables it + * (`features.title_refresh` governs only the later refresh; verified against a + * loopback stub). `[models] session_summary` does select the model it uses, so + * the title goes to a hidden model whose endpoint is a closed privileged + * loopback port: the connection is refused locally, Grok falls back to the + * truncated prompt as the title, and neither the proxy nor the provider sees a + * request. The placeholder `api_key` is not a credential; it only stops Grok + * from looking for one. + */ +export const GROK_SESSION_TITLE_SINK_MODEL_ID = "daimon-session-title-disabled" as const; +const renderSessionTitleSink = (): readonly string[] => [ + `[model.${GROK_SESSION_TITLE_SINK_MODEL_ID}]`, 'model = "disabled"', 'base_url = "http://127.0.0.1:9/v1"', 'api_key = "session-title-disabled"', + "max_retries = 0", "hidden = true", "" +]; + +type WorkerEndpoints =Readonly<{ helperPath: string; proxyPort: number; mcpUrl: string }>; +const PRODUCTION_ENDPOINTS: WorkerEndpoints = Object.freeze({ + helperPath: GROK_ENGINE_BROKER.nativeExecutablePath, + proxyPort: GROK_ENGINE_BROKER.providerProxy.port, + mcpUrl: `http://${GROK_ENGINE_BROKER.mcpFacade.host}:${GROK_ENGINE_BROKER.mcpFacade.port}${GROK_ENGINE_BROKER.mcpFacade.path}` +}); + +/** + * Sections shared by every Grok worker Daimon configures, broker or direct. + * + * Each toggle is either measured (skills −2.1k tokens, workflows −314, the + * per-turn `session_title` model request) or behavioural hardening that P0 + * showed to be token-neutral and warning-free on 1.0.34. + */ +export const renderGrokLeanBaseConfig = (): string => [ + "[cli]", "auto_update = false", "use_leader = false", "show_tips = false", "", + "[features]", "telemetry = false", "title_refresh = false", "session_recap = false", "turn_summary = false", + "repo_status_in_system_prompt = false", "codebase_indexing = false", "backend_tools = false", "ask_user_question = false", + "image_gen = false", "video_gen = false", "web_fetch = false", "campaigns = false", "managed_config = false", "", + "[managed_mcps]", "enabled = false", "", + "[skills]", `disabled = [${GROK_1_0_34_BUNDLED_SKILLS.map((name) => JSON.stringify(name)).join(", ")}]`, "", + "[workflows]", "enabled = false", "" +].join("\n"); + +/** + * The only source of broker worker `config.toml` bytes. + * + * Effort is declared here, not on the compiled launcher argv: the argv is one + * constant for every registration while effort is declared per deployment, and + * Grok 1.0.34 drops both `--reasoning-effort` and `[models] + * default_reasoning_effort` unless the model advertises effort support. A + * one-entry `reasoning_efforts` table makes the declared effort the model's + * default *and* its closed enum, so it reaches the request body and nothing + * else can be selected; the proxy then re-verifies it on every body. + */ +export function renderGrokBrokerWorkerConfig(policy: Partial = {}): string { + return renderGrokBrokerWorkerConfigWith(parseGrokBrokerModelPolicy(policy), PRODUCTION_ENDPOINTS); +} + +/** Explicit-endpoint variant for the local live probe; production bytes come only from the function above. */ +export function renderGrokBrokerWorkerConfigWith(policy: GrokBrokerModelPolicy, endpoints: WorkerEndpoints): string { + const declared = parseGrokBrokerModelPolicy(policy); + const { helperPath, proxyPort, mcpUrl } = endpoints; + if (!path.posix.isAbsolute(helperPath) || /[\r\n"'\\]/u.test(helperPath) || !Number.isInteger(proxyPort) || proxyPort < 1 || proxyPort > 65_535 || !/^http:\/\/127\.0\.0\.1:\d{1,5}\/mcp$/u.test(mcpUrl)) { + throw new TypeError("invalid Grok broker worker configuration"); + } + const label = `${declared.reasoningEffort[0]!.toUpperCase()}${declared.reasoningEffort.slice(1)}`; return [ - "[cli]", "auto_update = false", "use_leader = false", "", - "[features]", "telemetry = false", "", + renderGrokLeanBaseConfig(), + "[models]", `default = "${GROK_BROKER_WORKER_MODEL_ID}"`, `default_reasoning_effort = "${declared.reasoningEffort}"`, `session_summary = "${GROK_SESSION_TITLE_SINK_MODEL_ID}"`, "", + ...renderSessionTitleSink(), "[auth_provider.daimon]", `command = ${JSON.stringify(helperPath)}`, 'args = ["--auth-provider"]', "timeout_secs = 5", "token_ttl_secs = 600", "", - "[model.daimon-broker-grok]", 'model = "grok-build"', `base_url = "http://127.0.0.1:${proxyPort}/v1"`, 'auth_provider = "daimon"', "context_window = 131072", "supports_backend_search = false", "", - "[mcp_servers.daimon]", 'url = "http://127.0.0.1:43124/mcp"', 'headers = { Authorization = "Bearer ${DAIMON_MCP_CAPABILITY}" }', "" + `[model.${GROK_BROKER_WORKER_MODEL_ID}]`, `model = "${declared.model}"`, `base_url = "http://127.0.0.1:${proxyPort}/v1"`, 'auth_provider = "daimon"', + 'api_backend = "chat_completions"', "context_window = 131072", "supports_backend_search = false", "", + `[[model.${GROK_BROKER_WORKER_MODEL_ID}.reasoning_efforts]]`, `value = "${declared.reasoningEffort}"`, `label = "${label}"`, "default = true", "", + "[mcp_servers.daimon]", `url = "${mcpUrl}"`, 'bearer_token_env_var = "DAIMON_MCP_CAPABILITY"', "" ].join("\n"); } +export const grokBrokerWorkerConfigSha256 = (policy: Partial = {}): string => + createHash("sha256").update(renderGrokBrokerWorkerConfig(policy)).digest("hex"); + +/** + * TypeScript mirror of the argv compiled into `native/engineBrokerLauncherCore.inc`. + * `grokWorkerArgv.test.ts` parses the C source and fails on any divergence. + */ export const renderGrokBrokerWorkerArgs = (promptFile: string, cwd: string): readonly string[] => { if (!path.posix.isAbsolute(promptFile) || !path.posix.isAbsolute(cwd)) throw new TypeError("invalid Grok broker worker path"); - return ["--sandbox", "daimon-strict", "--always-approve", "--no-subagents", "--prompt-file", promptFile, "--no-memory", "--disable-web-search", "--cwd", cwd, "--output-format", "streaming-messages-json", "--model", "daimon-broker-grok"]; + return [ + "--sandbox", "daimon-strict", "--always-approve", "--no-subagents", "--prompt-file", promptFile, + "--no-memory", "--disable-web-search", "--no-plan", "--verbatim", + "--system-prompt-override", DAIMON_GROK_SYSTEM_PROMPT, + "--tools", GROK_WORKER_TOOL_IDS.join(","), + "--max-turns", String(GROK_WORKER_MAX_TURNS), + "--cwd", cwd, "--output-format", "streaming-messages-json", "--model", GROK_BROKER_WORKER_MODEL_ID + ]; }; From 18b9d4897b6591309b12fbe87b803bc6c72f1a5f Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 04:46:18 +0200 Subject: [PATCH 02/26] feat: refuse Grok broker requests outside the lean tool set and declared model policy --- src/runtime/grokBrokerProxy.test.ts | 27 ++++++++-- src/runtime/grokBrokerProxy.ts | 12 +++-- src/runtime/grokBrokerProxyRequest.test.ts | 57 +++++++++++++++------- src/runtime/grokBrokerProxyRequest.ts | 40 ++++++++++++--- 4 files changed, 103 insertions(+), 33 deletions(-) diff --git a/src/runtime/grokBrokerProxy.test.ts b/src/runtime/grokBrokerProxy.test.ts index 5f2a362..0d09eb8 100644 --- a/src/runtime/grokBrokerProxy.test.ts +++ b/src/runtime/grokBrokerProxy.test.ts @@ -2,6 +2,9 @@ import assert from "node:assert/strict"; import test from "node:test"; import { startGrokBrokerProxy } from "./grokBrokerProxy.js"; +const lean = ["run_terminal_command", "read_file", "list_dir", "grep", "search_tool", "use_tool"].map((name) => ({ type: "function", function: { name } })); +const leanBody = (overrides: Record = {}): string => JSON.stringify({ model: "grok-4.6", reasoning_effort: "low", stream: true, messages: [], tools: lean, ...overrides }); + test("proxy retries one 401 with refreshed broker bearer and shuts down", async () => { const calls: string[] = []; let refreshes = 0; const proxy = await startGrokBrokerProxy({ accessToken: async (force) => force ? "second" : "first", markRejected: async () => { refreshes += 1; } }, async (request) => { @@ -9,19 +12,35 @@ test("proxy retries one 401 with refreshed broker bearer and shuts down", async }); const token = proxy.capabilities.issue("agent", "turn"); proxy.registerIsolationGuard("turn", async () => undefined); - const result = await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json", "x-grok-client-version": "1.0.25" }, body: JSON.stringify({ stream: true, messages: [] }) }); + const result = await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json", "x-grok-client-version": "1.0.34" }, body: leanBody() }); assert.equal(result.status, 200); assert.equal(await result.text(), "data: done\n\n"); assert.deepEqual(calls, ["Bearer first", "Bearer second"]); assert.equal(refreshes, 0); await proxy.close(); await assert.rejects(fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`)); }); -test("proxy stale-fences a refreshed credential rejected by upstream",async()=>{let rejected=0;const proxy=await startGrokBrokerProxy({accessToken:async(force)=>force?"second":"first",markRejected:async()=>{rejected++;throw new Error("stale");}},async()=>({status:401,headers:{"content-type":"application/json"},body:new Uint8Array()}));const token=proxy.capabilities.issue("agent","turn");proxy.registerIsolationGuard("turn",async()=>undefined);const response=await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`,{method:"POST",headers:{authorization:`Bearer ${token}`,"x-grok-client-version":"1.0.25"},body:JSON.stringify({stream:true,messages:[]})});assert.equal(response.status,503);assert.equal(rejected,1);await proxy.close();}); +test("proxy stale-fences a refreshed credential rejected by upstream",async()=>{let rejected=0;const proxy=await startGrokBrokerProxy({accessToken:async(force)=>force?"second":"first",markRejected:async()=>{rejected++;throw new Error("stale");}},async()=>({status:401,headers:{"content-type":"application/json"},body:new Uint8Array()}));const token=proxy.capabilities.issue("agent","turn");proxy.registerIsolationGuard("turn",async()=>undefined);const response=await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`,{method:"POST",headers:{authorization:`Bearer ${token}`,"x-grok-client-version":"1.0.34"},body:leanBody()});assert.equal(response.status,503);assert.equal(rejected,1);await proxy.close();}); test("proxy failures expose only a fixed diagnostic", async () => { const proxy = await startGrokBrokerProxy({ accessToken: async () => { throw new Error("secret-token"); }, markRejected: async () => undefined }, async () => { throw new Error("unreachable"); }); const token = proxy.capabilities.issue("agent", "turn"); proxy.registerIsolationGuard("turn", async () => undefined); - const result = await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${token}`, "x-grok-client-version": "1.0.25" }, body: JSON.stringify({ stream: true, messages: [] }) }); + const result = await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${token}`, "x-grok-client-version": "1.0.34" }, body: leanBody() }); assert.equal(result.status, 503); const body = await result.text(); assert.equal(body, '{"error":"broker unavailable"}'); assert.doesNotMatch(body, /secret/u); await proxy.close(); }); -test("one turn capability supports multiple guarded cognition requests",async()=>{let guarded=0,calls=0;const proxy=await startGrokBrokerProxy({accessToken:async()=>"provider-token",markRejected:async()=>undefined},async()=>{calls++;return{status:200,headers:{"content-type":"application/json"},body:Buffer.from("{}")};});try{const token=proxy.capabilities.issue("agent","turn");proxy.registerIsolationGuard("turn",async()=>{guarded++;});for(let index=0;index<2;index++){const response=await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`,{method:"POST",headers:{authorization:`Bearer ${token}`,"x-grok-client-version":"1.0.25"},body:JSON.stringify({stream:true,messages:[]})});assert.equal(response.status,200);}assert.equal(calls,2);assert.equal(guarded,2);}finally{await proxy.close();}}); +test("one turn capability supports multiple guarded cognition requests",async()=>{let guarded=0,calls=0;const proxy=await startGrokBrokerProxy({accessToken:async()=>"provider-token",markRejected:async()=>undefined},async()=>{calls++;return{status:200,headers:{"content-type":"application/json"},body:Buffer.from("{}")};});try{const token=proxy.capabilities.issue("agent","turn");proxy.registerIsolationGuard("turn",async()=>{guarded++;});for(let index=0;index<2;index++){const response=await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`,{method:"POST",headers:{authorization:`Bearer ${token}`,"x-grok-client-version":"1.0.34"},body:leanBody()});assert.equal(response.status,200);}assert.equal(calls,2);assert.equal(guarded,2);}finally{await proxy.close();}}); + +test("proxy refuses a fail-open tool set or an undeclared effort without calling upstream", async () => { + let calls = 0; let accessed = 0; + const proxy = await startGrokBrokerProxy({ accessToken: async () => { accessed++; return "provider-token"; }, markRejected: async () => undefined }, async () => { calls++; return { status: 200, headers: { "content-type": "application/json" }, body: Buffer.from("{}") }; }, { model: "grok-4.6", reasoningEffort: "low" }); + try { + const token = proxy.capabilities.issue("agent", "turn"); proxy.registerIsolationGuard("turn", async () => undefined); + const full = [...lean, ...["search_replace", "todo_write", "write", "monitor"].map((name) => ({ type: "function", function: { name } }))]; + for (const payload of [leanBody({ tools: full }), leanBody({ tools: [{ type: "function", function: { name: "session_title" } }] }), leanBody({ reasoning_effort: "high" }), leanBody({ reasoning_effort: undefined })]) { + const response = await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${token}`, "x-grok-client-version": "1.0.34" }, body: payload }); + assert.equal(response.status, 503); await response.text(); + } + assert.equal(calls, 0); + const accepted = await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${token}`, "x-grok-client-version": "1.0.34" }, body: leanBody() }); + assert.equal(accepted.status, 200); await accepted.text(); assert.equal(calls, 1); assert.ok(accessed >= 1); + } finally { await proxy.close(); } +}); diff --git a/src/runtime/grokBrokerProxy.ts b/src/runtime/grokBrokerProxy.ts index 4dee112..023a474 100644 --- a/src/runtime/grokBrokerProxy.ts +++ b/src/runtime/grokBrokerProxy.ts @@ -2,24 +2,26 @@ import { createHash } from "node:crypto"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import type { AddressInfo } from "node:net"; import { EngineBrokerCapabilities } from "./engineBrokerCapabilities.js"; +import { DEFAULT_GROK_BROKER_MODEL_POLICY, parseGrokBrokerModelPolicy, type GrokBrokerModelPolicy } from "./grokBrokerModelPolicy.js"; import { authorizeGrokBrokerProxyRequest } from "./grokBrokerProxyRequest.js"; export type GrokBrokerCredentialAuthority = Readonly<{ accessToken(forceRefresh: boolean): Promise; refreshAfterRejection?(rejectedTokenDigest:string):Promise; markRejected(rejectedTokenDigest?:string): Promise }>; export type GrokBrokerUpstream = (request: ReturnType) => Promise>; body: Uint8Array }>>; -export async function startGrokBrokerProxy(authority: GrokBrokerCredentialAuthority, upstream: GrokBrokerUpstream = defaultUpstream): PromisePromise):void; revokeIsolationGuard(turnId:string):void; close(): Promise }>> { - const capabilities = new EngineBrokerCapabilities(); - const guards=new MapPromise>();const server = createServer((request, response) => { void serve(request, response, authority, upstream, capabilities,guards); }); +/** `policy` is the declared model/effort every forwarded body must carry (closed list; defaults grok-4.6/low). */ +export async function startGrokBrokerProxy(authority: GrokBrokerCredentialAuthority, upstream: GrokBrokerUpstream = defaultUpstream, policy: GrokBrokerModelPolicy = DEFAULT_GROK_BROKER_MODEL_POLICY): PromisePromise):void; revokeIsolationGuard(turnId:string):void; close(): Promise }>> { + const declared = parseGrokBrokerModelPolicy(policy); const capabilities = new EngineBrokerCapabilities(); + const guards=new MapPromise>();const server = createServer((request, response) => { void serve(request, response, authority, upstream, capabilities,guards,declared); }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(43_123, "127.0.0.1", () => { server.off("error", reject); resolve(); }); }); const address = server.address() as AddressInfo; return { port: address.port, capabilities,registerIsolationGuard(turnId,guard){guards.set(turnId,guard);},revokeIsolationGuard(turnId){guards.delete(turnId);}, close: () => new Promise((resolve, reject) => server.close((error) => error === undefined ? resolve() : reject(error))) }; } -async function serve(request: IncomingMessage, response: ServerResponse, authority: GrokBrokerCredentialAuthority, upstream: GrokBrokerUpstream, capabilities: EngineBrokerCapabilities,guards:MapPromise>): Promise { +async function serve(request: IncomingMessage, response: ServerResponse, authority: GrokBrokerCredentialAuthority, upstream: GrokBrokerUpstream, capabilities: EngineBrokerCapabilities,guards:MapPromise>,policy:GrokBrokerModelPolicy): Promise { try { const body = await readBody(request); const headers = Object.fromEntries(Object.entries(request.headers).map(([key, value]) => [key, Array.isArray(value) ? value[0] : value])); const match=headers.authorization?.match(/^Bearer ([A-Za-z0-9_-]{40,})$/u),scope=match?capabilities.inspectToken(match[1]!):undefined;if(!scope)throw new Error();const guard=guards.get(scope.turnId);if(!guard)throw new Error();await guard(); - let token = await authority.accessToken(false);const rejectedDigest=createHash("sha256").update(token).digest("hex"); let prepared = authorizeGrokBrokerProxyRequest({ method: request.method ?? "", pathname: new URL(request.url ?? "/", "http://127.0.0.1").pathname, headers, body }, capabilities, token); token = ""; + let token = await authority.accessToken(false);const rejectedDigest=createHash("sha256").update(token).digest("hex"); let prepared = authorizeGrokBrokerProxyRequest({ method: request.method ?? "", pathname: new URL(request.url ?? "/", "http://127.0.0.1").pathname, headers, body }, capabilities, token, policy); token = ""; let result = await upstream(prepared); if (result.status === 401) { token = authority.refreshAfterRejection?await authority.refreshAfterRejection(rejectedDigest):await authority.accessToken(true);const refreshedDigest=createHash("sha256").update(token).digest("hex"); prepared = { ...prepared, headers: { ...prepared.headers, authorization: `Bearer ${token}` } }; token = ""; result = await upstream(prepared);if(result.status===401)await authority.markRejected(refreshedDigest); } response.writeHead(result.status, { "content-type": result.headers["content-type"] ?? "application/json", "cache-control": "no-store" }); response.end(result.body); diff --git a/src/runtime/grokBrokerProxyRequest.test.ts b/src/runtime/grokBrokerProxyRequest.test.ts index 462c140..fb4752c 100644 --- a/src/runtime/grokBrokerProxyRequest.test.ts +++ b/src/runtime/grokBrokerProxyRequest.test.ts @@ -3,28 +3,49 @@ import test from "node:test"; import { EngineBrokerCapabilities } from "./engineBrokerCapabilities.js"; import { authorizeGrokBrokerProxyRequest } from "./grokBrokerProxyRequest.js"; -const body = Buffer.from(JSON.stringify({ stream: true, messages: [] })); - -test("proxy substitutes broker bearer, forwards bounded Grok client version, and rejects arbitrary routes and headers", () => { +const leanTools = ["run_terminal_command", "read_file", "list_dir", "grep", "search_tool", "use_tool"]; +const tool = (name: string) => ({ type: "function", function: { name, parameters: { type: "object" } } }); +const leanBody = (overrides: Record = {}): Buffer => Buffer.from(JSON.stringify({ model: "grok-4.6", reasoning_effort: "low", stream: true, messages: [], tools: leanTools.map(tool), ...overrides })); +const request = (body: Uint8Array, headers: Record = {}) => { const caps = new EngineBrokerCapabilities(); const opaque = caps.issue("a", "t"); - const request = authorizeGrokBrokerProxyRequest({ method: "POST", pathname: "/v1/chat/completions", headers: { authorization: `Bearer ${opaque}`, cookie: "forbidden", "x-grok-client-version": "1.0.25", "x-grok-client-identifier": "attacker" }, body, agentId: "a", turnId: "t" }, caps, "real-bearer"); - assert.equal(request.url, "https://cli-chat-proxy.grok.com/v1/chat/completions"); - assert.equal(request.headers.authorization, "Bearer real-bearer"); - assert.equal(request.headers["x-grok-client-version"], "1.0.25"); - assert.equal(request.headers["x-grok-client-identifier"], "grok-shell"); - assert.equal("cookie" in request.headers, false); - assert.throws(() => authorizeGrokBrokerProxyRequest({ method: "GET", pathname: "/", headers: { authorization: `Bearer ${opaque}`, "x-grok-client-version": "1.0.25" }, body, agentId: "a", turnId: "t" }, caps, "real-bearer"), /rejected/); + return { caps, input: { method: "POST", pathname: "/v1/chat/completions", headers: { authorization: `Bearer ${opaque}`, "x-grok-client-version": "1.0.34", ...headers }, body, agentId: "a", turnId: "t" } }; +}; + +test("proxy substitutes broker bearer, forwards the pinned client version, and rejects arbitrary routes and headers", () => { + const { caps, input } = request(leanBody(), { cookie: "forbidden", "x-grok-client-identifier": "attacker", "x-grok-model-override": "grok-build" }); + const upstream = authorizeGrokBrokerProxyRequest(input, caps, "real-bearer"); + assert.equal(upstream.url, "https://cli-chat-proxy.grok.com/v1/chat/completions"); + assert.equal(upstream.headers.authorization, "Bearer real-bearer"); + assert.equal(upstream.headers["x-grok-client-version"], "1.0.34"); + assert.equal(upstream.headers["x-grok-client-identifier"], "grok-shell"); + assert.equal(upstream.headers["x-grok-model-override"], "grok-4.6"); + assert.equal("cookie" in upstream.headers, false); + assert.throws(() => authorizeGrokBrokerProxyRequest({ ...input, method: "GET", pathname: "/" }, caps, "real-bearer"), /rejected/); }); -test("proxy fails closed when the worker omits or malforms the Grok client version", () => { - for (const version of [undefined, "", "1", "1.0", "v1.0.25", "1.0.25\nInjected: yes", "1.0.25+build", `1.2.3-${"a".repeat(65)}`] as const) { - const caps = new EngineBrokerCapabilities(); const opaque = caps.issue("a", "t"); - assert.throws(() => authorizeGrokBrokerProxyRequest({ method: "POST", pathname: "/v1/chat/completions", headers: { authorization: `Bearer ${opaque}`, ...(version === undefined ? {} : { "x-grok-client-version": version }) }, body, agentId: "a", turnId: "t" }, caps, "real-bearer"), /rejected/); +test("proxy fails closed on any client version other than the pinned Grok CLI", () => { + for (const version of [undefined, "", "1", "1.0", "v1.0.34", "1.0.13", "1.0.25", "1.0.33", "1.0.35", "1.0.34-beta.1", "1.0.34\nInjected: yes", "1.0.34+build"] as const) { + const { caps, input } = request(leanBody(), { "x-grok-client-version": version }); + assert.throws(() => authorizeGrokBrokerProxyRequest(input, caps, "real-bearer"), /rejected/, String(version)); } }); -test("proxy accepts prerelease Grok client versions", () => { - const caps = new EngineBrokerCapabilities(); const opaque = caps.issue("a", "t"); - const request = authorizeGrokBrokerProxyRequest({ method: "POST", pathname: "/v1/chat/completions", headers: { authorization: `Bearer ${opaque}`, "x-grok-client-version": "1.0.25-beta.1" }, body, agentId: "a", turnId: "t" }, caps, "real-bearer"); - assert.equal(request.headers["x-grok-client-version"], "1.0.25-beta.1"); +test("proxy refuses a request carrying Grok's fail-open full tool set before any upstream call", () => { + const full = [...leanTools, "search_replace", "kill_command_or_subagent", "todo_write", "get_command_or_subagent_output", "spawn_subagent", "scheduler_create", "scheduler_delete", "scheduler_list", "monitor", "workflow", "enter_plan_mode", "exit_plan_mode", "write"]; + for (const tools of [full.map(tool), [tool("session_title")], leanTools.slice(1).map(tool), [...leanTools, "use_tool"].map(tool), [...leanTools.slice(1), "run_terminal_cmd"].map(tool), undefined, [], leanTools.map((name) => ({ type: "custom", function: { name } }))]) { + const { caps, input } = request(leanBody({ tools })); + assert.throws(() => authorizeGrokBrokerProxyRequest(input, caps, "real-bearer"), /rejected/); + } +}); + +test("proxy refuses a reasoning effort or model other than the declared policy", () => { + for (const overrides of [{ reasoning_effort: "high" }, { reasoning_effort: undefined }, { reasoning_effort: "xhigh" }, { model: "grok-build" }, { model: "daimon-broker-grok" }]) { + const { caps, input } = request(leanBody(overrides)); + assert.throws(() => authorizeGrokBrokerProxyRequest(input, caps, "real-bearer"), /rejected/, JSON.stringify(overrides)); + } + const { caps, input } = request(leanBody({ model: "grok-build", reasoning_effort: "medium" })); + const upstream = authorizeGrokBrokerProxyRequest(input, caps, "real-bearer", { model: "grok-build", reasoningEffort: "medium" }); + assert.equal(upstream.headers["x-grok-model-override"], "grok-build"); + const other = request(leanBody()); + assert.throws(() => authorizeGrokBrokerProxyRequest(other.input, other.caps, "real-bearer", { model: "grok-3" as never, reasoningEffort: "low" }), /model policy/); }); diff --git a/src/runtime/grokBrokerProxyRequest.ts b/src/runtime/grokBrokerProxyRequest.ts index 15f79d4..309ad7e 100644 --- a/src/runtime/grokBrokerProxyRequest.ts +++ b/src/runtime/grokBrokerProxyRequest.ts @@ -1,12 +1,26 @@ +import { GROK_ENGINE_BROKER } from "../contracts/runtimeContractManifest.js"; +import { GROK_WORKER_VISIBLE_TOOLS } from "../contracts/grokWorkerContract.js"; import type { EngineBrokerCapabilities } from "./engineBrokerCapabilities.js"; +import { DEFAULT_GROK_BROKER_MODEL_POLICY, parseGrokBrokerModelPolicy, type GrokBrokerModelPolicy } from "./grokBrokerModelPolicy.js"; const MAX_BODY = 2 * 1024 * 1024; -const MAX_CLIENT_VERSION = 64; -const CLIENT_VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/u; export type GrokBrokerProxyInput = Readonly<{ method: string; pathname: string; headers: Readonly>; body: Uint8Array; agentId?: string; turnId?: string }>; export type GrokBrokerUpstreamRequest = Readonly<{ url: "https://cli-chat-proxy.grok.com/v1/chat/completions"; headers: Readonly>; body: Uint8Array }>; -export function authorizeGrokBrokerProxyRequest(input: GrokBrokerProxyInput, capabilities: EngineBrokerCapabilities, bearer: string): GrokBrokerUpstreamRequest { +/** + * Authorizes one worker request and rebuilds it for the provider. + * + * Everything that decides spend is checked here, before a bearer is attached + * and before any upstream call: + * - the client version is exactly the pinned Grok CLI (`GROK_ENGINE_BROKER.grokCliVersion`); + * - the body model and `reasoning_effort` are exactly the declared policy, and + * the model override header follows that declaration instead of a constant; + * - the offered tool names are exactly the lean visible set. Grok 1.0.34 turns + * an unmappable `--tools` entry into its full 19-tool set, and its per-turn + * `session_title` request carries a single forced tool; both are refused. + */ +export function authorizeGrokBrokerProxyRequest(input: GrokBrokerProxyInput, capabilities: EngineBrokerCapabilities, bearer: string, policy: GrokBrokerModelPolicy = DEFAULT_GROK_BROKER_MODEL_POLICY): GrokBrokerUpstreamRequest { + const declared = parseGrokBrokerModelPolicy(policy); if (input.method !== "POST" || input.pathname !== "/v1/chat/completions" || input.body.byteLength < 2 || input.body.byteLength > MAX_BODY) throw new Error("broker proxy request rejected"); const authorization = input.headers.authorization; const match = authorization?.match(/^Bearer ([A-Za-z0-9_-]{40,})$/u); if (match === null || match === undefined) throw new Error("broker proxy request rejected"); @@ -14,7 +28,21 @@ export function authorizeGrokBrokerProxyRequest(input: GrokBrokerProxyInput, cap if (scope === undefined || (input.agentId !== undefined && scope.agentId !== input.agentId) || (input.turnId !== undefined && scope.turnId !== input.turnId)) throw new Error("broker proxy request rejected"); if (!bearer || /[\r\n]/u.test(bearer)) throw new Error("broker credential authority unavailable"); const clientVersion = input.headers["x-grok-client-version"]; - if (clientVersion === undefined || clientVersion.length > MAX_CLIENT_VERSION || !CLIENT_VERSION.test(clientVersion)) throw new Error("broker proxy request rejected"); - try { const parsed = JSON.parse(Buffer.from(input.body).toString("utf8")) as Record; if (parsed.stream !== true || !Array.isArray(parsed.messages)) throw new Error(); } catch { throw new Error("broker proxy request rejected"); } - return { url: "https://cli-chat-proxy.grok.com/v1/chat/completions", headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json", "x-xai-token-auth": "xai-grok-cli", "x-grok-model-override": "grok-build", "x-grok-client-version": clientVersion, "x-grok-client-identifier": "grok-shell" }, body: input.body }; + if (clientVersion !== GROK_ENGINE_BROKER.grokCliVersion) throw new Error("broker proxy request rejected"); + let parsed: Record; + try { parsed = JSON.parse(Buffer.from(input.body).toString("utf8")) as Record; } catch { throw new Error("broker proxy request rejected"); } + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || parsed.stream !== true || !Array.isArray(parsed.messages)) throw new Error("broker proxy request rejected"); + if (parsed.model !== declared.model || parsed.reasoning_effort !== declared.reasoningEffort) throw new Error("broker proxy request rejected"); + if (!exactLeanTools(parsed.tools)) throw new Error("broker proxy request rejected"); + return { url: "https://cli-chat-proxy.grok.com/v1/chat/completions", headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json", "x-xai-token-auth": "xai-grok-cli", "x-grok-model-override": declared.model, "x-grok-client-version": clientVersion, "x-grok-client-identifier": "grok-shell" }, body: input.body }; +} + +export function exactLeanTools(tools: unknown): boolean { + if (!Array.isArray(tools) || tools.length !== GROK_WORKER_VISIBLE_TOOLS.length) return false; + const names = tools.map((tool) => { + if (tool === null || typeof tool !== "object" || (tool as { type?: unknown }).type !== "function") return undefined; + const fn = (tool as { function?: unknown }).function; + return fn !== null && typeof fn === "object" ? (fn as { name?: unknown }).name : undefined; + }); + return JSON.stringify([...names].sort()) === JSON.stringify(GROK_WORKER_VISIBLE_TOOLS); } From 8dbd8c04a9d7ec055a166f446dcea3234e2745f9 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 04:46:18 +0200 Subject: [PATCH 03/26] feat: attest Grok 1.0.34 sandbox events, deny lists and read-only worker home --- src/pi/grokSandbox.test.ts | 7 +- src/pi/grokSandbox.ts | 17 ++--- src/runtime/engineBrokerServiceCli.test.ts | 5 +- src/runtime/engineBrokerServiceCli.ts | 3 +- .../fixtures/grok-1.0.34-sandbox-events.jsonl | 2 + src/runtime/grokEngineBroker.ts | 12 ++-- src/runtime/grokWorkerAttestation.test.ts | 32 ++++++++- src/runtime/grokWorkerAttestation.ts | 37 +++++++---- src/runtime/grokWorkerHomeAttestation.test.ts | 62 ++++++++++++++++++ src/runtime/grokWorkerHomeAttestation.ts | 61 +++++++++++++++++ src/runtime/grokWorkerSandboxProfile.test.ts | 26 ++++++++ src/runtime/grokWorkerSandboxProfile.ts | Bin 0 -> 2154 bytes 12 files changed, 230 insertions(+), 34 deletions(-) create mode 100644 src/runtime/fixtures/grok-1.0.34-sandbox-events.jsonl create mode 100644 src/runtime/grokWorkerHomeAttestation.test.ts create mode 100644 src/runtime/grokWorkerHomeAttestation.ts create mode 100644 src/runtime/grokWorkerSandboxProfile.test.ts create mode 100644 src/runtime/grokWorkerSandboxProfile.ts diff --git a/src/pi/grokSandbox.test.ts b/src/pi/grokSandbox.test.ts index d8bd5d0..b34f194 100644 --- a/src/pi/grokSandbox.test.ts +++ b/src/pi/grokSandbox.test.ts @@ -45,7 +45,8 @@ test("rejects a protected root overlapping the selected agent workspace", async test("rotates its private enforcement receipt before the bounded log is exhausted", async () => { const fixture = await createFixture(); try { - const events = path.join(fixture.engineHomePath, "sandbox-events.jsonl"); + const events = path.join(fixture.engineHomePath, "sessions", "sandbox-events.jsonl"); + await mkdir(path.dirname(events), { mode: 0o700 }); await writeFile(events, "x".repeat(8 * 1024 * 1024), { mode: 0o600 }); await prepareAndVerifyGrokSandbox(fixture.authority); assert.ok((await readFile(events)).byteLength < 64 * 1024); @@ -75,8 +76,8 @@ const profile=fs.readFileSync(path.join(home,"sandbox.toml"),"utf8"); const deny=JSON.parse(profile.split("\\n").find((line)=>line.startsWith("deny = ")).slice(7)); const observed=${JSON.stringify(mode)}==="drop-deny"?deny.slice(1):deny; const event={event_type:"ProfileApplied",profile:"${GROK_DAIMON_SANDBOX_PROFILE}",workspace:fs.realpathSync(args[args.indexOf("--cwd")+1]),platform:"linux/landlock",enforced:${JSON.stringify(mode)}!=="unenforced",restrict_network:true,deny_paths:observed}; -fs.appendFileSync(path.join(home,"sandbox-events.jsonl"),JSON.stringify(event)+"\\n",{mode:0o600}); -fs.chmodSync(path.join(home,"sandbox-events.jsonl"),0o600); +fs.appendFileSync(path.join(home,"sessions","sandbox-events.jsonl"),JSON.stringify(event)+"\\n",{mode:0o600}); +fs.chmodSync(path.join(home,"sessions","sandbox-events.jsonl"),0o600); `); await chmod(command, 0o700); return { diff --git a/src/pi/grokSandbox.ts b/src/pi/grokSandbox.ts index 956095b..ddc9ee4 100644 --- a/src/pi/grokSandbox.ts +++ b/src/pi/grokSandbox.ts @@ -1,17 +1,19 @@ import { randomUUID } from "node:crypto"; import { spawn } from "node:child_process"; import { constants } from "node:fs"; -import { lstat, open, realpath, rename, unlink } from "node:fs/promises"; +import { lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises"; import path from "node:path"; import { readChild } from "./cliChildOutput.js"; import { cliChildEnvironment } from "./cliEnvironment.js"; import { terminateChild, trackCliChild } from "./cliProcess.js"; import { renderGrokSandboxArgs } from "./cliEngineSpawn.js"; +import { GROK_WORKER_SANDBOX_EVENTS_RELATIVE_PATH, GROK_WORKER_SANDBOX_PROFILE, renderGrokWorkerSandboxProfile } from "../runtime/grokWorkerSandboxProfile.js"; -export const GROK_DAIMON_SANDBOX_PROFILE = "daimon-strict"; +export const GROK_DAIMON_SANDBOX_PROFILE = GROK_WORKER_SANDBOX_PROFILE; const SANDBOX_CONFIG = "sandbox.toml"; -const SANDBOX_EVENTS = "sandbox-events.jsonl"; +/** Grok 1.0.34 logs sandbox events under `sessions/`; the root file stays empty. */ +const SANDBOX_EVENTS = GROK_WORKER_SANDBOX_EVENTS_RELATIVE_PATH; const MAX_EVENTS_BYTES = 16 * 1024 * 1024; const ROTATE_EVENTS_BYTES = 8 * 1024 * 1024; @@ -61,13 +63,7 @@ export async function prepareAndVerifyGrokSandbox( await verifyProfile(engineHome, denied); } -const profileText = (denied: readonly string[]): string => [ - `[profiles.${GROK_DAIMON_SANDBOX_PROFILE}]`, - 'extends = "strict"', - "restrict_network = true", - `deny = [${denied.map((entry) => JSON.stringify(entry)).join(", ")}]`, - "" -].join("\n"); +const profileText = (denied: readonly string[]): string => renderGrokWorkerSandboxProfile(denied); async function writeProfile(engineHome: string, denied: readonly string[]): Promise { const target = path.join(engineHome, SANDBOX_CONFIG); @@ -155,6 +151,7 @@ async function eventFileSize(file: string): Promise { return Number(entry.size); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { + await mkdir(path.dirname(file), { mode: 0o700, recursive: true }); const handle = await open(file, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | noFollow(), 0o600); try { await handle.sync(); } finally { await handle.close(); } await syncDirectory(path.dirname(file)); diff --git a/src/runtime/engineBrokerServiceCli.test.ts b/src/runtime/engineBrokerServiceCli.test.ts index 7e5ff97..5d11cf6 100644 --- a/src/runtime/engineBrokerServiceCli.test.ts +++ b/src/runtime/engineBrokerServiceCli.test.ts @@ -11,5 +11,8 @@ test("rejects caller-selected commands, duplicate identities, and traversal",()= assert.throws(()=>parseEngineBrokerServiceConfig({...base,grokCommand:"evil"})); assert.throws(()=>parseEngineBrokerServiceConfig({...base,turnStore:"/var/lib/../secret"})); assert.throws(()=>parseEngineBrokerServiceConfig({...base,registrations:[reg("agent-a",0),reg("agent-a",1)]})); + // Grok 1.0.34 logs sandbox events under $GROK_HOME/sessions/; the 1.0.13 root path stays empty and must not be attested. + assert.throws(()=>parseEngineBrokerServiceConfig({...base,registrations:[{...reg("agent-a",0),eventsPath:"/workers/0/.grok/sandbox-events.jsonl"}]})); + assert.throws(()=>parseEngineBrokerServiceConfig({...base,registrations:[{...reg("agent-a",0),eventsPath:"/workers/1/.grok/sessions/sandbox-events.jsonl"}]})); }); -const reg=(agentId:string,slot:number)=>({agentId,slot,workerUid:2200+slot,workspace:`/workspace/${slot}`,profilePath:`/workers/${slot}/.grok/sandbox.toml`,eventsPath:`/workers/${slot}/.grok/sandbox-events.jsonl`,profileSha256:"a".repeat(64)}); +const reg=(agentId:string,slot:number)=>({agentId,slot,workerUid:2200+slot,workspace:`/workspace/${slot}`,profilePath:`/workers/${slot}/.grok/sandbox.toml`,eventsPath:`/workers/${slot}/.grok/sessions/sandbox-events.jsonl`,profileSha256:"a".repeat(64)}); diff --git a/src/runtime/engineBrokerServiceCli.ts b/src/runtime/engineBrokerServiceCli.ts index bbc89cd..d70839c 100644 --- a/src/runtime/engineBrokerServiceCli.ts +++ b/src/runtime/engineBrokerServiceCli.ts @@ -2,6 +2,7 @@ import { constants } from "node:fs"; import { open } from "node:fs/promises"; import { startEngineBrokerService } from "./engineBrokerService.js"; import { startGrokEngineBroker, type GrokEngineBrokerRegistration } from "./grokEngineBroker.js"; +import { grokWorkerEventsPathFor } from "./grokWorkerSandboxProfile.js"; export const ENGINE_BROKER_SERVICE_CONFIG = "/etc/daimon-engine-broker/service.json"; const MAX_CONFIG_BYTES=65_536; @@ -19,7 +20,7 @@ export function parseEngineBrokerServiceConfig(value:unknown):Readonly<{credenti if(value===null||typeof value!=="object"||Array.isArray(value))throw new TypeError("invalid engine broker service config");const input=value as Record; if(Object.keys(input).length!==4||input.version!=="noopolis.daimon.engine-broker-service.v1"||typeof input.credentialHome!=="string"||typeof input.turnStore!=="string"||!Array.isArray(input.registrations))throw new TypeError("invalid engine broker service config"); const absolute=(item:string)=>item.startsWith("/")&&!item.includes("/../")&&!item.endsWith("/..");if(!absolute(input.credentialHome)||!absolute(input.turnStore))throw new TypeError("invalid engine broker service config"); - const seen=new Set();const registrations=input.registrations.map((entry)=>{if(entry===null||typeof entry!=="object"||Array.isArray(entry))throw new TypeError("invalid engine broker service config");const item=entry as Record;if(Object.keys(item).length!==7||typeof item.agentId!=="string"||!item.agentId.trim()||!Number.isSafeInteger(item.slot)||(item.slot as number)<0||!Number.isSafeInteger(item.workerUid)||(item.workerUid as number)<2200||typeof item.workspace!=="string"||!absolute(item.workspace)||typeof item.profilePath!=="string"||!absolute(item.profilePath)||typeof item.eventsPath!=="string"||!absolute(item.eventsPath)||typeof item.profileSha256!=="string"||!/^[a-f0-9]{64}$/u.test(item.profileSha256)||item.profilePath!==`${item.eventsPath.replace(/\/sandbox-events\.jsonl$/u,"")}/sandbox.toml`||seen.has(item.agentId))throw new TypeError("invalid engine broker service config");seen.add(item.agentId);return {agentId:item.agentId,slot:item.slot as number,workerUid:item.workerUid as number,workspace:item.workspace,profilePath:item.profilePath,eventsPath:item.eventsPath,profileSha256:item.profileSha256};}); + const seen=new Set();const registrations=input.registrations.map((entry)=>{if(entry===null||typeof entry!=="object"||Array.isArray(entry))throw new TypeError("invalid engine broker service config");const item=entry as Record;if(Object.keys(item).length!==7||typeof item.agentId!=="string"||!item.agentId.trim()||!Number.isSafeInteger(item.slot)||(item.slot as number)<0||!Number.isSafeInteger(item.workerUid)||(item.workerUid as number)<2200||typeof item.workspace!=="string"||!absolute(item.workspace)||typeof item.profilePath!=="string"||!absolute(item.profilePath)||typeof item.eventsPath!=="string"||!absolute(item.eventsPath)||typeof item.profileSha256!=="string"||!/^[a-f0-9]{64}$/u.test(item.profileSha256)||item.eventsPath!==grokWorkerEventsPathFor(item.profilePath)||!item.profilePath.endsWith("/sandbox.toml")||seen.has(item.agentId))throw new TypeError("invalid engine broker service config");seen.add(item.agentId);return {agentId:item.agentId,slot:item.slot as number,workerUid:item.workerUid as number,workspace:item.workspace,profilePath:item.profilePath,eventsPath:item.eventsPath,profileSha256:item.profileSha256};}); if(registrations.length===0)throw new TypeError("invalid engine broker service config");return {credentialHome:input.credentialHome,turnStore:input.turnStore,registrations}; } diff --git a/src/runtime/fixtures/grok-1.0.34-sandbox-events.jsonl b/src/runtime/fixtures/grok-1.0.34-sandbox-events.jsonl new file mode 100644 index 0000000..a728cef --- /dev/null +++ b/src/runtime/fixtures/grok-1.0.34-sandbox-events.jsonl @@ -0,0 +1,2 @@ +{"timestamp":"2026-09-17T02:33:59.302698678Z","event_type":"ProfileApplied","profile":"daimon-strict","workspace":"/var/lib/spawnfile/instance/workspace/agents/a1","platform":"linux/landlock","enforced":true,"restrict_network":true,"read_write_paths":["/var/lib/spawnfile/instance/workspace/agents/a1","/var/lib/daimon-workers/2200/.grok/sessions","/tmp","/var/tmp"],"read_only_paths":["/usr","/lib","/bin","/sbin","/etc","/dev","/proc","/sys","/tmp","/run","/var","/var/lib/spawnfile/instance/workspace/agents/a1","/var/lib/daimon-workers/2200/.grok"],"deny_paths":["/run/paideia"]} +{"timestamp":"2026-09-17T02:33:59.913077091Z","event_type":"FsViolation","profile":"daimon-strict","operation":"read","target":"/run/paideia/context.json"} diff --git a/src/runtime/grokEngineBroker.ts b/src/runtime/grokEngineBroker.ts index 6ae6eaf..bfcc3ec 100644 --- a/src/runtime/grokEngineBroker.ts +++ b/src/runtime/grokEngineBroker.ts @@ -7,6 +7,8 @@ import { EngineBrokerTurnRegistry } from "./engineBrokerTurnRegistry.js"; import { startGrokBrokerProxy } from "./grokBrokerProxy.js"; import { startEngineBrokerMcpFacade } from "./engineBrokerMcpFacade.js"; import { acquireGrokBrokerRealmLease } from "./grokBrokerRealmLease.js"; +import { grokBrokerWorkerConfigSha256 } from "./grokBrokerWorkerConfig.js"; +import { parseGrokBrokerModelPolicy, type GrokBrokerModelPolicy } from "./grokBrokerModelPolicy.js"; import { GrokWorkerAttestationFailure,prepareGrokWorkerAttestation,verifyGrokWorkerAttestation } from "./grokWorkerAttestation.js"; export type GrokEngineBrokerRegistration = Readonly<{ agentId:string;slot:number;workerUid:number;workspace:string;profilePath:string;eventsPath:string;profileSha256:string }>; @@ -32,18 +34,20 @@ export async function finishBrokerTurnWithUsage(turns: EngineBrokerTurnRegistry, if (usage === undefined) return; await recordTurnUsage(usageLedgerPath, { agent: agentId, wake: wakeId, engine: "grok", usage }); } -export async function startGrokEngineBroker(options: Readonly<{ grokCommand: string; nativeClient: string; credentialHome: string; turnStore: string; registrations: readonly GrokEngineBrokerRegistration[]; usageLedgerPath?: string }>) { +export async function startGrokEngineBroker(options: Readonly<{ grokCommand: string; nativeClient: string; credentialHome: string; turnStore: string; registrations: readonly GrokEngineBrokerRegistration[]; usageLedgerPath?: string; modelPolicy?: GrokBrokerModelPolicy }>) { + // One declared model/effort drives both the worker config bytes the broker attests and the bodies the proxy forwards. + const modelPolicy = parseGrokBrokerModelPolicy(options.modelPolicy ?? {}); const configSha256 = grokBrokerWorkerConfigSha256(modelPolicy); const usageLedgerPath = options.usageLedgerPath ?? TURN_USAGE_LEDGER.filePath; const registrations = new Map(options.registrations.map((entry) => [entry.agentId, entry])); if (registrations.size !== options.registrations.length) throw new Error("engine broker registration conflict"); - const lease=await acquireGrokBrokerRealmLease(options.credentialHome);const authority = new DurableGrokBrokerCredentialAuthority(options.grokCommand, options.credentialHome);try{await authority.initialize();}catch(error){await lease.close();throw error;} let proxy:Awaited>;try{proxy=await startGrokBrokerProxy(authority);}catch(error){await lease.close();throw error;}let mcp:Awaited>|undefined;try{mcp=await startEngineBrokerMcpFacade();for(const registration of registrations.values())await prepareGrokWorkerAttestation({...registration,brokerGid:2100});}catch(error){if(mcp)await mcp.close().catch(()=>undefined);await proxy.close();await lease.close();throw error;}if(!mcp)throw new Error("engine broker unavailable");const turns = new EngineBrokerTurnRegistry(options.turnStore); const active = new Map; resolve: () => void }>(); let closed = false; + const lease=await acquireGrokBrokerRealmLease(options.credentialHome);const authority = new DurableGrokBrokerCredentialAuthority(options.grokCommand, options.credentialHome);try{await authority.initialize();}catch(error){await lease.close();throw error;} let proxy:Awaited>;try{proxy=await startGrokBrokerProxy(authority,undefined,modelPolicy);}catch(error){await lease.close();throw error;}let mcp:Awaited>|undefined;try{mcp=await startEngineBrokerMcpFacade();for(const registration of registrations.values())await prepareGrokWorkerAttestation({...registration,brokerGid:2100,configSha256});}catch(error){if(mcp)await mcp.close().catch(()=>undefined);await proxy.close();await lease.close();throw error;}if(!mcp)throw new Error("engine broker unavailable");const turns = new EngineBrokerTurnRegistry(options.turnStore); const active = new Map; resolve: () => void }>(); let closed = false; return { async turn(agentId: string, wakeId: string, prompt: string, mcpEndpoint: string, signal?: AbortSignal): Promise> { if (closed) throw new Error("engine broker unavailable"); const registration = registrations.get(agentId); if (registration === undefined) throw new Error("engine broker unavailable"); const turnId = createHash("sha256").update(`${agentId}\0${wakeId}`).digest("hex"); const request = { version: "noopolis.daimon.engine-broker.v1", kind: "start_turn", requestId: randomUUID(), turnId, agentId, wakeId, prompt,mcpEndpoint } as const; const begun = await turns.begin(request); if (begun !== "start") { if (begun.replay.kind === "completed") return {text:begun.replay.text,workerPid:begun.replay.workerPid,workerUid:begun.replay.workerUid,workerStartTime:begun.replay.workerStartTime};const code=begun.replay.code==="auth_stale"||begun.replay.code==="cancelled"?begun.replay.code:"engine_failed";throw new EngineBrokerTurnFailure(code,begun.replay.diagnostic as NativeBrokerDiagnostic|undefined); } - const isolation=await prepareGrokWorkerAttestation({...registration,brokerGid:2100});proxy.registerIsolationGuard(turnId,()=>verifyGrokWorkerAttestation({...registration,brokerGid:2100},isolation));const providerCapability = proxy.capabilities.issue(agentId, turnId);const mcpCapability=mcp.register(agentId,turnId,mcpEndpoint); const controller = new AbortController();const onAbort=()=>controller.abort();signal?.addEventListener("abort",onAbort,{once:true});if(signal?.aborted)controller.abort(); let resolve!:()=>void;const done=new Promise((value)=>{resolve=value;});active.set(turnId,{controller,done,resolve}); + const attestation={...registration,brokerGid:2100,configSha256};const isolation=await prepareGrokWorkerAttestation(attestation);proxy.registerIsolationGuard(turnId,()=>verifyGrokWorkerAttestation(attestation,isolation));const providerCapability = proxy.capabilities.issue(agentId, turnId);const mcpCapability=mcp.register(agentId,turnId,mcpEndpoint); const controller = new AbortController();const onAbort=()=>controller.abort();signal?.addEventListener("abort",onAbort,{once:true});if(signal?.aborted)controller.abort(); let resolve!:()=>void;const done=new Promise((value)=>{resolve=value;});active.set(turnId,{controller,done,resolve}); let nativeDiagnostic:NativeBrokerDiagnostic|undefined,attested=false; - try { const result = await runNativeBrokerTurn(options.nativeClient, { slot: registration.slot, requestId: request.requestId, turnId, agentId, wakeId, prompt, providerCapability,mcpCapability }, controller.signal);nativeDiagnostic=result.diagnostic;if(result.workerUid!==registration.workerUid)throw new Error();await verifyGrokWorkerAttestation({...registration,brokerGid:2100},isolation);attested=true; const decoded = decodeGrokHeadlessTurn(result.text);const text = decoded.text;const completed={ version: request.version, kind: "completed", requestId: request.requestId, turnId, text, workerPid: result.workerPid, workerUid: result.workerUid, workerStartTime: result.startTicks.toString() } as const; await finishBrokerTurnWithUsage(turns,request,completed,usageLedgerPath,decoded.usage,agentId,wakeId); return {text,workerPid:result.workerPid,workerUid:result.workerUid,workerStartTime:result.startTicks.toString()}; } + try { const result = await runNativeBrokerTurn(options.nativeClient, { slot: registration.slot, requestId: request.requestId, turnId, agentId, wakeId, prompt, providerCapability,mcpCapability }, controller.signal);nativeDiagnostic=result.diagnostic;if(result.workerUid!==registration.workerUid)throw new Error();await verifyGrokWorkerAttestation(attestation,isolation);attested=true; const decoded = decodeGrokHeadlessTurn(result.text);const text = decoded.text;const completed={ version: request.version, kind: "completed", requestId: request.requestId, turnId, text, workerPid: result.workerPid, workerUid: result.workerUid, workerStartTime: result.startTicks.toString() } as const; await finishBrokerTurnWithUsage(turns,request,completed,usageLedgerPath,decoded.usage,agentId,wakeId); return {text,workerPid:result.workerPid,workerUid:result.workerUid,workerStartTime:result.startTicks.toString()}; } catch(error) { const code=authority.isStale()?"auth_stale":controller.signal.aborted?"cancelled":"engine_failed";const diagnostic=error instanceof NativeBrokerTurnFailure?error.diagnostic:nativeDiagnostic&&!attested?{...nativeDiagnostic,status:"worker_failed" as const,stage:"attestation" as const,failureClass:error instanceof GrokWorkerAttestationFailure?error.failureClass:"profile_invalid" as const,profileApplied:false}:undefined;await turns.finish(request, { version: request.version, kind: "failed", requestId: request.requestId, turnId, code,...(diagnostic?{diagnostic}:{}) }); throw new EngineBrokerTurnFailure(code,diagnostic); } finally { signal?.removeEventListener("abort",onAbort);active.get(turnId)?.resolve(); active.delete(turnId); proxy.revokeIsolationGuard(turnId);proxy.capabilities.revoke(turnId);mcp.revoke(turnId); } }, diff --git a/src/runtime/grokWorkerAttestation.test.ts b/src/runtime/grokWorkerAttestation.test.ts index 7dc230d..7c1b30a 100644 --- a/src/runtime/grokWorkerAttestation.test.ts +++ b/src/runtime/grokWorkerAttestation.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { appendFile, chmod, mkdtemp, rm, stat, symlink, truncate, writeFile } from "node:fs/promises"; +import { appendFile, chmod, mkdir, mkdtemp, readFile, rm, stat, symlink, truncate, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; @@ -220,8 +220,9 @@ test("refuses a sandbox profile that is not root-owned, and one reached through const text = `[profiles.daimon-strict]\nextends = "strict"\nrestrict_network = true\ndeny = []\n`; await writeFile(profile, text); await chmod(profile, 0o444); - const events = await eventsFile(dir); - const input = { profilePath: profile, eventsPath: events, profileSha256: sha256(text), workerUid: self.uid, brokerGid: self.gid }; + await mkdir(path.join(dir, "sessions")); + const events = await eventsFile(path.join(dir, "sessions")); + const input = { profilePath: profile, eventsPath: events, profileSha256: sha256(text), workerUid: self.uid, brokerGid: self.gid, configSha256: "0".repeat(64) }; // Owned by the test user rather than root: `secureOpen` must refuse it even // though its bytes hash correctly. await assert.rejects(prepareGrokWorkerAttestation(input), /attestation unavailable/u); @@ -244,3 +245,28 @@ test("holds the kernel's reported deny_paths to exactly what the pinned profile } assert.throws(() => parseGrokWorkerProfileApplied(bytes(["/a"]), workspace, []), /attestation unavailable/u); }); + +test("reads sandbox events only from the Grok 1.0.34 sessions log", async (t) => { + // Mutation-critical: 1.0.34 writes nothing to `$GROK_HOME/sandbox-events.jsonl`, + // so attesting that path would fail every turn as "not enforced" — or worse, + // accept a stale file. Restoring the old relation must turn this red. + const dir = await workspaceDir(); + t.after(() => rm(dir, { recursive: true, force: true })); + const input = { profilePath: path.join(dir, "sandbox.toml"), profileSha256: "0".repeat(64), workerUid: self.uid, brokerGid: self.gid, configSha256: "0".repeat(64) }; + await assert.rejects(prepareGrokWorkerAttestation({ ...input, eventsPath: path.join(dir, "sandbox-events.jsonl") }), /sessions\/sandbox-events\.jsonl/u); + await assert.rejects(prepareGrokWorkerAttestation({ ...input, eventsPath: path.join(dir, "sessions", "sandbox-events.jsonl") }), /attestation unavailable/u); +}); + +test("attests real Grok 1.0.34 events: ProfileApplied with a non-empty deny list, then the denial it caused", async (t) => { + const fixture = await readFile(new URL("./fixtures/grok-1.0.34-sandbox-events.jsonl", import.meta.url)); + const liveWorkspace = "/var/lib/spawnfile/instance/workspace/agents/a1"; + assert.doesNotThrow(() => parseGrokWorkerProfileApplied(fixture, liveWorkspace, ["/run/paideia"])); + assert.throws(() => parseGrokWorkerProfileApplied(fixture, liveWorkspace, []), /attestation unavailable/u); + assert.throws(() => parseGrokWorkerProfileApplied(fixture, liveWorkspace, ["/run/paideia", "/run/training"]), /attestation unavailable/u); + const dir = await workspaceDir(); + t.after(() => rm(dir, { recursive: true, force: true })); + const file = await eventsFile(dir); + const before = await watermark(file, ["/run/paideia"]); + await appendFile(file, fixture); + await verifyGrokWorkerAttestation({ eventsPath: file, workerUid: self.uid, brokerGid: self.gid, workspace: liveWorkspace }, before); +}); diff --git a/src/runtime/grokWorkerAttestation.ts b/src/runtime/grokWorkerAttestation.ts index ce450f3..70eae1b 100644 --- a/src/runtime/grokWorkerAttestation.ts +++ b/src/runtime/grokWorkerAttestation.ts @@ -1,6 +1,10 @@ import { createHash } from "node:crypto"; import { constants } from "node:fs"; import { lstat,open } from "node:fs/promises"; +import path from "node:path"; + +import { verifyGrokWorkerHome } from "./grokWorkerHomeAttestation.js"; +import { grokWorkerEventsPathFor } from "./grokWorkerSandboxProfile.js"; /** * The per-turn freshness watermark taken before the worker is launched: the @@ -15,15 +19,13 @@ export class GrokWorkerAttestationFailure extends Error { constructor(readonly f * Reads the `deny` list out of a worker sandbox profile, but only after the * bytes match `profileSha256` exactly. * - * There is no minimum length. The floor used to be 3 (subscription realm, - * bootstrap credential, peer roots), then 1, and an empty list is now the - * *expected* shape: Grok 1.0.13 re-execs itself inside bubblewrap whenever - * `deny` is non-empty and then opens each deny-path placeholder — which it - * created at mode 000 — from a capability-stripped process, gets EACCES, and - * refuses to start ("possible __GROK_INSIDE_BWRAP spoof") without ever - * emitting a `ProfileApplied` event. Spawnfile therefore renders `deny = []` - * and confines the worker with unix permissions plus builtin-`strict` - * Landlock instead (`containerDaimonBrokerRender.ts`). + * There is no minimum length, and a populated list is supported. Grok 1.0.13 + * refused to start on any non-empty `deny` (its mode-000 placeholders failed + * an EACCES "__GROK_INSIDE_BWRAP spoof" check), so deployments rendered + * `deny = []`. Grok 1.0.34 runs every profile inside bubblewrap and enforces a + * non-empty list, and since its strict base reads all of `/run`, `/var` and + * `/tmp`, the deny list is what keeps evaluator and host-bind paths away from + * the worker. `grokWorkerSandboxProfile.ts` renders these bytes. * * The integrity guarantee is the hash pin, not the length. `profileSha256` * comes from `/etc/daimon-engine-broker/service.json`, which the root @@ -52,8 +54,17 @@ export function parseGrokWorkerSandboxProfile(bytes:Uint8Array,profileSha256:str if(!Array.isArray(parsed)||parsed.some((entry)=>typeof entry!=="string")||new Set(parsed).size!==parsed.length)throw new Error("Grok worker isolation attestation unavailable"); return [...parsed as string[]].sort(); } -export async function prepareGrokWorkerAttestation(input:Readonly<{profilePath:string;eventsPath:string;profileSha256:string;workerUid:number;brokerGid:number}>):Promise{ +/** + * Pre-launch half of the per-turn attestation. Besides the pinned profile and + * the events watermark it requires the 1.0.34 layout: events under + * `$GROK_HOME/sessions/` (the root `sandbox-events.jsonl` stays empty on + * 1.0.34), and a root-owned read-only worker home whose `config.toml` hashes to + * the declared renderer output (`grokWorkerHomeAttestation.ts`). + */ +export async function prepareGrokWorkerAttestation(input:Readonly<{profilePath:string;eventsPath:string;profileSha256:string;workerUid:number;brokerGid:number;configSha256:string}>):Promise{ + if(input.eventsPath!==grokWorkerEventsPathFor(input.profilePath))throw new Error("Grok worker sandbox events must be read from $GROK_HOME/sessions/sandbox-events.jsonl"); const profile=await secureOpen(input.profilePath,0,0,0o444,65_536);let bytes:Buffer|undefined;let denyPaths:readonly string[]=[];try{bytes=await profile.readFile();denyPaths=parseGrokWorkerSandboxProfile(bytes,input.profileSha256);}catch{throw new Error("Grok worker isolation attestation unavailable");}finally{bytes?.fill(0);await profile.close();} + await verifyGrokWorkerHome(path.dirname(input.profilePath),input.configSha256); const events=await secureOpen(input.eventsPath,input.workerUid,input.brokerGid,0o640,16*1024*1024);try{const stat=await events.stat();return{dev:Number(stat.dev),ino:Number(stat.ino),size:Number(stat.size),mtimeMs:Number(stat.mtimeMs),denyPaths};}finally{await events.close();} } export async function verifyGrokWorkerAttestation(input:Readonly<{eventsPath:string;workerUid:number;brokerGid:number;workspace:string}>,before:Snapshot):Promise{ @@ -63,14 +74,16 @@ export async function verifyGrokWorkerAttestation(input:Readonly<{eventsPath:str * Requires one fully-conforming `ProfileApplied` event *somewhere* in the * fresh region — not as its last line. * - * `sandbox-events.jsonl` is not a profile log. Grok 1.0.13 writes its whole + * `sandbox-events.jsonl` is not a profile log. Grok (1.0.13 and 1.0.34) writes its whole * sandbox event vocabulary there — verified by reading the shipped binary: * `ProfileApplied, ApplyFailed, FsViolation, NetViolation, BypassGranted, * BypassDenied` (one contiguous enum blob beside the record fields * `timestamp, event_type, read_only_paths, deny_paths, operation, target, * command, tool_call_id`, emitted from `xai_grok_sandbox::logging`), and its * own embedded documentation says so outright: "Sandbox events (profile - * applied, violations) are logged to `~/.grok/sandbox-events.jsonl`". + * applied, violations) are logged to `~/.grok/sandbox-events.jsonl`" (1.0.34 + * moved the file to `~/.grok/sessions/`; a 1.0.34 turn logs `ProfileApplied` + * followed by an `FsViolation` for every denied read). * * Requiring `ProfileApplied` to be the *last* line therefore failed on the * first denied access of any turn: the violation Grok logged next became the diff --git a/src/runtime/grokWorkerHomeAttestation.test.ts b/src/runtime/grokWorkerHomeAttestation.test.ts new file mode 100644 index 0000000..1c2eceb --- /dev/null +++ b/src/runtime/grokWorkerHomeAttestation.test.ts @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { grokBrokerWorkerConfigSha256, renderGrokBrokerWorkerConfig } from "./grokBrokerWorkerConfig.js"; +import { assertGrokWorkerConfigBytes, assertGrokWorkerHomeEntries, verifyGrokWorkerHome } from "./grokWorkerHomeAttestation.js"; + +type Kind = "dir" | "file" | "link"; +const entry = (kind: Kind, mode: number, uid = 0, nlink = 1) => ({ + uid, mode: (kind === "dir" ? 0o040000 : kind === "file" ? 0o100000 : 0o120000) | mode, nlink, + isFile: () => kind === "file", isDirectory: () => kind === "dir", isSymbolicLink: () => kind === "link" +}); +const files = ["config.toml", "managed_config.toml", "requirements.toml", "sandbox.toml", "trusted_folders.toml"]; +const layout = (overrides: Record | undefined> = {}) => ({ + ".": entry("dir", 0o1771), sessions: entry("dir", 0o1771), ...Object.fromEntries(files.map((name) => [name, entry("file", 0o444)])), ...overrides +}); + +test("accepts the root-owned sticky worker home with root-owned read-only config and trust files", () => { + assert.doesNotThrow(() => assertGrokWorkerHomeEntries(layout())); + assert.doesNotThrow(() => assertGrokWorkerHomeEntries(layout({ ".": entry("dir", 0o711), sessions: entry("dir", 0o750) }))); +}); + +test("refuses a worker home where the worker could change its config or trust state", () => { + const refusals: Record> = { + "worker-owned home": layout({ ".": entry("dir", 0o1771, 2200) }), + "group-writable home without sticky bit": layout({ ".": entry("dir", 0o771) }), + "world-writable home": layout({ ".": entry("dir", 0o1777) }), + "worker-owned sessions": layout({ sessions: entry("dir", 0o700, 2200) }), + "group-writable sessions without sticky bit": layout({ sessions: entry("dir", 0o770) }) + }; + for (const name of files) { + refusals[`${name} missing`] = layout({ [name]: undefined }); + refusals[`${name} worker-owned`] = layout({ [name]: entry("file", 0o444, 2200) }); + refusals[`${name} owner-writable`] = layout({ [name]: entry("file", 0o644) }); + refusals[`${name} group-writable`] = layout({ [name]: entry("file", 0o464) }); + refusals[`${name} symlink`] = layout({ [name]: entry("link", 0o444) }); + refusals[`${name} hard-linked`] = layout({ [name]: entry("file", 0o444, 0, 2) }); + } + for (const [label, entries] of Object.entries(refusals)) assert.throws(() => assertGrokWorkerHomeEntries(entries), /attestation unavailable/u, label); +}); + +test("refuses a real home that is not root-owned even when every file exists", async (t) => { + const home = await mkdtemp(path.join(os.tmpdir(), "daimon-grok-home-")); + t.after(() => rm(home, { recursive: true, force: true })); + await mkdir(path.join(home, "sessions")); + for (const name of files) await writeFile(path.join(home, name), "", { mode: 0o444 }); + await assert.rejects(verifyGrokWorkerHome(home, "0".repeat(64)), /attestation unavailable/u); +}); + +test("accepts only the renderer's exact config bytes for the declared model policy", () => { + const declared = { model: "grok-4.6", reasoningEffort: "low" } as const; + const bytes = Buffer.from(renderGrokBrokerWorkerConfig(declared)); + assert.doesNotThrow(() => assertGrokWorkerConfigBytes(bytes, grokBrokerWorkerConfigSha256(declared))); + for (const tampered of [ + renderGrokBrokerWorkerConfig({ model: "grok-4.6", reasoningEffort: "high" }), + renderGrokBrokerWorkerConfig(declared).replace(/\[skills\]\ndisabled = \[[^\]]*\]\n/u, ""), + renderGrokBrokerWorkerConfig(declared).replace('session_summary = "daimon-session-title-disabled"', 'session_summary = "grok-4.6"'), + `${renderGrokBrokerWorkerConfig(declared)}\n[mcp_servers.extra]\nurl = "http://127.0.0.1:1/mcp"\n` + ]) assert.throws(() => assertGrokWorkerConfigBytes(Buffer.from(tampered), grokBrokerWorkerConfigSha256(declared)), /attestation unavailable/u); +}); diff --git a/src/runtime/grokWorkerHomeAttestation.ts b/src/runtime/grokWorkerHomeAttestation.ts new file mode 100644 index 0000000..f029757 --- /dev/null +++ b/src/runtime/grokWorkerHomeAttestation.ts @@ -0,0 +1,61 @@ +import { createHash } from "node:crypto"; +import { constants, type Stats } from "node:fs"; +import { lstat, open } from "node:fs/promises"; +import path from "node:path"; + +import { GROK_ENGINE_BROKER } from "../contracts/runtimeContractManifest.js"; + +type Entry = Pick & Readonly<{ isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean }>; +const HOME = GROK_ENGINE_BROKER.worker.home; + +/** + * The worker must not be able to change what Grok reads before its next turn. + * + * Grok writes its own state (sessions, hooks, locks, docs) into `$GROK_HOME`, + * so the directory is worker-group writable — but root-owned and sticky, so a + * worker can neither rename nor unlink a root-owned file in it. Every file + * that decides a turn's behaviour is root-owned with no write bit: `config.toml` + * (model, effort, MCP, skills), `sandbox.toml`, `trusted_folders.toml` (trust + * would re-enable cwd `AGENTS.md` and project skills), and the managed and + * requirements layers that can override user config. A missing file fails + * too: the worker could create it. + * + * Pure so every refusal is testable without root. + */ +export function assertGrokWorkerHomeEntries(entries: Readonly>): void { + const directory = (entry: Entry | undefined): boolean => + entry !== undefined && entry.isDirectory() && !entry.isSymbolicLink() && entry.uid === HOME.directory.uid + && (Number(entry.mode) & 0o002) === 0 && ((Number(entry.mode) & 0o020) === 0 || (Number(entry.mode) & 0o1000) !== 0); + if (!directory(entries["."]) || !directory(entries[HOME.sessionsDirectory.relativePath])) throw unavailable(); + for (const name of HOME.readOnlyFiles.names) { + const entry = entries[name]; + if (entry === undefined || !entry.isFile() || entry.isSymbolicLink() || entry.uid !== HOME.readOnlyFiles.uid || entry.nlink !== 1 || (Number(entry.mode) & 0o7222) !== 0) throw unavailable(); + } +} + +/** The worker's `config.toml` must be exactly the renderer's bytes for the declared policy. */ +export function assertGrokWorkerConfigBytes(bytes: Uint8Array, configSha256: string): void { + if (!/^[0-9a-f]{64}$/u.test(configSha256) || createHash("sha256").update(bytes).digest("hex") !== configSha256) throw unavailable(); +} + +/** + * Attests the worker home layout and that `config.toml` is exactly the + * renderer's bytes for the declared model policy (`configSha256`). + */ +export async function verifyGrokWorkerHome(grokHome: string, configSha256: string): Promise { + const entries: Record = {}; + for (const name of [".", HOME.sessionsDirectory.relativePath, ...HOME.readOnlyFiles.names]) { + try { entries[name] = await lstat(path.join(grokHome, name)); } catch { entries[name] = undefined; } + } + assertGrokWorkerHomeEntries(entries); + let handle: Awaited> | undefined; + try { + handle = await open(path.join(grokHome, "config.toml"), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + const opened = await handle.stat(); + const before = entries["config.toml"]!; + if (!opened.isFile() || opened.size > 65_536 || opened.uid !== before.uid || opened.mode !== before.mode || opened.nlink !== 1) throw unavailable(); + assertGrokWorkerConfigBytes(await handle.readFile(), configSha256); + } catch { throw unavailable(); } finally { await handle?.close().catch(() => undefined); } +} + +const unavailable = (): Error => new Error("Grok worker isolation attestation unavailable"); diff --git a/src/runtime/grokWorkerSandboxProfile.test.ts b/src/runtime/grokWorkerSandboxProfile.test.ts new file mode 100644 index 0000000..7c24264 --- /dev/null +++ b/src/runtime/grokWorkerSandboxProfile.test.ts @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import test from "node:test"; + +import { parseGrokWorkerSandboxProfile } from "./grokWorkerAttestation.js"; +import { grokWorkerEventsPathFor, grokWorkerSandboxProfileSha256, renderGrokWorkerSandboxProfile } from "./grokWorkerSandboxProfile.js"; + +test("renders a non-empty deny list deterministically and round-trips through the attestation parser", () => { + const profile = renderGrokWorkerSandboxProfile(["/run/training/inputs", "/run/paideia", "/run/paideia"]); + assert.equal(profile, '[profiles.daimon-strict]\nextends = "strict"\nrestrict_network = true\ndeny = ["/run/paideia", "/run/training/inputs"]\n'); + assert.equal(renderGrokWorkerSandboxProfile(["/run/paideia", "/run/training/inputs"]), profile); + const digest = createHash("sha256").update(profile).digest("hex"); + assert.equal(grokWorkerSandboxProfileSha256(["/run/training/inputs", "/run/paideia"]), digest); + assert.deepEqual(parseGrokWorkerSandboxProfile(Buffer.from(profile), digest), ["/run/paideia", "/run/training/inputs"]); + assert.equal(renderGrokWorkerSandboxProfile(), '[profiles.daimon-strict]\nextends = "strict"\nrestrict_network = true\ndeny = []\n'); +}); + +test("refuses deny paths that are relative, non-canonical, root, globbed, or TOML-breaking", () => { + for (const entry of ["run/paideia", "/run/../etc", "/run/paideia/", "/", "/run/*", '/run/"x', "/run/a\nb", "/run/a\\b", ""]) { + assert.throws(() => renderGrokWorkerSandboxProfile([entry]), /deny path/u, JSON.stringify(entry)); + } +}); + +test("derives the Grok 1.0.34 sandbox events log from the profile location", () => { + assert.equal(grokWorkerEventsPathFor("/var/lib/daimon-workers/2200/.grok/sandbox.toml"), "/var/lib/daimon-workers/2200/.grok/sessions/sandbox-events.jsonl"); +}); diff --git a/src/runtime/grokWorkerSandboxProfile.ts b/src/runtime/grokWorkerSandboxProfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..423fe217979515e49fe5b61d832766f700d5b48b GIT binary patch literal 2154 zcma)7;cnYT4Bqd@Ffq`;QDb=!{w?vSBw(>0BQ*k}eghIN+c=oyF1l!(_nju zJ>i~YT>vIb~U_wGq{|MPR?KaH2h_DaXEbX`fLDwhzcQ>T6L{6 zGItRO3*PPc%OHbygY)rdb~!jZ8NYrvm|dKVPZ`mowUR>jtWd>V-*xc=DreL8R;y|^ z!d~z2a1Rb)Tq2NZ4OTZMr{@J^e@iQa77A*k7qUW_uN~TyoZ0`C(YNqT`ZRs^9hgR0 zpnzr#XB58D`R&$8Dk}@}ZRYa|S4Pwwpd;W)Gzfx`6fuFz+N~)8RO<{XNer&ExwS`t zA}@g|A!un~E>+Q83VVwMEVO~Srd|*f*b=LX!JO8W4S^3I7C$f&c$j7!KMzIb{B zS&uUA)q)a}A_IylvFLCQoY9p9shr4N!liniLZ4tREJbp%s(=wK2zp}P7%uD3{|90G zzgyOs&m!xg%juKVoF?OOiJ`(jfB%DOcU#I1JfL3r^4UlQuS((I7O`f2Mb!vLRWpC3 zwyw)`FeXcooL6EUgvpFBe`r#;DkU)(PBHrbQ#~3UZCS(VBDGaq74UC6x&-jej_ewWaK`D_S(9{N9htUl#8jA(j z*xO9(qEWeLGUU(RSH1%y3QzNR5oRdn_Fr(jjtE8+VFHOGbu*dL98adn5lM{S;eHRo zT4)8z<}^(eu3&`j5h*=)Dq%4*ea+4ZRXN+C}mpES6I50+=IFgE_FN;k}Sl8R#$LwWPso+)%ZWOT%t2BTok zd_vTz{rfSNTWIwa(qK;8ZGF7CRVsaBw|x%R88p0gAlu${S-LZWLH3WF4Ez Date: Thu, 17 Sep 2026 04:46:18 +0200 Subject: [PATCH 04/26] fix: register direct Grok MCP endpoints in the Daimon-owned GROK_HOME config --- src/pi/cliMcpRegistration.test.ts | 11 +---- src/pi/cliMcpRegistration.ts | 17 +++---- src/pi/cliSession.test.ts | 6 +-- src/pi/cliSession.ts | 32 ++++++------- src/pi/cliSessionOutput.test.ts | 6 +-- src/pi/cliSessionProcess.test.ts | 23 +++++----- src/pi/cliSessionRemoval.test.ts | 74 ++++++++++++------------------- src/pi/grokHomeMcpRegistration.ts | 58 ++++++++++++++++++++++++ 8 files changed, 124 insertions(+), 103 deletions(-) create mode 100644 src/pi/grokHomeMcpRegistration.ts diff --git a/src/pi/cliMcpRegistration.test.ts b/src/pi/cliMcpRegistration.test.ts index f725b3d..efa1776 100644 --- a/src/pi/cliMcpRegistration.test.ts +++ b/src/pi/cliMcpRegistration.test.ts @@ -5,18 +5,9 @@ import { renderAgyArgs } from "./cliEngineSpawn.js"; import { DAIMON_MCP_SERVER_NAME, renderAgyMcpAddArgs, - renderAgyMcpRemoveArgs, - renderGrokMcpAddArgs, - renderGrokMcpRemoveArgs + renderAgyMcpRemoveArgs } from "./cliMcpRegistration.js"; -test("Grok's registration arguments are unchanged by the AGY generalization", () => { - assert.deepEqual(renderGrokMcpAddArgs([], "strict", "http://127.0.0.1:1/mcp"), - ["--sandbox", "strict", "mcp", "add", "--transport", "http", "--scope", "project", "daimon", "http://127.0.0.1:1/mcp"]); - assert.deepEqual(renderGrokMcpRemoveArgs([], "strict"), - ["--sandbox", "strict", "mcp", "remove", "--scope", "project", "daimon"]); -}); - test("AGY registers the per-wake endpoint as an http server, flags before the name", () => { const args = renderAgyMcpAddArgs([], "http://127.0.0.1:54321/mcp"); assert.deepEqual(args, ["mcp", "add", "--type", "http", DAIMON_MCP_SERVER_NAME, "http://127.0.0.1:54321/mcp"]); diff --git a/src/pi/cliMcpRegistration.ts b/src/pi/cliMcpRegistration.ts index 83d5993..0d5118f 100644 --- a/src/pi/cliMcpRegistration.ts +++ b/src/pi/cliMcpRegistration.ts @@ -2,16 +2,17 @@ import { spawn, type ChildProcess } from "node:child_process"; import { readChild } from "./cliChildOutput.js"; import { terminateChild, trackCliChild } from "./cliProcess.js"; -import { renderGrokSandboxArgs } from "./cliEngineSpawn.js"; /** * Per-wake MCP endpoint registration for the CLI engines that cannot take the * endpoint on their own command line. * * Codex takes `-c mcp_servers.daimon.url=` per invocation and needs - * nothing here. Grok and AGY are both config-file driven, so Daimon registers - * the ephemeral endpoint before the turn and removes it afterwards, through - * each CLI's own `mcp add`/`mcp remove` subcommands. + * nothing here. AGY is config-file driven, so Daimon registers the ephemeral + * endpoint before the turn and removes it afterwards through its own `mcp + * add`/`mcp remove` subcommands. Grok no longer uses this path: its project + * scope is ignored in untrusted workspaces on 1.0.34, so its endpoint is written + * into the agent's Daimon-owned GROK_HOME (`grokHomeMcpRegistration.ts`). * * The registration is deliberately performed by the engine CLI rather than by * writing its config file directly: the file format belongs to the engine, and @@ -90,15 +91,9 @@ export const registerCliMcpServer = async ( }; }; -/** The MCP server name both engines register Daimon's per-wake endpoint under. */ +/** The MCP server name every CLI engine registers Daimon's per-wake endpoint under. */ export const DAIMON_MCP_SERVER_NAME = "daimon" as const; -export const renderGrokMcpAddArgs = (commandArgs: readonly string[] | undefined, profile: string, endpoint: string): string[] => - [...renderGrokSandboxArgs(commandArgs, profile), "mcp", "add", "--transport", "http", "--scope", "project", DAIMON_MCP_SERVER_NAME, endpoint]; - -export const renderGrokMcpRemoveArgs = (commandArgs: readonly string[] | undefined, profile: string): string[] => - [...renderGrokSandboxArgs(commandArgs, profile), "mcp", "remove", "--scope", "project", DAIMON_MCP_SERVER_NAME]; - /** * `agy mcp add --type http `. * diff --git a/src/pi/cliSession.test.ts b/src/pi/cliSession.test.ts index 3e76d9d..4c9a489 100644 --- a/src/pi/cliSession.test.ts +++ b/src/pi/cliSession.test.ts @@ -471,16 +471,16 @@ test("disposing from a Server.prototype.listen interleaving never leaves an MCP } }); -test("disposing during Grok registration terminates setup before the engine starts", async (context) => { +test("disposing during AGY MCP registration terminates setup before the engine starts", async (context) => { if (!requirePosixProcessGroups(context)) return; - const root = await mkdtemp(path.join(os.tmpdir(), "daimon-cli-grok-cancel-")); + const root = await mkdtemp(path.join(os.tmpdir(), "daimon-cli-agy-cancel-")); const ready = path.join(root, "add-ready"); const marker = path.join(root, "engine-started"); const grok = path.join(root, "grok.mjs"); await writeFile(grok, `import { writeFileSync } from "node:fs"; const args = process.argv.slice(2); if (args.includes("add")) { writeFileSync(${JSON.stringify(ready)}, "ready"); process.on("SIGTERM", () => undefined); setInterval(() => undefined, 1000); } else if (args.includes("remove")) process.exit(0); else writeFileSync(${JSON.stringify(marker)}, "started");`); try { const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [grok], engine: "grok", maxToolTurns: 1, timeoutMs: 10_000 + command: process.execPath, commandArgs: [grok], engine: "agy", maxToolTurns: 1, timeoutMs: 10_000 })({ cwd: root }); const pending = session.prompt("cancel"); void pending.catch(() => undefined); diff --git a/src/pi/cliSession.ts b/src/pi/cliSession.ts index ac5982a..07b964c 100644 --- a/src/pi/cliSession.ts +++ b/src/pi/cliSession.ts @@ -18,7 +18,6 @@ import type { TurnUsageOutcome } from "../runtime/turnUsageLedger.js"; import { readChild } from "./cliChildOutput.js"; import { cliChildEnvironment } from "./cliEnvironment.js"; import { - GROK_STRICT_SANDBOX_PROFILE, renderCodexArgs, spawnEngine } from "./cliEngineSpawn.js"; @@ -26,10 +25,9 @@ import { registerCliMcpServer, renderAgyMcpAddArgs, renderAgyMcpRemoveArgs, - renderGrokMcpAddArgs, - renderGrokMcpRemoveArgs, type CliMcpRegistration } from "./cliMcpRegistration.js"; +import { registerGrokHomeMcpServer } from "./grokHomeMcpRegistration.js"; import { decodeAgyHeadlessTurn, type AgyTurnUsage } from "./agyHeadlessResult.js"; import { type CodexTurnUsage } from "./codexHeadlessResult.js"; import { createCliTurnMeter, decodeCodexTurn, failedTurnOutcome, publishTurnRequests, publishTurnUsage } from "./cliTurnMetering.js"; @@ -321,35 +319,33 @@ class CliSession implements PiSessionLike { const controller=new AbortController();this.activeBrokerTurn=controller; try{output=await this.options.grokBrokerTurn(`${this.options.identityPrompt ?? ""}${text}`,mount.endpoint,controller.signal);}finally{if(this.activeBrokerTurn===controller)this.activeBrokerTurn=undefined;} } else { - if ((this.options.engine === "grok" || this.options.engine === "agy") && mount !== undefined) { + if (this.options.engine === "grok" && mount !== undefined) { + await this.options.verifyExecutable?.(); + registration = await registerGrokHomeMcpServer({ + engineHomePath: this.options.engineHomePath, + endpoint: mount.endpoint, + ...(this.options.verifyGrokSandbox !== undefined ? { verify: this.options.verifyGrokSandbox } : {}) + }); + this.mcpRegistration = registration; + } else if (this.options.engine === "agy" && mount !== undefined) { await this.options.verifyExecutable?.(); - const profile = this.options.grokSandboxProfile ?? GROK_STRICT_SANDBOX_PROFILE; - const grok = this.options.engine === "grok"; registration = await registerCliMcpServer({ - addArgs: grok - ? renderGrokMcpAddArgs(this.options.commandArgs, profile, mount.endpoint) - : renderAgyMcpAddArgs(this.options.commandArgs, mount.endpoint), - removeArgs: grok - ? renderGrokMcpRemoveArgs(this.options.commandArgs, profile) - : renderAgyMcpRemoveArgs(this.options.commandArgs), + addArgs: renderAgyMcpAddArgs(this.options.commandArgs, mount.endpoint), + removeArgs: renderAgyMcpRemoveArgs(this.options.commandArgs), command: this.options.command ?? this.options.engine, cwd: this.input.cwd, env: cliChildEnvironment([ ...(this.options.redactedEnvironmentNames ?? []), ...(this.input.daimonSecretEnvironmentNames ?? []) ], this.input.runtimeHomePath, { - ...(this.options.engine === "agy" && this.options.dbusSessionBusAddress !== undefined - ? { dbusSessionBusAddress: this.options.dbusSessionBusAddress } - : {}), + ...(this.options.dbusSessionBusAddress !== undefined ? { dbusSessionBusAddress: this.options.dbusSessionBusAddress } : {}), engine: this.options.engine, executablePath: this.options.command, engineHomePath: this.options.engineHomePath }), - ...(grok ? { failureClassifier: classifyGrokAuthenticationDiagnostic } : {}), onChild: (setupChild) => { this.setupChildren.add(setupChild); }, onChildSettled: (setupChild) => this.setupChildren.delete(setupChild), - secretValues, - ...(grok && this.options.verifyGrokSandbox !== undefined ? { verify: this.options.verifyGrokSandbox } : {}) + secretValues }); this.mcpRegistration = registration; } diff --git a/src/pi/cliSessionOutput.test.ts b/src/pi/cliSessionOutput.test.ts index 2dc92ac..3e6ec4a 100644 --- a/src/pi/cliSessionOutput.test.ts +++ b/src/pi/cliSessionOutput.test.ts @@ -98,7 +98,7 @@ test("Grok replies redact both the staged credential and a credential rotated du let reads = 0; try { const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [engine], engine: "grok", + command: process.execPath, commandArgs: [engine], engine: "grok", engineHomePath: path.join(root, ".grok"), credentialSecretValues: async () => ++reads === 1 ? [oldSecret] : [rotatedSecret] })({ cwd: root }); let emitted = ""; @@ -123,7 +123,7 @@ test("Grok auth rejection is typed and never retains raw credential diagnostics" await writeFile(engine, `const a=process.argv.slice(2);if(a.includes("mcp"))process.stdout.write("ok");else{process.stderr.write("Authentication rejected by server ${secret}");process.exitCode=7;}`); try { const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [engine], engine: "grok", + command: process.execPath, commandArgs: [engine], engine: "grok", engineHomePath: path.join(root, ".grok"), credentialSecretValues: async () => [secret] })({ cwd: root }); await assert.rejects(session.prompt("work"), (error: unknown) => { @@ -146,7 +146,7 @@ test("Grok auth rejection is classified before a long secret and verbose tail ar ].join("\n")); try { const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [engine], engine: "grok", + command: process.execPath, commandArgs: [engine], engine: "grok", engineHomePath: path.join(root, ".grok"), credentialSecretValues: async () => [secret] })({ cwd: root }); await assert.rejects(session.prompt("work"), (error: unknown) => { diff --git a/src/pi/cliSessionProcess.test.ts b/src/pi/cliSessionProcess.test.ts index 937b347..7b150c9 100644 --- a/src/pi/cliSessionProcess.test.ts +++ b/src/pi/cliSessionProcess.test.ts @@ -6,10 +6,9 @@ import test from "node:test"; import { createCliSessionFactory, readChild, spawnEngine, terminateChild } from "./cliSession.js"; -const grokStream = (text: string): string => [ - { type: "assistant", parent_tool_use_id: null, session_id: "fake", message: { role: "assistant", stop_reason: "end_turn", content: [{ type: "text", text }] } }, - { type: "result", subtype: "success", is_error: false, result: text, stop_reason: "end_turn", session_id: "fake" } -].map((event) => JSON.stringify(event)).join("\n"); +// Only AGY still registers its per-wake MCP endpoint through setup/removal +// children; Grok writes it into its own GROK_HOME (`grokHomeMcpRegistration.ts`). +const agyStream = (text: string): string => JSON.stringify({ event: "result", result: { conversation_id: "fake", status: "SUCCESS", response: text, num_turns: 1, usage: { input_tokens: 1, output_tokens: 1, thinking_tokens: 0, cache_read_tokens: 0, total_tokens: 2 } } }); test("terminates a process group after its leader has exited", async (context) => { if (!requirePosixProcessGroups(context)) return; @@ -29,9 +28,9 @@ test("terminates a process group after its leader has exited", async (context) = } }); -test("Grok setup reaps a stubborn descendant after its successful leader exits", async (context) => { +test("AGY MCP setup reaps a stubborn descendant after its successful leader exits", async (context) => { if (!requirePosixProcessGroups(context)) return; - const root = await mkdtemp(path.join(os.tmpdir(), "daimon-grok-setup-group-")); + const root = await mkdtemp(path.join(os.tmpdir(), "daimon-agy-setup-group-")); const descendant = path.join(root, "setup-descendant-pid"); const grok = path.join(root, "grok.mjs"); await writeFile(grok, `import { spawn } from "node:child_process"; import { writeFileSync } from "node:fs"; @@ -41,10 +40,10 @@ test("Grok setup reaps a stubborn descendant after its successful leader exits", child.stdout.once("data", () => { writeFileSync(${JSON.stringify(descendant)}, String(child.pid)); process.exit(0); }); } if (args.includes("remove")) process.exit(0); - process.stdout.write(${JSON.stringify(grokStream("engine complete"))});`); + process.stdout.write(${JSON.stringify(agyStream("engine complete"))});`); try { const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [grok], engine: "grok", maxToolTurns: 1, timeoutMs: 10_000 + command: process.execPath, commandArgs: [grok], engine: "agy", maxToolTurns: 1, timeoutMs: 10_000 })({ cwd: root }); await session.prompt("research"); const pid = Number(await readFile(descendant, "utf8")); @@ -56,9 +55,9 @@ test("Grok setup reaps a stubborn descendant after its successful leader exits", } }); -test("Grok removal reaps a stubborn descendant after its successful leader exits", async (context) => { +test("AGY MCP removal reaps a stubborn descendant after its successful leader exits", async (context) => { if (!requirePosixProcessGroups(context)) return; - const root = await mkdtemp(path.join(os.tmpdir(), "daimon-grok-remove-group-")); + const root = await mkdtemp(path.join(os.tmpdir(), "daimon-agy-remove-group-")); const descendant = path.join(root, "remove-descendant-pid"); const grok = path.join(root, "grok.mjs"); await writeFile(grok, `import { spawn } from "node:child_process"; import { writeFileSync } from "node:fs"; @@ -66,10 +65,10 @@ test("Grok removal reaps a stubborn descendant after its successful leader exits if (args.includes("remove")) { const child = spawn(process.execPath, ["-e", "process.on('SIGTERM', () => undefined); process.stdout.write('ready'); setInterval(() => undefined, 1000)"], { stdio: ["ignore", "pipe", "ignore"] }); child.stdout.once("data", () => { writeFileSync(${JSON.stringify(descendant)}, String(child.pid)); process.exit(0); }); - } else if (args.includes("add")) process.exit(0); else process.stdout.write(${JSON.stringify(grokStream("engine complete"))});`); + } else if (args.includes("add")) process.exit(0); else process.stdout.write(${JSON.stringify(agyStream("engine complete"))});`); try { const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [grok], engine: "grok", maxToolTurns: 1, timeoutMs: 10_000 + command: process.execPath, commandArgs: [grok], engine: "agy", maxToolTurns: 1, timeoutMs: 10_000 })({ cwd: root }); await session.prompt("research"); const pid = Number(await readFile(descendant, "utf8")); diff --git a/src/pi/cliSessionRemoval.test.ts b/src/pi/cliSessionRemoval.test.ts index aa880ac..f8320f1 100644 --- a/src/pi/cliSessionRemoval.test.ts +++ b/src/pi/cliSessionRemoval.test.ts @@ -1,23 +1,21 @@ import assert from "node:assert/strict"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { createCliSessionFactory } from "./cliSession.js"; -import { GrokSubscriptionAuthenticationRejectedError } from "../runtime/grokAuthenticationError.js"; -const grokStream = (text: string): string => [ - { type: "assistant", parent_tool_use_id: null, session_id: "fake", message: { role: "assistant", stop_reason: "end_turn", content: [{ type: "text", text }] } }, - { type: "result", subtype: "success", is_error: false, result: text, stop_reason: "end_turn", session_id: "fake" } -].map((event) => JSON.stringify(event)).join("\n"); +// Grok has no removal child any more (its endpoint lives in GROK_HOME config), +// so removal-failure semantics are exercised through AGY's `mcp remove`. +const agyStream = (text: string): string => JSON.stringify({ event: "result", result: { conversation_id: "fake", status: "SUCCESS", response: text, num_turns: 1, usage: { input_tokens: 1, output_tokens: 1, thinking_tokens: 0, cache_read_tokens: 0, total_tokens: 2 } } }); -test("Grok removal failure rejects without emitting a successful turn", async () => { - const root = await mkdtemp(path.join(os.tmpdir(), "daimon-grok-remove-failure-")); +test("MCP removal failure rejects without emitting a successful turn", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "daimon-agy-remove-failure-")); const grok = path.join(root, "grok.mjs"); - await writeFile(grok, `const args = process.argv.slice(2); if (args.includes("remove")) process.exit(23); else if (args.includes("add")) process.exit(0); else process.stdout.write(${JSON.stringify(grokStream("engine complete"))});`); + await writeFile(grok, `const args = process.argv.slice(2); if (args.includes("remove")) process.exit(23); else if (args.includes("add")) process.exit(0); else process.stdout.write(${JSON.stringify(agyStream("engine complete"))});`); try { - const { session } = await createCliSessionFactory({ command: process.execPath, commandArgs: [grok], engine: "grok", maxToolTurns: 1, timeoutMs: 10_000 })({ cwd: root }); + const { session } = await createCliSessionFactory({ command: process.execPath, commandArgs: [grok], engine: "agy", maxToolTurns: 1, timeoutMs: 10_000 })({ cwd: root }); let turns = 0; session.subscribe((event) => { if (event.type === "turn_end") turns += 1; }); await assert.rejects(session.prompt("research"), /CLI engine exited 23/); @@ -29,43 +27,27 @@ test("Grok removal failure rejects without emitting a successful turn", async () } }); -test("Grok authentication rejection keeps precedence over bounded removal failure", async () => { - const root = await mkdtemp(path.join(os.tmpdir(), "daimon-grok-auth-remove-failure-")); +test("Grok direct sessions register the per-wake MCP endpoint in GROK_HOME, never through a project-scoped child", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "daimon-grok-home-mcp-")); const grok = path.join(root, "grok.mjs"); - const authSecret = "auth-rejection-secret-canary"; - const cleanupSecret = "cleanup-secret-canary"; - await writeFile(grok, `const args=process.argv.slice(2);if(args.includes("remove")){process.stderr.write("cleanup ${cleanupSecret}");process.exit(23)}else if(args.includes("add"))process.exit(0);else{process.stderr.write("RefreshTokenRejected ${authSecret}");process.exit(7)}`); + const engineHomePath = path.join(root, "home", ".grok"); + const seen = path.join(root, "seen"); + await writeFile(grok, `import { appendFileSync, readFileSync } from "node:fs"; const args = process.argv.slice(2); appendFileSync(${JSON.stringify(seen)}, args.includes("mcp") ? "MCP-CHILD\\n" : readFileSync(${JSON.stringify(path.join(engineHomePath, "config.toml"))}, "utf8")); process.stdout.write(${JSON.stringify([{ type: "assistant", parent_tool_use_id: null, session_id: "fake", message: { role: "assistant", stop_reason: "end_turn", content: [{ type: "text", text: "done" }] } }, { type: "result", subtype: "success", is_error: false, result: "done", stop_reason: "end_turn", session_id: "fake" }].map((event) => JSON.stringify(event)).join("\n"))});`); try { - const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [grok], engine: "grok", maxToolTurns: 1, - timeoutMs: 10_000, credentialSecretValues: async () => [authSecret, cleanupSecret] - })({ cwd: root }); - await assert.rejects(session.prompt("research"), (error: unknown) => { - assert.ok(error instanceof GrokSubscriptionAuthenticationRejectedError); - assert.doesNotMatch(error.message, /canary|cleanup|RefreshTokenRejected/u); - assert.ok(error.cause instanceof Error); - assert.match(error.cause.message, /CLI engine exited 23/u); - assert.doesNotMatch(error.cause.message, /canary|cleanup-secret/u); - assert.ok(Buffer.byteLength(error.cause.message) < 1_024); - return true; - }); - } finally { await rm(root, { recursive: true, force: true }); } -}); + const { session } = await createCliSessionFactory({ command: process.execPath, commandArgs: [grok], engine: "grok", maxToolTurns: 1, timeoutMs: 10_000, engineHomePath })({ cwd: root }); + await session.prompt("research"); + const during = await readFile(seen, "utf8"); + assert.doesNotMatch(during, /MCP-CHILD/u); + assert.match(during, /\[mcp_servers\.daimon\]\nurl = "http:\/\/127\.0\.0\.1:\d+\/mcp"/u); + assert.match(during, /\[skills\]\ndisabled = \[/u); + const after = await readFile(path.join(engineHomePath, "config.toml"), "utf8"); + assert.doesNotMatch(after, /mcp_servers/u); + await session.disposeAsync?.(); -test("Grok removal auth rejection is typed before verbose diagnostics are truncated", async () => { - const root = await mkdtemp(path.join(os.tmpdir(), "daimon-grok-remove-auth-rejection-")); - const grok = path.join(root, "grok.mjs"); - const secret = `refresh-start-${"r".repeat(1970)}-refresh-end`; - await writeFile(grok, `const a=process.argv.slice(2);if(a.includes("remove")){process.stderr.write("RefreshTokenRejected "+${JSON.stringify(secret)}+" "+"tail".repeat(250));process.exit(23)}else if(a.includes("add"))process.exit(0);else process.stdout.write(${JSON.stringify(grokStream("complete"))});`); - try { - const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [grok], engine: "grok", - credentialSecretValues: async () => [secret] - })({ cwd: root }); - await assert.rejects(session.prompt("research"), (error: unknown) => { - assert.ok(error instanceof GrokSubscriptionAuthenticationRejectedError); - assert.doesNotMatch(error.message, /RefreshToken|refresh-start|refresh-end|r{32}|tail/u); - return true; - }); - } finally { await rm(root, { recursive: true, force: true }); } + const unowned = await createCliSessionFactory({ command: process.execPath, commandArgs: [grok], engine: "grok", maxToolTurns: 1, timeoutMs: 10_000 })({ cwd: root }); + await assert.rejects(unowned.session.prompt("research"), /Daimon-owned GROK_HOME/u); + await unowned.session.disposeAsync?.(); + } finally { + await rm(root, { recursive: true, force: true }); + } }); diff --git a/src/pi/grokHomeMcpRegistration.ts b/src/pi/grokHomeMcpRegistration.ts new file mode 100644 index 0000000..2c1fcef --- /dev/null +++ b/src/pi/grokHomeMcpRegistration.ts @@ -0,0 +1,58 @@ +import { randomUUID } from "node:crypto"; +import { constants } from "node:fs"; +import { lstat, mkdir, open, rename, unlink } from "node:fs/promises"; +import path from "node:path"; + +import { renderGrokLeanBaseConfig } from "../runtime/grokBrokerWorkerConfig.js"; +import type { CliMcpRegistration } from "./cliMcpRegistration.js"; + +/** + * Per-wake MCP registration for the direct (non-broker) Grok CLI path. + * + * `grok mcp add --scope project` writes `/.grok/config.toml`, which Grok + * 1.0.34 skips entirely in an untrusted workspace — and Daimon keeps every + * workspace untrusted so cwd `AGENTS.md` and project skills never load. The + * endpoint therefore goes into the agent's own Daimon-owned `GROK_HOME` + * config, rendered from the same lean base the broker worker uses + * (`renderGrokLeanBaseConfig`), and is removed again after the turn by + * rewriting the base alone. + * + * There is no fallback to the operator's `~/.grok`: without an explicit + * `engineHomePath` the session refuses, rather than editing a human's config. + */ +export async function registerGrokHomeMcpServer(input: Readonly<{ engineHomePath: string | undefined; endpoint: string; verify?: () => Promise }>): Promise { + const home = input.engineHomePath; + if (home === undefined || !path.isAbsolute(home)) { + throw new Error("Grok direct CLI sessions require a Daimon-owned GROK_HOME (engineHomePath): Grok 1.0.34 ignores project-scoped MCP servers in untrusted workspaces"); + } + if (!/^http:\/\/127\.0\.0\.1:\d{1,5}\/[A-Za-z0-9/_-]*$/u.test(input.endpoint)) throw new Error("Grok MCP endpoint must be a loopback http URL"); + await input.verify?.(); + await writeConfig(home, `${renderGrokLeanBaseConfig()}[mcp_servers.daimon]\nurl = ${JSON.stringify(input.endpoint)}\n`); + let closePromise: Promise | undefined; + return { + close: (): Promise => closePromise ??= (async () => { + await input.verify?.(); + await writeConfig(home, renderGrokLeanBaseConfig()); + })() + }; +} + +async function writeConfig(home: string, text: string): Promise { + await mkdir(home, { recursive: true, mode: 0o700 }); + const directory = await lstat(home); + if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error("Grok engine home is not a private directory"); + const target = path.join(home, "config.toml"); + const temporary = path.join(home, `.config.toml.${randomUUID()}.tmp`); + let handle: Awaited> | undefined; + try { + handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600); + await handle.writeFile(text); + await handle.sync(); + await handle.close(); + handle = undefined; + await rename(temporary, target); + } finally { + await handle?.close().catch(() => undefined); + await unlink(temporary).catch(() => undefined); + } +} From 7529ef95f671fedb678aa4749078654c7eab5ba6 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 04:47:25 +0200 Subject: [PATCH 05/26] feat: compile the lean Grok worker argv and system prompt into the native launcher --- src/runtime/native/engineBrokerLauncher.h | 6 ++ .../native/engineBrokerLauncherCore.inc | 8 +++ ...ngineBrokerLauncherIntegrationLauncher.inc | 13 +++++ src/runtime/native/launcherArgv.test.ts | 55 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 src/runtime/native/launcherArgv.test.ts diff --git a/src/runtime/native/engineBrokerLauncher.h b/src/runtime/native/engineBrokerLauncher.h index 76e7ce9..d3a9512 100644 --- a/src/runtime/native/engineBrokerLauncher.h +++ b/src/runtime/native/engineBrokerLauncher.h @@ -21,6 +21,12 @@ #ifndef DBL_EXECUTABLE #define DBL_EXECUTABLE "/usr/local/bin/grok" #endif +/* Lean Grok worker contract; mirrored byte-for-byte by src/contracts/grokWorkerContract.ts + and checked by launcherArgv.test.ts. */ +#define DBL_GROK_SYSTEM_PROMPT \ + "You are a headless Daimon agent; no human is present. Your identity, instructions and wake event are in the user prompt. Daimon tools are MCP tools on server daimon: call a known one directly with use_tool (tool_name daimon__moltnet_read, daimon__moltnet_send, daimon__memory_search, daimon__memory_register, or another daimon__ name you were given); use search_tool only for a name you do not know. If a tool result says output was saved to a file, read that path with read_file. If a tool fails, do not retry it in a loop: stop and report the failure. Your final answer is a private note to the runtime: one line, or empty." +#define DBL_GROK_TOOLS "run_terminal_cmd,read_file,grep,list_dir,search_tool,use_tool" +#define DBL_GROK_MAX_TURNS "48" struct dbl_request { uint32_t version, slot; diff --git a/src/runtime/native/engineBrokerLauncherCore.inc b/src/runtime/native/engineBrokerLauncherCore.inc index 8329892..1bb8f45 100644 --- a/src/runtime/native/engineBrokerLauncherCore.inc +++ b/src/runtime/native/engineBrokerLauncherCore.inc @@ -321,6 +321,14 @@ static pid_t launch(const struct dbl_registration *r, int executable, "/proc/self/fd/3", "--no-memory", "--disable-web-search", + "--no-plan", + "--verbatim", + "--system-prompt-override", + DBL_GROK_SYSTEM_PROMPT, + "--tools", + DBL_GROK_TOOLS, + "--max-turns", + DBL_GROK_MAX_TURNS, "--cwd", (char *)r->workspace, "--output-format", diff --git a/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc b/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc index 8458226..adab67f 100644 --- a/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc +++ b/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc @@ -112,6 +112,19 @@ static void org_cases(void) { strstr(out, "uid=2200") && strstr(out, "--always-approve") && !strstr(out, "EVIL"), "fixed worker boundary"); + check(strstr(out, "=--verbatim\n") && strstr(out, "=--no-plan\n") && + strstr(out, "=--no-subagents\n") && strstr(out, "=--no-memory\n") && + strstr(out, "=--disable-web-search\n") && + strstr(out, "=--tools\n") && strstr(out, "=" DBL_GROK_TOOLS "\n") && + strstr(out, "=--max-turns\n") && + strstr(out, "=" DBL_GROK_MAX_TURNS "\n") && + strstr(out, "=--system-prompt-override\n") && + strstr(out, "=" DBL_GROK_SYSTEM_PROMPT "\n") && + strstr(out, "=--prompt-file\n") && + strstr(out, "=/proc/self/fd/3\n") && + !strstr(out, "--reasoning-effort") && + strstr(out, " argc=23 "), + "lean worker argv"); free(out); close(s); q = request(); diff --git a/src/runtime/native/launcherArgv.test.ts b/src/runtime/native/launcherArgv.test.ts new file mode 100644 index 0000000..3712891 --- /dev/null +++ b/src/runtime/native/launcherArgv.test.ts @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { GROK_ENGINE_BROKER } from "../../contracts/runtimeContractManifest.js"; +import { DAIMON_GROK_SYSTEM_PROMPT, GROK_WORKER_MAX_TURNS, GROK_WORKER_TOOL_IDS } from "../../contracts/grokWorkerContract.js"; +import { renderGrokBrokerWorkerArgs } from "../grokBrokerWorkerConfig.js"; + +const read = (name: string): string => readFileSync(new URL(`./${name}`, import.meta.url), "utf8"); +const unquote = (literal: string): string => { + assert.match(literal, /^"[^"\\]*"$/u, "launcher literals must not need escaping"); + return literal.slice(1, -1); +}; + +/** `#define NAME "value"` (optionally continued onto the next line) from the launcher header. */ +const defines = (): ReadonlyMap => { + const header = read("engineBrokerLauncher.h").replace(/\\\n\s*/gu, ""); + return new Map([...header.matchAll(/^#define (DBL_GROK_[A-Z_]+)\s+("[^"\n]*")$/gmu)].map((match) => [match[1]!, unquote(match[2]!)])); +}; + +/** The compiled worker argv, token by token, exactly as `launch()` passes it to `execveat`. */ +const compiledArgv = (): readonly string[] => { + const source = read("engineBrokerLauncherCore.inc"); + const block = source.match(/char \*const argv\[\] = \{([\s\S]*?)NULL\};/u); + assert.ok(block, "launcher argv array not found"); + const values = defines(); + return block[1]!.split(",").map((token) => token.trim()).filter(Boolean).map((token) => { + if (token.startsWith('"')) return unquote(token); + if (token === "(char *)r->workspace") return "/registered/workspace"; + const value = values.get(token); + assert.ok(value !== undefined, `unexpected launcher argv token ${token}`); + return value; + }); +}; + +test("the native launcher compiles exactly the lean Grok worker argv Daimon renders", () => { + const argv = compiledArgv(); + assert.equal(argv[0], "grok"); + assert.deepEqual(argv.slice(1), renderGrokBrokerWorkerArgs("/proc/self/fd/3", "/registered/workspace")); + for (const flag of ["--verbatim", "--no-plan", "--no-subagents", "--no-memory", "--disable-web-search", "--always-approve"]) assert.ok(argv.includes(flag), flag); + assert.equal(argv[argv.indexOf("--tools") + 1], GROK_WORKER_TOOL_IDS.join(",")); + assert.equal(argv[argv.indexOf("--max-turns") + 1], String(GROK_WORKER_MAX_TURNS)); + assert.equal(argv.includes("--reasoning-effort"), false, "effort is declared per deployment in config.toml, never compiled"); + assert.equal(argv.includes("--disallowed-tools"), false, "--disallowed-tools is ignored under --tools"); +}); + +test("the compiled system prompt is byte-identical to the contract prompt pinned in the manifest", () => { + const compiled = defines().get("DBL_GROK_SYSTEM_PROMPT"); + assert.equal(compiled, DAIMON_GROK_SYSTEM_PROMPT); + assert.equal(createHash("sha256").update(DAIMON_GROK_SYSTEM_PROMPT).digest("hex"), GROK_ENGINE_BROKER.worker.systemPromptSha256); + assert.match(DAIMON_GROK_SYSTEM_PROMPT, /^[\x20-\x7e]+$/u); + assert.ok(DAIMON_GROK_SYSTEM_PROMPT.length >= 320 && DAIMON_GROK_SYSTEM_PROMPT.length <= 700, "roughly 80-150 tokens"); + for (const tool of ["daimon__moltnet_read", "daimon__moltnet_send", "use_tool", "search_tool", "read_file"]) assert.ok(DAIMON_GROK_SYSTEM_PROMPT.includes(tool), tool); +}); From 4bbf22462a36ba3d30220479fc9c9c917bec457d Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 04:48:00 +0200 Subject: [PATCH 06/26] build: rebuild native engine broker artifacts for the lean Grok worker argv --- src/contracts/runtimeContractManifest.ts | 6 ++--- .../artifacts/daimon-engine-broker-arm64 | Bin 67456 -> 67456 bytes ...daimon-engine-broker-arm64.provenance.json | 2 +- .../native/artifacts/daimon-engine-broker-x64 | Bin 55392 -> 55392 bytes .../daimon-engine-broker-x64.provenance.json | 2 +- src/runtime/native/artifactsManifest.test.ts | 24 ++++++++++++++++++ 6 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 src/runtime/native/artifactsManifest.test.ts diff --git a/src/contracts/runtimeContractManifest.ts b/src/contracts/runtimeContractManifest.ts index 561e781..4dbf679 100644 --- a/src/contracts/runtimeContractManifest.ts +++ b/src/contracts/runtimeContractManifest.ts @@ -69,9 +69,9 @@ export const GROK_ENGINE_BROKER = { }, bounds: { promptBytes: 65_536, capabilityBytes: 4_096, capabilityBundleBytes: 8_196, outputBytes: 65_536 }, artifacts: { - sourceSha256: "bdcab1e12dcc531ed8e56f890263ca23a9ee7bac468191dd598e143df4ff8c58", - x64Sha256: "e3fe2738fc8a979861085b4003bf2d5d7c284874897cb6ec2e2e2383211768bd", - arm64Sha256: "ad44e02c38e6a3207ac4a3d5fd98b6d2e55341ce42dfd2f07204bbe54a7a653d" + sourceSha256: "7830ff308420cf1b84aba118903c1e8ef40f8c2a320136977a1e7f9e3292a734", + x64Sha256: "b3da1618ca218ff44ce178385f94146736439acccaedaf047d41709ab0aa5d43", + arm64Sha256: "b4f41d429db5f8fefaf880f9ff9d64e76d0687829e6002fe7c7052cc7a41af6e" } } as const; export const AGY_SUBSCRIPTION_REALM = { diff --git a/src/runtime/native/artifacts/daimon-engine-broker-arm64 b/src/runtime/native/artifacts/daimon-engine-broker-arm64 index f028a3ca832c3adb81418ca17d8b062aed9f056f..0fd341da3ef4f6496e05941770aefbb39a3ad5b7 100755 GIT binary patch delta 4173 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 Up>xMAYZzX(tl`i@V`zr(KW^+-m;e9( delta 3347 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 delta 6223 zcmZ9Q3v?4z8pku!7TSs=DAj@m(lXFOOCLbxA%(WIC6ox2#T8xD=uxc5qats?q;0B% zww^4PRm9Z|kFyJi8c3HdrO+TOAkT<^pdRfiYDPgSx}3_{b6|p9Zp%s{J`1y|;x~p}> z8hjb6R;&q;pw8oLyK=f$li_mwX4x9`vDB_DM0G`^rHVj8%tN<1qd4AZ#AF&~oHR$S z5#_-wg9p2Cb*I6PcHs~B4Ib*kt0Me@!JVe)1sWrc_AV8+MR>RiKO5m4U3hz>L-8S_ z1JU-Q`od|0(`?6@oQ9xdyX5G83!B0cA0+V(iI0_dr^Lrg+;Ykg)HW@*D0KZ~$)QF# z8LPx&B%UGhIEm*~9i^N?L?GI9P~Mk<^hE`BV9=@y`7^BIzyV0 zu^tlo7-`DEdQj*ONK@|B{X(~prU$6*6M7eECnaIsE5LRFlzerU(3?op0_#;mZy-%c zSa%4$mNeyFy+Y_Uq$%g>g+kYmrd+G%3cZ3fMo(vNK-D+b?oNg8R z+78@9m6Y3bivZtK0HrkDB=lFLDV6CR|D_rDl(dy}yU=GyQ|i(~LLVbdDN7Fu{Q+sD zD$Ib;Eu<+!={})%ISJ64f$kMxJ84Q)x=ZLyq$x$|RYGqdO{qzD2)&jxr6j#V=ryD% z6X}IQ*N~NO$~4>;cl0S@d?Hr<10P zqKAYoCrz0|4+>pOnlgy)7kVsdN*ub6^rS(<2~ft+y&_;JY04D3OXxJxbgS!CLiZ(2 znL&35P4#DcS+Cg9Yqgh@Zp^9e%Ng8&JK6JPIE;w8pxI5Sz}A?b9Kjz&@CgI=3OqMx zn4;7N-WdG7s;p-2Av%uphpxi$^3a3ol>uyLW}~v1O~^{>y_s5y+eKzf=b|KAecY1G ztTHPlUO4h=HnWDT6vS@J>OTa?v^euYTWvuao!UI#Eb3vaFBl9FzIdJU@Kcz9!*TsK z2RgE5bW;jh>4;k8rND#`FU~5GPZYYnzEFAG4_D6G~l~0 zPE{7MzZRUraq;;1$_wn$`0)v=F*WwaEt-{OPACUkGQp}WWUo(H=_Jn|9xa+x^k~sz zMUNk}H@0e3$R;*e7w)M&bmQCVlZqFbOh3P4_wYTGitL^(njbj|1dURh=c>)KPRl~# zvp4P#KD+1J7QB@f+K#xjX2dp-&$C1GBAVEGScTrn?4DN57lB*Z#=?8j{7@O@x+}QZ zZ1wB3t>mn|VK%?~nIH|Ck9|=%Bio9!soLrvwwUW)AP2gf0kppeJq#CIHWv9|ctW8|n!tnx$YIdw61wvVeIe+HzLYq!*Z6Xu2*B&x`ABjnHJXdmt3G+dUa^jBX^VAhiS^dKhIRzFe`KJ+zwFS0xBZ=Z0i)Z%Rze-T?*lC8{V@05(d$iFNp zwG^XoW_LD%@;H{j#YUDsq&&|WO4F5Q)>OI_=UHV_Q+=4y@RiRwuMI0^_gbjD>v=r9 z7X%lntBz%FmW@&J*txRQcr(0*-B~Vnv#d|bk1^<1?Gd#Z*7QRxP8IX+yWMBZ?z1s$ z#H1n4CJJa=q*b(Fpr&x@cSeg>fM=Vp2FX>`h89icAthQ>IQ28b-v+|d)Z5+1H#XQV=(#_fE zGTqGKe0c1hU-u{D0pG{&`3}y1!vNi+|Dw{KER!%as2zhkb!M?=1VsK2#Y#a(`?Yx z`Re4efj>>1ps4X52hLBM*Ij*Jd7$*6x5`JnhvzZgQF~(NpFu0;0WMTE6+LKcENH{) zSJ8|Ie`l+`9DgLP8iAN*Q+Pc~+B8bFWwWW9>iQnQK=F@cCGaXS=`Q}EVOebYj0ZK%guQcehX?j|G0+pH{F3be~(d} z&*9O9%QzNrZoz!|V4uPY1ebE&3?5v@c?Nh0b{uT(a?Tql4z`K(3eMYMy|B4h9m|WH zJ7BAD89%HK_7HJgx&zh$8;doyyv#X$fpVH&;d~`dd}}yA0bBSw=gH_mXgxXrYk314 zwgR>t*7_#rCCGufurpx`VduhH-s1evuvS<&X?) z3-KoJ{SozGgRtvhL$F(6+hJQ_J77ZzJUSy~?gPXDUlr+MXQcSvGx7`mlNE zD{YTRq1W|U_<5IbPEUAKNl%L%rV77?C9J%qj9uQ7fEKRrNyo8&OD5hh%3G%4xS=Hn z$73zilAN5U8g;E1u$@@svrd-1H#5n_c^P?7Z1|1Fvd(a^`Fp1*)vRT2rc%Sc-8(I= z4tu8u8{0Zi4L;8vZ%tLLYgtX}6lD%O)jEBsg>fGJ*hSsNr!~_!i8+9JgHfzhMv#5#6 zQ3dt`25Xv>5ECEsrE(Cb6rTZK2{zjkIHy8v9GDO6aX}1L5odbFbQ$nD3DT`%!_J%` zv1MTEz~s0Funw?FDb5FG-*AhSz}EzJVFUYqe_5Xj+~MPK$51K6&w|anau?_OSYz8j zHn%NB?F_OtZN+NSX?Ch@gKGPl%?XZIo4;n;gX5j?-*BErRZ(a}v~x8uv27wu?BXu0 z192%`SSIOz^IeA;ao3u)?(EkyX=yzk4Z9JZy82xD5 zzLQ-$o;}95*$9e$`HFswj(*uf!=gmLfJHw_r?8)tJK5Y5iCWj+z5EKJKQdZ#$WK zE;}*WMc-yN_S_^TmerigNoa~(rirzj8 readFileSync(new URL(`./${name}`, import.meta.url)); + +test("the manifest pins the committed native artifacts, their provenance, and the current launcher source", () => { + const source = createHash("sha256"); + for (const name of sources) source.update(read(name)); + assert.equal(GROK_ENGINE_BROKER.artifacts.sourceSha256, source.digest("hex"), "launcher source changed without rebuilding the native artifacts"); + for (const [architecture, pinned] of [["x64", GROK_ENGINE_BROKER.artifacts.x64Sha256], ["arm64", GROK_ENGINE_BROKER.artifacts.arm64Sha256]] as const) { + const binary = read(`artifacts/daimon-engine-broker-${architecture}`); + const provenance = JSON.parse(read(`artifacts/daimon-engine-broker-${architecture}.provenance.json`).toString("utf8")) as Record; + const digest = createHash("sha256").update(binary).digest("hex"); + assert.equal(digest, pinned, architecture); + assert.equal(provenance.binary_sha256, `sha256:${digest}`, architecture); + assert.equal(provenance.source_sha256, `sha256:${GROK_ENGINE_BROKER.artifacts.sourceSha256}`, architecture); + assert.equal(provenance.install_path, GROK_ENGINE_BROKER.nativeExecutablePath, architecture); + } +}); From 9761d8d4ee920b1da4ae364556719e383b0ae03f Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 04:51:41 +0200 Subject: [PATCH 07/26] fix: keep the sealed prompt fd open across the Grok worker exec --- .../native/engineBrokerLauncherCore.inc | 25 ++++++++++++++----- ...ngineBrokerLauncherIntegrationLauncher.inc | 1 + src/runtime/native/fixtureWorker.c | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/runtime/native/engineBrokerLauncherCore.inc b/src/runtime/native/engineBrokerLauncherCore.inc index 1bb8f45..c093307 100644 --- a/src/runtime/native/engineBrokerLauncherCore.inc +++ b/src/runtime/native/engineBrokerLauncherCore.inc @@ -306,12 +306,25 @@ static pid_t launch(const struct dbl_registration *r, int executable, if (prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0) || prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) launch_fail(status_pipe[1], 3); - if (dup2(prompt, 3) < 0 || dup2(capability, 4) < 0 || - dup2(output, STDOUT_FILENO) < 0 || dup2(output, STDERR_FILENO) < 0) + /* Received fds carry MSG_CMSG_CLOEXEC and may already sit on 3-5: dup2 onto + itself keeps close-on-exec (the prompt vanished at exec) and an + overlapping order clobbers a source. Lift all of them above the targets + first so every dup2 below changes the fd number and clears CLOEXEC. */ + int status_fd = fcntl(status_pipe[1], F_DUPFD_CLOEXEC, 16); + if (status_fd < 0) launch_fail(status_pipe[1], 4); - if (executable != 5 && dup2(executable, 5) < 0) - launch_fail(status_pipe[1], 5); - close_other_fds(status_pipe[1]); + int high_prompt = fcntl(prompt, F_DUPFD_CLOEXEC, 16), + high_capability = fcntl(capability, F_DUPFD_CLOEXEC, 16), + high_output = fcntl(output, F_DUPFD_CLOEXEC, 16), + high_executable = fcntl(executable, F_DUPFD_CLOEXEC, 16); + if (high_prompt < 0 || high_capability < 0 || high_output < 0 || + high_executable < 0 || dup2(high_prompt, 3) < 0 || + dup2(high_capability, 4) < 0 || dup2(high_output, STDOUT_FILENO) < 0 || + dup2(high_output, STDERR_FILENO) < 0) + launch_fail(status_fd, 4); + if (dup2(high_executable, 5) < 0) + launch_fail(status_fd, 5); + close_other_fds(status_fd); char *const argv[] = {"grok", "--sandbox", "daimon-strict", @@ -351,6 +364,6 @@ static pid_t launch(const struct dbl_registration *r, int executable, NULL}; syscall(SYS_execveat, 5, "", argv, envp, AT_EMPTY_PATH); erase(mcp_env, sizeof(mcp_env)); - launch_fail(status_pipe[1], 6); + launch_fail(status_fd, 6); } diff --git a/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc b/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc index adab67f..20d6849 100644 --- a/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc +++ b/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc @@ -110,6 +110,7 @@ static void org_cases(void) { char *out = calloc(1, r.output_length + 1); check(read(s, out, r.output_length) == (ssize_t)r.output_length && strstr(out, "uid=2200") && strstr(out, "--always-approve") && + strstr(out, " prompt=prompt\n") && !strstr(out, "EVIL"), "fixed worker boundary"); check(strstr(out, "=--verbatim\n") && strstr(out, "=--no-plan\n") && diff --git a/src/runtime/native/fixtureWorker.c b/src/runtime/native/fixtureWorker.c index 6baaad7..63af5b8 100644 --- a/src/runtime/native/fixtureWorker.c +++ b/src/runtime/native/fixtureWorker.c @@ -5,4 +5,4 @@ #include #include #include "engineBrokerLauncher.h" -int main(int argc,char**argv){unsigned char bundle[8200]={0};ssize_t size=pread(4,bundle,sizeof(bundle),0);if(size<6)return 21;size_t offset=0;unsigned provider_length=bundle[0]|(bundle[1]<<8);offset=2;if(offset+provider_length+2>(size_t)size)return 21;char provider[4097]={0};memcpy(provider,bundle+offset,provider_length);offset+=provider_length;unsigned mcp_length=bundle[offset]|(bundle[offset+1]<<8);offset+=2;if(offset+mcp_length!=(size_t)size)return 21;char mcp[4097]={0};memcpy(mcp,bundle+offset,mcp_length);memset(bundle,0,sizeof(bundle));int exact=!strcmp(provider,"exact-output"),overflow=!strcmp(provider,"overflow-output");if(!strcmp(provider,"sleep"))sleep(30);if(!getenv("DAIMON_MCP_CAPABILITY")||strcmp(getenv("DAIMON_MCP_CAPABILITY"),mcp)||strstr(getenv("DAIMON_MCP_CAPABILITY"),provider))return 24;int output[2];if(pipe(output))return 22;pid_t child=fork();if(!child){dup2(output[1],1);close(output[0]);execl("/opt/daimon/bin/daimon-engine-broker","daimon-engine-broker","--auth-provider",NULL);_exit(127);}close(output[1]);char auth[8192]={0};ssize_t got=read(output[0],auth,sizeof(auth)-1);close(output[0]);int status;waitpid(child,&status,0);char expected[8192];snprintf(expected,sizeof(expected),"{\"access_token\":\"%s\",\"expires_in\":600}\n",provider);memset(provider,0,sizeof(provider));memset(mcp,0,sizeof(mcp));if(!WIFEXITED(status)||WEXITSTATUS(status)||got<1||strcmp(auth,expected))return 23;memset(auth,0,sizeof(auth));memset(expected,0,sizeof(expected));if(exact||overflow){size_t count=DBL_MAX_OUTPUT+(size_t)overflow;for(size_t i=0;i(size_t)size)return 21;char provider[4097]={0};memcpy(provider,bundle+offset,provider_length);offset+=provider_length;unsigned mcp_length=bundle[offset]|(bundle[offset+1]<<8);offset+=2;if(offset+mcp_length!=(size_t)size)return 21;char mcp[4097]={0};memcpy(mcp,bundle+offset,mcp_length);memset(bundle,0,sizeof(bundle));int exact=!strcmp(provider,"exact-output"),overflow=!strcmp(provider,"overflow-output");if(!strcmp(provider,"sleep"))sleep(30);if(!getenv("DAIMON_MCP_CAPABILITY")||strcmp(getenv("DAIMON_MCP_CAPABILITY"),mcp)||strstr(getenv("DAIMON_MCP_CAPABILITY"),provider))return 24;int output[2];if(pipe(output))return 22;pid_t child=fork();if(!child){dup2(output[1],1);close(output[0]);execl("/opt/daimon/bin/daimon-engine-broker","daimon-engine-broker","--auth-provider",NULL);_exit(127);}close(output[1]);char auth[8192]={0};ssize_t got=read(output[0],auth,sizeof(auth)-1);close(output[0]);int status;waitpid(child,&status,0);char expected[8192];snprintf(expected,sizeof(expected),"{\"access_token\":\"%s\",\"expires_in\":600}\n",provider);memset(provider,0,sizeof(provider));memset(mcp,0,sizeof(mcp));if(!WIFEXITED(status)||WEXITSTATUS(status)||got<1||strcmp(auth,expected))return 23;memset(auth,0,sizeof(auth));memset(expected,0,sizeof(expected));if(exact||overflow){size_t count=DBL_MAX_OUTPUT+(size_t)overflow;for(size_t i=0;i Date: Thu, 17 Sep 2026 04:51:41 +0200 Subject: [PATCH 08/26] build: rebuild native engine broker artifacts with the prompt fd fix --- src/contracts/runtimeContractManifest.ts | 6 +++--- .../artifacts/daimon-engine-broker-arm64 | Bin 67456 -> 67456 bytes ...daimon-engine-broker-arm64.provenance.json | 2 +- .../native/artifacts/daimon-engine-broker-x64 | Bin 55392 -> 55392 bytes .../daimon-engine-broker-x64.provenance.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/contracts/runtimeContractManifest.ts b/src/contracts/runtimeContractManifest.ts index 4dbf679..0f4b786 100644 --- a/src/contracts/runtimeContractManifest.ts +++ b/src/contracts/runtimeContractManifest.ts @@ -69,9 +69,9 @@ export const GROK_ENGINE_BROKER = { }, bounds: { promptBytes: 65_536, capabilityBytes: 4_096, capabilityBundleBytes: 8_196, outputBytes: 65_536 }, artifacts: { - sourceSha256: "7830ff308420cf1b84aba118903c1e8ef40f8c2a320136977a1e7f9e3292a734", - x64Sha256: "b3da1618ca218ff44ce178385f94146736439acccaedaf047d41709ab0aa5d43", - arm64Sha256: "b4f41d429db5f8fefaf880f9ff9d64e76d0687829e6002fe7c7052cc7a41af6e" + sourceSha256: "5eadf15faeccd5fec14f03e701c1b7001d7f4058941987070bf5e512e9984e5e", + x64Sha256: "51dc67387cd0c9dbbbc36b577b5c98ea296ecaf3f2f015be52eb4df98affcbbc", + arm64Sha256: "5821e547aa6e7c5682eaae1a3f6106138ccd5b5666909c6e135862a61b35cc47" } } as const; export const AGY_SUBSCRIPTION_REALM = { diff --git a/src/runtime/native/artifacts/daimon-engine-broker-arm64 b/src/runtime/native/artifacts/daimon-engine-broker-arm64 index 0fd341da3ef4f6496e05941770aefbb39a3ad5b7..2afa07296b6cb9e546701b836575a7752a43c5ad 100755 GIT binary patch delta 2387 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 diff --git a/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json b/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json index ffc0023..405f166 100644 --- a/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json +++ b/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json @@ -1 +1 @@ -{"version":"daimon.engine-broker-native-build.v1","architecture":"x64","target":"linux/amd64","builder_image":"gcc:14-bookworm@sha256:5e927c284bf55a7dc796262e311a0703344f62f41f5621eb56843111b1d37e15","compiler":"gcc-14","source_sha256":"sha256:7830ff308420cf1b84aba118903c1e8ef40f8c2a320136977a1e7f9e3292a734","binary_sha256":"sha256:b3da1618ca218ff44ce178385f94146736439acccaedaf047d41709ab0aa5d43","install_path":"/opt/daimon/bin/daimon-engine-broker"} +{"version":"daimon.engine-broker-native-build.v1","architecture":"x64","target":"linux/amd64","builder_image":"gcc:14-bookworm@sha256:5e927c284bf55a7dc796262e311a0703344f62f41f5621eb56843111b1d37e15","compiler":"gcc-14","source_sha256":"sha256:5eadf15faeccd5fec14f03e701c1b7001d7f4058941987070bf5e512e9984e5e","binary_sha256":"sha256:51dc67387cd0c9dbbbc36b577b5c98ea296ecaf3f2f015be52eb4df98affcbbc","install_path":"/opt/daimon/bin/daimon-engine-broker"} From f41774ed9194761483cacb68f36c4ddc37b12e53 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:04:00 +0200 Subject: [PATCH 09/26] fix: pass the Grok provider capability through env_key because 1.0.34 never runs auth_provider helpers --- scripts/liveGrokBrokerSession.ts | 8 +++---- src/contracts/runtimeContractManifest.ts | 6 ++--- src/runtime/grokBrokerWorkerConfig.test.ts | 14 ++++++----- src/runtime/grokBrokerWorkerConfig.ts | 23 ++++++++++++++----- .../native/engineBrokerLauncherCore.inc | 22 +++++++++++++++--- src/runtime/native/fixtureWorker.c | 2 +- src/runtime/native/launcherArgv.test.ts | 9 +++++++- 7 files changed, 59 insertions(+), 25 deletions(-) diff --git a/scripts/liveGrokBrokerSession.ts b/scripts/liveGrokBrokerSession.ts index 0e0663e..05cf30d 100644 --- a/scripts/liveGrokBrokerSession.ts +++ b/scripts/liveGrokBrokerSession.ts @@ -10,7 +10,7 @@ import { decodeGrokHeadlessTurn } from "../src/pi/grokHeadlessResult.ts"; import { readGrokBrokerCredential } from "../src/runtime/grokBrokerCredentialReader.ts"; import { startGrokBrokerProxy } from "../src/runtime/grokBrokerProxy.ts"; import { DEFAULT_GROK_BROKER_MODEL_POLICY } from "../src/runtime/grokBrokerModelPolicy.ts"; -import { renderGrokBrokerWorkerArgs, renderGrokBrokerWorkerConfigWith } from "../src/runtime/grokBrokerWorkerConfig.ts"; +import { GROK_BROKER_PROVIDER_CAPABILITY_ENV, renderGrokBrokerWorkerArgs, renderGrokBrokerWorkerConfigWith } from "../src/runtime/grokBrokerWorkerConfig.ts"; // Explicit live auth/transport check, not the Linux native worker/isolation E2E. // Read the operator credential only in this process; never stage or rotate it. @@ -38,10 +38,8 @@ try { const capability = proxy.capabilities.issue("local-auth-probe", turnId); // This local transport probe deliberately does not attest a native worker. proxy.registerIsolationGuard(turnId, async () => undefined); - const helper = path.join(home, "auth-helper"); - await writeFile(helper, `#!/bin/sh\nprintf '{"access_token":"${capability}","expires_in":600}\\n'\n`, { mode: 0o700 }); // No MCP tools are needed for this exact-reply authentication probe. - await writeFile(path.join(home, "config.toml"), renderGrokBrokerWorkerConfigWith(DEFAULT_GROK_BROKER_MODEL_POLICY, { helperPath: helper, proxyPort: proxy.port, mcpUrl: "http://127.0.0.1:43124/mcp" }).split("[mcp_servers.daimon]")[0]); + await writeFile(path.join(home, "config.toml"), renderGrokBrokerWorkerConfigWith(DEFAULT_GROK_BROKER_MODEL_POLICY, { proxyPort: proxy.port, mcpUrl: "http://127.0.0.1:43124/mcp" }).split("[mcp_servers.daimon]")[0]); const prompt = path.join(home, "prompt.txt"); await writeFile(prompt, `Reply exactly ${sentinel}. Do not use tools.`); stage = `model turn ${round}`; @@ -52,7 +50,7 @@ try { args[args.indexOf("--max-turns") + 1] = "1"; const child = trackCliChild(spawn("grok", args, { cwd: home, detached: process.platform !== "win32", - env: { PATH: process.env.PATH, HOME: home, GROK_HOME: home, LANG: "C", LC_ALL: "C", TZ: "UTC" }, + env: { PATH: process.env.PATH, HOME: home, GROK_HOME: home, LANG: "C", LC_ALL: "C", TZ: "UTC", [GROK_BROKER_PROVIDER_CAPABILITY_ENV]: capability }, stdio: ["ignore", "pipe", "pipe"], })); let output: string; diff --git a/src/contracts/runtimeContractManifest.ts b/src/contracts/runtimeContractManifest.ts index 0f4b786..f122193 100644 --- a/src/contracts/runtimeContractManifest.ts +++ b/src/contracts/runtimeContractManifest.ts @@ -53,9 +53,9 @@ export const GROK_ENGINE_BROKER = { systemPromptSha256: "2c31c0085a54a4efbf9c0cf0b8124c56e47f38691b7f0c7fa233a74abaa8ddf8", // sha256 of `renderGrokBrokerWorkerConfig({ model, reasoningEffort })`, the only accepted config.toml bytes. configSha256: { - "grok-4.6": { low: "e09c127363094a7cad560586d89e994a9e6117b9c548feaffb1b5b01705cf363", medium: "b90807d3c73651a1a3f0bf89eacb0c73360e7cfc34d10e0185a381a27b40a718", high: "045171e44ba44b09522589ba85770f7c09fb8cab3e3f76fbb88cb534dcc01018" }, - "grok-4.5": { low: "848df2f71d0a88cf1185728cd6e0b7e15238b3a7536bf1538467f8c4868b0647", medium: "3e84795e834cf7b5857c24070d9f91b951c9c5f806823dd29c92cf88635a9318", high: "792f90bb7ae5154d6e002419f5b308e2ea975fc55ae7c324952f928329238f93" }, - "grok-build": { low: "1be3438799f9023b6acdaec991c139133bc791877f86a8b139129ef4b7b8386c", medium: "cadef8a2fcca778b515fcc10bfa8ab2ed8425fa46f37dc3a64095b477beb914a", high: "cb3ef9f71eefa913517dc775e5d72c49cbf718cf6c43ccc33d55b22401ef2e2f" } + "grok-4.6": { low: "cddeac5f845fa44890679934ad840ca728f5ddcdb0ffdb34a60ed232ef0eda3f", medium: "5a986b87f3888e8b2ec1fe5f7e5361a96494b67bd66a8b9f39836c26f2d602da", high: "e023b566aa28074c544c2f44dad634eca5511a329a2030c2584574e7f419a75a" }, + "grok-4.5": { low: "e22e543e72ea409cfbb3c229ed3bd605252374d28922f6ab05c29f6d590108de", medium: "706a74cb8401b045604078f526b7afcaf292dbe85c8c26f20b533e828f8a3acc", high: "3357ca76540480f4f3f7dc7e4e05ac92df770104a7ecf6d0aa3cb29eb9b6d0b6" }, + "grok-build": { low: "905efbe13bda08f3aa3907cb0215a56c07553965f8d81242687f7c993652b294", medium: "56aab068913b69aac68862ab729462b943a31b9efb7ddadd1d1deebc0ff72714", high: "52d66731dcad01ca001243424e324b8c4921a17ab31fe6b7b869dc8473d2c3b6" } }, // Worker `GROK_HOME` layout the broker attests before every turn. The home and // its `sessions/` directory are root-owned, worker-group writable and sticky so diff --git a/src/runtime/grokBrokerWorkerConfig.test.ts b/src/runtime/grokBrokerWorkerConfig.test.ts index af63d80..f5ea166 100644 --- a/src/runtime/grokBrokerWorkerConfig.test.ts +++ b/src/runtime/grokBrokerWorkerConfig.test.ts @@ -13,10 +13,11 @@ const section = (config: string, header: string): string => { return config.slice(start, end === -1 ? undefined : end); }; -test("worker config uses only named in-memory auth, the fixed loopback proxy, and the capability-scoped MCP facade", () => { +test("worker config uses only the launcher-set turn capability, the fixed loopback proxy, and the capability-scoped MCP facade", () => { const config = renderGrokBrokerWorkerConfig(); - assert.match(section(config, "[auth_provider.daimon]"), /command = "\/opt\/daimon\/bin\/daimon-engine-broker"\nargs = \["--auth-provider"\]/u); - assert.match(section(config, "[model.daimon-broker-grok]"), /base_url = "http:\/\/127\.0\.0\.1:43123\/v1"\nauth_provider = "daimon"/u); + assert.match(section(config, "[model.daimon-broker-grok]"), /base_url = "http:\/\/127\.0\.0\.1:43123\/v1"\nenv_key = "DAIMON_PROVIDER_CAPABILITY"\n/u); + // Grok 1.0.34 ignores [auth_provider.*] for custom models; a helper table would silently send no bearer. + assert.doesNotMatch(config, /auth_provider/u); assert.equal(section(config, "[mcp_servers.daimon]"), '[mcp_servers.daimon]\nurl = "http://127.0.0.1:43124/mcp"\nbearer_token_env_var = "DAIMON_MCP_CAPABILITY"\n'); assert.doesNotMatch(config, /access_token|refresh_token|auth\.json/u); }); @@ -54,10 +55,11 @@ test("the manifest pins the sha256 of every renderable worker config", () => { } }); -test("the probe-only renderer refuses non-loopback endpoints and injected helper paths", () => { +test("the probe-only renderer refuses non-loopback or injected endpoints", () => { const policy = { model: "grok-4.6", reasoningEffort: "low" } as const; - assert.throws(() => renderGrokBrokerWorkerConfigWith(policy, { helperPath: "/x\"\n[evil]", proxyPort: 1, mcpUrl: "http://127.0.0.1:1/mcp" }), /invalid/u); - assert.throws(() => renderGrokBrokerWorkerConfigWith(policy, { helperPath: "/x", proxyPort: 1, mcpUrl: "http://example.com/mcp" }), /invalid/u); + assert.throws(() => renderGrokBrokerWorkerConfigWith(policy, { proxyPort: 0, mcpUrl: "http://127.0.0.1:1/mcp" }), /invalid/u); + assert.throws(() => renderGrokBrokerWorkerConfigWith(policy, { proxyPort: 1, mcpUrl: "http://example.com/mcp" }), /invalid/u); + assert.throws(() => renderGrokBrokerWorkerConfigWith(policy, { proxyPort: 1, mcpUrl: "http://127.0.0.1:1/mcp\"\n[evil]" }), /invalid/u); const args = renderGrokBrokerWorkerArgs("/run/worker/prompt", "/workspace"); assert.equal(args.includes("--prompt-file"), true); assert.equal(args.includes("--single"), false); }); diff --git a/src/runtime/grokBrokerWorkerConfig.ts b/src/runtime/grokBrokerWorkerConfig.ts index da08c87..e204b80 100644 --- a/src/runtime/grokBrokerWorkerConfig.ts +++ b/src/runtime/grokBrokerWorkerConfig.ts @@ -39,9 +39,21 @@ const renderSessionTitleSink = (): readonly string[] => [ "max_retries = 0", "hidden = true", "" ]; -type WorkerEndpoints =Readonly<{ helperPath: string; proxyPort: number; mcpUrl: string }>; +/** + * The turn-scoped proxy capability reaches the worker's only model through + * `env_key`, set by the native launcher. Grok 1.0.34 accepts + * `[auth_provider.]` tables but never runs the helper for a custom model + * (verified against a loopback stub: no helper invocation and no Authorization + * header, with and without `args`, `api_backend`, `model_providers`, or a + * passwd-home config), while `env_key` attaches the bearer on every request. + * The capability is exactly as exposed as `DAIMON_MCP_CAPABILITY`: visible to + * the worker's own tool children, which run network-restricted, and revoked + * when the turn ends. + */ +export const GROK_BROKER_PROVIDER_CAPABILITY_ENV = "DAIMON_PROVIDER_CAPABILITY" as const; + +type WorkerEndpoints = Readonly<{ proxyPort: number; mcpUrl: string }>; const PRODUCTION_ENDPOINTS: WorkerEndpoints = Object.freeze({ - helperPath: GROK_ENGINE_BROKER.nativeExecutablePath, proxyPort: GROK_ENGINE_BROKER.providerProxy.port, mcpUrl: `http://${GROK_ENGINE_BROKER.mcpFacade.host}:${GROK_ENGINE_BROKER.mcpFacade.port}${GROK_ENGINE_BROKER.mcpFacade.path}` }); @@ -81,8 +93,8 @@ export function renderGrokBrokerWorkerConfig(policy: Partial 65_535 || !/^http:\/\/127\.0\.0\.1:\d{1,5}\/mcp$/u.test(mcpUrl)) { + const { proxyPort, mcpUrl } = endpoints; + if (!Number.isInteger(proxyPort) || proxyPort < 1 || proxyPort > 65_535 || !/^http:\/\/127\.0\.0\.1:\d{1,5}\/mcp$/u.test(mcpUrl)) { throw new TypeError("invalid Grok broker worker configuration"); } const label = `${declared.reasoningEffort[0]!.toUpperCase()}${declared.reasoningEffort.slice(1)}`; @@ -90,8 +102,7 @@ export function renderGrokBrokerWorkerConfigWith(policy: GrokBrokerModelPolicy, renderGrokLeanBaseConfig(), "[models]", `default = "${GROK_BROKER_WORKER_MODEL_ID}"`, `default_reasoning_effort = "${declared.reasoningEffort}"`, `session_summary = "${GROK_SESSION_TITLE_SINK_MODEL_ID}"`, "", ...renderSessionTitleSink(), - "[auth_provider.daimon]", `command = ${JSON.stringify(helperPath)}`, 'args = ["--auth-provider"]', "timeout_secs = 5", "token_ttl_secs = 600", "", - `[model.${GROK_BROKER_WORKER_MODEL_ID}]`, `model = "${declared.model}"`, `base_url = "http://127.0.0.1:${proxyPort}/v1"`, 'auth_provider = "daimon"', + `[model.${GROK_BROKER_WORKER_MODEL_ID}]`, `model = "${declared.model}"`, `base_url = "http://127.0.0.1:${proxyPort}/v1"`, `env_key = "${GROK_BROKER_PROVIDER_CAPABILITY_ENV}"`, 'api_backend = "chat_completions"', "context_window = 131072", "supports_backend_search = false", "", `[[model.${GROK_BROKER_WORKER_MODEL_ID}.reasoning_efforts]]`, `value = "${declared.reasoningEffort}"`, `label = "${label}"`, "default = true", "", "[mcp_servers.daimon]", `url = "${mcpUrl}"`, 'bearer_token_env_var = "DAIMON_MCP_CAPABILITY"', "" diff --git a/src/runtime/native/engineBrokerLauncherCore.inc b/src/runtime/native/engineBrokerLauncherCore.inc index c093307..db3abdd 100644 --- a/src/runtime/native/engineBrokerLauncherCore.inc +++ b/src/runtime/native/engineBrokerLauncherCore.inc @@ -257,14 +257,19 @@ static __attribute__((noreturn)) void launch_fail(int status_fd, static pid_t launch(const struct dbl_registration *r, int executable, int prompt, int capability, int output, uint32_t *failure, uint64_t *observed_start_ticks) { - unsigned char mcp[DBL_MAX_TOKEN + 1] = {0}; + unsigned char provider[DBL_MAX_TOKEN + 1] = {0}, mcp[DBL_MAX_TOKEN + 1] = {0}; int status_pipe[2]; - if (capability_bundle(capability, NULL, mcp) || pipe2(status_pipe, O_CLOEXEC)) + if (capability_bundle(capability, provider, mcp) || + pipe2(status_pipe, O_CLOEXEC)) { + erase(provider, sizeof(provider)); + erase(mcp, sizeof(mcp)); return -1; + } pid_t p = fork(); if (p < 0) { close(status_pipe[0]); close(status_pipe[1]); + erase(provider, sizeof(provider)); erase(mcp, sizeof(mcp)); return -1; } @@ -275,6 +280,7 @@ static pid_t launch(const struct dbl_registration *r, int executable, sizeof(*observed_start_ticks)) || !*observed_start_ticks) { close(status_pipe[0]); + erase(provider, sizeof(provider)); erase(mcp, sizeof(mcp)); kill(p, SIGKILL); waitpid(p, NULL, 0); @@ -282,6 +288,7 @@ static pid_t launch(const struct dbl_registration *r, int executable, } ssize_t got = read(status_pipe[0], &code, sizeof(code)); close(status_pipe[0]); + erase(provider, sizeof(provider)); erase(mcp, sizeof(mcp)); if (got == 0) return p; @@ -349,20 +356,29 @@ static pid_t launch(const struct dbl_registration *r, int executable, "--model", "daimon-broker-grok", NULL}; - char home[300], grok[300], mcp_env[DBL_MAX_TOKEN + 24]; + char home[300], grok[300], mcp_env[DBL_MAX_TOKEN + 24], + provider_env[DBL_MAX_TOKEN + 32]; snprintf(home, sizeof(home), "HOME=%s", r->home); snprintf(grok, sizeof(grok), "GROK_HOME=%s/.grok", r->home); snprintf(mcp_env, sizeof(mcp_env), "DAIMON_MCP_CAPABILITY=%s", mcp); + /* Grok 1.0.34 never runs `[auth_provider.*]` helpers for a custom model, so + the worker's `[model.daimon-broker-grok] env_key` reads the turn-scoped + proxy capability from here. It is as exposed as the MCP capability. */ + snprintf(provider_env, sizeof(provider_env), "DAIMON_PROVIDER_CAPABILITY=%s", + provider); + erase(provider, sizeof(provider)); erase(mcp, sizeof(mcp)); char *const envp[] = {home, grok, mcp_env, + provider_env, "DAIMON_CAPABILITY_FD=4", "PATH=/usr/local/bin:/usr/bin:/bin", "LANG=C.UTF-8", "TZ=UTC", NULL}; syscall(SYS_execveat, 5, "", argv, envp, AT_EMPTY_PATH); + erase(provider_env, sizeof(provider_env)); erase(mcp_env, sizeof(mcp_env)); launch_fail(status_fd, 6); } diff --git a/src/runtime/native/fixtureWorker.c b/src/runtime/native/fixtureWorker.c index 63af5b8..f53b77a 100644 --- a/src/runtime/native/fixtureWorker.c +++ b/src/runtime/native/fixtureWorker.c @@ -5,4 +5,4 @@ #include #include #include "engineBrokerLauncher.h" -int main(int argc,char**argv){unsigned char bundle[8200]={0};ssize_t size=pread(4,bundle,sizeof(bundle),0);if(size<6)return 21;size_t offset=0;unsigned provider_length=bundle[0]|(bundle[1]<<8);offset=2;if(offset+provider_length+2>(size_t)size)return 21;char provider[4097]={0};memcpy(provider,bundle+offset,provider_length);offset+=provider_length;unsigned mcp_length=bundle[offset]|(bundle[offset+1]<<8);offset+=2;if(offset+mcp_length!=(size_t)size)return 21;char mcp[4097]={0};memcpy(mcp,bundle+offset,mcp_length);memset(bundle,0,sizeof(bundle));int exact=!strcmp(provider,"exact-output"),overflow=!strcmp(provider,"overflow-output");if(!strcmp(provider,"sleep"))sleep(30);if(!getenv("DAIMON_MCP_CAPABILITY")||strcmp(getenv("DAIMON_MCP_CAPABILITY"),mcp)||strstr(getenv("DAIMON_MCP_CAPABILITY"),provider))return 24;int output[2];if(pipe(output))return 22;pid_t child=fork();if(!child){dup2(output[1],1);close(output[0]);execl("/opt/daimon/bin/daimon-engine-broker","daimon-engine-broker","--auth-provider",NULL);_exit(127);}close(output[1]);char auth[8192]={0};ssize_t got=read(output[0],auth,sizeof(auth)-1);close(output[0]);int status;waitpid(child,&status,0);char expected[8192];snprintf(expected,sizeof(expected),"{\"access_token\":\"%s\",\"expires_in\":600}\n",provider);memset(provider,0,sizeof(provider));memset(mcp,0,sizeof(mcp));if(!WIFEXITED(status)||WEXITSTATUS(status)||got<1||strcmp(auth,expected))return 23;memset(auth,0,sizeof(auth));memset(expected,0,sizeof(expected));if(exact||overflow){size_t count=DBL_MAX_OUTPUT+(size_t)overflow;for(size_t i=0;i(size_t)size)return 21;char provider[4097]={0};memcpy(provider,bundle+offset,provider_length);offset+=provider_length;unsigned mcp_length=bundle[offset]|(bundle[offset+1]<<8);offset+=2;if(offset+mcp_length!=(size_t)size)return 21;char mcp[4097]={0};memcpy(mcp,bundle+offset,mcp_length);memset(bundle,0,sizeof(bundle));int exact=!strcmp(provider,"exact-output"),overflow=!strcmp(provider,"overflow-output");if(!strcmp(provider,"sleep"))sleep(30);if(!getenv("DAIMON_MCP_CAPABILITY")||strcmp(getenv("DAIMON_MCP_CAPABILITY"),mcp)||strstr(getenv("DAIMON_MCP_CAPABILITY"),provider))return 24;if(!getenv("DAIMON_PROVIDER_CAPABILITY")||strcmp(getenv("DAIMON_PROVIDER_CAPABILITY"),provider))return 28;int output[2];if(pipe(output))return 22;pid_t child=fork();if(!child){dup2(output[1],1);close(output[0]);execl("/opt/daimon/bin/daimon-engine-broker","daimon-engine-broker","--auth-provider",NULL);_exit(127);}close(output[1]);char auth[8192]={0};ssize_t got=read(output[0],auth,sizeof(auth)-1);close(output[0]);int status;waitpid(child,&status,0);char expected[8192];snprintf(expected,sizeof(expected),"{\"access_token\":\"%s\",\"expires_in\":600}\n",provider);memset(provider,0,sizeof(provider));memset(mcp,0,sizeof(mcp));if(!WIFEXITED(status)||WEXITSTATUS(status)||got<1||strcmp(auth,expected))return 23;memset(auth,0,sizeof(auth));memset(expected,0,sizeof(expected));if(exact||overflow){size_t count=DBL_MAX_OUTPUT+(size_t)overflow;for(size_t i=0;i readFileSync(new URL(`./${name}`, import.meta.url), "utf8"); const unquote = (literal: string): string => { @@ -53,3 +53,10 @@ test("the compiled system prompt is byte-identical to the contract prompt pinned assert.ok(DAIMON_GROK_SYSTEM_PROMPT.length >= 320 && DAIMON_GROK_SYSTEM_PROMPT.length <= 700, "roughly 80-150 tokens"); for (const tool of ["daimon__moltnet_read", "daimon__moltnet_send", "use_tool", "search_tool", "read_file"]) assert.ok(DAIMON_GROK_SYSTEM_PROMPT.includes(tool), tool); }); + +test("the launcher exports the turn provider capability under the env_key the worker config reads", () => { + const source = read("engineBrokerLauncherCore.inc"); + assert.match(source, new RegExp(`"${GROK_BROKER_PROVIDER_CAPABILITY_ENV}=%s"`, "u")); + assert.match(source, /char \*const envp\[\] = \{home,\s*grok,\s*mcp_env,\s*provider_env,/u); + assert.match(renderGrokBrokerWorkerConfig(), new RegExp(`\\nenv_key = "${GROK_BROKER_PROVIDER_CAPABILITY_ENV}"\\n`, "u")); +}); From 7f5b62b614fadf236065ca3a1c11d1e656938313 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:04:02 +0200 Subject: [PATCH 10/26] build: rebuild native engine broker artifacts with the provider capability env --- src/contracts/runtimeContractManifest.ts | 6 +++--- .../artifacts/daimon-engine-broker-arm64 | Bin 67456 -> 67456 bytes ...daimon-engine-broker-arm64.provenance.json | 2 +- .../native/artifacts/daimon-engine-broker-x64 | Bin 55392 -> 55392 bytes .../daimon-engine-broker-x64.provenance.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/contracts/runtimeContractManifest.ts b/src/contracts/runtimeContractManifest.ts index f122193..33ec1cd 100644 --- a/src/contracts/runtimeContractManifest.ts +++ b/src/contracts/runtimeContractManifest.ts @@ -69,9 +69,9 @@ export const GROK_ENGINE_BROKER = { }, bounds: { promptBytes: 65_536, capabilityBytes: 4_096, capabilityBundleBytes: 8_196, outputBytes: 65_536 }, artifacts: { - sourceSha256: "5eadf15faeccd5fec14f03e701c1b7001d7f4058941987070bf5e512e9984e5e", - x64Sha256: "51dc67387cd0c9dbbbc36b577b5c98ea296ecaf3f2f015be52eb4df98affcbbc", - arm64Sha256: "5821e547aa6e7c5682eaae1a3f6106138ccd5b5666909c6e135862a61b35cc47" + sourceSha256: "7b0a1da4add25abb2176bb5c882f247e03bf099f23fb295d72509253c7696beb", + x64Sha256: "e7028d17c0aff843c37dd9393f9c30c6218329bae64f8ddf907296edaf2db349", + arm64Sha256: "7a37683ebefc6d935260619d635bad919b7bdbfb552440e044449d909a8f8ce5" } } as const; export const AGY_SUBSCRIPTION_REALM = { diff --git a/src/runtime/native/artifacts/daimon-engine-broker-arm64 b/src/runtime/native/artifacts/daimon-engine-broker-arm64 index 2afa07296b6cb9e546701b836575a7752a43c5ad..b836e7380a5cae750f1c199877dfe3f7550f8d7c 100755 GIT binary patch delta 5460 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 delta 4884 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 F`hOgqlpFv6 delta 6067 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 diff --git a/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json b/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json index 405f166..5b2a269 100644 --- a/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json +++ b/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json @@ -1 +1 @@ -{"version":"daimon.engine-broker-native-build.v1","architecture":"x64","target":"linux/amd64","builder_image":"gcc:14-bookworm@sha256:5e927c284bf55a7dc796262e311a0703344f62f41f5621eb56843111b1d37e15","compiler":"gcc-14","source_sha256":"sha256:5eadf15faeccd5fec14f03e701c1b7001d7f4058941987070bf5e512e9984e5e","binary_sha256":"sha256:51dc67387cd0c9dbbbc36b577b5c98ea296ecaf3f2f015be52eb4df98affcbbc","install_path":"/opt/daimon/bin/daimon-engine-broker"} +{"version":"daimon.engine-broker-native-build.v1","architecture":"x64","target":"linux/amd64","builder_image":"gcc:14-bookworm@sha256:5e927c284bf55a7dc796262e311a0703344f62f41f5621eb56843111b1d37e15","compiler":"gcc-14","source_sha256":"sha256:7b0a1da4add25abb2176bb5c882f247e03bf099f23fb295d72509253c7696beb","binary_sha256":"sha256:e7028d17c0aff843c37dd9393f9c30c6218329bae64f8ddf907296edaf2db349","install_path":"/opt/daimon/bin/daimon-engine-broker"} From 4bd43d39e135173eb224fb8f3b682999f33dd62c Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:05:27 +0200 Subject: [PATCH 11/26] docs: document the Grok 1.0.34 lean worker contract and worker home layout --- docs/engines.md | 7 +++++ src/runtime/AGENTS.md | 54 ++++++++++++++++++++++++++++++------ src/runtime/native/AGENTS.md | 25 +++++++++++++++++ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/docs/engines.md b/docs/engines.md index 7512b3a..ba2b288 100644 --- a/docs/engines.md +++ b/docs/engines.md @@ -81,6 +81,13 @@ the broker owns refresh and stale-credential recovery. The runtime checks broker readiness before admitting Grok agents and verifies their sandbox policy before turns. The older credential-lease helper is not the production host path. +The broker worker is pinned to Grok CLI 1.0.34 and runs lean: a fixed Daimon +system prompt, six tools (`run_terminal_command`, `read_file`, `grep`, +`list_dir`, and the MCP meta-tools `search_tool`/`use_tool`), no bundled +skills, workflows, plan mode, subagents, memory or web search, and a declared +model and reasoning effort from a closed list (default `grok-4.6` at `low`). +The broker proxy refuses any request outside that shape before it spends. + AGY uses OS-native secure storage through one private D-Bus and Secret Service realm. Enroll it once with: diff --git a/src/runtime/AGENTS.md b/src/runtime/AGENTS.md index 93723c1..4ddaa1b 100644 --- a/src/runtime/AGENTS.md +++ b/src/runtime/AGENTS.md @@ -13,11 +13,44 @@ coordinate wakes. Every source file stays below 400 lines. Keep tests beside the contract they cover. -`grokBrokerProxyRequest.ts` preserves the worker CLI's bounded -`x-grok-client-version` and supplies its `grok-shell` client identity when -rebuilding provider headers. Dropping the version makes the subscription -provider reject an otherwise valid login with HTTP 426; never replace it with -a fabricated version or pass arbitrary worker headers through. +`grokBrokerProxyRequest.ts` preserves the worker CLI's `x-grok-client-version` +and supplies its `grok-shell` client identity when rebuilding provider headers. +Dropping the version makes the subscription provider reject an otherwise valid +login with HTTP 426; never replace it with a fabricated version or pass +arbitrary worker headers through. The version must equal the pinned +`GROK_ENGINE_BROKER.grokCliVersion` (1.0.34) exactly. + +The proxy is also the spend gate for the lean Grok worker. Before a bearer is +attached it refuses any body whose tool names are not exactly +`GROK_WORKER_VISIBLE_TOOLS` (Grok 1.0.34 turns an unmappable `--tools` entry +into its full 19-tool set, and its `session_title` request carries one forced +tool), and any body whose `model`/`reasoning_effort` differ from the declared +`grokBrokerModelPolicy.ts` policy (closed lists; default `grok-4.6`/`low`). The +model override header follows that declaration. + +`grokBrokerWorkerConfig.ts` is the only source of worker `config.toml` bytes; +the manifest pins the sha256 of every model/effort combination and the broker +refuses a turn whose worker config does not hash to the declared one. Three +1.0.34 facts shape it, each verified against a loopback stub model: +`[auth_provider.*]` helpers never run for a custom model, so the turn's proxy +capability reaches the model through `env_key = "DAIMON_PROVIDER_CAPABILITY"` +set by the native launcher (as exposed as `DAIMON_MCP_CAPABILITY`); the +per-turn `session_title` request cannot be disabled by any key, so +`[models] session_summary` points it at a hidden model on closed loopback port +9; and effort is only sent when the model declares it, so the declared effort is +the model's single `reasoning_efforts` entry. HTTP MCP needs CA certificates in +the image even for a loopback `http://` URL ("Failed to build HTTP client"). + +Worker `GROK_HOME` layout the deployment must provision (attested before every +turn by `grokWorkerHomeAttestation.ts`, recorded in `GROK_ENGINE_BROKER.worker.home`): +`$GROK_HOME` and `$GROK_HOME/sessions` `root: 1771`; `config.toml`, +`sandbox.toml`, `trusted_folders.toml` (empty), `managed_config.toml` (empty) +and `requirements.toml` (empty) `root:root 0444`; and +`sessions/sandbox-events.jsonl` `: 0640`. Grok 1.0.34 writes its +sandbox events there (the root `sandbox-events.jsonl` stays empty) and runs +every profile inside bubblewrap, where a non-empty `deny` list is enforced; +`grokWorkerSandboxProfile.ts` renders those profile bytes. A worker-uid process +can neither write, rename, nor unlink any of the root-owned files. `agySubscriptionRealm.ts` owns the one host-level private D-Bus/Secret Service realm, durable keyring lease, bounded unlock stdin, and cleanup. @@ -28,10 +61,13 @@ runtime-writable home without clobbering a newer CLI-refreshed credential. `grokSubscriptionRealm.ts` owns the single durable rotating Grok credential, the lifetime lease, crash journal, stale fence, and serialized per-turn stage/promote cycle while each agent retains private non-auth home state. -`../pi/grokSandbox.ts` owns the production Grok process boundary: it replaces -the provider's fail-open built-in profile with an exact custom profile denying -the realm, bootstrap, and peer roots, and requires a kernel-enforcement event -before every Grok setup, turn, and cleanup process. +`../pi/grokSandbox.ts` owns the direct (non-broker) Grok process boundary: it +replaces the provider's fail-open built-in profile with an exact custom profile +denying the realm, bootstrap, and peer roots, and requires a kernel-enforcement +event (read from `$GROK_HOME/sessions/sandbox-events.jsonl`) before every Grok +turn. The direct path registers its per-wake MCP endpoint in the agent's +Daimon-owned `GROK_HOME` config (`../pi/grokHomeMcpRegistration.ts`), because +1.0.34 skips project-scoped MCP servers in untrusted workspaces. Strict Codex uses its native permission profile only for model-run local commands: the profile denies current `.codex/auth.json`, current `.daimon-inbound`, `/proc`, `/run`, shared protected stores, and peer roots diff --git a/src/runtime/native/AGENTS.md b/src/runtime/native/AGENTS.md index f8e468b..3e7f8ae 100644 --- a/src/runtime/native/AGENTS.md +++ b/src/runtime/native/AGENTS.md @@ -11,3 +11,28 @@ forbidden. Prompt and scoped capability bytes cross as inherited sealed file descriptors, not protocol strings. Workers must start in a private process group with no-new-privileges, no capabilities, no core dump, and a parent-death signal. Unsupported platforms fail closed. + +The worker argv is one compiled constant: the lean Grok 1.0.34 flags, the +`DBL_GROK_SYSTEM_PROMPT` operating contract, the `--tools` allowlist and the +`--max-turns` backstop live in `engineBrokerLauncher.h`, mirrored byte-for-byte +by `src/contracts/grokWorkerContract.ts` and checked by `launcherArgv.test.ts`. +Model and reasoning effort are per-deployment and belong to the worker +`config.toml`, never to the argv. + +The launcher sets exactly two turn-scoped capabilities in the worker +environment: `DAIMON_MCP_CAPABILITY` and `DAIMON_PROVIDER_CAPABILITY` (Grok +1.0.34 ignores `[auth_provider.*]` helpers for custom models, so the worker +config reads the proxy capability through `env_key`). `--auth-provider` mode +remains for callers of the older contract. + +Received descriptors carry `MSG_CMSG_CLOEXEC` and can already occupy fds 3-5, +so `launch()` lifts prompt, capability, output, executable and status fds above +16 before `dup2`-ing them into place; a `dup2` onto itself keeps close-on-exec +and the prompt used to vanish at exec. `fixtureWorker.c` reads +`/proc/self/fd/3` so the integration suite fails if that regresses. + +The executable is re-hashed on every spawn (~58 ms for the 136 MB Grok binary). +Holding one verified descriptor and `execveat`-ing it would not make a replaced +binary unrunnable: Grok 1.0.34 re-executes itself inside bubblewrap by path +(`/usr/local/bin/grok`), so the image path's root ownership, not the launcher +descriptor, is what protects the sandboxed process. From 0e09c759fd83a01ec6b1614422b26cea66203796 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:05:49 +0200 Subject: [PATCH 12/26] docs: point the worker argv mirror comment at launcherArgv.test.ts --- src/runtime/grokBrokerWorkerConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/grokBrokerWorkerConfig.ts b/src/runtime/grokBrokerWorkerConfig.ts index e204b80..e4967ef 100644 --- a/src/runtime/grokBrokerWorkerConfig.ts +++ b/src/runtime/grokBrokerWorkerConfig.ts @@ -114,7 +114,7 @@ export const grokBrokerWorkerConfigSha256 = (policy: Partial { if (!path.posix.isAbsolute(promptFile) || !path.posix.isAbsolute(cwd)) throw new TypeError("invalid Grok broker worker path"); From 596a87da7e8227f3046fea6b3fc5a2143cce2349 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:07:02 +0200 Subject: [PATCH 13/26] test: give Grok direct-session tests a GROK_HOME and unpool proxy rejection requests --- src/pi/grokHeadlessResult.test.ts | 4 ++-- src/runtime/grokBrokerProxy.test.ts | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/pi/grokHeadlessResult.test.ts b/src/pi/grokHeadlessResult.test.ts index 01d309b..376162e 100644 --- a/src/pi/grokHeadlessResult.test.ts +++ b/src/pi/grokHeadlessResult.test.ts @@ -77,7 +77,7 @@ test("exit-zero cancelled Grok sessions reject without emitting a turn", async ( ].join("\n")); try { const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [grok], engine: "grok", timeoutMs: 10_000 + command: process.execPath, commandArgs: [grok], engine: "grok", engineHomePath: path.join(root, ".grok"), timeoutMs: 10_000 })({ cwd: root }); let turns = 0; session.subscribe((event) => { if (event.type === "turn_end") turns += 1; }); @@ -108,7 +108,7 @@ test("successful Grok sessions emit only decoded terminal text", async () => { ].join("\n")); try { const { session } = await createCliSessionFactory({ - command: process.execPath, commandArgs: [grok], engine: "grok", timeoutMs: 10_000 + command: process.execPath, commandArgs: [grok], engine: "grok", engineHomePath: path.join(root, ".grok"), timeoutMs: 10_000 })({ cwd: root }); let reply = ""; session.subscribe((event) => { diff --git a/src/runtime/grokBrokerProxy.test.ts b/src/runtime/grokBrokerProxy.test.ts index 0d09eb8..3293903 100644 --- a/src/runtime/grokBrokerProxy.test.ts +++ b/src/runtime/grokBrokerProxy.test.ts @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { request as httpRequest } from "node:http"; import test from "node:test"; import { startGrokBrokerProxy } from "./grokBrokerProxy.js"; @@ -36,11 +37,18 @@ test("proxy refuses a fail-open tool set or an undeclared effort without calling const token = proxy.capabilities.issue("agent", "turn"); proxy.registerIsolationGuard("turn", async () => undefined); const full = [...lean, ...["search_replace", "todo_write", "write", "monitor"].map((name) => ({ type: "function", function: { name } }))]; for (const payload of [leanBody({ tools: full }), leanBody({ tools: [{ type: "function", function: { name: "session_title" } }] }), leanBody({ reasoning_effort: "high" }), leanBody({ reasoning_effort: undefined })]) { - const response = await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${token}`, "x-grok-client-version": "1.0.34" }, body: payload }); - assert.equal(response.status, 503); await response.text(); + assert.equal(await post(proxy.port, token, payload), 503); } assert.equal(calls, 0); - const accepted = await fetch(`http://127.0.0.1:${proxy.port}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${token}`, "x-grok-client-version": "1.0.34" }, body: leanBody() }); - assert.equal(accepted.status, 200); await accepted.text(); assert.equal(calls, 1); assert.ok(accessed >= 1); + assert.equal(await post(proxy.port, token, leanBody()), 200); assert.equal(calls, 1); assert.ok(accessed >= 1); } finally { await proxy.close(); } }); + +// One unpooled connection per request: the proxy listens on a fixed port that the +// previous test just closed, and a pooled keep-alive socket to it would be stale. +function post(port: number, token: string, body: string): Promise { + return new Promise((resolve, reject) => { + const req = httpRequest({ host: "127.0.0.1", port, path: "/v1/chat/completions", method: "POST", agent: false, headers: { authorization: `Bearer ${token}`, "x-grok-client-version": "1.0.34", "content-type": "application/json" } }, (response) => { response.resume(); response.on("end", () => resolve(response.statusCode ?? 0)); }); + req.on("error", reject); req.end(body); + }); +} From 18c26530d7170a65262d34b45bee98287525b71b Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:30:01 +0200 Subject: [PATCH 14/26] fix: escape control characters in the sandbox deny-path regex and forbid raw control bytes in sources --- scripts/scriptSourcePolicy.test.ts | 34 +++++++++++++++++++++++- src/runtime/grokWorkerSandboxProfile.ts | Bin 2154 -> 2169 bytes 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/scripts/scriptSourcePolicy.test.ts b/scripts/scriptSourcePolicy.test.ts index 1a73f68..bea5b23 100644 --- a/scripts/scriptSourcePolicy.test.ts +++ b/scripts/scriptSourcePolicy.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; @@ -37,3 +37,35 @@ test("script source policy detects a maintained JavaScript regression", async () await rm(root, { force: true, recursive: true }); } }); + +// Raw control bytes make review tooling classify a source file as binary and skip it. +// Escape them (`\u0000`) instead; tab, newline and carriage return are the only exceptions. +const RAW_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u; +const textSourcesWithRawControlBytes = async (roots: string[]): Promise => { + const results: string[] = []; + const walk = async (directory: string): Promise => { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { if (entry.name !== "artifacts" && entry.name !== "node_modules") await walk(entryPath); continue; } + if (!/\.(?:ts|mts|mjs|js|json|jsonl|md|c|h|inc|toml|sh|yml|yaml)$/u.test(entry.name)) continue; + if (RAW_CONTROL.test(await readFile(entryPath, "latin1"))) results.push(entryPath); + } + }; + await Promise.all(roots.map(walk)); + return results.sort(); +}; + +test("maintained text sources contain no raw control bytes", async () => { + assert.deepEqual(await textSourcesWithRawControlBytes(["src", "scripts", "docs"]), []); +}); + +test("raw control byte policy detects a regression", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "daimon-control-policy-")); + try { + await writeFile(path.join(root, "regex.ts"), `export const r = /[${String.fromCharCode(0)}-${String.fromCharCode(0x1f)}]/u;\n`); + await writeFile(path.join(root, "clean.ts"), "export const r = /[\\u0000-\\u001f]/u;\n"); + assert.deepEqual(await textSourcesWithRawControlBytes([root]), [path.join(root, "regex.ts")]); + } finally { + await rm(root, { force: true, recursive: true }); + } +}); diff --git a/src/runtime/grokWorkerSandboxProfile.ts b/src/runtime/grokWorkerSandboxProfile.ts index 423fe217979515e49fe5b61d832766f700d5b48b..b6ff1263af9c25dff9e796ca661f1d12bc563305 100644 GIT binary patch delta 32 hcmaDQ@Kazz4y$lXsR0n^f+)i@5NV#axq$T-69BdR3a|hG delta 17 Ycmew<@Je7q4l4_Tu6+IGde&b|06R(sCjbBd From 838b5075e9d473a82abb73a2dc422a06b4ad8bf4 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:30:31 +0200 Subject: [PATCH 15/26] fix: forward the validated Grok request object and refuse unknown top-level members --- src/runtime/grokBrokerProxyRequest.test.ts | 22 ++++++++++++++++++++++ src/runtime/grokBrokerProxyRequest.ts | 11 ++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/runtime/grokBrokerProxyRequest.test.ts b/src/runtime/grokBrokerProxyRequest.test.ts index fb4752c..576ff0a 100644 --- a/src/runtime/grokBrokerProxyRequest.test.ts +++ b/src/runtime/grokBrokerProxyRequest.test.ts @@ -49,3 +49,25 @@ test("proxy refuses a reasoning effort or model other than the declared policy", const other = request(leanBody()); assert.throws(() => authorizeGrokBrokerProxyRequest(other.input, other.caps, "real-bearer", { model: "grok-3" as never, reasoningEffort: "low" }), /model policy/); }); + +test("proxy forwards exactly the validated object, so duplicate members cannot smuggle a different tool set upstream", () => { + const full = [...leanTools, "search_replace", "kill_command_or_subagent", "todo_write", "get_command_or_subagent_output", "spawn_subagent", "scheduler_create", "scheduler_delete", "scheduler_list", "monitor", "workflow", "enter_plan_mode", "exit_plan_mode", "write"]; + const lean = leanTools.map(tool); + // First `tools`/`reasoning_effort`/`model` are the fail-open values; JSON.parse keeps the last (lean) ones. + const smuggled = `{"model":"grok-build","reasoning_effort":"high","tools":${JSON.stringify(full.map(tool))},"model":"grok-4.6","reasoning_effort":"low","stream":true,"messages":[],"stream_options":{"include_usage":true},"tools":${JSON.stringify(lean)}}`; + const { caps, input } = request(Buffer.from(smuggled)); + const upstream = authorizeGrokBrokerProxyRequest(input, caps, "real-bearer"); + const canonical = JSON.stringify({ model: "grok-4.6", reasoning_effort: "low", tools: lean, stream: true, messages: [], stream_options: { include_usage: true } }); + assert.equal(Buffer.from(upstream.body).toString("utf8"), canonical); + assert.equal(Buffer.from(upstream.body).toString("utf8").split('"tools"').length, 2); + assert.doesNotMatch(Buffer.from(upstream.body).toString("utf8"), /search_replace|grok-build|"high"/u); +}); + +test("proxy refuses top-level members a lean Grok 1.0.34 worker never sends", () => { + for (const overrides of [{ functions: [{ name: "write" }] }, { n: 2 }, { tool_choice: "required" }, { max_tokens: 100 }, { temperature: 0 }, { stream_options: { include_usage: true, extra: 1 } }, { stream_options: "yes" }]) { + const { caps, input } = request(leanBody(overrides)); + assert.throws(() => authorizeGrokBrokerProxyRequest(input, caps, "real-bearer"), /rejected/, JSON.stringify(overrides)); + } + const { caps, input } = request(leanBody({ stream_options: { include_usage: true } })); + assert.equal(Buffer.from(authorizeGrokBrokerProxyRequest(input, caps, "real-bearer").body).toString("utf8"), Buffer.from(leanBody({ stream_options: { include_usage: true } })).toString("utf8")); +}); diff --git a/src/runtime/grokBrokerProxyRequest.ts b/src/runtime/grokBrokerProxyRequest.ts index 309ad7e..24dcbc6 100644 --- a/src/runtime/grokBrokerProxyRequest.ts +++ b/src/runtime/grokBrokerProxyRequest.ts @@ -34,9 +34,18 @@ export function authorizeGrokBrokerProxyRequest(input: GrokBrokerProxyInput, cap if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || parsed.stream !== true || !Array.isArray(parsed.messages)) throw new Error("broker proxy request rejected"); if (parsed.model !== declared.model || parsed.reasoning_effort !== declared.reasoningEffort) throw new Error("broker proxy request rejected"); if (!exactLeanTools(parsed.tools)) throw new Error("broker proxy request rejected"); - return { url: "https://cli-chat-proxy.grok.com/v1/chat/completions", headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json", "x-xai-token-auth": "xai-grok-cli", "x-grok-model-override": declared.model, "x-grok-client-version": clientVersion, "x-grok-client-identifier": "grok-shell" }, body: input.body }; + if (Object.keys(parsed).some((key) => !LEAN_BODY_MEMBERS.has(key)) || !validStreamOptions(parsed.stream_options)) throw new Error("broker proxy request rejected"); + // Forward what was validated, never the worker's bytes: JSON.parse keeps the + // last of duplicate keys, and an upstream that keeps the first would + // otherwise see a different `tools`/`model`/`reasoning_effort` than the gate. + return { url: "https://cli-chat-proxy.grok.com/v1/chat/completions", headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json", "x-xai-token-auth": "xai-grok-cli", "x-grok-model-override": declared.model, "x-grok-client-version": clientVersion, "x-grok-client-identifier": "grok-shell" }, body: Buffer.from(JSON.stringify(parsed)) }; } +/** Top-level members of a Grok 1.0.34 lean worker chat-completions body (live stub capture). */ +const LEAN_BODY_MEMBERS: ReadonlySet = new Set(["messages", "model", "reasoning_effort", "stream", "stream_options", "tools"]); +const validStreamOptions = (value: unknown): boolean => value === undefined + || (value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every((key) => key === "include_usage") && typeof (value as { include_usage?: unknown }).include_usage === "boolean"); + export function exactLeanTools(tools: unknown): boolean { if (!Array.isArray(tools) || tools.length !== GROK_WORKER_VISIBLE_TOOLS.length) return false; const names = tools.map((tool) => { From 5fe9374ee8b04cd52366666a4a65f87a5f35678a Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:34:00 +0200 Subject: [PATCH 16/26] fix: send the Grok session-title request to the broker proxy where it is refused before any spend --- src/contracts/runtimeContractManifest.ts | 6 +++--- src/runtime/grokBrokerProxy.test.ts | 15 +++++++++++++++ src/runtime/grokBrokerWorkerConfig.test.ts | 4 +++- src/runtime/grokBrokerWorkerConfig.ts | 20 ++++++++++++-------- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/contracts/runtimeContractManifest.ts b/src/contracts/runtimeContractManifest.ts index 33ec1cd..55eabfb 100644 --- a/src/contracts/runtimeContractManifest.ts +++ b/src/contracts/runtimeContractManifest.ts @@ -53,9 +53,9 @@ export const GROK_ENGINE_BROKER = { systemPromptSha256: "2c31c0085a54a4efbf9c0cf0b8124c56e47f38691b7f0c7fa233a74abaa8ddf8", // sha256 of `renderGrokBrokerWorkerConfig({ model, reasoningEffort })`, the only accepted config.toml bytes. configSha256: { - "grok-4.6": { low: "cddeac5f845fa44890679934ad840ca728f5ddcdb0ffdb34a60ed232ef0eda3f", medium: "5a986b87f3888e8b2ec1fe5f7e5361a96494b67bd66a8b9f39836c26f2d602da", high: "e023b566aa28074c544c2f44dad634eca5511a329a2030c2584574e7f419a75a" }, - "grok-4.5": { low: "e22e543e72ea409cfbb3c229ed3bd605252374d28922f6ab05c29f6d590108de", medium: "706a74cb8401b045604078f526b7afcaf292dbe85c8c26f20b533e828f8a3acc", high: "3357ca76540480f4f3f7dc7e4e05ac92df770104a7ecf6d0aa3cb29eb9b6d0b6" }, - "grok-build": { low: "905efbe13bda08f3aa3907cb0215a56c07553965f8d81242687f7c993652b294", medium: "56aab068913b69aac68862ab729462b943a31b9efb7ddadd1d1deebc0ff72714", high: "52d66731dcad01ca001243424e324b8c4921a17ab31fe6b7b869dc8473d2c3b6" } + "grok-4.6": { low: "eed6a451150a72b2cb528b30c23b3d51c7d3bc38c67a8985d4dcdf956ff214d3", medium: "8850502dbebf8918c5161c63efcc4ccf18719488300f4cec1deceb2c112b451f", high: "3ce44ace503362326b47149b528b942ce638fe146313d62502f248acf9c7333d" }, + "grok-4.5": { low: "7aa13e90b9bc08d1a018f48b7a84de1dab41db586627ee2d5a25f69011ba7e25", medium: "218ba37e57a6f02fa36b265b4e154e68e30bd2d4794feb130cc226fdda7732a9", high: "0bb4ad8bfa5062169b28422d1d534b45420d4e46b1e546bda1c578eb34303646" }, + "grok-build": { low: "83ac7202442286a65c359cc596b0b8db7bc4529ee70e98224f6cd6f66deb6878", medium: "0146313f28739888eb4e861f1bfb285f7ee4e0a9164669256ebdf6492a2790ce", high: "bbe72aaf70c417dc7007823a7e9e1a7d1fa8d57e50bde6f24036083b32bcc859" } }, // Worker `GROK_HOME` layout the broker attests before every turn. The home and // its `sessions/` directory are root-owned, worker-group writable and sticky so diff --git a/src/runtime/grokBrokerProxy.test.ts b/src/runtime/grokBrokerProxy.test.ts index 3293903..0a935d5 100644 --- a/src/runtime/grokBrokerProxy.test.ts +++ b/src/runtime/grokBrokerProxy.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { request as httpRequest } from "node:http"; import test from "node:test"; import { startGrokBrokerProxy } from "./grokBrokerProxy.js"; +import { GROK_SESSION_TITLE_SINK_KEY } from "./grokBrokerWorkerConfig.js"; const lean = ["run_terminal_command", "read_file", "list_dir", "grep", "search_tool", "use_tool"].map((name) => ({ type: "function", function: { name } })); const leanBody = (overrides: Record = {}): string => JSON.stringify({ model: "grok-4.6", reasoning_effort: "low", stream: true, messages: [], tools: lean, ...overrides }); @@ -52,3 +53,17 @@ function post(port: number, token: string, body: string): Promise { req.on("error", reject); req.end(body); }); } + +test("the session-title sink is refused before capability, guard, credential, or upstream use", async () => { + let calls = 0, accessed = 0, guarded = 0; + const proxy = await startGrokBrokerProxy({ accessToken: async () => { accessed++; return "provider-token"; }, markRejected: async () => undefined }, async () => { calls++; return { status: 200, headers: { "content-type": "application/json" }, body: Buffer.from("{}") }; }); + try { + const token = proxy.capabilities.issue("agent", "turn", 60_000, 1); proxy.registerIsolationGuard("turn", async () => { guarded++; }); + const title = JSON.stringify({ model: "disabled", max_tokens: 100, temperature: 0, stream: true, messages: [{ role: "user", content: "prompt-derived" }], tool_choice: { type: "function", function: { name: "session_title" } }, tools: [{ type: "function", function: { name: "session_title" } }] }); + assert.equal(await post(proxy.port, GROK_SESSION_TITLE_SINK_KEY, title), 503); + assert.deepEqual({ calls, accessed, guarded }, { calls: 0, accessed: 0, guarded: 0 }); + // The turn capability (budget 1 request) is untouched and still serves the real request. + assert.equal(await post(proxy.port, token, leanBody()), 200); + assert.equal(calls, 1); + } finally { await proxy.close(); } +}); diff --git a/src/runtime/grokBrokerWorkerConfig.test.ts b/src/runtime/grokBrokerWorkerConfig.test.ts index f5ea166..091418a 100644 --- a/src/runtime/grokBrokerWorkerConfig.test.ts +++ b/src/runtime/grokBrokerWorkerConfig.test.ts @@ -28,7 +28,9 @@ test("worker config disables every bundled 1.0.34 skill, workflows, and the per- assert.equal(section(config, "[skills]"), `[skills]\ndisabled = [${GROK_1_0_34_BUNDLED_SKILLS.map((name) => JSON.stringify(name)).join(", ")}]\n`); assert.equal(section(config, "[workflows]"), "[workflows]\nenabled = false\n"); assert.match(section(config, "[models]"), /\nsession_summary = "daimon-session-title-disabled"\n/u); - assert.equal(section(config, "[model.daimon-session-title-disabled]"), '[model.daimon-session-title-disabled]\nmodel = "disabled"\nbase_url = "http://127.0.0.1:9/v1"\napi_key = "session-title-disabled"\nmax_retries = 0\nhidden = true\n'); + assert.equal(section(config, "[model.daimon-session-title-disabled]"), '[model.daimon-session-title-disabled]\nmodel = "disabled"\nbase_url = "http://127.0.0.1:43123/v1"\napi_key = "session-title-disabled"\nmax_retries = 0\nhidden = true\n'); + // The sink carries a static placeholder key only: no env_key, so it can never pick up the turn capability. + assert.doesNotMatch(section(config, "[model.daimon-session-title-disabled]"), /env_key|DAIMON_/u); for (const toggle of ["title_refresh", "telemetry", "session_recap", "turn_summary", "backend_tools", "ask_user_question"]) assert.match(section(config, "[features]"), new RegExp(`\\n${toggle} = false\\n`, "u")); assert.match(section(config, "[cli]"), /auto_update = false\nuse_leader = false/u); }); diff --git a/src/runtime/grokBrokerWorkerConfig.ts b/src/runtime/grokBrokerWorkerConfig.ts index e4967ef..5a2f88c 100644 --- a/src/runtime/grokBrokerWorkerConfig.ts +++ b/src/runtime/grokBrokerWorkerConfig.ts @@ -27,15 +27,19 @@ export const GROK_BROKER_WORKER_MODEL_ID = "daimon-broker-grok" as const; * turn, and no config key or environment variable disables it * (`features.title_refresh` governs only the later refresh; verified against a * loopback stub). `[models] session_summary` does select the model it uses, so - * the title goes to a hidden model whose endpoint is a closed privileged - * loopback port: the connection is refused locally, Grok falls back to the - * truncated prompt as the title, and neither the proxy nor the provider sees a - * request. The placeholder `api_key` is not a credential; it only stops Grok - * from looking for one. + * the title goes to a hidden model whose endpoint is the broker's own provider + * proxy with a placeholder key that can never be a turn capability (shorter + * than the 40-character capability alphabet). The proxy refuses it before any + * capability lookup, isolation guard, credential read, or upstream call, and + * Grok falls back to the truncated prompt as the title. The endpoint is always + * listening while a worker runs, so the refusal is bounded by one loopback + * round trip rather than by a connect timeout, and a prompt-derived title is + * never delivered anywhere but Daimon's own proxy. */ export const GROK_SESSION_TITLE_SINK_MODEL_ID = "daimon-session-title-disabled" as const; -const renderSessionTitleSink = (): readonly string[] => [ - `[model.${GROK_SESSION_TITLE_SINK_MODEL_ID}]`, 'model = "disabled"', 'base_url = "http://127.0.0.1:9/v1"', 'api_key = "session-title-disabled"', +export const GROK_SESSION_TITLE_SINK_KEY = "session-title-disabled" as const; +const renderSessionTitleSink = (proxyPort: number): readonly string[] => [ + `[model.${GROK_SESSION_TITLE_SINK_MODEL_ID}]`, 'model = "disabled"', `base_url = "http://127.0.0.1:${proxyPort}/v1"`, `api_key = "${GROK_SESSION_TITLE_SINK_KEY}"`, "max_retries = 0", "hidden = true", "" ]; @@ -101,7 +105,7 @@ export function renderGrokBrokerWorkerConfigWith(policy: GrokBrokerModelPolicy, return [ renderGrokLeanBaseConfig(), "[models]", `default = "${GROK_BROKER_WORKER_MODEL_ID}"`, `default_reasoning_effort = "${declared.reasoningEffort}"`, `session_summary = "${GROK_SESSION_TITLE_SINK_MODEL_ID}"`, "", - ...renderSessionTitleSink(), + ...renderSessionTitleSink(proxyPort), `[model.${GROK_BROKER_WORKER_MODEL_ID}]`, `model = "${declared.model}"`, `base_url = "http://127.0.0.1:${proxyPort}/v1"`, `env_key = "${GROK_BROKER_PROVIDER_CAPABILITY_ENV}"`, 'api_backend = "chat_completions"', "context_window = 131072", "supports_backend_search = false", "", `[[model.${GROK_BROKER_WORKER_MODEL_ID}.reasoning_efforts]]`, `value = "${declared.reasoningEffort}"`, `label = "${label}"`, "default = true", "", From 08ceda6cb93e0e7cdc7f15d154395643868bdb4b Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:34:00 +0200 Subject: [PATCH 17/26] fix: give Grok workers /dev/null stdin and keep the executable fd out of the worker --- src/runtime/native/engineBrokerLauncherCore.inc | 11 ++++++++--- .../engineBrokerLauncherIntegrationLauncher.inc | 2 +- .../native/engineBrokerLauncherIntegrationMain.inc | 7 ++++++- src/runtime/native/fixtureWorker.c | 3 ++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/runtime/native/engineBrokerLauncherCore.inc b/src/runtime/native/engineBrokerLauncherCore.inc index db3abdd..cb2b9ec 100644 --- a/src/runtime/native/engineBrokerLauncherCore.inc +++ b/src/runtime/native/engineBrokerLauncherCore.inc @@ -320,16 +320,21 @@ static pid_t launch(const struct dbl_registration *r, int executable, int status_fd = fcntl(status_pipe[1], F_DUPFD_CLOEXEC, 16); if (status_fd < 0) launch_fail(status_pipe[1], 4); + int null_input = open("/dev/null", O_RDONLY | O_CLOEXEC); int high_prompt = fcntl(prompt, F_DUPFD_CLOEXEC, 16), high_capability = fcntl(capability, F_DUPFD_CLOEXEC, 16), high_output = fcntl(output, F_DUPFD_CLOEXEC, 16), high_executable = fcntl(executable, F_DUPFD_CLOEXEC, 16); - if (high_prompt < 0 || high_capability < 0 || high_output < 0 || - high_executable < 0 || dup2(high_prompt, 3) < 0 || + if (null_input < 0 || high_prompt < 0 || high_capability < 0 || + high_output < 0 || high_executable < 0 || + dup2(null_input, STDIN_FILENO) < 0 || dup2(high_prompt, 3) < 0 || dup2(high_capability, 4) < 0 || dup2(high_output, STDOUT_FILENO) < 0 || dup2(high_output, STDERR_FILENO) < 0) launch_fail(status_fd, 4); - if (dup2(high_executable, 5) < 0) + /* The worker needs fd 5 only to be executed: close-on-exec keeps the Grok + image descriptor out of the worker and every tool child (execveat with + AT_EMPTY_PATH still works for an ELF; only a #! script would lose it). */ + if (dup2(high_executable, 5) < 0 || fcntl(5, F_SETFD, FD_CLOEXEC) < 0) launch_fail(status_fd, 5); close_other_fds(status_fd); char *const argv[] = {"grok", diff --git a/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc b/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc index 20d6849..ee189d9 100644 --- a/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc +++ b/src/runtime/native/engineBrokerLauncherIntegrationLauncher.inc @@ -110,7 +110,7 @@ static void org_cases(void) { char *out = calloc(1, r.output_length + 1); check(read(s, out, r.output_length) == (ssize_t)r.output_length && strstr(out, "uid=2200") && strstr(out, "--always-approve") && - strstr(out, " prompt=prompt\n") && + strstr(out, " prompt=prompt fds=0,1,2,3,4 stdin=/dev/null\n") && !strstr(out, "EVIL"), "fixed worker boundary"); check(strstr(out, "=--verbatim\n") && strstr(out, "=--no-plan\n") && diff --git a/src/runtime/native/engineBrokerLauncherIntegrationMain.inc b/src/runtime/native/engineBrokerLauncherIntegrationMain.inc index 0b816dd..f5ea29b 100644 --- a/src/runtime/native/engineBrokerLauncherIntegrationMain.inc +++ b/src/runtime/native/engineBrokerLauncherIntegrationMain.inc @@ -43,8 +43,13 @@ int main(void) { auth_adversarial_cases(); puts("native-stage auth complete"); pid_t broker = fork(); - if (!broker) + if (!broker) { + /* A real launcher stdin, so a worker inheriting it is visible. */ + int launcher_stdin = open("/etc/hostname", O_RDONLY); + if (launcher_stdin < 0 || dup2(launcher_stdin, STDIN_FILENO) < 0) + _exit(1); execl("/opt/daimon/bin/daimon-engine-broker", "daimon-engine-broker", NULL); + } check(broker > 0, "broker fork"); wait_launcher_ready(broker); root_peer_rejects(); diff --git a/src/runtime/native/fixtureWorker.c b/src/runtime/native/fixtureWorker.c index f53b77a..b26a4ae 100644 --- a/src/runtime/native/fixtureWorker.c +++ b/src/runtime/native/fixtureWorker.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "engineBrokerLauncher.h" -int main(int argc,char**argv){unsigned char bundle[8200]={0};ssize_t size=pread(4,bundle,sizeof(bundle),0);if(size<6)return 21;size_t offset=0;unsigned provider_length=bundle[0]|(bundle[1]<<8);offset=2;if(offset+provider_length+2>(size_t)size)return 21;char provider[4097]={0};memcpy(provider,bundle+offset,provider_length);offset+=provider_length;unsigned mcp_length=bundle[offset]|(bundle[offset+1]<<8);offset+=2;if(offset+mcp_length!=(size_t)size)return 21;char mcp[4097]={0};memcpy(mcp,bundle+offset,mcp_length);memset(bundle,0,sizeof(bundle));int exact=!strcmp(provider,"exact-output"),overflow=!strcmp(provider,"overflow-output");if(!strcmp(provider,"sleep"))sleep(30);if(!getenv("DAIMON_MCP_CAPABILITY")||strcmp(getenv("DAIMON_MCP_CAPABILITY"),mcp)||strstr(getenv("DAIMON_MCP_CAPABILITY"),provider))return 24;if(!getenv("DAIMON_PROVIDER_CAPABILITY")||strcmp(getenv("DAIMON_PROVIDER_CAPABILITY"),provider))return 28;int output[2];if(pipe(output))return 22;pid_t child=fork();if(!child){dup2(output[1],1);close(output[0]);execl("/opt/daimon/bin/daimon-engine-broker","daimon-engine-broker","--auth-provider",NULL);_exit(127);}close(output[1]);char auth[8192]={0};ssize_t got=read(output[0],auth,sizeof(auth)-1);close(output[0]);int status;waitpid(child,&status,0);char expected[8192];snprintf(expected,sizeof(expected),"{\"access_token\":\"%s\",\"expires_in\":600}\n",provider);memset(provider,0,sizeof(provider));memset(mcp,0,sizeof(mcp));if(!WIFEXITED(status)||WEXITSTATUS(status)||got<1||strcmp(auth,expected))return 23;memset(auth,0,sizeof(auth));memset(expected,0,sizeof(expected));if(exact||overflow){size_t count=DBL_MAX_OUTPUT+(size_t)overflow;for(size_t i=0;i(size_t)size)return 21;char provider[4097]={0};memcpy(provider,bundle+offset,provider_length);offset+=provider_length;unsigned mcp_length=bundle[offset]|(bundle[offset+1]<<8);offset+=2;if(offset+mcp_length!=(size_t)size)return 21;char mcp[4097]={0};memcpy(mcp,bundle+offset,mcp_length);memset(bundle,0,sizeof(bundle));int exact=!strcmp(provider,"exact-output"),overflow=!strcmp(provider,"overflow-output");if(!strcmp(provider,"sleep"))sleep(30);if(!getenv("DAIMON_MCP_CAPABILITY")||strcmp(getenv("DAIMON_MCP_CAPABILITY"),mcp)||strstr(getenv("DAIMON_MCP_CAPABILITY"),provider))return 24;if(!getenv("DAIMON_PROVIDER_CAPABILITY")||strcmp(getenv("DAIMON_PROVIDER_CAPABILITY"),provider))return 28;int output[2];if(pipe(output))return 22;pid_t child=fork();if(!child){dup2(output[1],1);close(output[0]);execl("/opt/daimon/bin/daimon-engine-broker","daimon-engine-broker","--auth-provider",NULL);_exit(127);}close(output[1]);char auth[8192]={0};ssize_t got=read(output[0],auth,sizeof(auth)-1);close(output[0]);int status;waitpid(child,&status,0);char expected[8192];snprintf(expected,sizeof(expected),"{\"access_token\":\"%s\",\"expires_in\":600}\n",provider);memset(provider,0,sizeof(provider));memset(mcp,0,sizeof(mcp));if(!WIFEXITED(status)||WEXITSTATUS(status)||got<1||strcmp(auth,expected))return 23;memset(auth,0,sizeof(auth));memset(expected,0,sizeof(expected));if(exact||overflow){size_t count=DBL_MAX_OUTPUT+(size_t)overflow;for(size_t i=0;id_name[0]=='.')continue;int fd_number=atoi(fd_entry->d_name);if(fd_number==dirfd(fd_dir))continue;if(fd_number<0||fd_number>=64)return 30;fd_seen[fd_number]=1;}closedir(fd_dir);for(int fd_number=0;fd_number<64;fd_number++)if(fd_seen[fd_number])fds_used+=(size_t)snprintf(fds+fds_used,sizeof(fds)-fds_used,"%s%d",fds_used?",":"",fd_number);char stdin_target[64]={0};if(readlink("/proc/self/fd/0",stdin_target,sizeof(stdin_target)-1)<0)return 31;printf("uid=%ld argc=%d home=%s auth=ok mcp=ok prompt=%s fds=%s stdin=%s\n",(long)getuid(),argc,getenv("HOME"),prompt_bytes,fds,stdin_target);for(int i=0;i Date: Thu, 17 Sep 2026 05:34:13 +0200 Subject: [PATCH 18/26] build: rebuild native engine broker artifacts with the worker fd hardening --- src/contracts/runtimeContractManifest.ts | 6 +++--- .../artifacts/daimon-engine-broker-arm64 | Bin 67456 -> 67456 bytes ...daimon-engine-broker-arm64.provenance.json | 2 +- .../native/artifacts/daimon-engine-broker-x64 | Bin 55392 -> 55392 bytes .../daimon-engine-broker-x64.provenance.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/contracts/runtimeContractManifest.ts b/src/contracts/runtimeContractManifest.ts index 55eabfb..20ae6be 100644 --- a/src/contracts/runtimeContractManifest.ts +++ b/src/contracts/runtimeContractManifest.ts @@ -69,9 +69,9 @@ export const GROK_ENGINE_BROKER = { }, bounds: { promptBytes: 65_536, capabilityBytes: 4_096, capabilityBundleBytes: 8_196, outputBytes: 65_536 }, artifacts: { - sourceSha256: "7b0a1da4add25abb2176bb5c882f247e03bf099f23fb295d72509253c7696beb", - x64Sha256: "e7028d17c0aff843c37dd9393f9c30c6218329bae64f8ddf907296edaf2db349", - arm64Sha256: "7a37683ebefc6d935260619d635bad919b7bdbfb552440e044449d909a8f8ce5" + sourceSha256: "36f60689f0a8af0e3108f5f53d78ed52b7d4b6f934c75b6184606dfa82bc741e", + x64Sha256: "36dc76b134eb59cf5a6720b6f94228eb279108e20ea3343fa6efd9ffcb60a4d3", + arm64Sha256: "c93216cc6fa4ca50dc404fe41e68da9150a869b14f46eb42484ae77c3aa400a9" } } as const; export const AGY_SUBSCRIPTION_REALM = { diff --git a/src/runtime/native/artifacts/daimon-engine-broker-arm64 b/src/runtime/native/artifacts/daimon-engine-broker-arm64 index b836e7380a5cae750f1c199877dfe3f7550f8d7c..daeaf6e3c64540ced18ab7c60f8e232824e7ba86 100755 GIT binary patch delta 3362 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 delta 3288 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 Wtz-!$ALJ%hylj|ML<&B*G4B6`YxHpd diff --git a/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json b/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json index 5b2a269..f2bcf17 100644 --- a/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json +++ b/src/runtime/native/artifacts/daimon-engine-broker-x64.provenance.json @@ -1 +1 @@ -{"version":"daimon.engine-broker-native-build.v1","architecture":"x64","target":"linux/amd64","builder_image":"gcc:14-bookworm@sha256:5e927c284bf55a7dc796262e311a0703344f62f41f5621eb56843111b1d37e15","compiler":"gcc-14","source_sha256":"sha256:7b0a1da4add25abb2176bb5c882f247e03bf099f23fb295d72509253c7696beb","binary_sha256":"sha256:e7028d17c0aff843c37dd9393f9c30c6218329bae64f8ddf907296edaf2db349","install_path":"/opt/daimon/bin/daimon-engine-broker"} +{"version":"daimon.engine-broker-native-build.v1","architecture":"x64","target":"linux/amd64","builder_image":"gcc:14-bookworm@sha256:5e927c284bf55a7dc796262e311a0703344f62f41f5621eb56843111b1d37e15","compiler":"gcc-14","source_sha256":"sha256:36f60689f0a8af0e3108f5f53d78ed52b7d4b6f934c75b6184606dfa82bc741e","binary_sha256":"sha256:36dc76b134eb59cf5a6720b6f94228eb279108e20ea3343fa6efd9ffcb60a4d3","install_path":"/opt/daimon/bin/daimon-engine-broker"} From 0878c976aa3d25081e39324f65f37f18e2481a5b Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:34:23 +0200 Subject: [PATCH 19/26] test: cover capability expiry on the proxy token lookups --- src/runtime/engineBrokerCapabilities.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/runtime/engineBrokerCapabilities.test.ts b/src/runtime/engineBrokerCapabilities.test.ts index af9835f..b29fc54 100644 --- a/src/runtime/engineBrokerCapabilities.test.ts +++ b/src/runtime/engineBrokerCapabilities.test.ts @@ -13,3 +13,11 @@ test("proxy resolves scope from opaque token without caller identity", () => { const capabilities = new EngineBrokerCapabilities(); const token = capabilities.issue("agent-a", "turn-a"); assert.deepEqual(capabilities.authorizeToken(token), { agentId: "agent-a", turnId: "turn-a" }); }); + +test("the live proxy lookups refuse an expired capability", async () => { + const capabilities = new EngineBrokerCapabilities(); const token = capabilities.issue("agent-a", "turn-a", 20, 64); + assert.deepEqual(capabilities.inspectToken(token), { agentId: "agent-a", turnId: "turn-a" }); + await new Promise((resolve) => setTimeout(resolve, 40)); + assert.equal(capabilities.inspectToken(token), undefined); + assert.equal(capabilities.authorizeToken(token), undefined); +}); From b1823ec974d4a59584b813a562b3c0f0135d45b9 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:50:41 +0200 Subject: [PATCH 20/26] fix: lock each Grok turn to the ProfileApplied event accepted before its first model request --- src/runtime/grokEngineBroker.ts | 6 +- src/runtime/grokWorkerAttestation.test.ts | 2 +- src/runtime/grokWorkerAttestation.ts | 73 +++++++++++++++--- src/runtime/grokWorkerIsolationGuard.test.ts | 79 ++++++++++++++++++++ 4 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 src/runtime/grokWorkerIsolationGuard.test.ts diff --git a/src/runtime/grokEngineBroker.ts b/src/runtime/grokEngineBroker.ts index bfcc3ec..98a8384 100644 --- a/src/runtime/grokEngineBroker.ts +++ b/src/runtime/grokEngineBroker.ts @@ -9,7 +9,7 @@ import { startEngineBrokerMcpFacade } from "./engineBrokerMcpFacade.js"; import { acquireGrokBrokerRealmLease } from "./grokBrokerRealmLease.js"; import { grokBrokerWorkerConfigSha256 } from "./grokBrokerWorkerConfig.js"; import { parseGrokBrokerModelPolicy, type GrokBrokerModelPolicy } from "./grokBrokerModelPolicy.js"; -import { GrokWorkerAttestationFailure,prepareGrokWorkerAttestation,verifyGrokWorkerAttestation } from "./grokWorkerAttestation.js"; +import { createGrokWorkerIsolationGuard,GrokWorkerAttestationFailure,prepareGrokWorkerAttestation } from "./grokWorkerAttestation.js"; export type GrokEngineBrokerRegistration = Readonly<{ agentId:string;slot:number;workerUid:number;workspace:string;profilePath:string;eventsPath:string;profileSha256:string }>; export type GrokEngineBroker = Awaited>; @@ -45,9 +45,9 @@ export async function startGrokEngineBroker(options: Readonly<{ grokCommand: str if (closed) throw new Error("engine broker unavailable"); const registration = registrations.get(agentId); if (registration === undefined) throw new Error("engine broker unavailable"); const turnId = createHash("sha256").update(`${agentId}\0${wakeId}`).digest("hex"); const request = { version: "noopolis.daimon.engine-broker.v1", kind: "start_turn", requestId: randomUUID(), turnId, agentId, wakeId, prompt,mcpEndpoint } as const; const begun = await turns.begin(request); if (begun !== "start") { if (begun.replay.kind === "completed") return {text:begun.replay.text,workerPid:begun.replay.workerPid,workerUid:begun.replay.workerUid,workerStartTime:begun.replay.workerStartTime};const code=begun.replay.code==="auth_stale"||begun.replay.code==="cancelled"?begun.replay.code:"engine_failed";throw new EngineBrokerTurnFailure(code,begun.replay.diagnostic as NativeBrokerDiagnostic|undefined); } - const attestation={...registration,brokerGid:2100,configSha256};const isolation=await prepareGrokWorkerAttestation(attestation);proxy.registerIsolationGuard(turnId,()=>verifyGrokWorkerAttestation(attestation,isolation));const providerCapability = proxy.capabilities.issue(agentId, turnId);const mcpCapability=mcp.register(agentId,turnId,mcpEndpoint); const controller = new AbortController();const onAbort=()=>controller.abort();signal?.addEventListener("abort",onAbort,{once:true});if(signal?.aborted)controller.abort(); let resolve!:()=>void;const done=new Promise((value)=>{resolve=value;});active.set(turnId,{controller,done,resolve}); + const attestation={...registration,brokerGid:2100,configSha256};const isolation=await prepareGrokWorkerAttestation(attestation);const isolationGuard=createGrokWorkerIsolationGuard(attestation,isolation);proxy.registerIsolationGuard(turnId,isolationGuard);const providerCapability = proxy.capabilities.issue(agentId, turnId);const mcpCapability=mcp.register(agentId,turnId,mcpEndpoint); const controller = new AbortController();const onAbort=()=>controller.abort();signal?.addEventListener("abort",onAbort,{once:true});if(signal?.aborted)controller.abort(); let resolve!:()=>void;const done=new Promise((value)=>{resolve=value;});active.set(turnId,{controller,done,resolve}); let nativeDiagnostic:NativeBrokerDiagnostic|undefined,attested=false; - try { const result = await runNativeBrokerTurn(options.nativeClient, { slot: registration.slot, requestId: request.requestId, turnId, agentId, wakeId, prompt, providerCapability,mcpCapability }, controller.signal);nativeDiagnostic=result.diagnostic;if(result.workerUid!==registration.workerUid)throw new Error();await verifyGrokWorkerAttestation(attestation,isolation);attested=true; const decoded = decodeGrokHeadlessTurn(result.text);const text = decoded.text;const completed={ version: request.version, kind: "completed", requestId: request.requestId, turnId, text, workerPid: result.workerPid, workerUid: result.workerUid, workerStartTime: result.startTicks.toString() } as const; await finishBrokerTurnWithUsage(turns,request,completed,usageLedgerPath,decoded.usage,agentId,wakeId); return {text,workerPid:result.workerPid,workerUid:result.workerUid,workerStartTime:result.startTicks.toString()}; } + try { const result = await runNativeBrokerTurn(options.nativeClient, { slot: registration.slot, requestId: request.requestId, turnId, agentId, wakeId, prompt, providerCapability,mcpCapability }, controller.signal);nativeDiagnostic=result.diagnostic;if(result.workerUid!==registration.workerUid)throw new Error();await isolationGuard();attested=true; const decoded = decodeGrokHeadlessTurn(result.text);const text = decoded.text;const completed={ version: request.version, kind: "completed", requestId: request.requestId, turnId, text, workerPid: result.workerPid, workerUid: result.workerUid, workerStartTime: result.startTicks.toString() } as const; await finishBrokerTurnWithUsage(turns,request,completed,usageLedgerPath,decoded.usage,agentId,wakeId); return {text,workerPid:result.workerPid,workerUid:result.workerUid,workerStartTime:result.startTicks.toString()}; } catch(error) { const code=authority.isStale()?"auth_stale":controller.signal.aborted?"cancelled":"engine_failed";const diagnostic=error instanceof NativeBrokerTurnFailure?error.diagnostic:nativeDiagnostic&&!attested?{...nativeDiagnostic,status:"worker_failed" as const,stage:"attestation" as const,failureClass:error instanceof GrokWorkerAttestationFailure?error.failureClass:"profile_invalid" as const,profileApplied:false}:undefined;await turns.finish(request, { version: request.version, kind: "failed", requestId: request.requestId, turnId, code,...(diagnostic?{diagnostic}:{}) }); throw new EngineBrokerTurnFailure(code,diagnostic); } finally { signal?.removeEventListener("abort",onAbort);active.get(turnId)?.resolve(); active.delete(turnId); proxy.revokeIsolationGuard(turnId);proxy.capabilities.revoke(turnId);mcp.revoke(turnId); } }, diff --git a/src/runtime/grokWorkerAttestation.test.ts b/src/runtime/grokWorkerAttestation.test.ts index 7c1b30a..92920e3 100644 --- a/src/runtime/grokWorkerAttestation.test.ts +++ b/src/runtime/grokWorkerAttestation.test.ts @@ -102,7 +102,7 @@ const eventsFile = async (dir: string, initial = ""): Promise => { }; const watermark = async (file: string, denyPaths: readonly string[] = []): Promise => { const info = await stat(file); - return { dev: Number(info.dev), ino: Number(info.ino), size: Number(info.size), mtimeMs: Number(info.mtimeMs), denyPaths }; + return { dev: Number(info.dev), ino: Number(info.ino), size: Number(info.size), denyPaths }; }; const verify = (eventsPath: string, before: GrokWorkerAttestationSnapshot, brokerGid = self.gid): Promise => verifyGrokWorkerAttestation({ eventsPath, workerUid: self.uid, brokerGid, workspace }, before); diff --git a/src/runtime/grokWorkerAttestation.ts b/src/runtime/grokWorkerAttestation.ts index 70eae1b..adfa3f5 100644 --- a/src/runtime/grokWorkerAttestation.ts +++ b/src/runtime/grokWorkerAttestation.ts @@ -12,7 +12,7 @@ import { grokWorkerEventsPathFor } from "./grokWorkerSandboxProfile.js"; * `size` is pre-turn history and is never read back — a `ProfileApplied` down * there is a replay, not evidence about this turn. */ -export type GrokWorkerAttestationSnapshot=Readonly<{dev:number;ino:number;size:number;mtimeMs:number;denyPaths:readonly string[]}>; +export type GrokWorkerAttestationSnapshot=Readonly<{dev:number;ino:number;size:number;denyPaths:readonly string[]}>; type Snapshot=GrokWorkerAttestationSnapshot; export class GrokWorkerAttestationFailure extends Error { constructor(readonly failureClass:"profile_missing"|"profile_invalid"){super("Grok worker isolation attestation unavailable");} } /** @@ -61,14 +61,65 @@ export function parseGrokWorkerSandboxProfile(bytes:Uint8Array,profileSha256:str * 1.0.34), and a root-owned read-only worker home whose `config.toml` hashes to * the declared renderer output (`grokWorkerHomeAttestation.ts`). */ -export async function prepareGrokWorkerAttestation(input:Readonly<{profilePath:string;eventsPath:string;profileSha256:string;workerUid:number;brokerGid:number;configSha256:string}>):Promise{ +export async function prepareGrokWorkerAttestation(input:Readonly<{profilePath:string;eventsPath:string;profileSha256:string;workerUid:number;brokerGid:number;configSha256:string}>,profileOwner:Readonly<{uid:number;gid:number}>={uid:0,gid:0}):Promise{ if(input.eventsPath!==grokWorkerEventsPathFor(input.profilePath))throw new Error("Grok worker sandbox events must be read from $GROK_HOME/sessions/sandbox-events.jsonl"); - const profile=await secureOpen(input.profilePath,0,0,0o444,65_536);let bytes:Buffer|undefined;let denyPaths:readonly string[]=[];try{bytes=await profile.readFile();denyPaths=parseGrokWorkerSandboxProfile(bytes,input.profileSha256);}catch{throw new Error("Grok worker isolation attestation unavailable");}finally{bytes?.fill(0);await profile.close();} + const profile=await secureOpen(input.profilePath,profileOwner.uid,profileOwner.gid,0o444,65_536);let bytes:Buffer|undefined;let denyPaths:readonly string[]=[];try{bytes=await profile.readFile();denyPaths=parseGrokWorkerSandboxProfile(bytes,input.profileSha256);}catch{throw new Error("Grok worker isolation attestation unavailable");}finally{bytes?.fill(0);await profile.close();} await verifyGrokWorkerHome(path.dirname(input.profilePath),input.configSha256); - const events=await secureOpen(input.eventsPath,input.workerUid,input.brokerGid,0o640,16*1024*1024);try{const stat=await events.stat();return{dev:Number(stat.dev),ino:Number(stat.ino),size:Number(stat.size),mtimeMs:Number(stat.mtimeMs),denyPaths};}finally{await events.close();} + const events=await secureOpen(input.eventsPath,input.workerUid,input.brokerGid,0o640,16*1024*1024);try{const stat=await events.stat();return{dev:Number(stat.dev),ino:Number(stat.ino),size:Number(stat.size),denyPaths};}finally{await events.close();} } -export async function verifyGrokWorkerAttestation(input:Readonly<{eventsPath:string;workerUid:number;brokerGid:number;workspace:string}>,before:Snapshot):Promise{ - let handle:Awaited>;try{handle=await secureOpen(input.eventsPath,input.workerUid,input.brokerGid,0o640,16*1024*1024);}catch{throw new GrokWorkerAttestationFailure("profile_invalid");}let bytes:Buffer|undefined;try{const stat=await handle.stat();if(Number(stat.dev)!==before.dev||Number(stat.ino)!==before.ino)throw new GrokWorkerAttestationFailure("profile_invalid");if(Number(stat.size)<=before.size)throw new GrokWorkerAttestationFailure("profile_missing");bytes=Buffer.alloc(Number(stat.size)-before.size);const read=await handle.read(bytes,0,bytes.length,before.size);if(read.bytesRead!==bytes.length)throw new GrokWorkerAttestationFailure("profile_invalid");const after=await handle.stat();if(Number(after.size)!==Number(stat.size)||Number(after.mtimeMs)!==Number(stat.mtimeMs))throw new GrokWorkerAttestationFailure("profile_invalid");parseGrokWorkerProfileApplied(bytes,input.workspace,before.denyPaths);}catch(error){if(error instanceof GrokWorkerAttestationFailure)throw error;throw new GrokWorkerAttestationFailure("profile_invalid");}finally{bytes?.fill(0);await handle.close();} +/** + * The accepted `ProfileApplied` line of one turn, as an absolute byte range of + * the events file plus its digest. + */ +export type GrokWorkerAttestationLock={accepted?:Readonly<{offset:number;length:number;digest:string}>;refused?:GrokWorkerAttestationFailure["failureClass"]}; + +/** + * The per-turn isolation guard: every model request of a turn, and the + * post-turn check, go through the same lock. + * + * Why the first verification is trustworthy: the proxy awaits this guard before + * *every* upstream request, including the first, and the only worker-uid + * process that exists before request 1 is the Grok the launcher started under + * the pinned profile — tool children are created only after a model response. + * So the event accepted at request 1 was written before any tool child could + * write to the (worker-owned) events file. Later requests must find that exact + * line, byte for byte, at the same offset: a `ProfileApplied` appended later — + * which a tool child could forge — is never a substitute, and once a turn has + * been refused it stays refused. + */ +export function createGrokWorkerIsolationGuard(input:Parameters[0],before:Snapshot):()=>Promise{ + const lock:GrokWorkerAttestationLock={}; + return async()=>{ + if(lock.refused!==undefined)throw new GrokWorkerAttestationFailure(lock.refused); + try{await verifyGrokWorkerAttestation(input,before,lock);} + catch(error){const failure=error instanceof GrokWorkerAttestationFailure?error:new GrokWorkerAttestationFailure("profile_invalid");lock.refused=failure.failureClass;throw failure;} + }; +} +export async function verifyGrokWorkerAttestation(input:Readonly<{eventsPath:string;workerUid:number;brokerGid:number;workspace:string}>,before:Snapshot,lock?:GrokWorkerAttestationLock):Promise{ + let handle:Awaited>;try{handle=await secureOpen(input.eventsPath,input.workerUid,input.brokerGid,0o640,16*1024*1024);}catch{throw new GrokWorkerAttestationFailure("profile_invalid");} + let bytes:Buffer|undefined; + try{ + const stat=await handle.stat(); + if(Number(stat.dev)!==before.dev||Number(stat.ino)!==before.ino)throw new GrokWorkerAttestationFailure("profile_invalid"); + if(Number(stat.size)<=before.size)throw new GrokWorkerAttestationFailure("profile_missing"); + const accepted=lock?.accepted; + const start=accepted===undefined?before.size:before.size+accepted.offset; + const length=accepted===undefined?Number(stat.size)-before.size:accepted.length; + if(start+length>Number(stat.size))throw new GrokWorkerAttestationFailure("profile_invalid"); + bytes=Buffer.alloc(length); + const read=await handle.read(bytes,0,bytes.length,start); + if(read.bytesRead!==bytes.length)throw new GrokWorkerAttestationFailure("profile_invalid"); + // The file must not change while it is read: a concurrent writer could + // otherwise show this check bytes that no single state of the file held. + const after=await handle.stat(); + if(Number(after.size)!==Number(stat.size)||Number(after.mtimeMs)!==Number(stat.mtimeMs))throw new GrokWorkerAttestationFailure("profile_invalid"); + if(accepted!==undefined){ + if(createHash("sha256").update(bytes).digest("hex")!==accepted.digest)throw new GrokWorkerAttestationFailure("profile_invalid"); + return; + } + const event=locateGrokWorkerProfileApplied(bytes,input.workspace,before.denyPaths); + if(lock!==undefined)lock.accepted={offset:event.offset,length:event.length,digest:createHash("sha256").update(bytes.subarray(event.offset,event.offset+event.length)).digest("hex")}; + }catch(error){if(error instanceof GrokWorkerAttestationFailure)throw error;throw new GrokWorkerAttestationFailure("profile_invalid");}finally{bytes?.fill(0);await handle.close();} } /** * Requires one fully-conforming `ProfileApplied` event *somewhere* in the @@ -101,15 +152,19 @@ export async function verifyGrokWorkerAttestation(input:Readonly<{eventsPath:str * guard exists for: a Grok that came up without kernel enforcement, which * emits no conforming `ProfileApplied` at all. */ -export function parseGrokWorkerProfileApplied(bytes:Uint8Array,workspace:string,denyPaths:readonly string[]=[]):void{ +export function parseGrokWorkerProfileApplied(bytes:Uint8Array,workspace:string,denyPaths:readonly string[]=[]):void{locateGrokWorkerProfileApplied(bytes,workspace,denyPaths);} +/** Byte range (within `bytes`) of the first fully-conforming `ProfileApplied` line. */ +export function locateGrokWorkerProfileApplied(bytes:Uint8Array,workspace:string,denyPaths:readonly string[]=[]):Readonly<{offset:number;length:number}>{ const expected=JSON.stringify([...denyPaths].sort()); - for(const line of Buffer.from(bytes).toString("utf8").split("\n")){ + const buffer=Buffer.from(bytes.buffer,bytes.byteOffset,bytes.byteLength); + for(let offset=0;offset; try{const parsed=JSON.parse(line) as unknown;if(parsed===null||typeof parsed!=="object"||Array.isArray(parsed))continue;event=parsed as Record;}catch{continue;} if(event.event_type!=="ProfileApplied")continue; const observed=Array.isArray(event.deny_paths)?event.deny_paths.filter((entry):entry is string=>typeof entry==="string").sort():[]; - if(event.profile==="daimon-strict"&&event.enforced===true&&event.restrict_network===true&&event.platform==="linux/landlock"&&event.workspace===workspace&&JSON.stringify(observed)===expected)return; + if(event.profile==="daimon-strict"&&event.enforced===true&&event.restrict_network===true&&event.platform==="linux/landlock"&&event.workspace===workspace&&JSON.stringify(observed)===expected)return{offset:lineOffset,length:end-lineOffset}; } throw new Error("Grok worker isolation attestation unavailable"); } diff --git a/src/runtime/grokWorkerIsolationGuard.test.ts b/src/runtime/grokWorkerIsolationGuard.test.ts new file mode 100644 index 0000000..98fa729 --- /dev/null +++ b/src/runtime/grokWorkerIsolationGuard.test.ts @@ -0,0 +1,79 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { appendFile, chmod, link, mkdir, mkdtemp, open, readFile, rm, stat, truncate, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test, { mock } from "node:test"; + +import { + createGrokWorkerIsolationGuard, + GrokWorkerAttestationFailure, + prepareGrokWorkerAttestation, + verifyGrokWorkerAttestation, + type GrokWorkerAttestationSnapshot +} from "./grokWorkerAttestation.js"; + +const workspace = "/var/lib/daimon-workers/2200/workspace"; +const self = { uid: process.getuid?.() ?? 0, gid: process.getgid?.() ?? 0 }; +const applied = (overrides: Record = {}): string => + `${JSON.stringify({ event_type: "ProfileApplied", profile: "daimon-strict", enforced: true, restrict_network: true, platform: "linux/landlock", workspace, deny_paths: [], ...overrides })}\n`; +const violation = `${JSON.stringify({ event_type: "FsViolation", profile: "daimon-strict", operation: "read", target: "/run/paideia/context.json" })}\n`; + +const fixture = async (t: { after(fn: () => Promise): void }, initial = "") => { + const dir = await mkdtemp(path.join(os.tmpdir(), "daimon-guard-")); + t.after(() => rm(dir, { recursive: true, force: true })); + const file = path.join(dir, "sandbox-events.jsonl"); + await writeFile(file, initial); + await chmod(file, 0o640); + const info = await stat(file); + const before: GrokWorkerAttestationSnapshot = { dev: Number(info.dev), ino: Number(info.ino), size: Number(info.size), denyPaths: [] }; + const input = { eventsPath: file, workerUid: self.uid, brokerGid: self.gid, workspace }; + return { dir, file, before, input }; +}; +const refusal = async (run: Promise): Promise => { + try { await run; } catch (error) { + assert.ok(error instanceof GrokWorkerAttestationFailure, `expected a GrokWorkerAttestationFailure, got ${String(error)}`); + return error.failureClass; + } + throw new Error("expected the attestation to be refused"); +}; + +test("a turn refused at its first request stays refused after a conforming ProfileApplied is appended", async (t) => { + // Before request 1 only the launcher-started Grok can have written events; a + // line appended after it can come from a tool child and must never repair the turn. + const { file, before, input } = await fixture(t); + const guard = createGrokWorkerIsolationGuard(input, before); + await appendFile(file, applied({ enforced: false })); + assert.equal(await refusal(guard()), "profile_invalid"); + await appendFile(file, applied()); + assert.equal(await refusal(guard()), "profile_invalid"); + // Without the lock the same bytes would be accepted, so the refusal above is the lock's doing. + await verifyGrokWorkerAttestation(input, before); +}); + +test("later requests need the accepted event at the same offset, not any newly appended one", async (t) => { + const { file, before, input } = await fixture(t, "{\"event_type\":\"stale\"}\n"); + const guard = createGrokWorkerIsolationGuard(input, before); + await appendFile(file, `${violation}${applied()}`); + await guard(); + await appendFile(file, `${violation}${applied()}`); + await guard(); + + // Rewrite the accepted line in place (same length, different bytes) and append a fresh conforming one. + const bytes = await readFile(file, "utf8"); + const acceptedAt = bytes.indexOf(applied()); + const handle = await open(file, "r+"); + try { await handle.write(Buffer.from(applied({ workspace: workspace.replace("workspace", "workspacX") })), 0, undefined, acceptedAt); } finally { await handle.close(); } + await appendFile(file, applied()); + assert.equal(await refusal(guard()), "profile_invalid"); +}); + +test("a truncated accepted event region is refused on the next request", async (t) => { + const { file, before, input } = await fixture(t); + const guard = createGrokWorkerIsolationGuard(input, before); + await appendFile(file, applied()); + await guard(); + await truncate(file, 10); + await appendFile(file, `\n${applied()}`); + assert.equal(await refusal(guard()), "profile_invalid"); +}); From fdbc7a0ce52bbee0ecfef60626c884c0d50dff97 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:50:41 +0200 Subject: [PATCH 21/26] test: cover worker home, hard link and read stability checks in Grok attestation --- .../grokWorkerAttestationChecks.test.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/runtime/grokWorkerAttestationChecks.test.ts diff --git a/src/runtime/grokWorkerAttestationChecks.test.ts b/src/runtime/grokWorkerAttestationChecks.test.ts new file mode 100644 index 0000000..8a6480e --- /dev/null +++ b/src/runtime/grokWorkerAttestationChecks.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { appendFile, chmod, link, mkdir, mkdtemp, open, readFile, rm, stat, truncate, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test, { mock } from "node:test"; + +import { + GrokWorkerAttestationFailure, + prepareGrokWorkerAttestation, + verifyGrokWorkerAttestation, + type GrokWorkerAttestationSnapshot +} from "./grokWorkerAttestation.js"; + +const workspace = "/var/lib/daimon-workers/2200/workspace"; +const self = { uid: process.getuid?.() ?? 0, gid: process.getgid?.() ?? 0 }; +const applied = (overrides: Record = {}): string => + `${JSON.stringify({ event_type: "ProfileApplied", profile: "daimon-strict", enforced: true, restrict_network: true, platform: "linux/landlock", workspace, deny_paths: [], ...overrides })}\n`; +const violation = `${JSON.stringify({ event_type: "FsViolation", profile: "daimon-strict", operation: "read", target: "/run/paideia/context.json" })}\n`; + +const fixture = async (t: { after(fn: () => Promise): void }, initial = "") => { + const dir = await mkdtemp(path.join(os.tmpdir(), "daimon-guard-")); + t.after(() => rm(dir, { recursive: true, force: true })); + const file = path.join(dir, "sandbox-events.jsonl"); + await writeFile(file, initial); + await chmod(file, 0o640); + const info = await stat(file); + const before: GrokWorkerAttestationSnapshot = { dev: Number(info.dev), ino: Number(info.ino), size: Number(info.size), denyPaths: [] }; + const input = { eventsPath: file, workerUid: self.uid, brokerGid: self.gid, workspace }; + return { dir, file, before, input }; +}; +const refusal = async (run: Promise): Promise => { + try { await run; } catch (error) { + assert.ok(error instanceof GrokWorkerAttestationFailure, `expected a GrokWorkerAttestationFailure, got ${String(error)}`); + return error.failureClass; + } + throw new Error("expected the attestation to be refused"); +}; + +test("refuses an events file with a second hard link", async (t) => { + const { dir, file, before, input } = await fixture(t); + await appendFile(file, applied()); + await verifyGrokWorkerAttestation(input, before); + await link(file, path.join(dir, "second-name.jsonl")); + assert.equal(await refusal(verifyGrokWorkerAttestation(input, before)), "profile_invalid"); +}); + +test("refuses events that change while they are being read", async (t) => { + const { file, before, input } = await fixture(t); + await appendFile(file, applied()); + const probe = await open(file, "r"); + const prototype = Object.getPrototypeOf(probe) as { read: (...args: unknown[]) => Promise }; + await probe.close(); + const original = prototype.read; + const reading = mock.method(prototype, "read", async function (this: unknown, ...args: unknown[]) { + const result = await original.apply(this, args); + await appendFile(file, violation); + return result; + }); + try { assert.equal(await refusal(verifyGrokWorkerAttestation(input, before)), "profile_invalid"); } finally { reading.mock.restore(); } + assert.ok(reading.mock.callCount() >= 1); +}); + +test("prepare refuses a worker home that fails attestation even when profile and events are valid", async (t) => { + // Run as a non-root owner so the profile and events legs pass; the home leg + // (root-owned, read-only config) cannot, and must be what refuses. + const home = await mkdtemp(path.join(os.tmpdir(), "daimon-guard-home-")); + t.after(() => rm(home, { recursive: true, force: true })); + const profile = path.join(home, "sandbox.toml"); + const text = '[profiles.daimon-strict]\nextends = "strict"\nrestrict_network = true\ndeny = []\n'; + await writeFile(profile, text); await chmod(profile, 0o444); + await mkdir(path.join(home, "sessions")); + const events = path.join(home, "sessions", "sandbox-events.jsonl"); + await writeFile(events, ""); await chmod(events, 0o640); + const input = { profilePath: profile, eventsPath: events, profileSha256: createHash("sha256").update(text).digest("hex"), workerUid: self.uid, brokerGid: self.gid, configSha256: "0".repeat(64) }; + await assert.rejects(prepareGrokWorkerAttestation(input, { uid: self.uid, gid: Number((await stat(profile)).gid) }), /attestation unavailable/u); +}); From a78a9937d509d14d08ddaffa173ab39930cf28f2 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:50:56 +0200 Subject: [PATCH 22/26] test: prove the Grok isolation guard gates the first upstream call of a turn --- src/runtime/grokBrokerProxy.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/runtime/grokBrokerProxy.test.ts b/src/runtime/grokBrokerProxy.test.ts index 0a935d5..6c541b6 100644 --- a/src/runtime/grokBrokerProxy.test.ts +++ b/src/runtime/grokBrokerProxy.test.ts @@ -67,3 +67,21 @@ test("the session-title sink is refused before capability, guard, credential, or assert.equal(calls, 1); } finally { await proxy.close(); } }); + +test("the isolation guard is awaited before the first upstream call, and a failing guard makes no upstream call", async () => { + const order: string[] = []; let upstreamCalls = 0; let fail = true; + const proxy = await startGrokBrokerProxy({ accessToken: async () => { order.push("credential"); return "provider-token"; }, markRejected: async () => undefined }, async () => { upstreamCalls++; order.push("upstream"); return { status: 200, headers: { "content-type": "application/json" }, body: Buffer.from("{}") }; }); + try { + const token = proxy.capabilities.issue("agent", "turn"); + proxy.registerIsolationGuard("turn", async () => { + order.push("guard-start"); await new Promise((resolve) => setTimeout(resolve, 30)); order.push("guard-end"); + if (fail) throw new Error("no enforcement evidence"); + }); + assert.equal(await post(proxy.port, token, leanBody()), 503); + assert.equal(upstreamCalls, 0); + assert.deepEqual(order, ["guard-start", "guard-end"]); + fail = false; order.length = 0; + assert.equal(await post(proxy.port, token, leanBody()), 200); + assert.deepEqual(order, ["guard-start", "guard-end", "credential", "upstream"]); + } finally { await proxy.close(); } +}); From ad67499fec421f9dcf4c82621a83a58647cfd4c3 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:51:28 +0200 Subject: [PATCH 23/26] fix: require the opened Grok worker config to be the inode attested by lstat --- src/runtime/grokWorkerHomeAttestation.test.ts | 34 +++++++++++++++++-- src/runtime/grokWorkerHomeAttestation.ts | 15 ++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/runtime/grokWorkerHomeAttestation.test.ts b/src/runtime/grokWorkerHomeAttestation.test.ts index 1c2eceb..22f591e 100644 --- a/src/runtime/grokWorkerHomeAttestation.test.ts +++ b/src/runtime/grokWorkerHomeAttestation.test.ts @@ -1,8 +1,8 @@ import assert from "node:assert/strict"; -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { chmod, mkdir, mkdtemp, open, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test from "node:test"; +import test, { mock } from "node:test"; import { grokBrokerWorkerConfigSha256, renderGrokBrokerWorkerConfig } from "./grokBrokerWorkerConfig.js"; import { assertGrokWorkerConfigBytes, assertGrokWorkerHomeEntries, verifyGrokWorkerHome } from "./grokWorkerHomeAttestation.js"; @@ -60,3 +60,33 @@ test("accepts only the renderer's exact config bytes for the declared model poli `${renderGrokBrokerWorkerConfig(declared)}\n[mcp_servers.extra]\nurl = "http://127.0.0.1:1/mcp"\n` ]) assert.throws(() => assertGrokWorkerConfigBytes(Buffer.from(tampered), grokBrokerWorkerConfigSha256(declared)), /attestation unavailable/u); }); + +const ownHome = async (t: { after(fn: () => Promise): void }) => { + const home = await mkdtemp(path.join(os.tmpdir(), "daimon-grok-home-owned-")); + t.after(async () => { await chmod(home, 0o700); await rm(home, { recursive: true, force: true }); }); + await mkdir(path.join(home, "sessions")); + const declared = { model: "grok-4.6", reasoningEffort: "low" } as const; + for (const name of files) await writeFile(path.join(home, name), name === "config.toml" ? renderGrokBrokerWorkerConfig(declared) : "", { mode: 0o444 }); + await chmod(path.join(home, "sessions"), 0o1771); await chmod(home, 0o1771); + return { home, sha: grokBrokerWorkerConfigSha256(declared), uid: process.getuid?.() ?? 0 }; +}; + +test("attests a correctly laid out home whose config is the declared renderer output", async (t) => { + const { home, sha, uid } = await ownHome(t); + await verifyGrokWorkerHome(home, sha, uid); + await assert.rejects(verifyGrokWorkerHome(home, grokBrokerWorkerConfigSha256({ model: "grok-4.6", reasoningEffort: "high" }), uid), /attestation unavailable/u); +}); + +test("refuses a config.toml whose opened inode is not the one lstat saw", async (t) => { + const { home, sha, uid } = await ownHome(t); + const probe = await open(path.join(home, "config.toml"), "r"); + const prototype = Object.getPrototypeOf(probe) as { stat: (...args: unknown[]) => Promise<{ ino: number }> }; + await probe.close(); + const original = prototype.stat; + const swapped = mock.method(prototype, "stat", async function (this: unknown, ...args: unknown[]) { + const real = await original.apply(this, args); + return Object.assign(Object.create(Object.getPrototypeOf(real)), real, { ino: Number(real.ino) + 1 }); + }); + try { await assert.rejects(verifyGrokWorkerHome(home, sha, uid), /attestation unavailable/u); } finally { swapped.mock.restore(); } + await verifyGrokWorkerHome(home, sha, uid); +}); diff --git a/src/runtime/grokWorkerHomeAttestation.ts b/src/runtime/grokWorkerHomeAttestation.ts index f029757..47b15ba 100644 --- a/src/runtime/grokWorkerHomeAttestation.ts +++ b/src/runtime/grokWorkerHomeAttestation.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { GROK_ENGINE_BROKER } from "../contracts/runtimeContractManifest.js"; -type Entry = Pick & Readonly<{ isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean }>; +type Entry = Pick & Partial> & Readonly<{ isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean }>; const HOME = GROK_ENGINE_BROKER.worker.home; /** @@ -22,14 +22,14 @@ const HOME = GROK_ENGINE_BROKER.worker.home; * * Pure so every refusal is testable without root. */ -export function assertGrokWorkerHomeEntries(entries: Readonly>): void { +export function assertGrokWorkerHomeEntries(entries: Readonly>, rootUid: number = HOME.directory.uid): void { const directory = (entry: Entry | undefined): boolean => - entry !== undefined && entry.isDirectory() && !entry.isSymbolicLink() && entry.uid === HOME.directory.uid + entry !== undefined && entry.isDirectory() && !entry.isSymbolicLink() && entry.uid === rootUid && (Number(entry.mode) & 0o002) === 0 && ((Number(entry.mode) & 0o020) === 0 || (Number(entry.mode) & 0o1000) !== 0); if (!directory(entries["."]) || !directory(entries[HOME.sessionsDirectory.relativePath])) throw unavailable(); for (const name of HOME.readOnlyFiles.names) { const entry = entries[name]; - if (entry === undefined || !entry.isFile() || entry.isSymbolicLink() || entry.uid !== HOME.readOnlyFiles.uid || entry.nlink !== 1 || (Number(entry.mode) & 0o7222) !== 0) throw unavailable(); + if (entry === undefined || !entry.isFile() || entry.isSymbolicLink() || entry.uid !== rootUid || entry.nlink !== 1 || (Number(entry.mode) & 0o7222) !== 0) throw unavailable(); } } @@ -42,18 +42,19 @@ export function assertGrokWorkerConfigBytes(bytes: Uint8Array, configSha256: str * Attests the worker home layout and that `config.toml` is exactly the * renderer's bytes for the declared model policy (`configSha256`). */ -export async function verifyGrokWorkerHome(grokHome: string, configSha256: string): Promise { +export async function verifyGrokWorkerHome(grokHome: string, configSha256: string, rootUid: number = HOME.directory.uid): Promise { const entries: Record = {}; for (const name of [".", HOME.sessionsDirectory.relativePath, ...HOME.readOnlyFiles.names]) { try { entries[name] = await lstat(path.join(grokHome, name)); } catch { entries[name] = undefined; } } - assertGrokWorkerHomeEntries(entries); + assertGrokWorkerHomeEntries(entries, rootUid); let handle: Awaited> | undefined; try { handle = await open(path.join(grokHome, "config.toml"), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); const opened = await handle.stat(); const before = entries["config.toml"]!; - if (!opened.isFile() || opened.size > 65_536 || opened.uid !== before.uid || opened.mode !== before.mode || opened.nlink !== 1) throw unavailable(); + // Same inode as the lstat above, as in `secureOpen`: a rename between the two cannot swap in other bytes. + if (!opened.isFile() || opened.size > 65_536 || opened.dev !== before.dev || opened.ino !== before.ino || opened.uid !== before.uid || opened.mode !== before.mode || opened.nlink !== 1) throw unavailable(); assertGrokWorkerConfigBytes(await handle.readFile(), configSha256); } catch { throw unavailable(); } finally { await handle?.close().catch(() => undefined); } } From f4fe7f3253779cfb505af5d3ceed991242a4ad67 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:52:04 +0200 Subject: [PATCH 24/26] fix: refuse __proto__ members and extra tool members in Grok broker requests --- src/runtime/grokBrokerProxyRequest.test.ts | 36 ++++++++++++++++++++++ src/runtime/grokBrokerProxyRequest.ts | 11 +++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/runtime/grokBrokerProxyRequest.test.ts b/src/runtime/grokBrokerProxyRequest.test.ts index 576ff0a..7cdcdfb 100644 --- a/src/runtime/grokBrokerProxyRequest.test.ts +++ b/src/runtime/grokBrokerProxyRequest.test.ts @@ -71,3 +71,39 @@ test("proxy refuses top-level members a lean Grok 1.0.34 worker never sends", () const { caps, input } = request(leanBody({ stream_options: { include_usage: true } })); assert.equal(Buffer.from(authorizeGrokBrokerProxyRequest(input, caps, "real-bearer").body).toString("utf8"), Buffer.from(leanBody({ stream_options: { include_usage: true } })).toString("utf8")); }); + +test("nested duplicate keys in tools and messages are forwarded only as the parsed values", () => { + const tools = leanTools.map((name) => `{"type":"function","function":{"name":${JSON.stringify(name === "read_file" ? "write" : name)},"name":${JSON.stringify(name)},"parameters":{"type":"object"}}}`).join(","); + const raw = `{"model":"grok-4.6","reasoning_effort":"low","stream":true,"messages":[{"role":"system","role":"user","content":"a","content":"b"}],"tools":[${tools}]}`; + const { caps, input } = request(Buffer.from(raw)); + const forwarded = Buffer.from(authorizeGrokBrokerProxyRequest(input, caps, "real-bearer").body).toString("utf8"); + assert.equal(forwarded, JSON.stringify(JSON.parse(raw))); + assert.doesNotMatch(forwarded, /"write"|"system"|"content":"a"/u); + assert.equal(forwarded.split('"role"').length, 2); + // The reverse order puts the forbidden name last, so the parsed (and gated) value is refused. + const hostile = raw.replace('"name":"write","name":"read_file"', '"name":"read_file","name":"write"'); + const second = request(Buffer.from(hostile)); + assert.throws(() => authorizeGrokBrokerProxyRequest(second.input, second.caps, "real-bearer"), /rejected/u); +}); + +test("__proto__ members are refused anywhere in the body", () => { + const lean = JSON.stringify(leanTools.map(tool)); + const base = `"model":"grok-4.6","reasoning_effort":"low","stream":true,"messages":[]`; + for (const raw of [ + `{${base},"tools":${lean},"__proto__":{"tools":[]}}`, + `{${base},"tools":${lean.replace('{"type":"function"', '{"__proto__":{"type":"function"},"type":"function"')}}`, + `{${base},"tools":${lean.replace('"parameters":{"type":"object"}', '"parameters":{"type":"object","__proto__":{"x":1}}')}}`, + `{"model":"grok-4.6","reasoning_effort":"low","stream":true,"messages":[{"role":"user","content":"hi","__proto__":{"role":"system"}}],"tools":${lean}}` + ]) { + const { caps, input } = request(Buffer.from(raw)); + assert.throws(() => authorizeGrokBrokerProxyRequest(input, caps, "real-bearer"), /rejected/u, raw.slice(0, 120)); + } +}); + +test("tool entries carry only the members a lean worker sends", () => { + for (const extra of [{ strict: true }, { function: { name: "read_file", parameters: {}, x: 1 } }]) { + const tools = leanTools.map((name) => name === "read_file" ? { ...tool(name), ...extra } : tool(name)); + const { caps, input } = request(leanBody({ tools })); + assert.throws(() => authorizeGrokBrokerProxyRequest(input, caps, "real-bearer"), /rejected/u, JSON.stringify(extra)); + } +}); diff --git a/src/runtime/grokBrokerProxyRequest.ts b/src/runtime/grokBrokerProxyRequest.ts index 24dcbc6..79956ac 100644 --- a/src/runtime/grokBrokerProxyRequest.ts +++ b/src/runtime/grokBrokerProxyRequest.ts @@ -30,7 +30,9 @@ export function authorizeGrokBrokerProxyRequest(input: GrokBrokerProxyInput, cap const clientVersion = input.headers["x-grok-client-version"]; if (clientVersion !== GROK_ENGINE_BROKER.grokCliVersion) throw new Error("broker proxy request rejected"); let parsed: Record; - try { parsed = JSON.parse(Buffer.from(input.body).toString("utf8")) as Record; } catch { throw new Error("broker proxy request rejected"); } + // `__proto__` is refused at any depth: JSON.parse makes it an ordinary own + // member, but an upstream JavaScript parser may treat it as a prototype. + try { parsed = JSON.parse(Buffer.from(input.body).toString("utf8"), (key, value: unknown) => { if (key === "__proto__") throw new Error(); return value; }) as Record; } catch { throw new Error("broker proxy request rejected"); } if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || parsed.stream !== true || !Array.isArray(parsed.messages)) throw new Error("broker proxy request rejected"); if (parsed.model !== declared.model || parsed.reasoning_effort !== declared.reasoningEffort) throw new Error("broker proxy request rejected"); if (!exactLeanTools(parsed.tools)) throw new Error("broker proxy request rejected"); @@ -43,15 +45,18 @@ export function authorizeGrokBrokerProxyRequest(input: GrokBrokerProxyInput, cap /** Top-level members of a Grok 1.0.34 lean worker chat-completions body (live stub capture). */ const LEAN_BODY_MEMBERS: ReadonlySet = new Set(["messages", "model", "reasoning_effort", "stream", "stream_options", "tools"]); +/** Members of each lean tool `function` entry (live stub capture). */ +const LEAN_FUNCTION_MEMBERS: ReadonlySet = new Set(["description", "name", "parameters"]); const validStreamOptions = (value: unknown): boolean => value === undefined || (value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every((key) => key === "include_usage") && typeof (value as { include_usage?: unknown }).include_usage === "boolean"); export function exactLeanTools(tools: unknown): boolean { if (!Array.isArray(tools) || tools.length !== GROK_WORKER_VISIBLE_TOOLS.length) return false; const names = tools.map((tool) => { - if (tool === null || typeof tool !== "object" || (tool as { type?: unknown }).type !== "function") return undefined; + if (tool === null || typeof tool !== "object" || Array.isArray(tool) || (tool as { type?: unknown }).type !== "function" || Object.keys(tool).some((key) => key !== "type" && key !== "function")) return undefined; const fn = (tool as { function?: unknown }).function; - return fn !== null && typeof fn === "object" ? (fn as { name?: unknown }).name : undefined; + if (fn === null || typeof fn !== "object" || Array.isArray(fn) || Object.keys(fn).some((key) => !LEAN_FUNCTION_MEMBERS.has(key))) return undefined; + return (fn as { name?: unknown }).name; }); return JSON.stringify([...names].sort()) === JSON.stringify(GROK_WORKER_VISIBLE_TOOLS); } From b55f678ba93fc03290aee44a92ec21362daf27f4 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:53:18 +0200 Subject: [PATCH 25/26] test: prove a Grok worker that fails at exec runs nothing --- .../engineBrokerLauncherIntegrationMain.inc | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/runtime/native/engineBrokerLauncherIntegrationMain.inc b/src/runtime/native/engineBrokerLauncherIntegrationMain.inc index f5ea29b..1191e2d 100644 --- a/src/runtime/native/engineBrokerLauncherIntegrationMain.inc +++ b/src/runtime/native/engineBrokerLauncherIntegrationMain.inc @@ -1,3 +1,52 @@ +/* A worker that fails before or at exec must not run anything: the launcher + child exits instead. The registered "executable" is a #! script; fd 5 is + close-on-exec, so execveat(AT_EMPTY_PATH) cannot run it (a script needs + /dev/fd/5 after exec) and the turn fails at the exec stage. */ +static void exec_failure_case(struct dbl_registration r) { + const char *marker = "/tmp/exec-failure-script-ran"; + unlink(marker); + check(rename("/usr/local/bin/grok", "/usr/local/bin/grok.fixture") == 0, + "exec failure fixture move"); + int script = open("/usr/local/bin/grok", O_CREAT | O_EXCL | O_WRONLY, 0755); + const char body[] = "#!/bin/sh\ntouch /tmp/exec-failure-script-ran\n"; + check(script >= 0 && write(script, body, sizeof(body) - 1) == + (ssize_t)(sizeof(body) - 1), + "exec failure script"); + close(script); + digest("/usr/local/bin/grok", r.executable_sha256); + int f = open(DBL_REGISTRY, O_TRUNC | O_WRONLY, 0600); + check(f >= 0 && write(f, &r, sizeof(r)) == sizeof(r), "exec failure registry"); + close(f); + pid_t child = fork(); + if (!child) { + setgid(DBL_BROKER_UID); + setuid(DBL_BROKER_UID); + int s = connect_socket(), p = sealed("prompt"), + c = sealed_bundle("provider.Exec-1", "mcp.Exec-2"); + struct dbl_request q = request(); + struct dbl_result result; + send_request(s, &q, p, c); + _exit(read_all(s, &result, sizeof(result)) && + result.status == DBL_STATUS_PRELAUNCH_FAILED && + result.stage == DBL_STAGE_EXEC && + result.failure_class == DBL_FAILURE_EXEC && + result.worker_pid == 0 + ? 0 + : 1); + } + int status; + waitpid(child, &status, 0); + check(WIFEXITED(status) && WEXITSTATUS(status) == 0, "exec failure result"); + sleep(1); + check(access(marker, F_OK) != 0, "exec failure ran nothing"); + unlink("/usr/local/bin/grok"); + check(rename("/usr/local/bin/grok.fixture", "/usr/local/bin/grok") == 0, + "exec failure fixture restore"); + digest("/usr/local/bin/grok", r.executable_sha256); + f = open(DBL_REGISTRY, O_TRUNC | O_WRONLY, 0600); + check(f >= 0 && write(f, &r, sizeof(r)) == sizeof(r), "registry restore"); + close(f); +} int main(void) { setbuf(stdout, NULL); alarm(40); @@ -61,6 +110,9 @@ int main(void) { } int status; waitpid(org, &status, 0); + check(WIFEXITED(status) && WEXITSTATUS(status) == 0, "org cases"); + exec_failure_case(r); + puts("native-stage exec failure complete"); kill(broker, SIGKILL); waitpid(broker, 0, 0); check(WIFEXITED(status) && WEXITSTATUS(status) == 0, "org cases"); From 85bb35745a7584aff765eecbb0c50a0e62f16bb3 Mon Sep 17 00:00:00 2001 From: Juan Cruz Fortunatti Date: Thu, 17 Sep 2026 05:54:19 +0200 Subject: [PATCH 26/26] test: drop unused imports from Grok attestation tests --- src/runtime/grokWorkerAttestationChecks.test.ts | 2 +- src/runtime/grokWorkerIsolationGuard.test.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/runtime/grokWorkerAttestationChecks.test.ts b/src/runtime/grokWorkerAttestationChecks.test.ts index 8a6480e..694391c 100644 --- a/src/runtime/grokWorkerAttestationChecks.test.ts +++ b/src/runtime/grokWorkerAttestationChecks.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import { appendFile, chmod, link, mkdir, mkdtemp, open, readFile, rm, stat, truncate, writeFile } from "node:fs/promises"; +import { appendFile, chmod, link, mkdir, mkdtemp, open, rm, stat, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test, { mock } from "node:test"; diff --git a/src/runtime/grokWorkerIsolationGuard.test.ts b/src/runtime/grokWorkerIsolationGuard.test.ts index 98fa729..cae9d81 100644 --- a/src/runtime/grokWorkerIsolationGuard.test.ts +++ b/src/runtime/grokWorkerIsolationGuard.test.ts @@ -1,14 +1,12 @@ import assert from "node:assert/strict"; -import { createHash } from "node:crypto"; -import { appendFile, chmod, link, mkdir, mkdtemp, open, readFile, rm, stat, truncate, writeFile } from "node:fs/promises"; +import { appendFile, chmod, mkdtemp, open, readFile, rm, stat, truncate, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import test, { mock } from "node:test"; +import test from "node:test"; import { createGrokWorkerIsolationGuard, GrokWorkerAttestationFailure, - prepareGrokWorkerAttestation, verifyGrokWorkerAttestation, type GrokWorkerAttestationSnapshot } from "./grokWorkerAttestation.js";