Skip to content

0.14.0 — the Claude approve-and-execute experience, on Codex - #12

Merged
thaildhe172591 merged 6 commits into
mainfrom
feat/mcp-approver
Sep 14, 2026
Merged

thaildhe172591 merged 6 commits into
mainfrom
feat/mcp-approver

Conversation

@thaildhe172591

Copy link
Copy Markdown
Owner

0.13.0 put the harness on Codex but left approval on a second terminal. This gives Codex the Claude approve-and-execute UX — the agent asks, a select prompt appears, the developer clicks Approve, the write runs — inline, without weakening the gate.

Why an MCP approver

Codex's hooks cannot read a human's answer to a prompt and it has no custom question tool (openai/codex#28833, #23465), so Claude's AskUserQuestion + hook mint has no analog. The one mechanism that carries a verified human choice back to pythia is MCP elicitation (Codex ≥ v0.120): a server issues a structured request, Codex renders the select UI, and the answer returns to the server — the agent cannot fabricate it. IDEAS.md already sanctioned a write-free MCP approver (writes never go through MCP).

What changed

  • pythia mcp — a stdio JSON-RPC server, stdlib only, write-free, one tool pythia_approve(token). It builds pythia's own card from the journal, issues an elicitation/create with a {Approve, Reject} enum, and mints the same grant the console mints (approver="mcp") only on an explicit accept-with-Approve. A Reject, decline, cancel, unknown/applied token, or any transport error fails closed.
  • pythia install registers [mcp_servers.pythia] + mcp_elicitations = true in .codex/config.toml and writes .codex/requirements.toml forbidding danger-full-access (which auto-approves elicitations) and never. Both are append-only and refuse to touch a file that already defines those tables — the stdlib cannot safely merge TOML, so a conflict prints what to add instead. --no-hooks skips it; -g does not register the server.
  • The write path is untouched: apply --confirm still verifies the grant itself. The console pythia approve <token> stays as the fallback for older Codex and headless runs — nothing regresses.

Verification

  • New suite tests/test_mcp.py, wired into CI and the bench suite list: transport handshake/tools-list, the fail-closed mint logic (a fake elicit for every accept/reject/decline/cancel/error path), the full mcp_serve loop via a scripted client, and both TOML merges (append, idempotent, preserve, refuse-on-collision).
  • The real python -m pythia mcp subprocess was driven over OS pipes end to end: initialize, tools/list, a pythia_approve call, an elicitation answered Approve, a grant minted approver="mcp".
  • All suites green locally (phase1/2/3/5 + mcp + install).
  • Not proven here: a live Codex actually rendering the elicitation and returning the choice, and that the elicitation/create method name matches the installed Codex — the field test. The method name is isolated in one constant (MCP_ELICIT_METHOD) if it needs adjusting.

🤖 Generated with Claude Code

thaildhe172591 and others added 6 commits September 14, 2026 13:35
A new stdio JSON-RPC surface for the pythia MCP approver: mcp_handle answers
initialize (echoing the client's protocol version, declaring a tools
capability), the initialized notification (silently), and tools/list (one
tool, pythia_approve). Unknown methods get a JSON-RPC method-not-found error,
never a crash. Hand-rolled with json + stdio, stdlib only.

tests/test_mcp.py is new and wired into CI and the bench suite list, so the
whole approver is CI-covered from the first commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mcp_approve_decision is the security-critical logic, kept transport-free: it
takes an elicit(message, schema) callable, builds pythia's own card from the
journal, and mints the grant (approver=mcp) ONLY when the elicitation returns
accept with decision Approve. A Reject, a decline, a cancel, an unknown or
applied token, or an elicit that raises — all fail closed, minting nothing.
Reuses approval_card and mint_grant, so an MCP-minted grant is identical to a
console one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mcp_serve is the stdio loop: newline-delimited JSON-RPC in and out. A
tools/call for pythia_approve runs the elicitation round-trip inline — send
elicitation/create, read until the matching-id response, ignore anything
interleaved — then answers the call with the decision's tool result. One call
in flight at a time, which is how Codex drives tools in a turn. cmd_mcp runs
it on real stdio; the command is registered no-DB and given its place in the
guide so the coherence lint passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stdlib cannot parse or write TOML on 3.9, so these never parse it. Both
merges use a marker-delimited block: merge_codex_mcp_config appends
[mcp_servers.pythia] + mcp_elicitations when neither table is present (else
refuses, so the developer's TOML is never corrupted); merge_codex_requirements
writes a fresh requirements.toml forbidding danger-full-access and never (else
refuses on an existing file). The caller prints what to add on a conflict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cmd_install's project branch now calls report_codex_mcp after the Codex hooks:
it registers [mcp_servers.pythia] in .codex/config.toml, enables
mcp_elicitations, and writes .codex/requirements.toml forbidding
danger-full-access — the pin that keeps elicitations reaching a human. On a
table collision or a pre-existing requirements.toml it prints what to add
instead of touching the file. --no-hooks skips it; -g does not register the
server, as the guide stays per-project.

Verified end to end by driving the real  subprocess over
OS pipes: initialize, tools/list, a pythia_approve call, an elicitation
answered Approve, and a minted grant with approver=mcp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A write-free MCP approver (pythia mcp) gives Codex the inline approve-and-run
UX via MCP elicitation: the agent calls pythia_approve, Codex shows the card
as a select prompt, and the developer's Approve mints the same grant the
console mints. install registers the server and pins the sandbox so
elicitations always reach a human. Four manifests bumped, CHANGELOG in the
repo voice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thaildhe172591
thaildhe172591 merged commit 19be1e8 into main Sep 14, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant