The harness is a six-clause contract the bot holds a process to; pi is its one implementation; a second harness enters as a conformance row before it enters an image
The ask. Two decisions, the maintainer's, taken on 2026-09-15 (both accepted; one design choice changed on acceptance, the amendment at the end), ahead of the deletion step of record 0032. First: adopt the six-clause harness contract below as the seam between Switchboard and any process that runs a model loop for it, with pi as its one implementation and OpenAI's Codex CLI, driven over its app-server protocol, as a conformance row filled from its documentation and built nowhere; and schedule the code that makes the contract checkable (an interface pi implements, a scripted harness in tests, a conformance suite) as one unit after the deletion step. Second: accept that the survival clause covers a replaced container as well as a dead bot, so the same unit gives pi a relaunch with a rotated credential; its mechanics go to that unit's plan. Asked 2026-09-15: push the design's boundaries with a second harness and make sure the abstraction is clean, not necessarily to use it. Written for a veteran engineer new to Switchboard who has read record 0032, its amendment of 2026-09-15 (pi's row as production runs it) and harness-pi.md.
Success criteria: (1) every clause is bound to a pi behaviour a test or a live receipt already proves; (2) the Codex row answers every clause with how, or with cannot and the fact that says so; (3) every clause is stated as behaviour observable from outside the harness's process, so a harness with no extension can be judged against it; (4) nothing on a production path changes before the deletion step; (5) the conformance suite is harness-neutral: CodexHarness passes it against a fake of Codex's protocol with none of pi's hooks or files, and removing one clause's behaviour from it fails the suite.
TL;DR
After the deletion step every preset runs on pi and nothing says what a harness owes: the seam is a 21-field run type, an 11-method container seam, three routes and a 23-kind event table, and the row's facts changed four times in two days. The bet is a six-clause contract that trusts the harness with nothing but the conversation: a bearer for a key, the bot's decision on every tool call, our tools over one relay, every event a home on the record, the conversation as seed, steer and prompt, and facts on the row so the bot finds the process or rebuilds it. It costs one record, one interface with pi behind it and a Codex harness proven against a fake of Codex's own protocol, and one table; nothing enters an image. Codex's row found the first honest cannot: it cannot ask before a read-only shell command, and the clause stays strict because those rules guard credentials. Doing nothing leaves pi's accidents as Switchboard's architecture.
Today at 1f1359f2
The five facts the design turns on; every fact checked, with its proof, is in Appendix A.
- The seam is one word and one branch, not a type.
HARNESSES = ["native", "pi"](src/agents/registry.ts:51),effectiveHarnesspicks the word, and the run stage branches on it three times, buildingPiHarnessRun(21 fields) andPiHarnessDeps(11) by hand (src/core/dispatch/runLoop.ts:569, 589, 689). Record 0032 retires the word with the native loop, so after the deletion step the tree has one loop and no seam. - The clauses already exist, in pi's names. The extension asks the bot before every call pi makes and blocks after 90 s without an answer (
src/core/harness/pi/extensionSource.ts:34-46, 180); relayed tools run in the bot, the ask for one answered allow by name because the tool then runs under the bot's own gates (src/core/harness/pi/relay.ts:198); pi's 23 event kinds each carry one of five dispositions (src/core/harness/pi/bridge.ts:38-61); the proxy speaks two wire shapes, pins the model and counts turns (src/channels/modelProxy.ts:28-29, 199). - The process seam is typed; nothing above it is.
PiContainerhas 11 methods and two implementations, one over/execfor the resident and sandbox classes and one overnode:child_processfor classnone(src/core/harness/pi/container.ts:25, 193;src/core/harness/pi/botHostContainer.ts:55). A second harness reuses it unchanged. - The row's facts moved four times with nothing to move them against.
{pid, logOffset, sessionFile, root, bearerHash, container}(src/core/harness/pi/harness.ts:74-106): alive here means re-attach, another container or dead means a fresh pi on the mirrored transcript (harness-pi item 8). Three fields arrived and one was redefined on 2026-09-14 and 2026-09-15 (#1145, #1152, #1169, #1189). - A replaced container kills a pi run, and the only code that survives one is being deleted. Review run
529ee3a1lost its pi when release 1.225.0 rebuilt the execution image (#1201) and replaced the resident container at 18:06Z on 2026-09-15; the run failed "pi exited before the run settled" (#1219). The executor already names the event,ExecSandboxRestartedError(src/execution/resident.ts:539); its one handler is the native loop's (src/runner.ts:596, run-loop item 19), which the deletion step removes.
The shape
A harness is a process that runs the model loop for one run, in the container the run's machine class provides. The contract is the six things the bot holds it to. Credential: it starts with the run bearer and the proxy's URL, never a provider key. Gate: every tool call it runs is decided by the bot, before it runs when it runs in the container, at execution when it runs in the bot, and a call that ran undecided fails the run closed. Relay: Switchboard's tools are served to it by the bot and executed by the bot under the run's context and the bot's own gates. Record: every event kind it emits has a disposition onto the run stream, and its finished turns are mirrored into the ledger in the record's vocabulary. Conversation: it is driven as a seed, steers and prompts, counts its own turns, and is ended. Survival: it leaves facts on the row so the bot, in this generation or the next, finds it alive or rebuilds it from our record, and a rebuilt process is the only one the run's credential still serves. A conformance row is one harness's answer to the six clauses; pi's is code, Codex's is a client of Codex's protocol proven against a fake of it and never shipped, and a clause Codex cannot meet from outside the process is either a clause that was pi-shaped or a guarantee Codex lacks, and the row argues which.
The closest well-known shape is the Kubernetes container runtime interface: the kubelet never runs a container itself, it drives a runtime such as containerd over a fixed contract, keeps the pod's status as the record while the runtime keeps its own state, and a conformance suite is what a runtime passes before anyone ships it. The one difference: our runtime is not trusted. It holds a per-run bearer instead of the node's credentials, and it asks the kubelet before every system call.
| Clause | pi (code) | Codex (documentation) |
|---|---|---|
| Credential | proxy as the one provider; bearer in the environment | model_providers with base_url, env_key; needs a third proxy shape |
| Gate | tool_call hook asks the bot on every call | asks on writes and network only; cannot for read-only commands |
| Relay | tools served, registered, run in the bot | the bot as an MCP server; mcpToolCall items |
| Record | 23 kinds, 5 dispositions; mirror to ledger steps | 15 item types map the same way; contextCompaction is the row |
| Conversation | authored session, prompt, steer, one more prompt, abort | thread/start, turn/steer, turn/start, turn/interrupt |
| Survival | facts on the row; re-attach or rebuild from the ledger | own store: thread/resume where it is; gone means restart the request |
One trace: the release that rolls the resident under a review
Real case, #1219, walked under the contract on pi, with the one step Codex takes differently.
- At 18:05Z pi is inside a tool call the gate allowed; the mirror wrote the assistant turn with the call in flight as a ledger step (item 8), and the row's facts name the pid, the root, the log boundary, the bearer's hash and container A's boot id.
- At 18:06Z the release replaces the resident container; pi's process group dies with it, and the tool's command with it. The bot lives.
- Today the harness reads the next command's
ExecSandboxRestartedErroras pi's death and fails the run (#1219). From here the trace is the contract. - Survival: the harness asks the container it holds to name itself. The row names A and the answer is B, so the row's pid is a stranger's here: it is neither probed nor ended, and the verdict is another container, the state the spec calls "elsewhere".
- The run loop, not the harness, re-attaches the run's workspace binding in B, never re-provisioning; a worktree B cannot keep is refused by name, the resident naming it in the run's note, and the run closes
interruptedwith its request dispatched again as a new run (#1169). Here it is kept. - Credential: the run loop rotates the run's bearer, revoking A's hash at the proxy and the harness door, so a pi that somehow survived in A can neither spend nor ask; the new pi gets the new bearer once, in its environment. Rotation is only possible with a relaunch: a live process cannot be handed a new bearer, which is why a re-attach adopts the old hash instead (#1152).
- Rebuild: the harness writes pi's session from the ledger, the mirrored steps with each compaction where it sat, the notepad, and one user turn carrying the settlement's note (
settlementText,harness.ts:334): the bot did not see this call finish, its effects are the container's, do not run it again. - Gate: pi's first call in B asks the bot, which answers from the table as the requester, as it did in A. The proxy counts the next model call on the run's meter, pinned to the run's model.
- Record: one
resumedrun note names the orphan by pid and container; the run page shows one run with a relaunch, never two. - The review runs to its verdict; if the head moved during the outage, one more
prompton the same session names the new head (item 14). - Codex diverges at step 7: no session can be authored and
thread/resumeneeds a rollout that lived on A's disk, so the own-store rule closes the runinterruptedwith a note and dispatches its request again, the outcome of step 5's refusal. - The property: a replaced container costs a run at most one model call and never a tool's effects, and the record is complete without anything the container held. Record 0032 promised this for a bot death; the contract promises it for the container too.
The difficulty map
Ranked by the risk of being wrong; the section lengths follow this order.
- Whether the gate may decide relayed tools by class and still refuse Codex a class decision on read-only commands. The gate as a protocol ask.
- Surviving a replaced container needs a credential fence against the orphan and a step a live run cannot reach today, on a base rate of one. A replaced container.
- A harness that keeps its own store cannot be rebuilt from our record, and a fresh run's seed depends on the same fact. Own store or authored session.
- The record across a harness version bump. The record clause.
- (most work) Carving
Harness,HarnessRunandHarnessDepsout of the hand-built pi types, writingCodexHarnessagainst a fake of Codex's protocol, and the scenario table both pass; the stage gates keep the rest from being built early. Rollout, Stage gates and the review each must survive.
The gate as a protocol ask
Today the gate is pi.on("tool_call"), an in-process hook that sees every call pi makes, its seven built-ins included, and asks the bot before it runs (src/core/harness/pi/extensionSource.ts:180). The rules read the command, not only its effect: origin-only pushes, the protected base, no merge or approval, no credential reads or environment dumps, paths inside the checkout (judgeToolCall, src/core/harness/pi/toolRules.ts:135). For a relayed tool the same ask is answered allow by name (src/core/harness/pi/relay.ts:198), because the tool then runs in the bot, where the bot's own gates decide it at execution: a spawn_run naming coding is refused by the spawn, a GitHub write by the identity the tool context carries. A Rust harness has no hook. Codex offers approval requests for command execution and file changes, a request_permissions tool for escalation, built-ins that ask nothing (webSearch, imageView), and its own sandbox and approval policy, which decide inside the container, the one place record 0007 forbids a decision.
The clause is about where the decision is made, not how often: the bot decides every call, before it runs when the call runs in the container, at execution when it runs in the bot. That is why a relayed tool may be allowed by class at the ask and a shell command may not: the bot gets a second look at the relayed tool and none at the shell command, which has run in the container by the time anyone else sees it. Three roads follow. First, relayed tools are served by identity and run in the bot under its gates, so the ask for one is allow by name on every harness. Second, the harness's own shell and file tools are decided by its protocol ask, answered by judgeToolCall with the run's identity and rules, accept or decline and never acceptForSession, so a grant lost mid-run is refused on the next call, the property the extension has today because it caches nothing. Third, a built-in with no ask is disabled by configuration and Switchboard's relayed tool of the same purpose stands in. A call the record shows ran without a decision fails the run closed (#1067's rule, kept).
Codex's row on the second road is the record's one cannot. Its on-request policy asks only when a command exceeds the sandbox or the model requests escalation; under danger-full-access its documentation says "no sandbox; no approvals"; the policy that asked before every untrusted command, untrusted, is no longer supported; and execpolicy rules ask by command prefix (prefix_rule with decision = "prompt") with no documented catch-all. So Codex under sandbox_mode = "read-only" asks before every write and every network call, the sandbox being the wall that raises the ask and the bot's answer the decision, and prefix rules ask before the credential-reading prefixes the rules name, but a read-only command no prefix names runs undecided. The clause stays strict because the read-side rules guard real credentials: the sandbox's exec logs carried a token once, an open finding on the tracker. The row reads: met for writes and network by sandbox escalation, met for named read prefixes by rules, cannot for the rest until Codex offers an ask-before-every-command policy again. The alternative, relaxing the clause to calls that write or reach the network, was rejected for that reason. A harness whose row says cannot is not admitted, and the contract has done its job by saying so in one sentence.
Invariants: every tool call that ran has a decision beside it in the record, or the run failed closed; a call the harness itself refused before running, such as pi answering a call whose arguments fail validation, is a harness_error note and not a bypass (src/core/harness/pi/bridge.ts:367); no decision is cached outside the bot; a gate unreachable for 90 s blocks the call, never the run, and the door answers a call it cannot yet judge as unavailable, never as refused. Failure modes: a harness release adds a built-in, it appears in the record undecided, the run fails closed, and the conformance row is extended before the pin moves; a surviving harness re-asks in the window between a new bot generation's listener coming up and the run's re-registration, about 10 s, and today's door answers 4xx, which the extension takes as final (#1220, a fix in flight), so the invariant's last clause is a gap row in harness-pi.md until that door answers unavailable.
A replaced container
Today "another container means relaunch here" runs only on a bot generation change, through the ledger's resume (src/core/dispatch/runLoop.ts:569). When the bot lives and the container is replaced under a live pi, the executor waits for the container's wake and then hands the harness ExecSandboxRestartedError (#1146, src/execution/resident.ts:539); the pi seam has no catch for it and the run fails (#1219), while the native loop catches it and settles the call (src/runner.ts:596, run-loop item 19) and the deletion step removes that handler with the loop. The base rate is one incident in the two days pi has run on the resident; every release that rebuilds the execution image is a candidate, as 1.225.0 was (#1201), and the rate is the first number the code unit's plan measures.
The clause is one rule for every class, and it is the run loop's to carry: the harness reports its process not alive here, and the loop relaunches from the record in the container it was handed. "Not alive here" has three triggers: the liveness probe answers no; the container names itself differently from the row; a container command fails with ExecSandboxRestartedError. Before the relaunch the loop does two things the resume path does today and a live run cannot reach: it re-attaches the workspace binding or refuses it by name (attachWorkspace, abandonLostWorkspace, src/core/dispatch/reattach.ts:44, made callable mid-run; a refusal closes the run interrupted and dispatches the request again, or ends it without re-dispatch when the request cannot be read), and it rotates the credential. The rotation is the fence against the orphan: the bearer store accumulates hashes per run and admits any live one (src/core/modelProxy/runBearers.ts:76, 96), so a pi that survived in a container the deploy did not destroy would keep spending the meter and asking the door under the same run; the loop mints a new bearer, revokes the old hashes, and the orphan's next call fails at the proxy and at the door. The old process is ended and its root removed only in a container whose identity matches the row's. Relaunches are counted on the row's harness facts, so the count survives a bot generation, and bounded at two; the third finding closes the run interrupted with a note, so a crash-looping container cannot spin a run.
Invariants: at most one credentialed process per run; the rebuilt session carries every settled call's note; a relaunch never re-runs a tool. Failure modes: the worktree is not kept, so the run closes and its request is dispatched again; the second container dies too, so the second relaunch runs and the third finding closes the run; the container cannot name itself because it is dead, which is the third trigger, not the second, and relaunches the same way. What the plan must answer before the unit lands: whether the container names itself at the moment the old command fails or fails too, what holds the workspace binding so the resume path's two functions can run mid-run without re-provisioning, and whether the bound of two should be the wall clock instead.
The alternative it beat is leaning on the executor's wait for the container's wake (#1146). The wait re-attaches the executor and hands the harness the typed error; it cannot rebuild a process it did not start, and the harness has to survive the bot-death case anyway.
Own store or authored session
Records 0032 and 0035 make Switchboard's record the source of truth and the harness's file a cache, and pi honours that because its history is a JSONL file the bot writes (piSessionFile, src/core/harness/pi/mirror.ts:226) and pi reads on start. The same file carries a fresh run's seed, the thread so far as the session and the request as the prompt (item 9). Codex has neither half: thread/resume reads only its own rollout, kept under CODEX_HOME in a format its documentation does not state, and thread/start takes no history, so the thread's earlier turns can reach it only as text inside the first turn.
Each harness therefore declares one of two words, and three clauses read it. An authored session harness accepts a history the bot wrote as its own; pi is one. An own store harness has no history but its private store; Codex is one. For an authored-session harness a fresh run's seed is the written session and a rebuild is a rewrite of it. For an own-store harness the seed's earlier turns are quoted into the first prompt as one fenced block marked as quotation, a rebuild is thread/resume when the store is where the row says, and a store that is gone closes the run interrupted with a note and dispatches its request again as a new run, the outcome #1169 already chose for a refused worktree. The record is complete under both words because the mirror writes from the harness's events, never from its store; what an own-store record may lack is the compaction summary itself, if the harness's compaction event carries none, and the row says so.
Invariants: the ledger's transcript is complete without any file the container holds, on both words; an own-store harness never resumes a thread the bot did not watch begin, so a thread/resume of an id the row does not name is refused; a quoted seed enters the record as a text part of the request turn, the way record 0037 fences a referenced thread, never as prior assistant turns. Failure modes: an authored session the harness rejects after a version bump, which pi shows as an exit before its first turn (#1166's shape), fails the run closed with the process's stderr tail; an own-store harness whose store survived in another container is not probed, and its run restarts from the request.
The alternative it beat is requiring an authored session of every harness. That keeps Codex's seed exact, but it means reverse-engineering an undocumented JSONL and re-doing it on every Codex release, so the effort lands on a file format instead of on the seam. Revisable: the day Codex documents its rollout format, its row moves to authored-session by changing one word.
The record clause
Each harness brings a disposition table over its own event vocabulary, held to pi's five words (structure, mapped, folded, note, impossible), and a kind the table does not name lands as a harness_error run note carrying the kind's name (src/core/harness/pi/bridge.ts:205), so a version bump is visible in the first run's record. The harness counts its own turns for the wrap-up steer (harness-pi item 6) and the proxy enforces the cap (model-proxy item 5); the hard stop is the protocol's abort (harness.ts:793) and the container seam's kill when the session ends (harness.ts:541). The effort map from Switchboard's five tiers (src/effort.ts:9) to the harness's words is a table per harness; pi's is the identity by a coincidence of names.
Why not X
Why not adopt the Agent Client Protocol as the contract, since pi, Codex and Claude all speak it through adapters? ACP is an editor protocol. It has the gate (session/request_permission), the steer, the cancel and a usage update, and it is the right reference for the contract's names, so Appendix B maps each clause onto it. It lacks the one thing the survival clause turns on: an authored session. session/load replays the agent's own history to the client and takes none from it, so under ACP every harness is own-store and the record can never rebuild one. Using ACP as the wire for the gate and the conversation while the record and the credential ride beside it is an implementation choice open to any row; it is not the contract.
Why not just fix #1219 in the pi path and skip the contract? Catching ExecSandboxRestartedError in harness.ts is the smallest patch and it lands regardless; the contract is what says what the catch must do. The smallest catch settles the call and fails the run, as the native loop does; the survival clause says rebuild from the record, rotate the credential, bound the relaunches, and that is a different fix. The same applies to the interface: after the deletion step there is no seam, and the row's facts moved four times in two days without a sentence to check the moves against (today, fact 4); CodexHarness against its fake is what makes that sentence a check instead of prose.
Why Codex and not Claude Code as the second row? Claude Code is the more popular harness and the worse probe: closed, Anthropic-only, with a private session format. It pushes one boundary, that the record must come from the event stream alone, and then goes dark. Codex is open source and differs from pi on every clause at once, which is how it found the gate's cannot.
Why not build Codex for real now, or not at all? Neither. The interface needs a second implementation to be tested at all, and a toy stub would only prove that the suite passes our own imagination, so the second implementation is CodexHarness, a client of Codex's app-server protocol proven against a fake generated from Codex's published schema (stage A). What only a shipped Codex needs, the Responses shape at the proxy, an MCP relay and an image whose size nobody has measured (pi's cost 434 MB per image), waits behind measurements (stage B) or a new decision with a user (stage C), and the gate's cannot stands until Codex changes. Weeks of work and a second prompt surface, which record 0032 rejected, are what the gates keep out.
Boundaries
No second harness enters an image under this record. The provider layer for calls made outside a run loop (the router and reflection on pi's library, harness-pi item 13) is not a harness. The harness: word on a preset goes with the native loop, as record 0032 says; the contract's roster is the code's, with one member. Ship's orchestrator is never a harness (record 0031). A harness's own sandbox is a wall, never a decision. What a shipped second row would cost is named and gated behind measurements, not scheduled: the proxy declares the wire shapes it speaks, today two (src/channels/modelProxy.ts:28-29), and configuration validation refuses a harness whose wire is not in the roster, so Codex would add the Responses shape, a route, a pin and a meter; and the relay for a harness without an extension is the bot as an MCP server, which lacks the relay's 202 pending, so how long a relayed call really runs is the p99 to measure from the run store before anyone builds it. Compatibility: nothing a user sees changes; pi's rows in harness-pi.md stay, and the contract's harness-neutral rows go to a new living spec, harness.md, in the same pull request as the code seam.
What would change our mind
| Assumption | Cheapest evidence | When |
|---|---|---|
The suite can be passed by CodexHarness with none of pi's hooks | Write CodexHarness and its fake first in the code unit; a clause it needs a hook to pass is pi-shaped and moves | First day of the code unit |
| Container replacement under a live run recurs | Count ExecSandboxRestartedError in the run store per release | Before the plan sizes the relaunch |
| Codex's rollout format stays undocumented | Read codex-rs/core/src/rollout once at the version the row cites | Before the code unit; the row moves to authored-session if it is documented |
| Codex has no ask-before-every-command policy | Re-read the approvals page at each Codex release named in the row | At each re-read of the row |
Reversibility: the record and the interface are deletable and nothing in production changes; the last point of retreat is the day a second harness enters an image.
Rollout
Three stages, each gated by the section below; only the first is scheduled. Stage A, one unit after the deletion step merges: src/core/harness/contract.ts with the Harness interface (the name is free once the deletion step removes the "native" | "pi" union) and the HarnessFacts row type; PiHarness over the code that exists; PiContainer renamed HarnessContainer because it is the harness-neutral seam; CodexHarness, a real client of Codex's app-server protocol (the thread and turn calls, the approval answers, the item dispositions, the row facts), exercised against a fake app-server whose every message validates against Codex's published schema at the pinned version, so the fake cannot drift into our imagination; and the conformance suite as one scenario table run against both harnesses, whose rows are the validation criteria below. The same unit carries the two behaviours this record adds to pi, the relaunch on ExecSandboxRestartedError with the credential rotation and the mid-run workspace re-attach, each behind its own gate. Scope: the 160-line seam in src/core/dispatch/runLoop.ts (542 to 700), the two type blocks in harness.ts, one rename, three new files; a guess of under 1,500 lines with tests, checked when the plan is written. Same pull request: the living spec harness.md with the six clauses as rows, and harness-pi.md re-pointed where a pi row is now a contract row. Stage B, unscheduled: the Responses shape at the proxy and the MCP relay, so the real codex app-server binary runs against the proxy on a developer machine or a staging bot-host container and the suite's live rows are receipted there; no image changes. Stage C, a new decision with a user: Codex in an image and a preset on it. Sequencing and blast radius belong to the program plan.
Stage gates and the review each must survive
The trap is building stage B or C before stage A has proven the seam, or landing the relaunch before its two assumptions are tested. Each gate is a measurement or a spike that must exist before the work it guards starts, and each stage's pull request is reviewed adversarially against the attack points listed, with the design method's correctness pass: every claim checked at the sha, every invariant given one scenario that would break it.
| Guards | Gate, before the work starts | The review attacks |
|---|---|---|
| The relaunch (decision 2) | A spike showing attachWorkspace and abandonLostWorkspace run mid-run on a live row without re-provisioning; if it fails, decision 2 shrinks to settle the call and close the run interrupted, the native loop's behaviour today | The orphan fence: the rotated bearer is refused at the proxy and the harness door in the same tick; a re-attach adopts and never rotates; the count on the row survives a bot generation |
| The relaunch's triggers | One measurement, forced on staging: does the container name itself while the old command is failing, or fail too | A trigger that fires on a live process ends a healthy pi; a probe that hangs past the executor's timeout stalls the run |
| Stage A's second implementation | The fake app-server's messages validate against Codex's published schema at the pinned version; a hand-written fake is refused | The fake answers what Codex would refuse (a turn/start on a busy thread, a thread/resume of an unknown id), so the suite passes for a reason the real binary would not give |
| Stage A's neutrality claim | The suite fails for CodexHarness when a clause is implemented only in pi, once per clause, before the unit is called done | A scenario both harnesses pass trivially because it asserts nothing on the record |
| Stage B, the Responses shape | The p99 of relayed-call duration from the run store, and one stub run of codex app-server against a sleeping MCP tool past that p99 | The pin misses the field the Responses shape uses for the output cap; the meter reads the wrong usage block; a streamed answer is metered twice |
| Stage B, the MCP relay | The same stub run answers whether an mcpToolCall asks approval under the granular policy | A relayed call that outlives the request runs twice; the door admits an MCP call from a rotated bearer |
| Stage C | A user who wants a preset on Codex, and a new record | Not this record's |
Open questions
| Question | Owner | Resolves it | Needed before |
|---|---|---|---|
| Does the container name itself at the moment the old command fails, or fail too? | The maintainer of the container seam | The first live relaunch receipt | The code unit's relaunch rows |
| The relaunch bound: two, or the preset's wall clock alone? | The maintainer | The same receipt | The code unit |
Does Codex's contextCompaction item carry the summary text, so an own-store record can hold it? | Whoever writes the code unit | One read of the app-server item schema | The row's wording only |
| Can one MCP streamable-HTTP request be held past the relayed call p99 through the resident shim? | The maintainer of the harness routes | The p99 query, then a stub run | Any Codex code, which is unscheduled |
Validation criteria
Each criterion is one row of the conformance suite's scenario table, run against pi and against CodexHarness; every row is [gap] until the code unit binds it, and pi's existing proofs are named so the binding is a re-point, not a new test.
[gap]CodexHarnesspasses every row against the fake app-server with none of pi's hooks or files, and the fake's every message validates against Codex's published schema at the pinned version.[gap]Removing one clause's behaviour fromCodexHarnessfails the suite, once per clause.[gap]A command the harness runs without asking fails the run closed (pi's proof today: the #1067 rows in harness-pi.md item 7).[gap]Own store, store gone: the run closesinterruptedwith a note and its request is dispatched again (pi's proof for the refused worktree:src/core/dispatch/reattach.test.ts).[gap]A replaced container under a living bot relaunches the harness from the record in the new container with a rotated bearer, once; the old hash is refused at the proxy and the door; a third finding closes the run. Human-gated live receipt: a resident roll under a review run, the successor to #1219.[gap]An unknown event kind lands as aharness_errornote naming the kind (pi's proof today:src/core/harness/pi/bridge.test.ts).[gap]A quoted seed enters the record as a text part of the request turn and never as prior assistant turns.[gap]Configuration validation refuses a harness whose declared wire is not in the proxy's roster.[gap]The relaunch spike:attachWorkspaceandabandonLostWorkspacecomplete mid-run on a live row without re-provisioning, or the relaunch rows above are replaced by settle-and-close.[gap]A rotated bearer's old hash is refused at the proxy and at the harness door in the same test, and the re-attach path never calls the rotation.[gap]Stage B only: the Responses route pinsmodelandmax_output_tokensand meters that shape's usage block; the MCP relay holds a call past the measured p99 without running it twice. Human-gated: a livecodex app-serverrun against the proxy on a staging bot-host container, one refused command visible in the record.
Sources
- Records 0002, 0007, 0019, 0031, 0032 with its as-built sections and its amendment of 2026-09-15 (#1221), 0034, 0035, 0037.
- Living specs: harness-pi.md items 4, 6, 7, 8, 9, 12, 13, 14; model-proxy.md items 1, 4, 5; run-loop.md item 19; session-log.md.
- On the tracker: #1219, #1220, #1161, #1201, #1067, #1145, #1146, #1148, #1152, #1166, #1169, #1188, #1189, and the deletion step's item #1010.
- Codex: the
app-serverprotocol page, the configuration reference, the agent approvals and security page and the non-interactive page of its documentation site, and theexecpolicyREADME in its repository, read 2026-09-15. Agent Client Protocol: the schema, initialization and session-setup pages, read the same day. - The peer sessions' state of the pi series at 18:35Z on 2026-09-15, which "today" was checked against before the code.
Appendix A: the survey
| Fact | Value | Proof at 1f1359f2 |
|---|---|---|
| Harness words | ["native", "pi"]; type Harness is that union | src/agents/registry.ts:51-52 |
| Selection | one pure function of preset and the deployment's block | src/core/harness/select.ts:10-15 |
| Run stage branches | harness === "pi" at 569 (resume finish) and 589; runAgent at 689 | src/core/dispatch/runLoop.ts:542-700 |
| pi implementation size | 3,625 non-test lines, 11 files | src/core/harness/pi/*.ts |
| Routes and proxy | 174 and 685 lines | src/channels/harnessRoutes.ts, src/channels/modelProxy.ts |
| Native loop and adapters the deletion step removes | 1,100 + 542 + 190 lines at 3626c745 | src/runner.ts, src/providers/*.ts, src/tools/workspace.ts |
PiHarnessRun fields | 21 | src/core/harness/pi/harness.ts:142-186 |
PiHarnessDeps fields | 11 | src/core/harness/pi/harness.ts:188-218 |
PiContainer methods | 11: makeRoot, cwd, writeFile, start, writeLine, readLog, alive, identity, kill, tail, remove | src/core/harness/pi/container.ts:25-65 |
| Container implementations | ExecPiContainer, BotHostPiContainer | container.ts:193, botHostContainer.ts:55 |
| RPC commands the harness sends | prompt, steer, abort, get_state, set_auto_retry | src/core/harness/pi/*.ts; abort at harness.ts:793 |
| Extension hooks used | registerTool per served definition, tool_call | src/core/harness/pi/extensionSource.ts:179-180 |
| Authorize timing | retry 2 s, wait 90 s; tool request timeout 60 s, wait 90 s, re-ask floor 1 s | extensionSource.ts:34-46 |
| Relay hold; relayed ask allowed by name; settlement | 30 s then 202 pending; allow: true for a served name; settle | src/core/harness/pi/relay.ts:106, 198, 145 |
| The two routes' handlers | authorizeToolCall on /harness/authorize, relayToolCall on /harness/tool | src/channels/harnessRoutes.ts:116, 120 |
| Event kinds and dispositions; unknown kind; pi's own refusal | 23 kinds; five dispositions; harness_error; piAnsweredWithoutRunning | src/core/harness/pi/bridge.ts:38-61, 205, 367 |
| Proxy shapes | /v1/messages, /v1/chat/completions | src/channels/modelProxy.ts:28-29 |
| Proxy pin | model and max_tokens or max_completion_tokens from the grant | modelProxy.ts:199 |
| Bearer store | hashes accumulate per run; any live one admits | src/core/modelProxy/runBearers.ts:76, 96 |
| Row facts | pid, logOffset, sessionFile?, root?, bearerHash?, container?; a row without the shape is no facts | harness.ts:74-106, 108 |
| Settlement note | settlementText, the in-flight call's result on a rebuild | harness.ts:334 |
| Unvetted call fails the run closed | GateBypassed | harness.ts:267 |
| Built-in tools by identity | write: read, bash, edit, write, grep, find, ls; read: without edit, write; none: nothing | src/core/harness/pi/process.ts:25-43 |
| Bearer's environment variable | SWITCHBOARD_RUN_BEARER | process.ts:18 |
| Effort tiers | low, medium, high, xhigh, max; pi map is the identity | src/effort.ts:9, process.ts:48 |
| Post-turns on the session | runPiHarnessOpen keeps pi alive for the description turn and the head-move re-review | harness.ts:389, harness-pi item 14 |
| Session file authored by the bot | piSessionFile | src/core/harness/pi/mirror.ts:226 |
| Container roll as a typed error | ExecSandboxRestartedError thrown by the resident executor; caught only by the native loop | src/execution/resident.ts:539, src/runner.ts:596 |
| Workspace re-attach and refusal | resume path only | src/core/dispatch/reattach.ts:44, src/core/dispatch/provision.ts |
| Resident roll kills a pi run | review 529ee3a1, 18:06Z 2026-09-15, after the image rebuild of #1201 | #1219 |
Appendix B: the Codex row in full, with the ACP mapping
Checked against Codex's documentation site (the app-server, configuration, approvals and non-interactive pages) and its repository's execpolicy README, and against the Agent Client Protocol's site, on 2026-09-15. Codex has fifteen documented item types: userMessage, agentMessage, functionCallOutput, plan, reasoning, commandExecution, fileChange, mcpToolCall, dynamicToolCall, collabToolCall, webSearch, imageView, enteredReviewMode, exitedReviewMode, contextCompaction; four server-to-client requests, the two approval requests answered accept, acceptForSession, decline or cancel (command approval also acceptWithExecpolicyAmendment), item/permissions/requestApproval answered with the granted subset and an optional scope, and mcpServer/elicitation/request; rollouts live under CODEX_HOME in a format the documentation does not state.
| Clause | Codex configuration and protocol | ACP method the clause maps onto |
|---|---|---|
| Credential | model_providers.switchboard = { base_url = <proxy>, env_key = "SWITCHBOARD_RUN_BEARER", wire_api = "responses" }; the proxy pins the model regardless | none; ACP leaves the model to the agent |
| Gate | sandbox_mode = "read-only" so every write and network call raises item/commandExecution/requestApproval or item/fileChange/requestApproval, answered accept or decline from judgeToolCall; approval_policy = { granular = { sandbox_approval = true, rules = true, mcp_elicitations = false, request_permissions = false, skill_approval = false } }, so permission and skill prompts fail closed by configuration; a .rules file with decision = "prompt" on the credential-reading prefixes the rules name; web_search = "disabled"; tools.view_image = false for identity none; a dynamicToolCall or collabToolCall item, which no road covers, fails the run closed by the gate's rule; cannot for a read-only command no prefix names | session/request_permission |
| Relay | mcp_servers.switchboard = { url = <harness URL>/harness/mcp, bearer_token_env_var = "SWITCHBOARD_RUN_BEARER" }; the bot serves the run's relayed tools and runs them under its own gates; MCP prompts are off, so no call asks twice | mcpServers on session/new |
| Record | item/started and item/completed by item type: agentMessage, reasoning, userMessage, functionCallOutput as messages; commandExecution, fileChange, mcpToolCall as tool steps; contextCompaction as the compacted row, its summary if the item carries one; plan, enteredReviewMode, exitedReviewMode as structure; webSearch, imageView, dynamicToolCall, collabToolCall as impossible; thread/tokenUsage/updated folded; an unknown item type a harness_error note | session/update kinds agent_message_chunk, tool_call, tool_call_update, plan, usage_update |
| Conversation | thread/start { cwd } with the quoted seed in the first turn/start; turn/steer for steers and the wrap-up; turn/start on the same thread for post-turns; turn/interrupt for the hard stop; turns counted at turn/completed; effort low, medium, high, xhigh through, max to xhigh | session/new, session/prompt, session/cancel, session/set_mode |
| Survival | facts {pid, threadId, root: CODEX_HOME, bearerHash, container}; alive here on a workspace class: reconnect to the wrapper's FIFO and log, the thread still open in the live process; dead here with the store present: thread/resume threadId in a new process; another container, or store gone: the own-store rule | session/load replays the agent's own history; ACP has no authored session |
Appendix C: the interface, as a sketch for the code unit
interface Harness<Facts extends HarnessFacts = HarnessFacts> {
readonly name: string; // the roster key, and the facts' discriminator
readonly wire: ReadonlySet<"anthropic-messages" | "openai-completions" | "openai-responses">;
readonly history: "authored-session" | "own-store"; // the word three clauses read
readonly dispositions: Readonly<Record<string, Disposition>>;
readonly effort: (tier: Effort) => string | undefined;
builtinTools(identity: Identity): readonly string[]; // every one on a gate road or disabled
open(deps: HarnessDeps, run: HarnessRun<Facts>): Promise<HarnessSession>;
find(facts: Facts, container: HarnessContainer): Promise<"alive-here" | "another-container" | "dead">;
}
interface HarnessFacts { harness: string; relaunches: number } // each harness extends it with its own fields
interface HarnessSession {
readonly answer: string;
followUp(text: string, hooks: FollowUpHooks): Promise<string>; // one more prompt on the same session
end(): Promise<void>;
}HarnessRun and HarnessDeps are PiHarnessRun and PiHarnessDeps with the pi-only fields (compaction, the root layout) moved behind the implementation. PiHarnessFacts extends HarnessFacts with today's six fields; a row written before the discriminator existed is read as pi's, the one harness that ever wrote a row. The relaunch and the credential rotation are the run loop's, called with the find verdict; HarnessContainer is today's PiContainer with its eleven methods and a truthful name.
Amended 2026-09-15 — accepted, and the second row is wired as far as the interface test needs
Re-evaluation. The record was accepted the day it merged as proposed, on two grounds: the deletion step leaves one loop and no written seam, and a second row is the only way to tell a clause of the contract from an accident of pi. The maintainer agreed with both decisions and every design choice but one, "nothing gets built for Codex", and asked that the pushback the author had recorded become adversarial review and hardened validation, so that nothing is built too early. The change is checked against both grounds. It strengthens the second: a scripted stub speaking a toy protocol proves only that the suite passes our own imagination, while CodexHarness against a fake generated from Codex's published schema is the same second implementation the interface test needs anyway, so writing it costs the fake and not a toy. It leaves the first untouched: nothing enters an image, no preset runs on Codex, and the proxy and relay changes a shipped Codex would need sit behind the stage gates. What could regress is the fake's fidelity and the temptation to slide from stage A into B, which is why the gates section exists, why the fake is validated against the published schema at the pinned version, and why stage B has measurements as its price and stage C a new record. Changed in place: the frontmatter status, the ask, the TL;DR's cost sentence, success criterion 5, the shape's description of Codex's row, difficulty 5, the fourth "Why not X", one sentence of Boundaries, the first "What would change our mind" row, Rollout as three stages, the new "Stage gates and the review each must survive" section, and validation rows 1, 2, 9, 10 and 11.
Amended 2026-09-15 — the proof is the real Codex binary, not a fake of its protocol, and OpenRouter is the Responses route's upstream
Re-evaluation. The same evening, the maintainer read the first amendment and asked that the second harness be proven working with Codex itself. The record was accepted on two grounds, a written seam before the deletion step and a second row that tells a clause from an accident of pi; this change bears on the second. A fake that speaks Codex's protocol, however faithfully it validates against the published schema, proves only that our code agrees with our reading of Codex's documentation; the real codex app-server binary, pinned from its npm package (@openai/codex, 0.154.0 at the time of writing) and driven by CodexHarness, proves the seam against Codex's behaviour, which is what the row is for. The cost is that two bot-side pieces the first amendment left to a later stage, the Responses route at the proxy and an MCP relay, are needed for the proof and move into stage A, so stage A grows (the scope guess rises from 1,500 to 3,000 lines) and the later stages collapse into one: shipping Codex to a user, by a new record. The first ground is untouched: nothing enters an image, no preset runs on Codex, production configuration does not change. What could regress is the trap the gates guard, writing the two bot-side pieces before the seam is proven, so the order inside stage A is fixed (the seam and pi's rows green first, Codex's rows second) and the gates say so; and a Codex sandbox that cannot start on a test runner could make the rows pass for the wrong reason, so that gate names it and the rows fail loudly rather than skip.
The maintainer also asked whether stage A should wait for a planned OpenRouter integration. Nothing is scheduled beyond the documented example already on main (the program plan's OpenRouter example unit: the commented providers: block of type openai-compatible, which pi's library already serves for the router and reflection); no unit, no owner. OpenRouter serves the Responses API on its own path (beta, stateless, a drop-in for OpenAI's), and the provider table already has the type it needs, so stage A's Responses route forwards to the upstream the provider table names for the run's model, and OpenRouter is the first such upstream, a pass-through. That is the OpenRouter integration this record needs and there is nothing to wait for; the paid A/B record 0032 still owes stays owed there. It also gives the Codex rows a real, cheap model beside the scripted one, so the live rows need no separate stage.
The text above stands as accepted history; this amendment overrides these passages:
- Success criterion 5 reads: the conformance suite is harness-neutral: the real
codex app-serverbinary, driven byCodexHarness, passes it with none of pi's hooks or files, and removing one clause's behaviour fromCodexHarnessfails the suite. - The ask's first decision ends: as a conformance row filled from its documentation and proven against the real binary in tests, never shipped; the code unit's second implementation is
CodexHarnessdriving the real binary, not a scripted harness. - The TL;DR's cost sentence, the shape's description of Codex's row, difficulty 5 and the first "What would change our mind" row say the real binary where they said a fake: Codex's row is a client of Codex's protocol proven against the real binary and never shipped; the cost adds the two bot-side pieces the proof needs.
- The fourth "Why not X" reads: Why not build Codex for real now, or not at all? Neither, and not a fake either. A toy stub, or a fake that speaks Codex's protocol, would only prove that the suite passes our own reading of Codex's documentation, so the proof is the real binary with a scripted model, or OpenRouter, behind the proxy (stage A). That proof needs the Responses shape at the proxy and an MCP relay, so they are in stage A too. What only a shipped Codex needs, an image whose size nobody has measured (pi's cost 434 MB per image) and a preset on it, waits for a new decision with a user (stage B), and the gate's cannot stands until Codex changes. A second prompt surface, which record 0032 rejected, is what the gate keeps out.
- Boundaries loses "gated behind measurements, not scheduled": stage A adds the Responses shape as a route, a pin and a meter, forwarding to the provider table's upstream, OpenRouter first; the p99 of a relayed call, measured from the run store, is what the MCP relay must hold and the suite asserts.
- Rollout reads: two stages; only the first is scheduled. Stage A, one unit after the deletion step merges, in this order inside the unit: first the seam and pi's rows green (
contract.tswithHarnessandHarnessFacts;PiHarness;PiContainerrenamedHarnessContainer; the conformance suite whose rows are the validation criteria), then Codex's rows:CodexHarness; the pinnedcodexbinary installed where the tests run and driven as a child process on the bot-host class; a scripted model server speaking the Responses API beside the load suite's chat-completions one; the Responses route at the proxy with its pin and meter, OpenRouter as its first upstream; an MCP streamable-HTTP relay endpoint beside the harness routes. A runner that cannot run the binary fails the Codex rows loudly and never fakes them. The same unit carries the relaunch onExecSandboxRestartedErrorwith the credential rotation and the mid-run workspace re-attach, each behind its gate, as the ceiling over the settle-and-close floor #1219's own fix lands first. Scope: the run-loop seam, the two type blocks inharness.ts, one rename, a route and a meter inmodelProxy.ts, an endpoint inharnessRoutes.ts, four new files; a guess of under 3,000 lines with tests, checked when the plan is written. Same pull request:harness.mdand the re-pointed harness-pi.md rows. Stage B, a new decision with a user: Codex in an image, a preset on it, and the live receipts. - The gates section's opening sentence reads: the trap is shipping Codex (stage B) before stage A has proven the seam with the real binary, writing the two bot-side pieces before pi's rows are green, or landing the relaunch before its two assumptions are tested. Its "Stage A's second implementation" row reads: gate, the pinned
codexbinary runs on the test runner undersandbox_mode = "read-only"and its own sandbox (Landlock on Linux, Seatbelt on macOS) starts there, or the Codex rows fail loudly, and a fake in the binary's place is refused; the review attacks a scripted model answer the real model would never give, shaped to pass the gate, and a Codex sandbox that silently falls back to no sandbox so the write-and-network asks never fire. Its two "Stage B" rows become stage A rows: the Responses route, gated on pi's rows green first and the Responses request and usage schema diffed against the two shapes' pin and meter, the review attacking a pin that misses the output-cap field, a meter on the wrong usage block, a streamed answer metered twice; the MCP relay, gated on pi's rows green first and the p99 of relayed-call duration from the run store, which the suite then holds one call past through the real binary, the review attacking a call that outlives the request and runs twice, a door that admits an MCP call from a rotated bearer, and anmcpToolCallthat asks approval under the granular policy and is answered by nobody. "Stage C" becomes "Stage B": a user who wants a preset on Codex, and a new record. - Validation row 1 reads: the pinned
codexbinary, driven byCodexHarnesswith the scripted Responses model or OpenRouter behind the proxy, passes every row with none of pi's hooks or files; a runner that cannot run the binary fails these rows, never skips them. Row 11 reads: the Responses route pinsmodelandmax_output_tokensand meters that shape's usage block, proven by the real binary's calls through the proxy; the MCP relay holds a call past the measured p99 without running it twice; one refused command (a write underread-only) and one refused MCP call from a rotated bearer are visible in the record. - The first amendment's "Changed in place" list omitted the
patternfrontmatter line, which that change also rewrote; the frontmatter is frozen now, so the index row keeps the first amendment's wording ("a second adapter proven against a fake of its protocol") and this amendment is where the real-binary wording lives.
Amended 2026-09-15 — stage A is deferred; the shape to keep, and where hosted agents belong
Re-evaluation. The record was accepted on two grounds: a written seam before the deletion step, and a second row that tells a clause of the contract from an accident of pi. Both were served the same evening without code. The deletion step merged, the amendment of record 0032 carries pi's row as production runs it, and stage A's planning (three research passes over Codex's protocol and sandbox, the repository at the deletion step's head, and the parity of the two harnesses, then a flow analysis of the hard cases) found what the maintainer asked the exercise to find: the contract holds against a program that differs from pi on every clause, every place Codex fails is a guarantee Codex lacks and not a pi accident, and the places where the code is pi-shaped beneath a neutral name are now a list. The maintainer therefore deferred stage A: the assurance against lock-in was the purpose, and the seam's soundness is established on paper. Nothing in the two grounds regresses, because nothing was built that could rot; what could regress is the shape, which is why this amendment names the facts that must stay true. The execution plan that holds the research is filed beside the program plan as a plan that is not scheduled, so a later stage A starts from it rather than from this record alone.
Where hosted coding agents belong. Devin, Codex Cloud, Copilot's coding agent, Jules and the background agents of editors are not harnesses and the contract is what shows it: they hold their own model keys, run tools in their own sandbox with no per-call ask, keep their own transcript and survive on their own infrastructure, so the credential, gate and survival clauses cannot hold and the record clause holds only as far as their API reports. They are a child run whose executor is remote: spawned by the coordinator under the spawn seam, handed a repository-scoped token in the resident's second credential domain (record 0009), observed through the service's API and through GitHub as the shared record, and finished as a child's result. The shape to keep for them is a remote machine class at the spawn and executor seams, never a row in this record's table.
The facts that must stay true for stage A to remain possible. Each is a place the code is pi-shaped beneath a neutral name at the deletion step's head, found by the planning and left as it is; a change that hardens any of them is a change to make deliberately, not by accident.
- The container seam's
startmust take the binary, its arguments, the stdout filter and the run directory layout as inputs before the seam is renamed; today the start script namespiand pi's streaming-delta filter, and the layout is pi's. - The model proxy binds each route to one provider type; a third dialect needs the binding loosened to the set of wires a provider type serves, with the spec's first item rewritten in the same change.
- The bearer store has no rotation: minting resets the turn count and every hash, so a relaunch that mints would give each relaunch a fresh budget; a rotate verb keeps the count, the expiry and the span, and the new hash reaches the row before the old ones are dropped.
- The run loop has no mid-run path to
interrupted, and the workspace re-attach and refusal live on the resumed-dispatch path only; the settle-and-close floor that landed for the replaced container closes the run through the loop's failure path with that status and re-dispatches the request, which is the floor this record's survival clause names. - The kernel's boot id is the container's identity word, and a container replaced on a shared kernel keeps it; the typed runtime-replaced error from any container operation is the trigger a replacement can be trusted to raise.
- The relay is idempotent by the caller's call id, which pi's extension repeats and an MCP client does not; a relay over MCP must hold the response open, honour cancellation and refuse a duplicate spawn by name, or a spawned child is created twice.
Changed by this amendment: nothing above it; stage A's Rollout is suspended rather than rewritten, and the validation criteria stay [gap] with no unit to bind them until stage A is scheduled.
Amended 2026-09-16 — the second harness is OpenCode, built for real; stage A is scheduled; Codex stays the probe
Re-evaluation. Hours after deferring stage A, the maintainer redirected it: "lets do opencode/openapi instead of codex and actually wire it up." The record was accepted on two grounds, a written seam before the deletion step and a second row that tells a clause from an accident of pi. This change bears on the second and adds a third purpose the record did not have: a second harness that runs in production. Codex served the probe; every place it failed was a guarantee it lacked, and that finding stands as the documented alternative. OpenCode serves the build, and the reason is the contract itself: OpenCode meets every clause from outside its process, where Codex met four. Its provider layer speaks the Anthropic and OpenAI dialects the model proxy already serves, so the credential clause needs no third route; every built-in tool, the read-only ones included, asserts a permission that a single session rule {action: "*", resource: "*", effect: "ask"} turns into an ask the bot answers over HTTP, so the gate clause is met where Codex's could not be; its server admits an authored session over HTTP (POST /api/session/import), so the survival word is authored-session, stronger than pi's file and unlike Codex's own store; it has a steer with a durable inbox (delivery: "steer"), so the conversation clause is met; and its tools run as the process user with no sandbox of their own, which is pi's shape and what record 0007 wants: the container is the wall and the bot the decision. The first ground is untouched, and the deferral's list of facts becomes stage A's work, changed on purpose.
Decided. The second row is OpenCode's v2 line, @opencode/cli@2.0.3, pinned, driven over its server API with a client generated from @opencode/protocol@2.0.3, not from prose (its API reference pages do not exist yet). The v1 line (opencode-ai@1.18.31) is the fallback if v2's churn bites, and its row differs in two clauses: own store (import only through the CLI) and no steer verb. Relay: a local plugin custom tool speaking pi's /harness/tool protocol from inside the OpenCode process is the first road, because it needs no MCP endpoint on the bot and the relay's idempotency by the caller's call id holds as it does for pi; the MCP client over loopback with the bearer in its headers is the fallback road. Record: the store is the truth and the stream an accelerator; because the executor holds no stream open, a per-run tailer inside the container subscribes to the event stream and appends it, with the store's changed messages and pending asks at each step end, to one file the harness reads through pi's existing log transport on every container class, and the container seam gains one HTTP verb for the writes and the readiness probe. Gate: the ask arrives as permission.asked naming the tool call, is answered once or reject with the rule's reason as the message, never always; a tool success whose call never asked fails the run closed, as pi's does; the 90-second policy is the bot's, since no ask times out inside OpenCode. Identity: deny rules hide tools, so a preset without a workspace is a configuration shape (shell denied), not a refusal. Codex's bug and sandbox items in the third amendment no longer apply to the build; they stay as the probe's record.
What is built, and what is not. Both execution images and the bot image pin @opencode/cli@2.0.3 beside pi (about 206 MB unpacked per image; the wrapper's install script needs --allow-scripts); the retired harness: configuration word returns with two values, pi and opencode, mapping to a harness object in the wiring roster; the run loop still compares no string. A live receipt on a scratch coding task closes stage A. No preset defaults to OpenCode and no production configuration changes in stage A; the switch is a deployment's to flip per preset.
What could regress, and the gates that hold it. The v2 line is four days old, its API groups are marked experimental and its event stream is volatile by contract, so the client is generated and pinned, the record is rebuilt from the store at every step end, and the first day of the build closes the spike's fourteen unverified items before any unit is called done. OpenCode's headless server has an open-issue history (asks that hang a turn when nobody answers, a stream that closes mid-turn, memory growth under a hung upstream); the bot answers every ask within its wait, polls rather than trusts the stream, and bounds the process's lifetime to the run's. The install script is a supply-chain surface the pi line does not have; the image pins the exact version and proves opencode --version as the thread user. Every phone-home the binary knows (models catalogue, update check, share) is switched off, and the credential receipt is an egress log showing only the proxy and loopback.
Sequencing. The seam and the conformance suite over pi land first, harness-agnostic, so OpenCode plugs into a seam already holding pi; then the process and its image, the bridge (gate and record), the relay and the conversation with the authored session, the configuration word and the live receipt; the relaunch ceiling last, now cheaper for OpenCode than for pi because a rebuild is an import. The plan for stage A is rewritten for this row in the same change as this amendment; the "starts when" condition the program plan carried for the cheap slice is superseded by this direction.
Changed by this amendment: nothing above it in the body; it overrides the third amendment's deferral, the second amendment's Codex build (Codex is the probe, documented in Appendix B, and builds nothing), and the Rollout's second-row content; the validation criteria's [gap] rows bind to the rewritten plan's units. The three bot-side pieces the second amendment moved into stage A for Codex, the Responses route at the proxy with its pin and meter, the MCP relay endpoint on the bot and the scripted Responses model server, are withdrawn with it: OpenCode's provider layer speaks the two dialects the proxy already serves, so no client for a third route exists, and a harness that needs one is a change to this record. OpenRouter, which the second amendment named as that route's first upstream, lands instead as an upstream of the routes that exist, under the program plan's own unit and with no dependency on stage A. Validation row 11 is retired with the route; a row in its place asserts that the proxy serves exactly the two dialects and refuses a third.
Amended 2026-09-16 — the gate decides intent and the walls enforce effects; OpenCode continues with its gate as detection; no egress work
Re-evaluation. The record was accepted for two grounds: assurance that Switchboard is not locked to pi, and a seam sound enough that a second harness enters as a row and not as a rewrite. Building the second harness's process (the stage A plan's tenth unit) put the gate clause under a light the pi-only tree never had. The clause reads "every tool call it runs is decided by the bot", and for pi that is literally true: the decision lives in the bot, the extension inside pi can approve nothing, and a result with no decision beside it fails the run closed. On OpenCode the approval lives in its own server, answered by a reply the bot posts with a password that must sit in the server's environment; the model's shell runs as the same user, so it can read that password and reply to its own ask. The bot can detect such a reply and fail the run closed after at most one tool call; it cannot prevent it. That is a real asymmetry, and a smaller one than it first appears: on pi too, the gate decides the call and not its effects, since an approved shell command runs as the container user with everything the container holds, and a model that means to route around a refusal can do so through any approved command. On both harnesses the effects of an approved call are bounded by the walls: the container the run owns, the proxy holding the credentials, the token's scope and branch protection. The read identity's walls exist today (a read-only worktree and no write token; the gate's read rules restate them). The write identity's are partly rules: the one-push-target rule is not a wall, since the container's credential can push any unprotected branch, and the harness process's secrets are readable from the shell on either harness. None of this touches the grounds for acceptance; it corrects how the gate clause should be read.
Decided. The gate clause reads: the gate decides intent, the walls enforce effects. OpenCode continues, its gate as enforcement by detection written on its conformance row as the honest cannot: a permission.replied the bot did not send is GateBypassed and the run fails closed. Two walls are P2, by the maintainer's decision, named with the residual exposure so the choice stands in the open: a per-run credential that can push only to the run's branch (a push proxy in the bot or a scoped token), leaving a push to any unprotected branch and a GitHub API write with the container's token bounded by branch protection and the token's scope; and the harness process and the model's shell as different users, leaving the shell able to read the harness's secrets and, on OpenCode, forge a reply the bridge detects after one call. No egress allowlist and no egress receipt, and neither is planned: coding runs need open egress (registries, GitHub, whatever the tests call) and the platform gives no per-container egress policy, so an allowlist that let runs work would protect little; every egress line leaves the stage A plan and this record's spec.
What could regress, and the gates that hold it. A forged reply on OpenCode is detected by the bridge and fails the run closed, proven by the bypass row the eleventh unit adds to the conformance table beside pi's; a reply the bot sent is matched to the ask it answers by the request id. The P2 gaps are two [gap] rows in the spec that name their exposure, revisited at stage A's end with the record's other gap rows. The read identity's walls are unchanged and remain the model the write identity's walls are measured against.
Changed by this amendment: nothing above it in the body; the gate clause's reading; the stage A plan's OpenCode gate requirement and live-receipt requirement and its process and configuration units' verifications as described; and harness.md item 2 and its two new gap rows, in the same PR.
Amended 2026-09-18 — stage A's end: what the live reads proved, what they corrected, and what stays owed
Re-evaluation. The record was accepted on two grounds, a written seam before the deletion step and a second row that tells a clause of the contract from an accident of pi, and the fourth amendment added a third purpose, a second harness that runs. Stage A's code is complete on main and live on the release that rolled every Worker this morning. This amendment records what the first live reads proved and what they corrected, and checks each against those grounds: nothing below weakens the seam or the second row; the first two grounds gain a rule the live reads forced (the real binary is the gate for every clause), and the third is met with a caveat (the second harness ran a live coding turn and then ended on a finding of its own gate, so its clean run is still owed).
What the live reads proved.
- The survival clause, decision 2. On the release that rolled the resident under a live pi run, the typed-first transport-loss verdict (the executor's
runtime-replacedword decides; words decide only an untyped answer) took the whole path: the replaced verdict, the workspace re-attach refused by name (worktree-missingthat night;reuse-refused: no worktreeon the next roll, the refusal's wording having moved with the workspace-release follow-ups; both say the sandbox rollout recycled the container disk), the run closed and its request dispatched again as a new run in its thread, which ran to its budget. The same path ran again on the next roll. Criterion 5's human-gated receipt is met, with one correction to its text: the floor that holds live is settle, close and re-dispatch, not a relaunch in the new container, because a VERSION rollout recycles the disk (the design item below). - The ledger's landing rule. The first roll's successor ran untracked: its reservation raced the predecessor's fire-and-forget finish and was refused
thread-live. The follow-ups made a finish in flight a thing a claim waits on (the wait is the finish attempt's own promise, never a timer) andabandonedthe writer's last word. On the second roll the memory Worker's own log shows the successor refusedthread-liveand claimed 328 ms later, and the bot wrote nonot trackedwarning. That is the before and after the fix promised. - The skew clause. For three and a half hours the bot and the state store ran two releases ahead of the resident and the sandbox, because the resident preflight (#1494's rule) refused for two full thirty-minute windows on runs in flight; runs completed on both sides throughout, and nothing was forced. The clause's first sustained live exercise.
What they corrected: a table written from a manifest is wrong until the binary has spoken. Four places in stage A were written from protocol prose and corrected by the first real run of each. First and largest, the model reference: the fake accepted any reference, and the real-binary row ran under the one provider name that happened to match its test configuration, so the production provider name was never asked for; two live coding runs spent their whole budget silently before the reference named the configuration's provider on every call and every refusal of the server ended the run by name (#1516, fixed in the pull request that followed it). The resident probe's verdict by provenance (a 5xx with no Worker document is the platform's blip, not the resident's answer), found by the first sixteen-run live receipt. OpenCode's disposition table, which filed the shell tool's own process lifecycle (shell.created, shell.exited, shell.deleted) and the web-search catalogue the server registers at boot under the features this run turns off, so the first live coding run carried ninety-eight harness_error notes; the naming note itself had no once-per-kind rule. And the gate's reply path, which read every non-2xx as fatal on the direct path where the reset path already knew that an ask gone from the pending list is the server's answer: a reject cascades to the step's other pending asks, so the reply to a refused call's sibling answers 404 and the run died on the gate's own correct finding. The stage gates gain the rule these four share: the real-binary row is the gate for every clause, not the record clause alone, and it runs under production-shaped configuration. A harness's conformance rows run against the pinned binary before it enters an image, with the provider name, the model id and the identity production would give it, never the fake's convenient names, or the binary is asked a question it will never be asked live; the record table is bound to the manifest and asserted empty of harness_error across a real run's boot and a real tool call; the gate row includes a step with two calls, one refused.
Design items, not defects.
- A recycled thread workspace. A sandbox VERSION rollout recycles the container disk, so a relaunch cannot re-attach; the floor (close and re-dispatch) holds and the request is not lost. Restoring the thread's workspace at its recorded ref and replaying the settled calls is a design for a later record.
- The decline cascade. On OpenCode a reject cascades to the step's other pending asks and ends the execution with no further model call, so a step with two calls and one refusal ends the run with no answer, where a single refusal is read by the model and worked around. The loop should re-prompt with the step's refusals as it does after one (#1711).
- The resident preflight is a wait, not a hold. Under continuous load it cannot land a deploy, since new runs keep starting while it waits. The shape that lands it is an admission hold once a deploy is pending, the bot's own drain door; the resident's owner's gap, cited here because the skew clause is what covered the wait.
- A mention during the bot's roll is lost. Between the old generation's drain and the new one's socket, a mention gets no reaction and no card (#1716); the bot host's gap, not a clause of this contract.
- Where the survival clause is going: replacement by identity, not by wording. The typed-first verdict holds live, but its untyped half still reads the executor's words, and every new wording is a new classifier. The end state: the container names itself once at start, the resident stamps that id on every answer, and the harness compares it with the id it launched under; a mismatch is a replacement with no words needed, no answer is the liveness probe, the same id means the failure stands as reported. The wording classifiers retire to diagnostics. The in-place re-attach after a waitable-typed third shape (#1609) is the first half of that direction.
- Where steer fate is going: the server's own inbox events. OpenCode says what became of a follow-up in its typed stream (
session.inbox.enqueued,session.inbox.delivered,session.inbox.cancelled), so the loop should learn a steer's fate from those events as they arrive, not by re-reading the store at turn boundaries; a steer the server took but had not delivered when the finale's interrupt landed (#1598) is the case that shows the difference.
What stays owed. The pi scripted double that can play an already-running pi (#1482); the waitable-typed third shape's re-attach in place (#1609); a steer the server took but had not delivered at the finale (#1598); the store-only reading of the tombstone (#1640); the heartbeat under an attaching row (#1655); OpenCode's finale wording residuals (#1670); and the second harness's clean live coding run: the first ran the harness, a non-Anthropic model through the OpenRouter route and forty-eight tool calls, then ended on the gate's correct refusal of a credential read and the cascade above. The retake with a real task closes the live rows the stage A plan's thirteenth unit names.
Changed by this amendment: nothing above it in the body except criterion 5's wording (settle, close and re-dispatch is the floor that holds) and the stage gates' rule (the real-binary row is the gate for every clause); the OpenCode conformance rows and harness.md items 2 and 4 already carry the real-binary rule in the pull requests merged before this note.
Amended 2026-10-09: declarations belong to the existing launch
Re-evaluation. This record keeps authority and the durable record in the bot while each runtime implements registration and its protocol. Startup discovery duplicated metadata already owned by the run and coupled local registration to the bot's boot takeover. That dependency is unnecessary for the gate or relay clauses.
Both existing launch adapters now serialize only the registered run's names, descriptions and input schemas. The same snapshot supplies the adapter's local registration, prompt and allowlist. Definitions are data; each real tool call still asks the current bot and carries its original call id. The native gates, bearer binding, admitted allowance, custody and survival owners do not change.
This removes the new-launch discovery dependency rather than adding a shared retry client or another readiness controller. Retained processes keep their original files, so the discovery endpoint cannot retire without separate consumer evidence. Failed local registration remains closed. Shared generated-adapter tests and the existing conformance rows are the source proof; exact-head review, deployment and live recovery remain distinct.
Amended 2026-10-09: launch intent precedes producer facts
Re-evaluation. Policy captured only with process facts cannot distinguish a never-begun launch from a start whose response was lost. The original run's existing state owner must retain that distinction before another bot generation can act.
The resumable seed now records a prepared launch intent: original harness and logical command policy. Both adapters acknowledge its irreversible begun phase for each launch ordinal before starting a process. Observed process/session facts remain separate. A prepared launch continues under its original permissions and allowance; begun without a producer receipt stays held. Compatible recorded producers retain their existing continuation and explicit replacement protocol.
The existing run state and checkpoint transactions preserve policy, ordinal and phase. Only the existing vouched replacement decision admits a next ordinal; the number itself is no restart authority. Positive producer facts identify their launch ordinal, so older facts cannot clear a newer unknown start. Same-ID reservation reads retain the original prepared or begun state under the existing owner fence, avoiding another carrier through the dispatcher. There is no new controller, store, scheduler or permission source. Missing legacy evidence remains unknown and cannot be backfilled from current labels. Local restart and persistence proofs qualify the source; they do not establish the initiating production cause or authorize replay, deployment or physical retirement.