# @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.

```ts
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's
  `submit`); a code-mode model is offered code mode's `exec` and `wait`, 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 passes `exec` and
  `wait` and 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 no `exec` while 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 no `exec`, so
  the session ties it to the one live script: the `exec` the proxy's latest answer carried (read before Codex has that
  part of the answer) or that the rollout shows unfinished. A `never` tool's call waits for that `exec`'s line to be in
  the log, and is declined, never run, while two scripts are live. `test/tool-off.test.mjs` runs 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.mjs` runs a `never` tool 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 with `thread/resume`), and the resumed turn's user input is the user's own words or `Continue.`.
- **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/responses` only, 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"` sets `web_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 in `test/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's
  `mcp_servers` at the proxy's `/mcp/<server>`, with the proxy key as Codex's bearer, only the granted tools enabled,
  approve mode and `required = true`; the proxy admits those tools in the `mcp__<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 an `AGENTS.md` in 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 its `SKILL.md` and the files it names with `read_skill_file` (it has
  no shell). The skills are written into the session's `CODEX_HOME` at 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/skills` there).
  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, by `skillProblems` of `@parcha/agentrun-kernel`,
  the Claude adapter's check: a lowercase name, a `SKILL.md` whose 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 `safe` tool runs
  again, a `never` tool'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`.
