Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
551f596
feat: pin Grok CLI 1.0.34 lean worker contract and single config rend…
apresmoi Sep 17, 2026
18b9d48
feat: refuse Grok broker requests outside the lean tool set and decla…
apresmoi Sep 17, 2026
8dbd8c0
feat: attest Grok 1.0.34 sandbox events, deny lists and read-only wor…
apresmoi Sep 17, 2026
dcfdc8a
fix: register direct Grok MCP endpoints in the Daimon-owned GROK_HOME…
apresmoi Sep 17, 2026
7529ef9
feat: compile the lean Grok worker argv and system prompt into the na…
apresmoi Sep 17, 2026
4bbf224
build: rebuild native engine broker artifacts for the lean Grok worke…
apresmoi Sep 17, 2026
9761d8d
fix: keep the sealed prompt fd open across the Grok worker exec
apresmoi Sep 17, 2026
883111c
build: rebuild native engine broker artifacts with the prompt fd fix
apresmoi Sep 17, 2026
f41774e
fix: pass the Grok provider capability through env_key because 1.0.34…
apresmoi Sep 17, 2026
7f5b62b
build: rebuild native engine broker artifacts with the provider capab…
apresmoi Sep 17, 2026
4bd43d3
docs: document the Grok 1.0.34 lean worker contract and worker home l…
apresmoi Sep 17, 2026
0e09c75
docs: point the worker argv mirror comment at launcherArgv.test.ts
apresmoi Sep 17, 2026
596a87d
test: give Grok direct-session tests a GROK_HOME and unpool proxy rej…
apresmoi Sep 17, 2026
18c2653
fix: escape control characters in the sandbox deny-path regex and for…
apresmoi Sep 17, 2026
838b507
fix: forward the validated Grok request object and refuse unknown top…
apresmoi Sep 17, 2026
5fe9374
fix: send the Grok session-title request to the broker proxy where it…
apresmoi Sep 17, 2026
08ceda6
fix: give Grok workers /dev/null stdin and keep the executable fd out…
apresmoi Sep 17, 2026
cd33c5f
build: rebuild native engine broker artifacts with the worker fd hard…
apresmoi Sep 17, 2026
0878c97
test: cover capability expiry on the proxy token lookups
apresmoi Sep 17, 2026
b1823ec
fix: lock each Grok turn to the ProfileApplied event accepted before …
apresmoi Sep 17, 2026
fdbc7a0
test: cover worker home, hard link and read stability checks in Grok …
apresmoi Sep 17, 2026
a78a993
test: prove the Grok isolation guard gates the first upstream call of…
apresmoi Sep 17, 2026
ad67499
fix: require the opened Grok worker config to be the inode attested b…
apresmoi Sep 17, 2026
f4fe7f3
fix: refuse __proto__ members and extra tool members in Grok broker r…
apresmoi Sep 17, 2026
b55f678
test: prove a Grok worker that fails at exec runs nothing
apresmoi Sep 17, 2026
85bb357
test: drop unused imports from Grok attestation tests
apresmoi Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
20 changes: 10 additions & 10 deletions scripts/liveGrokBrokerSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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.
Expand Down Expand Up @@ -37,20 +38,19 @@ 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"), renderGrokBrokerWorkerConfig(helper, proxy.port).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}`;
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" },
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;
Expand Down
34 changes: 33 additions & 1 deletion scripts/scriptSourcePolicy.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<string[]> => {
const results: string[] = [];
const walk = async (directory: string): Promise<void> => {
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 });
}
});
42 changes: 42 additions & 0 deletions src/contracts/grokWorkerContract.ts
Original file line number Diff line number Diff line change
@@ -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__<tool>` (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;
39 changes: 36 additions & 3 deletions src/contracts/runtimeContractManifest.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -34,11 +35,43 @@ 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: "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
// 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",
x64Sha256: "e3fe2738fc8a979861085b4003bf2d5d7c284874897cb6ec2e2e2383211768bd",
arm64Sha256: "ad44e02c38e6a3207ac4a3d5fd98b6d2e55341ce42dfd2f07204bbe54a7a653d"
sourceSha256: "36f60689f0a8af0e3108f5f53d78ed52b7d4b6f934c75b6184606dfa82bc741e",
x64Sha256: "36dc76b134eb59cf5a6720b6f94228eb279108e20ea3343fa6efd9ffcb60a4d3",
arm64Sha256: "c93216cc6fa4ca50dc404fe41e68da9150a869b14f46eb42484ae77c3aa400a9"
}
} as const;
export const AGY_SUBSCRIPTION_REALM = {
Expand Down
11 changes: 1 addition & 10 deletions src/pi/cliMcpRegistration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
17 changes: 6 additions & 11 deletions src/pi/cliMcpRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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=<endpoint>` 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
Expand Down Expand Up @@ -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 <name> <url>`.
*
Expand Down
6 changes: 3 additions & 3 deletions src/pi/cliSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading