Skip to main content
Glama
Raylen-berry

DSH → Codex Delegate MCP

by Raylen-berry

DSH → Codex Delegate MCP

A governed stdio MCP server that lets DeepSeek Harness (dsh) delegate work to Codex on the same machine. As of 0.2.0 the engine is Codex's own codex mcp-server (tools codex / codex-reply), and this file is a thin governance proxy in front of it.

Why not mount codex mcp-server directly: on that server the model chooses sandbox (the enum includes danger-full-access), cwd (any path on the machine), config (a free-form object that overrides any Codex setting, including MCP servers) and base-instructions. Mounting it as-is hands a privilege knob to the delegated agent — or to anything it reads. The proxy takes only prompt, workspace, mode and thread, pins approval-policy: never, and never forwards config/base-instructions/model.

It does not expose the current Codex desktop conversation. It uses the Codex account and configuration already available to the local CLI.

Safety defaults

  • The default tool mode is read-only.

  • The server only accepts the directory where dsh was started, or a descendant of it.

  • workspace-write is disabled unless the DSH patch explicitly sets CODEX_DELEGATE_ALLOW_WRITE: 'true'.

  • danger-full-access is never available.

  • The server uses stdio; it does not open a network port.

Related MCP server: dsh-codex-bridge

Pre-release check (same command in CI and locally)

Every push / PR runs .github/workflows/ci.yml, which does exactly one thing: npm test. That is the same command you run locally — no dependencies, no network:

npm test                       # = node tools/run-all.mjs
node tools/run-all.mjs --list  # list what runs, what is excluded, and why

tools/run-all.mjs runs every suite to completion before summarizing, so one broken suite never hides the next; any non-zero suite makes npm test exit 1, which turns CI red. CI runs a Node 20/22/24 matrix on windows-latest (the suite is written platform-agnostically — process.execPath + os.tmpdir + path.join, no shell — but only Windows was actually measured on this machine, so no unverified runner was added).

Measured locally (Node 24.9.0):

Suite

Local result

tools/verify-engine-restart.mjs

3 passed (fake engine = a mcp-server script this suite writes itself; real Codex is never called)

tools/verify-runs.mjs

58 passed (same fake-engine trick, plus a scenario file; covers the run ledger and cancel/resume/retry)

tools/verify-config-drift.mjs

19 passed (checks the drift checker against synthetic configs in a temp dir + one read-only run over the real pair)

Nothing is excluded in this repo. All suites are offline: CODEX_BINARY points at node itself and the fake engine is a script the suite writes into its own temp workspace, so no model is called and no network is touched. tools/run-all.mjs fails the whole run if a new tools/verify-*.mjs is not registered in its SUITES list, so a suite cannot be added silently.

Test isolation is asserted, not assumed. The ledger defaults to $DSH_HOME/dsh-codex-delegate-mcp/runs/, and the suites start a real server.mjs that inherits this process's environment — so a local npm test wrote fake delegation records into the user's real data directory once (measured 2026-09-14: 24 rows of the restart fixture leaked into the live ledger). Two independent guards now prevent that:

  1. tools/run-all.mjs runs every suite with CODEX_DELEGATE_RUNS_DIR=<os.tmpdir()>/dsh-codex-delegate-runs-selftest-<pid>, so a suite — including one added later — cannot reach $DSH_HOME even by omitting it. Each of the two suites that start a bridge also sets that variable itself, so running a single file directly is equally safe.

  2. After the suites finish, run-all.mjs re-reads the real $DSH_HOME/dsh-codex-delegate-mcp/runs/ and compares file-level sha256 against the snapshot taken before them. Any added, removed or changed file makes the whole run exit 1. When that directory does not exist (CI), it reports "not applicable" rather than failing.

Reverse verification of the run ledger

CODEX_SERVER=<path to another server.mjs> node tools/verify-runs.mjs points the same assertions at a different build. Measured against the pre-0.3.0 server.mjs materialized from git HEAD in a temp git worktree: 14 passed / 34 failed, exit 1 — the old build has no run records at all, so every record assertion fails. (Report the failure count, not "the suite is green": the 14 that still pass are the ones whose subject exists in both builds, such as a DELEGATION FAILED header still being present, or a secret-absence check that trivially holds when nothing was recorded.)

Get it

The public repository is dsh-codex-delegate-mcp; clone it and the folder name becomes the repo name, not the dsh-codex-delegate-mcp shorthand used in the examples below. Clone it anywhere, then adjust two machine-local facts in dsh-codex-delegate.cordis.patch.yml: the args path to server.mjs, and CODEX_BINARY, which must point at a real codex executable (an .exe; on Windows a .cmd shim fails a shell-less stdio spawn). CODEX_DELEGATE_ROOT and CODEX_DELEGATE_WRITE_ROOT are pinned to one root for the bridge's whole lifetime — widen or move them to change the scope.

Enable in DSH

The folder location and the interpreter path are baked into the patch, so keep this folder where it is on this machine: D:\DeepSeek\dsh-codex-delegate-mcp (see "Get it" above for a fresh clone elsewhere).

For a terminal / CLI run:

dsh web --patch .\dsh-codex-delegate-mcp\dsh-codex-delegate.cordis.patch.yml

For a terminal/headless run:

dsh --profile headless --patch .\dsh-codex-delegate-mcp\dsh-codex-delegate.cordis.patch.yml "Use mcp__codex__delegate_to_codex to inspect this workspace and summarize its purpose."

For DSH Desktop (the app builds its own argv, so --patch is not reachable): put the same - insert: block into the profile's user patch layer %DSH_HOME%\profiles\web\cordis.patch.yml. That layer is watched and re-applied on save — no restart needed (it re-applies on a config diff, not on mtime; see "Reloading" below). That is the route used on this machine. Validate the composition without booting anything:

dsh web --dump-config --patch .\dsh-codex-delegate-mcp\dsh-codex-delegate.cordis.patch.yml

DSH will discover the tool as mcp__codex__delegate_to_codex.

In DSH, make the delegation explicit, for example: 请委派给 Codex:检查当前项目的测试失败原因,只读分析。 The bridge does not take over this Codex desktop conversation; it starts a fresh local Codex CLI task.

Machine-local corrections verified on 2026-09-04 (codex-cli 0.148.0-alpha.9)

Historical: these three bugs belonged to the codex exec engine and are gone in 0.2.0, where Codex's own MCP server is the engine. They are kept because they explain why the wrapper exists at all — and item 4 (a real executable for command, plus CODEX_BINARY) still applies.

The original package could complete an MCP handshake but never a real delegation. Three defects, all fixed in server.mjs:

  1. --sandbox <mode> and --approve-for-me are mutually exclusive, so every call died with exit=2 before Codex started. Now read-only pins --sandbox read-only and workspace-write uses --approve-for-me.

  2. spawn() inherited an open stdin pipe. codex exec appends stdin as a <stdin> block and waits for EOF, so the call hung until the timeout killed it. Now stdio: ['ignore', 'pipe', 'pipe'].

  3. A non-zero exit or timeout was reported as a successful tool result. Now it is returned with isError: true.

Two configuration corrections in the patch (not in server.mjs): command must be a real executable — on this machine node on PATH resolves to a .cmd shim, and a stdio spawn without a shell fails on it — and CODEX_BINARY must point at C:\Users\Administrator\.codex\plugins\.plugin-appserver\codex.exe, because codex is not on PATH here.

CODEX_DELEGATE_ROOT is pinned to D:\DeepSeek: the bridge accepts one root for its whole lifetime, so it cannot follow whichever workspace a session opens. Change it there to widen or move the scope.

Allow edits

Write mode is fenced, not global. Two envs decide it:

env

effect

CODEX_DELEGATE_ALLOW_WRITE

master switch; 'false' refuses workspace-write before any spawn

CODEX_DELEGATE_WRITE_ROOT

optional narrower root. When set, a write call whose workspace is outside it is refused before spawning, and the recorded cwd / workspace_roots are re-checked against it afterwards

So "open the write gate" need not mean "free to write anywhere under the read root". The live config on this machine runs ALLOW_WRITE: 'true' with WRITE_ROOT: <workspace>\DeepSeek子代理: Codex may write into the deliverables folder and nowhere else. To withdraw write capability, set ALLOW_WRITE: 'false'. Bump CODEX_BRIDGE_REV after either change, or the running child keeps the old environment.

One nuance to read correctly in a result: a fresh write run uses --approve-for-me and reads back Approval: on-request; a resumed write run must use -c approval_policy="never" (because exec resume accepts no approval flags) and reads back Approval: never. Only the second is fully unprompted.

Activity: what Codex did, not just what it answered

codex exec --json would cost the policy header, so the bridge keeps the plain invocation and reads Codex's own rollout record ($CODEX_HOME/sessions/**/rollout-*<session-id>.jsonl) for the turn that just ran. Every result then carries:

Steps: 10

Codex activity:
  $ exec: const r = await tools.shell_command({command: "Get-ChildItem …"…
  → result: Script completed Wall time 0.3 seconds Output: Exit code: 0 …

The last 14 steps (each truncated to 240 chars) are returned; pass include_activity: false to drop the block. This is also where the safety read-back comes from: turn_context records sandbox_policy.type, approval_policy, cwd, and workspace_roots per turn, so the bridge compares the recorded policy against the requested one and fails the call on a mismatch or on any root outside the allowed root — instead of trusting what it asked for.

One practical side effect of watching the activity: Codex's shell child runs PowerShell in ConstrainedLanguage mode, so [PSCustomObject]@{…} and .NET static calls fail there. Phrase delegated prompts with cmdlets and core types, or expect a wasted round trip.

Multi-step delegation

Each result opens with a read-back block:

Codex completed the delegated task.
Session: 01a07fef-4494-76d3-b674-9ae5821898ec
Workdir: D:\DeepSeek
Sandbox: workspace-write [workdir, /tmp, $TMPDIR]
Approval: never
Model: gpt-5.6-luna
Tokens: 17956
Next step: pass resume_session: "01a07fef-..." to continue this exact conversation.

Pass that id back as resume_session to continue the same Codex conversation instead of starting a fresh one — the follow-up sees what Codex already read and wrote. Sessions live in $CODEX_HOME/sessions/rollout-*<id>.jsonl, so a resume survives a bridge restart; deleting those files does not.

codex exec resume accepts neither --sandbox, --approve-for-me, nor --cd, so a resumed run pins its policy through -c sandbox_mode=... (plus -c approval_policy="never" for writes), and the bridge then verifies the policy from codex's own header: a mismatch — or a workdir outside the allowed root — fails the call instead of reporting a success.

Reloading after editing server.mjs

The harness spawns the bridge once, at mount. Editing server.mjs alone changes nothing, and touching only its mtime changes nothing either: the layer re-applies on a config diff. Bump CODEX_BRIDGE_REV — in the live copy under %DSH_HOME%\profiles\web\cordis.patch.yml, past whatever value it already has — to force the stdio child to respawn and pick up new code. The repo template's copy of that key is documentation; see "Which copy of the config actually takes effect" above, and use tools/check-config-drift.mjs to see whether the two copies currently agree.

Verified on 2026-09-04, through DSH's own MCP client (codex-cli 0.148.0-alpha.9)

Case

Result

handshake + tools/list

pass

workspace outside the allowed root

refused before spawning

workspace-write while the gate is off

refused before spawning

read-only delegation + policy read-back

pass, Sandbox: read-only

resume_session context continuity

pass — recalled a value from the earlier turn

write delegation

pass — file created with the requested bytes, checked on disk

write + resume

pass — same session id, Approval: never in force

codex argument error / timeout

reported as isError: true with DELEGATION FAILED

Cost note: one delegated run persists a ~60–80 KB rollout-*.jsonl, and --ephemeral is deliberately not used because it would break resume_session. Delete old rollouts to reclaim space.

Requirements

  • Node.js 20 or newer

  • DeepSeek Harness CLI (dsh) with @deepseek-ai/dsh-mcp-client available

  • Codex CLI authenticated on the same machine

No npm package installation is required for this MCP server itself.

Which copy of the config actually takes effect (and why the repo one is only a template)

On DSH Desktop the bridge is mounted from %DSH_HOME%\profiles\web\cordis.patch.yml — the profile's user patch layer. dsh-codex-delegate.cordis.patch.yml in this repo is a template: editing it mounts nothing by itself. It is useful for --patch CLI runs, for --dump-config validation, and as the documented shape of the block.

That means two hand-maintained copies of one block, which drift — measured on 2026-09-14: the live file was already at CODEX_BRIDGE_REV: '14' (untouched since 09-11) while this repo still said 13, so "aligning the repo to 14" produced no config diff and the host would not have respawned the stdio child — the new tools would simply not appear. Two rules came out of it:

  • Bump CODEX_BRIDGE_REV in both copies whenever server.mjs changes behavior, and bump it past whatever the live copy currently says. Equal values mean no diff, and no diff means no remount. (Remounting restarts the resident engine, so open threadIds die with Session not found.)

  • Check for drift instead of trusting the copies. tools/check-config-drift.mjs is a read-only comparison of the two files' key values (including CODEX_BRIDGE_REV):

    node tools\check-config-drift.mjs                 # 0 = consistent / 1 = drift / 2 = usage problem
    node tools\check-config-drift.mjs --live <path> --template <path>

    A key written in one copy but omitted in the other counts as equal (≈) only where the omitted value is the built-in default — which is why CODEX_DELEGATE_RUNS_DIR shows as ≈ and not as drift. This check is what a "config is actually applied" doctor step should own; until dsh-doctor exists, this script is that check. tools/verify-config-drift.mjs asserts the checker itself (19 assertions, all against synthetic copies in a temp dir) so a wrong verdict cannot pass silently.

0.2.0 — engine semantics (measured 2026-09-08)

Threads live in the engine process. Codex's mcp-server keeps a conversation in memory, so the proxy holds one persistent engine instead of spawning per call. A per-call process made every codex-reply fail with Session not found — that was the first thing this version had to fix. Consequences to know:

  • bumping CODEX_BRIDGE_REV, or anything that remounts the row, restarts the engine and invalidates open resume_session ids; the bridge says so explicitly in that error.

  • concurrent delegations multiplex over the one engine by JSON-RPC id; Codex tags its codex/event notifications with _meta.requestId, so a call only ever sees its own run.

Policy is read from the event stream, not inferred. The engine emits session_configured with permission_profile, approval_policy, cwd, model and rollout_path. The proxy compares that against what was asked and cancels the running turn on a mismatch (notifications/cancelled), so an over-privileged run stops instead of being reported after the fact. codex-reply does not re-emit the event, so a resumed turn is vouched for by the ledger entry recorded when its thread was created; a thread the ledger doesn't know is reported unverifiable.

Activity comes from the same stream: item_completed for CommandExecution (argv + exit code + output), FileChange (the whole patch), WebSearch, McpToolCall, plus token_count total usage and the account rate-limit window.

Case

Result

read-only delegation, policy read-back

pass — Sandbox: read-only, Approval: never, Steps: 2, command argv and exit code shown

resume the same thread

pass — answered from context, Sandbox: read-only (vouched from creation)

resume a read-only thread as workspace-write

refused before spawn — created with mode "read-only"

workspace-write inside the write fence

pass — Codex created the file itself, Workdir = fenced dir, patch shown in activity

mode: danger-full-access

refused — never available through this bridge

engine hygiene

1 proxy + 1 engine, no leftovers

Cost note, measured: a fresh thread on the official engine starts at ~22k tokens even for a one-command task, because mcp-server loads the skills and plugin context into every new session (codex exec cost 574 tokens for the same trivial prompt). Resuming is incremental (a second turn showed 33k total against 22k at creation). For many tiny pings the older codex exec path is cheaper; for anything multi-turn or write-scoped this one is worth it.

Reverse hand: Codex → DSH (dsh-inbound-mcp.mjs)

Codex has no ACP client, so the way in is the other half of the same protocol pair: this bridge is an MCP server that Codex mounts, and it speaks ACP to dsh --profile acp — the supported automation surface for driving a persistent DSH agent.

Tools exposed to Codex: dsh_run(task, cwd?, timeout_seconds?), dsh_reply(session, task), dsh_sessions(limit?). One persistent dsh --profile acp child serves them all, and unlike Codex threads, ACP sessions survive a bridge restart (session/list / session/resume).

Mount it from a Codex config layer ($CODEX_HOME/<name>.config.toml, used with codex exec -p <name>):

[mcp_servers.dsh]
command = "C:\\Program Files\\nodejs\\node.exe"
args = ["D:\\DeepSeek\\dsh-codex-delegate-mcp\\dsh-inbound-mcp.mjs"]
cwd = "D:\\DeepSeek"
startup_timeout_sec = 20
tool_timeout_sec = 900
default_tools_approval_mode = "approve"
env_vars = ["DSH_DELEGATE_DEPTH", "DSH_INBOUND_ROOTS", "DSH_HOME", "PATH", "HOME", "USERPROFILE", "SystemRoot", "COMSPEC", "TEMP", "TMP", "APPDATA", "LOCALAPPDATA", "CODEX_HOME"]

[mcp_servers.dsh.env]
DSH_INBOUND_ROOTS = "D:\\DeepSeek"
DSH_HOME = "C:\\Users\\Administrator\\AppData\\Roaming\\dsh-desktop\\harness"

Two Codex defaults cost me a failed round each, so take them literally:

  • default_tools_approval_mode — an MCP tool call wants approval and codex exec has nobody to ask, so the call dies as user cancelled MCP tool call (stderr mcp: dsh/dsh_run (failed)) while the bridge is perfectly healthy. Easy to misread as a bridge bug.

  • env_vars — Codex does not hand an MCP child its full environment. Without DSH_DELEGATE_DEPTH in that list the bridge reads depth 0 and the recursion guard silently never fires. The bridge now prints Delegation depth seen by this bridge: N in every result so this can't fail quietly again.

Two-tier mounting (decided 2026-09-08)

The same [mcp_servers.dsh] block now lives in both places, with different approval modes on purpose:

Where

Approval

Effect

Codex base config.toml

default_tools_approval_mode = "prompt"

any interactive Codex session sees the tools, and each dsh_run/dsh_reply asks the human first — an injection-driven automatic call stalls in front of the user

-p dshreverse overlay

"approve"

unattended codex exec -p dshreverse … keeps working without a human at the keyboard

Rationale: merging into the base config makes the reverse hand reachable from every Codex session, which is exactly the surface you do not want callable silently by file contents (prompt injection). prompt in the base layer makes it loud; the overlay re-opens it for automation you explicitly start. Delete the [mcp_servers.dsh] block from config.toml to withdraw entirely (backup: config.toml.bak-before-dsh).

Audit trail

Every inbound call appends one JSON line to dsh-inbound-audit.jsonl next to the bridge (override with DSH_INBOUND_AUDIT, disable with 'none'): timestamp, depth, tool, workspace, task head (200 chars), reply head, ok/blocked. Both a successful run and a guard refusal are recorded — this is the "who asked for what, when, and what happened" log to review when a session acted on its own.

Ring break, two independent layers

DSH → Codex → DSH → Codex has no bottom unless something refuses. Two separate things refuse:

  1. Depth marker. The forward bridge spawns Codex with DSH_DELEGATE_DEPTH = own depth + 1. The inbound bridge refuses any call at depth ≥ DSH_INBOUND_MAX_DEPTH (default 0) unless DSH_INBOUND_ALLOW_NESTED=true, and the refusal states its own reason so it can't be mistaken for a crash.

  2. Composition. The inbound bridge starts dsh --profile acp, and the acp profile does not carry the mcp-codex-delegate row (verified with dsh --profile acp --dump-config). An inbound DSH agent has no mcp__codex__* tool to reach for, guard or no guard.

Permission for inbound work

Measured here: an inbound task runs workspace-writable inside the requested cwd, which must sit inside DSH_INBOUND_ROOTS — a delegated DSH agent created reverse-write-check.txt with no approval round-trip. Anything beyond that arrives as a *_request_permission, which the bridge answers deny plus a NEEDS USER APPROVAL (denied by bridge): … line so it reaches a human; the bridge never escalates on its own. Honest caveat: that deny path is implemented but was never exercised, because no inbound agent asked.

Case

Result

Codex calls dsh_run, DSH agent counts files

pass — RESULT=ok, agent ran a real pwsh tool call, answer 4 matched an independent count

same call at DSH_DELEGATE_DEPTH=1

refused — recursion guard: ... depth 1 (limit 0), RESULT=blocked, mcp: dsh/dsh_run (failed)

inbound write inside the root

pass — file created by the inner DSH agent, content checked on disk

MCP boot without default_tools_approval_mode

failed with user cancelled MCP tool call (documented above)

guard without env_vars

silently depth 0 → guard never fires (documented above)

0.3.0 — run ledger: 批次 / 当前步骤 / 失败原因 / 产物 + 取消 · 续跑 · 仅重试失败项

Every delegate_to_codex call now leaves one queryable record. The ledger is an added side channel: delegate_to_codex's input schema, sandbox policy, error wording and result body are unchanged, except for one added header line Run: <runId> [status/kind].

Codex completed the delegated task.
Run: 4c95c806-3070-4ff9-af4d-670806c329e0 [succeeded/completed]
Thread: 01a07fef-4494-76d3-b674-9ae5821898ec
…
Steps: 3
Artifacts: 1 (C:\Users\…\AppData\Roaming\dsh-desktop\harness\dsh-codex-delegate-mcp\runs)

Where it is stored

$DSH_HOME/dsh-codex-delegate-mcp/runs/YYYY-MM-DD.jsonl, overridable with CODEX_DELEGATE_RUNS_DIR; if neither DSH_HOME nor that variable is set (bare CI, a hand-started bridge) it falls back to <os.tmpdir()>/dsh-codex-delegate-runs.

One JSONL file per day, and every state change appends a line — the same runId written later wins. So a run in progress is already readable (running, with the steps so far), and a process killed mid-run leaves a complete readable file rather than a half-written JSON. A record that outlives the bridge process simply stays running; nothing repairs it afterwards. Files older than 14 days are pruned on the next list. There is no writer lock: the bridge process is the only writer in practice.

What a record contains

Field

Meaning

runId

one per delegation; retry_codex_run creates a new one and points back via parentRunId

batchId

optional batch_id argument on delegate_to_codex, purely a grouping label

status / kind

running · succeeded · failed · cancelled, plus a machine-readable reason kind

steps / stepCount / currentStep

the digest of the same codex/event stream the result header already summarizes

artifacts

workspace paths Codex actually touched (from file_change events), or paths seen in command activity

error

{kind, detail} — the failure/cancel reason as recorded

startedAt / finishedAt / durationMs

wall clock; while running, finishedAt is null

threadId / resumedFrom / resumable

the Codex thread this run used, and whether it can still be continued

engineGeneration

the bridge engine's generation for that run, reset to 0 when the engine retires — the number answers "was an engine live, and is this the one a later restart replaced", not "how many engines has this process ever started"

promptChars / promptHead

length, plus the first 200 characters only, redacted

result

tail of the final text, token total, rate-limit window

workspace / mode / timeoutSeconds / pid

the request as the bridge pinned it

kind is one of: timeout, policy, client_cancel, engine_lost, tool_error, thread_lost, refused (the bridge refused before spawning), completed.

Three new tools (two read-only, one that runs Codex again)

Tool

What it does

list_codex_runs {limit?, status?, batch_id?}

most recent runs, newest first, one line each

get_codex_run {run_id}

one record in full: status, kind, failure detail, steps, artifacts, times, thread, resumability

retry_codex_run {run_id, prompt, timeout_seconds?, include_activity?}

continues that run's thread (codex-reply with the recorded threadId) and records a new run pointing back at it

A refusal (a workspace outside the allowed root, workspace-write while the gate is off, a thread resumed under a different mode) also leaves a record — failed/refused, with no thread and no steps — and its error text now leads with Run: <id> [failed/refused] so the record is findable. The original error wording is unchanged below that line.

The bridge also emits an MCP notification notifications/tools/run after each run settles. It is a notification, not a request: a client that does not know it (including DSH's own bridge today) drops it, so nothing is pushed — the three tools are the way to read the ledger.

Cancel · resume · retry-only-failures: what actually works

Nothing new was invented for cancelling. The three cancellation paths already existed; the record now names them instead of reporting them as generic failures.

Capability

Support level

Mechanism / limit

Cancel (timeout)

works

the bridge's own wall clock: on timeout_seconds it sends notifications/cancelled and lets the caller settle, recorded as cancelled/timeout. codex has no per-turn cancellation that waits for confirmation, so the turn may keep running inside the engine — the record says "we stopped waiting and told it to stop", not "Codex stopped"

Cancel (policy mismatch)

works

session_configured reports a sandbox/approval/cwd looser than requested ⇒ same cancel path, recorded as cancelled/policy

Cancel (client)

works, rarely used

an MCP notifications/cancelled for an in-flight tools/call is mapped to that call's runId and recorded as cancelled/client_cancel. The transport-level cancel is what DSH itself has never sent, so this path is exercised by the test suite, not by daily use

Cancel (explicit "stop run X now" tool)

not supported

there is no cancel_codex_run: cancelling from the record would need a new kill path, and the repo has none today

Resume

works while the engine lives

retry_codex_run re-declares nothing — it replays the recorded threadId into codex-reply, and the original workspace/mode are reused and re-checked (a resume can never widen them). Codex threads live in the engine process, so after an engine crash, or a CODEX_BRIDGE_REV bump that remounts the child, the reply fails with Session not found and the new run is recorded as failed/thread_lost. Records do not survive the bridge process as resumable threads

Retry (one run)

works

retry_codex_run {run_id, prompt} retries the single run you name, recording lineage through parentRunId

Retry only the failed items of a run

not applicable here

one delegation is exactly one prompt, one turn — there is no multi-item list to select from. steps are a digest of what Codex did, not addressable work items, and re-running a step is impossible: a step is a command Codex already ran inside its own turn. So "only the failed items" collapses to "retry the single failed run" (above). If you need per-item retry, the granularity has to come from the caller: give each item its own delegate_to_codex call with a shared batch_id, then list that batch and retry the runs whose status is failed/cancelled one runId at a time

Push updates while a run is in progress

not supported

records are written during the run, but only when something happens (event digested, run settles); a run that produces no events writes nothing, and no client is notified unless it understands notifications/tools/run

Per-step retry, run cancellation, cross-restart threads

not supported

as above

Credential hygiene is unchanged and mechanical: every string written to the ledger goes through the same redact() used for tool results (Bearer …, sk-…, api_key/token/secret = …), the full prompt is never stored — only its length and a redacted 200-character head — and the verify-runs suite asserts that no Bearer/sk-/api_key= plaintext reaches the file.

Measured end to end with the fake engine (tools/verify-runs.mjs, 58 assertions): a successful delegation produces a complete record; a tool-layer error is failed/tool_error with the engine's own text; a hung engine is cancelled/timeout after the bridge's clock; a killed engine is failed/engine_lost (and the next delegation reconnects); a client cancel is cancelled/client_cancel; a retry reuses the recorded threadId in codex-reply and links back via parentRunId; list_codex_runs is newest-first; and the ledger contains no credential plaintext.

Related MCP Connectors

Related MCP Servers