Based on AgentRun release · Synced .
@parcha/agentrun-harness-codex
Codex as an AgentRun harness: a DSL agent node runs on Codex through @parcha/agentrun-kernel, durable in AgentRun's
own store, with the host's tools as its only tools.
import { createHarnessRunner } from "@parcha/agentrun-kernel";
import { codexAdapter } from "@parcha/agentrun-harness-codex";
const runNode = createHarnessRunner(
codexAdapter({
model: "gpt-5.4-mini",
provider: { origin: "https://api.openai.com", credential: () => process.env.OPENAI_API_KEY! },
sessionsDir: "/var/lib/my-host/codex-sessions",
}),
{ store, tools },
);
- The host's tools only. Codex's app-server runs with its built-in tools off: no shell, no file edits, no web, no
images, no sub-agents. Codex's sandbox stays read-only for those built-ins, and its message describing that sandbox
is not sent, so the model is never told its files are read-only while the host's tools write them
(
test/developer-messages.test.mjs). A classic model is offered exactly the host's tools (and the runner'ssubmit); a code-mode model is offered code mode'sexecandwait, whose runtime has no Node modules, no process and no network, and reaches the host's tools by their own names, as nested calls of the script it writes. The proxy passesexecandwaitand drops the sub-agent and user-input tools Codex lists beside them; it keeps parallel tool calls off for a code-mode request, and offers noexecwhile a script that yielded still runs, so one script is live at a time. A nested call has an id of Codex's own (exec-<uuid>) that no line of the rollout carries and that names noexec, so the session ties it to the one live script: theexecthe proxy's latest answer carried (read before Codex has that part of the answer) or that the rollout shows unfinished. Anevertool's call waits for thatexec's line to be in the log, and is declined, never run, while two scripts are live.test/tool-off.test.mjsruns the pinned binary against a scripted model that calls every built-in, and checks that nothing runs, writes or spawns;test/code-mode-never.test.mjsruns anevertool from code mode, from a script that yielded, and from a second script a model wrote while the first ran. After a crash the harness's note about the interrupted turn reaches the model once, as a developer message injected on the recovery resume (Codex ignores new developer instructions on resume, and every turn opens withthread/resume), and the resumed turn's user input is the user's own words orContinue.. - The model behind a proxy. Codex talks to a model proxy on the loopback interface with a key of its session's own;
it never holds the provider key. The proxy forwards
POST /v1/responsesonly, fixes the model, stores nothing at the provider, drops any tool that is not the session's, and asks the runner before every paid request, so the turn cap and the host's budget hold. Each response's usage comes back to the runner. - Web search, when asked. The one exception to the above:
webSearch: "live"setsweb_search = "live"in Codex's config; the proxy then passes exactly the hosted tool Codex sends for it ({ type: "web_search", external_web_access: true }, recorded intest/fixtures/responses-request-web-search-live.json) and refuses any other web search tool. Each search a response ran (tool_usage.web_search.num_requests) is added to its price at OpenAI's $0.01 a call; a model with no known search price books an unknown cost. A request that offers web search has no input bound, so a cap treats its input as unknown. - Remote MCP servers, when granted. A session given
spec.mcp(a host's MCP gateway) writes each server in Codex'smcp_serversat the proxy's/mcp/<server>, with the proxy key as Codex's bearer, only the granted tools enabled, approve mode andrequired = true; the proxy admits those tools in themcp__<server>namespace Codex sends them in. Codex sends a call without waiting for the session, so the proxy holds it until its line (a script's nested call: its script's) is in the log, records its intent and only then sends it, through the host's gateway (src/mcp.ts). - A private machine. A session runs in its own directories. Its working directory is the host's workspace when the
host names one (
io.cwd, where the host's file tools write, an untrusted project to Codex), and otherwise an empty directory of the session's. Codex keeps nothing there, and takes no instructions from anAGENTS.mdin it (test/developer-messages.test.mjs). Codex's environment is an allowlist (PATH,LANG,TZ, its own homes and the proxy key); nothing else of the host's reaches it, and its process group ends with the session. - The host's skills, and no others.
codexAdapter({ skills: [{ name, files }] }): Codex lists each skill to the model by its name and description, and the model reads itsSKILL.mdand the files it names withread_skill_file(it has no shell). The skills are written into the session'sCODEX_HOMEat the start of every drive, where Codex lists a user's skills even in a turn with its built-ins off (it lists none from a working directory's.agents/skillsthere). Codex's bundled skills (imagegen, openai-docs, skill-creator, skill-installer, ...) are off in every session, with skills or without. The skills are checked when the adapter is made, byskillProblemsof@parcha/agentrun-kernel, the Claude adapter's check: a lowercase name, aSKILL.mdwhose front matter names it and describes it, plain relative paths, 1 MiB in all. Codex itself skips a skill with no front matter or no description and lists one under its front matter's name, so each of those is refused here instead. - Durable. A session's records are in the store the host lends (Codex's rollout mirrored line by line, every tool
call an effect recorded before it runs). A session a process died inside resumes from its records: a
safetool runs again, anevertool's cut-off call is reported to the model as interrupted and never repeated.
Tool schemas. Codex sanitizes the schema its model is given (measured on the pinned binary, recorded in test/fixtures/schema-column.json). The adapter refuses, when a session is checked, the shapes Codex drops or retypes (a tuple items, prefixItems, patternProperties, if/then/else, not, a non-scalar or untyped non-string enum/const, contains, dependentRequired, propertyNames, a boolean items, constraints inside allOf), naming the tool, the JSON pointer and the keyword. The keywords Codex only strips (bounds, patterns, formats, titles, defaults, examples) are listed in the tool's description, so the model still reads them; the host checks every call against the original schema. A schema object reused in two places is described at its first path.
The adapter is pinned to Codex 0.164.0. @openai/codex is the host's: an exact optional peer dependency, never shipped
here (npm install --save-exact @openai/codex@0.164.0). codexAdapter() checks the binary against the pins when it is
made and throws CodexNotInstalled or CodexPinMismatch before any run starts. CODEX_BIN names another path to the
same build.
Code this package copies from another project is listed in vendor/durable-harness/UPSTREAM.json and credited in
NOTICE.