diff --git a/fixtures/grok-lean-worker/AGENTS.md b/fixtures/grok-lean-worker/AGENTS.md new file mode 100644 index 00000000..253460a8 --- /dev/null +++ b/fixtures/grok-lean-worker/AGENTS.md @@ -0,0 +1,3 @@ +# Grok lean worker live check + +You are a test agent. When woken, answer with the single word OK and call no tools. diff --git a/fixtures/grok-lean-worker/Spawnfile b/fixtures/grok-lean-worker/Spawnfile new file mode 100644 index 00000000..3e83acbc --- /dev/null +++ b/fixtures/grok-lean-worker/Spawnfile @@ -0,0 +1,22 @@ +spawnfile_version: "0.1" +kind: agent +name: grok-lean-worker +description: "One brokered Daimon Grok agent for the Grok 1.0.34 lean-worker live check" + +runtime: + name: daimon + options: + engine: grok + +execution: + model: + primary: + provider: xai + name: grok-4.6 + auth: + method: grok + reasoning_effort: low + +workspace: + docs: + system: AGENTS.md diff --git a/package.json b/package.json index ea2122f4..cc05d83e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "audit:generate": "tsx src/audit/auditCli.ts", "build:local-moltnet": "node --experimental-strip-types ./scripts/build-local-moltnet.ts", "build:local-daimon": "node --experimental-strip-types ./scripts/build-local-daimon-runtime.ts", + "vendor:daimon-contract": "node --import tsx ./scripts/vendor-daimon-grok-contract.ts", "bundle:source-provenance": "node --experimental-strip-types ./scripts/create-source-provenance-bundle.ts", "prepare:linux-amd64-closure": "node --experimental-strip-types ./scripts/create-linux-amd64-dependency-closure.ts", "prepare:linux-amd64-go-closure": "node --experimental-strip-types ./scripts/create-linux-amd64-go-closure.ts", @@ -63,7 +64,8 @@ "test:e2e:distribution-image": "tsx src/e2e/cli.ts distribution-image", "test:e2e:distribution-roundtrip": "tsx src/e2e/cli.ts distribution-roundtrip", "test:e2e:daimon-org": "tsx src/e2e/cli.ts daimon-org", - "test:scripts": "node --experimental-strip-types --test scripts/build-local-daimon-runtime.test.ts scripts/build-local-moltnet.test.ts scripts/compile-explicit-test-mcp.test.ts scripts/native-helper-workflows.test.ts scripts/typescript-policy.test.ts", + "test:live:grok-lean-worker": "node --experimental-strip-types ./scripts/grok-lean-worker-live-check.ts", + "test:scripts": "node --experimental-strip-types --test scripts/build-local-daimon-runtime.test.ts scripts/grok-lean-worker-live-check.test.ts scripts/build-local-moltnet.test.ts scripts/compile-explicit-test-mcp.test.ts scripts/native-helper-workflows.test.ts scripts/typescript-policy.test.ts", "test:unit": "npm run test:vitest; vitest_status=$?; npm run test:verdict; verdict_status=$?; npm run test:coverage-verdict; coverage_status=$?; npm run test:node; node_status=$?; failed_lanes=''; if [ \"$verdict_status\" -eq 2 ]; then failed_lanes='vitest test failures'; fi; if [ \"$node_status\" -eq 2 ]; then if [ -n \"$failed_lanes\" ]; then failed_lanes=\"$failed_lanes; node:test suite\"; else failed_lanes='node:test suite'; fi; fi; result_status=0; if [ -n \"$failed_lanes\" ]; then echo \"FAIL(tests): $failed_lanes\"; result_status=1; fi; if [ \"$verdict_status\" -ne 0 ] && [ \"$verdict_status\" -ne 2 ]; then echo 'FAIL(vitest-verdict): could not determine the vitest result'; result_status=1; fi; if [ \"$node_status\" -ne 0 ] && [ \"$node_status\" -ne 2 ]; then echo 'FAIL(node-lane): could not determine the node:test result'; result_status=1; fi; if [ \"$result_status\" -ne 0 ]; then exit 1; elif [ \"$vitest_status\" -ne 0 ]; then echo \"FAIL(vitest): exited $vitest_status with no test failure\"; exit 1; elif [ \"$coverage_status\" -eq 2 ]; then echo 'FAIL(coverage): thresholds not met (tests all passed)'; exit 2; elif [ \"$coverage_status\" -ne 0 ]; then echo 'FAIL(coverage-verdict): could not evaluate coverage (tests all passed)'; exit 1; else echo 'PASS'; fi", "test": "npm run test:unit && npm run test:scripts" }, diff --git a/runtime-images/daimon/Dockerfile b/runtime-images/daimon/Dockerfile index f2768443..834a264e 100644 --- a/runtime-images/daimon/Dockerfile +++ b/runtime-images/daimon/Dockerfile @@ -139,10 +139,14 @@ RUN test -n "${GROK_CLI_VERSION}" \ && test -x ${RUNTIME_ROOT}/node_modules/@openai/codex/bin/codex.js \ && test -f ${RUNTIME_ROOT}/node_modules/@noopolis/daimon/dist/runtime/cli.js \ && case "${TARGETARCH}" in \ - amd64) broker_arch=x64; broker_sha=e3fe2738fc8a979861085b4003bf2d5d7c284874897cb6ec2e2e2383211768bd ;; \ - arm64) broker_arch=arm64; broker_sha=ad44e02c38e6a3207ac4a3d5fd98b6d2e55341ce42dfd2f07204bbe54a7a653d ;; \ + amd64) broker_arch=x64 ;; \ + arm64) broker_arch=arm64 ;; \ *) echo "Unsupported Daimon engine-broker architecture: ${TARGETARCH}" >&2; exit 1 ;; \ esac \ + && package_manifest=${RUNTIME_ROOT}/node_modules/@noopolis/daimon/dist/runtime/contract-manifest.json \ + && test "$(sha256sum "${package_manifest}" | awk '{print "sha256:" $1}')" = "${DAIMON_MANIFEST_SHA256}" \ + && broker_sha="$(node -e 'const m=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).grokEngineBroker; const v=m?.artifacts?.[process.argv[2]+"Sha256"]; if(typeof v!=="string"||!/^[a-f0-9]{64}$/.test(v))process.exit(1); process.stdout.write(v)' "${package_manifest}" "${broker_arch}")" \ + && node -e 'const m=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).grokEngineBroker, a=m?.grokCliArtifacts?.[process.argv[2]]; if(!a||m.grokCliVersion!==process.argv[3]||a.url!==process.argv[4]||a.sha256!==process.argv[5].replace(/^sha256:/,"")){console.error("Grok CLI pin does not match the Daimon contract manifest ("+(m?.grokCliVersion)+")");process.exit(1)}' "${package_manifest}" "${broker_arch}" "${GROK_CLI_VERSION}" "${GROK_CLI_URL}" "${GROK_CLI_SHA256}" \ && broker_source=${RUNTIME_ROOT}/node_modules/@noopolis/daimon/dist/runtime/native/daimon-engine-broker \ && test -x "${broker_source}" \ && test "$(sha256sum "${broker_source}" | awk '{print $1}')" = "${broker_sha}" \ diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index 61f5a172..04b40a7e 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -24,6 +24,14 @@ tools live in `../archive/legacy-worktree-tools/` and are not active helpers. - Daimon builds accept only explicit versions, credential-free HTTPS URLs, and executable/archive digest pins. Reject URLs with credentials, queries, or fragments before Docker runs. +- Daimon builds also accept only the Grok CLI build the vendored Daimon + contract manifest pins for the target architecture (`readPinnedGrokCli`). +- `vendor-daimon-grok-contract.ts` (run with `node --import tsx`) is the only + way Daimon's contract enters Spawnfile: it reads a Daimon checkout as data and + writes `src/runtime/daimon/contract-manifest.{json,sha256}` plus + `grokWorkerConfigBytes.ts` (Daimon's own worker `config.toml` renderer bytes + and sandbox-profile samples). `--check` fails on drift. It never builds or + writes inside Daimon. - The local builder pushes only to the fixed loopback development repository. Its generated immutable manifest/receipt identity is ignored and never edits `runtimes.yaml`. Clean-source builds select the native Docker diff --git a/scripts/README.md b/scripts/README.md index cc91c3eb..a9c7df70 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,6 +7,8 @@ in the npm package. Scripts run as native TypeScript on Node 22.19+. | Entrypoint | Caller | Purpose / prerequisites | | --- | --- | --- | | `build-local-daimon-runtime.ts` | `npm run build:local-daimon` | Build a locally sourced runtime; explicit artifact pins, Docker, and loopback registry required | +| `vendor-daimon-grok-contract.ts` | `npm run vendor:daimon-contract [-- --check]` | Vendor Daimon's contract manifest and Grok worker renderer bytes from a Daimon checkout (`SPAWNFILE_DAIMON_SOURCE_DIR`) | +| `grok-lean-worker-live-check.ts` | `SPAWNFILE_GROK_LIVE_CHECK=1 npm run test:live:grok-lean-worker` | Opt-in live check: one brokered Grok 1.0.34 agent, worker-home/deny/service attestation dump, one cheap wake, one deduped broker usage row; Docker, a local Daimon identity, and a dedicated Grok login required | | `build-local-moltnet.ts` | `npm run build:local-moltnet` | Build and stamp release binaries from an explicit source checkout | | `create-source-provenance-bundle.ts` | `npm run bundle:source-provenance` | Create deterministic archives with manifests and credential exclusions | | `create-linux-amd64-dependency-closure.ts` | `npm run prepare:linux-amd64-closure` | Prepare reviewed npm dependencies/cache in the pinned build container | diff --git a/scripts/build-local-daimon-runtime.test.ts b/scripts/build-local-daimon-runtime.test.ts index 0762af47..3da558cc 100644 --- a/scripts/build-local-daimon-runtime.test.ts +++ b/scripts/build-local-daimon-runtime.test.ts @@ -5,8 +5,10 @@ import path from "node:path"; import test from "node:test"; import { + assertPinnedGrokCli, createLocalDaimonCapabilityReceipt, readDaimonCliArtifactPins, + readPinnedGrokCli, resolveDaimonSourceMode, resolveLocalBuildArchitecture, resolveLocalImageTag, @@ -355,3 +357,23 @@ test("Daimon Dockerfile stage graph preserves cache and offline-network boundari assert.deepEqual(stagesContaining(/\bapt-get\b/u), ["base_registry"]); assert.deepEqual(stagesContaining(/\bcurl\s+-/u), ["agy_source_registry", "grok_source_registry"]); }); + +test("local Daimon builds accept only the Grok CLI build the vendored contract manifest pins", () => { + const amd64 = readPinnedGrokCli("amd64"), arm64 = readPinnedGrokCli("arm64"); + assert.equal(amd64.version, "1.0.34"); + assert.equal(amd64.url, "https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-1.0.34-linux-x86_64"); + assert.equal(amd64.sha256, "sha256:be5905e107d2b8b5f3c142d21ecfe4c8fd32a913d2fd551b788707930c4dc80d"); + assert.equal(arm64.sha256, "sha256:39ab87666877d64ef3a40aa60fbe0c3b6a6acd7001b78fe60e2c76bb6cfc4a94"); + const pinned = { executable_sha256: amd64.sha256, url: amd64.url, version: amd64.version }; + assert.doesNotThrow(() => assertPinnedGrokCli(pinned, amd64)); + assert.throws(() => assertPinnedGrokCli(pinned, arm64), /pinned Grok CLI 1\.0\.34/u); + assert.throws(() => assertPinnedGrokCli({ ...pinned, version: "1.0.13" }, amd64), /pinned Grok CLI/u); + assert.throws(() => assertPinnedGrokCli({ ...pinned, url: "https://example.invalid/grok" }, amd64), /pinned Grok CLI/u); +}); + +test("the runtime image derives broker and Grok pins from the attested Daimon manifest instead of literals", () => { + const dockerfile = readFileSync(new URL("../runtime-images/daimon/Dockerfile", import.meta.url), "utf8"); + assert.doesNotMatch(dockerfile, /broker_sha=[a-f0-9]{64}/u); + assert.match(dockerfile, /grokCliArtifacts\?\.\[process\.argv\[2\]\]/u); + assert.match(dockerfile, /m\.grokCliVersion!==process\.argv\[3\]/u); +}); diff --git a/scripts/build-local-daimon-runtime.ts b/scripts/build-local-daimon-runtime.ts index ad520aff..8e99db3a 100644 --- a/scripts/build-local-daimon-runtime.ts +++ b/scripts/build-local-daimon-runtime.ts @@ -146,6 +146,30 @@ export const readDaimonCliArtifactPins = (env: Record { + const broker = (JSON.parse(readFileSync(manifestPath, "utf8")) as { grokEngineBroker?: { grokCliArtifacts?: Record; grokCliVersion?: unknown } }).grokEngineBroker; + const artifact = broker?.grokCliArtifacts?.[architecture === "amd64" ? "x64" : "arm64"]; + if (typeof broker?.grokCliVersion !== "string" || typeof artifact?.url !== "string" || typeof artifact.sha256 !== "string" || !sha256Digest.test(artifact.sha256)) { + throw new Error("Vendored Daimon contract manifest does not pin a Grok CLI artifact"); + } + return { sha256: `sha256:${artifact.sha256}`, url: artifact.url, version: broker.grokCliVersion }; +}; + +export const assertPinnedGrokCli = (grok: DaimonCliArtifacts["grok"], pin: GrokCliPin): void => { + if (grok.version !== pin.version || grok.url !== pin.url || grok.executable_sha256 !== pin.sha256) { + throw new Error(`GROK_CLI_VERSION/GROK_CLI_URL/GROK_CLI_SHA256 must be the pinned Grok CLI ${pin.version} (${pin.url}, ${pin.sha256})`); + } +}; + export const resolveLocalImageTag = (value: string | undefined): string => { const tag = value?.trim(); const match = tag?.match(/^127\.0\.0\.1:((?:[1-9]\d{0,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]))\/noopolis\/spawnfile-runtime-daimon:([A-Za-z0-9_][A-Za-z0-9_.-]{0,127})$/u); @@ -315,6 +339,7 @@ const main = (): void => { const imageTag = resolveLocalImageTag(process.env.SPAWNFILE_DAIMON_LOCAL_IMAGE_TAG); const architecture = resolveLocalBuildArchitecture(process.arch); const artifacts = readDaimonCliArtifactPins(); + assertPinnedGrokCli(artifacts.grok, readPinnedGrokCli(architecture)); const packageDirectory = mkdtempSync(path.join(os.tmpdir(), "spawnfile-daimon-package-")); try { const bundled = sourceMode === "source-bundle" ? stageBundleBuiltDaimon(packageDirectory, artifacts, architecture) : null; diff --git a/scripts/grok-lean-worker-live-check.test.ts b/scripts/grok-lean-worker-live-check.test.ts new file mode 100644 index 00000000..7e51300e --- /dev/null +++ b/scripts/grok-lean-worker-live-check.test.ts @@ -0,0 +1,14 @@ +import assert from "node:assert/strict"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { requireLiveCheckEnvironment } from "./grok-lean-worker-live-check.ts"; + +test("the Grok lean-worker live check is explicit opt-in and refuses the desktop Grok login", () => { + const env = { SPAWNFILE_DAIMON_LOCAL_RUNTIME_IDENTITY: "/tmp/identity.json", SPAWNFILE_DAIMON_SOURCE_GROK_AUTH: "/tmp/grok-training/auth.json", SPAWNFILE_GROK_LIVE_CHECK: "1" }; + assert.deepEqual(requireLiveCheckEnvironment(env), { grokAuth: "/tmp/grok-training/auth.json", identity: "/tmp/identity.json" }); + assert.throws(() => requireLiveCheckEnvironment({ ...env, SPAWNFILE_GROK_LIVE_CHECK: undefined }), /SPAWNFILE_GROK_LIVE_CHECK=1/u); + assert.throws(() => requireLiveCheckEnvironment({ ...env, SPAWNFILE_DAIMON_SOURCE_GROK_AUTH: path.join(os.homedir(), ".grok", "auth.json") }), /desktop/u); + assert.throws(() => requireLiveCheckEnvironment({ ...env, SPAWNFILE_DAIMON_LOCAL_RUNTIME_IDENTITY: "relative.json" }), /identity/u); +}); diff --git a/scripts/grok-lean-worker-live-check.ts b/scripts/grok-lean-worker-live-check.ts new file mode 100644 index 00000000..4166adcd --- /dev/null +++ b/scripts/grok-lean-worker-live-check.ts @@ -0,0 +1,96 @@ +#!/usr/bin/env node +// Explicit opt-in live check for a one-agent brokered Grok 1.0.34 organization: +// deploy fixtures/grok-lean-worker against a locally built Daimon runtime image, +// dump the worker home/sandbox/service attestation inputs, run one cheap wake, +// assert the P1b temp/spill layout and broker TMPDIR, and require exactly one +// deduplicated usage row written by the broker. The spill read is a unix-level +// read as the worker uid; a `read_file` spill read needs a >16 KiB tool result. +// Needs Docker, a built CLI (`npm run build`), a local Daimon runtime identity +// built from the Daimon Grok accounting contract, and a dedicated Grok login +// file (never the desktop ~/.grok/auth.json). Makes one real model call. + +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const deployment = "grok-lean-live"; +const workerHome = "/var/lib/daimon-workers/2200"; +const usageLedger = "/var/lib/spawnfile/daimon/usage/usage.jsonl"; + +export const requireLiveCheckEnvironment = (env: Record): { grokAuth: string; identity: string } => { + if (env.SPAWNFILE_GROK_LIVE_CHECK !== "1") throw new Error("Set SPAWNFILE_GROK_LIVE_CHECK=1 to run the live Grok lean-worker check (one real model call)"); + const identity = env.SPAWNFILE_DAIMON_LOCAL_RUNTIME_IDENTITY?.trim(); + const grokAuth = env.SPAWNFILE_DAIMON_SOURCE_GROK_AUTH?.trim(); + if (!identity || !path.isAbsolute(identity)) throw new Error("SPAWNFILE_DAIMON_LOCAL_RUNTIME_IDENTITY must name the local Daimon runtime identity file"); + if (!grokAuth || !path.isAbsolute(grokAuth)) throw new Error("SPAWNFILE_DAIMON_SOURCE_GROK_AUTH must name a dedicated 0600 Grok login file"); + if (path.resolve(grokAuth) === path.join(os.homedir(), ".grok", "auth.json")) throw new Error("Refusing the desktop ~/.grok/auth.json; use a dedicated Grok login"); + return { grokAuth, identity }; +}; + +const run = (command: string, args: string[], input?: string): string => + execFileSync(command, args, { cwd: repoRoot, encoding: "utf8", input, stdio: [input === undefined ? "ignore" : "pipe", "pipe", "inherit"] }); + +const containerFor = (): string => { + const ids = run("docker", ["ps", "--filter", `label=com.spawnfile.deployment=${deployment}`, "--format", "{{.ID}}"]).trim().split("\n").filter(Boolean); + if (ids.length !== 1) throw new Error(`expected one running container for ${deployment}, found ${ids.length}`); + return ids[0]!; +}; + +const exec = (container: string, script: string): string => run("docker", ["exec", container, "bash", "-ceu", script]); + +const main = (): void => { + requireLiveCheckEnvironment(process.env); + const out = mkdtempSync(path.join(os.tmpdir(), "spawnfile-grok-lean-live-")); + try { + run(process.execPath, ["dist/cli/index.js", "up", "fixtures/grok-lean-worker", "--detach", "--deployment", deployment, "--out", out]); + const container = containerFor(); + const sysctl = exec(container, "cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null || echo absent").trim(); + process.stdout.write(`host apparmor_restrict_unprivileged_userns=${sysctl}\n`); + const layout = exec(container, `stat -c '%u:%g %a %F %n' ${workerHome} ${workerHome}/.grok ${workerHome}/.grok/sessions ${workerHome}/.grok/*.toml ${workerHome}/.grok/sessions/sandbox-events.jsonl`); + process.stdout.write(layout); + for (const expected of [ + `2200:2100 710 directory ${workerHome}\n`, `0:2200 1771 directory ${workerHome}/.grok\n`, `0:2200 1771 directory ${workerHome}/.grok/sessions\n`, + `0:0 444 regular file ${workerHome}/.grok/config.toml\n`, `0:0 444 regular file ${workerHome}/.grok/trusted_folders.toml\n`, + `2200:2100 640 regular` + ]) if (!layout.includes(expected)) throw new Error(`worker home layout is missing: ${expected.trim()}`); + const profile = exec(container, `cat ${workerHome}/.grok/sandbox.toml`); + if (!/deny = \["\//u.test(profile)) throw new Error("worker sandbox profile has an empty deny list"); + for (const required of ["/var/lib/spawnfile/moltnet", "/var/lib/spawnfile/memory", "/run/daimon-engine-broker"]) if (!profile.includes(JSON.stringify(required))) throw new Error(`worker sandbox profile does not deny ${required}`); + // /run denies rely on /var/run being the /run symlink (a bind mask covers both spellings); report what the image has. + process.stdout.write(`/var/run -> ${exec(container, "readlink /var/run || echo not-a-symlink").trim()}\n`); + const runtimeHome = "/var/lib/spawnfile/instances/daimon/daimon-organization/runtime-homes/grok-lean-worker"; + const tempLayout = exec(container, `stat -c '%u:%g %a %n' /tmp /var/tmp ${workerHome}/tmp ${runtimeHome} ${runtimeHome}/tool-output`); + process.stdout.write(tempLayout); + for (const expected of [ + "0:2000 1774 /tmp\n", "0:2000 1774 /var/tmp\n", `2200:2200 700 ${workerHome}/tmp\n`, `2000:2200 710 ${runtimeHome}\n`, `2000:2200 2750 ${runtimeHome}/tool-output\n` + ]) if (!tempLayout.includes(expected)) throw new Error(`temp/spill layout is missing: ${expected.trim()}`); + for (const pattern of ["engine-broker serve", "daimon-engine-broker --relay"]) { + const environ = exec(container, `pid=$(pgrep -f -o ${JSON.stringify(pattern)}); tr '\\0' '\\n' < /proc/$pid/environ | grep '^TMPDIR=' || true`).trim(); + if (environ !== "TMPDIR=/run/daimon-engine-broker/tmp") throw new Error(`${pattern} runs without its private TMPDIR (${environ || "unset"})`); + } + // A spill as Daimon writes it (uid 2000, 0640, setgid group) must be readable by the worker, and shared /tmp must not be. + exec(container, `setpriv --reuid 2000 --regid 2000 --clear-groups bash -c 'umask 027; printf spill-ok > ${runtimeHome}/tool-output/live-check.log' && setpriv --reuid 2200 --regid 2200 --clear-groups bash -c 'test "$(cat ${runtimeHome}/tool-output/live-check.log)" = spill-ok && ! printf x > /tmp/worker-probe' && rm -f ${runtimeHome}/tool-output/live-check.log`); + const service = JSON.parse(exec(container, "cat /etc/daimon-engine-broker/service.json")) as { version: string; registrations: Array<{ model: { id: string } }> }; + if (service.version !== "noopolis.daimon.engine-broker-service.v2" || service.registrations[0]?.model.id !== "grok-4.6") throw new Error("service.json is not the declared v2 registration"); + exec(container, `setpriv --reuid 2200 --regid 2200 --clear-groups bash -c '! printf x >> ${workerHome}/.grok/trusted_folders.toml' && setpriv --reuid 2200 --regid 2200 --clear-groups bash -c '! test -r /var/lib/spawnfile/daimon/grok-subscription-realm'`); + const wakeId = `live-${Date.now()}`; + const wake = JSON.stringify({ agentId: "agent:grok-lean-worker", event: { version: "noopolis.daimon.wake.v1", id: wakeId, kind: "manual", text: "Reply OK.", occurredAt: new Date().toISOString() } }); + const result = exec(container, `curl -fsS -X POST -H 'content-type: application/json' -H "authorization: Bearer $SPAWNFILE_DAIMON_CONTROL_TOKEN" --data-binary @- http://127.0.0.1:19700/v1/wake <<'WAKE'\n${wake}\nWAKE`); + process.stdout.write(`wake: ${result}\n`); + const events = exec(container, `tail -n 5 ${workerHome}/.grok/sessions/sandbox-events.jsonl`); + if (!events.includes("\"ProfileApplied\"") || !events.includes("\"deny_paths\"")) throw new Error("no enforced ProfileApplied event with deny_paths for the wake"); + // The ledger volume is exclusive-reattach and survives earlier runs: count only this wake's rows. + const rows = exec(container, `cat ${usageLedger}`).trim().split("\n").filter(Boolean).map((line) => JSON.parse(line) as { engine: string; model?: string; turn?: string; wake: string }); + const grokRows = rows.filter((row) => row.engine === "grok" && row.wake === wakeId && typeof row.turn === "string"); + if (grokRows.length !== 1 || grokRows[0]!.model !== "grok-4.6") throw new Error(`expected exactly one broker usage row for grok-4.6, found ${grokRows.length}`); + process.stdout.write(`${run(process.execPath, ["dist/cli/index.js", "usage", "fixtures/grok-lean-worker", "--out", out, "--deployment", deployment])}\nGROK LEAN WORKER LIVE CHECK: PASS\n`); + } finally { + try { run(process.execPath, ["dist/cli/index.js", "down", "fixtures/grok-lean-worker", "--compiled", out, "--deployment", deployment, "--force"]); } catch { /* reported by Docker */ } + rmSync(out, { force: true, recursive: true }); + } +}; + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) main(); diff --git a/scripts/source-provenance-bundle.integration.test.ts b/scripts/source-provenance-bundle.integration.test.ts index 4ed0741f..98a2b5d9 100644 --- a/scripts/source-provenance-bundle.integration.test.ts +++ b/scripts/source-provenance-bundle.integration.test.ts @@ -15,6 +15,14 @@ const { renderRuntimeLinkMaterializer } = await import( const repository = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const digest = (file: string): string => `sha256:${execFileSync("shasum", ["-a", "256", file], { encoding: "utf8" }).split(" ")[0] ?? ""}`; +const pinnedManifest = JSON.parse(readFileSync(path.join(repository, "src", "runtime", "daimon", "contract-manifest.json"), "utf8")) as { grokEngineBroker: { artifacts: { x64Sha256: string }; grokCliArtifacts: { x64: { url: string } }; grokCliVersion: string } }; +// The runtime image refuses any Grok executable but the manifest-pinned build, +// so this test needs the real linux-x86_64 binary (136+ MB) rather than a stub. +const pinnedGrokCli = (): string => { + const file = process.env.SPAWNFILE_GROK_CLI_FILE; + if (!file || !path.isAbsolute(file)) throw new Error("SPAWNFILE_GROK_CLI_FILE must name the pinned linux-x86_64 Grok CLI executable"); + return file; +}; const sha512 = (file: string): string => `sha512:${createHash("sha512").update(readFileSync(file)).digest("hex")}`; test("actual Daimon lock produces a real offline linux/amd64 shipped artifact and rejects tampering", { timeout: 360_000 }, () => { @@ -47,7 +55,7 @@ test("actual Daimon lock produces a real offline linux/amd64 shipped artifact an assert.match(execFileSync("tar", ["-tzf", path.join(output, "daimon.tgz")], { encoding: "utf8" }), /package\/dist\/runtime\/contract-manifest\.json/u); assert.match(execFileSync("tar", ["-tvzf", path.join(output, "daimon.tgz")], { encoding: "utf8" }), /-rwxr-xr-x[^\n]*package\/dist\/runtime\/native\/daimon-engine-broker/u); const packedBroker = execFileSync("tar", ["-xOf", path.join(output, "daimon.tgz"), "package/dist/runtime/native/daimon-engine-broker"]); - assert.equal(`sha256:${createHash("sha256").update(packedBroker).digest("hex")}`, "sha256:e3fe2738fc8a979861085b4003bf2d5d7c284874897cb6ec2e2e2383211768bd"); + assert.equal(`sha256:${createHash("sha256").update(packedBroker).digest("hex")}`, `sha256:${pinnedManifest.grokEngineBroker.artifacts.x64Sha256}`); const packageContext = path.join(temporary, "package-context"), probe = path.join(temporary, "probe"); mkdirSync(packageContext); mkdirSync(probe); cpSync(path.join(output, "daimon.tgz"), path.join(packageContext, "daimon.tgz")); cpSync(path.join(output, "runtime-dependencies.tar"), path.join(packageContext, "dependencies.tar")); cpSync(path.join(output, "source-inputs.json"), path.join(packageContext, "source-inputs.json")); execFileSync("docker", ["build", "--network=none", "--platform", "linux/amd64", "--target", "offline_dependency_probe", "--build-context", `daimon_package=${packageContext}`, @@ -55,7 +63,7 @@ test("actual Daimon lock produces a real offline linux/amd64 shipped artifact an "--build-arg", `DAIMON_DEPENDENCY_ARCHIVE_SHA256=${digest(path.join(output, "runtime-dependencies.tar"))}`, "-f", path.join(repository, "runtime-images", "daimon", "Dockerfile"), repository], { stdio: "inherit" }); assert.deepEqual(JSON.parse(readFileSync(path.join(probe, "probe", "source-inputs.json"), "utf8")), identity); const grok = path.join(packageContext, "grok"), agyTree = path.join(temporary, "agy-tree"), agy = path.join(agyTree, "antigravity"), agyTar = path.join(packageContext, "agy.tar.gz"); - writeFileSync(grok, "#!/bin/sh\nexit 0\n", { mode: 0o755 }); mkdirSync(agyTree); writeFileSync(agy, "#!/bin/sh\nexit 0\n", { mode: 0o755 }); + cpSync(pinnedGrokCli(), grok); mkdirSync(agyTree); writeFileSync(agy, "#!/bin/sh\nexit 0\n", { mode: 0o755 }); execFileSync("tar", ["-czf", agyTar, "-C", agyTree, "antigravity"]); const runtimeArchive = digest(path.join(output, "runtime-dependencies.tar")); const dependencyLock = dependencyReceipt.manifest.dependency_lock; @@ -71,17 +79,17 @@ test("actual Daimon lock produces a real offline linux/amd64 shipped artifact an const port = mapped.slice(mapped.lastIndexOf(":") + 1); const identityPath = path.join(repository, ".local-daimon-runtime-identity.json"), priorIdentity = existsSync(identityPath) ? readFileSync(identityPath) : null; try { - execFileSync("npm", ["run", "--silent", "build:local-daimon"], { cwd: repository, env: { ...process.env, AGY_CLI_SHA256: agySha, AGY_CLI_SHA512: sha512(agyTar), AGY_CLI_URL: "https://invalid.example/agy", AGY_CLI_VERSION: "fixture", CODEX_CLI_SHA256: codexSha, GROK_CLI_SHA256: grokSha, GROK_CLI_URL: "https://invalid.example/grok", GROK_CLI_VERSION: "fixture", SPAWNFILE_AGY_CLI_ARCHIVE: agyTar, SPAWNFILE_DAIMON_DEPENDENCY_BUNDLE: dependencyTar, SPAWNFILE_DAIMON_LOCAL_IMAGE_TAG: `127.0.0.1:${port}/noopolis/spawnfile-runtime-daimon:archive-wrapper`, SPAWNFILE_DAIMON_SOURCE_BUNDLE: sourceTar, SPAWNFILE_GROK_CLI_FILE: grok }, stdio: "ignore" }); + execFileSync("npm", ["run", "--silent", "build:local-daimon"], { cwd: repository, env: { ...process.env, AGY_CLI_SHA256: agySha, AGY_CLI_SHA512: sha512(agyTar), AGY_CLI_URL: "https://invalid.example/agy", AGY_CLI_VERSION: "fixture", CODEX_CLI_SHA256: codexSha, GROK_CLI_SHA256: grokSha, GROK_CLI_URL: pinnedManifest.grokEngineBroker.grokCliArtifacts.x64.url, GROK_CLI_VERSION: pinnedManifest.grokEngineBroker.grokCliVersion, SPAWNFILE_AGY_CLI_ARCHIVE: agyTar, SPAWNFILE_DAIMON_DEPENDENCY_BUNDLE: dependencyTar, SPAWNFILE_DAIMON_LOCAL_IMAGE_TAG: `127.0.0.1:${port}/noopolis/spawnfile-runtime-daimon:archive-wrapper`, SPAWNFILE_DAIMON_SOURCE_BUNDLE: sourceTar, SPAWNFILE_GROK_CLI_FILE: grok }, stdio: "ignore" }); const wrapperIdentity = JSON.parse(readFileSync(identityPath, "utf8")); assert.equal(wrapperIdentity.image_architecture, "amd64"); assert.match(wrapperIdentity.image_reference, new RegExp(`^127\\.0\\.0\\.1:${port}/noopolis/spawnfile-runtime-daimon@sha256:[a-f0-9]{64}$`, "u")); } finally { if (priorIdentity) writeFileSync(identityPath, priorIdentity); else rmSync(identityPath, { force: true }); } - const receipt = { architecture: "amd64", daimon: { package_sha256: packageSha, source_inputs: sourceInputs, source_sha256: digest(path.join(packageContext, "source-inputs.json")) }, engines: { agy: { executable_sha256: agySha }, codex: { executable_sha256: codexSha }, grok: { executable_sha256: grokSha } }, manifest_sha256: manifestSha, provenance: { agy: { archive: { format: "tar.gz", sha512: sha512(agyTar), url: "https://invalid.example/agy", version: "fixture" } }, grok: { executable: { sha256: grokSha, url: "https://invalid.example/grok", version: "fixture" } } }, version: "spawnfile.daimon-runtime-capability-receipt.v1" }; + const receipt = { architecture: "amd64", daimon: { package_sha256: packageSha, source_inputs: sourceInputs, source_sha256: digest(path.join(packageContext, "source-inputs.json")) }, engines: { agy: { executable_sha256: agySha }, codex: { executable_sha256: codexSha }, grok: { executable_sha256: grokSha } }, manifest_sha256: manifestSha, provenance: { agy: { archive: { format: "tar.gz", sha512: sha512(agyTar), url: "https://invalid.example/agy", version: "fixture" } }, grok: { executable: { sha256: grokSha, url: pinnedManifest.grokEngineBroker.grokCliArtifacts.x64.url, version: pinnedManifest.grokEngineBroker.grokCliVersion } } }, version: "spawnfile.daimon-runtime-capability-receipt.v1" }; const shipped = path.join(temporary, "shipped"), shippedTar = path.join(temporary, "shipped.tar"); mkdirSync(shipped); execFileSync("docker", ["build", "--network=none", "--platform", "linux/amd64", "--build-context", `daimon_package=${packageContext}`, "--output", `type=tar,dest=${shippedTar}`, - "--build-arg", `DAIMON_CAPABILITY_RECEIPT_BASE64=${Buffer.from(`${JSON.stringify(receipt)}\n`).toString("base64")}`, "--build-arg", `DAIMON_MANIFEST_SHA256=${manifestSha}`, "--build-arg", `DAIMON_PACKAGE_SHA256=${packageSha}`, "--build-arg", `DAIMON_SOURCE_SHA256=${receipt.daimon.source_sha256}`, "--build-arg", "DAIMON_DEPENDENCY_MODE=offline-bundle", "--build-arg", `DAIMON_DEPENDENCY_ARCHIVE_SHA256=${runtimeArchive}`, "--build-arg", `CODEX_CLI_SHA256=${codexSha}`, "--build-arg", "GROK_CLI_VERSION=fixture", "--build-arg", "GROK_CLI_URL=https://invalid.example/grok", "--build-arg", `GROK_CLI_SHA256=${grokSha.slice(7)}`, "--build-arg", "AGY_CLI_VERSION=fixture", "--build-arg", "AGY_CLI_URL=https://invalid.example/agy", "--build-arg", `AGY_CLI_SHA512=${sha512(agyTar).slice(7)}`, "--build-arg", `AGY_CLI_SHA256=${agySha.slice(7)}`, "-f", path.join(repository, "runtime-images", "daimon", "Dockerfile"), repository], { stdio: "inherit" }); + "--build-arg", `DAIMON_CAPABILITY_RECEIPT_BASE64=${Buffer.from(`${JSON.stringify(receipt)}\n`).toString("base64")}`, "--build-arg", `DAIMON_MANIFEST_SHA256=${manifestSha}`, "--build-arg", `DAIMON_PACKAGE_SHA256=${packageSha}`, "--build-arg", `DAIMON_SOURCE_SHA256=${receipt.daimon.source_sha256}`, "--build-arg", "DAIMON_DEPENDENCY_MODE=offline-bundle", "--build-arg", `DAIMON_DEPENDENCY_ARCHIVE_SHA256=${runtimeArchive}`, "--build-arg", `CODEX_CLI_SHA256=${codexSha}`, "--build-arg", `GROK_CLI_VERSION=${pinnedManifest.grokEngineBroker.grokCliVersion}`, "--build-arg", `GROK_CLI_URL=${pinnedManifest.grokEngineBroker.grokCliArtifacts.x64.url}`, "--build-arg", `GROK_CLI_SHA256=${grokSha.slice(7)}`, "--build-arg", "AGY_CLI_VERSION=fixture", "--build-arg", "AGY_CLI_URL=https://invalid.example/agy", "--build-arg", `AGY_CLI_SHA512=${sha512(agyTar).slice(7)}`, "--build-arg", `AGY_CLI_SHA256=${agySha.slice(7)}`, "-f", path.join(repository, "runtime-images", "daimon", "Dockerfile"), repository], { stdio: "inherit" }); execFileSync("tar", ["-xf", shippedTar, "-C", shipped], { stdio: "inherit" }); assert.deepEqual(JSON.parse(readFileSync(path.join(shipped, "opt", "spawnfile", "runtime-installs", "daimon", "source-inputs.json"), "utf8")), receipt.daimon.source_inputs); const shippedRoot=path.join(shipped,"opt","spawnfile","runtime-installs","daimon"); - assert.equal(digest(path.join(shippedRoot,"bin","daimon-engine-broker")),"sha256:e3fe2738fc8a979861085b4003bf2d5d7c284874897cb6ec2e2e2383211768bd"); + assert.equal(digest(path.join(shippedRoot,"bin","daimon-engine-broker")),`sha256:${pinnedManifest.grokEngineBroker.artifacts.x64Sha256}`); assert.equal(readFileSync(path.join(shippedRoot,"contract-manifest.sha256"),"utf8"),`${manifestSha}\n`); const orgContext = path.join(temporary, "literal-org-context"), orgTag = `spawnfile-literal-org-${Date.now().toString(36)}`; mkdirSync(orgContext); diff --git a/scripts/vendor-daimon-grok-contract.ts b/scripts/vendor-daimon-grok-contract.ts new file mode 100644 index 00000000..74d02e56 --- /dev/null +++ b/scripts/vendor-daimon-grok-contract.ts @@ -0,0 +1,90 @@ +#!/usr/bin/env node +// Vendors Daimon's Grok broker contract into Spawnfile from a Daimon checkout: +// the canonical runtime contract manifest (+ digest sidecar) and the exact +// worker `config.toml` bytes Daimon's single renderer produces for every +// declared model x reasoning effort, plus sandbox-profile samples that pin +// Spawnfile's profile mirror to Daimon's renderer. Run with +// `node --import tsx scripts/vendor-daimon-grok-contract.ts [--check]`; +// SPAWNFILE_DAIMON_SOURCE_DIR selects the checkout (default ../daimon). +// Reads Daimon source as data; never builds, writes, or commits in Daimon. + +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const vendorDirectory = path.join(repoRoot, "src", "runtime", "daimon"); +const models = ["grok-4.6", "grok-4.5", "grok-build"] as const; +const efforts = ["low", "medium", "high"] as const; + +/** Deny sets whose rendered bytes the Spawnfile mirror must reproduce exactly. */ +export const PROFILE_SAMPLE_DENY_PATHS: readonly (readonly string[])[] = [ + [], + ["/var/lib/spawnfile/daimon/usage", "/run/daimon-engine-broker", "/run/daimon-engine-broker", "/var/lib/daimon-workers/2201"] +]; + +type DaimonModules = { + contractManifestArtifacts: () => { digest: Buffer; manifest: Buffer }; + renderGrokBrokerWorkerConfig: (policy: { model: string; reasoningEffort: string }) => string; + renderGrokWorkerSandboxProfile: (denyPaths: readonly string[]) => string; +}; + +const loadDaimon = async (daimonDirectory: string): Promise => { + const load = (relative: string): Promise> => import(pathToFileURL(path.join(daimonDirectory, relative)).href); + const [emitter, config, profile] = await Promise.all([ + load("scripts/emitRuntimeContractManifest.ts"), + load("src/runtime/grokBrokerWorkerConfig.ts"), + load("src/runtime/grokWorkerSandboxProfile.ts") + ]); + return { + contractManifestArtifacts: emitter.contractManifestArtifacts as DaimonModules["contractManifestArtifacts"], + renderGrokBrokerWorkerConfig: config.renderGrokBrokerWorkerConfig as DaimonModules["renderGrokBrokerWorkerConfig"], + renderGrokWorkerSandboxProfile: profile.renderGrokWorkerSandboxProfile as DaimonModules["renderGrokWorkerSandboxProfile"] + }; +}; + +export const renderVendoredWorkerModule = (daimon: Pick): string => { + const configs = Object.fromEntries(models.map((model) => [model, Object.fromEntries(efforts.map((reasoningEffort) => + [reasoningEffort, daimon.renderGrokBrokerWorkerConfig({ model, reasoningEffort })]))])); + const samples = PROFILE_SAMPLE_DENY_PATHS.map((denyPaths) => ({ bytes: daimon.renderGrokWorkerSandboxProfile(denyPaths), denyPaths })); + return [ + "/* v8 ignore file -- generated data module */", + "// Generated by scripts/vendor-daimon-grok-contract.ts from Daimon's own renderers. Do not edit by hand.", + "", + "/** Daimon `renderGrokBrokerWorkerConfig({ model, reasoningEffort })` bytes, verified against the manifest pins on use. */", + `export const DAIMON_GROK_WORKER_CONFIG_BYTES = ${JSON.stringify(configs, null, 2)} as const;`, + "", + "/** Daimon `renderGrokWorkerSandboxProfile(denyPaths)` samples; the Spawnfile mirror must reproduce them byte for byte. */", + `export const DAIMON_GROK_WORKER_PROFILE_SAMPLES = ${JSON.stringify(samples, null, 2)} as const;`, + "" + ].join("\n"); +}; + +const main = async (): Promise => { + const args = process.argv.slice(2); + if (args.length > 1 || (args.length === 1 && args[0] !== "--check")) throw new Error("usage: vendor-daimon-grok-contract.ts [--check]"); + const configured = process.env.SPAWNFILE_DAIMON_SOURCE_DIR?.trim(); + if (configured && !path.isAbsolute(configured)) throw new Error("SPAWNFILE_DAIMON_SOURCE_DIR must be absolute"); + const daimon = await loadDaimon(configured || path.resolve(repoRoot, "..", "daimon")); + const artifacts = daimon.contractManifestArtifacts(); + const outputs: Array<[string, Buffer | string]> = [ + [path.join(vendorDirectory, "contract-manifest.json"), artifacts.manifest], + [path.join(vendorDirectory, "contract-manifest.sha256"), artifacts.digest], + [path.join(vendorDirectory, "grokWorkerConfigBytes.ts"), renderVendoredWorkerModule(daimon)] + ]; + if (args[0] === "--check") { + const drifted = outputs.filter(([file, bytes]) => !readFileSync(file).equals(Buffer.from(bytes))); + if (drifted.length > 0) throw new Error(`vendored Daimon contract drifted: ${drifted.map(([file]) => path.relative(repoRoot, file)).join(", ")}`); + process.stdout.write("vendored Daimon Grok contract matches the checkout\n"); + return; + } + for (const [file, bytes] of outputs) writeFileSync(file, bytes); + process.stdout.write(`vendored Daimon Grok contract ${artifacts.digest.toString("ascii").trim()}\n`); +}; + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + main().catch((error: unknown) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; + }); +} diff --git a/specs/CONTAINERS.md b/specs/CONTAINERS.md index 1097aa55..df9dca74 100644 --- a/specs/CONTAINERS.md +++ b/specs/CONTAINERS.md @@ -161,10 +161,28 @@ official AGY artifact exists for another architecture, this seam accepts only `linux/amd64` and fails closed elsewhere. The helper requires explicit `AGY_CLI_VERSION`, `AGY_CLI_URL`, -`AGY_CLI_SHA512`, and extracted `AGY_CLI_SHA256` pins. It preserves the Grok -artifact's `GROK_CLI_URL`/`GROK_CLI_SHA256` pin and additionally requires -`GROK_CLI_VERSION` for provenance. `CODEX_CLI_SHA256` remains required. All -artifact URLs must be credential-free HTTPS URLs without query or fragment. +`AGY_CLI_SHA512`, and extracted `AGY_CLI_SHA256` pins. `GROK_CLI_VERSION`, +`GROK_CLI_URL`, and `GROK_CLI_SHA256` must equal the Grok CLI build the vendored +Daimon contract manifest pins for the target architecture (1.0.34: +`grok-1.0.34-linux-aarch64` `39ab8766…c4a94`, `grok-1.0.34-linux-x86_64` +`be5905e1…c80d`); the image build re-checks them against the packaged manifest, +derives the native broker digest from it rather than a literal, and generated +organization images re-verify `/usr/local/bin/grok` against the same pin. +`CODEX_CLI_SHA256` remains required. All artifact URLs must be credential-free +HTTPS URLs without query or fragment. + +`npm run vendor:daimon-contract` (optionally `-- --check`) is the only way the +Daimon contract enters Spawnfile: it reads a Daimon checkout +(`SPAWNFILE_DAIMON_SOURCE_DIR`) and writes the canonical contract manifest and +digest plus Daimon's worker `config.toml` renderer bytes. + +Daimon organizations containing a Grok agent run with +`--security-opt=seccomp=` and +`--security-opt=apparmor=unconfined`; the profile ships with Spawnfile and is +materialized for each `docker run`. Codex's fully unconfined options are used +instead only when a strict Codex agent is present. The Docker host must have +`kernel.apparmor_restrict_unprivileged_userns=0`; the container entrypoint +fails with that instruction otherwise. OpenClaw and PicoClaw have equivalent overrides: diff --git a/specs/RUNTIMES.md b/specs/RUNTIMES.md index 550028f3..3340b159 100644 --- a/specs/RUNTIMES.md +++ b/specs/RUNTIMES.md @@ -200,12 +200,91 @@ The consumed Daimon manifest declares one per-agent opaque slot for Codex. For Grok it declares one durable rotating-credential realm and one read-only operator bootstrap slot; Daimon serializes Grok turns through that authority, atomically reconciles provider rotation, and leaves sessions/cache per agent. -The generated Linux container installs `bubblewrap`, which Grok requires to -fail closed while applying the realm and peer-home deny set. +The generated Linux container installs `bubblewrap` and `ca-certificates`: +Grok CLI 1.0.34 runs every sandbox profile inside bubblewrap (even an empty +deny list), and its HTTP MCP client refuses to build without CA certificates +even for Daimon's loopback `http://` endpoint. The realm mount is `exclusive-reattach`: its host-stable volume survives run and deployment identities, cannot be attached by two live deployments, and is never copied by product-state migration. Standard concurrent canary is rejected; stop the old deployment and reattach the same realm for replacement. +### Daimon brokered Grok workers (Grok CLI 1.0.34) + +Every Daimon Grok agent runs through Daimon's engine broker as a lean worker +pinned to Grok CLI 1.0.34. Its model is declared, never inherited: + +```yaml +runtime: + name: daimon + options: + engine: grok +execution: + model: + primary: + provider: xai + name: grok-4.6 # grok-4.6 | grok-4.5 | grok-build + auth: + method: grok + reasoning_effort: low # low | medium | high +``` + +Both fields are required together, target-level, with no fallback; they lower +to Daimon's `engine.model`/`engine.reasoningEffort`. `grok` auth is only valid +for provider `xai` and is Daimon-owned (never a host import or an api-key +secret); `reasoning_effort` is only valid with it. Codex and AGY are unchanged. + +The worker `config.toml` is Daimon's own renderer output for that model x +effort, vendored and refused unless it hashes to the contract manifest pin the +broker attests every turn. Each worker's `GROK_HOME` is root-owned `1771` with +root-owned `0444` `config.toml`, `sandbox.toml`, `trusted_folders.toml`, +`managed_config.toml`, and `requirements.toml`, so the worker cannot change +model, trust, sandbox, or managed layers between turns; sandbox events are read +from `$GROK_HOME/sessions/sandbox-events.jsonl`. `service.json` is v2 with a +per-registration usage ledger (the container ledger in production), limits +(the manifest's v1 defaults), and model. + +On 1.0.13 a non-empty sandbox `deny` list made Grok refuse to start, so the +list was empty and unix modes were the only boundary. On 1.0.34 the deny list +is enforced inside bubblewrap for both shell and `read_file`, and it is +mandatory: Grok's strict base reads all of `/run`, `/var`, `/tmp`, and `/etc`, +and macOS bind mounts ignore unix modes. Each worker denies the Grok bootstrap +and realm, AGY realm and unlock secret when present, the wake-acceptance store, +every peer agent's runtime home and workspace, the organization config +directory, every persistent mount (its own tool state, credential home and +memory included), other runtime instance roots, the shared Moltnet, agent-token +and memory roots under `/var/lib/spawnfile`, every workspace resource backing +path not linked into its own workspace, every other worker's home, the broker's +`/etc` and `/run` directories, the usage-ledger and wake-fuse volumes, and +`/run/secrets`, `/run/spawnfile`, `/run/spawnfile-secrets`, `/run/world`. It +reaches Moltnet and memory only through Daimon's MCP tools. Every deny +entry and registration path must be canonical (present, not a symlink, its own +realpath) at provisioning or the container refuses to start. + +Workspace skills are not emitted for Grok agents: the worker workspace stays +untrusted and Daimon overrides the system prompt, so Grok loads neither +`.agents/skills` nor `.codex/skills`; a declared skill produces a compile +warning. + +Grok containers run with Docker's default seccomp profile plus bubblewrap's +namespace syscalls (a pinned profile) and `apparmor=unconfined` unless a strict +Codex agent already requires both fully unconfined. The Docker host must allow +unprivileged user namespaces: set `kernel.apparmor_restrict_unprivileged_userns=0` +(Ubuntu 24.04+ and Colima default to `1`). The entrypoint refuses to start a +Grok organization, naming the sysctl, when it is not. + +Grok refuses a profile whose deny list contains a path equal to or above a +base-profile grant (`/tmp`, `/var/tmp`, `/run`, `/etc`, `/var`, the workspace, +`GROK_HOME`, `sessions`, the worker's `TMPDIR`); Spawnfile refuses such an entry +at compile time. Shared temp is therefore closed by modes: `/tmp` and +`/var/tmp` are `root:2000 1774` (a worker can list names, not read or create), +each worker writes only its launcher-compiled `TMPDIR` `/tmp` +(`: 0700`), and the broker and relay, the only non-root +processes outside group 2000, run with `TMPDIR=/run/daimon-engine-broker/tmp`. +Tool-result spills go to `/tool-output` `2000: 2750` +under a runtime home `2000: 0710`, so only the agent's own worker can +read them. Registered workspace and home paths are canonical and at most 255 +bytes. + For AGY it declares one host-realm durable mount plus one independent opaque unlock source slot. Spawnfile emits the stable RW volume, metadata-authorizes the caller-owned `0600` unlock source, and mounts it read-only; it never reads diff --git a/specs/USAGE_ACCOUNTING_DESIGN.md b/specs/USAGE_ACCOUNTING_DESIGN.md index 68923447..da9124fc 100644 --- a/specs/USAGE_ACCOUNTING_DESIGN.md +++ b/specs/USAGE_ACCOUNTING_DESIGN.md @@ -269,6 +269,39 @@ usage-adjacent command is `du`. not rewritten as failed. Each must turn a test red. 7. **Regression** — targeted `src/pi` and `src/runtime`, then the full suite. +## Revision 4 — Grok 1.0.34 broker accounting (2026-09-17) + +The broker remains the single sealed writer. What changed for readers: + +- **Dedupe by `turn`.** A broker row carries `turn` (64-hex turn id). The broker + seals a turn's ledger bytes into its turn record and re-appends them on replay + when it cannot find the row, so two replays can append identical bytes. + `spawnfile usage` keeps the first row per `turn` — within each generation, + across `usage.jsonl.1` and `usage.jsonl`, and inside every aggregate. Rows + without a key are kept. Rows sharing a key but differing are a conflict: the + first is counted, the keys are named in a warning, and coverage is PARTIAL + (`coverage.conflictingTurnCount`). +- **Additive fields.** `limit_reason` (`tokens`/`requests`/`timeout`/`none`), + `model` (the declared model the broker verified), `outcome`, and + `estimated_requests` stay inside the unchanged `turn-usage.v1` record. A + malformed optional field is dropped, never the row. +- **Estimated usage is shown as estimated.** A request whose response carried no + valid usage is charged `ceil(bodyBytes/2) + 4096` tokens; the row counts those + in `estimated_requests`. The table prefixes such tokens with `~` and states + how many turns and requests are estimates; JSON exposes `estimatedTurns`, + `estimatedRequests`, and `coverage.estimatedTurnCount`. +- **Per-request stream.** `requests.jsonl` Grok rows carry `turn`, `model`, + proxy-measured `started_at`/`ended_at`, and `usage_source` + (`stream`/`upstream`/`estimated`); `src/runtime/usageRequestLedger.ts` parses + them and dedupes by `(turn, request)`. +- **Ledger location.** `service.json` v2 names a `usageLedgerPath` per + registration. Production points every registration at the container ledger, + because both `spawnfile usage` and Daimon's wake fuse read only that file. +- **Deny list.** Grok 1.0.13 could not start with a non-empty sandbox deny list, + so the ledger directory's protection was its unix mode alone. On 1.0.34 the + ledger directory is an enforced deny entry for every worker, as originally + designed above. + ## Findings folded in (revision 2 → 3) Found independently by two reviewers, by tracing container machinery: diff --git a/src/cli/usageCommand.test.ts b/src/cli/usageCommand.test.ts index d4354b8f..52468baf 100644 --- a/src/cli/usageCommand.test.ts +++ b/src/cli/usageCommand.test.ts @@ -170,6 +170,33 @@ describe("spawnfile usage", () => { expect(result.output).toMatch(/^agy\s+\S*\s*1\s+45\.4k\s+—/mu); }); + it("counts a replayed broker turn once and marks estimated usage distinctly", async () => { + const turn = "a".repeat(64); + const sealed = line({ turn, limit_reason: "none", model: "grok-4.6", total: 100_000, notional_usd: 1, estimated_requests: 2, outcome: "completed" }); + const result = await executeUsageCommand("/tmp/project", { json: true }, handlersFor({ + [DAIMON_GROK_TURN_USAGE_LEDGER.rotatedFilePath]: `${sealed}\n`, + [DAIMON_GROK_TURN_USAGE_LEDGER.filePath]: `${sealed}\n${sealed}\n${line({ agent: "foreman", wake: "w2", total: 5_000, turn: "b".repeat(64) })}\n` + })); + const rendered = JSON.parse(result.output!); + expect(rendered.byEngine).toEqual([expect.objectContaining({ engine: "grok", estimatedRequests: 2, estimatedTurns: 1, tokens: 105_000, turns: 2 })]); + expect(rendered.coverage).toMatchObject({ estimatedTurnCount: 1 }); + const table = await executeUsageCommand("/tmp/project", {}, handlersFor({ + [DAIMON_GROK_TURN_USAGE_LEDGER.filePath]: `${sealed}\n${sealed}\n` + })); + expect(table.output).toMatch(/^cogsworth\s+grok\s+1\s+~100\.0k/mu); + expect(table.output).toMatch(/^grok\s+1\s+~100\.0k/mu); + expect(table.output).toContain("~ 1 turn(s) include ESTIMATED usage: 2 request(s) returned no provider-reported usage"); + }); + + it("warns and reports PARTIAL when differing rows share one turn key", async () => { + const turn = "c".repeat(64); + const table = await executeUsageCommand("/tmp/project", {}, handlersFor({ + [DAIMON_GROK_TURN_USAGE_LEDGER.filePath]: `${line({ turn, total: 10 })}\n${line({ turn, total: 99 })}\n` + })); + expect(table.output).toContain("WARNING 1 turn(s) carry differing ledger rows under one turn key"); + expect(table.output).toContain("coverage PARTIAL"); + }); + it("counts an all-zero turn as unknown rather than free", async () => { const result = await executeUsageCommand("/tmp/project", {}, handlersFor({ [DAIMON_GROK_TURN_USAGE_LEDGER.filePath]: `${line({ complete: false, input: 0, output: 0, cache_read: 0, cache_write: 0, total: 0 })}\n` diff --git a/src/cli/usageCommand.ts b/src/cli/usageCommand.ts index 18193321..2fdbeeea 100644 --- a/src/cli/usageCommand.ts +++ b/src/cli/usageCommand.ts @@ -10,7 +10,9 @@ import { readUsageLedgerViaExec, type UsageLedgerExec } from "../runtime/usageLe import { DEFAULT_OUTPUT_DIRECTORY, errorExitCode } from "../shared/index.js"; import { computeUsageCoverage, + dedupeUsageRecordsByTurn, DEFAULT_USAGE_SINCE, + findConflictingUsageTurns, filterUsageRecordsSince, groupUsageByAgent, groupUsageByEngine, @@ -109,7 +111,7 @@ const renderTable = ( ): string => { const groupBy = options.by ?? "agent"; const coverage = computeUsageCoverage(windowed, usage.roster.length, usage.unreadableUnits.length); - const totalTokens = windowed.reduce((sum, record) => sum + record.total, 0); + const totalTokens = dedupeUsageRecordsByTurn(windowed).reduce((sum, record) => sum + record.total, 0); const lines: string[] = []; const coverageLabel = coverage.partial @@ -128,7 +130,7 @@ const renderTable = ( const width = Math.max(8, ...rows.map((row) => row.agent.length)); lines.push(`${pad("agent", width)} ${pad("engine", 8)}${padStart("turns", 7)}${padStart("tokens", 9)}${padStart("notional", 11)}${padStart("share", 7)}`); for (const row of rows) { - lines.push(`${pad(row.agent, width)} ${pad(row.engine ?? "—", 8)}${padStart(row.turns === 0 ? "—" : String(row.turns), 7)}${padStart(formatTokens(row.tokens), 9)}${padStart(formatUsd(row.notionalUsd, row.turns), 11)}${padStart(formatShare(row.tokens, totalTokens), 7)}`); + lines.push(`${pad(row.agent, width)} ${pad(row.engine ?? "—", 8)}${padStart(row.turns === 0 ? "—" : String(row.turns), 7)}${padStart(`${row.estimatedTurns > 0 ? "~" : ""}${formatTokens(row.tokens)}`, 9)}${padStart(formatUsd(row.notionalUsd, row.turns), 11)}${padStart(formatShare(row.tokens, totalTokens), 7)}`); } lines.push("─".repeat(width + 44)); } @@ -137,12 +139,20 @@ const renderTable = ( .sort((left, right) => right.tokens - left.tokens || left.engine.localeCompare(right.engine)); const engineWidth = Math.max(8, ...engineRows.map((row) => row.engine.length)); for (const row of engineRows) { - lines.push(`${pad(row.engine, engineWidth)} ${pad("", 8)}${padStart(String(row.turns), 7)}${padStart(formatTokens(row.tokens), 9)}${padStart(formatUsd(row.notionalUsd, row.turns), 11)}`); + lines.push(`${pad(row.engine, engineWidth)} ${pad("", 8)}${padStart(String(row.turns), 7)}${padStart(`${row.estimatedTurns > 0 ? "~" : ""}${formatTokens(row.tokens)}`, 9)}${padStart(formatUsd(row.notionalUsd, row.turns), 11)}`); } if (engineRows.length === 0) lines.push("no metered turns in this window"); lines.push(""); lines.push("Counts are a lower bound: the engine stream carries no completeness marker."); + const conflicts = findConflictingUsageTurns(windowed); + if (conflicts.length > 0) { + lines.push(`WARNING ${conflicts.length} turn(s) carry differing ledger rows under one turn key; the first row is counted and coverage is PARTIAL: ${conflicts.map((turn) => turn.slice(0, 12)).join(", ")}`); + } + if (coverage.estimatedTurnCount > 0) { + const estimatedRequests = engineRows.reduce((sum, row) => sum + row.estimatedRequests, 0); + lines.push(`~ ${coverage.estimatedTurnCount} turn(s) include ESTIMATED usage: ${estimatedRequests} request(s) returned no provider-reported usage and were charged a conservative estimate, not a measurement.`); + } if (coverage.incompleteRecordCount > 0) { lines.push(`${coverage.incompleteRecordCount} turn(s) reported all-zero usage and are counted as unknown, not free.`); } diff --git a/src/compiler/AGENTS.md b/src/compiler/AGENTS.md index 273b82b8..93342f35 100644 --- a/src/compiler/AGENTS.md +++ b/src/compiler/AGENTS.md @@ -33,7 +33,9 @@ src/compiler/ ├── containerPersistentMounts.ts # Durable-mount merge across sources + volume-name uniqueness ├── deploymentLineage.ts # Dev/production lineage namespacing + declared-volume refusal ├── containerEntrypointShell.ts # Shell quoting, recipe env, and CLI credential materialization helpers -├── containerDaimonBrokerRender.ts # Fixed Daimon broker identities, registrations, worker config, and root-launch provisioning +├── containerDaimonBrokerRender.ts # Fixed Daimon broker identities, registrations binary, credential realm, and root-launch provisioning +├── containerDaimonGrokWorkerRender.ts # Brokered Grok registrations: pinned worker config, sandbox deny list, service.json v2 +├── containerDaimonGrokWorkerProvisioning.ts # Root program lines for the attested worker GROK_HOME layout and canonical-path checks ├── containerArtifactsPlans.ts # Environment inventory and runtime target-plan orchestration ├── containerTargetPlanResolution.ts # Per-target paths, packages, auth, secrets, and exposure resolution ├── teamRoster.ts # Context-scoped team roster generation and diagnostics @@ -160,6 +162,48 @@ src/compiler/ existing uid, capability-drop, and `no-new-privileges` posture; Codex owns the per-turn filesystem/network/tool boundary, while trusted MCP servers and Daimon code outside that native boundary remain trusted container processes. +- Without a strict Codex agent, a Daimon organization with a Grok agent gets + `--security-opt=seccomp=` plus `apparmor=unconfined` instead: + Docker's default seccomp profile with bubblewrap's seven namespace syscalls + (`src/shared/daimonGrokSeccompProfile.ts`, sha-pinned, materialized under + `/container/security/` or the image-up work directory), the narrowest + combination under which Grok 1.0.34's always-on bubblewrap starts. Codex's + fully unconfined options are a superset and win when both engines are + present. The Docker host must allow unprivileged user namespaces + (`kernel.apparmor_restrict_unprivileged_userns=0`); the Daimon entrypoint + refuses to start a Grok organization, naming that sysctl, when it is not. +- Brokered Grok workers (`containerDaimonGrokWorkerRender.ts`) take their + `config.toml` bytes only from Daimon's renderer output vendored in + `src/runtime/daimon/grokWorkerConfigBytes.ts`, refused unless they hash to the + manifest pin for the agent's declared model x effort. The sandbox profile's + `deny` list is never empty: Daimon's protected set (realms, bootstrap, peers, + acceptance store, kept verbatim) plus the organization config directory, + every persistent mount of every runtime plan (the worker's own tool state, + credential home, and memory banks included), other runtime instance roots, + `/var/lib/spawnfile/{moltnet,agents,memory}`, every workspace resource backing + path not linked from the agent's own workspace, the broker's `/etc` and `/run` + directories, the usage ledger and wake fuse, every other worker's home, and + `/run/{secrets,spawnfile,spawnfile-secrets,world}`. Allowed on purpose: own + workspace, own worker home, own runtime home directory, own resource + backings. Masks never nest; `containerDaimonGrokWorkerDenyCoverage.test.ts` + enumerates everything the container provisions and fails on any uncovered, + unjustified path. Provisioning + (`containerDaimonGrokWorkerProvisioning.ts`) writes Daimon's attested + `GROK_HOME` layout — `root: 1771` home and `sessions/`, `root:root + 0444` config/sandbox/trust/managed/requirements files, events under + `sessions/` — and refuses any registration or deny path that is missing, a + symlink, or not its own realpath. It also provisions Daimon's temp and spill + contract: `/tmp` `: 0700` (the launcher's + `TMPDIR`); `/tmp` and `/var/tmp` `root:2000 1774` (Grok refuses to start if + they are denied, so modes close them); `/tool-output` + `2000: 2750` under a runtime home `2000: 0710` whose + `/var/lib/spawnfile` ancestors are made traversable by reclaim-mode-restore. + The broker and relay (uid 2100, outside group 2000) run with + `TMPDIR=/run/daimon-engine-broker/tmp`; every other entrypoint process runs as + root or uid 2000, and workers get their `TMPDIR` from the launcher. The start + script never restates a Grok agent's runtime home mode. Production registrations all point + `usageLedgerPath` at the one container ledger, because `spawnfile usage` and + Daimon's wake fuse read only that file. - Declared names are checked for uniqueness across EVERY mount source (`containerPersistentMounts.ts`), not just within one source. A resource `name: X` and a store `persistence.name: X` used to compile to two diff --git a/src/compiler/containerArtifactsPlans.test.ts b/src/compiler/containerArtifactsPlans.test.ts index 9b6f915e..4a13cfb6 100644 --- a/src/compiler/containerArtifactsPlans.test.ts +++ b/src/compiler/containerArtifactsPlans.test.ts @@ -205,6 +205,7 @@ describe("runtime target plan source identity", () => { ] as const).map(([slug, engine]) => ({ node: { ...createAgent(), + ...(engine === "grok" ? { execution: { model: { primary: { auth: { method: "grok" }, name: "grok-4.6", provider: "xai", reasoning_effort: "low" } } } } : {}), name: slug, runtime: { name: "daimon", options: { engine } } } as ResolvedAgentNode, diff --git a/src/compiler/containerArtifactsPlans.ts b/src/compiler/containerArtifactsPlans.ts index 080fad9c..fb5e8fd7 100644 --- a/src/compiler/containerArtifactsPlans.ts +++ b/src/compiler/containerArtifactsPlans.ts @@ -160,6 +160,7 @@ export const createRuntimeTargetPlans = async ( runtimePlans.push({ configEnvBindings: resolveTargetConfigEnvBindings(adapter.container, target) ?? [], ...(target.engineByNodeId ? { engineByNodeId: target.engineByNodeId } : {}), + ...(target.grokModelByNodeId ? { grokModelByNodeId: target.grokModelByNodeId } : {}), envFiles: resolveTargetEnvFiles(instancePaths.configPath, target), packages: resolveTargetPackages(target, targetInputs), id: target.id, diff --git a/src/compiler/containerArtifactsTypes.ts b/src/compiler/containerArtifactsTypes.ts index c7c0d6b5..b3ad929e 100644 --- a/src/compiler/containerArtifactsTypes.ts +++ b/src/compiler/containerArtifactsTypes.ts @@ -26,6 +26,8 @@ export interface RuntimeTargetPlan { /** Passthrough of `ContainerTarget.engineByNodeId` (see `runtime/types.ts`), for * `containerArtifacts.ts` to stamp onto `ContainerRuntimeInstanceReport.engine_by_node_id`. */ engineByNodeId?: Record; + /** Passthrough of `ContainerTarget.grokModelByNodeId` for the Daimon broker worker render. */ + grokModelByNodeId?: Record; packages?: ResolvedPackage[]; envFiles: Array<{ envName: string; diff --git a/src/compiler/containerDaimonBrokerRender.test.ts b/src/compiler/containerDaimonBrokerRender.test.ts index 2799986a..857af10d 100644 --- a/src/compiler/containerDaimonBrokerRender.test.ts +++ b/src/compiler/containerDaimonBrokerRender.test.ts @@ -10,35 +10,33 @@ import { DAIMON_GROK_TURN_USAGE_LEDGER } from "../runtime/daimon/contractManifes import { DAIMON_BROKER_REALM, DAIMON_ORGANIZATION_STATE_DIRECTORY, - GROK_SANDBOX_DENY_PATHS, renderDaimonBrokerProvisioning, renderDaimonUsageLedgerProvisioning, renderDaimonWorkspaceResourceSecurity } from "./containerDaimonBrokerRender.js"; const execFile = promisify(execFileCallback); +type Plan = Parameters[0][number]; +const grokPlan = (agentIds: string[], extra: Record = {}, model = { model: "grok-4.6", reasoningEffort: "low" }): Plan => ({ + runtimeName: "daimon", + engineByNodeId: { ...Object.fromEntries(agentIds.map((agentId) => [agentId, "grok"])), ...extra }, + grokModelByNodeId: Object.fromEntries(agentIds.map((agentId) => [agentId, model])), + instancePaths: { configPath: "/var/lib/spawnfile/instances/daimon/daimon-organization/daimon/config.json", instanceRoot: "/var/lib/spawnfile/instances/daimon/daimon-organization", workspacePath: "/var/lib/spawnfile/instances/daimon/daimon-organization/workspace" } +}) as unknown as Plan; const uid = process.getuid?.() ?? 501; const gid = process.getgid?.() ?? 20; const owners = { linkUid: uid, linkGid: gid, readonlyUid: uid, readonlyGid: gid, privilegedUid: uid, privilegedGid: gid }; describe("Daimon broker registration ABI", () => { it("renders the manifest-declared native ABI version", () => { - const plan = { - runtimeName: "daimon", - engineByNodeId: { "agent:grok": "grok" }, - instancePaths: { workspacePath: "/workspace" } - } as unknown as Parameters[0][number]; + const plan = grokPlan(["agent:grok"]); expect(renderDaimonBrokerProvisioning([plan]).join("\n")) .toContain("record.writeUInt32LE(2, 0)"); }); }); describe("Daimon broker registration directory provisioning", () => { - const plan = { - runtimeName: "daimon", - engineByNodeId: { "agent:grok": "grok" }, - instancePaths: { workspacePath: "/workspace" } - } as unknown as Parameters[0][number]; + const plan = grokPlan(["agent:grok"]); it("keeps the broker directory writable until its files are provisioned", () => { const lines = renderDaimonBrokerProvisioning([plan]).join("\n").split("\n"); @@ -74,11 +72,7 @@ describe("Daimon broker registration directory provisioning", () => { }); describe("Daimon broker usage ledger provisioning", () => { - const plan = { - runtimeName: "daimon", - engineByNodeId: { "agent:grok": "grok" }, - instancePaths: { workspacePath: "/workspace" } - } as unknown as Parameters[0][number]; + const plan = grokPlan(["agent:grok"]); it("fixes the usage ledger directory group-writable so Codex/AGY's organization-uid process can also write it, unconditionally (not just alongside the realm)", () => { const { directoryPath } = DAIMON_GROK_TURN_USAGE_LEDGER; @@ -92,86 +86,10 @@ describe("Daimon broker usage ledger provisioning", () => { expect(renderDaimonBrokerProvisioning([])).toEqual([]); }); - it("never lists the usage ledger directory in the rendered sandbox deny list", () => { - // The usage ledger directory is unix-denied to every worker uid - // unconditionally by `renderDaimonUsageLedgerProvisioning` (0770, - // broker:organization — a worker uid never matches either), so Grok - // could never verify a mask over it and would refuse to start if it were - // still listed. See `GROK_SANDBOX_DENY_PATHS`'s doc comment. - const program = renderDaimonBrokerProvisioning([plan]).join("\n"); - const deniedPathsLine = program.split("\n").find((line) => line.includes("const deniedPaths =")); - expect(deniedPathsLine).toBeDefined(); - expect(deniedPathsLine).not.toContain(DAIMON_GROK_TURN_USAGE_LEDGER.directoryPath); - }); -}); - -describe("Grok sandbox deny list: empty, because a non-empty one cannot start", () => { - // Grok 1.0.13 re-execs itself inside bubblewrap whenever `deny` is - // non-empty, then opens every deny-path placeholder to prove the bind-over - // is genuine. It creates that placeholder at mode 000 and the re-exec'd - // process is capability-stripped, so the open returns EACCES and Grok - // refuses to start ("possible __GROK_INSIDE_BWRAP spoof") without ever - // writing a ProfileApplied event — reproduced against the real Linux binary, - // with the deny target proven irrelevant. Every path this list once carried - // is unix-denied to a worker uid unconditionally anyway, the organization - // state directory included now that the ownership guard secures it to 0700 - // (see `containerDaimonUidEntrypointRender.ts`). - const planWithAgents = (agentIds: string[]) => ({ - runtimeName: "daimon", - engineByNodeId: Object.fromEntries(agentIds.map((agentId) => [agentId, "grok"])), - instancePaths: { workspacePath: "/workspace" } - } as unknown as Parameters[0][number]); - - const deniedPathsLineFor = (agentIds: string[]): string | undefined => - renderDaimonBrokerProvisioning([planWithAgents(agentIds)]) - .join("\n").split("\n").find((line) => line.includes("const deniedPaths =")); - - it("renders an empty deny list and a profile that interpolates exactly it", () => { - for (const agentIds of [["agent:solo"], ["agent:cogsworth", "agent:foreman", "agent:graves"]]) { - expect(deniedPathsLineFor(agentIds)).toBe("const deniedPaths = [];"); - } - // Run the rendered program's own profile expression rather than a - // re-typed copy of it, so this asserts the bytes the container writes. - const lines = renderDaimonBrokerProvisioning([planWithAgents(["agent:solo"])]).join("\n").split("\n"); - const source = lines.filter((line) => - line.startsWith("const deniedPaths =") || line.startsWith("const profileFor =")).join("\n"); - expect(source.split("\n")).toHaveLength(2); - const profile = new Function(`${source}\nreturn profileFor();`)() as string; - expect(profile).toBe("[profiles.daimon-strict]\nextends = \"strict\"\nrestrict_network = true\ndeny = []\n"); - }); - - it("never lists a peer worker's home or workspace, the realm, the credential, or the state directory", () => { - const deniedPathsLine = deniedPathsLineFor(["agent:cogsworth", "agent:foreman", "agent:graves"]); - expect(deniedPathsLine).toBeDefined(); - for (const forbidden of [ - "daimon-workers", - "/workspace/agents/cogsworth", - "/workspace/agents/foreman", - "/workspace/agents/graves", - DAIMON_BROKER_REALM, - "/var/lib/spawnfile/daimon/grok-bootstrap-auth", - "/run/daimon-engine-broker", - DAIMON_GROK_TURN_USAGE_LEDGER.directoryPath, - DAIMON_ORGANIZATION_STATE_DIRECTORY - ]) { - expect(deniedPathsLine).not.toContain(forbidden); - } - }); - - it("keeps the exported deny-path constant in sync with what gets rendered", () => { - expect(GROK_SANDBOX_DENY_PATHS).toEqual([]); - expect(DAIMON_ORGANIZATION_STATE_DIRECTORY).toBe( - "/var/lib/spawnfile/instances/daimon/daimon-organization/state" - ); - }); }); describe("Daimon root provisioning capability-safe ordering", () => { - const plan = { - runtimeName: "daimon", - engineByNodeId: { "agent:grok": "grok" }, - instancePaths: { workspacePath: "/workspace" } - } as unknown as Parameters[0][number]; + const plan = grokPlan(["agent:grok"]); const render = () => renderDaimonBrokerProvisioning([plan]).join("\n"); it("lets grok write hook state but never replace the sandbox profile", () => { @@ -180,14 +98,14 @@ describe("Daimon root provisioning capability-safe ordering", () => { // Grok creates hook registries under .grok to enforce its deny list, so the worker // needs write access there. The sticky bit means it still cannot unlink or rename the // root-owned sandbox.toml, which is what the profile attestation depends on. - expect(program).toContain("ensureDirectory(configRoot, 0, entry.uid, 0o1771)"); - expect(program).toContain("ensureExactFile(profilePath, profileFor(), 0, 0, 0o444)"); + expect(program).toContain("ensureDirectory(entry.grokHome, 0, entry.uid, 0o1771)"); + expect(program).toContain("ensureExactFile(entry.profilePath, entry.profile, 0o444)"); }); it("tightens the worker config directory after its last file write", () => { const program = render(); - const lastWrite = program.indexOf("ensureEventsFile(eventsPath, entry.uid)"); - const tighten = program.indexOf("ensureDirectory(configRoot, 0, entry.uid, 0o1771)", lastWrite); + const lastWrite = program.indexOf("ensureEventsFile(entry.eventsPath, entry.uid)"); + const tighten = program.indexOf("ensureDirectory(entry.grokHome, 0, entry.uid, 0o1771)", lastWrite); expect(lastWrite).toBeGreaterThanOrEqual(0); expect(tighten).toBeGreaterThan(lastWrite); diff --git a/src/compiler/containerDaimonBrokerRender.ts b/src/compiler/containerDaimonBrokerRender.ts index ddc2ed42..6574e5f4 100644 --- a/src/compiler/containerDaimonBrokerRender.ts +++ b/src/compiler/containerDaimonBrokerRender.ts @@ -1,12 +1,10 @@ -import path from "node:path"; - -import { DAIMON_ORGANIZATION_TARGET_ID } from "../runtime/daimon/config.js"; import { DAIMON_GROK_ENGINE_BROKER, - DAIMON_GROK_TURN_USAGE_LEDGER, - DAIMON_RUNTIME_HOME_ROOT + DAIMON_GROK_TURN_USAGE_LEDGER } from "../runtime/daimon/contractManifest.js"; import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js"; +import { resolveDaimonGrokRegistrations } from "./containerDaimonGrokWorkerRender.js"; +import { renderDaimonGrokWorkerProvisioning } from "./containerDaimonGrokWorkerProvisioning.js"; import { DAIMON_BROKER_UID, DAIMON_FIRST_WORKER_UID, @@ -21,75 +19,18 @@ export const DAIMON_BROKER_BACKEND_SOCKET = "/run/daimon-engine-broker/backend.s export const DAIMON_BROKER_LAUNCHER_SOCKET = "/run/daimon-engine-broker/launcher.sock"; export const DAIMON_BROKER_SERVICE_CONFIG = "/etc/daimon-engine-broker/service.json"; export const DAIMON_BROKER_REALM = "/var/lib/spawnfile/daimon/grok-subscription-realm"; -export const DAIMON_WORKER_ROOT = "/var/lib/daimon-workers"; -/** - * The organization runtime state directory — the parent of the durable wake - * acceptance store (`state/wake-acceptance`) and the runtime readiness - * receipt inside it. - * - * It used to be left at mode 0755 because Docker creates it root-owned and - * world-readable when it materializes the acceptance-store volume mount, and - * nothing tightened it afterwards: the Daimon ownership guard only *chowned* - * ancestor `privateDirectories` and never chmoded them. That made it the one - * path under `/var/lib/spawnfile` a Grok worker uid could actually open, and - * the whole reason a `deny` entry existed at all. - * - * `resolveDaimonUidEntrypointOwnershipPlan` now lists it as a - * `privateModeDirectory`, so the ownership guard secures it to `0700` - * `2000:2000` — the same treatment the acceptance store beneath it already - * had. That is what actually denies the worker, and it does so for every - * Daimon organization, with or without Grok. No non-root, non-organization - * reader loses anything: the only content under it is the acceptance store, - * which was already `0700 2000:2000`, so every reader that works today is - * either the organization uid or a `docker exec` root holding - * `CAP_DAC_READ_SEARCH` (`runProject.ts`), and neither is affected by the - * parent's mode. - */ -export const DAIMON_ORGANIZATION_STATE_DIRECTORY = path.posix.join( - DAIMON_RUNTIME_HOME_ROOT, - DAIMON_ORGANIZATION_TARGET_ID, - "state" -); /** - * The Grok worker sandbox profile's `deny` list — see `profileFor` below. - * Deliberately empty, which leaves the worker on builtin-`strict` Landlock - * confinement plus `restrict_network`. - * - * A non-empty `deny` list is not a usable mechanism on Grok 1.0.13. Whenever - * one is present, Grok re-execs itself inside bubblewrap and then `open()`s - * every deny-path placeholder to prove its own bind-over is genuine — but it - * creates that placeholder at mode `000` and the re-exec'd process is - * capability-stripped (`--cap-drop ALL`), so the open returns `EACCES` and - * Grok refuses to start: - * - * error: sandbox reports bwrap but required read-deny mounts are not in - * effect (read-deny path could not be opened: Permission denied - * (os error 13)); refusing to start (possible __GROK_INSIDE_BWRAP spoof) - * - * It never reaches `ProfileApplied`, so `sandbox-events.jsonl` stays empty and - * Daimon's attestation never runs either. Reproduced locally against the real - * Linux `grok 1.0.13` binary, with controls confirming the deny *target* is - * irrelevant: a plain directory, a directory with a child mount, and a vanilla - * root-owned home denying an unrelated file all fail identically, while an - * empty deny list exits 0 and emits - * `ProfileApplied {platform: "linux/landlock", enforced: true, - * restrict_network: true}` with no bwrap at all. - * - * Nothing is lost by emptying it. Every path this list ever carried — peer - * worker homes/workspaces, the subscription realm, the bootstrap-auth file, - * the broker's `/run` socket directory, the usage-ledger directory, and now - * `DAIMON_ORGANIZATION_STATE_DIRECTORY` — is unix-denied to a worker uid - * unconditionally, by construction: each is force-chowned/chmoded (never - * merely checked) to a mode whose "other" class carries no read bit, and a - * worker uid never matches the owning uid or gid of any of them (workers run - * under `setresuid`/`setresgid` to a dedicated uid==gid with every - * supplementary group cleared — see `engineBrokerLauncherCore.inc`). - * - * Daimon's `grokWorkerAttestation.ts` still pins the profile by SHA-256 to - * exactly these bytes, so an empty list is not an unconstrained one: the - * worker's profile cannot differ from what is rendered here. + * Private temp for the broker and its relay (uid 2100, outside the organization + * group): shared `/tmp` and `/var/tmp` are `root:2000 1774` in a Grok + * organization, so any non-root process outside group 2000 needs its own + * `TMPDIR`. It lives in the broker's `/run` directory, which every worker denies. */ -export const GROK_SANDBOX_DENY_PATHS: readonly string[] = []; +export const DAIMON_BROKER_TMPDIR = "/run/daimon-engine-broker/tmp"; +export { + DAIMON_ORGANIZATION_STATE_DIRECTORY, + DAIMON_WORKER_ROOT, + resolveDaimonGrokRegistrations +} from "./containerDaimonGrokWorkerRender.js"; interface WorkspaceSecurityResource { backingPath: string; @@ -110,25 +51,6 @@ export const renderDaimonWorkspaceResourceSecurity = ( "const secureWorkspace = (root, uid) => { const visit = (target) => { const info = fs.lstatSync(target); if (info.isSymbolicLink()) { validateResourceLink(target, info); return; } if (info.isDirectory()) { fs.chownSync(target, 0, 0); fs.chmodSync(target, 0o750); for (const name of fs.readdirSync(target)) visit(`${target}/${name}`); fs.chownSync(target, 2000, uid); } else if (info.isFile()) { fs.chownSync(target, 0, 0); fs.chmodSync(target, 0o640); fs.chownSync(target, 2000, uid); } else throw new Error('unsafe worker workspace node'); }; visit(root); };" ]; -const nodeSlug = (nodeId: string): string => nodeId.replace(/^agent:/u, "") - .toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, ""); - -export const resolveDaimonGrokRegistrations = (plans: RuntimeTargetPlan[]) => plans - .filter((plan) => plan.runtimeName === "daimon") - .flatMap((plan) => Object.entries(plan.engineByNodeId ?? {}) - .filter(([, engine]) => engine === "grok") - .map(([agentId]) => ({ - agentId, - workspace: path.posix.join(plan.instancePaths.workspacePath, "agents", nodeSlug(agentId)) - }))) - .sort((left, right) => left.agentId.localeCompare(right.agentId)) - .map((entry, slot) => ({ - ...entry, - home: path.posix.join(DAIMON_WORKER_ROOT, String(DAIMON_FIRST_WORKER_UID + slot)), - slot, - uid: DAIMON_FIRST_WORKER_UID + slot - })); - /** * Fixes ownership and mode of the per-turn usage ledger directory for every * Daimon organization, not just ones with a Grok agent. AGY and Codex both @@ -181,9 +103,10 @@ export const renderDaimonBrokerProvisioning = (plans: RuntimeTargetPlan[]): stri .sort((left, right) => left.linkPath.localeCompare(right.linkPath)); const program = [ "const crypto = require('node:crypto'); const fs = require('node:fs');", - `const registrations = ${JSON.stringify(registrations)};`, - "const executable = '/usr/local/bin/grok';", + `const registrations = ${JSON.stringify(registrations.map(({ agentId, home, slot, uid, workspace }) => ({ agentId, home, slot, uid, workspace })))};`, + `const executable = '${DAIMON_GROK_ENGINE_BROKER.grokExecutablePath}';`, "const digest = crypto.createHash('sha256').update(fs.readFileSync(executable)).digest();", + `if (!${JSON.stringify([DAIMON_GROK_ENGINE_BROKER.grokCliArtifacts.arm64.sha256, DAIMON_GROK_ENGINE_BROKER.grokCliArtifacts.x64.sha256])}.includes(digest.toString('hex'))) throw new Error('Grok executable is not the manifest-pinned ${DAIMON_GROK_ENGINE_BROKER.grokCliVersion} build');`, "const cString = (buffer, offset, length, value) => { const bytes = Buffer.from(value); if (bytes.length < 1 || bytes.length >= length || bytes.includes(0)) throw new Error('invalid broker registration'); bytes.copy(buffer, offset); };", `const records = registrations.map((entry) => { const record = Buffer.alloc(692); record.writeUInt32LE(${DAIMON_GROK_ENGINE_BROKER.nativeAbiVersion}, 0); record.writeUInt32LE(entry.slot, 4); record.writeUInt32LE(entry.uid, 8); record.writeUInt32LE(entry.uid, 12); cString(record, 16, 129, entry.agentId); cString(record, 145, 256, entry.workspace); cString(record, 401, 256, entry.home); digest.copy(record, 657); return record; });`, "fs.mkdirSync('/etc/daimon-engine-broker', { recursive: true, mode: 0o700 }); fs.chownSync('/etc/daimon-engine-broker', 0, 0); fs.chmodSync('/etc/daimon-engine-broker', 0o700);", @@ -202,24 +125,8 @@ export const renderDaimonBrokerProvisioning = (plans: RuntimeTargetPlan[]): stri `const journalRoot = '${DAIMON_BROKER_REALM}/.daimon-broker'; let journalRootExists = false; try { const info = fs.lstatSync(journalRoot); if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== 2100 || info.gid !== 2100 || (info.mode & 0o777) !== 0o700) throw new Error('unsafe broker credential journal directory'); fs.chownSync(journalRoot, 0, 0); fs.chmodSync(journalRoot, 0o700); journalRootExists = true; } catch (error) { if (error.code !== 'ENOENT') throw error; }`, `try { const journalPath = '${DAIMON_BROKER_REALM}/.daimon-broker/credential-journal.json'; let journal; try { const raw = readSecure(journalPath, 2100, 'recovery journal'); journal = JSON.parse(raw.toString('utf8')); raw.fill(0); } catch (error) { if (error.code !== 'ENOENT') throw error; } const stale = journal?.version === 'noopolis.daimon.broker-credential-journal.v1' && journal.state === 'stale'; const recover = () => { if (!stale || !Number.isSafeInteger(journal.generation) || journal.generation < 0 || !/^[a-f0-9]{64}$/.test(journal.sourceDigest) || journal.sourceDigest !== journal.promotedDigest || bootstrapDigest === journal.sourceDigest) throw new Error('unsafe broker credential recovery'); atomicOwned(authority, bootstrapBytes); const recovered = Buffer.from(\`${"${JSON.stringify({ version: 'noopolis.daimon.broker-credential-journal.v1', state: 'promoted', generation: journal.generation + 1, sourceDigest: journal.sourceDigest, promotedDigest: bootstrapDigest })}"}\\n\`); try { atomicOwned(journalPath, recovered); } finally { recovered.fill(0); } }; if (!existing) { if (stale) recover(); else atomicOwned(authority, bootstrapBytes); } else { const authorityBytes = readSecure(authority, 2100, 'authority'); try { const authorityDigest = crypto.createHash('sha256').update(authorityBytes).digest('hex'); if (stale) { if (authorityDigest !== journal.sourceDigest && authorityDigest !== bootstrapDigest) throw new Error('unsafe broker credential recovery'); if (authorityDigest === bootstrapDigest) { const recovered = Buffer.from(\`${"${JSON.stringify({ version: 'noopolis.daimon.broker-credential-journal.v1', state: 'promoted', generation: journal.generation + 1, sourceDigest: journal.sourceDigest, promotedDigest: bootstrapDigest })}"}\\n\`); try { atomicOwned(journalPath, recovered); } finally { recovered.fill(0); } } else recover(); } } finally { authorityBytes.fill(0); } } } finally { bootstrapBytes.fill(0); }`, "if (journalRootExists) { fs.chownSync(journalRoot, 0, 0); fs.chmodSync(journalRoot, 0o700); fs.chownSync(journalRoot, 2100, 2100); }", - "const config = '[auth_provider.daimon]\\ntype = \"custom\"\\ncommand = \"/opt/daimon/bin/daimon-engine-broker\"\\nargs = [\"--auth-provider\"]\\n\\n[model.daimon-broker-grok]\\nmodel = \"grok-build\"\\nbase_url = \"http://127.0.0.1:43123/v1\"\\nauth_provider = \"daimon\"\\ncontext_window = 131072\\nsupports_backend_search = false\\n\\n[mcp_servers.daimon]\\nurl = \"http://127.0.0.1:43124/mcp\"\\nheaders = { Authorization = \"Bearer ${DAIMON_MCP_CAPABILITY}\" }\\n';", - `for (const root of ['${DAIMON_WORKER_ROOT}']) { fs.mkdirSync(root, { recursive: true, mode: 0o711 }); fs.chownSync(root, 0, 0); fs.chmodSync(root, 0o711); }`, ...renderDaimonWorkspaceResourceSecurity(workspaceResources), - // Empty on purpose: a non-empty `deny` list makes Grok 1.0.13 refuse to - // start before it ever applies a profile, and every path it used to carry - // is unix-denied to a worker uid unconditionally anyway. See - // `GROK_SANDBOX_DENY_PATHS`'s doc comment. - `const deniedPaths = ${JSON.stringify(GROK_SANDBOX_DENY_PATHS)};`, - "const profileFor = () => `[profiles.daimon-strict]\\nextends = \"strict\"\\nrestrict_network = true\\ndeny = [${deniedPaths.map(JSON.stringify).join(', ')}]\\n`;", - "const ensureDirectory = (target, uid, gid, mode) => { fs.mkdirSync(target, { recursive: true, mode }); const info = fs.lstatSync(target); if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('unsafe worker runtime directory'); fs.chownSync(target, 0, 0); fs.chmodSync(target, mode); fs.chownSync(target, uid, gid); };", - "const ensureExactFile = (target, content, uid, gid, mode) => { let info; try { info = fs.lstatSync(target); } catch (error) { if (error.code !== 'ENOENT') throw error; fs.writeFileSync(target, content, { mode, flag: 'wx' }); info = fs.lstatSync(target); } if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) throw new Error('unsafe worker runtime file'); const existing = fs.readFileSync(target, 'utf8'); if (existing !== content) throw new Error('worker runtime file identity mismatch'); fs.chownSync(target, 0, 0); fs.chmodSync(target, mode); fs.chownSync(target, uid, gid); };", - "const ensureEventsFile = (target, uid) => { let info; try { info = fs.lstatSync(target); } catch (error) { if (error.code !== 'ENOENT') throw error; fs.writeFileSync(target, '', { mode: 0o640, flag: 'wx' }); info = fs.lstatSync(target); } if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || (info.uid !== uid && info.uid !== 0) || (info.gid !== 2100 && info.gid !== 0) || ![0o600,0o640].includes(info.mode & 0o777)) throw new Error('unsafe worker attestation events'); fs.chownSync(target, 0, 0); fs.chmodSync(target, 0o640); fs.chownSync(target, uid, 2100); };", - "// Grok refuses a sandbox profile reached through a symlink ('retargetable'), and", - "// Daimon's attestation requires nlink === 1, so the profile is one plain root-owned", - "// 0444 file written in place: no symlink, and no hard link either.", - `for (const entry of registrations) { for (let ancestor = require('node:path').dirname(entry.workspace); ancestor.startsWith('/var/lib/spawnfile/') && ancestor.length > '/var/lib/spawnfile'.length; ancestor = require('node:path').dirname(ancestor)) fs.chmodSync(ancestor, fs.statSync(ancestor).mode & 0o7777 | 0o011); secureWorkspace(entry.workspace, entry.uid); ensureDirectory(entry.home, 0, 0, 0o700); const configRoot = \`${"${entry.home}"}/.grok\`; ensureDirectory(configRoot, 0, 0, 0o700); const configPath = \`${"${configRoot}"}/config.toml\`; ensureExactFile(configPath, config, 0, 0, 0o444); const profilePath = \`${"${configRoot}"}/sandbox.toml\`, eventsPath = \`${"${configRoot}"}/sandbox-events.jsonl\`; ensureExactFile(profilePath, profileFor(), 0, 0, 0o444); ensureEventsFile(eventsPath, entry.uid); ensureDirectory(configRoot, 0, entry.uid, 0o1771); ensureDirectory(entry.home, entry.uid, ${DAIMON_BROKER_UID}, 0o710); }`, - `const service = { version: 'noopolis.daimon.engine-broker-service.v1', credentialHome: '/var/lib/spawnfile/daimon/grok-subscription-realm', turnStore: '/var/lib/spawnfile/daimon/grok-subscription-realm/turns', registrations: registrations.map((entry) => { const configRoot = \`${"${entry.home}"}/.grok\`, profilePath = \`${"${configRoot}"}/sandbox.toml\`, eventsPath = \`${"${configRoot}"}/sandbox-events.jsonl\`; return { agentId: entry.agentId, slot: entry.slot, workerUid: entry.uid, workspace: entry.workspace, profilePath, eventsPath, profileSha256: crypto.createHash('sha256').update(profileFor()).digest('hex') }; }) };`, - "fs.writeFileSync('/etc/daimon-engine-broker/service.json', `${JSON.stringify(service)}\n`, { mode: 0o440, flag: 'wx' }); fs.chownSync('/etc/daimon-engine-broker/service.json', 0, 2100); fs.chmodSync('/etc/daimon-engine-broker/service.json', 0o440);", + ...renderDaimonGrokWorkerProvisioning(registrations), `fs.chownSync('${DAIMON_BROKER_REALM}', 0, 0); fs.chmodSync('${DAIMON_BROKER_REALM}', 0o700); fs.chownSync('${DAIMON_BROKER_REALM}', 2100, 2100);`, "fs.chmodSync('/etc/daimon-engine-broker', 0o555);" ].join("\n"); @@ -228,6 +135,7 @@ export const renderDaimonBrokerProvisioning = (plans: RuntimeTargetPlan[]): stri "if [ -d /run/daimon-engine-broker ]; then chmod u+rwx /run/daimon-engine-broker; fi", "rm -rf /etc/daimon-engine-broker /run/daimon-engine-broker", `install -d -o root -g ${DAIMON_BROKER_UID} -m 0731 /run/daimon-engine-broker`, + `install -d -o ${DAIMON_BROKER_UID} -g ${DAIMON_BROKER_UID} -m 0700 ${DAIMON_BROKER_TMPDIR}`, "node <<'SPAWNFILE_DAIMON_BROKER_PROVISION'", program, "SPAWNFILE_DAIMON_BROKER_PROVISION" diff --git a/src/compiler/containerDaimonCapabilityOrdering.test.ts b/src/compiler/containerDaimonCapabilityOrdering.test.ts index ce35c95c..71b5f612 100644 --- a/src/compiler/containerDaimonCapabilityOrdering.test.ts +++ b/src/compiler/containerDaimonCapabilityOrdering.test.ts @@ -6,6 +6,7 @@ import { createStateOwnershipCommand } from "./containerStateOwnershipRender.js" const daimonPlan = { engineByNodeId: { "agent:grok": "grok" }, + grokModelByNodeId: { "agent:grok": { model: "grok-4.6", reasoningEffort: "low" } }, instancePaths: { configPath: "/var/lib/spawnfile/instances/daimon/organization/daimon/config.json", instanceRoot: "/var/lib/spawnfile/instances/daimon/organization", diff --git a/src/compiler/containerDaimonGrokWorkerDenyCoverage.test.ts b/src/compiler/containerDaimonGrokWorkerDenyCoverage.test.ts new file mode 100644 index 00000000..0c12aa53 --- /dev/null +++ b/src/compiler/containerDaimonGrokWorkerDenyCoverage.test.ts @@ -0,0 +1,132 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { daimonAdapter } from "../runtime/daimon/adapter.js"; +import { DAIMON_CONTRACT_MANIFEST_SHA256, DAIMON_GROK_ENGINE_BROKER, DAIMON_GROK_SUBSCRIPTION_REALM, DAIMON_GROK_TURN_USAGE_LEDGER } from "../runtime/daimon/contractManifest.js"; +import { DAIMON_WAKE_FUSE_DIRECTORY } from "../runtime/daimon/config.js"; +import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js"; +import { createRuntimeTargetPlans } from "./containerArtifactsPlans.js"; +import { resolveDaimonGrokRegistrations } from "./containerDaimonGrokWorkerRender.js"; +import { resolveDaimonUidEntrypointOwnershipPlan } from "./containerDaimonUidEntrypointRender.js"; +import { MOLTNET_READINESS_DIRECTORY } from "./containerReadinessPaths.js"; +import type { EntrypointOptions } from "./containerEntrypointRender.js"; +import { createMoltnetDaimonReceiptStorePath, createMoltnetNetworkStateDirectory, createMoltnetOpenTokenPath } from "./moltnetConfigLowering.js"; +import type { CompilePlan, ResolvedAgentNode } from "./types.js"; + +const INSTANCE = "/var/lib/spawnfile/instances/daimon/daimon-organization"; +const temporary: string[] = []; +afterEach(async () => { + delete process.env.SPAWNFILE_DAIMON_LOCAL_RUNTIME_IDENTITY; + await Promise.all(temporary.splice(0).map((directory) => rm(directory, { force: true, recursive: true }))); +}); + +const useCompatibleDaimonRuntime = async (): Promise => { + const directory = await mkdtemp(path.join(os.tmpdir(), "spawnfile-deny-coverage-")); + temporary.push(directory); + const identity = path.join(directory, "identity.json"), digest = `sha256:${"a".repeat(64)}`; + await writeFile(identity, `${JSON.stringify({ + capability_receipt_sha256: digest, development: { mode: "local-development", non_production: true, unpublished: true, unsigned: true }, + image_architecture: "amd64", image_config_digest: digest, image_manifest_digest: digest, + image_reference: `127.0.0.1:54321/noopolis/spawnfile-runtime-daimon@${digest}`, manifest_sha256: DAIMON_CONTRACT_MANIFEST_SHA256, + registry_authority: "127.0.0.1:54321", version: "spawnfile.local-daimon-runtime-identity.v3" + })}\n`, { mode: 0o600 }); + process.env.SPAWNFILE_DAIMON_LOCAL_RUNTIME_IDENTITY = identity; +}; + +const agent = (name: string, engine: string): ResolvedAgentNode => ({ + description: "", docs: [], env: {}, kind: "agent", mcpServers: [], name, policyMode: null, policyOnDegrade: null, + runtime: { name: "daimon", options: { engine } }, secrets: [], skills: [], source: `/tmp/${name}/Spawnfile`, subagents: [], + execution: engine === "grok" ? { model: { primary: { auth: { method: "grok" }, name: "grok-4.6", provider: "xai", reasoning_effort: "low" } } } + : engine === "codex" ? { model: { primary: { auth: { method: "codex" }, name: "gpt-5.4-mini", provider: "openai" } } } : undefined +} as ResolvedAgentNode); + +const resource = (id: string, linkPath: string, backingPath: string) => ({ backingPath, id, kind: "git" as const, linkPath, mode: "readonly" as const, mount: `./${id}`, sharing: "agent" as const }); + +const plans = async (): Promise => { + await useCompatibleDaimonRuntime(); + const nodes = [["alpha", "grok"], ["beta", "grok"], ["gamma", "codex"], ["delta", "agy"]] as const; + const compiled = await Promise.all(nodes.map(async ([slug, engine]) => { + const value = agent(slug, engine); + return { emittedFiles: (await daimonAdapter.compileAgent(value)).files, id: `agent:${slug}`, kind: "agent" as const, runtimeName: "daimon", slug, value }; + })); + const [daimon] = await createRuntimeTargetPlans({ edges: [], nodes: [], root: "/tmp/Spawnfile", runtimes: { daimon: { nodeIds: [] } } } as unknown as CompilePlan, compiled); + const withResources: RuntimeTargetPlan = { + ...daimon!, + persistentMounts: [...(daimon!.persistentMounts ?? []), { id: "memory-bank", mount_path: "/var/lib/spawnfile/memory/alpha/notes", reason: "memory", volume_name: "memory" }], + resources: [ + resource("own", `${INSTANCE}/workspace/agents/alpha/own`, "/var/lib/spawnfile/resources/instances/daimon-organization/own"), + resource("peer", `${INSTANCE}/workspace/agents/beta/peer`, "/var/lib/spawnfile/resources/instances/daimon-organization/peer"), + resource("team", `${INSTANCE}/workspace/agents/alpha/team`, "/var/lib/spawnfile/resources/teams/crew/team"), + resource("team", `${INSTANCE}/workspace/agents/gamma/team`, "/var/lib/spawnfile/resources/teams/crew/team") + ] as unknown as RuntimeTargetPlan["resources"] + }; + const sibling = { + envFiles: [{ envName: "PICO_KEY", filePath: "/var/lib/spawnfile/instances/picoclaw/pico/picoclaw/.env" }], + instancePaths: { configPath: "/var/lib/spawnfile/instances/picoclaw/pico/picoclaw/config.json", instanceRoot: "/var/lib/spawnfile/instances/picoclaw/pico", workspacePath: "/var/lib/spawnfile/instances/picoclaw/pico/workspace" }, + persistentMounts: [{ id: "pico-state", mount_path: "/var/lib/spawnfile/instances/picoclaw/pico/home", reason: "state", volume_name: "pico" }], + runtimeName: "picoclaw" + } as unknown as RuntimeTargetPlan; + return [withResources, sibling]; +}; + +const within = (candidate: string, root: string): boolean => candidate === root || candidate.startsWith(`${root}/`); + +describe("Grok worker deny coverage over everything the Daimon container provisions", () => { + it("denies every provisioned state path except the agent's own, explicitly justified ones", async () => { + const runtimePlans = await plans(); + const moltnet = { + nodePlans: [{ configPath: "/var/lib/spawnfile/moltnet/nodes/crew-net-alpha.json", receiptStorePath: createMoltnetDaimonReceiptStorePath("net", "alpha") }], + serverPlans: [{ configPath: "/var/lib/spawnfile/moltnet/servers/local/Moltnet.json", mode: "managed" }] + } as unknown as EntrypointOptions["moltnet"]; + const persistentMountPaths = runtimePlans.flatMap((plan) => (plan.persistentMounts ?? []).map((mount) => mount.mount_path)); + const ownership = resolveDaimonUidEntrypointOwnershipPlan(runtimePlans, persistentMountPaths, moltnet); + const registrations = resolveDaimonGrokRegistrations(runtimePlans); + const provisioned = [...new Set([ + ...ownership.stateRoots, ...ownership.privateDirectories, ...ownership.privateFiles, ...ownership.privateModeDirectories, + ...ownership.opaqueDescendantRoots, ...ownership.creatablePrivateDirectories.map((entry) => entry.target), + ...persistentMountPaths, ...runtimePlans.flatMap((plan) => plan.opaqueMountTargets ?? []), + ...runtimePlans.flatMap((plan) => [plan.instancePaths.configPath, ...(plan.envFiles ?? []).map((file) => file.filePath)]), + ...runtimePlans.flatMap((plan) => plan.resources ?? []).flatMap((entry) => [entry.backingPath, entry.linkPath]), + MOLTNET_READINESS_DIRECTORY, createMoltnetNetworkStateDirectory("net"), createMoltnetOpenTokenPath("net", "alpha"), "/var/lib/spawnfile/moltnet/servers", + DAIMON_GROK_ENGINE_BROKER.registrationPath, DAIMON_GROK_ENGINE_BROKER.serviceConfigPath, DAIMON_GROK_ENGINE_BROKER.controlSocketPath, + DAIMON_GROK_ENGINE_BROKER.backendSocketPath, DAIMON_GROK_ENGINE_BROKER.launcherSocketPath, DAIMON_GROK_ENGINE_BROKER.turnStorePath, + DAIMON_GROK_SUBSCRIPTION_REALM.bootstrapMountPath, DAIMON_GROK_TURN_USAGE_LEDGER.filePath, DAIMON_WAKE_FUSE_DIRECTORY, + ...registrations.map((entry) => entry.home), "/run/secrets", "/run/spawnfile-secrets/token", "/run/world/evidence" + ])].filter((entry) => entry.startsWith("/")); + + for (const registration of registrations) { + const slug = registration.agentId.replace(/^agent:/u, ""); + const ownRuntimeHome = `${INSTANCE}/runtime-homes/${slug}`; + const own = [registration.workspace, registration.home, ...runtimePlans.flatMap((plan) => plan.resources ?? []) + .filter((entry) => within(entry.linkPath, registration.workspace)).map((entry) => entry.backingPath)]; + /** Directories whose only provisioned children are separately denied; listing them reveals names, not content. */ + const justified = new Map([ + [`${INSTANCE}/state`, "sole child is the denied wake-acceptance store"], + ["/var/lib/spawnfile/daimon", "shared parent; every realm, ledger, fuse and bootstrap child is denied"], + [ownRuntimeHome, "Daimon names tool-result spill paths under it; every mount inside it is denied"], + ["/var/lib/spawnfile/instances/picoclaw", "runtime-kind parent of another runtime's denied instance root"] + ]); + const uncovered = provisioned.filter((entry) => + !registration.denyPaths.some((denied) => within(entry, denied)) + && !own.some((allowed) => within(entry, allowed)) + && ![...own, ownRuntimeHome].some((allowed) => allowed.startsWith(`${entry}/`)) + && !justified.has(entry)); + expect(uncovered, registration.agentId).toEqual([]); + for (const moltnetPath of ["/var/lib/spawnfile/moltnet", "/var/lib/spawnfile/agents", "/var/lib/spawnfile/memory"]) expect(registration.denyPaths).toContain(moltnetPath); + } + const [alpha, beta] = registrations; + expect(alpha!.denyPaths).toContain("/var/lib/spawnfile/resources/instances/daimon-organization/peer"); + expect(alpha!.denyPaths).not.toContain("/var/lib/spawnfile/resources/instances/daimon-organization/own"); + expect(alpha!.denyPaths).not.toContain("/var/lib/spawnfile/resources/teams/crew/team"); + expect(beta!.denyPaths).toEqual(expect.arrayContaining([ + "/var/lib/spawnfile/resources/instances/daimon-organization/own", "/var/lib/spawnfile/resources/teams/crew/team" + ])); + expect(beta!.denyPaths).not.toContain("/var/lib/spawnfile/resources/instances/daimon-organization/peer"); + expect(alpha!.deferredDenyPaths).toEqual(["/var/lib/spawnfile/resources/instances/daimon-organization/peer"]); + expect(alpha!.denyPaths).toEqual(expect.arrayContaining([`${INSTANCE}/runtime-homes/alpha/tool-state`, `${INSTANCE}/runtime-homes/alpha/.grok`, `${INSTANCE}/daimon`, "/var/lib/spawnfile/instances/picoclaw/pico"])); + for (const entry of alpha!.denyPaths) expect(alpha!.denyPaths.some((other) => other !== entry && within(entry, other))).toBe(false); + }); +}); diff --git a/src/compiler/containerDaimonGrokWorkerProvisioning.test.ts b/src/compiler/containerDaimonGrokWorkerProvisioning.test.ts new file mode 100644 index 00000000..3d3e0ef7 --- /dev/null +++ b/src/compiler/containerDaimonGrokWorkerProvisioning.test.ts @@ -0,0 +1,196 @@ +import crypto from "node:crypto"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { DAIMON_GROK_WORKER_CONFIG_BYTES } from "../runtime/daimon/grokWorkerConfigBytes.js"; +import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js"; +import { renderDaimonGrokHostPreflight, renderDaimonGrokWorkerProvisioning } from "./containerDaimonGrokWorkerProvisioning.js"; +import { resolveDaimonGrokRegistrations, type DaimonGrokRegistration } from "./containerDaimonGrokWorkerRender.js"; + +const INSTANCE = "/var/lib/spawnfile/instances/daimon/daimon-organization"; +const BROKER = 2100; + +type Node = { content: string; gid: number; kind: "dir" | "file" | "link"; mode: number; target?: string; uid: number }; + +/** Just enough of `node:fs` to run the rendered root program without root. */ +const memoryFs = (seed: Record>) => { + const nodes = new Map([["/", { content: "", gid: 0, kind: "dir", mode: 0o755, uid: 0 }]]); + const enoent = (target: string) => Object.assign(new Error(`ENOENT: ${target}`), { code: "ENOENT" }); + const put = (target: string, node: Partial) => { + for (let parent = path.posix.dirname(target); !nodes.has(parent); parent = path.posix.dirname(parent)) { + nodes.set(parent, { content: "", gid: 0, kind: "dir", mode: 0o755, uid: 0 }); + } + nodes.set(target, { content: "", gid: 0, kind: "dir", mode: 0o755, uid: 0, ...node }); + }; + for (const [target, node] of Object.entries(seed)) put(target, node); + const get = (target: string): Node => { const node = nodes.get(target); if (!node) throw enoent(target); return node; }; + const stat = (target: string) => { + const node = get(target); + const type = node.kind === "dir" ? 0o040000 : node.kind === "file" ? 0o100000 : 0o120000; + return { gid: node.gid, isDirectory: () => node.kind === "dir", isFile: () => node.kind === "file", isSymbolicLink: () => node.kind === "link", mode: type | node.mode, nlink: 1, uid: node.uid }; + }; + const fs = { + chmodSync: (target: string, mode: number) => { get(target).mode = mode & 0o7777; }, + chownSync: (target: string, uid: number, gid: number) => { const node = get(target); node.uid = uid; node.gid = gid; }, + lstatSync: stat, + mkdirSync: (target: string, options: { mode?: number; recursive?: boolean } = {}) => { + if (nodes.has(target)) { if (options.recursive) return; throw Object.assign(new Error("EEXIST"), { code: "EEXIST" }); } + if (!options.recursive) get(path.posix.dirname(target)); + put(target, { kind: "dir", mode: options.mode ?? 0o755 }); + }, + readFileSync: (target: string) => get(target).content, + realpathSync: (target: string) => { + let resolved = "/"; + for (const segment of target.split("/").filter(Boolean)) { + resolved = path.posix.join(resolved, segment); + const node = get(resolved); + if (node.kind === "link") resolved = node.target!; + } + return resolved; + }, + statSync: stat, + writeFileSync: (target: string, content: string, options: { flag?: string; mode?: number } = {}) => { + if (options.flag === "wx" && nodes.has(target)) throw Object.assign(new Error("EEXIST"), { code: "EEXIST" }); + put(target, { content: String(content), kind: "file", mode: options.mode ?? 0o644 }); + } + }; + return { fs, nodes }; +}; + +const plan = (engines: Record): RuntimeTargetPlan => ({ + engineByNodeId: engines, + grokModelByNodeId: Object.fromEntries(Object.entries(engines).filter(([, engine]) => engine === "grok").map(([id]) => [id, { model: "grok-4.6", reasoningEffort: "low" }])), + instancePaths: { configPath: `${INSTANCE}/daimon/config.json`, instanceRoot: INSTANCE, workspacePath: `${INSTANCE}/workspace` }, + runtimeName: "daimon" +}) as unknown as RuntimeTargetPlan; + +const seedFor = (registrations: readonly DaimonGrokRegistration[], omit: string[] = []) => Object.fromEntries([ + ...registrations.map((entry) => [entry.workspace, { kind: "dir" as const }] as const), + ["/etc/daimon-engine-broker", { kind: "dir" as const, mode: 0o700 }] as const, + ...registrations.flatMap((entry) => entry.denyPaths) + .filter((denied) => !denied.startsWith("/var/lib/daimon-workers/") && !(["/run/secrets", "/run/spawnfile", "/run/spawnfile-secrets", "/run/world"] as string[]).includes(denied)) + .map((denied) => [denied, { kind: denied.endsWith("grok-bootstrap-auth") ? "file" as const : "dir" as const, mode: 0o700 }] as const) +].filter(([target]) => !omit.includes(target))); + +const run = (registrations: readonly DaimonGrokRegistration[], seed: Record>, lines = renderDaimonGrokWorkerProvisioning(registrations)) => { + const memory = memoryFs(seed); + const secureWorkspace = (): void => undefined; + const program = new Function("fs", "crypto", "secureWorkspace", "require", lines.join("\n")); + program(memory.fs, crypto, secureWorkspace, (name: string) => name === "node:path" ? path.posix : undefined); + return memory.nodes; +}; + +describe("Grok worker home provisioning", () => { + const registrations = resolveDaimonGrokRegistrations([plan({ "agent:a": "grok", "agent:b": "grok", "agent:c": "codex" })]); + + it("provisions Daimon's attested layout: sticky root:worker homes and root-owned read-only turn files", () => { + const nodes = run(registrations, seedFor(registrations)); + for (const entry of registrations) { + expect(nodes.get(entry.home)).toMatchObject({ gid: BROKER, mode: 0o710, uid: entry.uid }); + for (const directory of [entry.grokHome, `${entry.grokHome}/sessions`]) { + expect(nodes.get(directory)).toMatchObject({ gid: entry.uid, kind: "dir", mode: 0o1771, uid: 0 }); + } + expect(nodes.get(`${entry.grokHome}/config.toml`)!.content).toBe(DAIMON_GROK_WORKER_CONFIG_BYTES["grok-4.6"].low); + expect(nodes.get(entry.profilePath)!.content).toBe(entry.profile); + for (const name of ["config.toml", "sandbox.toml", "trusted_folders.toml", "managed_config.toml", "requirements.toml"]) { + const file = nodes.get(`${entry.grokHome}/${name}`)!; + expect(file, name).toMatchObject({ gid: 0, kind: "file", mode: 0o444, uid: 0 }); + // No worker-uid process may write any file that decides a turn, trust included. + expect(file.mode & 0o222, name).toBe(0); + } + for (const name of ["trusted_folders.toml", "managed_config.toml", "requirements.toml"]) expect(nodes.get(`${entry.grokHome}/${name}`)!.content).toBe(""); + expect(nodes.get(entry.eventsPath)).toMatchObject({ gid: BROKER, kind: "file", mode: 0o640, uid: entry.uid }); + expect(nodes.has(`${entry.grokHome}/sandbox-events.jsonl`)).toBe(false); + } + const service = JSON.parse(nodes.get("/etc/daimon-engine-broker/service.json")!.content); + expect(service.version).toBe("noopolis.daimon.engine-broker-service.v2"); + expect(nodes.get("/etc/daimon-engine-broker/service.json")).toMatchObject({ gid: BROKER, mode: 0o440, uid: 0 }); + for (const optional of ["/run/secrets", "/run/spawnfile", "/run/spawnfile-secrets"]) expect(nodes.get(optional)).toMatchObject({ kind: "dir", mode: 0o700, uid: 0 }); + }); + + it("provisions the P1b temp and spill contract: private TMPDIR, closed shared temp, setgid spills under a traversable runtime home", () => { + const runtimeHomes = `${INSTANCE}/runtime-homes`; + const nodes = run(registrations, { ...seedFor(registrations), [runtimeHomes]: { gid: 2000, kind: "dir", mode: 0o700, uid: 2000 }, "/tmp": { kind: "dir", mode: 0o1777 } }); + for (const shared of ["/tmp", "/var/tmp"]) expect(nodes.get(shared), shared).toMatchObject({ gid: 2000, kind: "dir", mode: 0o1774, uid: 0 }); + // Owner and group survive the traversal fix: root reclaims, modes, and restores (no CAP_FOWNER). + expect(nodes.get(runtimeHomes)).toMatchObject({ gid: 2000, mode: 0o711, uid: 2000 }); + for (const entry of registrations) { + expect(entry.privateTmp).toBe(`${entry.home}/tmp`); + expect(nodes.get(entry.privateTmp)).toMatchObject({ gid: entry.uid, kind: "dir", mode: 0o700, uid: entry.uid }); + expect(nodes.get(entry.runtimeHome)).toMatchObject({ gid: entry.uid, kind: "dir", mode: 0o710, uid: 2000 }); + expect(entry.spillDirectory).toBe(`${entry.runtimeHome}/tool-output`); + expect(nodes.get(entry.spillDirectory)).toMatchObject({ gid: entry.uid, kind: "dir", mode: 0o2750, uid: 2000 }); + expect(entry.profilePath).toBe(`${entry.home}/.grok/sandbox.toml`); + } + }); + + it("allows a peer resource backing to be absent at provisioning (the entrypoint prepares it later) but never a symlink", () => { + const withResource = resolveDaimonGrokRegistrations([{ + ...plan({ "agent:a": "grok", "agent:b": "grok" }), + resources: [{ backingPath: "/var/lib/spawnfile/resources/instances/daimon-organization/b-repo", id: "b-repo", kind: "git", linkPath: `${INSTANCE}/workspace/agents/b/b-repo`, mode: "readonly", mount: "./b-repo", sharing: "agent" }] + } as unknown as RuntimeTargetPlan]); + const backing = "/var/lib/spawnfile/resources/instances/daimon-organization/b-repo"; + expect(withResource[0]!.deferredDenyPaths).toEqual([backing]); + expect(() => run(withResource, seedFor(withResource, [backing]))).not.toThrow(); + expect(() => run(withResource, { ...seedFor(withResource, [backing]), [backing]: { kind: "link", target: "/tmp/elsewhere" }, "/tmp/elsewhere": { kind: "dir" } })) + .toThrow(/canonical non-symlink/u); + expect(() => run(withResource, seedFor(withResource, ["/var/lib/spawnfile/daimon/usage"]))).toThrow(/deny path is missing/u); + }); + + it("is restart-idempotent over an already provisioned home", () => { + const first = run(registrations, seedFor(registrations)); + const reseeded = Object.fromEntries([...first.entries()].filter(([target]) => target !== "/etc/daimon-engine-broker/service.json")); + expect(() => run(registrations, reseeded)).not.toThrow(); + }); + + it("fails closed on a missing or symlinked deny path, or bytes that drifted from their pins", () => { + const realm = "/var/lib/spawnfile/daimon/grok-subscription-realm"; + expect(() => run(registrations, seedFor(registrations, [realm]))).toThrow(/deny path is missing: \/var\/lib\/spawnfile\/daimon\/grok-subscription-realm/u); + expect(() => run(registrations, { ...seedFor(registrations), [realm]: { kind: "link", target: "/tmp/elsewhere" }, "/tmp/elsewhere": { kind: "dir" } })) + .toThrow(/canonical non-symlink/u); + const workspace = registrations[0]!.workspace; + expect(() => run(registrations, { ...seedFor(registrations), [workspace]: { kind: "link", target: "/tmp/ws" }, "/tmp/ws": { kind: "dir" } })) + .toThrow(/canonical non-symlink/u); + const tampered = registrations.map((entry, index) => index === 0 ? { ...entry, config: entry.config.replace("grok-4.6", "grok-4.5") } : entry); + expect(() => run(tampered, seedFor(tampered))).toThrow(/do not match their pins/u); + // Another model's pinned bytes are still refused: the pin is per declared model x effort, not any pin. + const otherPair = registrations.map((entry, index) => index === 0 ? { ...entry, config: DAIMON_GROK_WORKER_CONFIG_BYTES["grok-4.5"].high, configSha256: crypto.createHash("sha256").update(DAIMON_GROK_WORKER_CONFIG_BYTES["grok-4.5"].high).digest("hex") } : entry); + expect(() => run(otherPair, seedFor(otherPair))).toThrow(/do not match their pins/u); + const otherEffort = registrations.map((entry, index) => index === 0 ? { ...entry, reasoningEffort: "medium" as const } : entry); + expect(() => run(otherEffort, seedFor(otherEffort))).toThrow(/do not match their pins/u); + const unpinned = registrations.map((entry, index) => index === 0 ? { ...entry, denyPaths: [] } : entry); + expect(() => run(unpinned, seedFor(unpinned))).toThrow(/do not match their pins/u); + const replaced = run(registrations, seedFor(registrations)); + const reseeded = Object.fromEntries([...replaced.entries()].filter(([target]) => target !== "/etc/daimon-engine-broker/service.json")); + reseeded[`${registrations[0]!.grokHome}/trusted_folders.toml`] = { ...reseeded[`${registrations[0]!.grokHome}/trusted_folders.toml`], content: "[trusted]\n" }; + expect(() => run(registrations, reseeded)).toThrow(/identity mismatch/u); + }); +}); + +describe("Grok host user-namespace preflight", () => { + const preflight = async (restrict: string | null, maxNamespaces: string | null) => { + const { mkdtemp, mkdir, rm, writeFile } = await import("node:fs/promises"); + const os = await import("node:os"); + const { spawnSync } = await import("node:child_process"); + const root = await mkdtemp(path.join(os.tmpdir(), "spawnfile-grok-procsys-")); + try { + await mkdir(path.join(root, "kernel")); await mkdir(path.join(root, "user")); + if (restrict !== null) await writeFile(path.join(root, "kernel", "apparmor_restrict_unprivileged_userns"), `${restrict}\n`); + if (maxNamespaces !== null) await writeFile(path.join(root, "user", "max_user_namespaces"), `${maxNamespaces}\n`); + const script = ["set -euo pipefail", ...renderDaimonGrokHostPreflight(root), "echo preflight-ok"].join("\n"); + return spawnSync("bash", ["-c", script], { encoding: "utf8" }); + } finally { + await rm(root, { force: true, recursive: true }); + } + }; + + it("refuses to start when the host restricts unprivileged user namespaces, naming the sysctl", async () => { + const restricted = await preflight("1", "63000"); + expect(restricted.status).toBe(1); + expect(restricted.stderr).toContain("kernel.apparmor_restrict_unprivileged_userns=0"); + expect((await preflight("0", "0")).stderr).toContain("user.max_user_namespaces is 0"); + expect((await preflight("0", "63000")).stdout).toContain("preflight-ok"); + expect((await preflight(null, null)).stdout).toContain("preflight-ok"); + }); +}); diff --git a/src/compiler/containerDaimonGrokWorkerProvisioning.ts b/src/compiler/containerDaimonGrokWorkerProvisioning.ts new file mode 100644 index 00000000..1e160c16 --- /dev/null +++ b/src/compiler/containerDaimonGrokWorkerProvisioning.ts @@ -0,0 +1,98 @@ +import { DAIMON_GROK_ENGINE_BROKER } from "../runtime/daimon/contractManifest.js"; +import { DAIMON_BROKER_UID, DAIMON_ORGANIZATION_UID } from "../runtime/daimon/runtimeIdentity.js"; +import { + DAIMON_GROK_DENIED_STATE_ROOTS, + DAIMON_GROK_OPTIONAL_DENY_PATHS, + DAIMON_GROK_WORKER_READ_ONLY_FILES, + DAIMON_WORKER_ROOT, + renderDaimonGrokServiceConfig, + type DaimonGrokRegistration +} from "./containerDaimonGrokWorkerRender.js"; + +/** Only the bytes the program writes; everything else it needs is recomputed or checked in place. */ +const programRegistration = (entry: DaimonGrokRegistration) => ({ + agentId: entry.agentId, + config: entry.config, + configSha256: entry.configSha256, + deferredDenyPaths: entry.deferredDenyPaths, + denyPaths: entry.denyPaths, + eventsPath: entry.eventsPath, + grokHome: entry.grokHome, + home: entry.home, + model: entry.model, + profile: entry.profile, + profilePath: entry.profilePath, + privateTmp: entry.privateTmp, + profileSha256: entry.profileSha256, + reasoningEffort: entry.reasoningEffort, + runtimeHome: entry.runtimeHome, + spillDirectory: entry.spillDirectory, + slot: entry.slot, + uid: entry.uid, + workspace: entry.workspace +}); + +/** + * Root provisioning for every brokered Grok worker, as lines of the broker's + * node provisioning program (which already defines `crypto`, `fs`, and + * `secureWorkspace`). + * + * Layout per Daimon's `GROK_ENGINE_BROKER.worker.home` (attested by the broker + * before every turn): `$GROK_HOME` (`/.grok`) and `$GROK_HOME/sessions` + * are `root: 1771`; `config.toml` (Daimon's renderer bytes for the + * declared model x effort), `sandbox.toml`, and the empty `trusted_folders.toml`, + * `managed_config.toml` and `requirements.toml` are `root:root 0444`, so the + * worker can neither write, rename nor unlink any file that decides a turn — + * trust included; `sessions/sandbox-events.jsonl` is `: 0640`. + * + * Every path handed to Daimon (workspace, home, profile, events, and each deny + * entry) must be canonical: it exists, is not a symlink, and resolves to itself. + * Daimon never resolves these paths, so a symlinked entry would silently mask + * the wrong inode. Root chmods only paths it owns at that moment (no + * `CAP_FOWNER`): reclaim, mode, then hand over. + */ +export const renderDaimonGrokWorkerProvisioning = (registrations: readonly DaimonGrokRegistration[]): string[] => [ + `const grokWorkers = ${JSON.stringify(registrations.map(programRegistration))};`, + `const optionalDenyPaths = new Set(${JSON.stringify([...DAIMON_GROK_OPTIONAL_DENY_PATHS, ...DAIMON_GROK_DENIED_STATE_ROOTS])});`, + `const pinnedConfigSha256 = ${JSON.stringify(DAIMON_GROK_ENGINE_BROKER.worker.configSha256)};`, + "const sha256Hex = (value) => crypto.createHash('sha256').update(value).digest('hex');", + "for (const entry of grokWorkers) { if (sha256Hex(entry.config) !== entry.configSha256 || !Object.hasOwn(pinnedConfigSha256, entry.model) || !Object.hasOwn(pinnedConfigSha256[entry.model], entry.reasoningEffort) || pinnedConfigSha256[entry.model][entry.reasoningEffort] !== entry.configSha256 || sha256Hex(entry.profile) !== entry.profileSha256 || entry.denyPaths.length === 0) throw new Error(`Grok worker contract bytes for ${entry.agentId} do not match their pins`); }", + // Root holds no CAP_FOWNER: every mode change reclaims the inode, sets the mode, then restores or hands over ownership. + "const withMode = (target, mode, uid, gid) => { const info = fs.lstatSync(target); if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`unsafe Grok worker directory: ${target}`); fs.chownSync(target, 0, 0); fs.chmodSync(target, mode); fs.chownSync(target, uid, gid); };", + "const traversable = (target) => { for (let ancestor = require('node:path').dirname(target); ancestor.startsWith('/var/lib/spawnfile/') && ancestor.length > '/var/lib/spawnfile'.length; ancestor = require('node:path').dirname(ancestor)) { const info = fs.lstatSync(ancestor); if ((info.mode & 0o011) !== 0o011) withMode(ancestor, (info.mode & 0o7777) | 0o011, info.uid, info.gid); } };", + "const assertCanonical = (target, label) => { const info = fs.lstatSync(target); if (info.isSymbolicLink() || fs.realpathSync(target) !== target) throw new Error(`Grok worker ${label} is not a canonical non-symlink path: ${target}`); return info; };", + "const ensureDirectory = (target, uid, gid, mode) => { fs.mkdirSync(target, { recursive: true, mode: 0o700 }); const info = fs.lstatSync(target); if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('unsafe worker runtime directory'); fs.chownSync(target, 0, 0); fs.chmodSync(target, mode); fs.chownSync(target, uid, gid); };", + "const ensureExactFile = (target, content, mode) => { let info; try { info = fs.lstatSync(target); } catch (error) { if (error.code !== 'ENOENT') throw error; fs.writeFileSync(target, content, { mode, flag: 'wx' }); info = fs.lstatSync(target); } if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) throw new Error('unsafe worker runtime file'); if (fs.readFileSync(target, 'utf8') !== content) throw new Error(`worker runtime file identity mismatch: ${target}`); fs.chownSync(target, 0, 0); fs.chmodSync(target, mode); };", + `const ensureEventsFile = (target, uid) => { let info; try { info = fs.lstatSync(target); } catch (error) { if (error.code !== 'ENOENT') throw error; fs.writeFileSync(target, '', { mode: 0o640, flag: 'wx' }); info = fs.lstatSync(target); } if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || (info.uid !== uid && info.uid !== 0) || ![0, uid, ${DAIMON_BROKER_UID}].includes(info.gid) || ![0o600, 0o640, 0o644].includes(info.mode & 0o777)) throw new Error('unsafe worker attestation events'); fs.chownSync(target, 0, 0); fs.chmodSync(target, 0o640); fs.chownSync(target, uid, ${DAIMON_BROKER_UID}); };`, + `fs.mkdirSync('${DAIMON_WORKER_ROOT}', { recursive: true, mode: 0o711 }); fs.chownSync('${DAIMON_WORKER_ROOT}', 0, 0); fs.chmodSync('${DAIMON_WORKER_ROOT}', 0o711); assertCanonical('${DAIMON_WORKER_ROOT}', 'worker root');`, + // Pass 1: every workspace and home exists, root-held, before any deny list is checked. + `for (const entry of grokWorkers) { traversable(entry.workspace); secureWorkspace(entry.workspace, entry.uid); assertCanonical(entry.workspace, 'workspace'); ensureDirectory(entry.home, 0, 0, 0o700); ensureDirectory(entry.grokHome, 0, 0, 0o700); ensureDirectory(\`\${entry.grokHome}/sessions\`, 0, 0, 0o700); for (const target of [entry.home, entry.grokHome]) assertCanonical(target, 'home'); }`, + "for (const denied of optionalDenyPaths) { try { fs.lstatSync(denied); } catch (error) { if (error.code !== 'ENOENT') throw error; fs.mkdirSync(denied, { mode: 0o700 }); fs.chownSync(denied, 0, 0); fs.chmodSync(denied, 0o700); } }", + "for (const entry of grokWorkers) for (const denied of entry.denyPaths) { try { assertCanonical(denied, 'deny path'); } catch (error) { if (error.code === 'ENOENT' && entry.deferredDenyPaths.includes(denied)) continue; if (error.code === 'ENOENT') throw new Error(`Grok worker deny path is missing: ${denied}`); throw error; } }", + // Pass 2: exact root-owned read-only files, the events file, then the final sticky modes. + `for (const entry of grokWorkers) { ensureExactFile(\`\${entry.grokHome}/config.toml\`, entry.config, 0o444); ensureExactFile(entry.profilePath, entry.profile, 0o444); for (const name of ${JSON.stringify(DAIMON_GROK_WORKER_READ_ONLY_FILES.filter((name) => name !== "config.toml" && name !== "sandbox.toml"))}) ensureExactFile(\`\${entry.grokHome}/\${name}\`, '', 0o444); ensureEventsFile(entry.eventsPath, entry.uid); ensureDirectory(\`\${entry.grokHome}/sessions\`, 0, entry.uid, 0o1771); ensureDirectory(entry.grokHome, 0, entry.uid, 0o1771); ensureDirectory(entry.home, entry.uid, ${DAIMON_BROKER_UID}, 0o710); for (const target of [entry.profilePath, entry.eventsPath, \`\${entry.grokHome}/config.toml\`]) assertCanonical(target, 'home file'); }`, + // Worker-private temp: the launcher compiles TMPDIR=/tmp, the only temp the worker may write. + "for (const entry of grokWorkers) { ensureDirectory(entry.privateTmp, entry.uid, entry.uid, 0o700); assertCanonical(entry.privateTmp, 'private temp'); }", + // Spills: /tool-output 2000: 2750 (setgid) under a runtime home the worker's group can traverse. + `for (const entry of grokWorkers) { traversable(entry.runtimeHome); fs.mkdirSync(entry.runtimeHome, { recursive: true, mode: 0o700 }); assertCanonical(entry.runtimeHome, 'runtime home'); withMode(entry.runtimeHome, 0o710, ${DAIMON_ORGANIZATION_UID}, entry.uid); try { fs.mkdirSync(entry.spillDirectory, { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; } assertCanonical(entry.spillDirectory, 'spill directory'); withMode(entry.spillDirectory, 0o2750, ${DAIMON_ORGANIZATION_UID}, entry.uid); }`, + // Shared temp: Grok refuses a profile denying /tmp or /var/tmp, so modes close them: root: 1774 lets workers list names only. + `for (const shared of ${JSON.stringify(DAIMON_GROK_ENGINE_BROKER.worker.home.sharedTmp.paths)}) { fs.mkdirSync(shared, { recursive: true, mode: 0o1777 }); assertCanonical(shared, 'shared temp'); withMode(shared, 0o${DAIMON_GROK_ENGINE_BROKER.worker.home.sharedTmp.mode.toString(8)}, 0, ${DAIMON_ORGANIZATION_UID}); }`, + `const service = ${JSON.stringify(renderDaimonGrokServiceConfig(registrations))};`, + "for (const registration of service.registrations) { const entry = grokWorkers.find((worker) => worker.agentId === registration.agentId); if (!entry || registration.profileSha256 !== sha256Hex(fs.readFileSync(entry.profilePath, 'utf8'))) throw new Error('Grok broker service registration does not match its provisioned profile'); }", + `fs.writeFileSync('${DAIMON_GROK_ENGINE_BROKER.serviceConfigPath}', \`\${JSON.stringify(service)}\\n\`, { mode: 0o440, flag: 'wx' }); fs.chownSync('${DAIMON_GROK_ENGINE_BROKER.serviceConfigPath}', 0, ${DAIMON_BROKER_UID}); fs.chmodSync('${DAIMON_GROK_ENGINE_BROKER.serviceConfigPath}', 0o440);` +]; + +const shellQuote = (value: string): string => `'${value.replace(/'/g, `'"'"'`)}'`; + +/** + * Grok 1.0.34 runs every sandbox profile inside bubblewrap, which needs + * unprivileged user namespaces. Ubuntu 24.04+ hosts (and Colima's default VM) + * ship `kernel.apparmor_restrict_unprivileged_userns=1`, under which bubblewrap + * cannot create them even with the pinned seccomp profile — every worker turn + * would then fail attestation long after startup. The container sees the host + * kernel's sysctls read-only, so this refuses to start with the fix named. + */ +export const renderDaimonGrokHostPreflight = (procSys = "/proc/sys"): string[] => [ + `if [ -r ${shellQuote(`${procSys}/kernel/apparmor_restrict_unprivileged_userns`)} ] && [ "$(cat ${shellQuote(`${procSys}/kernel/apparmor_restrict_unprivileged_userns`)})" != 0 ]; then echo "Daimon Grok workers need unprivileged user namespaces for bubblewrap: set kernel.apparmor_restrict_unprivileged_userns=0 on the Docker host (sysctl -w kernel.apparmor_restrict_unprivileged_userns=0; persist it in /etc/sysctl.d)" >&2; exit 1; fi`, + `if [ -r ${shellQuote(`${procSys}/user/max_user_namespaces`)} ] && [ "$(cat ${shellQuote(`${procSys}/user/max_user_namespaces`)})" = 0 ]; then echo "Daimon Grok workers need unprivileged user namespaces for bubblewrap: user.max_user_namespaces is 0 on the Docker host" >&2; exit 1; fi` +]; diff --git a/src/compiler/containerDaimonGrokWorkerRender.test.ts b/src/compiler/containerDaimonGrokWorkerRender.test.ts new file mode 100644 index 00000000..c38cb3c5 --- /dev/null +++ b/src/compiler/containerDaimonGrokWorkerRender.test.ts @@ -0,0 +1,150 @@ +import { createHash } from "node:crypto"; + +import { describe, expect, it } from "vitest"; + +import { DAIMON_GROK_ENGINE_BROKER, DAIMON_GROK_TURN_USAGE_LEDGER } from "../runtime/daimon/contractManifest.js"; +import { DAIMON_GROK_WORKER_CONFIG_BYTES } from "../runtime/daimon/grokWorkerConfigBytes.js"; +import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js"; +import { + assertCanonicalRegisteredPath, + DAIMON_GROK_OPTIONAL_DENY_PATHS, + renderDaimonGrokServiceConfig, + resolveDaimonGrokRegistrations +} from "./containerDaimonGrokWorkerRender.js"; + +const INSTANCE = "/var/lib/spawnfile/instances/daimon/daimon-organization"; +const sha256 = (value: string): string => createHash("sha256").update(value).digest("hex"); + +const grokWorkerPlan = ( + engines: Record, + models: Record = Object.fromEntries( + Object.entries(engines).filter(([, engine]) => engine === "grok").map(([id]) => [id, { model: "grok-4.6", reasoningEffort: "low" }]) + ) +): RuntimeTargetPlan => ({ + engineByNodeId: engines, + grokModelByNodeId: models, + instancePaths: { configPath: `${INSTANCE}/daimon/daimon-organization-runtime.json`, instanceRoot: INSTANCE, workspacePath: `${INSTANCE}/workspace` }, + runtimeName: "daimon" +}) as unknown as RuntimeTargetPlan; + +describe("Daimon Grok worker registrations", () => { + it("uses Daimon's pinned worker config bytes for each agent's declared model and reasoning effort", () => { + const registrations = resolveDaimonGrokRegistrations([grokWorkerPlan({ "agent:a": "grok", "agent:b": "grok" }, { + "agent:a": { model: "grok-4.6", reasoningEffort: "low" }, + "agent:b": { model: "grok-build", reasoningEffort: "high" } + })]); + expect(registrations.map((entry) => [entry.agentId, entry.uid, entry.home, entry.grokHome])).toEqual([ + ["agent:a", 2200, "/var/lib/daimon-workers/2200", "/var/lib/daimon-workers/2200/.grok"], + ["agent:b", 2201, "/var/lib/daimon-workers/2201", "/var/lib/daimon-workers/2201/.grok"] + ]); + const [a, b] = registrations; + expect(a!.config).toBe(DAIMON_GROK_WORKER_CONFIG_BYTES["grok-4.6"].low); + expect(sha256(a!.config)).toBe(DAIMON_GROK_ENGINE_BROKER.worker.configSha256["grok-4.6"].low); + expect(b!.config).toBe(DAIMON_GROK_WORKER_CONFIG_BYTES["grok-build"].high); + expect(sha256(b!.config)).toBe(DAIMON_GROK_ENGINE_BROKER.worker.configSha256["grok-build"].high); + expect(a!.config).not.toContain("auth_provider"); + expect(a!.profilePath).toBe("/var/lib/daimon-workers/2200/.grok/sandbox.toml"); + expect(a!.eventsPath).toBe("/var/lib/daimon-workers/2200/.grok/sessions/sandbox-events.jsonl"); + }); + + it("refuses a Grok registration without a declared closed-list model and effort", () => { + expect(() => resolveDaimonGrokRegistrations([grokWorkerPlan({ "agent:a": "grok" }, {})])).toThrow(/no declared broker model/u); + const rootless = { ...grokWorkerPlan({ "agent:a": "grok" }), instancePaths: { configPath: "/c.json", workspacePath: "/w" } } as RuntimeTargetPlan; + expect(() => resolveDaimonGrokRegistrations([rootless])).toThrow(/require an instance root/u); + expect(() => resolveDaimonGrokRegistrations([grokWorkerPlan({ "agent:a": "grok" }, { "agent:a": { model: "grok-4", reasoningEffort: "low" } })])).toThrow(/no declared broker model/u); + expect(() => resolveDaimonGrokRegistrations([grokWorkerPlan({ "agent:a": "grok" }, { "agent:a": { model: "grok-4.6", reasoningEffort: "xhigh" } })])).toThrow(/no declared broker model/u); + }); + + it("denies every realm, broker, ledger, secret, peer, and other-worker path, and never the worker's own", () => { + const [a, b] = resolveDaimonGrokRegistrations([grokWorkerPlan({ "agent:a": "grok", "agent:b": "grok", "agent:c": "codex", "agent:d": "agy" })]); + const denied = a!.denyPaths; + expect(denied.length).toBeGreaterThan(0); + expect(denied).toEqual(expect.arrayContaining([ + "/var/lib/spawnfile/daimon/grok-bootstrap-auth", + "/var/lib/spawnfile/daimon/grok-subscription-realm", + "/var/lib/spawnfile/daimon/agy-unlock-secret", + "/var/lib/spawnfile/daimon/agy-subscription-realm", + "/etc/daimon-engine-broker", + "/run/daimon-engine-broker", + DAIMON_GROK_TURN_USAGE_LEDGER.directoryPath, + "/var/lib/spawnfile/daimon/wake-fuse", + `${INSTANCE}/state/wake-acceptance`, + `${INSTANCE}/runtime-homes/b`, `${INSTANCE}/workspace/agents/b`, + `${INSTANCE}/runtime-homes/c`, `${INSTANCE}/workspace/agents/c`, + `${INSTANCE}/runtime-homes/d`, `${INSTANCE}/workspace/agents/d`, + "/var/lib/daimon-workers/2201", + ...DAIMON_GROK_OPTIONAL_DENY_PATHS + ])); + for (const own of [`${INSTANCE}/workspace/agents/a`, `${INSTANCE}/runtime-homes/a`, "/var/lib/daimon-workers/2200"]) expect(denied).not.toContain(own); + expect(b!.denyPaths).toContain("/var/lib/daimon-workers/2200"); + expect(denied).toEqual([...denied].sort()); + for (const entry of denied) expect(denied.some((other) => entry.startsWith(`${other}/`))).toBe(false); + expect(a!.profile).toBe(`[profiles.daimon-strict]\nextends = "strict"\nrestrict_network = true\ndeny = [${denied.map((entry) => JSON.stringify(entry)).join(", ")}]\n`); + const soloDenied = resolveDaimonGrokRegistrations([grokWorkerPlan({ "agent:a": "grok" })])[0]!.denyPaths; + expect(soloDenied).not.toContain("/var/lib/spawnfile/daimon/agy-subscription-realm"); + expect(soloDenied.length).toBeGreaterThan(8); + }); + + it("renders service.json v2 with per-slot ledger, limits, model, and the profile digest", () => { + const registrations = resolveDaimonGrokRegistrations([grokWorkerPlan({ "agent:a": "grok" }, { "agent:a": { model: "grok-4.5", reasoningEffort: "medium" } })]); + const service = renderDaimonGrokServiceConfig(registrations); + expect(Object.keys(service)).toEqual(["version", "credentialHome", "turnStore", "registrations"]); + expect(service.version).toBe("noopolis.daimon.engine-broker-service.v2"); + expect(service.registrations).toEqual([{ + agentId: "agent:a", + slot: 0, + workerUid: 2200, + workspace: `${INSTANCE}/workspace/agents/a`, + profilePath: "/var/lib/daimon-workers/2200/.grok/sandbox.toml", + eventsPath: "/var/lib/daimon-workers/2200/.grok/sessions/sandbox-events.jsonl", + profileSha256: sha256(registrations[0]!.profile), + usageLedgerPath: DAIMON_GROK_TURN_USAGE_LEDGER.filePath, + limits: { maxRequests: 32, maxTokens: 300_000, timeoutMs: 240_000 }, + model: { id: "grok-4.5", reasoningEffort: "medium" } + }]); + }); +}); + +describe("Grok base-profile grant guard and nested-mask refusal", () => { + const plan = (extra: Partial): RuntimeTargetPlan => ({ ...grokWorkerPlan({ "agent:a": "grok", "agent:b": "grok" }), ...extra }) as RuntimeTargetPlan; + const mount = (mount_path: string) => ({ id: mount_path, mount_path, reason: "test", volume_name: "v" }); + + it("refuses a deny entry equal to or above a base-profile grant, and keeps entries below grants", () => { + for (const grant of ["/run", "/var", "/tmp", "/var/tmp", "/etc"]) { + expect(() => resolveDaimonGrokRegistrations([plan({ persistentMounts: [mount(grant)] })]), grant).toThrow(/base profile grant/u); + } + expect(() => resolveDaimonGrokRegistrations([plan({ persistentMounts: [mount("/var/lib/daimon-workers/2200/.grok/sessions")] })])).toThrow(/own workspace, home|base profile grant/u); + const [a] = resolveDaimonGrokRegistrations([plan({})]); + const grants = ["/bin", "/dev", "/etc", "/lib", "/proc", "/run", "/sbin", "/sys", "/tmp", "/usr", "/var", "/var/tmp", a!.workspace, a!.grokHome, `${a!.grokHome}/sessions`, `${a!.home}/tmp`]; + for (const entry of a!.denyPaths) { + for (const grant of grants) expect(grant === entry || grant.startsWith(`${entry}/`), `${entry} vs ${grant}`).toBe(false); + } + expect(a!.denyPaths).toEqual(expect.arrayContaining(["/run/secrets", "/run/daimon-engine-broker"])); + }); + + it("refuses an added ancestor that would cover a Daimon deny entry, since masks cannot nest", () => { + expect(() => resolveDaimonGrokRegistrations([plan({ persistentMounts: [mount("/var/lib/spawnfile/instances/daimon/daimon-organization/state")] })])) + .toThrow(/would cover .*state\/wake-acceptance; masks cannot nest/u); + }); +}); + +describe("canonical registered paths", () => { + it("accepts only absolute canonical paths of at most 255 bytes", () => { + expect(assertCanonicalRegisteredPath("home", "/var/lib/daimon-workers/2200")).toBe("/var/lib/daimon-workers/2200"); + for (const bad of ["relative/home", "/", "/var/lib/", "/var//lib", "/var/./lib", "/var/../lib", "/var/lib/.", `/${"a".repeat(255)}`]) { + expect(() => assertCanonicalRegisteredPath("home", bad), bad).toThrow(/canonical registered path/u); + } + expect(assertCanonicalRegisteredPath("home", `/${"a".repeat(254)}`)).toHaveLength(255); + }); + + it("writes only canonical workspace, home, and runtime paths, and refuses one that cannot fit the registration record", () => { + const messy = { ...grokWorkerPlan({ "agent:a": "grok" }), instancePaths: { configPath: `${INSTANCE}/daimon/config.json`, instanceRoot: `${INSTANCE}/`, workspacePath: `${INSTANCE}//workspace/` } } as RuntimeTargetPlan; + const [entry] = resolveDaimonGrokRegistrations([messy]); + for (const value of [entry!.workspace, entry!.home, entry!.grokHome, entry!.profilePath, entry!.eventsPath, entry!.privateTmp, entry!.runtimeHome, entry!.spillDirectory]) { + expect(() => assertCanonicalRegisteredPath("path", value), value).not.toThrow(); + } + const longId = `agent:${"x".repeat(240)}`; + expect(() => resolveDaimonGrokRegistrations([grokWorkerPlan({ [longId]: "grok" })])).toThrow(/canonical registered path/u); + expect(() => resolveDaimonGrokRegistrations([grokWorkerPlan({ "agent:!!!": "grok" })])).toThrow(/no path-safe slug/u); + }); +}); diff --git a/src/compiler/containerDaimonGrokWorkerRender.ts b/src/compiler/containerDaimonGrokWorkerRender.ts new file mode 100644 index 00000000..5bab54c0 --- /dev/null +++ b/src/compiler/containerDaimonGrokWorkerRender.ts @@ -0,0 +1,285 @@ +import path from "node:path"; + +import { SpawnfileError } from "../shared/index.js"; +import { + DAIMON_ORGANIZATION_TARGET_ID, + DAIMON_RUNTIME_ACCEPTANCE_STORE_DIRECTORY, + DAIMON_RUNTIME_HOMES_DIRECTORY, + DAIMON_WAKE_FUSE_DIRECTORY +} from "../runtime/daimon/config.js"; +import { + DAIMON_AGY_SUBSCRIPTION_REALM, + DAIMON_GROK_BROKER_MODELS, + DAIMON_GROK_BROKER_REASONING_EFFORTS, + DAIMON_GROK_ENGINE_BROKER, + DAIMON_GROK_SUBSCRIPTION_REALM, + DAIMON_GROK_TURN_USAGE_LEDGER, + DAIMON_RUNTIME_HOME_ROOT, + type DaimonGrokBrokerModel, + type DaimonGrokBrokerReasoningEffort +} from "../runtime/daimon/contractManifest.js"; +import { + DAIMON_GROK_WORKER_HOME_DIRECTORY, + daimonGrokWorkerSandboxProfileSha256, + renderDaimonGrokWorkerSandboxProfile, + resolveDaimonGrokWorkerConfig +} from "../runtime/daimon/grokWorkerContract.js"; +import { DAIMON_FIRST_WORKER_UID } from "../runtime/daimon/runtimeIdentity.js"; +import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js"; + +export const DAIMON_WORKER_ROOT = "/var/lib/daimon-workers"; +export const DAIMON_GROK_WORKER_READ_ONLY_FILES = DAIMON_GROK_ENGINE_BROKER.worker.home.readOnlyFiles.names; +/** Container paths no Grok worker may read that nothing else guarantees to exist; provisioning creates them root-owned 0700 when absent. */ +export const DAIMON_GROK_OPTIONAL_DENY_PATHS = ["/run/secrets", "/run/spawnfile", "/run/spawnfile-secrets", "/run/world"] as const; + +/** + * The organization runtime state directory — the parent of the durable wake + * acceptance store. The ownership guard secures it to `0700 2000:2000` for + * every Daimon organization; the acceptance store beneath it is also on every + * Grok worker's sandbox deny list. + */ +export const DAIMON_ORGANIZATION_STATE_DIRECTORY = path.posix.join( + DAIMON_RUNTIME_HOME_ROOT, + DAIMON_ORGANIZATION_TARGET_ID, + "state" +); + +/** + * Paths Grok 1.0.34's strict base profile grants (read or read-write). Grok + * refuses to start when a deny entry equals or contains one of them (verified + * for `/tmp`, `/var/tmp`, `/run`, `/etc` and `sessions`; `/tmp/sub` works), so a + * deny entry must always sit strictly below every grant it touches. + */ +export const DAIMON_GROK_BASE_PROFILE_GRANTS = ["/bin", "/dev", "/etc", "/lib", "/proc", "/run", "/sbin", "/sys", "/tmp", "/usr", "/var", "/var/tmp"] as const; +export const DAIMON_GROK_MAX_REGISTERED_PATH_BYTES = 255; + +/** + * The canonical path rule Daimon's service parser and native launcher enforce: + * absolute, no empty, `.` or `..` component, no trailing slash, and at most 255 + * bytes (the registration record's NUL-terminated 256-byte fields). + */ +export const assertCanonicalRegisteredPath = (label: string, value: string): string => { + const components = value.split("/").slice(1); + if (!value.startsWith("/") || value.length < 2 || value.endsWith("/") || components.some((part) => part === "" || part === "." || part === "..") + || path.posix.normalize(value) !== value || value.includes("\0") || Buffer.byteLength(value) > DAIMON_GROK_MAX_REGISTERED_PATH_BYTES) { + fail(`Grok worker ${label} is not a canonical registered path: ${JSON.stringify(value)}`); + } + return value; +}; + +export interface DaimonGrokRegistration { + agentId: string; + config: string; + configSha256: string; + denyPaths: string[]; + /** Deny entries the main entrypoint materializes after root provisioning (workspace resource backings); absent is allowed there, a symlink never. */ + deferredDenyPaths: string[]; + eventsPath: string; + grokHome: string; + home: string; + model: DaimonGrokBrokerModel; + profile: string; + profilePath: string; + profileSha256: string; + /** `/tmp`: the launcher's compiled `TMPDIR` for this worker. */ + privateTmp: string; + reasoningEffort: DaimonGrokBrokerReasoningEffort; + /** The organization runtime home whose `tool-output/` this worker reads. */ + runtimeHome: string; + /** `/tool-output`: setgid spill directory in the worker's group. */ + spillDirectory: string; + slot: number; + uid: number; + usageLedgerPath: string; + workspace: string; +} + +const nodeSlug = (nodeId: string): string => nodeId.replace(/^agent:/u, "") + .toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, ""); + +const fail = (message: string): never => { + throw new SpawnfileError("compile_error", message); +}; + +const declaredModel = (plan: RuntimeTargetPlan, agentId: string): { model: DaimonGrokBrokerModel; reasoningEffort: DaimonGrokBrokerReasoningEffort } => { + const declared = plan.grokModelByNodeId?.[agentId]; + if (!declared || !(DAIMON_GROK_BROKER_MODELS as readonly string[]).includes(declared.model) + || !(DAIMON_GROK_BROKER_REASONING_EFFORTS as readonly string[]).includes(declared.reasoningEffort)) { + return fail(`Daimon Grok agent ${agentId} has no declared broker model and reasoning effort`); + } + return declared as { model: DaimonGrokBrokerModel; reasoningEffort: DaimonGrokBrokerReasoningEffort }; +}; + +/** + * Spawnfile-managed state roots no Grok worker needs: the shared Moltnet store + * (server data, node/bridge configs, receipt stores, network state), per-agent + * Moltnet open-mode token directories, and declared Mneme memory banks. A + * worker reaches Moltnet and memory only through Daimon's MCP tools, which run + * in the organization process, never by reading these files. Provisioning + * creates each root-owned `0700` when absent so the mask always has a target. + */ +export const DAIMON_GROK_DENIED_STATE_ROOTS = ["/var/lib/spawnfile/agents", "/var/lib/spawnfile/memory", "/var/lib/spawnfile/moltnet"] as const; + +const within = (candidate: string, root: string): boolean => candidate === root || candidate.startsWith(`${root}/`); + +/** + * One brokered worker's sandbox deny list. + * + * Daimon's own protected set for the agent (`grokSandboxProtectedPaths`: the + * Grok bootstrap and realm, the AGY realm and unlock secret when any agent is + * AGY, the wake-acceptance store, and every peer's runtime home and workspace) + * is always kept verbatim, so a Daimon projection over the same inputs renders + * the same profile. This deployment adds everything else the container + * provisions that the worker does not need: the organization config directory + * (all agents' instructions and any env files), every persistent mount of + * every runtime plan (the worker's own tool state, credential home and memory + * included — it reaches them only through Daimon), every other runtime + * instance root, the shared Moltnet/agent/memory state roots, every workspace + * resource backing path not linked from this agent's own workspace, the broker's + * `/etc` and `/run` directories, the usage ledger and wake fuse, every other + * worker's home, and the container secret roots. + * + * Allowed on purpose: the agent's own workspace (the worker's cwd), its own + * runtime home directory itself (Daimon's tool-result spill contract names paths + * under it; its contents are persistent mounts and denied), its own worker home, + * and backing paths of resources linked into its own workspace (a mask over the + * backing inode would also hide the agent's own resource through its link). + * + * Grok 1.0.34's strict base reads all of `/run`, `/var`, `/tmp` and `/etc`, and + * macOS bind mounts ignore unix modes, so this list — not file modes — is the + * boundary. Masks cannot nest: an added entry already covered by another entry + * is dropped, and an added entry that would cover a Daimon entry is refused. + */ +export const resolveDaimonGrokWorkerDenyPaths = ( + plans: readonly RuntimeTargetPlan[], + plan: RuntimeTargetPlan, + agentId: string, + workerHomes: readonly string[], + ownHome: string +): string[] => { + const instanceRoot = plan.instancePaths.instanceRoot ?? fail("Daimon Grok registrations require an instance root"); + const agents = Object.entries(plan.engineByNodeId ?? {}); + const ownWorkspace = path.posix.join(plan.instancePaths.workspacePath, "agents", nodeSlug(agentId)); + const ownRuntimeHome = path.posix.join(instanceRoot, DAIMON_RUNTIME_HOMES_DIRECTORY, nodeSlug(agentId)); + const peers = agents.filter(([id]) => id !== agentId).map(([id]) => nodeSlug(id)); + const daimonOwn = [ + DAIMON_GROK_SUBSCRIPTION_REALM.bootstrapMountPath, + DAIMON_GROK_SUBSCRIPTION_REALM.durableMountPath, + ...(agents.some(([, engine]) => engine === "agy") ? [DAIMON_AGY_SUBSCRIPTION_REALM.unlockMountPath, DAIMON_AGY_SUBSCRIPTION_REALM.durableMountPath] : []), + path.posix.join(instanceRoot, DAIMON_RUNTIME_ACCEPTANCE_STORE_DIRECTORY), + ...peers.flatMap((slug) => [ + path.posix.join(instanceRoot, DAIMON_RUNTIME_HOMES_DIRECTORY, slug), + path.posix.join(plan.instancePaths.workspacePath, "agents", slug) + ]) + ]; + const ownResourceBackings = new Set((plan.resources ?? []) + .filter((resource) => within(resource.linkPath, ownWorkspace)) + .map((resource) => resource.backingPath)); + const added = [ + path.posix.dirname(plan.instancePaths.configPath), + ...plans.flatMap((candidate) => (candidate.persistentMounts ?? []).map((mount) => mount.mount_path)), + ...plans.filter((candidate) => candidate !== plan).flatMap((candidate) => [ + candidate.instancePaths.instanceRoot ?? path.posix.dirname(candidate.instancePaths.configPath), + ...(candidate.envFiles ?? []).map((binding) => binding.filePath) + ]), + ...(plan.envFiles ?? []).map((binding) => binding.filePath), + ...plans.flatMap((candidate) => candidate.resources ?? []).map((resource) => resource.backingPath) + .filter((backing) => !ownResourceBackings.has(backing)), + ...DAIMON_GROK_DENIED_STATE_ROOTS, + path.posix.dirname(DAIMON_GROK_ENGINE_BROKER.registrationPath), + path.posix.dirname(DAIMON_GROK_ENGINE_BROKER.controlSocketPath), + DAIMON_GROK_TURN_USAGE_LEDGER.directoryPath, + DAIMON_WAKE_FUSE_DIRECTORY, + ...workerHomes.filter((home) => home !== ownHome), + ...DAIMON_GROK_OPTIONAL_DENY_PATHS + ].filter((entry) => entry.startsWith("/")); + const grokHome = path.posix.join(ownHome, DAIMON_GROK_WORKER_HOME_DIRECTORY); + const grants = [...DAIMON_GROK_BASE_PROFILE_GRANTS, ownWorkspace, grokHome, path.posix.join(grokHome, "sessions"), path.posix.join(ownHome, "tmp")]; + for (const entry of [...daimonOwn, ...added]) { + const grant = grants.find((candidate) => within(candidate, entry)); + if (grant) fail(`Grok worker deny path ${entry} equals or contains the base profile grant ${grant}; Grok refuses such a profile`); + } + for (const entry of added) { + if (within(ownWorkspace, entry) || within(ownHome, entry) || entry === ownRuntimeHome || within(ownRuntimeHome, entry) + || [...ownResourceBackings].some((backing) => within(backing, entry))) { + fail(`Grok worker deny path ${entry} would hide ${agentId}'s own workspace, home, or resources`); + } + } + const candidates = [...new Set([...daimonOwn, ...added])]; + const daimonSet = new Set(daimonOwn); + const denied = candidates.filter((entry) => daimonSet.has(entry) + || !candidates.some((other) => other !== entry && within(entry, other))).sort(); + for (const entry of denied) { + const ancestor = denied.find((other) => other !== entry && within(entry, other)); + if (ancestor) fail(`Grok worker deny path ${ancestor} would cover ${entry}; masks cannot nest`); + } + return denied; +}; + +export const resolveDaimonGrokRegistrations = (plans: RuntimeTargetPlan[]): DaimonGrokRegistration[] => { + const entries = plans + .filter((plan) => plan.runtimeName === "daimon") + .flatMap((plan) => Object.entries(plan.engineByNodeId ?? {}) + .filter(([, engine]) => engine === "grok") + .map(([agentId]) => ({ agentId, plan }))) + .sort((left, right) => left.agentId.localeCompare(right.agentId)); + const homes = entries.map((_, slot) => path.posix.join(DAIMON_WORKER_ROOT, String(DAIMON_FIRST_WORKER_UID + slot))); + return entries.map(({ agentId, plan }, slot) => { + if (nodeSlug(agentId) === "") fail(`Daimon Grok agent ${agentId} has no path-safe slug`); + const home = assertCanonicalRegisteredPath("home", homes[slot]!); + const grokHome = path.posix.join(home, DAIMON_GROK_WORKER_HOME_DIRECTORY); + const instanceRoot = plan.instancePaths.instanceRoot ?? fail("Daimon Grok registrations require an instance root"); + const runtimeHome = assertCanonicalRegisteredPath("runtime home", path.posix.join(instanceRoot, DAIMON_RUNTIME_HOMES_DIRECTORY, nodeSlug(agentId))); + const { model, reasoningEffort } = declaredModel(plan, agentId); + const config = resolveDaimonGrokWorkerConfig(model, reasoningEffort); + const denyPaths = resolveDaimonGrokWorkerDenyPaths(plans, plan, agentId, homes, home); + const profile = renderDaimonGrokWorkerSandboxProfile(denyPaths); + for (const [label, value] of [["GROK_HOME", grokHome], ["profile", path.posix.join(grokHome, "sandbox.toml")], ["events", path.posix.join(grokHome, DAIMON_GROK_ENGINE_BROKER.worker.home.sandboxEvents.relativePath)], ["private temp", path.posix.join(home, DAIMON_GROK_ENGINE_BROKER.worker.home.privateTmp.relativeToWorkerHome)]] as const) assertCanonicalRegisteredPath(label, value); + // The launcher derives HOME, GROK_HOME=/.grok and TMPDIR=/tmp from the registered home, and the broker reads the profile from GROK_HOME. + if (path.posix.dirname(path.posix.dirname(path.posix.join(grokHome, "sandbox.toml"))) !== home) fail(`Grok worker ${agentId} profile is not under its registered home`); + const backings = new Set(plans.flatMap((candidate) => candidate.resources ?? []).map((resource) => resource.backingPath)); + return { + deferredDenyPaths: denyPaths.filter((entry) => backings.has(entry)), + agentId, + config: config.bytes, + configSha256: config.sha256, + denyPaths, + eventsPath: path.posix.join(grokHome, DAIMON_GROK_ENGINE_BROKER.worker.home.sandboxEvents.relativePath), + grokHome, + home, + model, + profile, + profilePath: path.posix.join(grokHome, "sandbox.toml"), + profileSha256: daimonGrokWorkerSandboxProfileSha256(denyPaths), + privateTmp: path.posix.join(home, DAIMON_GROK_ENGINE_BROKER.worker.home.privateTmp.relativeToWorkerHome), + reasoningEffort, + runtimeHome, + spillDirectory: path.posix.join(runtimeHome, DAIMON_GROK_ENGINE_BROKER.worker.home.spillDirectory.relativeToRuntimeHome), + slot, + uid: DAIMON_FIRST_WORKER_UID + slot, + // Production keeps one container ledger: `spawnfile usage` and Daimon's + // wake fuse both read it, so a per-slot file would hide Grok spend from both. + usageLedgerPath: DAIMON_GROK_TURN_USAGE_LEDGER.filePath, + workspace: assertCanonicalRegisteredPath("workspace", path.posix.join(plan.instancePaths.workspacePath, "agents", nodeSlug(agentId))) + }; + }); +}; + +/** `service.json` v2, exactly the shape Daimon's strict `parseEngineBrokerServiceConfig` accepts. */ +export const renderDaimonGrokServiceConfig = (registrations: readonly DaimonGrokRegistration[]) => ({ + version: DAIMON_GROK_ENGINE_BROKER.serviceConfigVersions[1], + credentialHome: DAIMON_GROK_ENGINE_BROKER.credentialHomePath, + turnStore: DAIMON_GROK_ENGINE_BROKER.turnStorePath, + registrations: registrations.map((entry) => ({ + agentId: entry.agentId, + slot: entry.slot, + workerUid: entry.uid, + workspace: entry.workspace, + profilePath: entry.profilePath, + eventsPath: entry.eventsPath, + profileSha256: entry.profileSha256, + usageLedgerPath: entry.usageLedgerPath, + limits: { ...DAIMON_GROK_ENGINE_BROKER.turnLimits.v1Defaults }, + model: { id: entry.model, reasoningEffort: entry.reasoningEffort } + })) +}); diff --git a/src/compiler/containerDaimonUidEntrypointLifecycle.test.ts b/src/compiler/containerDaimonUidEntrypointLifecycle.test.ts index c76373da..e6a1c5d0 100644 --- a/src/compiler/containerDaimonUidEntrypointLifecycle.test.ts +++ b/src/compiler/containerDaimonUidEntrypointLifecycle.test.ts @@ -30,6 +30,7 @@ const privateStateAncestors = (target: string): string[] => const daimonPlan: RuntimeTargetPlan = { engineByNodeId: { "agent:AGY": "agy", "agent:Codex One": "codex", "agent:Grok Two": "grok" }, + grokModelByNodeId: { "agent:Grok Two": { model: "grok-4.6", reasoningEffort: "low" } }, envFiles: [], id: "daimon-organization", instancePaths: { configPath: "/var/lib/spawnfile/instances/daimon/daimon-organization/daimon/config.json", diff --git a/src/compiler/containerDaimonUidEntrypointRender.test.ts b/src/compiler/containerDaimonUidEntrypointRender.test.ts index e72b3d53..dd91e2ab 100644 --- a/src/compiler/containerDaimonUidEntrypointRender.test.ts +++ b/src/compiler/containerDaimonUidEntrypointRender.test.ts @@ -26,6 +26,7 @@ const authorizedUid = 2000; const daimonPlan: RuntimeTargetPlan = { engineByNodeId: { "agent:AGY": "agy", "agent:Codex One": "codex", "agent:Grok Two": "grok" }, + grokModelByNodeId: { "agent:Grok Two": { model: "grok-4.6", reasoningEffort: "low" } }, envFiles: [], id: "daimon-organization", instancePaths: { configPath: "/var/lib/spawnfile/instances/daimon/daimon-organization/daimon/config.json", @@ -210,7 +211,7 @@ describe("renderDaimonUidEntrypoint", () => { expect(rendered).toContain("/var/lib/daimon-workers/"); // Grok refuses a profile that is a symlink or carries a hard-link alias, so it is an // unaliased file in the worker's own read-only .grok directory. - expect(rendered).toContain("ensureExactFile(profilePath, profileFor(), 0, 0, 0o444)"); + expect(rendered).toContain("ensureExactFile(entry.profilePath, entry.profile, 0o444)"); expect(rendered).not.toContain("ensureExactLink"); expect(rendered).toContain("sandbox-events.jsonl"); expect(rendered).toContain("restrict_network = true"); @@ -228,8 +229,14 @@ describe("renderDaimonUidEntrypoint", () => { expect(rendered).toContain("validateResourceLink(target, info); return;"); expect(rendered).toContain("worker runtime file identity mismatch"); expect(rendered).toContain("worker runtime file identity mismatch"); - expect(rendered).toContain("ensureEventsFile(eventsPath, entry.uid)"); - expect(rendered).toContain("noopolis.daimon.engine-broker-service.v1"); + expect(rendered).toContain("ensureEventsFile(entry.eventsPath, entry.uid)"); + expect(rendered).toContain("noopolis.daimon.engine-broker-service.v2"); + expect(rendered).not.toContain("noopolis.daimon.engine-broker-service.v1"); + expect(rendered).toContain("/var/lib/daimon-workers/2200/.grok/sessions/sandbox-events.jsonl"); + expect(rendered).not.toContain("[auth_provider.daimon]"); + const preflight = rendered.indexOf("kernel.apparmor_restrict_unprivileged_userns=0 on the Docker host"); + expect(preflight).toBeGreaterThan(-1); + expect(preflight).toBeLessThan(rendered.indexOf("node <<'SPAWNFILE_DAIMON_BROKER_PROVISION'")); expect(rendered).toContain("/etc/daimon-engine-broker/service.json"); expect(rendered).toContain("readSecure(bootstrap, undefined, 'bootstrap')"); expect(rendered).toContain("noopolis.daimon.broker-credential-journal.v1"); @@ -238,9 +245,13 @@ describe("renderDaimonUidEntrypoint", () => { expect(rendered).toContain("generation: journal.generation + 1"); expect(rendered).toContain("state: 'promoted'"); expect(rendered).toContain("bootstrapBytes.fill(0)"); - expect(rendered).toContain("ensureExactFile(profilePath, profileFor(), 0, 0, 0o444)"); + expect(rendered).toContain("ensureExactFile(entry.profilePath, entry.profile, 0o444)"); expect(rendered).toContain("--bounding-set=-all,+chown,+setuid,+setgid -- '/opt/daimon/bin/daimon-engine-broker' &"); - expect(rendered).toContain("--bounding-set=-all,+chown,+setuid,+setgid,+setpcap -- '/opt/daimon/bin/daimon-engine-broker' --relay &"); + expect(rendered).toContain("--bounding-set=-all,+chown,+setuid,+setgid,+setpcap -- env TMPDIR='/run/daimon-engine-broker/tmp' '/opt/daimon/bin/daimon-engine-broker' --relay &"); + // Shared /tmp is root:2000 1774 in a Grok organization: every non-root process outside group 2000 needs its own TMPDIR. + expect(rendered).toContain("--bounding-set=-all -- env TMPDIR='/run/daimon-engine-broker/tmp' '/opt/daimon/bin/daimon-runtime' engine-broker serve &"); + expect(rendered).toContain("install -d -o 2100 -g 2100 -m 0700 /run/daimon-engine-broker/tmp"); + expect(rendered.match(/--reuid 2100 --regid 2100 [^\n]*daimon-runtime' engine-broker serve/gu)?.every((line) => line.includes("env TMPDIR="))).toBe(true); expect(rendered).toContain('"$relay_pid:2100:0000000000000000"'); expect(rendered).toContain('expected_caps=${rest##*:}'); expect(rendered).toContain("--reuid 2100 --regid 2100"); @@ -394,7 +405,8 @@ describe("renderDaimonUidEntrypoint", () => { ...daimonPlan.instancePaths, instanceRoot: undefined, workspacePath: "relative-workspace" - } + }, + engineByNodeId: { "agent:Codex One": "codex" } }], ["relative-state", "/persisted-state", "/persisted-state"]); expect(rendered).toContain("state_roots=('/persisted-state')"); diff --git a/src/compiler/containerDaimonUidEntrypointRender.ts b/src/compiler/containerDaimonUidEntrypointRender.ts index 0a05747a..665d7da7 100644 --- a/src/compiler/containerDaimonUidEntrypointRender.ts +++ b/src/compiler/containerDaimonUidEntrypointRender.ts @@ -17,12 +17,13 @@ import { resolveDaimonVolumeIdentityFiles } from "./containerDaimonOwnershipGuardRender.js"; export { resolveDaimonVolumeIdentityFiles } from "./containerDaimonOwnershipGuardRender.js"; +import { renderDaimonGrokHostPreflight } from "./containerDaimonGrokWorkerProvisioning.js"; import { DAIMON_BROKER_EXECUTABLE, DAIMON_BROKER_BACKEND_SOCKET, DAIMON_BROKER_LAUNCHER_SOCKET, DAIMON_BROKER_REALM, - DAIMON_BROKER_SOCKET, + DAIMON_BROKER_SOCKET, DAIMON_BROKER_TMPDIR, DAIMON_BROKER_UID, DAIMON_ORGANIZATION_UID, renderDaimonBrokerProvisioning, @@ -168,9 +169,9 @@ const privateModeDirectories = (runtimePlans: RuntimeTargetPlan[], moltnet?: Ent // creates that parent root-owned and world-readable when it materializes // this mount, and the ancestor pass below only *chowns* it, so it stayed // 0755 and was the one path under `/var/lib/spawnfile` a Grok worker uid - // could open. Securing it to 0700 2000:2000 is what replaces the sandbox - // `deny` entry that Grok 1.0.13 can no longer honour (see - // `GROK_SANDBOX_DENY_PATHS`). Nothing loses access: the only thing + // could open. Securing it to 0700 2000:2000 backs the acceptance store's + // sandbox `deny` entry with unix modes as defense in depth (see + // `containerDaimonGrokWorkerRender.ts`). Nothing loses access: the only thing // beneath it is this store, already 0700 2000:2000, so every reader that // works today is the organization uid or a `docker exec` root holding // CAP_DAC_READ_SEARCH. @@ -331,6 +332,7 @@ export const renderDaimonUidEntrypoint = ( ' if ! getent group "$fixed_uid" >/dev/null; then groupadd -K GID_MIN=1 --gid "$fixed_uid" "daimon-$fixed_uid"; fi', ' if ! getent passwd "$fixed_uid" >/dev/null; then useradd -K UID_MIN=1 --no-create-home --no-log-init --uid "$fixed_uid" --gid "$fixed_uid" --home-dir /nonexistent --shell /usr/sbin/nologin "daimon-$fixed_uid"; fi', "done", + ...(resolveDaimonGrokRegistrations(runtimePlans).length === 0 ? [] : renderDaimonGrokHostPreflight()), ...renderDaimonBrokerProvisioning(runtimePlans), 'if ! getent passwd "$uid" >/dev/null; then', ' runtime_identity="daimon-$uid"', @@ -362,12 +364,12 @@ export const renderDaimonUidEntrypoint = ( "startup_children+=(\"$launcher_pid\")", `wait_for_broker_socket ${quote(DAIMON_BROKER_LAUNCHER_SOCKET)} "$launcher_pid" "engine broker launcher"`, `wait_for_broker_identity "$launcher_pid" 0 00000000000000c1 "engine broker launcher"`, - `setpriv --clear-groups --reuid ${DAIMON_BROKER_UID} --regid ${DAIMON_BROKER_UID} --inh-caps=-all --ambient-caps=-all --bounding-set=-all -- ${quote(path.posix.join(daimonPlan?.runtimeRoot ?? "", "bin/daimon-runtime"))} engine-broker serve &`, + `setpriv --clear-groups --reuid ${DAIMON_BROKER_UID} --regid ${DAIMON_BROKER_UID} --inh-caps=-all --ambient-caps=-all --bounding-set=-all -- env TMPDIR=${quote(DAIMON_BROKER_TMPDIR)} ${quote(path.posix.join(daimonPlan?.runtimeRoot ?? "", "bin/daimon-runtime"))} engine-broker serve &`, "broker_pid=$!", "startup_children+=(\"$broker_pid\")", `wait_for_broker_socket ${quote(DAIMON_BROKER_BACKEND_SOCKET)} "$broker_pid" "engine broker backend"`, `wait_for_broker_identity "$broker_pid" ${DAIMON_BROKER_UID} 0000000000000000 "engine broker backend"`, - `setpriv --inh-caps=-all --ambient-caps=-all --bounding-set=-all,+chown,+setuid,+setgid,+setpcap -- ${quote(DAIMON_BROKER_EXECUTABLE)} --relay &`, + `setpriv --inh-caps=-all --ambient-caps=-all --bounding-set=-all,+chown,+setuid,+setgid,+setpcap -- env TMPDIR=${quote(DAIMON_BROKER_TMPDIR)} ${quote(DAIMON_BROKER_EXECUTABLE)} --relay &`, "relay_pid=$!", "startup_children+=(\"$relay_pid\")", `wait_for_broker_socket ${quote(DAIMON_BROKER_SOCKET)} "$relay_pid" "engine broker control relay"`, diff --git a/src/compiler/modelEnv.test.ts b/src/compiler/modelEnv.test.ts index 0f4593be..7f184519 100644 --- a/src/compiler/modelEnv.test.ts +++ b/src/compiler/modelEnv.test.ts @@ -6,6 +6,7 @@ import { listEffectiveExecutionModelTargets, listExecutionModelProviders, listExecutionModelSecretNames, + modelAuthMethodNeedsCliCredential, resolveEffectiveModelTarget, resolveExecutionModelAuthMethods, resolveModelProviderEnvName @@ -203,4 +204,13 @@ describe("modelEnv", () => { it("formats provider env names for unknown providers", () => { expect(resolveModelProviderEnvName("my-proxy")).toBe("MY_PROXY_API_KEY"); }); + + it("never turns Daimon-owned grok auth into a host CLI credential or api key secret", () => { + const execution = { model: { primary: { auth: { method: "grok" as const }, name: "grok-4.6", provider: "xai", reasoning_effort: "low" as const } } }; + expect(listExecutionModelSecretNames(execution)).toEqual([]); + expect(modelAuthMethodNeedsCliCredential("grok")).toBe(false); + expect(modelAuthMethodNeedsCliCredential("codex")).toBe(true); + expect(modelAuthMethodNeedsCliCredential("claude-code")).toBe(true); + expect(resolveEffectiveModelTarget(execution.model.primary, execution)).toEqual({ auth: { method: "grok" }, name: "grok-4.6", provider: "xai", reasoningEffort: "low" }); + }); }); diff --git a/src/compiler/modelEnv.ts b/src/compiler/modelEnv.ts index 1b739239..8c9141b6 100644 --- a/src/compiler/modelEnv.ts +++ b/src/compiler/modelEnv.ts @@ -15,8 +15,13 @@ const MODEL_PROVIDER_ENV_VARS = new Map([ export const CLI_CREDENTIAL_SECRET_NAME = "SPAWNFILE_CLI_AUTH_JSON"; +/** + * Only the host-imported CLI logins travel as `SPAWNFILE_CLI_AUTH_JSON`. `grok` + * is a Daimon-owned subscription: its credential is the broker's bootstrap + * slot (`runtime/daimon/runAuth.ts`), never this secret. + */ export const modelAuthMethodNeedsCliCredential = (method: ModelAuthMethod): boolean => - method !== "api_key" && method !== "none"; + method === "claude-code" || method === "codex"; const resolveLegacyModelAuthMethod = ( execution: ExecutionBlock | undefined, @@ -122,7 +127,8 @@ export const resolveEffectiveModelTarget = ( }, ...(target.endpoint ? { endpoint: target.endpoint } : {}), name: target.name, - provider: target.provider + provider: target.provider, + ...(target.reasoning_effort ? { reasoningEffort: target.reasoning_effort } : {}) }; }; diff --git a/src/compiler/runProjectAuth.ts b/src/compiler/runProjectAuth.ts index 6d4cba21..573522cd 100644 --- a/src/compiler/runProjectAuth.ts +++ b/src/compiler/runProjectAuth.ts @@ -27,6 +27,7 @@ const MODEL_AUTH_IMPORT_KINDS: Record = { api_key: null, "claude-code": "claude-code", codex: "codex", + grok: null, none: null }; diff --git a/src/compiler/runProjectDaimonCodexDocker.ts b/src/compiler/runProjectDaimonCodexDocker.ts index 9738e84b..b3a11ff3 100644 --- a/src/compiler/runProjectDaimonCodexDocker.ts +++ b/src/compiler/runProjectDaimonCodexDocker.ts @@ -3,7 +3,8 @@ import path from "node:path"; import { readUtf8File } from "../filesystem/index.js"; import type { ContainerRuntimeInstanceReport } from "../report/index.js"; import { - codexNativeSandboxDockerSecurityArgsForConfigs, + daimonEngineDockerSecurityArgsForConfigs, + materializeDaimonGrokSeccompProfile, SpawnfileError } from "../shared/index.js"; @@ -55,5 +56,8 @@ export const resolveDaimonCodexNativeSandboxDockerSecurityOptions = async ( ); } } - return codexNativeSandboxDockerSecurityArgsForConfigs(sources); + return daimonEngineDockerSecurityArgsForConfigs( + sources, + () => materializeDaimonGrokSeccompProfile(path.join(compileResult.outputDirectory, "container", "security")) + ); }; diff --git a/src/compiler/syncProjectAuth.ts b/src/compiler/syncProjectAuth.ts index 31a6ea8e..928d4533 100644 --- a/src/compiler/syncProjectAuth.ts +++ b/src/compiler/syncProjectAuth.ts @@ -7,7 +7,7 @@ import { setAuthProfileEnv } from "../auth/index.js"; import { readUtf8File } from "../filesystem/index.js"; -import { SpawnfileError } from "../shared/index.js"; +import { type ModelAuthMethod, SpawnfileError } from "../shared/index.js"; import { listAgentSurfaceSecretNames } from "./agentSurfaces.js"; import { buildCompilePlan } from "./buildCompilePlan.js"; @@ -28,12 +28,12 @@ export interface SyncProjectAuthOptions { const resolveAuthRequirements = async ( inputPath: string ): Promise<{ - methods: Set<"api_key" | "claude-code" | "codex" | "none">; + methods: Set; optionalEnvNames: Set; requiredEnvNames: Set; }> => { const plan = await buildCompilePlan(inputPath); - const methods = new Set<"api_key" | "claude-code" | "codex" | "none">(); + const methods = new Set(); const optionalEnvNames = new Set(); const requiredEnvNames = new Set(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 26dcbcbe..f1ddb32d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -236,6 +236,7 @@ export interface EffectiveModelTarget { endpoint?: ModelEndpoint; name: string; provider: string; + reasoningEffort?: "high" | "low" | "medium"; } export interface ResolvedSubagentRef { diff --git a/src/compiler/updateProjectModels.ts b/src/compiler/updateProjectModels.ts index 3dac9000..667c4147 100644 --- a/src/compiler/updateProjectModels.ts +++ b/src/compiler/updateProjectModels.ts @@ -50,6 +50,7 @@ const AUTH_METHOD_PROVIDER_HINTS: Record = { "api_key": "use a model provider like openai or anthropic, then pass --auth api_key", "claude-code": "use provider anthropic, then pass --auth claude-code", codex: "use provider openai, then pass --auth codex", + grok: "use provider xai with a Daimon grok engine agent, then pass --auth grok and declare reasoning_effort", none: "use a model provider like local, then pass --auth none" }; diff --git a/src/distribution/consumeImage.ts b/src/distribution/consumeImage.ts index dc66155d..bd94988a 100644 --- a/src/distribution/consumeImage.ts +++ b/src/distribution/consumeImage.ts @@ -238,7 +238,7 @@ const consumeImageUpLocked = async ( }) ).mountArgs; const daimonDockerSecurityArgs = await resolveDaimonDockerSecurityArgsForImage( - imageRef, report, runDocker + imageRef, report, runDocker, workDir ); volumeReservation = await acquireExclusiveVolumeReservations( diff --git a/src/distribution/consumeImageDaimonDocker.ts b/src/distribution/consumeImageDaimonDocker.ts index 698ddcd9..eece4681 100644 --- a/src/distribution/consumeImageDaimonDocker.ts +++ b/src/distribution/consumeImageDaimonDocker.ts @@ -2,8 +2,9 @@ import { randomUUID } from "node:crypto"; import { SpawnfileError } from "../shared/errors.js"; import { - codexNativeSandboxDockerSecurityArgsForConfigs, - DAIMON_DOCKER_RUNTIME_SECURITY_ARGS + DAIMON_DOCKER_RUNTIME_SECURITY_ARGS, + daimonEngineDockerSecurityArgsForConfigs, + materializeDaimonGrokSeccompProfile } from "../shared/daimonCodexDocker.js"; import type { DockerCommandRunner } from "./dockerRunner.js"; @@ -56,13 +57,14 @@ const readDaimonConfigSourcesFromImage = async ( export const resolveDaimonDockerSecurityArgsForImage = async ( imageRef: string, report: DistributionReport, - runDocker: DockerCommandRunner + runDocker: DockerCommandRunner, + securityDirectory: string ): Promise => { const daimonInstances = daimonInstancesFrom(report); if (daimonInstances.length === 0) return []; const sources = await readDaimonConfigSourcesFromImage(imageRef, daimonInstances, runDocker); return [ ...DAIMON_DOCKER_RUNTIME_SECURITY_ARGS, - ...codexNativeSandboxDockerSecurityArgsForConfigs(sources) + ...(await daimonEngineDockerSecurityArgsForConfigs(sources, () => materializeDaimonGrokSeccompProfile(securityDirectory))) ]; }; diff --git a/src/distribution/preflight.test.ts b/src/distribution/preflight.test.ts index 71b76c66..7ea4e53f 100644 --- a/src/distribution/preflight.test.ts +++ b/src/distribution/preflight.test.ts @@ -118,4 +118,14 @@ describe("runImagePreflight", () => { }) ).not.toThrow(); }); + + it("treats Daimon-owned grok auth as satisfied by the runtime's own credential slot", () => { + const base = importReport(); + const report = { + ...base, + model_auth_methods: { xai: "grok" as const }, + runtime_instances: base.runtime_instances.map((instance) => ({ ...instance, model_auth_methods: { xai: "grok" as const }, runtime: "daimon" })) + }; + expect(() => runImagePreflight({ authValues: { DIST_REQUIRED_TOKEN: "y" }, report })).not.toThrow(); + }); }); diff --git a/src/distribution/preflight.ts b/src/distribution/preflight.ts index 4ea38746..6c7e5b1b 100644 --- a/src/distribution/preflight.ts +++ b/src/distribution/preflight.ts @@ -6,6 +6,10 @@ import type { } from "./types.js"; const ENV_BASED_AUTH_METHODS = new Set(["api_key", "none"]); +// Satisfied by the runtime's own credential slot (the Daimon Grok broker's +// bootstrap realm, checked by `imageRuntimeAuth.ts`), never by an env value +// or a host CLI import. +const RUNTIME_OWNED_AUTH_METHODS = new Set(["grok"]); // Import-based auth methods are satisfiable sourceless when the consumer has the // matching local credential import (their logged-in Claude Code / Codex session). @@ -32,7 +36,7 @@ const collectUnsupportedAuth = ( const unsupported: Array<{ instance: string; method: string; provider: string; runtime: string }> = []; for (const instance of report.runtime_instances) { for (const [provider, method] of Object.entries(instance.model_auth_methods)) { - if (ENV_BASED_AUTH_METHODS.has(method)) { + if (ENV_BASED_AUTH_METHODS.has(method) || RUNTIME_OWNED_AUTH_METHODS.has(method)) { continue; } const importKind = IMPORT_AUTH_METHOD_KINDS[method]; diff --git a/src/manifest/executionSchemas.test.ts b/src/manifest/executionSchemas.test.ts new file mode 100644 index 00000000..86b01cd6 --- /dev/null +++ b/src/manifest/executionSchemas.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; + +import { executionSchema } from "./executionSchemas.js"; + +const grokPrimary = { auth: { method: "grok" }, name: "grok-4.6", provider: "xai", reasoning_effort: "low" }; + +describe("brokered Grok model declarations", () => { + it("accepts xai grok auth with a closed reasoning effort", () => { + for (const reasoning_effort of ["low", "medium", "high"]) { + expect(executionSchema.safeParse({ model: { primary: { ...grokPrimary, reasoning_effort } } }).success).toBe(true); + } + const { reasoning_effort: _effort, ...withoutEffort } = grokPrimary; + // Presence is enforced per engine by the Daimon adapter; the schema only fences where it may appear. + expect(executionSchema.safeParse({ model: { primary: withoutEffort } }).success).toBe(true); + }); + + it("refuses grok auth off xai, an endpoint, an unknown effort, or an effort on any other auth", () => { + const invalid = [ + { ...grokPrimary, provider: "openai" }, + { ...grokPrimary, provider: "custom", endpoint: { base_url: "http://127.0.0.1/v1", compatibility: "openai" } }, + { ...grokPrimary, reasoning_effort: "xhigh" }, + { ...grokPrimary, reasoning_effort: "minimal" }, + { auth: { method: "codex" }, name: "gpt-5.4", provider: "openai", reasoning_effort: "low" }, + { auth: { method: "api_key" }, name: "grok-4.6", provider: "xai", reasoning_effort: "low" }, + { name: "grok-4.6", provider: "xai", reasoning_effort: "low" } + ]; + for (const primary of invalid) { + expect(executionSchema.safeParse({ model: { primary } }).success, JSON.stringify(primary)).toBe(false); + } + // Inherited (model-level) grok auth never carries an effort. + const { auth: _auth, ...inherited } = grokPrimary; + expect(executionSchema.safeParse({ model: { auth: { method: "grok" }, primary: inherited } }).success).toBe(false); + }); +}); diff --git a/src/manifest/executionSchemas.ts b/src/manifest/executionSchemas.ts index bd52129e..3474f38d 100644 --- a/src/manifest/executionSchemas.ts +++ b/src/manifest/executionSchemas.ts @@ -1,6 +1,12 @@ import { z } from "zod"; -const modelAuthMethodSchema = z.enum(["api_key", "claude-code", "codex", "none"]); +const modelAuthMethodSchema = z.enum(["api_key", "claude-code", "codex", "grok", "none"]); +/** + * The closed reasoning-effort vocabulary a brokered Daimon Grok worker accepts + * (`DAIMON_GROK_BROKER_REASONING_EFFORTS`). Only valid on a target that + * declares `auth.method: grok` itself; it is never inherited. + */ +export const MODEL_REASONING_EFFORTS = ["low", "medium", "high"] as const; const modelEndpointCompatibilitySchema = z.enum(["anthropic", "openai"]); const modelAuthSchema = z @@ -66,10 +72,25 @@ export const modelTargetSchema = z auth: modelEntryAuthSchema.optional(), endpoint: modelEndpointSchema.optional(), name: z.string(), - provider: z.string() + provider: z.string(), + reasoning_effort: z.enum(MODEL_REASONING_EFFORTS).optional() }) .strict() .superRefine((value, context) => { + if (value.auth?.method === "grok" && (value.provider !== "xai" || value.endpoint)) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: "grok auth is only valid for provider xai without an endpoint" + }); + } + + if (value.reasoning_effort !== undefined && value.auth?.method !== "grok") { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: "reasoning_effort is only valid on a model that declares auth.method grok" + }); + } + const usesCustomEndpoint = value.provider === "custom" || value.provider === "local"; if (usesCustomEndpoint && !value.endpoint) { diff --git a/src/runtime/AGENTS.md b/src/runtime/AGENTS.md index 55a5cb45..c8db663b 100644 --- a/src/runtime/AGENTS.md +++ b/src/runtime/AGENTS.md @@ -17,6 +17,7 @@ src/runtime/ ├── registry.ts # Bundled adapter registration and lookup ├── usageLedger.ts # Pure parser/aggregator for Daimon's per-turn usage ledger ├── usageLedgerRead.ts # Ledger read transport: `cat`s both generations through a caller-supplied exec and separates "absent" from "unreadable" +├── usageRequestLedger.ts # Pure parser for Daimon's per-request stream (`requests.jsonl`): timing, model, usage source, broker-turn dedupe ├── scheduleUtils.ts # Shared duration schedule helpers for runtime lowering ├── daimon/ # Public Daimon organization-host adapter ├── openclaw/ # OpenClaw adapter implementation @@ -36,7 +37,7 @@ calls it to decide whether to emit an agent `memory` block at all. Keep those tw sides on this one function: a config that points an in-process Mneme runtime at a path the container does not mount fails at its first write instead of degrading. -`common.ts` owns where declared `workspace.skills` are emitted. `createSkillFiles` accepts either one root or a list of roots, and the roots are named constants there: `WORKSPACE_SKILL_BASE_DIRECTORY` (`workspace/skills`) for OpenClaw and PicoClaw, which read that directory with their own skill loaders, and `CLI_ENGINE_SKILL_BASE_DIRECTORIES` (`workspace/.agents/skills` and `workspace/.codex/skills`) for Daimon and Pi, whose skills are discovered by an external coding-agent CLI. Both CLI-engine roots are required and their files are byte-identical on purpose: `.codex/skills` is Codex's own discovery root and `.agents/skills` is the generic root grok, agy, and other file-reading engines use. This mirrors the Moltnet skill install exactly — `resolveMoltnetWorkspaceLayout` in `src/compiler/moltnetClientConfig.ts` runs `moltnet skill install --runtime codex` for these runtimes and Moltnet writes both roots — and it is the reason declared skills now reach an engine at all: a plain `workspace/skills/` root is read by no engine Daimon or Pi can host, so everything emitted there was invisible. +`common.ts` owns where declared `workspace.skills` are emitted. `createSkillFiles` accepts either one root or a list of roots, and the roots are named constants there: `WORKSPACE_SKILL_BASE_DIRECTORY` (`workspace/skills`) for OpenClaw and PicoClaw, which read that directory with their own skill loaders, and `CLI_ENGINE_SKILL_BASE_DIRECTORIES` (`workspace/.agents/skills` and `workspace/.codex/skills`) for Daimon and Pi, whose skills are discovered by an external coding-agent CLI. Both CLI-engine roots are required and their files are byte-identical on purpose: `.codex/skills` is Codex's own discovery root and `.agents/skills` is the generic root grok, agy, and other file-reading engines use. This mirrors the Moltnet skill install exactly — `resolveMoltnetWorkspaceLayout` in `src/compiler/moltnetClientConfig.ts` runs `moltnet skill install --runtime codex` for these runtimes and Moltnet writes both roots — and it is the reason declared skills now reach an engine at all: a plain `workspace/skills/` root is read by no engine Daimon or Pi can host, so everything emitted there was invisible. The one exception is a brokered Daimon Grok agent (`daimonSkillBaseDirectories` in `daimon/adapter.ts`): Grok 1.0.34 discovers `.agents/skills` (never `.codex/skills`) only in a trusted workspace, and Daimon keeps the worker workspace untrusted and overrides the system prompt, so no root is emitted and a compile warning names the declared skills. `common.ts` also owns the `NOOPOLIS_RUN_ID` container env constant (`NOOPOLIS_RUN_ID_ENV` / `resolveNoopolisRunId`); `container.ts` reads it via `createRuntimeContainerEnv` and stamps it into every generated `RuntimeInstallRecipe.env` so every authority container agrees on one run id for causal event envelopes (see `specs/CAUSAL.md`). Never read `run_id` or `principal_id` from model output here. @@ -63,3 +64,14 @@ or receipt environment overrides fail closed. With no identity path, - Adapters receive resolved nodes, not raw manifests. - Keep runtime-specific behavior isolated here. - Share only the adapter contract, not runtime-specific implementation details. + +Daimon's Grok broker is the single sealed usage writer, and a replayed turn may +re-append its sealed rows. `usageLedger.ts` therefore dedupes every +`turn-usage.v1` row by its `turn` key (`dedupeUsageRecordsByTurn`) in parsing, +across both ledger generations (`usageLedgerRead.ts`), and inside every +aggregate; rows without a key are kept. It reads the broker's additive fields +only from Daimon's closed vocabularies — `limit_reason`, `model`, `outcome`, +`estimated_requests` — dropping a malformed optional field rather than the +row's spend. Rows with `estimated_requests` carry a conservative charge for +requests whose provider response had no valid usage; `spawnfile usage` marks +them `~` and says so, never presenting them as measured. diff --git a/src/runtime/container.ts b/src/runtime/container.ts index 1be8b529..3f2b5784 100644 --- a/src/runtime/container.ts +++ b/src/runtime/container.ts @@ -12,7 +12,7 @@ import { DAIMON_LOCAL_RUNTIME_IDENTITY_ENV, loadLocalDaimonRuntimeIdentity } from "./localDaimonAuthority.js"; -import { DAIMON_CONTRACT_MANIFEST_SHA256 } from "./daimon/contractManifest.js"; +import { DAIMON_CONTRACT_MANIFEST_SHA256, DAIMON_GROK_ENGINE_BROKER } from "./daimon/contractManifest.js"; import { DAIMON_WAKE_FUSE_DIRECTORY, DAIMON_WAKE_FUSE_DIRECTORY_ENV @@ -244,9 +244,9 @@ export const createRuntimeInstallRecipe = async ( `test -f ${installRoot}/contract-manifest.json && test -f ${installRoot}/contract-manifest.sha256 && manifest="$(cat ${installRoot}/contract-manifest.sha256)" && test "$manifest" = ${JSON.stringify(DAIMON_CONTRACT_MANIFEST_SHA256)} && test "$(sha256sum ${installRoot}/contract-manifest.json | awk '{print "sha256:" $1}')" = "$manifest" && node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if(r.manifest_sha256!==process.argv[2])process.exit(1)' ${installRoot}/${DAIMON_CAPABILITY_RECEIPT_FILE} "$manifest"`, `ln -sf ${installRoot}/bin/daimon-runtime /usr/local/bin/daimon-runtime`, `ln -sf ${installRoot}/bin/codex /usr/local/bin/codex`, - `install -o root -g root -m 0555 ${installRoot}/bin/grok /usr/local/bin/grok`, + `install -o root -g root -m 0555 ${installRoot}/bin/grok /usr/local/bin/grok && arch="$(dpkg --print-architecture)" && case "$arch" in amd64) expected=${DAIMON_GROK_ENGINE_BROKER.grokCliArtifacts.x64.sha256} ;; arm64) expected=${DAIMON_GROK_ENGINE_BROKER.grokCliArtifacts.arm64.sha256} ;; *) exit 1 ;; esac && test "$(sha256sum /usr/local/bin/grok | awk '{print $1}')" = "$expected"`, `ln -sf ${installRoot}/bin/agy /usr/local/bin/agy`, - `mkdir -p /opt/daimon/bin && install -o root -g root -m 0555 ${installRoot}/bin/daimon-engine-broker /opt/daimon/bin/daimon-engine-broker && arch="$(dpkg --print-architecture)" && case "$arch" in amd64) expected=e3fe2738fc8a979861085b4003bf2d5d7c284874897cb6ec2e2e2383211768bd ;; arm64) expected=ad44e02c38e6a3207ac4a3d5fd98b6d2e55341ce42dfd2f07204bbe54a7a653d ;; *) exit 1 ;; esac && test "$(sha256sum /opt/daimon/bin/daimon-engine-broker | awk '{print $1}')" = "$expected"` + `mkdir -p /opt/daimon/bin && install -o root -g root -m 0555 ${installRoot}/bin/daimon-engine-broker /opt/daimon/bin/daimon-engine-broker && arch="$(dpkg --print-architecture)" && case "$arch" in amd64) expected=${DAIMON_GROK_ENGINE_BROKER.artifacts.x64Sha256} ;; arm64) expected=${DAIMON_GROK_ENGINE_BROKER.artifacts.arm64Sha256} ;; *) exit 1 ;; esac && test "$(sha256sum /opt/daimon/bin/daimon-engine-broker | awk '{print $1}')" = "$expected"` ], copyCommands: [createRuntimeImageCopyCommand(daimonRuntime.image, installRoot)], env: { diff --git a/src/runtime/daimon/AGENTS.md b/src/runtime/daimon/AGENTS.md index 52f67263..519d4464 100644 --- a/src/runtime/daimon/AGENTS.md +++ b/src/runtime/daimon/AGENTS.md @@ -54,11 +54,31 @@ excluded until Daimon learned to register its per-wake MCP endpoint through `agy mcp add`; the compiler-side MCP validations (explicit tools allowlist, absolute stdio command) are engine-independent and still apply. +`contract-manifest.json`/`.sha256` and `grokWorkerConfigBytes.ts` are vendored +from a Daimon checkout by `scripts/vendor-daimon-grok-contract.ts` and never +edited by hand. `grokWorkerContract.ts` serves Daimon's worker `config.toml` +bytes only when they hash to the manifest's per model x effort pin, and mirrors +Daimon's sandbox-profile renderer byte for byte (pinned by Daimon-rendered +samples). The manifest also pins Grok CLI 1.0.34 (URL + sha256 per +architecture), which the runtime image, the local builder, and generated +organization images all verify. + The consumed manifest also pins the native Grok broker source/x64/arm64 digests, fixed root/org/broker/worker identities, root-only registrations, and loopback-only provider/MCP endpoints. Container provisioning must match that authority exactly and must not publish either broker port. +Every Grok agent is brokered, so `grokModel.ts` requires its model in full: +`execution.model.primary` with `provider: xai`, a name from the manifest's +closed list (`grok-4.6`, `grok-4.5`, `grok-build`), a target-level +`auth.method: grok`, and `reasoning_effort` (`low`, `medium`, `high`), with no +fallback and no model-level auth. The pair lowers to `engine.model` and +`engine.reasoningEffort` and selects the manifest-pinned worker `config.toml`. +Nothing is defaulted: Grok 1.0.34 silently drops an undeclared effort and its +catalog default for `grok-4.6` is `high`. `grok` auth is Daimon-owned — it +never becomes `SPAWNFILE_CLI_AUTH_JSON`, an api-key secret, or a host import. +AGY still refuses any `execution.model`; Codex refuses Grok auth. + Codex keeps an isolated per-agent credential home. Grok keeps isolated per-agent non-auth state but one durable rotating subscription credential realm; never fan out Grok refresh authority across writable homes. diff --git a/src/runtime/daimon/adapter.test.ts b/src/runtime/daimon/adapter.test.ts index 8f0b101b..d3b73a7a 100644 --- a/src/runtime/daimon/adapter.test.ts +++ b/src/runtime/daimon/adapter.test.ts @@ -25,6 +25,9 @@ const createDaimonNode = (id: string, name = id, engine = "codex") => { source: `/tmp/agent/${id}/Spawnfile` }); if (engine === "codex") return node; + if (engine === "grok") { + return { ...node, execution: { ...node.execution!, model: { primary: { auth: { method: "grok" as const }, name: "grok-4.6", provider: "xai", reasoning_effort: "low" as const } } } }; + } const { model: _model, ...execution } = node.execution!; return { ...node, execution }; }; @@ -73,6 +76,22 @@ describe("daimonAdapter", () => { expect(compiled.files.some((file) => file.path.startsWith("workspace/skills/"))).toBe(false); }); + it("emits no workspace skill root for a brokered Grok agent, never .codex/skills, and reports the declaration", async () => { + const grok = await daimonAdapter.compileAgent(createDaimonNode("grok", "Grok", "grok")); + const skillPaths = grok.files.map((file) => file.path).filter((filePath) => filePath.includes("/skills/")); + expect(skillPaths).toEqual([]); + expect(grok.files.some((file) => file.path.startsWith("workspace/.codex/"))).toBe(false); + expect(grok.diagnostics).toEqual(expect.arrayContaining([ + expect.objectContaining({ level: "warn", message: expect.stringContaining("declares workspace skills (note)") }) + ])); + for (const engine of ["codex", "agy"]) { + const compiled = await daimonAdapter.compileAgent(createDaimonNode(engine, engine, engine)); + expect(compiled.files.map((file) => file.path).filter((filePath) => filePath.endsWith("/SKILL.md"))) + .toEqual(["workspace/.agents/skills/note/SKILL.md", "workspace/.codex/skills/note/SKILL.md"]); + expect(compiled.diagnostics.some((diagnostic) => diagnostic.message.includes("declares workspace skills"))).toBe(false); + } + }); + it("emits one strict organization host and no generated engine application", async () => { const plan = await createPlan(); const config = plan.targetFiles.find((file) => file.path === DAIMON_CONFIG_FILE); @@ -192,7 +211,7 @@ describe("daimonAdapter", () => { expect(JSON.parse(target.files.find((file) => file.path === DAIMON_CONFIG_FILE)!.content).agents) .toEqual(expect.arrayContaining([ expect.objectContaining({ engine: { kind: "codex", model: "gpt-5.4-mini" } }), - expect.objectContaining({ engine: { kind: "grok" } }), + expect.objectContaining({ engine: { kind: "grok", model: "grok-4.6", reasoningEffort: "low" } }), expect.objectContaining({ engine: { kind: "agy" } }) ])); expect(target.opaqueMountTargets).toEqual([ @@ -383,11 +402,44 @@ describe("daimonAdapter", () => { ]) }); await expect(daimonAdapter.compileAgent(createPiTestNode({ runtime: { name: "daimon", options: { engine: "grok" } } + }))).rejects.toThrow("must use provider xai with auth.method grok"); + await expect(daimonAdapter.compileAgent(createPiTestNode({ + runtime: { name: "daimon", options: { engine: "agy" } } }))).rejects.toThrow("must omit Spawnfile execution.model"); expect(() => daimonAdapter.assertSupportedSurfaces?.({ moltnet: [{ network: "test" }] } as any)).not.toThrow(); expect(() => daimonAdapter.assertSupportedSurfaces?.({ discord: [{}] } as any)).toThrow("only lowers Moltnet"); }); + it("requires a brokered Grok agent to declare one listed xAI model and its reasoning effort, never inherited", async () => { + const grok = createDaimonNode("grok", "Grok", "grok"); + const withPrimary = (primary: Record, extra: Record = {}) => + ({ ...grok, execution: { ...grok.execution!, model: { primary, ...extra } } }) as typeof grok; + const declared = { auth: { method: "grok" }, name: "grok-4.6", provider: "xai", reasoning_effort: "low" }; + await expect(daimonAdapter.compileAgent(grok)).resolves.toBeDefined(); + for (const name of ["grok-4.5", "grok-build"]) { + for (const reasoning_effort of ["medium", "high"]) { + await expect(daimonAdapter.compileAgent(withPrimary({ ...declared, name, reasoning_effort }))).resolves.toBeDefined(); + } + } + await expect(daimonAdapter.compileAgent({ ...grok, execution: { sandbox: { mode: "workspace" } } } as typeof grok)) + .rejects.toThrow("must declare its brokered model and reasoning effort"); + await expect(daimonAdapter.compileAgent(withPrimary({ ...declared, name: "grok-4" }))).rejects.toThrow("unsupported model grok-4"); + await expect(daimonAdapter.compileAgent(withPrimary({ ...declared, name: "grok-code-fast-1" }))).rejects.toThrow("unsupported model"); + const { reasoning_effort: _effort, ...withoutEffort } = declared; + await expect(daimonAdapter.compileAgent(withPrimary(withoutEffort))).rejects.toThrow("must declare reasoning_effort"); + await expect(daimonAdapter.compileAgent(withPrimary({ ...declared, reasoning_effort: "xhigh" }))).rejects.toThrow("must declare reasoning_effort"); + await expect(daimonAdapter.compileAgent(withPrimary({ ...declared, provider: "openai" }))).rejects.toThrow("must use provider xai"); + await expect(daimonAdapter.compileAgent(withPrimary({ ...declared, auth: undefined }, { auth: { method: "grok" } }))) + .rejects.toThrow("exactly one model with target-level auth"); + await expect(daimonAdapter.compileAgent(withPrimary(declared, { fallback: [declared] }))).rejects.toThrow("exactly one model"); + const codex = createDaimonNode("codex", "Codex", "codex"); + await expect(daimonAdapter.compileAgent({ ...codex, execution: { ...codex.execution!, model: { primary: declared } } } as typeof codex)) + .rejects.toThrow("cannot declare Grok model auth"); + expect(() => daimonAdapter.assertSupportedModelTarget?.({ auth: { method: "grok" }, name: "grok-4.6", provider: "xai", reasoningEffort: "low" })).not.toThrow(); + expect(() => daimonAdapter.assertSupportedModelTarget?.({ auth: { method: "codex" }, name: "gpt", provider: "openai", reasoningEffort: "low" })).toThrow(/brokered xAI Grok/u); + expect(() => daimonAdapter.assertSupportedModelTarget?.({ auth: { method: "grok" }, name: "grok-4.6", provider: "openai" })).toThrow(/brokered xAI Grok/u); + }); + it("validates the complete public model, option, MCP, and empty-target boundaries", async () => { expect(() => daimonAdapter.assertSupportedModelTarget?.({ auth: { method: "codex" }, provider: "openai" diff --git a/src/runtime/daimon/adapter.ts b/src/runtime/daimon/adapter.ts index 344be639..8d7ebf96 100644 --- a/src/runtime/daimon/adapter.ts +++ b/src/runtime/daimon/adapter.ts @@ -21,6 +21,7 @@ import { DAIMON_ENGINES, resolveDaimonEngine } from "./config.js"; +import { resolveDaimonGrokModel } from "./grokModel.js"; import { prepareDaimonRuntimeAuth } from "./runAuth.js"; import { hasDaimonScheduleAuthority } from "./scheduleAuthority.js"; import { resolveDaimonAttention } from "./attention.js"; @@ -40,10 +41,11 @@ const assertDaimonSurfaces = (surfaces: ResolvedAgentSurfaces | undefined): void }; const assertDaimonModel = (target: EffectiveModelTarget): void => { - if (target.provider === "openai" && target.auth.method === "codex" && !target.endpoint) return; + if (target.provider === "openai" && target.auth.method === "codex" && !target.endpoint && !target.reasoningEffort) return; + if (target.provider === "xai" && target.auth.method === "grok" && !target.endpoint) return; throw new SpawnfileError( "validation_error", - "Daimon organization runtime v1 accepts only the optional OpenAI Codex subscription intent; Grok and AGY engine auth stays Daimon-owned" + "Daimon organization runtime v1 accepts only the optional OpenAI Codex subscription intent or a brokered xAI Grok declaration; AGY engine auth stays Daimon-owned" ); }; @@ -80,6 +82,26 @@ const daimonWorkspaceRestrictionWarning = (node: ResolvedAgentNode): string | un + "the container boundary as this agent's only isolation."; }; +/** + * Workspace skill roots per Daimon engine. + * + * Codex and AGY keep both CLI-engine roots. A brokered Grok worker loads no + * workspace skill at all: Grok 1.0.34 discovers `.agents/skills` (never + * `.codex/skills`) only in a trusted folder, and Daimon keeps the workspace + * untrusted (root-owned empty `trusted_folders.toml`) and replaces the system + * prompt, whose fixed text references no skill. Emitting either root would ship + * files nothing reads, so none are emitted and the declaration is reported. + */ +export const daimonSkillBaseDirectories = (node: ResolvedAgentNode): readonly string[] => + resolveDaimonEngine(node) === "grok" ? [] : CLI_ENGINE_SKILL_BASE_DIRECTORIES; + +const daimonGrokSkillWarning = (node: ResolvedAgentNode): string | undefined => { + if (resolveDaimonEngine(node) !== "grok" || node.skills.length === 0) return undefined; + return `Daimon Grok agent ${node.name} declares workspace skills (${node.skills.map((skill) => skill.name).sort().join(", ")}) ` + + "that its brokered worker never loads: Grok discovers project skills only in a trusted workspace, and Daimon keeps the " + + "workspace untrusted and supplies instructions through the prompt. No skill files are emitted; move the guidance into the agent's docs."; +}; + const daimonCodexPolicyError = (node: ResolvedAgentNode): string | undefined => { if (node.runtime.options.codex_policy === undefined) return undefined; if (node.runtime.options.codex_policy !== "workspace-no-network") { @@ -109,12 +131,17 @@ const unsupportedAgentFeatures = (node: ResolvedAgentNode): void => { if (!server.tools?.length) throw new SpawnfileError("validation_error", `Daimon MCP server ${server.name} requires an explicit tools allowlist`); if (server.transport === "stdio" && !server.command?.startsWith("/")) throw new SpawnfileError("validation_error", `Daimon stdio MCP server ${server.name} requires an absolute command`); } - if (resolveDaimonEngine(node) !== "codex" && node.execution?.model) { + const engine = resolveDaimonEngine(node); + if (engine === "agy" && node.execution?.model) { throw new SpawnfileError( "validation_error", - "Daimon Grok and AGY agents must omit Spawnfile execution.model; their subscription auth and model selection are Daimon-owned" + "Daimon AGY agents must omit Spawnfile execution.model; their subscription auth and model selection are Daimon-owned" ); } + if (engine === "codex" && node.execution?.model?.primary.auth?.method === "grok") { + throw new SpawnfileError("validation_error", `Daimon Codex agent ${node.name} cannot declare Grok model auth`); + } + if (engine === "grok") resolveDaimonGrokModel(node); }; const scheduleCapabilityFor = async ( @@ -182,6 +209,7 @@ export const daimonAdapter: RuntimeAdapter = { const memoryVectorWarning = daimonMemoryVectorRecallWarning(node); const workspaceRestrictionWarning = daimonWorkspaceRestrictionWarning(node); const codexPolicyError = daimonCodexPolicyError(node); + const grokSkillWarning = daimonGrokSkillWarning(node); return { capabilities: createAgentCapabilities(node, { mcpOutcome: "supported", @@ -198,11 +226,12 @@ export const daimonAdapter: RuntimeAdapter = { ...(memorySelectionWarning ? [createDiagnostic("warn", memorySelectionWarning)] : []), ...(memoryVectorWarning ? [createDiagnostic("warn", memoryVectorWarning)] : []), ...(workspaceRestrictionWarning ? [createDiagnostic("warn", workspaceRestrictionWarning)] : []), + ...(grokSkillWarning ? [createDiagnostic("warn", grokSkillWarning)] : []), ...(codexPolicyError ? [createDiagnostic("error", codexPolicyError)] : []) ], files: [ ...createDocumentFiles("workspace", node.docs), - ...createSkillFiles(CLI_ENGINE_SKILL_BASE_DIRECTORIES, node.skills) + ...createSkillFiles(daimonSkillBaseDirectories(node), node.skills) ] }; }, diff --git a/src/runtime/daimon/config.test.ts b/src/runtime/daimon/config.test.ts index 5817c7c0..d70719c8 100644 --- a/src/runtime/daimon/config.test.ts +++ b/src/runtime/daimon/config.test.ts @@ -162,6 +162,25 @@ describe("Daimon memory lowering", () => { expect(config.agents[0]!.engine).toEqual({ kind: "codex", model: "gpt-5.4-codex", codexSandbox: { mode: "workspace-write", networkAccess: false, webSearch: "disabled" } }); }); + it("lowers a brokered Grok agent's declared model and reasoning effort into Daimon's engine config", async () => { + const config = await emitConfig(createDaimonNode({ + execution: { model: { primary: { auth: { method: "grok" }, name: "grok-4.5", provider: "xai", reasoning_effort: "medium" } } }, + runtime: { name: "daimon", options: { engine: "grok" } } + })); + expect(config.agents[0]!.engine).toEqual({ kind: "grok", model: "grok-4.5", reasoningEffort: "medium" }); + }); + + it("never restates a Grok agent's runtime home mode, which its worker's group must traverse for spills", async () => { + const [target] = await createDaimonContainerTargets([{ emittedFiles: [], id: "agent:grok", kind: "agent", slug: "grok", value: createPiTestNode({ + execution: { model: { primary: { auth: { method: "grok" }, name: "grok-4.6", provider: "xai", reasoning_effort: "low" } } }, + runtime: { name: "daimon", options: { engine: "grok" } } + }) }, { emittedFiles: [], id: "agent:codex", kind: "agent", slug: "codex", value: createDaimonNode() }]); + const start = target!.files.find((file) => file.path === "runtime/daimon-start.sh")!.content as string; + expect(start).toContain('[ -d "/runtime-homes/grok" ] || install -d -m 700 "/runtime-homes/grok"'); + expect(start).not.toMatch(/^install -d -m 700 "\/runtime-homes\/grok"/mu); + expect(start).toContain('install -d -m 700 "/runtime-homes/codex"'); + }); + it("omits Daimon's engine model selector when no execution model is declared", async () => { const config = await emitConfig(createDaimonNode({ execution: undefined })); diff --git a/src/runtime/daimon/config.ts b/src/runtime/daimon/config.ts index 4f8a56ef..2ba1ea54 100644 --- a/src/runtime/daimon/config.ts +++ b/src/runtime/daimon/config.ts @@ -20,6 +20,7 @@ import { } from "./memory.js"; import { assertDaimonScheduleAuthority } from "./scheduleAuthority.js"; import { assertDaimonAttentionAuthority, resolveDaimonAttention } from "./attention.js"; +import { resolveDaimonGrokModel } from "./grokModel.js"; export const DAIMON_CODEX_WORKSPACE_NO_NETWORK_POLICY = { mode: "workspace-write", networkAccess: false, webSearch: "disabled" } as const; @@ -100,8 +101,12 @@ export const resolveDaimonEngine = (node: ResolvedAgentNode): DaimonEngine => { const resolveDaimonEngineConfig = ( node: ResolvedAgentNode -): { kind: DaimonEngine; model?: string; codexSandbox?: typeof DAIMON_CODEX_WORKSPACE_NO_NETWORK_POLICY } => { +): { kind: DaimonEngine; model?: string; reasoningEffort?: string; codexSandbox?: typeof DAIMON_CODEX_WORKSPACE_NO_NETWORK_POLICY } => { const kind = resolveDaimonEngine(node); + if (kind === "grok") { + const declared = resolveDaimonGrokModel(node); + return { kind, model: declared.model, reasoningEffort: declared.reasoningEffort }; + } const model = kind === "codex" ? node.execution?.model?.primary?.name : undefined; const codexSandbox = kind === "codex" && node.runtime.options.codex_policy === "workspace-no-network" ? DAIMON_CODEX_WORKSPACE_NO_NETWORK_POLICY @@ -135,10 +140,15 @@ const renderStartScript = (agents: Array<{ // group access. Forcing 0700 here would lock that worker out of its own workspace, // so create it only when absent and never restate the mode of an existing directory. `[ -d ${JSON.stringify(agent.workspacePath)} ] || install -d -m 700 ${JSON.stringify(agent.workspacePath)}`, - `install -d -m 700 ${[ - agent.runtimeHomePath, - ...(credential === undefined ? [] : [inbound]) - ].map((entry) => JSON.stringify(entry)).join(" ")}`, + // A Grok agent's runtime home is traversable by its worker's group (the + // broker provisioning grants `0710` so the worker can read its setgid + // `tool-output/` spills); restating 0700 here would revoke that. + agent.engine.kind === "grok" + ? `[ -d ${JSON.stringify(agent.runtimeHomePath)} ] || install -d -m 700 ${JSON.stringify(agent.runtimeHomePath)}` + : `install -d -m 700 ${[ + agent.runtimeHomePath, + ...(credential === undefined ? [] : [inbound]) + ].map((entry) => JSON.stringify(entry)).join(" ")}`, ...(credential === undefined ? [] : [ `if [ -e ${JSON.stringify(path.posix.join(agent.runtimeHomePath, credential.sourceRelativePath))} ]; then test "$(stat -c %a ${JSON.stringify(path.posix.join(agent.runtimeHomePath, credential.sourceRelativePath))})" = 600; fi` ]) @@ -204,6 +214,9 @@ export const createDaimonContainerTargets = async ( }) .sort((left, right) => left.id.localeCompare(right.id)); const engineByNodeId = Object.fromEntries(configAgents.map((agent) => [agent.id, agent.engine.kind])); + const grokModelByNodeId = Object.fromEntries(configAgents + .filter((agent) => agent.engine.kind === "grok" && agent.engine.model !== undefined && agent.engine.reasoningEffort !== undefined) + .map((agent) => [agent.id, { model: agent.engine.model!, reasoningEffort: agent.engine.reasoningEffort! }])); const hasAgy = configAgents.some((agent) => agent.engine.kind === "agy"); const hasGrok = configAgents.some((agent) => agent.engine.kind === "grok"); const agyRuntimeHomeMounts = configAgents @@ -248,6 +261,7 @@ export const createDaimonContainerTargets = async ( return [{ engineByNodeId, + ...(hasGrok ? { grokModelByNodeId } : {}), files: [ ...agents.flatMap((input) => input.emittedFiles.map((file) => moveWorkspaceFile(file, input.slug))), { content: serializedConfig, path: DAIMON_CONFIG_FILE }, diff --git a/src/runtime/daimon/contract-manifest.json b/src/runtime/daimon/contract-manifest.json index ead74419..0d92caf1 100644 --- a/src/runtime/daimon/contract-manifest.json +++ b/src/runtime/daimon/contract-manifest.json @@ -1 +1 @@ -{"activityResponseSchema":{"additionalProperties":false,"properties":{"items":{"items":{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"id":{"format":"uuid","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$","type":"string"},"kind":{"enum":["wake_started","wake_completed","wake_rejected","wake_aborted","agent_stopped"]},"occurredAt":{"format":"date-time","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$","type":"string"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["id","agentId","kind","occurredAt"],"type":"object"},"maxItems":100,"type":"array"},"nextCursor":{"maxLength":16,"minLength":1,"pattern":"^(0|[1-9][0-9]{0,15})$","type":"string"},"version":{"const":"noopolis.daimon.organization-runtime-activity.v1"}},"required":["version","items"],"type":"object"},"activityV2ResponseSchema":{"additionalProperties":false,"properties":{"executions":{"items":{"additionalProperties":false,"properties":{"agent_id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"delivery_ids":{"items":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"maxItems":32,"type":"array"},"execution_id":{"type":"string"},"state":{"const":"running"}},"required":["agent_id","execution_id","state","delivery_ids"],"type":"object"},"maxItems":32,"type":"array"},"items":{"items":{"additionalProperties":false,"properties":{"acceptance_id":{"type":"string"},"accepted_at":{"format":"date-time","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$","type":"string"},"active":{"type":"boolean"},"agent_id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"code":{"enum":["engine_failed","host_stopped","host_stopping","queue_full","unknown_agent"]},"deferred":{"type":"boolean"},"delivery_id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"execution_id":{"type":"string"},"queue_position":{"minimum":1,"type":"integer"},"request_digest":{"type":"string"},"state":{"enum":["accepted","running","completed","failed","stopped"]},"text":{"maxLength":16384,"type":"string"},"updated_at":{"format":"date-time","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$","type":"string"},"version":{"const":"noopolis.daimon.wake-receipt-status.v2"}},"required":["version","acceptance_id","agent_id","delivery_id","request_digest","state","accepted_at","updated_at","active"],"type":"object"},"maxItems":2112,"type":"array"},"version":{"const":"noopolis.daimon.organization-runtime-activity.v2"}},"required":["version","items"],"type":"object"},"agySubscriptionRealm":{"directoryMode":448,"durableMountPath":"/var/lib/spawnfile/daimon/agy-subscription-realm","fileMode":384,"maxUnlockBytes":4096,"unlockMountPath":"/var/lib/spawnfile/daimon/agy-unlock-secret","unlockSourceSlot":"agy-unlock-secret"},"attention":{"accounting":"execution-start-reservations","busyDispatch":"bounded-pending-message-batch","completion":"explicit-per-delivery","defaultMaxBatchBytes":12000,"defaultMaxBatchMessages":8,"enabledBy":"agents[].attention","idleDispatch":"immediate","unhandled":"deferred-until-new-input"},"consumedConfigFields":["version","host.bindHost","host.port","host.controlTokenEnv","agents[].id","agents[].name","agents[].instructions","agents[].workspacePath","agents[].runtimeHomePath","agents[].engine.kind","agents[].engine.model","agents[].engine.reasoningEffort","agents[].engine.codexSandbox","agents[].schedule.kind","agents[].schedule.interval_ms","agents[].schedule.cron","agents[].schedule.timezone","agents[].schedule.prompt","agents[].schedule.jitter_seconds","agents[].mcp","agents[].moltnet","agents[].memory","agents[].attention"],"deliverySemantics":{"activeDeliveryIdempotency":"unbounded-until-terminal","concurrentSameAgentTurns":false,"externalEffectsExactlyOnce":false,"recovery":"at-least-once-with-stable-wake-id","terminalReceiptHorizon":2048},"engineCredentialMaterial":{"codex":{"destinationRelativePath":".codex/auth.json","directoryMode":448,"fileMode":384,"sourceRelativePath":".daimon-inbound/codex-auth","sourceSlot":"codex-auth"}},"grokEngineBroker":{"artifacts":{"arm64Sha256":"ad44e02c38e6a3207ac4a3d5fd98b6d2e55341ce42dfd2f07204bbe54a7a653d","sourceSha256":"bdcab1e12dcc531ed8e56f890263ca23a9ee7bac468191dd598e143df4ff8c58","x64Sha256":"e3fe2738fc8a979861085b4003bf2d5d7c284874897cb6ec2e2e2383211768bd"},"backendSocketPath":"/run/daimon-engine-broker/backend.sock","bounds":{"capabilityBundleBytes":8196,"capabilityBytes":4096,"outputBytes":65536,"promptBytes":65536},"controlSocketPath":"/run/daimon-engine-broker/control.sock","credentialHomePath":"/var/lib/spawnfile/daimon/grok-subscription-realm","grokExecutablePath":"/usr/local/bin/grok","identities":{"brokerUid":2100,"firstWorkerUid":2200,"organizationUid":2000},"launcherSocketPath":"/run/daimon-engine-broker/launcher.sock","mcpFacade":{"host":"127.0.0.1","path":"/mcp","port":43124},"nativeAbiVersion":2,"nativeExecutablePath":"/opt/daimon/bin/daimon-engine-broker","providerProxy":{"host":"127.0.0.1","port":43123},"registrationPath":"/etc/daimon-engine-broker/registrations.bin","serviceConfigPath":"/etc/daimon-engine-broker/service.json","turnStorePath":"/var/lib/spawnfile/daimon/grok-subscription-realm/turns"},"grokSubscriptionRealm":{"agentCredentialRelativePath":".grok/auth.json","bootstrapMountPath":"/var/lib/spawnfile/daimon/grok-bootstrap-auth","bootstrapSourceSlot":"grok-auth","directoryMode":448,"durableMountPath":"/var/lib/spawnfile/daimon/grok-subscription-realm","fileMode":384,"maxCredentialBytes":65536},"healthResponseSchema":{"additionalProperties":false,"properties":{"agents":{"items":{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"state":{"enum":["starting","running","stopping","stopped","idle","failed"]}},"required":["agentId","state"],"type":"object"},"maxItems":32,"type":"array"},"state":{"enum":["starting","running","stopping","stopped"]},"version":{"const":"noopolis.daimon.organization-runtime-health.v1"}},"required":["version","state","agents"],"type":"object"},"organizationRuntimeConfigSchema":{"$id":"noopolis.daimon.organization-runtime.v1","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"agents":{"items":{"additionalProperties":false,"properties":{"attention":{"additionalProperties":false,"properties":{"maxBatchBytes":{"maximum":12000,"minimum":1024,"type":"integer"},"maxBatchMessages":{"maximum":32,"minimum":1,"type":"integer"},"maxExecutions":{"maximum":9007199254740991,"minimum":1,"type":"integer"},"maxTokens":{"maximum":9007199254740991,"minimum":1,"type":"integer"}},"type":"object"},"engine":{"additionalProperties":false,"properties":{"codexSandbox":{"additionalProperties":false,"properties":{"mode":{"const":"workspace-write"},"networkAccess":{"const":false},"webSearch":{"const":"disabled"}},"required":["mode","networkAccess","webSearch"],"type":"object"},"kind":{"enum":["codex","grok","agy"]},"model":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"reasoningEffort":{"enum":["none","minimal","low","medium","high","xhigh","max","ultra","persistent"]}},"required":["kind"],"type":"object"},"id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"instructions":{"maxLength":16384,"minLength":1,"pattern":"\\S","type":"string"},"mcp":{"items":{"additionalProperties":false,"properties":{"args":{"items":{"maxLength":4096,"type":"string"},"maxItems":32,"type":"array"},"authSecretEnv":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"},"command":{"pattern":"^/","type":"string"},"env":{"additionalProperties":{"type":"string"},"type":"object"},"name":{"maxLength":4096,"minLength":1,"type":"string"},"tools":{"items":{"maxLength":4096,"minLength":1,"type":"string"},"maxItems":32,"minItems":1,"type":"array","uniqueItems":true},"transport":{"enum":["stdio","sse","streamable_http"]},"url":{"type":"string"}},"required":["name","transport","args","env","tools"],"type":"object"},"maxItems":8,"type":"array"},"memory":{"additionalProperties":false,"properties":{"runtimeHomePath":{"maxLength":4096,"pattern":"^/","type":"string"},"source":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"tokenBudget":{"maximum":1000000,"minimum":1,"type":"integer"}},"required":["runtimeHomePath"],"type":"object"},"moltnet":{"additionalProperties":false,"properties":{"cliPath":{"pattern":"^/","type":"string"},"configPath":{"pattern":"^/","type":"string"},"networks":{"items":{"additionalProperties":false,"properties":{"dms":{"type":"boolean"},"id":{"minLength":1,"type":"string"},"rooms":{"items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true}},"required":["id","rooms","dms"],"type":"object"},"maxItems":16,"type":"array"}},"required":["cliPath","configPath","networks"],"type":"object"},"name":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"runtimeHomePath":{"maxLength":4096,"pattern":"^/","type":"string"},"workspacePath":{"maxLength":4096,"pattern":"^/","type":"string"}},"required":["id","name","instructions","workspacePath","runtimeHomePath","engine"],"type":"object"},"maxItems":32,"minItems":1,"type":"array"},"host":{"additionalProperties":false,"properties":{"bindHost":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"controlTokenEnv":{"maxLength":4096,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"},"port":{"maximum":65535,"minimum":1,"type":"integer"}},"required":["bindHost","port","controlTokenEnv"],"type":"object"},"version":{"const":"noopolis.daimon.organization-runtime.v1"}},"required":["version","host","agents"],"type":"object"},"organizationRuntimeConfigV2Schema":{"$id":"noopolis.daimon.organization-runtime.v2","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"agents":{"items":{"additionalProperties":false,"properties":{"attention":{"additionalProperties":false,"properties":{"maxBatchBytes":{"maximum":12000,"minimum":1024,"type":"integer"},"maxBatchMessages":{"maximum":32,"minimum":1,"type":"integer"},"maxExecutions":{"maximum":9007199254740991,"minimum":1,"type":"integer"},"maxTokens":{"maximum":9007199254740991,"minimum":1,"type":"integer"}},"type":"object"},"engine":{"additionalProperties":false,"properties":{"codexSandbox":{"additionalProperties":false,"properties":{"mode":{"const":"workspace-write"},"networkAccess":{"const":false},"webSearch":{"const":"disabled"}},"required":["mode","networkAccess","webSearch"],"type":"object"},"kind":{"enum":["codex","grok","agy"]},"model":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"reasoningEffort":{"enum":["none","minimal","low","medium","high","xhigh","max","ultra","persistent"]}},"required":["kind"],"type":"object"},"id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"instructions":{"maxLength":16384,"minLength":1,"pattern":"\\S","type":"string"},"mcp":{"items":{"additionalProperties":false,"properties":{"args":{"items":{"maxLength":4096,"type":"string"},"maxItems":32,"type":"array"},"authSecretEnv":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"},"command":{"pattern":"^/","type":"string"},"env":{"additionalProperties":{"type":"string"},"type":"object"},"name":{"maxLength":4096,"minLength":1,"type":"string"},"tools":{"items":{"maxLength":4096,"minLength":1,"type":"string"},"maxItems":32,"minItems":1,"type":"array","uniqueItems":true},"transport":{"enum":["stdio","sse","streamable_http"]},"url":{"type":"string"}},"required":["name","transport","args","env","tools"],"type":"object"},"maxItems":8,"type":"array"},"memory":{"additionalProperties":false,"properties":{"runtimeHomePath":{"maxLength":4096,"pattern":"^/","type":"string"},"source":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"tokenBudget":{"maximum":1000000,"minimum":1,"type":"integer"}},"required":["runtimeHomePath"],"type":"object"},"moltnet":{"additionalProperties":false,"properties":{"cliPath":{"pattern":"^/","type":"string"},"configPath":{"pattern":"^/","type":"string"},"networks":{"items":{"additionalProperties":false,"properties":{"dms":{"type":"boolean"},"id":{"minLength":1,"type":"string"},"rooms":{"items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true}},"required":["id","rooms","dms"],"type":"object"},"maxItems":16,"type":"array"}},"required":["cliPath","configPath","networks"],"type":"object"},"name":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"runtimeHomePath":{"maxLength":4096,"pattern":"^/","type":"string"},"schedule":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"disabled"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"interval_ms":{"maximum":31536000000,"minimum":1,"type":"integer"},"jitter_seconds":{"maximum":3600,"minimum":0,"type":"integer"},"kind":{"const":"every"},"prompt":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["kind","interval_ms","prompt"],"type":"object"},{"additionalProperties":false,"properties":{"cron":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"jitter_seconds":{"maximum":3600,"minimum":0,"type":"integer"},"kind":{"const":"cron"},"prompt":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"timezone":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["kind","cron","timezone","prompt"],"type":"object"}]},"workspacePath":{"maxLength":4096,"pattern":"^/","type":"string"}},"required":["id","name","instructions","workspacePath","runtimeHomePath","engine","schedule"],"type":"object"},"maxItems":32,"minItems":1,"type":"array"},"host":{"additionalProperties":false,"properties":{"bindHost":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"controlTokenEnv":{"maxLength":4096,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"},"port":{"maximum":65535,"minimum":1,"type":"integer"}},"required":["bindHost","port","controlTokenEnv"],"type":"object"},"version":{"const":"noopolis.daimon.organization-runtime.v2"}},"required":["version","host","agents"],"type":"object"},"supportedEngineKinds":["agy","codex","grok"],"version":"noopolis.daimon.runtime-contract-manifest.v3","wakeAcceptanceTypes":["manual","message","schedule","external"],"wakeRequestSchema":{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"event":{"additionalProperties":false,"properties":{"id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"kind":{"enum":["manual","message","schedule","external"]},"occurredAt":{"format":"date-time","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$","type":"string"},"text":{"maxLength":4096,"type":"string"},"version":{"const":"noopolis.daimon.wake.v1"}},"required":["version","id","kind","text","occurredAt"],"type":"object"}},"required":["agentId","event"],"type":"object"},"wakeResultSchema":{"oneOf":[{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"durationMs":{"minimum":0,"type":"integer"},"status":{"const":"completed"},"text":{"maxLength":4096,"type":"string"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["version","status","agentId","wakeId","text","durationMs"],"type":"object"},{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"code":{"enum":["unauthorized","unknown_agent","queue_full","durable_inbox_required"]},"status":{"const":"rejected"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["version","status","agentId","wakeId","code"],"type":"object"},{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"type":"string"},"code":{"const":"invalid_request"},"status":{"const":"rejected"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"type":"string"}},"required":["version","status","agentId","wakeId","code"],"type":"object"},{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"code":{"enum":["host_stopping","host_stopped","queued_wake_stopped","active_wake_aborted"]},"status":{"const":"stopped"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["version","status","agentId","wakeId","code"],"type":"object"},{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"code":{"const":"engine_failed"},"status":{"const":"failed"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["version","status","agentId","wakeId","code"],"type":"object"}]},"workAvailabilityResponseSchema":{"additionalProperties":false,"properties":{"agents":{"items":{"additionalProperties":false,"properties":{"agent_id":{"type":"string"},"budget":{"additionalProperties":false,"properties":{"agent_executions_remaining":{"minimum":0,"type":"integer"},"agent_executions_used":{"minimum":0,"type":"integer"},"agent_tokens_remaining":{"minimum":0,"type":"integer"},"agent_tokens_used":{"minimum":0,"type":"integer"},"armed":{"type":"boolean"},"epoch":{"type":"string"},"executions_remaining":{"minimum":0,"type":"integer"},"executions_used":{"minimum":0,"type":"integer"},"reason":{"type":"string"},"state":{"enum":["available","paused","stopped"]},"tokens_remaining":{"minimum":0,"type":"integer"},"tokens_used":{"minimum":0,"type":"integer"}},"required":["armed","epoch","state","executions_used","executions_remaining","tokens_used","tokens_remaining","agent_executions_used","agent_tokens_used"],"type":"object"},"deferred":{"minimum":0,"type":"integer"},"error":{"type":"string"},"pending":{"minimum":0,"type":"integer"},"running":{"type":"boolean"}},"required":["agent_id","pending","running","deferred","budget"],"type":"object"},"maxItems":32,"type":"array"},"state":{"enum":["running","paused","stopped"]},"version":{"const":"noopolis.daimon.work-availability.v1"}},"required":["version","state","agents"],"type":"object"},"workBlockedSchema":{"additionalProperties":false,"properties":{"reason":{"enum":["operator_stop","ledger_unavailable","host_stopping","host_stopped","queue_full"]},"retry_after_ms":{"maximum":300000,"minimum":1000,"type":"integer"},"version":{"const":"noopolis.daimon.work-blocked.v1"}},"required":["version","reason","retry_after_ms"],"type":"object"}} +{"activityResponseSchema":{"additionalProperties":false,"properties":{"items":{"items":{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"id":{"format":"uuid","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$","type":"string"},"kind":{"enum":["wake_started","wake_completed","wake_rejected","wake_aborted","agent_stopped"]},"occurredAt":{"format":"date-time","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$","type":"string"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["id","agentId","kind","occurredAt"],"type":"object"},"maxItems":100,"type":"array"},"nextCursor":{"maxLength":16,"minLength":1,"pattern":"^(0|[1-9][0-9]{0,15})$","type":"string"},"version":{"const":"noopolis.daimon.organization-runtime-activity.v1"}},"required":["version","items"],"type":"object"},"activityV2ResponseSchema":{"additionalProperties":false,"properties":{"executions":{"items":{"additionalProperties":false,"properties":{"agent_id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"delivery_ids":{"items":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"maxItems":32,"type":"array"},"execution_id":{"type":"string"},"state":{"const":"running"}},"required":["agent_id","execution_id","state","delivery_ids"],"type":"object"},"maxItems":32,"type":"array"},"items":{"items":{"additionalProperties":false,"properties":{"acceptance_id":{"type":"string"},"accepted_at":{"format":"date-time","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$","type":"string"},"active":{"type":"boolean"},"agent_id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"code":{"enum":["engine_failed","host_stopped","host_stopping","queue_full","unknown_agent"]},"deferred":{"type":"boolean"},"delivery_id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"execution_id":{"type":"string"},"queue_position":{"minimum":1,"type":"integer"},"request_digest":{"type":"string"},"state":{"enum":["accepted","running","completed","failed","stopped"]},"text":{"maxLength":16384,"type":"string"},"updated_at":{"format":"date-time","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$","type":"string"},"version":{"const":"noopolis.daimon.wake-receipt-status.v2"}},"required":["version","acceptance_id","agent_id","delivery_id","request_digest","state","accepted_at","updated_at","active"],"type":"object"},"maxItems":2112,"type":"array"},"version":{"const":"noopolis.daimon.organization-runtime-activity.v2"}},"required":["version","items"],"type":"object"},"agySubscriptionRealm":{"directoryMode":448,"durableMountPath":"/var/lib/spawnfile/daimon/agy-subscription-realm","fileMode":384,"maxUnlockBytes":4096,"unlockMountPath":"/var/lib/spawnfile/daimon/agy-unlock-secret","unlockSourceSlot":"agy-unlock-secret"},"attention":{"accounting":"execution-start-reservations","busyDispatch":"bounded-pending-message-batch","completion":"explicit-per-delivery","defaultMaxBatchBytes":12000,"defaultMaxBatchMessages":8,"enabledBy":"agents[].attention","idleDispatch":"immediate","unhandled":"deferred-until-new-input"},"consumedConfigFields":["version","host.bindHost","host.port","host.controlTokenEnv","agents[].id","agents[].name","agents[].instructions","agents[].workspacePath","agents[].runtimeHomePath","agents[].engine.kind","agents[].engine.model","agents[].engine.reasoningEffort","agents[].engine.codexSandbox","agents[].schedule.kind","agents[].schedule.interval_ms","agents[].schedule.cron","agents[].schedule.timezone","agents[].schedule.prompt","agents[].schedule.jitter_seconds","agents[].mcp","agents[].moltnet","agents[].memory","agents[].attention"],"deliverySemantics":{"activeDeliveryIdempotency":"unbounded-until-terminal","concurrentSameAgentTurns":false,"externalEffectsExactlyOnce":false,"recovery":"at-least-once-with-stable-wake-id","terminalReceiptHorizon":2048},"engineCredentialMaterial":{"codex":{"destinationRelativePath":".codex/auth.json","directoryMode":448,"fileMode":384,"sourceRelativePath":".daimon-inbound/codex-auth","sourceSlot":"codex-auth"}},"grokEngineBroker":{"artifacts":{"arm64Sha256":"16a3f89d84b7139d556b070a626c75ece224d5e05c52d48e045b38086c0a0382","sourceSha256":"c0082d4b366ffdb860d8154ee7f402b8f8be1f09d4eda277eda6965198ab6a75","x64Sha256":"69e2865c722606a71501bc38d8b9c4c2c397e748327a8077e51e040319d1280d"},"backendSocketPath":"/run/daimon-engine-broker/backend.sock","bounds":{"capabilityBundleBytes":8196,"capabilityBytes":4096,"outputBytes":65536,"promptBytes":65536},"controlProtocolVersion":"noopolis.daimon.engine-broker.v2","controlSocketPath":"/run/daimon-engine-broker/control.sock","credentialHomePath":"/var/lib/spawnfile/daimon/grok-subscription-realm","grokCliArtifacts":{"arm64":{"bytes":136090504,"sha256":"39ab87666877d64ef3a40aa60fbe0c3b6a6acd7001b78fe60e2c76bb6cfc4a94","url":"https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-1.0.34-linux-aarch64"},"x64":{"bytes":163035648,"sha256":"be5905e107d2b8b5f3c142d21ecfe4c8fd32a913d2fd551b788707930c4dc80d","url":"https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-1.0.34-linux-x86_64"}},"grokCliBuild":"3736acbc8658","grokCliVersion":"1.0.34","grokExecutablePath":"/usr/local/bin/grok","identities":{"brokerUid":2100,"firstWorkerUid":2200,"organizationUid":2000},"inferenceGrants":{"bodyMembers":["messages","model","reasoning_effort","response_format","stream","stream_options"],"client":{"configSha256":{"grok-4.5":{"high":"ffbc33728b821e9854fbc7c93601e599225da421ecfd6ebf10d314afcc28d6f2","low":"a07f7436f1268bb399ec233c65d3b3d8fb99a11a1f175f8da1ca133c9367bc74","medium":"1f4c0d4dad1f3b09419b5739db6423a09e0049abc091594c64123a75dd53dfb9"},"grok-4.6":{"high":"5652656effa82f0c4f09cf8226b16e6140332a5a358b571194bb5563312367ac","low":"79314d039f787e4ebfec7dacf57adc969086b948f564dec008f0ed6367e6062f","medium":"6f538de0547c0c4e6a3f04ae08595ceadadabb06b75f6b6ee4c428744bb95cd8"},"grok-build":{"high":"98d16f2b7d12f4eb540d625c853e51d227933e204923e43e8b9b4176f10aca2c","low":"ca15c6a562a008227d39c51d3a3a83715663089b3784e8b46debb1fb67b3c4a1","medium":"01783fb6beadcf6f8486fff0836820ad43fab5662b812a907fe9cfdb83e9804d"}},"envKey":"DAIMON_INFERENCE_GRANT","modelId":"daimon-inference-grok"},"failureCodes":["auth_stale","grant_limit","invalid_request","unavailable"],"ledgerDedupeKey":["grant","request"],"ledgerVersion":"noopolis.daimon.inference-usage.v1","limits":{"maxRequests":64,"maxTokens":2000000},"maxInFlightRequestsPerGrant":1,"maxLiveGrants":8,"messageRoles":["system","user","assistant"],"purposes":["judge","optimizer"],"requestKinds":["request_inference_grant","release_inference_grant"],"tokenPrefix":"inference_","ttlMs":600000},"launcherSocketPath":"/run/daimon-engine-broker/launcher.sock","mcpFacade":{"host":"127.0.0.1","path":"/mcp","port":43124},"nativeAbiVersion":2,"nativeExecutablePath":"/opt/daimon/bin/daimon-engine-broker","projectionVersion":"noopolis.daimon.grok-broker-projection.v1","providerProxy":{"host":"127.0.0.1","port":43123},"registrationPath":"/etc/daimon-engine-broker/registrations.bin","serviceConfigPath":"/etc/daimon-engine-broker/service.json","serviceConfigVersions":["noopolis.daimon.engine-broker-service.v1","noopolis.daimon.engine-broker-service.v2"],"slotPreflightVersion":"noopolis.daimon.grok-slot-preflight.v2","turnLimits":{"bounds":{"maxRequests":[1,48],"maxTokens":[1,10000000],"timeoutMs":[1000,3600000]},"keys":["maxRequests","maxTokens","timeoutMs"],"limitReasons":["tokens","requests","timeout","none"],"maxInFlightRequests":1,"missingUsageEstimate":{"inputBytesPerToken":2,"outputTokens":4096},"requestUsageMaxTokens":500000,"tokenCeilingOvershoot":"at-most-one-request","v1Defaults":{"maxRequests":32,"maxTokens":300000,"timeoutMs":240000},"wakeMayOnlyLower":true},"turnRecordVersions":["noopolis.daimon.engine-broker-turn.v1","noopolis.daimon.engine-broker-turn.v2"],"turnStorePath":"/var/lib/spawnfile/daimon/grok-subscription-realm/turns","wakeLimitEnvironment":{"maxTokens":"DAIMON_ENGINE_WAKE_TOKEN_CEILING","timeoutMs":"DAIMON_ENGINE_WAKE_TIMEOUT_MS"},"worker":{"configSha256":{"grok-4.5":{"high":"0bb4ad8bfa5062169b28422d1d534b45420d4e46b1e546bda1c578eb34303646","low":"7aa13e90b9bc08d1a018f48b7a84de1dab41db586627ee2d5a25f69011ba7e25","medium":"218ba37e57a6f02fa36b265b4e154e68e30bd2d4794feb130cc226fdda7732a9"},"grok-4.6":{"high":"3ce44ace503362326b47149b528b942ce638fe146313d62502f248acf9c7333d","low":"eed6a451150a72b2cb528b30c23b3d51c7d3bc38c67a8985d4dcdf956ff214d3","medium":"8850502dbebf8918c5161c63efcc4ccf18719488300f4cec1deceb2c112b451f"},"grok-build":{"high":"bbe72aaf70c417dc7007823a7e9e1a7d1fa8d57e50bde6f24036083b32bcc859","low":"83ac7202442286a65c359cc596b0b8db7bc4529ee70e98224f6cd6f66deb6878","medium":"0146313f28739888eb4e861f1bfb285f7ee4e0a9164669256ebdf6492a2790ce"}},"defaultModel":"grok-4.6","defaultReasoningEffort":"low","home":{"directory":{"group":"worker","mode":1017,"uid":0},"privateTmp":{"mode":448,"owner":"worker","relativeToWorkerHome":"tmp"},"readOnlyFiles":{"gid":0,"mode":292,"names":["config.toml","managed_config.toml","requirements.toml","sandbox.toml","trusted_folders.toml"],"uid":0},"sandboxEvents":{"group":"broker","mode":416,"owner":"worker","relativePath":"sessions/sandbox-events.jsonl"},"sessionsDirectory":{"group":"worker","mode":1017,"relativePath":"sessions","uid":0},"sharedTmp":{"maxGroupExclusive":2200,"mode":1020,"otherMode":4,"paths":["/tmp","/var/tmp"],"uid":0},"spillDirectory":{"fileMode":416,"group":"worker","mode":1512,"owner":"organization","relativeToRuntimeHome":"tool-output"}},"maxTurns":48,"modelId":"daimon-broker-grok","models":["grok-4.6","grok-4.5","grok-build"],"reasoningEfforts":["low","medium","high"],"systemPromptSha256":"2c31c0085a54a4efbf9c0cf0b8124c56e47f38691b7f0c7fa233a74abaa8ddf8","toolIds":["run_terminal_cmd","read_file","grep","list_dir","search_tool","use_tool"],"visibleTools":["grep","list_dir","read_file","run_terminal_command","search_tool","use_tool"]}},"grokSubscriptionRealm":{"agentCredentialRelativePath":".grok/auth.json","bootstrapMountPath":"/var/lib/spawnfile/daimon/grok-bootstrap-auth","bootstrapSourceSlot":"grok-auth","directoryMode":448,"durableMountPath":"/var/lib/spawnfile/daimon/grok-subscription-realm","fileMode":384,"maxCredentialBytes":65536},"healthResponseSchema":{"additionalProperties":false,"properties":{"agents":{"items":{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"state":{"enum":["starting","running","stopping","stopped","idle","failed"]}},"required":["agentId","state"],"type":"object"},"maxItems":32,"type":"array"},"state":{"enum":["starting","running","stopping","stopped"]},"version":{"const":"noopolis.daimon.organization-runtime-health.v1"}},"required":["version","state","agents"],"type":"object"},"organizationRuntimeConfigSchema":{"$id":"noopolis.daimon.organization-runtime.v1","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"agents":{"items":{"additionalProperties":false,"properties":{"attention":{"additionalProperties":false,"properties":{"maxBatchBytes":{"maximum":12000,"minimum":1024,"type":"integer"},"maxBatchMessages":{"maximum":32,"minimum":1,"type":"integer"},"maxExecutions":{"maximum":9007199254740991,"minimum":1,"type":"integer"},"maxTokens":{"maximum":9007199254740991,"minimum":1,"type":"integer"}},"type":"object"},"engine":{"additionalProperties":false,"allOf":[{"if":{"properties":{"kind":{"const":"grok"}}},"then":{"dependentRequired":{"model":["reasoningEffort"],"reasoningEffort":["model"]},"properties":{"codexSandbox":false,"model":{"enum":["grok-4.6","grok-4.5","grok-build"]},"reasoningEffort":{"enum":["low","medium","high"]}}}},{"if":{"properties":{"kind":{"const":"agy"}}},"then":{"properties":{"codexSandbox":false,"model":false,"reasoningEffort":false}}}],"properties":{"codexSandbox":{"additionalProperties":false,"properties":{"mode":{"const":"workspace-write"},"networkAccess":{"const":false},"webSearch":{"const":"disabled"}},"required":["mode","networkAccess","webSearch"],"type":"object"},"kind":{"enum":["codex","grok","agy"]},"model":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"reasoningEffort":{"enum":["none","minimal","low","medium","high","xhigh","max","ultra","persistent"]}},"required":["kind"],"type":"object"},"id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"instructions":{"maxLength":16384,"minLength":1,"pattern":"\\S","type":"string"},"mcp":{"items":{"additionalProperties":false,"properties":{"args":{"items":{"maxLength":4096,"type":"string"},"maxItems":32,"type":"array"},"authSecretEnv":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"},"command":{"pattern":"^/","type":"string"},"env":{"additionalProperties":{"type":"string"},"type":"object"},"name":{"maxLength":4096,"minLength":1,"type":"string"},"tools":{"items":{"maxLength":4096,"minLength":1,"type":"string"},"maxItems":32,"minItems":1,"type":"array","uniqueItems":true},"transport":{"enum":["stdio","sse","streamable_http"]},"url":{"type":"string"}},"required":["name","transport","args","env","tools"],"type":"object"},"maxItems":8,"type":"array"},"memory":{"additionalProperties":false,"properties":{"runtimeHomePath":{"maxLength":4096,"pattern":"^/","type":"string"},"source":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"tokenBudget":{"maximum":1000000,"minimum":1,"type":"integer"}},"required":["runtimeHomePath"],"type":"object"},"moltnet":{"additionalProperties":false,"properties":{"cliPath":{"pattern":"^/","type":"string"},"configPath":{"pattern":"^/","type":"string"},"networks":{"items":{"additionalProperties":false,"properties":{"dms":{"type":"boolean"},"id":{"minLength":1,"type":"string"},"rooms":{"items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true}},"required":["id","rooms","dms"],"type":"object"},"maxItems":16,"type":"array"}},"required":["cliPath","configPath","networks"],"type":"object"},"name":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"runtimeHomePath":{"maxLength":4096,"pattern":"^/","type":"string"},"workspacePath":{"maxLength":4096,"pattern":"^/","type":"string"}},"required":["id","name","instructions","workspacePath","runtimeHomePath","engine"],"type":"object"},"maxItems":32,"minItems":1,"type":"array"},"host":{"additionalProperties":false,"properties":{"bindHost":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"controlTokenEnv":{"maxLength":4096,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"},"port":{"maximum":65535,"minimum":1,"type":"integer"}},"required":["bindHost","port","controlTokenEnv"],"type":"object"},"version":{"const":"noopolis.daimon.organization-runtime.v1"}},"required":["version","host","agents"],"type":"object"},"organizationRuntimeConfigV2Schema":{"$id":"noopolis.daimon.organization-runtime.v2","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"agents":{"items":{"additionalProperties":false,"properties":{"attention":{"additionalProperties":false,"properties":{"maxBatchBytes":{"maximum":12000,"minimum":1024,"type":"integer"},"maxBatchMessages":{"maximum":32,"minimum":1,"type":"integer"},"maxExecutions":{"maximum":9007199254740991,"minimum":1,"type":"integer"},"maxTokens":{"maximum":9007199254740991,"minimum":1,"type":"integer"}},"type":"object"},"engine":{"additionalProperties":false,"allOf":[{"if":{"properties":{"kind":{"const":"grok"}}},"then":{"dependentRequired":{"model":["reasoningEffort"],"reasoningEffort":["model"]},"properties":{"codexSandbox":false,"model":{"enum":["grok-4.6","grok-4.5","grok-build"]},"reasoningEffort":{"enum":["low","medium","high"]}}}},{"if":{"properties":{"kind":{"const":"agy"}}},"then":{"properties":{"codexSandbox":false,"model":false,"reasoningEffort":false}}}],"properties":{"codexSandbox":{"additionalProperties":false,"properties":{"mode":{"const":"workspace-write"},"networkAccess":{"const":false},"webSearch":{"const":"disabled"}},"required":["mode","networkAccess","webSearch"],"type":"object"},"kind":{"enum":["codex","grok","agy"]},"model":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"reasoningEffort":{"enum":["none","minimal","low","medium","high","xhigh","max","ultra","persistent"]}},"required":["kind"],"type":"object"},"id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"instructions":{"maxLength":16384,"minLength":1,"pattern":"\\S","type":"string"},"mcp":{"items":{"additionalProperties":false,"properties":{"args":{"items":{"maxLength":4096,"type":"string"},"maxItems":32,"type":"array"},"authSecretEnv":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"},"command":{"pattern":"^/","type":"string"},"env":{"additionalProperties":{"type":"string"},"type":"object"},"name":{"maxLength":4096,"minLength":1,"type":"string"},"tools":{"items":{"maxLength":4096,"minLength":1,"type":"string"},"maxItems":32,"minItems":1,"type":"array","uniqueItems":true},"transport":{"enum":["stdio","sse","streamable_http"]},"url":{"type":"string"}},"required":["name","transport","args","env","tools"],"type":"object"},"maxItems":8,"type":"array"},"memory":{"additionalProperties":false,"properties":{"runtimeHomePath":{"maxLength":4096,"pattern":"^/","type":"string"},"source":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"tokenBudget":{"maximum":1000000,"minimum":1,"type":"integer"}},"required":["runtimeHomePath"],"type":"object"},"moltnet":{"additionalProperties":false,"properties":{"cliPath":{"pattern":"^/","type":"string"},"configPath":{"pattern":"^/","type":"string"},"networks":{"items":{"additionalProperties":false,"properties":{"dms":{"type":"boolean"},"id":{"minLength":1,"type":"string"},"rooms":{"items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true}},"required":["id","rooms","dms"],"type":"object"},"maxItems":16,"type":"array"}},"required":["cliPath","configPath","networks"],"type":"object"},"name":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"runtimeHomePath":{"maxLength":4096,"pattern":"^/","type":"string"},"schedule":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"disabled"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"interval_ms":{"maximum":31536000000,"minimum":1,"type":"integer"},"jitter_seconds":{"maximum":3600,"minimum":0,"type":"integer"},"kind":{"const":"every"},"prompt":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["kind","interval_ms","prompt"],"type":"object"},{"additionalProperties":false,"properties":{"cron":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"jitter_seconds":{"maximum":3600,"minimum":0,"type":"integer"},"kind":{"const":"cron"},"prompt":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"timezone":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["kind","cron","timezone","prompt"],"type":"object"}]},"workspacePath":{"maxLength":4096,"pattern":"^/","type":"string"}},"required":["id","name","instructions","workspacePath","runtimeHomePath","engine","schedule"],"type":"object"},"maxItems":32,"minItems":1,"type":"array"},"host":{"additionalProperties":false,"properties":{"bindHost":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"controlTokenEnv":{"maxLength":4096,"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"},"port":{"maximum":65535,"minimum":1,"type":"integer"}},"required":["bindHost","port","controlTokenEnv"],"type":"object"},"version":{"const":"noopolis.daimon.organization-runtime.v2"}},"required":["version","host","agents"],"type":"object"},"supportedEngineKinds":["agy","codex","grok"],"version":"noopolis.daimon.runtime-contract-manifest.v3","wakeAcceptanceTypes":["manual","message","schedule","external"],"wakeRequestSchema":{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"event":{"additionalProperties":false,"properties":{"id":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"kind":{"enum":["manual","message","schedule","external"]},"occurredAt":{"format":"date-time","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$","type":"string"},"text":{"maxLength":4096,"type":"string"},"version":{"const":"noopolis.daimon.wake.v1"}},"required":["version","id","kind","text","occurredAt"],"type":"object"}},"required":["agentId","event"],"type":"object"},"wakeResultSchema":{"oneOf":[{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"durationMs":{"minimum":0,"type":"integer"},"status":{"const":"completed"},"text":{"maxLength":4096,"type":"string"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["version","status","agentId","wakeId","text","durationMs"],"type":"object"},{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"code":{"enum":["unauthorized","unknown_agent","queue_full","durable_inbox_required"]},"status":{"const":"rejected"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["version","status","agentId","wakeId","code"],"type":"object"},{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"type":"string"},"code":{"const":"invalid_request"},"status":{"const":"rejected"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"type":"string"}},"required":["version","status","agentId","wakeId","code"],"type":"object"},{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"code":{"enum":["host_stopping","host_stopped","queued_wake_stopped","active_wake_aborted"]},"status":{"const":"stopped"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["version","status","agentId","wakeId","code"],"type":"object"},{"additionalProperties":false,"properties":{"agentId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"},"code":{"const":"engine_failed"},"status":{"const":"failed"},"version":{"const":"noopolis.daimon.wake-result.v1"},"wakeId":{"maxLength":4096,"minLength":1,"pattern":"\\S","type":"string"}},"required":["version","status","agentId","wakeId","code"],"type":"object"}]},"workAvailabilityResponseSchema":{"additionalProperties":false,"properties":{"agents":{"items":{"additionalProperties":false,"properties":{"agent_id":{"type":"string"},"budget":{"additionalProperties":false,"properties":{"agent_executions_remaining":{"minimum":0,"type":"integer"},"agent_executions_used":{"minimum":0,"type":"integer"},"agent_tokens_remaining":{"minimum":0,"type":"integer"},"agent_tokens_used":{"minimum":0,"type":"integer"},"armed":{"type":"boolean"},"epoch":{"type":"string"},"executions_remaining":{"minimum":0,"type":"integer"},"executions_used":{"minimum":0,"type":"integer"},"reason":{"type":"string"},"state":{"enum":["available","paused","stopped"]},"tokens_remaining":{"minimum":0,"type":"integer"},"tokens_used":{"minimum":0,"type":"integer"}},"required":["armed","epoch","state","executions_used","executions_remaining","tokens_used","tokens_remaining","agent_executions_used","agent_tokens_used"],"type":"object"},"deferred":{"minimum":0,"type":"integer"},"error":{"type":"string"},"pending":{"minimum":0,"type":"integer"},"running":{"type":"boolean"}},"required":["agent_id","pending","running","deferred","budget"],"type":"object"},"maxItems":32,"type":"array"},"state":{"enum":["running","paused","stopped"]},"version":{"const":"noopolis.daimon.work-availability.v1"}},"required":["version","state","agents"],"type":"object"},"workBlockedSchema":{"additionalProperties":false,"properties":{"reason":{"enum":["operator_stop","ledger_unavailable","host_stopping","host_stopped","queue_full"]},"retry_after_ms":{"maximum":300000,"minimum":1000,"type":"integer"},"version":{"const":"noopolis.daimon.work-blocked.v1"}},"required":["version","reason","retry_after_ms"],"type":"object"}} diff --git a/src/runtime/daimon/contract-manifest.sha256 b/src/runtime/daimon/contract-manifest.sha256 index b3f7bdc7..1ed539ba 100644 --- a/src/runtime/daimon/contract-manifest.sha256 +++ b/src/runtime/daimon/contract-manifest.sha256 @@ -1 +1 @@ -sha256:79bc6cd06aad3038ea26937f5b3e02d51abc001cf3629d80f49e377e45047b62 +sha256:401da56de1182a4c1834bc872ab3121d0b45d63486d729a02ff98d5627f30829 diff --git a/src/runtime/daimon/contractManifest.ts b/src/runtime/daimon/contractManifest.ts index 3d563c51..117dbbd9 100644 --- a/src/runtime/daimon/contractManifest.ts +++ b/src/runtime/daimon/contractManifest.ts @@ -7,7 +7,7 @@ import { SpawnfileError } from "../../shared/index.js"; export const DAIMON_CONTRACT_MANIFEST_VERSION = "noopolis.daimon.runtime-contract-manifest.v3" as const; export const DAIMON_CONTRACT_MANIFEST_SHA256 = - "sha256:79bc6cd06aad3038ea26937f5b3e02d51abc001cf3629d80f49e377e45047b62" as const; + "sha256:401da56de1182a4c1834bc872ab3121d0b45d63486d729a02ff98d5627f30829" as const; export const DAIMON_CONTRACT_MANIFEST_FILE = "contract-manifest.json"; export const DAIMON_CONTRACT_MANIFEST_DIGEST_FILE = "contract-manifest.sha256"; export const DAIMON_RUNTIME_HOME_ROOT = "/var/lib/spawnfile/instances/daimon"; @@ -31,6 +31,15 @@ export const DAIMON_GROK_SUBSCRIPTION_REALM = { fileMode: 0o600, maxCredentialBytes: 64 * 1024 } as const; +export const DAIMON_GROK_BROKER_MODELS = ["grok-4.6", "grok-4.5", "grok-build"] as const; +export const DAIMON_GROK_BROKER_REASONING_EFFORTS = ["low", "medium", "high"] as const; +export type DaimonGrokBrokerModel = typeof DAIMON_GROK_BROKER_MODELS[number]; +export type DaimonGrokBrokerReasoningEffort = typeof DAIMON_GROK_BROKER_REASONING_EFFORTS[number]; +/** + * Byte mirror of Daimon's `GROK_ENGINE_BROKER` (daimon + * `src/contracts/runtimeContractManifest.ts`), attested key-for-key against the + * vendored `contract-manifest.json` by `parseDaimonContractManifest`. + */ export const DAIMON_GROK_ENGINE_BROKER = { nativeAbiVersion: 2, nativeExecutablePath: "/opt/daimon/bin/daimon-engine-broker", @@ -45,11 +54,82 @@ export const DAIMON_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: DAIMON_GROK_BROKER_MODELS, + reasoningEfforts: DAIMON_GROK_BROKER_REASONING_EFFORTS, + defaultModel: "grok-4.6", + defaultReasoningEffort: "low", + toolIds: ["run_terminal_cmd", "read_file", "grep", "list_dir", "search_tool", "use_tool"], + visibleTools: ["grep", "list_dir", "read_file", "run_terminal_command", "search_tool", "use_tool"], + maxTurns: 48, + systemPromptSha256: "2c31c0085a54a4efbf9c0cf0b8124c56e47f38691b7f0c7fa233a74abaa8ddf8", + 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" } + }, + 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 }, + privateTmp: { relativeToWorkerHome: "tmp", owner: "worker", mode: 0o700 }, + sharedTmp: { paths: ["/tmp", "/var/tmp"], uid: 0, maxGroupExclusive: 2_200, otherMode: 0o4, mode: 0o1774 }, + spillDirectory: { relativeToRuntimeHome: "tool-output", owner: "organization", group: "worker", mode: 0o2750, fileMode: 0o640 } + } + }, bounds: { promptBytes: 65_536, capabilityBytes: 4_096, capabilityBundleBytes: 8_196, outputBytes: 65_536 }, + controlProtocolVersion: "noopolis.daimon.engine-broker.v2", + turnRecordVersions: ["noopolis.daimon.engine-broker-turn.v1", "noopolis.daimon.engine-broker-turn.v2"], + serviceConfigVersions: ["noopolis.daimon.engine-broker-service.v1", "noopolis.daimon.engine-broker-service.v2"], + turnLimits: { + keys: ["maxRequests", "maxTokens", "timeoutMs"], + v1Defaults: { maxRequests: 32, maxTokens: 300_000, timeoutMs: 240_000 }, + bounds: { maxRequests: [1, 48], maxTokens: [1, 10_000_000], timeoutMs: [1_000, 3_600_000] }, + limitReasons: ["tokens", "requests", "timeout", "none"], + wakeMayOnlyLower: true, + tokenCeilingOvershoot: "at-most-one-request", + maxInFlightRequests: 1, + requestUsageMaxTokens: 500_000, + missingUsageEstimate: { inputBytesPerToken: 2, outputTokens: 4_096 } + }, + wakeLimitEnvironment: { timeoutMs: "DAIMON_ENGINE_WAKE_TIMEOUT_MS", maxTokens: "DAIMON_ENGINE_WAKE_TOKEN_CEILING" }, + projectionVersion: "noopolis.daimon.grok-broker-projection.v1", + slotPreflightVersion: "noopolis.daimon.grok-slot-preflight.v2", + inferenceGrants: { + requestKinds: ["request_inference_grant", "release_inference_grant"], + purposes: ["judge", "optimizer"], + tokenPrefix: "inference_", + ttlMs: 600_000, + limits: { maxRequests: 64, maxTokens: 2_000_000 }, + maxLiveGrants: 8, + maxInFlightRequestsPerGrant: 1, + bodyMembers: ["messages", "model", "reasoning_effort", "response_format", "stream", "stream_options"], + messageRoles: ["system", "user", "assistant"], + failureCodes: ["auth_stale", "grant_limit", "invalid_request", "unavailable"], + ledgerVersion: "noopolis.daimon.inference-usage.v1", + ledgerDedupeKey: ["grant", "request"], + client: { + modelId: "daimon-inference-grok", + envKey: "DAIMON_INFERENCE_GRANT", + configSha256: { + "grok-4.6": { low: "79314d039f787e4ebfec7dacf57adc969086b948f564dec008f0ed6367e6062f", medium: "6f538de0547c0c4e6a3f04ae08595ceadadabb06b75f6b6ee4c428744bb95cd8", high: "5652656effa82f0c4f09cf8226b16e6140332a5a358b571194bb5563312367ac" }, + "grok-4.5": { low: "a07f7436f1268bb399ec233c65d3b3d8fb99a11a1f175f8da1ca133c9367bc74", medium: "1f4c0d4dad1f3b09419b5739db6423a09e0049abc091594c64123a75dd53dfb9", high: "ffbc33728b821e9854fbc7c93601e599225da421ecfd6ebf10d314afcc28d6f2" }, + "grok-build": { low: "ca15c6a562a008227d39c51d3a3a83715663089b3784e8b46debb1fb67b3c4a1", medium: "01783fb6beadcf6f8486fff0836820ad43fab5662b812a907fe9cfdb83e9804d", high: "98d16f2b7d12f4eb540d625c853e51d227933e204923e43e8b9b4176f10aca2c" } + } + } + }, artifacts: { - sourceSha256: "bdcab1e12dcc531ed8e56f890263ca23a9ee7bac468191dd598e143df4ff8c58", - x64Sha256: "e3fe2738fc8a979861085b4003bf2d5d7c284874897cb6ec2e2e2383211768bd", - arm64Sha256: "ad44e02c38e6a3207ac4a3d5fd98b6d2e55341ce42dfd2f07204bbe54a7a653d" + sourceSha256: "c0082d4b366ffdb860d8154ee7f402b8f8be1f09d4eda277eda6965198ab6a75", + x64Sha256: "69e2865c722606a71501bc38d8b9c4c2c397e748327a8077e51e040319d1280d", + arm64Sha256: "16a3f89d84b7139d556b070a626c75ece224d5e05c52d48e045b38086c0a0382" } } as const; /** diff --git a/src/runtime/daimon/grokModel.ts b/src/runtime/daimon/grokModel.ts new file mode 100644 index 00000000..70eae8ed --- /dev/null +++ b/src/runtime/daimon/grokModel.ts @@ -0,0 +1,44 @@ +import type { ResolvedAgentNode } from "../../compiler/types.js"; +import { SpawnfileError } from "../../shared/index.js"; + +import { + DAIMON_GROK_BROKER_MODELS, + DAIMON_GROK_BROKER_REASONING_EFFORTS, + type DaimonGrokBrokerModel, + type DaimonGrokBrokerReasoningEffort +} from "./contractManifest.js"; + +export interface DaimonGrokModel { + model: DaimonGrokBrokerModel; + reasoningEffort: DaimonGrokBrokerReasoningEffort; +} + +/** + * The declared model of one brokered Daimon Grok agent. + * + * Every Grok agent runs through Daimon's engine broker, whose worker + * `config.toml` bytes are pinned per model x effort and whose provider proxy + * refuses any request body carrying another pair. Nothing may be inherited: Grok + * 1.0.34 silently drops an effort a model does not declare and its catalog + * default for `grok-4.6` is `high`, so the declaration is required in full — + * `execution.model.primary` with `provider: xai`, a name from Daimon's closed + * list, a target-level `auth.method: grok`, and `reasoning_effort` — and there + * is exactly one model (no fallback, no legacy model-level auth). + */ +export const resolveDaimonGrokModel = (node: ResolvedAgentNode): DaimonGrokModel => { + const fail = (detail: string): never => { + throw new SpawnfileError( + "validation_error", + `Daimon Grok agent ${node.name} ${detail}; declare execution.model.primary { provider: xai, name: ${DAIMON_GROK_BROKER_MODELS.join(" | ")}, auth: { method: grok }, reasoning_effort: ${DAIMON_GROK_BROKER_REASONING_EFFORTS.join(" | ")} }` + ); + }; + const declared = node.execution?.model; + if (!declared) return fail("must declare its brokered model and reasoning effort"); + if (declared.fallback?.length || declared.auth) fail("must declare exactly one model with target-level auth"); + const primary = declared.primary; + if (primary.provider !== "xai" || primary.endpoint || primary.auth?.method !== "grok") fail("must use provider xai with auth.method grok"); + if (!(DAIMON_GROK_BROKER_MODELS as readonly string[]).includes(primary.name)) fail(`declares unsupported model ${primary.name}`); + const effort = primary.reasoning_effort; + if (effort === undefined || !(DAIMON_GROK_BROKER_REASONING_EFFORTS as readonly string[]).includes(effort)) fail("must declare reasoning_effort"); + return { model: primary.name as DaimonGrokBrokerModel, reasoningEffort: effort as DaimonGrokBrokerReasoningEffort }; +}; diff --git a/src/runtime/daimon/grokWorkerConfigBytes.ts b/src/runtime/daimon/grokWorkerConfigBytes.ts new file mode 100644 index 00000000..24bb788c --- /dev/null +++ b/src/runtime/daimon/grokWorkerConfigBytes.ts @@ -0,0 +1,38 @@ +/* v8 ignore file -- generated data module */ +// Generated by scripts/vendor-daimon-grok-contract.ts from Daimon's own renderers. Do not edit by hand. + +/** Daimon `renderGrokBrokerWorkerConfig({ model, reasoningEffort })` bytes, verified against the manifest pins on use. */ +export const DAIMON_GROK_WORKER_CONFIG_BYTES = { + "grok-4.6": { + "low": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"low\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-4.6\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"low\"\nlabel = \"Low\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n", + "medium": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"medium\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-4.6\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"medium\"\nlabel = \"Medium\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n", + "high": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"high\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-4.6\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"high\"\nlabel = \"High\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n" + }, + "grok-4.5": { + "low": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"low\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-4.5\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"low\"\nlabel = \"Low\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n", + "medium": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"medium\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-4.5\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"medium\"\nlabel = \"Medium\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n", + "high": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"high\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-4.5\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"high\"\nlabel = \"High\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n" + }, + "grok-build": { + "low": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"low\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-build\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"low\"\nlabel = \"Low\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n", + "medium": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"medium\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-build\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"medium\"\nlabel = \"Medium\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n", + "high": "[cli]\nauto_update = false\nuse_leader = false\nshow_tips = false\n\n[features]\ntelemetry = false\ntitle_refresh = false\nsession_recap = false\nturn_summary = false\nrepo_status_in_system_prompt = false\ncodebase_indexing = false\nbackend_tools = false\nask_user_question = false\nimage_gen = false\nvideo_gen = false\nweb_fetch = false\ncampaigns = false\nmanaged_config = false\n\n[managed_mcps]\nenabled = false\n\n[skills]\ndisabled = [\"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\"]\n\n[workflows]\nenabled = false\n\n[models]\ndefault = \"daimon-broker-grok\"\ndefault_reasoning_effort = \"high\"\nsession_summary = \"daimon-session-title-disabled\"\n\n[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\n[model.daimon-broker-grok]\nmodel = \"grok-build\"\nbase_url = \"http://127.0.0.1:43123/v1\"\nenv_key = \"DAIMON_PROVIDER_CAPABILITY\"\napi_backend = \"chat_completions\"\ncontext_window = 131072\nsupports_backend_search = false\n\n[[model.daimon-broker-grok.reasoning_efforts]]\nvalue = \"high\"\nlabel = \"High\"\ndefault = true\n\n[mcp_servers.daimon]\nurl = \"http://127.0.0.1:43124/mcp\"\nbearer_token_env_var = \"DAIMON_MCP_CAPABILITY\"\n" + } +} as const; + +/** Daimon `renderGrokWorkerSandboxProfile(denyPaths)` samples; the Spawnfile mirror must reproduce them byte for byte. */ +export const DAIMON_GROK_WORKER_PROFILE_SAMPLES = [ + { + "bytes": "[profiles.daimon-strict]\nextends = \"strict\"\nrestrict_network = true\ndeny = []\n", + "denyPaths": [] + }, + { + "bytes": "[profiles.daimon-strict]\nextends = \"strict\"\nrestrict_network = true\ndeny = [\"/run/daimon-engine-broker\", \"/var/lib/daimon-workers/2201\", \"/var/lib/spawnfile/daimon/usage\"]\n", + "denyPaths": [ + "/var/lib/spawnfile/daimon/usage", + "/run/daimon-engine-broker", + "/run/daimon-engine-broker", + "/var/lib/daimon-workers/2201" + ] + } +] as const; diff --git a/src/runtime/daimon/grokWorkerContract.test.ts b/src/runtime/daimon/grokWorkerContract.test.ts new file mode 100644 index 00000000..c8fe548a --- /dev/null +++ b/src/runtime/daimon/grokWorkerContract.test.ts @@ -0,0 +1,42 @@ +import { createHash } from "node:crypto"; + +import { describe, expect, it } from "vitest"; + +import { DAIMON_GROK_BROKER_MODELS, DAIMON_GROK_BROKER_REASONING_EFFORTS, DAIMON_GROK_ENGINE_BROKER } from "./contractManifest.js"; +import { DAIMON_GROK_WORKER_CONFIG_BYTES, DAIMON_GROK_WORKER_PROFILE_SAMPLES } from "./grokWorkerConfigBytes.js"; +import { renderDaimonGrokWorkerSandboxProfile, resolveDaimonGrokWorkerConfig } from "./grokWorkerContract.js"; + +const sha256 = (value: string): string => createHash("sha256").update(value).digest("hex"); + +describe("vendored Daimon Grok worker contract", () => { + it("serves Daimon's renderer bytes for every declared model x effort, each matching the manifest pin", () => { + for (const model of DAIMON_GROK_BROKER_MODELS) { + for (const effort of DAIMON_GROK_BROKER_REASONING_EFFORTS) { + const resolved = resolveDaimonGrokWorkerConfig(model, effort); + expect(resolved.bytes).toBe(DAIMON_GROK_WORKER_CONFIG_BYTES[model][effort]); + expect(sha256(resolved.bytes)).toBe(DAIMON_GROK_ENGINE_BROKER.worker.configSha256[model][effort]); + expect(resolved.bytes).toContain(`model = "${model}"`); + expect(resolved.bytes).toContain(`value = "${effort}"`); + } + } + }); + + it("refuses config bytes that differ from the manifest pin by a single byte", () => { + const tampered = { + ...DAIMON_GROK_WORKER_CONFIG_BYTES, + "grok-4.6": { ...DAIMON_GROK_WORKER_CONFIG_BYTES["grok-4.6"], low: DAIMON_GROK_WORKER_CONFIG_BYTES["grok-4.6"].low.replace("grok-4.6", "grok-4.5") } + }; + expect(() => resolveDaimonGrokWorkerConfig("grok-4.6", "low", tampered)).toThrow(/does not match the contract manifest pin/u); + const swapped = { ...DAIMON_GROK_WORKER_CONFIG_BYTES, "grok-4.6": { ...DAIMON_GROK_WORKER_CONFIG_BYTES["grok-4.6"], low: DAIMON_GROK_WORKER_CONFIG_BYTES["grok-4.6"].medium } }; + expect(() => resolveDaimonGrokWorkerConfig("grok-4.6", "low", swapped)).toThrow(/manifest pin/u); + }); + + it("renders sandbox profile bytes identical to Daimon's renderer", () => { + for (const sample of DAIMON_GROK_WORKER_PROFILE_SAMPLES) { + expect(renderDaimonGrokWorkerSandboxProfile(sample.denyPaths)).toBe(sample.bytes); + } + for (const unsafe of ["relative/path", "/", "/trailing/", "/a/../b", "/quote\"d", "/glob*"]) { + expect(() => renderDaimonGrokWorkerSandboxProfile([unsafe])).toThrow(/deny path/u); + } + }); +}); diff --git a/src/runtime/daimon/grokWorkerContract.ts b/src/runtime/daimon/grokWorkerContract.ts new file mode 100644 index 00000000..5c0f4086 --- /dev/null +++ b/src/runtime/daimon/grokWorkerContract.ts @@ -0,0 +1,68 @@ +import { createHash } from "node:crypto"; +import path from "node:path"; + +import { SpawnfileError } from "../../shared/index.js"; + +import { + DAIMON_GROK_ENGINE_BROKER, + type DaimonGrokBrokerModel, + type DaimonGrokBrokerReasoningEffort +} from "./contractManifest.js"; +import { DAIMON_GROK_WORKER_CONFIG_BYTES } from "./grokWorkerConfigBytes.js"; + +export const DAIMON_GROK_WORKER_SANDBOX_PROFILE = "daimon-strict" as const; +export const DAIMON_GROK_WORKER_HOME_DIRECTORY = ".grok" as const; + +const sha256 = (bytes: string): string => createHash("sha256").update(bytes).digest("hex"); + +/** Characters TOML or Grok would reinterpret inside a deny entry (mirrors Daimon's refusal set). */ +const UNSAFE_DENY_PATH_CHARACTER = new RegExp("[\"\\\\\\u0000-\\u001f\\u007f*?[\\]]", "u"); + +/** + * The worker `config.toml` bytes for one declared model x effort: Daimon's own + * renderer output (vendored by `scripts/vendor-daimon-grok-contract.ts`), + * refused unless it hashes to the manifest pin the broker attests every turn. + */ +export const resolveDaimonGrokWorkerConfig = ( + model: DaimonGrokBrokerModel, + reasoningEffort: DaimonGrokBrokerReasoningEffort, + vendored: Readonly>>> = DAIMON_GROK_WORKER_CONFIG_BYTES +): { bytes: string; sha256: string } => { + const pins = DAIMON_GROK_ENGINE_BROKER.worker.configSha256 as Readonly> | undefined>>; + const pin = pins[model]?.[reasoningEffort]; + const bytes = vendored[model]?.[reasoningEffort]; + if (pin === undefined || bytes === undefined || sha256(bytes) !== pin) { + throw new SpawnfileError( + "compile_error", + `Vendored Daimon Grok worker config for ${model}/${reasoningEffort} does not match the contract manifest pin; re-vendor the Daimon contract` + ); + } + return { bytes, sha256: pin }; +}; + +/** + * Byte mirror of Daimon's `renderGrokWorkerSandboxProfile` + * (`daimon/src/runtime/grokWorkerSandboxProfile.ts`), pinned to Daimon's own + * output by `DAIMON_GROK_WORKER_PROFILE_SAMPLES`. Grok 1.0.34 runs every profile + * inside bubblewrap and enforces a non-empty `deny` list; its strict base still + * reads all of `/run`, `/var`, `/tmp` and `/etc`, so this list is what keeps + * protected paths from the worker. + */ +export const renderDaimonGrokWorkerSandboxProfile = (denyPaths: readonly string[]): string => { + const denied = [...new Set(denyPaths)].sort(); + for (const entry of denied) { + if (!path.posix.isAbsolute(entry) || path.posix.normalize(entry) !== entry || entry === "/" || entry.endsWith("/") || UNSAFE_DENY_PATH_CHARACTER.test(entry)) { + throw new SpawnfileError("compile_error", `Invalid Grok worker sandbox deny path: ${JSON.stringify(entry)}`); + } + } + return [ + `[profiles.${DAIMON_GROK_WORKER_SANDBOX_PROFILE}]`, + 'extends = "strict"', + "restrict_network = true", + `deny = [${denied.map((entry) => JSON.stringify(entry)).join(", ")}]`, + "" + ].join("\n"); +}; + +export const daimonGrokWorkerSandboxProfileSha256 = (denyPaths: readonly string[]): string => + sha256(renderDaimonGrokWorkerSandboxProfile(denyPaths)); diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 32fa4b88..90b8e20a 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -8,3 +8,4 @@ export * from "./statusProbes.js"; export * from "./types.js"; export * from "./usageLedger.js"; export * from "./usageLedgerRead.js"; +export * from "./usageRequestLedger.js"; diff --git a/src/runtime/pi/appTemplateTypes.ts b/src/runtime/pi/appTemplateTypes.ts index 9f8b9c41..3c0c7afb 100644 --- a/src/runtime/pi/appTemplateTypes.ts +++ b/src/runtime/pi/appTemplateTypes.ts @@ -34,7 +34,7 @@ export interface PiGeneratedAgent { id: string; instructions: string; model: { - auth_method: "api_key" | "claude-code" | "codex" | "none" | "unknown"; + auth_method: "api_key" | "claude-code" | "codex" | "grok" | "none" | "unknown"; name: string; provider: string; }; diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 93e58c55..ed072457 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -58,6 +58,8 @@ export interface ContainerTarget { */ engineByNodeId?: Record; envFiles?: ContainerTargetEnvFile[]; + /** Daimon only: each brokered Grok agent's declared model and reasoning effort, which select its pinned worker config. */ + grokModelByNodeId?: Record; files: EmittedFile[]; id: string; opaqueMountTargets?: string[]; diff --git a/src/runtime/usageLedger.test.ts b/src/runtime/usageLedger.test.ts index 83cdcbef..1eb51464 100644 --- a/src/runtime/usageLedger.test.ts +++ b/src/runtime/usageLedger.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest"; import { computeUsageCoverage, + dedupeUsageRecordsByTurn, DEFAULT_USAGE_SINCE, + findConflictingUsageTurns, filterUsageRecordsSince, groupUsageByAgent, groupUsageByEngine, @@ -32,6 +34,41 @@ const record = (overrides: Partial = {}): UsageRecord => ({ const line = (overrides: Partial = {}): string => JSON.stringify(record(overrides)); +describe("broker usage rows", () => { + const turn = "c".repeat(64); + + it("accepts turn, limit_reason, model, estimated_requests, and outcome from Daimon's closed vocabularies", () => { + expect(parseUsageLedgerLine(line({ estimated_requests: 3, limit_reason: "tokens", model: "grok-4.6", outcome: "failed", turn }))) + .toEqual(record({ estimated_requests: 3, limit_reason: "tokens", model: "grok-4.6", outcome: "failed", turn })); + const malformed = JSON.stringify({ ...record(), estimated_requests: 0, limit_reason: "budget", model: "gpt-5", outcome: "maybe", turn: "not-hex" }); + // A malformed optional field is dropped, never the row's spend. + expect(parseUsageLedgerLine(malformed)).toEqual(record()); + }); + + it("dedupes a replayed turn in parsing and in every aggregate, keeping unkeyed rows", () => { + const text = [line({ total: 10, turn }), line({ total: 10, turn }), line({ total: 5 }), line({ total: 5 })].join("\n"); + const parsed = parseUsageLedger(text); + expect(dedupeUsageRecordsByTurn(parsed).map((entry) => entry.total)).toEqual([10, 5, 5]); + expect(groupUsageByEngine(parsed)[0]).toMatchObject({ tokens: 20, turns: 3 }); + expect(computeUsageCoverage(parsed, 1)).toMatchObject({ conflictingTurnCount: 0, partial: false }); + const duplicated = [record({ total: 10, turn }), record({ total: 10, turn })]; + expect(dedupeUsageRecordsByTurn(duplicated)).toHaveLength(1); + expect(groupUsageByAgent(duplicated)[0]).toMatchObject({ tokens: 10, turns: 1 }); + expect(groupUsageByEngine(duplicated)[0]).toMatchObject({ tokens: 10, turns: 1 }); + expect(computeUsageCoverage([record({ complete: false, turn }), record({ complete: false, turn })], 1).incompleteRecordCount).toBe(1); + }); +}); + +describe("conflicting rows under one turn key", () => { + it("counts the first row, names the conflict, and marks coverage partial", () => { + const turn = "f".repeat(64); + const records = [record({ total: 10, turn }), record({ total: 99, turn }), record({ total: 10, turn: "0".repeat(64) }), record({ total: 10, turn: "0".repeat(64) })]; + expect(findConflictingUsageTurns(records)).toEqual([turn]); + expect(groupUsageByAgent(records)[0]).toMatchObject({ tokens: 20, turns: 2 }); + expect(computeUsageCoverage(records, 1)).toMatchObject({ agentsReporting: 1, conflictingTurnCount: 1, partial: true }); + }); +}); + describe("parseUsageLedgerLine", () => { it("parses a well-formed line", () => { expect(parseUsageLedgerLine(line())).toEqual(record()); @@ -164,6 +201,8 @@ describe("groupUsageByAgent", () => { expect(cogsworth).toEqual({ agent: "cogsworth", engine: "grok", + estimatedRequests: 0, + estimatedTurns: 0, incompleteTurns: 1, notionalUsd: 3, tokens: 300, @@ -172,6 +211,8 @@ describe("groupUsageByAgent", () => { expect(groups.find((g) => g.agent === "foreman")).toEqual({ agent: "foreman", engine: "grok", + estimatedRequests: 0, + estimatedTurns: 0, incompleteTurns: 0, notionalUsd: 3, tokens: 300, @@ -189,6 +230,8 @@ describe("groupUsageByAgent", () => { expect(groups.find((g) => g.agent === "brass")).toEqual({ agent: "brass", engine: "codex", + estimatedRequests: 0, + estimatedTurns: 0, incompleteTurns: 0, notionalUsd: 0, tokens: 0, @@ -208,6 +251,8 @@ describe("groupUsageByEngine", () => { expect(groups.find((g) => g.engine === "grok")).toEqual({ engine: "grok", + estimatedRequests: 0, + estimatedTurns: 0, incompleteTurns: 0, notionalUsd: 11.1, tokens: 3_500_000, @@ -215,6 +260,8 @@ describe("groupUsageByEngine", () => { }); expect(groups.find((g) => g.engine === "codex")).toEqual({ engine: "codex", + estimatedRequests: 0, + estimatedTurns: 0, incompleteTurns: 0, notionalUsd: 0, tokens: 0, @@ -231,6 +278,8 @@ describe("computeUsageCoverage", () => { expect(coverage).toEqual({ agentsReporting: 2, agentsTotal: 16, + conflictingTurnCount: 0, + estimatedTurnCount: 0, incompleteRecordCount: 0, partial: true, unreadableUnitCount: 0 diff --git a/src/runtime/usageLedger.ts b/src/runtime/usageLedger.ts index 079cef35..f9abb213 100644 --- a/src/runtime/usageLedger.ts +++ b/src/runtime/usageLedger.ts @@ -34,8 +34,76 @@ export interface UsageRecord { total: number; v: typeof USAGE_TURN_RECORD_VERSION; wake: string; + /** Broker rows: the sha256 turn id every reader dedupes on (a replay may re-append a sealed row). */ + turn?: string; + /** Broker rows: which per-turn limit ended the turn (`none` when none did). */ + limit_reason?: UsageLimitReason; + /** Broker rows: the declared model the broker verified the turn ran. */ + model?: string; + /** Broker rows: requests charged a conservative estimate because the provider response carried no valid usage. */ + estimated_requests?: number; + outcome?: "completed" | "failed"; } +export const USAGE_LIMIT_REASONS = ["tokens", "requests", "timeout", "none"] as const; +export type UsageLimitReason = typeof USAGE_LIMIT_REASONS[number]; +const USAGE_MODELS = ["grok-4.6", "grok-4.5", "grok-build"] as const; + +/** + * The additive broker fields of a `turn-usage.v1` row. Each is copied only when + * it is a member of Daimon's closed vocabulary; a malformed optional field is + * dropped rather than discarding the row's spend. + */ +const brokerFields = (record: Record): Partial => ({ + ...(typeof record.turn === "string" && /^[a-f0-9]{64}$/u.test(record.turn) ? { turn: record.turn } : {}), + ...((USAGE_LIMIT_REASONS as readonly unknown[]).includes(record.limit_reason) ? { limit_reason: record.limit_reason as UsageLimitReason } : {}), + ...((USAGE_MODELS as readonly unknown[]).includes(record.model) ? { model: record.model as string } : {}), + ...(Number.isSafeInteger(record.estimated_requests) && (record.estimated_requests as number) > 0 ? { estimated_requests: record.estimated_requests as number } : {}), + ...(record.outcome === "completed" || record.outcome === "failed" ? { outcome: record.outcome } : {}) +}); + +/** + * Keeps the first row of every `turn` and every row without one. + * + * Daimon's broker seals a turn's ledger bytes into the turn record and appends + * them again on replay when it cannot see the row, so two replays of one sealed + * turn can both append identical bytes. The key exists precisely so such a turn + * is never counted twice; every aggregate here must see deduplicated rows. + * Readers keep every parsed row so {@link findConflictingUsageTurns} can see + * rows that share a key but differ. + */ +export const dedupeUsageRecordsByTurn = (records: UsageRecord[]): UsageRecord[] => { + const seen = new Set(); + return records.filter((record) => { + if (record.turn === undefined) return true; + if (seen.has(record.turn)) return false; + seen.add(record.turn); + return true; + }); +}; + +const canonicalRecord = (record: UsageRecord): string => + JSON.stringify(Object.keys(record).sort().map((key) => [key, record[key as keyof UsageRecord]])); + +/** + * `turn` keys whose rows are not byte-for-byte the same record. A replay + * re-appends identical sealed bytes, so differing rows under one key mean the + * ledger is not what the broker sealed; the first row is still the one counted, + * but the window is reported PARTIAL and the keys are named. + */ +export const findConflictingUsageTurns = (records: UsageRecord[]): string[] => { + const first = new Map(); + const conflicts = new Set(); + for (const record of records) { + if (record.turn === undefined) continue; + const canonical = canonicalRecord(record); + const seen = first.get(record.turn); + if (seen === undefined) first.set(record.turn, canonical); + else if (seen !== canonical) conflicts.add(record.turn); + } + return [...conflicts].sort(); +}; + const NUMERIC_FIELDS = [ "input", "output", @@ -115,7 +183,8 @@ export const parseUsageLedgerLine = (line: string): UsageRecord | null => { output: record.output as number, total: record.total as number, v: USAGE_TURN_RECORD_VERSION, - wake: record.wake + wake: record.wake, + ...brokerFields(record) }; }; @@ -180,6 +249,9 @@ export interface UsageRosterEntry { export interface UsageAgentGroup { agent: string; engine: string | null; + /** Requests charged an estimate (no provider-reported usage); their tokens are a conservative charge, not a measurement. */ + estimatedRequests: number; + estimatedTurns: number; incompleteTurns: number; notionalUsd: number; tokens: number; @@ -189,6 +261,8 @@ export interface UsageAgentGroup { const emptyAgentGroup = (agent: string, engine: string | null): UsageAgentGroup => ({ agent, engine, + estimatedRequests: 0, + estimatedTurns: 0, incompleteTurns: 0, notionalUsd: 0, tokens: 0, @@ -206,9 +280,11 @@ export const groupUsageByAgent = ( for (const entry of roster) { byAgent.set(entry.agent, emptyAgentGroup(entry.agent, entry.engine)); } - for (const record of records) { + for (const record of dedupeUsageRecordsByTurn(records)) { const existing = byAgent.get(record.agent) ?? emptyAgentGroup(record.agent, record.engine); existing.turns += 1; + existing.estimatedRequests += record.estimated_requests ?? 0; + if (record.estimated_requests !== undefined) existing.estimatedTurns += 1; existing.tokens += record.total; existing.notionalUsd += record.notional_usd; if (!record.complete) { @@ -224,6 +300,8 @@ export const groupUsageByAgent = ( export interface UsageEngineGroup { engine: string; + estimatedRequests: number; + estimatedTurns: number; incompleteTurns: number; notionalUsd: number; tokens: number; @@ -232,6 +310,8 @@ export interface UsageEngineGroup { const emptyEngineGroup = (engine: string): UsageEngineGroup => ({ engine, + estimatedRequests: 0, + estimatedTurns: 0, incompleteTurns: 0, notionalUsd: 0, tokens: 0, @@ -249,9 +329,11 @@ export const groupUsageByEngine = ( for (const engine of engines) { byEngine.set(engine, emptyEngineGroup(engine)); } - for (const record of records) { + for (const record of dedupeUsageRecordsByTurn(records)) { const existing = byEngine.get(record.engine) ?? emptyEngineGroup(record.engine); existing.turns += 1; + existing.estimatedRequests += record.estimated_requests ?? 0; + if (record.estimated_requests !== undefined) existing.estimatedTurns += 1; existing.tokens += record.total; existing.notionalUsd += record.notional_usd; if (!record.complete) { @@ -265,6 +347,10 @@ export const groupUsageByEngine = ( export interface UsageCoverage { agentsReporting: number; agentsTotal: number; + /** `turn` keys carrying differing rows (first row counted); nonzero forces `partial`. */ + conflictingTurnCount: number; + /** Turns whose total includes an estimated charge for at least one request. */ + estimatedTurnCount: number; /** Count of `complete:false` records in the window — every count here is a * lower bound regardless of this number (see module doc / design * "Verification" — grok's `streaming-messages-json` carries no @@ -291,12 +377,16 @@ export const computeUsageCoverage = ( totalAgents: number, unreadableUnitCount = 0 ): UsageCoverage => { - const reporting = new Set(records.map((record) => record.agent)).size; + const unique = dedupeUsageRecordsByTurn(records); + const reporting = new Set(unique.map((record) => record.agent)).size; + const conflictingTurnCount = findConflictingUsageTurns(records).length; return { agentsReporting: reporting, agentsTotal: totalAgents, - incompleteRecordCount: records.filter((record) => !record.complete).length, - partial: reporting < totalAgents || unreadableUnitCount > 0, + conflictingTurnCount, + estimatedTurnCount: unique.filter((record) => record.estimated_requests !== undefined).length, + incompleteRecordCount: unique.filter((record) => !record.complete).length, + partial: reporting < totalAgents || unreadableUnitCount > 0 || conflictingTurnCount > 0, unreadableUnitCount }; }; diff --git a/src/runtime/usageLedgerRead.test.ts b/src/runtime/usageLedgerRead.test.ts index a97600f3..b9fe9dbb 100644 --- a/src/runtime/usageLedgerRead.test.ts +++ b/src/runtime/usageLedgerRead.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { USAGE_TURN_RECORD_VERSION, type UsageRecord } from "./usageLedger.js"; +import { computeUsageCoverage, groupUsageByEngine, USAGE_TURN_RECORD_VERSION, type UsageRecord } from "./usageLedger.js"; import { readUsageLedgerViaExec } from "./usageLedgerRead.js"; const record = (overrides: Partial = {}): UsageRecord => ({ @@ -28,6 +28,16 @@ describe("readUsageLedgerViaExec", () => { rotatedFilePath: "/var/lib/spawnfile/daimon/usage/usage.jsonl.1" }; + it("counts a broker turn re-appended across a rotation exactly once", async () => { + const sealed = line({ turn: "d".repeat(64) }); + const exec = async (command: string[]) => ({ stderr: "", stdout: `${sealed}\n` }); + const read = await readUsageLedgerViaExec(exec, paths); + expect(read.records).toHaveLength(2); + expect(groupUsageByEngine(read.records)[0]).toMatchObject({ tokens: 14_535, turns: 1 }); + expect(computeUsageCoverage(read.records, 1)).toMatchObject({ conflictingTurnCount: 0, partial: false }); + expect(read.unreadable).toEqual([]); + }); + it("merges both generations, rotated (older) first", async () => { const exec = async (command: string[]) => { const target = command[1]; diff --git a/src/runtime/usageLedgerRead.ts b/src/runtime/usageLedgerRead.ts index fbe067bb..b08709e3 100644 --- a/src/runtime/usageLedgerRead.ts +++ b/src/runtime/usageLedgerRead.ts @@ -131,6 +131,9 @@ export const readUsageLedgerViaExec = async ( readLedgerGeneration(exec, paths.filePath) ]); return { + // Every row is kept, across both generations: aggregates dedupe by `turn` + // (a replay can re-append after a rotation) and coverage reports rows that + // share a key but differ. records: [...parseUsageLedger(rotated.text), ...parseUsageLedger(primary.text)], unreadable: [rotated.failure, primary.failure].filter( (failure): failure is UsageLedgerReadFailure => failure !== undefined diff --git a/src/runtime/usageRequestLedger.test.ts b/src/runtime/usageRequestLedger.test.ts new file mode 100644 index 00000000..c5f0522c --- /dev/null +++ b/src/runtime/usageRequestLedger.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; + +import { parseUsageRequestLedger, parseUsageRequestLedgerLine } from "./usageRequestLedger.js"; + +const turn = "e".repeat(64); +const grokRow = (overrides: Record = {}) => JSON.stringify({ + v: "noopolis.daimon.turn-requests.v1", agent: "foreman", wake: "wake-1", engine: "grok", at: "2026-09-17T10:00:03.000Z", + turn, model: "grok-4.6", request: 0, requests: 2, input: 2_800, cached_input: 2_400, fresh_input: 400, cache_write: 0, + output: 60, total: 2_860, usage_source: "stream", started_at: "2026-09-17T10:00:01.120Z", ended_at: "2026-09-17T10:00:02.004Z", + ...overrides +}); + +describe("per-request usage stream", () => { + it("accepts Grok broker rows with timing, model, and usage source, and Codex rows without them", () => { + expect(parseUsageRequestLedgerLine(grokRow())).toMatchObject({ + ended_at: "2026-09-17T10:00:02.004Z", model: "grok-4.6", started_at: "2026-09-17T10:00:01.120Z", turn, usage_source: "stream" + }); + expect(parseUsageRequestLedgerLine(grokRow({ usage_source: "estimated" }))).toMatchObject({ usage_source: "estimated" }); + const codex = JSON.stringify({ + v: "noopolis.daimon.turn-requests.v1", agent: "desk", wake: "w", engine: "codex", at: "2026-09-17T10:00:00.000Z", thread: "t-1", + request: 1, requests: 3, input: 30_000, cached_input: 28_000, fresh_input: 2_000, cache_write: 0, output: 200, reasoning: 50, total: 30_200 + }); + const parsed = parseUsageRequestLedgerLine(codex)!; + expect(parsed).toMatchObject({ engine: "codex", reasoning: 50, thread: "t-1" }); + expect(parsed.started_at).toBeUndefined(); + expect(parsed.usage_source).toBeUndefined(); + }); + + it("rejects malformed rows rather than inventing values", () => { + for (const bad of [ + grokRow({ usage_source: "guessed" }), grokRow({ started_at: "yesterday" }), grokRow({ turn: "short" }), + grokRow({ input: -1 }), grokRow({ requests: 0 }), grokRow({ v: "noopolis.daimon.turn-usage.v1" }), "{torn" + ]) expect(parseUsageRequestLedgerLine(bad)).toBeNull(); + }); + + it("counts a replayed broker turn's request rows once", () => { + const content = [grokRow(), grokRow(), grokRow({ request: 1 }), grokRow({ request: 1 }), ""].join("\n"); + expect(parseUsageRequestLedger(content).map((row) => row.request)).toEqual([0, 1]); + }); +}); diff --git a/src/runtime/usageRequestLedger.ts b/src/runtime/usageRequestLedger.ts new file mode 100644 index 00000000..d5f4b4e7 --- /dev/null +++ b/src/runtime/usageRequestLedger.ts @@ -0,0 +1,87 @@ +/** + * Pure parser for Daimon's per-model-request stream + * (`noopolis.daimon.turn-requests.v1`, `requests.jsonl` beside the usage + * ledger). Codex rows carry a `thread`; Grok broker rows carry the broker + * `turn`, the declared `model`, and `usage_source` (`stream`, `upstream`, or + * `estimated` — a conservative charge for a response without valid usage). + * Either may carry proxy- or rollout-measured `started_at`/`ended_at`, absent + * rather than substituted when not measured. No I/O happens here. + */ + +export const USAGE_REQUEST_RECORD_VERSION = "noopolis.daimon.turn-requests.v1" as const; +export const USAGE_REQUEST_SOURCES = ["stream", "upstream", "estimated"] as const; +export type UsageRequestSource = typeof USAGE_REQUEST_SOURCES[number]; + +export interface UsageRequestRecord { + agent: string; + at: string; + cache_write: number; + cached_input: number; + ended_at?: string; + engine: string; + fresh_input: number; + input: number; + model?: string; + output: number; + reasoning?: number; + request: number; + requests: number; + started_at?: string; + thread?: string; + total: number; + turn?: string; + usage_source?: UsageRequestSource; + v: typeof USAGE_REQUEST_RECORD_VERSION; + wake: string; +} + +const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?Z$/u; +const COUNTS = ["input", "cached_input", "fresh_input", "cache_write", "output", "total"] as const; +const nonNegative = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0; +const text = (value: unknown): value is string => typeof value === "string" && value.length > 0; + +/** One row, or `null` for a blank, torn, foreign-version, or malformed line. Never throws. */ +export const parseUsageRequestLedgerLine = (line: string): UsageRequestRecord | null => { + let parsed: unknown; + try { parsed = JSON.parse(line.trim()); } catch { return null; } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; + const row = parsed as Record; + if (row.v !== USAGE_REQUEST_RECORD_VERSION || !text(row.agent) || !text(row.wake) || !text(row.engine) || !text(row.at) || Number.isNaN(Date.parse(row.at))) return null; + if (!Number.isSafeInteger(row.request) || !Number.isSafeInteger(row.requests) || (row.request as number) < 0 || (row.requests as number) < 1) return null; + if (COUNTS.some((field) => !nonNegative(row[field]))) return null; + if ((row.started_at !== undefined && !(text(row.started_at) && TIMESTAMP.test(row.started_at))) + || (row.ended_at !== undefined && !(text(row.ended_at) && TIMESTAMP.test(row.ended_at))) + || (row.usage_source !== undefined && !(USAGE_REQUEST_SOURCES as readonly unknown[]).includes(row.usage_source)) + || (row.turn !== undefined && !(typeof row.turn === "string" && /^[a-f0-9]{64}$/u.test(row.turn))) + || (row.reasoning !== undefined && !nonNegative(row.reasoning))) return null; + return { + agent: row.agent, at: row.at, cache_write: row.cache_write as number, cached_input: row.cached_input as number, + engine: row.engine, fresh_input: row.fresh_input as number, input: row.input as number, output: row.output as number, + request: row.request as number, requests: row.requests as number, total: row.total as number, + v: USAGE_REQUEST_RECORD_VERSION, wake: row.wake, + ...(row.started_at === undefined ? {} : { started_at: row.started_at as string }), + ...(row.ended_at === undefined ? {} : { ended_at: row.ended_at as string }), + ...(text(row.model) ? { model: row.model } : {}), + ...(row.reasoning === undefined ? {} : { reasoning: row.reasoning as number }), + ...(text(row.thread) ? { thread: row.thread } : {}), + ...(row.turn === undefined ? {} : { turn: row.turn as string }), + ...(row.usage_source === undefined ? {} : { usage_source: row.usage_source as UsageRequestSource }) + }; +}; + +/** + * Parses a whole stream, keeping the first row of each broker `(turn, request)` + * pair: a replayed broker turn re-appends its sealed request rows exactly as it + * re-appends its usage row. + */ +export const parseUsageRequestLedger = (content: string): UsageRequestRecord[] => { + const seen = new Set(); + return content.split("\n").map(parseUsageRequestLedgerLine).filter((row): row is UsageRequestRecord => { + if (row === null) return false; + if (row.turn === undefined) return true; + const key = `${row.turn}:${row.request}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +}; diff --git a/src/shared/daimonCodexDocker.test.ts b/src/shared/daimonCodexDocker.test.ts new file mode 100644 index 00000000..704650c9 --- /dev/null +++ b/src/shared/daimonCodexDocker.test.ts @@ -0,0 +1,57 @@ +import { createHash } from "node:crypto"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { + daimonEngineDockerSecurityArgsForConfigs, + materializeDaimonGrokSeccompProfile, + resolveDaimonEngineDockerInterop +} from "./daimonCodexDocker.js"; +import { DAIMON_GROK_SECCOMP_PROFILE_BYTES, DAIMON_GROK_SECCOMP_PROFILE_SHA256 } from "./daimonGrokSeccompProfile.js"; + +const config = (...engines: Array>): string => JSON.stringify({ + agents: engines.map((engine, index) => ({ engine, id: `agent:${index}` })), + version: "noopolis.daimon.organization-runtime.v1" +}); +const strictCodex = { codexSandbox: { mode: "workspace-write", networkAccess: false, webSearch: "disabled" }, kind: "codex" }; +const grok = { kind: "grok", model: "grok-4.6", reasoningEffort: "low" }; +const profilePath = "/tmp/spawnfile-security/seccomp.json"; + +describe("Daimon engine Docker security options", () => { + it("runs Grok under the pinned default-plus-userns seccomp profile and AppArmor unconfined, never fully unconfined", async () => { + expect(resolveDaimonEngineDockerInterop(config(grok))).toEqual({ codex: false, grok: true }); + const args = await daimonEngineDockerSecurityArgsForConfigs([config(grok, { kind: "codex" }, { kind: "agy" })], async () => profilePath); + expect(args).toEqual([`--security-opt=seccomp=${profilePath}`, "--security-opt=apparmor=unconfined"]); + expect(args).not.toContain("--security-opt=seccomp=unconfined"); + }); + + it("keeps Codex's fully unconfined options only when a strict Codex agent exists, and nothing for neither", async () => { + let materialized = 0; + const materialize = async () => { materialized += 1; return profilePath; }; + await expect(daimonEngineDockerSecurityArgsForConfigs([config(grok), config(strictCodex)], materialize)) + .resolves.toEqual(["--security-opt=seccomp=unconfined", "--security-opt=apparmor=unconfined"]); + await expect(daimonEngineDockerSecurityArgsForConfigs([config({ kind: "codex" }, { kind: "agy" })], materialize)).resolves.toEqual([]); + expect(materialized).toBe(0); + await expect(daimonEngineDockerSecurityArgsForConfigs([config(grok)], async () => "relative.json")).rejects.toThrow(/absolute/u); + }); + + it("materializes the pinned seccomp profile byte-for-byte", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "spawnfile-grok-seccomp-")); + try { + const written = await materializeDaimonGrokSeccompProfile(path.join(directory, "security")); + expect(path.isAbsolute(written)).toBe(true); + const bytes = await readFile(written); + expect(createHash("sha256").update(bytes).digest("hex")).toBe(DAIMON_GROK_SECCOMP_PROFILE_SHA256); + expect(createHash("sha256").update(DAIMON_GROK_SECCOMP_PROFILE_BYTES).digest("hex")).toBe(DAIMON_GROK_SECCOMP_PROFILE_SHA256); + const profile = JSON.parse(bytes.toString("utf8")) as { defaultAction: string; syscalls: Array<{ action: string; names: string[] }> }; + expect(profile.defaultAction).toBe("SCMP_ACT_ERRNO"); + expect(profile.syscalls.some((rule) => rule.action === "SCMP_ACT_ALLOW" + && ["clone", "clone3", "unshare", "mount", "umount2", "pivot_root", "setns"].every((name) => rule.names.includes(name)))).toBe(true); + } finally { + await rm(directory, { force: true, recursive: true }); + } + }); +}); diff --git a/src/shared/daimonCodexDocker.ts b/src/shared/daimonCodexDocker.ts index 778059f0..5004807a 100644 --- a/src/shared/daimonCodexDocker.ts +++ b/src/shared/daimonCodexDocker.ts @@ -1,3 +1,12 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; + +import { + DAIMON_GROK_SECCOMP_PROFILE_BYTES, + DAIMON_GROK_SECCOMP_PROFILE_FILE_NAME, + DAIMON_GROK_SECCOMP_PROFILE_SHA256 +} from "./daimonGrokSeccompProfile.js"; import { SpawnfileError } from "./errors.js"; export const DAIMON_DOCKER_RUNTIME_SECURITY_ARGS = [ @@ -56,7 +65,14 @@ const hasExactCodexStrictSandbox = (engine: Record): boolean => return true; }; -export const configRequiresCodexNativeSandboxDockerInterop = (source: string): boolean => { +export interface DaimonEngineDockerInterop { + /** A strict Codex agent needs seccomp and AppArmor fully unconfined for its own bubblewrap sandbox. */ + codex: boolean; + /** A brokered Grok worker needs bubblewrap's namespace syscalls: the pinned seccomp profile plus AppArmor unconfined. */ + grok: boolean; +} + +export const resolveDaimonEngineDockerInterop = (source: string): DaimonEngineDockerInterop => { let parsed: unknown; try { parsed = JSON.parse(source); @@ -78,7 +94,7 @@ export const configRequiresCodexNativeSandboxDockerInterop = (source: string): b "Daimon generated organization config is not a supported v1/v2 contract" ); } - let requiresInterop = false; + const interop = { codex: false, grok: false }; for (const agent of parsed.agents) { if (!isRecord(agent) || !isRecord(agent.engine)) { throw new SpawnfileError( @@ -93,19 +109,59 @@ export const configRequiresCodexNativeSandboxDockerInterop = (source: string): b "Daimon generated organization config has an unsupported agent engine" ); } - if (kind === "codex" && hasExactCodexStrictSandbox(agent.engine)) { - requiresInterop = true; - } + if (kind === "codex" && hasExactCodexStrictSandbox(agent.engine)) interop.codex = true; + if (kind === "grok") interop.grok = true; } - return requiresInterop; + return interop; }; -export const codexNativeSandboxDockerSecurityArgsForConfigs = (sources: string[]): string[] => { - let requiresInterop = false; - for (const source of sources) { - if (configRequiresCodexNativeSandboxDockerInterop(source)) { - requiresInterop = true; - } +export const configRequiresCodexNativeSandboxDockerInterop = (source: string): boolean => + resolveDaimonEngineDockerInterop(source).codex; + +export const DAIMON_GROK_HOST_USERNS_PREREQUISITE = + "kernel.apparmor_restrict_unprivileged_userns=0" as const; + +/** + * Writes the pinned Grok seccomp profile into `directory` (content-addressed, + * verified after writing) and returns its absolute path. Docker reads a + * `seccomp=` option on the client at `docker run` and stores the profile + * in the container config, so the file only has to exist for that call. + */ +export const materializeDaimonGrokSeccompProfile = async (directory: string): Promise => { + const target = path.resolve(directory, `${DAIMON_GROK_SECCOMP_PROFILE_SHA256.slice(0, 16)}-${DAIMON_GROK_SECCOMP_PROFILE_FILE_NAME}`); + await mkdir(path.dirname(target), { recursive: true }); + await writeFile(target, DAIMON_GROK_SECCOMP_PROFILE_BYTES, { mode: 0o644 }); + const written = await readFile(target); + if (createHash("sha256").update(written).digest("hex") !== DAIMON_GROK_SECCOMP_PROFILE_SHA256) { + throw new SpawnfileError("runtime_error", "Pinned Grok seccomp profile did not materialize byte-for-byte"); } - return requiresInterop ? [...DAIMON_CODEX_NATIVE_SANDBOX_DOCKER_SECURITY_OPTS] : []; + return target; +}; + +/** + * Docker security options for a Daimon container, by the engines its configs run. + * + * Codex's strict native sandbox needs seccomp and AppArmor fully unconfined, and + * that superset also lets Grok's bubblewrap run, so it wins whenever a strict + * Codex agent is present. Otherwise a Grok agent gets Docker's default seccomp + * profile plus bubblewrap's seven namespace syscalls (pinned bytes) and AppArmor + * unconfined — the narrowest combination under which Grok 1.0.34 starts. Grok + * additionally needs the Docker host to allow unprivileged user namespaces + * (`kernel.apparmor_restrict_unprivileged_userns=0`); the container entrypoint + * checks that before the broker starts. + */ +export const daimonEngineDockerSecurityArgsForConfigs = ( + sources: string[], + grokSeccompProfilePath: () => Promise +): Promise => { + const interop = sources.map(resolveDaimonEngineDockerInterop); + if (interop.some((entry) => entry.codex)) return Promise.resolve([...DAIMON_CODEX_NATIVE_SANDBOX_DOCKER_SECURITY_OPTS]); + if (!interop.some((entry) => entry.grok)) return Promise.resolve([]); + return grokSeccompProfilePath().then((profilePath) => { + if (!path.isAbsolute(profilePath)) throw new SpawnfileError("runtime_error", "Grok seccomp profile path must be absolute"); + return [`--security-opt=seccomp=${profilePath}`, "--security-opt=apparmor=unconfined"]; + }); }; + +export const codexNativeSandboxDockerSecurityArgsForConfigs = (sources: string[]): string[] => + sources.some(configRequiresCodexNativeSandboxDockerInterop) ? [...DAIMON_CODEX_NATIVE_SANDBOX_DOCKER_SECURITY_OPTS] : []; diff --git a/src/shared/daimonGrokSeccompProfile.ts b/src/shared/daimonGrokSeccompProfile.ts new file mode 100644 index 00000000..7fdf5a84 --- /dev/null +++ b/src/shared/daimonGrokSeccompProfile.ts @@ -0,0 +1,11 @@ +/* v8 ignore file -- pinned data module */ +/** + * Docker's default seccomp profile plus the seven namespace syscalls bubblewrap + * needs without CAP_SYS_ADMIN (clone, clone3, unshare, mount, umount2, pivot_root, + * setns): the narrowest profile under which Grok 1.0.34's sandbox starts + * (Noopolis P0 container evidence, 2026-09-17). Bytes are pinned by sha256; Daimon's + * Grok broker projection and slot preflight receipt bind the same digest. + */ +export const DAIMON_GROK_SECCOMP_PROFILE_FILE_NAME = "seccomp-default-plus-userns.json" as const; +export const DAIMON_GROK_SECCOMP_PROFILE_SHA256 = "9666074d2d6b1a261a410598b9ce1714d9060a6ef4c9c1d1cfb6f8915d7e842a" as const; +export const DAIMON_GROK_SECCOMP_PROFILE_BYTES = "{\n \"defaultAction\": \"SCMP_ACT_ERRNO\",\n \"defaultErrnoRet\": 1,\n \"archMap\": [\n {\n \"architecture\": \"SCMP_ARCH_X86_64\",\n \"subArchitectures\": [\n \"SCMP_ARCH_X86\",\n \"SCMP_ARCH_X32\"\n ]\n },\n {\n \"architecture\": \"SCMP_ARCH_AARCH64\",\n \"subArchitectures\": [\n \"SCMP_ARCH_ARM\"\n ]\n },\n {\n \"architecture\": \"SCMP_ARCH_MIPS64\",\n \"subArchitectures\": [\n \"SCMP_ARCH_MIPS\",\n \"SCMP_ARCH_MIPS64N32\"\n ]\n },\n {\n \"architecture\": \"SCMP_ARCH_MIPS64N32\",\n \"subArchitectures\": [\n \"SCMP_ARCH_MIPS\",\n \"SCMP_ARCH_MIPS64\"\n ]\n },\n {\n \"architecture\": \"SCMP_ARCH_MIPSEL64\",\n \"subArchitectures\": [\n \"SCMP_ARCH_MIPSEL\",\n \"SCMP_ARCH_MIPSEL64N32\"\n ]\n },\n {\n \"architecture\": \"SCMP_ARCH_MIPSEL64N32\",\n \"subArchitectures\": [\n \"SCMP_ARCH_MIPSEL\",\n \"SCMP_ARCH_MIPSEL64\"\n ]\n },\n {\n \"architecture\": \"SCMP_ARCH_S390X\",\n \"subArchitectures\": [\n \"SCMP_ARCH_S390\"\n ]\n },\n {\n \"architecture\": \"SCMP_ARCH_RISCV64\",\n \"subArchitectures\": null\n },\n {\n \"architecture\": \"SCMP_ARCH_LOONGARCH64\",\n \"subArchitectures\": null\n }\n ],\n \"syscalls\": [\n {\n \"names\": [\n \"clone\",\n \"clone3\",\n \"unshare\",\n \"mount\",\n \"umount2\",\n \"pivot_root\",\n \"setns\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"comment\": \"P0 spike: userns/mount syscalls bubblewrap needs, without CAP_SYS_ADMIN\"\n },\n {\n \"names\": [\n \"accept\",\n \"accept4\",\n \"access\",\n \"adjtimex\",\n \"alarm\",\n \"bind\",\n \"brk\",\n \"cachestat\",\n \"capget\",\n \"capset\",\n \"chdir\",\n \"chmod\",\n \"chown\",\n \"chown32\",\n \"clock_adjtime\",\n \"clock_adjtime64\",\n \"clock_getres\",\n \"clock_getres_time64\",\n \"clock_gettime\",\n \"clock_gettime64\",\n \"clock_nanosleep\",\n \"clock_nanosleep_time64\",\n \"close\",\n \"close_range\",\n \"connect\",\n \"copy_file_range\",\n \"creat\",\n \"dup\",\n \"dup2\",\n \"dup3\",\n \"epoll_create\",\n \"epoll_create1\",\n \"epoll_ctl\",\n \"epoll_ctl_old\",\n \"epoll_pwait\",\n \"epoll_pwait2\",\n \"epoll_wait\",\n \"epoll_wait_old\",\n \"eventfd\",\n \"eventfd2\",\n \"execve\",\n \"execveat\",\n \"exit\",\n \"exit_group\",\n \"faccessat\",\n \"faccessat2\",\n \"fadvise64\",\n \"fadvise64_64\",\n \"fallocate\",\n \"fanotify_mark\",\n \"fchdir\",\n \"fchmod\",\n \"fchmodat\",\n \"fchmodat2\",\n \"fchown\",\n \"fchown32\",\n \"fchownat\",\n \"fcntl\",\n \"fcntl64\",\n \"fdatasync\",\n \"fgetxattr\",\n \"flistxattr\",\n \"flock\",\n \"fork\",\n \"fremovexattr\",\n \"fsetxattr\",\n \"fstat\",\n \"fstat64\",\n \"fstatat64\",\n \"fstatfs\",\n \"fstatfs64\",\n \"fsync\",\n \"ftruncate\",\n \"ftruncate64\",\n \"futex\",\n \"futex_requeue\",\n \"futex_time64\",\n \"futex_wait\",\n \"futex_waitv\",\n \"futex_wake\",\n \"futimesat\",\n \"getcpu\",\n \"getcwd\",\n \"getdents\",\n \"getdents64\",\n \"getegid\",\n \"getegid32\",\n \"geteuid\",\n \"geteuid32\",\n \"getgid\",\n \"getgid32\",\n \"getgroups\",\n \"getgroups32\",\n \"getitimer\",\n \"getpeername\",\n \"getpgid\",\n \"getpgrp\",\n \"getpid\",\n \"getppid\",\n \"getpriority\",\n \"getrandom\",\n \"getresgid\",\n \"getresgid32\",\n \"getresuid\",\n \"getresuid32\",\n \"getrlimit\",\n \"get_robust_list\",\n \"getrusage\",\n \"getsid\",\n \"getsockname\",\n \"getsockopt\",\n \"get_thread_area\",\n \"gettid\",\n \"gettimeofday\",\n \"getuid\",\n \"getuid32\",\n \"getxattr\",\n \"getxattrat\",\n \"inotify_add_watch\",\n \"inotify_init\",\n \"inotify_init1\",\n \"inotify_rm_watch\",\n \"io_cancel\",\n \"ioctl\",\n \"io_destroy\",\n \"io_getevents\",\n \"io_pgetevents\",\n \"io_pgetevents_time64\",\n \"ioprio_get\",\n \"ioprio_set\",\n \"io_setup\",\n \"io_submit\",\n \"ipc\",\n \"kill\",\n \"landlock_add_rule\",\n \"landlock_create_ruleset\",\n \"landlock_restrict_self\",\n \"lchown\",\n \"lchown32\",\n \"lgetxattr\",\n \"link\",\n \"linkat\",\n \"listen\",\n \"listmount\",\n \"listxattr\",\n \"listxattrat\",\n \"llistxattr\",\n \"_llseek\",\n \"lremovexattr\",\n \"lseek\",\n \"lsetxattr\",\n \"lstat\",\n \"lstat64\",\n \"madvise\",\n \"map_shadow_stack\",\n \"membarrier\",\n \"memfd_create\",\n \"memfd_secret\",\n \"mincore\",\n \"mkdir\",\n \"mkdirat\",\n \"mknod\",\n \"mknodat\",\n \"mlock\",\n \"mlock2\",\n \"mlockall\",\n \"mmap\",\n \"mmap2\",\n \"mprotect\",\n \"mq_getsetattr\",\n \"mq_notify\",\n \"mq_open\",\n \"mq_timedreceive\",\n \"mq_timedreceive_time64\",\n \"mq_timedsend\",\n \"mq_timedsend_time64\",\n \"mq_unlink\",\n \"mremap\",\n \"mseal\",\n \"msgctl\",\n \"msgget\",\n \"msgrcv\",\n \"msgsnd\",\n \"msync\",\n \"munlock\",\n \"munlockall\",\n \"munmap\",\n \"name_to_handle_at\",\n \"nanosleep\",\n \"newfstatat\",\n \"_newselect\",\n \"open\",\n \"openat\",\n \"openat2\",\n \"pause\",\n \"pidfd_open\",\n \"pidfd_send_signal\",\n \"pipe\",\n \"pipe2\",\n \"pkey_alloc\",\n \"pkey_free\",\n \"pkey_mprotect\",\n \"poll\",\n \"ppoll\",\n \"ppoll_time64\",\n \"prctl\",\n \"pread64\",\n \"preadv\",\n \"preadv2\",\n \"prlimit64\",\n \"process_mrelease\",\n \"pselect6\",\n \"pselect6_time64\",\n \"pwrite64\",\n \"pwritev\",\n \"pwritev2\",\n \"read\",\n \"readahead\",\n \"readlink\",\n \"readlinkat\",\n \"readv\",\n \"recv\",\n \"recvfrom\",\n \"recvmmsg\",\n \"recvmmsg_time64\",\n \"recvmsg\",\n \"remap_file_pages\",\n \"removexattr\",\n \"removexattrat\",\n \"rename\",\n \"renameat\",\n \"renameat2\",\n \"restart_syscall\",\n \"riscv_hwprobe\",\n \"rmdir\",\n \"rseq\",\n \"rt_sigaction\",\n \"rt_sigpending\",\n \"rt_sigprocmask\",\n \"rt_sigqueueinfo\",\n \"rt_sigreturn\",\n \"rt_sigsuspend\",\n \"rt_sigtimedwait\",\n \"rt_sigtimedwait_time64\",\n \"rt_tgsigqueueinfo\",\n \"sched_getaffinity\",\n \"sched_getattr\",\n \"sched_getparam\",\n \"sched_get_priority_max\",\n \"sched_get_priority_min\",\n \"sched_getscheduler\",\n \"sched_rr_get_interval\",\n \"sched_rr_get_interval_time64\",\n \"sched_setaffinity\",\n \"sched_setattr\",\n \"sched_setparam\",\n \"sched_setscheduler\",\n \"sched_yield\",\n \"seccomp\",\n \"select\",\n \"semctl\",\n \"semget\",\n \"semop\",\n \"semtimedop\",\n \"semtimedop_time64\",\n \"send\",\n \"sendfile\",\n \"sendfile64\",\n \"sendmmsg\",\n \"sendmsg\",\n \"sendto\",\n \"setfsgid\",\n \"setfsgid32\",\n \"setfsuid\",\n \"setfsuid32\",\n \"setgid\",\n \"setgid32\",\n \"setgroups\",\n \"setgroups32\",\n \"setitimer\",\n \"setpgid\",\n \"setpriority\",\n \"setregid\",\n \"setregid32\",\n \"setresgid\",\n \"setresgid32\",\n \"setresuid\",\n \"setresuid32\",\n \"setreuid\",\n \"setreuid32\",\n \"setrlimit\",\n \"set_robust_list\",\n \"setsid\",\n \"setsockopt\",\n \"set_thread_area\",\n \"set_tid_address\",\n \"setuid\",\n \"setuid32\",\n \"setxattr\",\n \"setxattrat\",\n \"shmat\",\n \"shmctl\",\n \"shmdt\",\n \"shmget\",\n \"shutdown\",\n \"sigaltstack\",\n \"signalfd\",\n \"signalfd4\",\n \"sigprocmask\",\n \"sigreturn\",\n \"socketcall\",\n \"socketpair\",\n \"splice\",\n \"stat\",\n \"stat64\",\n \"statfs\",\n \"statfs64\",\n \"statmount\",\n \"statx\",\n \"symlink\",\n \"symlinkat\",\n \"sync\",\n \"sync_file_range\",\n \"syncfs\",\n \"sysinfo\",\n \"tee\",\n \"tgkill\",\n \"time\",\n \"timer_create\",\n \"timer_delete\",\n \"timer_getoverrun\",\n \"timer_gettime\",\n \"timer_gettime64\",\n \"timer_settime\",\n \"timer_settime64\",\n \"timerfd_create\",\n \"timerfd_gettime\",\n \"timerfd_gettime64\",\n \"timerfd_settime\",\n \"timerfd_settime64\",\n \"times\",\n \"tkill\",\n \"truncate\",\n \"truncate64\",\n \"ugetrlimit\",\n \"umask\",\n \"uname\",\n \"unlink\",\n \"unlinkat\",\n \"uretprobe\",\n \"utime\",\n \"utimensat\",\n \"utimensat_time64\",\n \"utimes\",\n \"vfork\",\n \"vmsplice\",\n \"wait4\",\n \"waitid\",\n \"waitpid\",\n \"write\",\n \"writev\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\"\n },\n {\n \"names\": [\n \"process_vm_readv\",\n \"process_vm_writev\",\n \"ptrace\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"minKernel\": \"4.8\"\n }\n },\n {\n \"names\": [\n \"socket\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 38,\n \"op\": \"SCMP_CMP_LT\"\n }\n ]\n },\n {\n \"names\": [\n \"socket\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 39,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"socket\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 41,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"socket\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 42,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"socket\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 43,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"socket\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 44,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"socket\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 45,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"personality\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 0,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"personality\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 8,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"personality\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 131072,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"personality\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 131080,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"personality\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 4294967295,\n \"op\": \"SCMP_CMP_EQ\"\n }\n ]\n },\n {\n \"names\": [\n \"sync_file_range2\",\n \"swapcontext\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"arches\": [\n \"ppc64le\"\n ]\n }\n },\n {\n \"names\": [\n \"arm_fadvise64_64\",\n \"arm_sync_file_range\",\n \"sync_file_range2\",\n \"breakpoint\",\n \"cacheflush\",\n \"set_tls\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"arches\": [\n \"arm\",\n \"arm64\"\n ]\n }\n },\n {\n \"names\": [\n \"arch_prctl\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"arches\": [\n \"amd64\",\n \"x32\"\n ]\n }\n },\n {\n \"names\": [\n \"modify_ldt\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"arches\": [\n \"amd64\",\n \"x32\",\n \"x86\"\n ]\n }\n },\n {\n \"names\": [\n \"s390_pci_mmio_read\",\n \"s390_pci_mmio_write\",\n \"s390_runtime_instr\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"arches\": [\n \"s390\",\n \"s390x\"\n ]\n }\n },\n {\n \"names\": [\n \"riscv_flush_icache\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"arches\": [\n \"riscv64\"\n ]\n }\n },\n {\n \"names\": [\n \"open_by_handle_at\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_DAC_READ_SEARCH\"\n ]\n }\n },\n {\n \"names\": [\n \"bpf\",\n \"clone\",\n \"clone3\",\n \"fanotify_init\",\n \"fsconfig\",\n \"fsmount\",\n \"fsopen\",\n \"fspick\",\n \"lookup_dcookie\",\n \"lsm_get_self_attr\",\n \"lsm_list_modules\",\n \"lsm_set_self_attr\",\n \"mount\",\n \"mount_setattr\",\n \"move_mount\",\n \"open_tree\",\n \"perf_event_open\",\n \"quotactl\",\n \"quotactl_fd\",\n \"setdomainname\",\n \"sethostname\",\n \"setns\",\n \"syslog\",\n \"umount\",\n \"umount2\",\n \"unshare\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_ADMIN\"\n ]\n }\n },\n {\n \"names\": [\n \"clone\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 0,\n \"value\": 2114060288,\n \"op\": \"SCMP_CMP_MASKED_EQ\"\n }\n ],\n \"excludes\": {\n \"caps\": [\n \"CAP_SYS_ADMIN\"\n ],\n \"arches\": [\n \"s390\",\n \"s390x\"\n ]\n }\n },\n {\n \"names\": [\n \"clone\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"args\": [\n {\n \"index\": 1,\n \"value\": 2114060288,\n \"op\": \"SCMP_CMP_MASKED_EQ\"\n }\n ],\n \"comment\": \"s390 parameter ordering for clone is different\",\n \"includes\": {\n \"arches\": [\n \"s390\",\n \"s390x\"\n ]\n },\n \"excludes\": {\n \"caps\": [\n \"CAP_SYS_ADMIN\"\n ]\n }\n },\n {\n \"names\": [\n \"reboot\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_BOOT\"\n ]\n }\n },\n {\n \"names\": [\n \"chroot\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_CHROOT\"\n ]\n }\n },\n {\n \"names\": [\n \"delete_module\",\n \"init_module\",\n \"finit_module\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_MODULE\"\n ]\n }\n },\n {\n \"names\": [\n \"acct\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_PACCT\"\n ]\n }\n },\n {\n \"names\": [\n \"kcmp\",\n \"pidfd_getfd\",\n \"process_madvise\",\n \"process_vm_readv\",\n \"process_vm_writev\",\n \"ptrace\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_PTRACE\"\n ]\n }\n },\n {\n \"names\": [\n \"iopl\",\n \"ioperm\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_RAWIO\"\n ]\n }\n },\n {\n \"names\": [\n \"settimeofday\",\n \"stime\",\n \"clock_settime\",\n \"clock_settime64\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_TIME\"\n ]\n }\n },\n {\n \"names\": [\n \"vhangup\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_TTY_CONFIG\"\n ]\n }\n },\n {\n \"names\": [\n \"get_mempolicy\",\n \"mbind\",\n \"set_mempolicy\",\n \"set_mempolicy_home_node\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYS_NICE\"\n ]\n }\n },\n {\n \"names\": [\n \"syslog\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_SYSLOG\"\n ]\n }\n },\n {\n \"names\": [\n \"bpf\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_BPF\"\n ]\n }\n },\n {\n \"names\": [\n \"perf_event_open\"\n ],\n \"action\": \"SCMP_ACT_ALLOW\",\n \"includes\": {\n \"caps\": [\n \"CAP_PERFMON\"\n ]\n }\n }\n ]\n}"; diff --git a/src/shared/types.ts b/src/shared/types.ts index 3013f75b..c1b7d781 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,6 +1,6 @@ export type JsonPrimitive = boolean | null | number | string; export type JsonValue = JsonObject | JsonPrimitive | JsonValue[]; -export type ModelAuthMethod = "api_key" | "claude-code" | "codex" | "none"; +export type ModelAuthMethod = "api_key" | "claude-code" | "codex" | "grok" | "none"; export type ModelEndpointCompatibility = "anthropic" | "openai"; export type RuntimeLifecycleStatus = "active" | "deprecated" | "exploratory";