akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
151 lines (150 loc) • 7.96 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* OpenAI Codex CLI agent command builder (P2, plan §"The adapter contract" /
* §"Capability matrix").
*
* Translates a platform-agnostic {@link AgentDispatchRequest} into the exact
* headless argv the `codex` CLI expects:
*
* codex exec --sandbox workspace-write [--model <m>] --json [--output-schema <file>] -- "<prompt>"
*
* Capability-matrix facts this builder encodes (July 2026 research):
* - Headless invocation is the `exec` subcommand (`codex exec "<p>"`), not a
* flag. The built-in codex profiles carry `args: []`, so the builder
* prepends `exec` itself; a user profile that already pins `exec` as its
* first arg is not doubled.
* - `--json` switches stdout to a JSONL event stream — the input contract of
* `./result-extractor.ts`. Always emitted, mirroring how the Claude builder
* always emits `--print`: dispatch is the captured, non-interactive path.
* - Codex is the NATIVE-SCHEMA tier (plan §"Structured-output
* normalization"): `req.schema` is written to a file and passed via
* `--output-schema <file>`. The file is named by the hash of the schema
* and lives in akm's cache dir, so a schema is written once however many
* units dispatch with it: `BuiltCommand` has no post-run hook, and the
* spawned process reads the file after `build()` returns, so a file per
* dispatch could only leak (#1051). The engine still validates the output
* defensively (the constrained output is trusted but verified).
* - `codex exec` has no system-prompt flag; `req.systemPrompt` is folded
* into the prompt payload (system text first, blank line, then the task),
* after the `--` end-of-options separator so it can never be parsed as a
* flag.
* - Tool policy is omitted, but the builder DOES inject `--sandbox
* workspace-write` so dispatched units can write to their working
* directory. `codex exec` defaults to a read-only sandbox that silently
* blocks file writes; without this flag the unit returns "workspace is
* read-only" and the engine marks the step complete with no real mutation.
* `--ask-for-approval` is NOT injected — that flag only exists on the
* interactive `codex` command, not on `codex exec`, and exec mode is
* already non-approval-blocking. Profile-supplied `--sandbox` flags
* (long or short form) are preserved (not duplicated).
* - Resume is the `codex exec resume <id>` SUBCOMMAND, not a flag, so it is
* not expressible through `AgentDispatchRequest` (which has no session
* field yet); {@link codexResumeArgs} exposes the argv prefix for the
* integration task that wires session-id reuse from `workflow_run_units`.
* - The request's inference is not translated: the shared lowering reports
* each field as untranslated. codex would take `reasoningEffort` as
* `-c model_reasoning_effort=<v>`, which is left to the integration task.
*
* Registered: `codexBuilder` is `CodexHarness.agentBuilder` (`./index.ts`),
* one of the ten harnesses `HARNESS_REGISTRY` constructs
* (`harnesses/index.ts`); `agent/builders.ts` derives `BUILTIN_BUILDERS` from
* that registry, so this builder is reachable under the `"codex"` platform
* name without any further wiring.
*/
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { writeFileAtomic } from "../../../core/common.js";
import { getCacheDir } from "../../../core/paths.js";
import { sha256Hex } from "../../../runtime.js";
import { resolveDispatchModel } from "../../agent/builder-shared.js";
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
/**
* Write a node's JSON Schema to a file for `--output-schema`, named by the hash
* of its text, in akm's cache dir. Returns the absolute file path (the value
* handed to the flag).
*
* The same schema is the same file, so concurrent fan-out units share it, and
* the atomic rename makes a write of identical bytes safe under a reader: no
* unit can see a half-written schema, and none removes it from another. Nothing
* is cleaned up, because nothing accumulates but one file per distinct schema.
*/
export function writeCodexOutputSchemaFile(schema) {
const text = `${JSON.stringify(schema, null, 2)}\n`;
const dir = join(getCacheDir(), "codex-output-schemas");
mkdirSync(dir, { recursive: true });
const file = join(dir, `${sha256Hex(text)}.json`);
writeFileAtomic(file, text);
return file;
}
/**
* Argv prefix that resumes a previous codex session: `exec resume <id>`.
* Codex resume is a subcommand chain, not a flag. The harness-native session
* id comes from the unit row (stored opportunistically by the result
* extractor); akm never depends on it (plan §"Session, MCP, and identity").
*/
export function codexResumeArgs(sessionId) {
return ["exec", "resume", sessionId];
}
/**
* Return `base` plus the `--sandbox workspace-write` flag that makes `codex exec`
* able to write to its working directory. `codex exec` defaults to a read-only
* sandbox that silently blocks file writes — without this flag the unit returns
* "workspace is read-only" and the engine marks the step complete with no real
* mutation. If the profile already pins `--sandbox` (long or short form) the
* default is not duplicated.
*
* Note: `--ask-for-approval` is an *interactive* codex flag only — `codex exec`
* does not accept it (it errors with "unexpected argument"). Non-interactive
* exec mode is implicitly non-approval-blocking; `--sandbox workspace-write`
* alone is sufficient for dispatched units.
*/
function ensureSandboxFlags(base) {
const out = [...base];
if (!out.includes("--sandbox") && !out.includes("-s")) {
out.push("--sandbox", "workspace-write");
}
return out;
}
/**
* OpenAI Codex builder.
* Command shape: codex exec --sandbox workspace-write [--model <m>] --json [--output-schema <file>] -- "<prompt>"
*/
export const codexBuilder = {
platform: "codex",
personaChannel: "prompt",
lower: createAgentRequestLowerer({
adapter: "codex",
personaChannel: "prompt",
tools: "none",
}),
build(profile, req) {
// Built-in codex profiles ship `args: []`; headless dispatch is the `exec`
// subcommand. Don't double it when a user profile already pins it.
const extra = profile.args[0] === "exec" ? profile.args.slice(1) : [...profile.args];
// `codex exec` defaults to a read-only sandbox that silently blocks file
// writes — dispatched units would return "workspace is read-only" and the
// engine would mark them complete with no real mutation. Force
// `workspace-write` (writes scoped to cwd) unless the profile already pins
// its own --sandbox flag (`--ask-for-approval` is not injectable here —
// see ensureSandboxFlags).
const sandboxArgs = ensureSandboxFlags(extra);
const args = ["exec", ...sandboxArgs];
if (req.model) {
const resolved = resolveDispatchModel(req, profile, "codex");
args.push("--model", resolved);
}
// JSONL event stream on stdout — the codex result extractor's input.
args.push("--json");
if (req.schema) {
// Native-schema tier: pass the node schema straight through.
args.push("--output-schema", writeCodexOutputSchemaFile(req.schema));
}
// No system-prompt flag exists on `codex exec` — fold it into the prompt.
const prompt = req.systemPrompt ? `${req.systemPrompt}\n\n${req.prompt}` : req.prompt;
args.push("--");
args.push(prompt);
return { argv: [profile.bin, ...args] };
},
};