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
143 lines (142 loc) • 7.51 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 temp file and passed via
* `--output-schema <file>`. The file is tiny, uniquely named under the OS
* temp dir, and intentionally NOT cleaned up here — `BuiltCommand` has no
* post-run hook, and the spawned process reads the file after `build()`
* returns. OS temp reaping owns the lifecycle. 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 { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { resolveDispatchModel } from "../../agent/builder-shared.js";
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
/**
* Write a node's JSON Schema to a fresh temp file for `--output-schema`.
*
* A unique `mkdtemp` directory per build avoids collisions between concurrent
* fan-out units dispatching in the same process. Returns the absolute file
* path (the value handed to the flag).
*/
export function writeCodexOutputSchemaFile(schema) {
const dir = mkdtempSync(join(tmpdir(), "akm-codex-schema-"));
const file = join(dir, "output-schema.json");
writeFileSync(file, `${JSON.stringify(schema, null, 2)}\n`, "utf8");
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] };
},
};