UNPKG

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
// 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] }; }, };