Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ Codex uses a private `.codex/auth.json` under each agent runtime home. Optional
Codex config fields are `model`, `reasoningEffort`, and the fixed no-network
workspace sandbox policy.

Every Codex wake enables and requires Daimon's per-wake MCP server, including
standalone and strict sandbox launches. If the server cannot initialize, Codex
fails the wake before model work instead of continuing with only built-in tools.
The startup error follows the existing bounded, redacted engine failure path.

The production Grok path uses an external Daimon engine broker with one durable
subscription credential authority. Agent workers receive scoped capabilities;
the broker owns refresh and stale-credential recovery. The runtime checks broker
Expand Down
22 changes: 15 additions & 7 deletions src/pi/cliEngineSpawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,19 @@ test("Codex output, sandbox, config, and cwd boundaries reject caller overrides"
assert.equal(args.includes("--json"), true);
});

test("codex argv is byte-identical to before model selection existed when model/reasoningEffort are absent", () => {
const before = ["exec", "--sandbox", "danger-full-access", "--skip-git-repo-check", "--color", "never", "--json", "-C", "/workspace",
"-c", "mcp_servers.daimon.url=http://127.0.0.1:1/mcp", "-"];
const after = renderCodexArgs({ commandArgs: [] }, "/workspace", "http://127.0.0.1:1/mcp");
assert.deepEqual(after, before);
for (const mode of ["standalone", "strict"] as const) {
test(`Codex ${mode} launch requires the per-wake MCP server`, () => {
const args = renderCodexArgs(mode === "standalone" ? {} : {
codexSandbox: { mode: "workspace-write", networkAccess: false, webSearch: "disabled" }
}, "/workspace", "http://127.0.0.1:1/mcp");
assert.deepEqual(args.slice(-5), ["-c", "mcp_servers.daimon.enabled=true", "-c", "mcp_servers.daimon.required=true", "-"]);
});
}

test("codex argv preserves model defaults while requiring the per-wake MCP server", () => {
const args = renderCodexArgs({ commandArgs: [] }, "/workspace", "http://127.0.0.1:1/mcp");
assert.deepEqual(args, ["exec", "--sandbox", "danger-full-access", "--skip-git-repo-check", "--color", "never", "--json", "-C", "/workspace",
"-c", "mcp_servers.daimon.url=http://127.0.0.1:1/mcp", "-c", "mcp_servers.daimon.enabled=true", "-c", "mcp_servers.daimon.required=true", "-"]);
});

test("codex strict policy is rendered as per-turn Daimon-owned CLI config", () => {
Expand Down Expand Up @@ -125,13 +133,13 @@ test("Codex permission profile lets protected denies override readable paths", (
test("codex argv renders -m for a pinned model and leaves everything else untouched", () => {
const args = renderCodexArgs({ commandArgs: [], model: "gpt-5-codex" }, "/workspace", "http://127.0.0.1:1/mcp");
assert.deepEqual(args, ["exec", "--sandbox", "danger-full-access", "--skip-git-repo-check", "--model=gpt-5-codex", "--color", "never", "--json", "-C", "/workspace",
"-c", "mcp_servers.daimon.url=http://127.0.0.1:1/mcp", "-"]);
"-c", "mcp_servers.daimon.url=http://127.0.0.1:1/mcp", "-c", "mcp_servers.daimon.enabled=true", "-c", "mcp_servers.daimon.required=true", "-"]);
});

test("codex argv renders both model and reasoningEffort together in stable order", () => {
const args = renderCodexArgs({ commandArgs: [], model: "gpt-5-codex", reasoningEffort: "xhigh" }, "/workspace", "http://127.0.0.1:1/mcp");
assert.deepEqual(args, ["exec", "--sandbox", "danger-full-access", "--skip-git-repo-check", "--model=gpt-5-codex", "-c", "model_reasoning_effort=xhigh", "--color", "never", "--json", "-C", "/workspace",
"-c", "mcp_servers.daimon.url=http://127.0.0.1:1/mcp", "-"]);
"-c", "mcp_servers.daimon.url=http://127.0.0.1:1/mcp", "-c", "mcp_servers.daimon.enabled=true", "-c", "mcp_servers.daimon.required=true", "-"]);
});

test("codex argv renders reasoningEffort alone without a model flag", () => {
Expand Down
11 changes: 8 additions & 3 deletions src/pi/cliEngineSpawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ export const renderGrokSandboxArgs = (
* `--flag=value` argv token (never a bare flag followed by a separate value
* token), so a value that itself looks like a flag (e.g. `--sandbox`) can
* never be parsed as a second, independent argument — the same shape already
* used below for `mcp_servers.daimon.url=`. Omitting both fields renders the
* exact argv Daimon produced before model selection existed.
* used below for `mcp_servers.daimon.url=`. Omitting both fields leaves Codex's
* model defaults unchanged.
*
* The per-wake MCP server is always enabled and required. Otherwise Codex can
* continue a normal turn with only built-in tools after MCP startup fails.
*/
export const renderCodexArgs = (
options: Pick<CliEngineOptions, "commandArgs" | "model" | "reasoningEffort" | "codexSandbox" | "codexSandboxProtectedPaths" | "codexSandboxReadablePaths">,
Expand Down Expand Up @@ -51,7 +54,9 @@ export const renderCodexArgs = (
...(options.model === undefined ? [] : [`--model=${options.model}`]),
...(options.reasoningEffort === undefined ? [] : ["-c", `model_reasoning_effort=${options.reasoningEffort}`]),
"--color", "never", "--json", "-C", cwd,
"-c", `mcp_servers.daimon.url=${endpoint}`, "-"];
"-c", `mcp_servers.daimon.url=${endpoint}`,
"-c", "mcp_servers.daimon.enabled=true",
"-c", "mcp_servers.daimon.required=true", "-"];
};

export const renderCodexPermissionProfile = (
Expand Down
50 changes: 50 additions & 0 deletions src/pi/cliSessionRequiredMcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";

import { createCliSessionFactory } from "./cliSession.js";

for (const mode of ["standalone", "strict"] as const) {
test(`Codex ${mode} MCP startup failure rejects the wake without publishing or inventing usage`, async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "daimon-required-mcp-"));
const engine = path.join(root, "codex-stub.mjs");
const secret = "required-mcp-diagnostic-secret";
// Simulate Codex's optional-server continuation and required-server exit.
// The generated launch config must select the failure before a normal turn.
await writeFile(engine, [
"for await (const chunk of process.stdin) {}",
"if (process.argv.includes('mcp_servers.daimon.enabled=true') && process.argv.includes('mcp_servers.daimon.required=true')) {",
` process.stderr.write(${JSON.stringify(`required MCP server daimon failed startup: ${secret}`)});`,
" process.exit(1);",
"}",
"process.stdout.write(JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'continued without tools' } }) + '\\n');",
"process.stdout.write(JSON.stringify({ type: 'turn.completed', usage: { input_tokens: 10, cached_input_tokens: 0, output_tokens: 1 } }) + '\\n');"
].join("\n"));
const published: unknown[] = [];
const usage: unknown[] = [];
try {
const { session } = await createCliSessionFactory({
command: process.execPath,
commandArgs: [engine],
engine: "codex",
timeoutMs: 10_000,
credentialSecretValues: async () => [secret],
onTurnUsage: async (value) => { usage.push(value); },
...(mode === "strict" ? { codexSandbox: { mode: "workspace-write", networkAccess: false, webSearch: "disabled" } as const } : {})
})({ cwd: root });
session.subscribe((event) => { published.push(event); });
try {
await assert.rejects(session.prompt("wake"), (error: unknown) => {
assert.ok(error instanceof Error);
assert.match(error.message, /CLI engine exited 1: required MCP server daimon failed startup:/u);
assert.equal(error.message.includes(secret), false);
return true;
});
assert.deepEqual(published, []);
assert.deepEqual(usage, []);
} finally { await session.disposeAsync?.(); }
} finally { await rm(root, { recursive: true, force: true }); }
});
}
Loading