cyberine
Providers

OpenAI Codex CLI (headless)

Model: CYBERINECODEXMODEL if set, else Codex's configured default (no -m added). Deliberately unpinned (kd7ax, 2026-09-09): Codex 0.153.4 made GPT-6-Astra the bundled default, and Cyberine keeps...

  • Kind: headless coding agent (subprocess)
  • Official docs: https://developers.openai.com/codex/cli · /noninteractive · https://github.com/openai/codex
  • Cyberine adapter: CliAgentAdapter via makeCodexHeadlessAdapter (build worker codex) and makeCodexAdapter (read-only --json)
  • Researched/verified: 2026-06-07, Bedrock models + writes-mode gotcha added 2026-07-10 — confidence high. Updated 2026-07-17 (T623/T625/T631): both factories now forward Task.images as repeated -i <path> args (critical bug fix — task.images was documented but never wired); the headless build worker also passes --dangerously-bypass-hook-trust (Codex's stable hooks can otherwise stall headless on a trust-confirmation prompt the approvals-and-sandbox bypass doesn't cover); the read-only variant gained an opt-in --output-schema <path> (structured output). Updated 2026-08-20 (T656, live-verified against the installed codex; corrected 2026-08-21 -- the earlier text said v0.148.0, which was the latest upstream stable at the time, not the installed binary): the headless build worker now also takes --output-schema via CYBERINE_CODEX_OUTPUT_SCHEMA; the read-only makeCodexAdapter is marked LIBRARY-ONLY (no production call site); unknown-flag rejections from an older codex surface as named flag-compat errors (see integration notes). Updated 2026-08-21 (k2n3k, docs half; re-verified live via codex exec --help / codex --help, re-verified again against the installed codex-cli 0.149.1): --full-auto is REMOVED from both surfaces, not merely deprecated; -a/--ask-for-approval exists only on top-level codex --help (now on-request|never, the earlier untrusted value is gone) and is absent from codex exec --help entirely, so a headless worker can never reach it — flag table and gotchas below corrected to match. Verified against the installed 0.149.1 binary: --approve-for-me (0.147.0, 2026-08-07, PR #36373) is an ALTERNATIVE approval mode that CONFLICTS with --dangerously-bypass-approvals-and-sandbox (never additive), superseding the archived T572 writes-mode deferral; Amazon Bedrock became a built-in provider and codex exec fork was added (0.148.0), both confirmed present. Version pinned at codex-cli 0.149.1. Updated 2026-08-26 (k2n3k, evaluation half — closes the task): --approve-for-me now has a written NO-GO verdict plus the exact replacement argv, proven by three codex exec parse probes against the installed 0.149.1 binary that exit on argument parsing alone (no session, no spend) — see "--approve-for-me go/no-go" below.

Cyberine invocation

# build worker (makeCodexHeadlessAdapter)
codex exec --skip-git-repo-check --cd <cwd> -m <model> [-i <img>...] [--output-schema <path>] \
  --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust "<prompt>"

# read-only variant (makeCodexAdapter) — JSONL events; LIBRARY-ONLY, no production call site (T656)
codex exec --json [--output-schema <path>] [-i <img>...] "<prompt>"

Model: CYBERINE_CODEX_MODEL if set, else Codex's configured default (no -m added). Deliberately unpinned (kd7ax, 2026-09-09): Codex 0.153.4 made GPT-6-Astra the bundled default, and Cyberine keeps riding Codex's own configured default so the operator's ~/.codex/config.toml stays the single source of truth; the model that actually answered is read from the model: header codex exec prints on stderr (parseCodexEffectiveModel) and recorded as raw.model on the Result, workerRuns[].model in run.json, and model on the provenance candidate. Auth dir allow-listed by the Cyberine sandbox: CODEX_HOME when set, else both ~/.codex (official default) and ~/.config/codex (fixed 2026-06-07 — see gotcha).

Auth

  • CODEX_API_KEY env (recommended for exec) or CODEX_ACCESS_TOKEN (ChatGPT/account flows).
  • Interactive auth stored in $CODEX_HOME (default ~/.codex): auth.json (secret), config.toml (user) or .codex/config.toml (project). CODEX_HOME overrides the default.

MCP servers (client config, added kskdf, verified 2026-09-18)

Codex CLI is also an MCP client: ~/.codex/config.toml (or $CODEX_HOME/config.toml; a trusted project may use .codex/config.toml) takes a [mcp_servers.<name>] TOML table per server, fields command (required for stdio), args (optional array), env (optional table). Live-verified against the installed codex-cli 0.154.0: codex mcp --help lists list|get|add|remove|login|logout, and codex mcp add <name> [--env KEY=VALUE...] -- <command> [args...] writes exactly that table (--url + --bearer-token-env-var for a streamable-HTTP server instead of stdio). codex mcp add context7 -- npx -y @upstash/context7-mcp is the documented shape. Source: https://learn.chatgpt.com/docs/extend/mcp?surface=cli (redirected from https://developers.openai.com/codex/mcp), retrieved 2026-09-18.

cyberine mcp install --target codex writes this table directly (never shells out to codex mcp add, so it works even without codex on PATH) — merges into any existing config.toml by appending a new table, is idempotent (detects an existing [mcp_servers.cyberine] header and no-ops), and backs the file up once to config.toml.bak-cyberine before its first write. --print shows the table without touching the file.

Commands & key flags

ItemPurpose
codex exec PROMPTNon-interactive run; - reads prompt from stdin. Progress → stderr, final answer → stdout.
codex exec --json PROMPTJSONL typed events on stdout (thread.started, turn.started, item.completed, turn.completed, turn.failed).
codex exec resume [ID]Continue a prior session (--last, --all).
-m, --modelOverride model.
--sandbox, -s read-only|workspace-write|danger-full-accessCodex's own sandbox (default read-only).
--ask-for-approval, -a on-request|neverTop-level codex only (interactive TUI) — re-verified absent from codex exec --help entirely on the installed codex-cli 0.149.1 binary; top-level now lists only on-request and never (the earlier untrusted value is gone), no writes value. A headless worker (always codex exec) cannot reach this flag at all — see gotcha below.
--dangerously-bypass-approvals-and-sandbox (alias --yolo)Disable BOTH — required headless (stdin closed).
--dangerously-bypass-hook-trustRun enabled hooks without requiring persisted hook trust. Required headless once a repo/CODEX_HOME has hooks configured (Codex ships stable hooks per codex features list) — otherwise a hook-trust prompt stalls the same way an unapproved command would. Always passed by makeCodexHeadlessAdapter (T625).
--skip-git-repo-checkAllow running outside a git repo.
--cd, -C <path>Set workspace root before executing (Cyberine pins the tree with this).
-i, --image <FILE>...Attach image(s) to the prompt. Cyberine maps each Task.images[].path (resolved against task.cwd) to a repeated -i <path> in both makeCodexAdapter and makeCodexHeadlessAdapter (T623).
--output-schema <path>Path to a JSON Schema file describing the final response shape (unlike Claude Code's --json-schema, which takes the schema text inline). Codex injects it as an "## Output schema" instruction section rather than changing the JSONL event shape — the final answer still arrives as a normal agent_message.content string, just schema-constrained JSON text. On the headless build worker (makeCodexHeadlessAdapter) it is opt-in via CYBERINE_CODEX_OUTPUT_SCHEMA / opts.outputSchema (T631, wired T656); on the library-only makeCodexAdapter via opts.outputSchema, combined with that variant's unconditional --json.
--output-last-message, -o <path>, --ephemeral, -c KEY=VALUE, --ossMisc.

Models (Jun 2026)

gpt-5.5 (recommended default), gpt-5.4 (flagship), gpt-5.4-mini (fast/cheap), gpt-5.3-codex-spark (preview, ChatGPT Pro only). gpt-5.2/gpt-5.3-codex deprecated for ChatGPT sign-in (version strings unverified). Underlying API: openai.md.

Bedrock-hosted models (added 0.143.0, verified 2026-07-10 against installed v0.143.0 binary)

gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna — exact ids confirmed via strings on the installed codex binary at /opt/homebrew/bin/codex (the literal substrings gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna are present). They route through a Codex-native model_providers.amazon-bedrock provider (endpoint bedrock-mantle.<region>.api.aws, auth via AWS_BEARER_TOKEN_BEDROCK or a configured bedrock_api_key) — separate from Cyberine's own BedrockAdapter / loop-bedrock-build path, which talks to AWS Bedrock directly and does not go through Codex CLI at all.

In scope, documented here as valid CYBERINE_CODEX_MODEL values — makeCodexHeadlessAdapter already passes CYBERINE_CODEX_MODEL straight through as -m <value> with no allowlist (src/adapters/CliAgentAdapter.ts), so no code change is required to use these three ids. Out of scope: Cyberine does not configure Bedrock auth for Codex CLI. The operator's own ~/.codex/config.toml must already set model_providers.amazon-bedrock.aws.region + AWS_BEARER_TOKEN_BEDROCK (or bedrock_api_key) before CYBERINE_CODEX_MODEL=gpt-5.6-sol (etc.) will actually reach Bedrock — that setup step is the operator's responsibility, not Cyberine's.

Reasoning-effort enum values confirmed in the binary: minimal, low, medium, high, ultra. No literal "max" value was found in the binary strings — the "first-class max reasoning effort" premise could not be corroborated beyond the model ids themselves; not blocking since reasoning effort is a Codex CLI config knob independent of the model-id passthrough above.

Upstream 0.148.0 (2026-08-18) — Bedrock became a built-in provider, verified on the installed 0.149.1 binary. The release promotes Amazon Bedrock from the operator-configured model_providers.amazon-bedrock entry above to a Codex-native built-in provider (with gpt-5.6-sol as its default model) — confirmed since the installed binary is 0.149.1 (>= 0.148.0). Same 0.148.0 release added codex exec fork (fork a saved session non-interactively) — also now present on the installed binary; no Cyberine touch-point identified yet.

JSONL event schema (--json)

  • item.completed with type=agent_message → the answer. Top-level agent_message event uses field content (string), not text — Cyberine reads event['content'].
  • turn.completed → usage object: input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens.

Gotchas

  • Partial-trust approval mode — was T572's writes-mode question, now ANSWERED, and the alternative flag is now VERIFIED present. It never shipped as a writes value on --ask-for-approval — re-verified live against the installed codex-cli 0.149.1 binary (codex exec --help has no -a at all; top-level codex --help lists only on-request|never, the earlier untrusted value is gone), so the original T572 guess would have been wrong to wire. Upstream instead added a standalone --approve-for-me flag in 0.147.0 (2026-08-07, PR #36373; reported semantics: approvals_reviewer=auto_review + approval_policy=on-request + sandbox_mode=workspace-write) — the installed 0.149.1 binary DOES now have this flag (present in both top-level codex --help and codex exec --help, described as "route approval requests through automatic review using the workspace-write sandbox"). It is an ALTERNATIVE approval mode, never additive: it CONFLICTS with --dangerously-bypass-approvals-and-sandbox, which makeCodexHeadlessAdapter passes unconditionally today — the two cannot both be passed. Evaluated and decided 2026-08-26 (k2n3k): NO-GO, do not wire — the conflict is now proven at clap parse time and the exact replacement argv is recorded, but the headless-stall property that would justify the swap is not establishable without a live session. Full verdict, argv and probe evidence in "--approve-for-me go/no-go" below.
  • --dangerously-bypass-approvals-and-sandbox is required for headless (stdin-closed) — else the first approval stalls indefinitely (GH issues #4565/#9144/#14345; confirmed in Cyberine adapter).
  • --full-auto is REMOVED, not merely deprecated — re-verified live: absent from both codex exec --help and top-level codex --help on the installed codex-cli 0.149.1 binary. Use --sandbox workspace-write directly. Cyberine's adapters never passed --full-auto (grep-confirmed against src/adapters/CliAgentAdapter.ts), so no code change was needed here.
  • Config/auth path discrepancy — FIXED (2026-06-07): official CODEX_HOME defaults to ~/.codex, but CliAgentAdapter.cliConfigDirs() previously allow-listed only ~/.config/codex — so under seatbelt the codex worker could not write to its real config dir. Now honors CODEX_HOME when set, else allow-lists BOTH ~/.codex and ~/.config/codex. Verified the generated seatbelt profile grants write to ~/.codex.
  • turn.completed usage reports cumulative session totals, not per-call deltas (GH #17539) — diff consecutive events for per-call.
  • cost_usd is read by Cyberine's parse code but is not in the official usage schema — kept as a harmless forward-compat fallback (stays undefined); the parse comment was corrected to stop presenting it as the Codex shape.
  • codex exec without --json: progress on stderr, answer on stdout — handle the two streams separately.

Credential-refresh progress on the parsed streams (kjcwz, measured 2026-09-04 against 0.153.2)

Codex 0.152.0 added, verbatim: "The terminal UI and codex exec show credential-refresh progress, including Amazon Bedrock reauthentication" (#41239). New output on a stream Cyberine parses is the one upstream change class that can corrupt a worker result silently rather than failing loudly, so the R02 sweep filed it instead of assuming it harmless. Measured verdict: usage is safe on both factories; the answer TEXT of the headless worker can pick up a stdout progress line, and that is recorded rather than filtered.

The line texts were read out of the installed binary (strings $(which codex), 0.153.2) rather than invented: Access token expired or nearly expired, refreshing., refresh token present, attempting refresh, Refreshed access token., Token refresh not possible, re-authorization required., plus the failure strings failed to refresh token while getting account: and OAuth refresh token was rejected:. They sit beside codex's tracing strings in the binary, so stderr is their likely home — but which stream they land on is exactly what a Cyberine change must not depend on, so both are exercised.

The two factories have different contracts and the answer differs between them:

  • makeCodexHeadlessAdapter (the build worker codex) runs plain codex exec and takes stdout.trim() as the answer. In exec mode codex sends the FINAL MESSAGE to stdout and its reasoning/progress to stderr, so stdout is normally just the answer — which means a progress line that DOES land on stdout is appended into the answer text. Deliberately NOT filtered: matching upstream log wording would rot at the next rewording and could delete legitimate answer text. The smoke pins this as the current contract, so if a future codex starts putting refresh progress on stdout in volume, that check fails loudly first and makes the cost of filtering worth paying.
  • makeCodexAdapter (codex exec --json) decodes events, so the noise is skipped outright: the answer comes from the last agent_message and usage from turn.completed.

Usage is unaffected on both paths, which is the assertion that actually matters — a wrong token count feeds cost estimation, while extra prose in an answer is visible to a judge. parseCodexHumanUsage cannot mistake any refresh line for a tokens used summary, and a refresh line landing BETWEEN the label and its count degrades to "no usage", never to a wrong one. Guard: npm run selftest:codex-usage section 10 (three sub-sections, offline, stub codex on PATH, no credentials and no network).

--approve-for-me go/no-go (k2n3k, decided 2026-08-26 against codex-cli 0.149.1)

Verdict: NO-GO for now — do not wire. The flag parses cleanly in the exact shape Cyberine would use, but the single property that matters headless — never stalling on closed stdin — cannot be established by a parse probe, and the CYBERINE_TASK_TRUSTED=false path it would serve is already sandboxed twice over.

Exact argv Cyberine would pass (if wired)

--approve-for-me can only REPLACE --dangerously-bypass-approvals-and-sandbox, never join it, so the candidate CYBERINE_CODEX_APPROVAL=approve-for-me build in makeCodexHeadlessAdapter (src/adapters/CliAgentAdapter.ts) is exactly today's argv with that one flag swapped:

codex exec --skip-git-repo-check --cd <cwd> [-m <model>] [-i <img>...] [--output-schema <path>] \
  --approve-for-me --dangerously-bypass-hook-trust "<prompt>"

--dangerously-bypass-hook-trust stays (T625 — it covers a different prompt, and P2 below proves the two compose). -s/--sandbox is NOT added: --approve-for-me already implies workspace-write, so passing both parses (P3) but is redundant.

Parse probes (2026-08-26, installed codex-cli 0.149.1 — each exits on argument parsing alone, no model session, no spend)

ProbeargvResult
P1 conflictcodex exec --skip-git-repo-check --approve-for-me --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust "probe"exit 2, error: the argument '--approve-for-me' cannot be used with '--dangerously-bypass-approvals-and-sandbox'. The conflict is enforced at clap PARSE time — replacement only, provably never additive.
P2 replacementthe candidate argv above plus a trailing unknown --cyberine-parse-probe-sentinelexit 2, error: unexpected argument '--cyberine-parse-probe-sentinel' found, and ONLY that. Every real flag ahead of the sentinel was accepted, so the candidate argv parses clean.
P3 sandbox composecodex exec --approve-for-me -s workspace-write --cyberine-parse-probe-sentinel "probe"exit 2 on the sentinel only — --approve-for-me does not conflict with -s.

The sentinel is what makes P2/P3 evidence rather than assumption: clap reports a conflict or a bad value for any flag it rejects, so a run that dies solely on the sentinel proves every preceding flag was accepted.

Why NO-GO despite a clean parse

  1. The stall question is untestable without spend, and it is the entire reason the bypass flag exists. --dangerously-bypass-approvals-and-sandbox is mandatory today precisely because any approval prompt on closed stdin hangs until the timeout. approvals_reviewer=auto_review is meant to answer those prompts with a model instead of a human, but whether a reviewer DENIAL falls back to a human prompt cannot be observed by parsing argv — it needs a live session driving a command the reviewer refuses. Guessing wrong yields a silently-killed dispatch, the exact failure class of the 2026-08-26 stuck-detector bug.
  2. It costs model calls the spend cap cannot see. auto_review adjudicates each approval request with a model call. A Codex worker's usage already bypasses the daily spend cap — the gate is adapter.info.kind === 'loop' in src/engine/WorkflowEngine.ts, correct for a subscription Codex and wrong for a metered one (the same reason T682's NO-GO stands). Wiring approve-for-me would add invisible spend on top.
  3. The marginal security gain is near zero on the path it targets. CYBERINE_TASK_TRUSTED=false already REFUSES to spawn a CLI worker without a host sandbox (src/adapters/hostSandbox.ts) and already forces riskLevel: 'high' with a mandatory L3 adversarial gate (src/engine/WorkflowEngine.ts, src/engine/trustGate.ts). Codex's own workspace-write would be a third layer over a path that cannot run unsandboxed in the first place.
  4. workspace-write may not cover what a build worker actually writes. Beyond --cd <cwd> a worker touches $CODEX_HOME, /tmp, and package/tsx caches. The Cyberine host sandbox allow-lists those explicitly; whether Codex's own workspace-write does is unverified.

What would flip it to GO

One live smoke on a throwaway worktree proving all three: (a) a run completes with --approve-for-me and stdin closed, (b) a reviewer-denied command surfaces as a normal failure rather than a hang, (c) the worker's writes to $CODEX_HOME / /tmp / caches are not blocked. Until then the argv above is recorded so that wiring is mechanical rather than exploratory.

--no-daemon (Codex 0.156.0) — not wired (kdhbf, checked 2026-09-25)

Codex rust-v0.156.0 added a --no-daemon flag. The binary installed on this machine is codex-cli 0.155.1: neither codex --help nor codex exec --help lists the flag, and the only daemon surfaces in --help are codex agents and codex remote-control, which manage the shared app-server daemon for interactive sessions. Passing an unknown flag to codex exec would fail every worker on this install, so the codex worker's argv does not carry it. Revisit on the first upgrade to 0.156+: check whether codex exec attaches to the shared daemon by default there, and wire --no-daemon only if it does (a worker must not share session state with the operator's interactive Codex).

Usage limits and reset time (ktwg5, measured 2026-09-23 against codex-cli 0.155.1)

Reactive: the refusal names its reset. The installed binary's strings carry the two refusal shapes, "You've hit your usage limit. Upgrade to Plus ... or try again at ." and the Pro/credits variant ending "... purchase more credits or try again at .", plus a bare "Try again at ". <when> is %-I:%M %p when the reset falls today ("9:16 AM") and %b %-d<st|nd|rd|th>, %Y %-I:%M %p otherwise ("Sep 24th, 2026 9:16 AM"), both local time. src/core/cliUsageLimit.ts parseUsageLimitResetMs reads either form (and a relative "try again in 2 hours 13 minutes"), and CliAgentAdapter records the cooldown to end exactly then instead of the generic 24-hour cli-unavailable TTL. A clock time already past today rolls to tomorrow; a parse more than 8 days out is discarded as a misread.

Proactive: YES, headroom is readable for free. Every rollout file under $CODEX_HOME/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl carries token_count events with rate_limits: primary (the 5-hour window, window_minutes: 300) and secondary (weekly, window_minutes: 10080), each with used_percent and resets_at in epoch SECONDS, plus a credits block. Measured on this machine: primary 10% used, secondary 63% used. readCodexRateLimits() reads the newest reading without spending a turn; codexExhaustedUntilMs() returns the latest reset of any window at 100%. When a codex refusal carries no parsable time, the adapter falls back to that reading. The reading is only as fresh as the last codex session on the machine, so a quiet machine reports an old number.

Pricing

No per-token Codex CLI price; included in ChatGPT Plus/Pro/Business/Edu/Enterprise. API-key usage billed at standard OpenAI model rates. https://openai.com/pricing

Cyberine integration notes

  • codex build worker gated on install + headless login. Cyberine pins the tree with --cd <cwd>; the host sandbox (seatbelt/bubblewrap) is separate from Codex's own --sandbox.
  • Plain-text output mode (no --json) means no usage data for the build worker — Cyberine falls back to stdout/stderr text.
  • Flag-compat diagnosis (T656): an older installed codex that rejects a version-gated flag (clap emits error: unexpected argument '--flag' found, exit 2) surfaces a named error carrying the flag, the owning task, a known-good version, and the disable knob — registry CLI_FLAG_COMPAT in src/adapters/CliAgentAdapter.ts, smoke npm run selftest:cli-flag-compat.
  • makeCodexAdapter (the --json read-only variant) is LIBRARY-ONLY (T656): no production call site — buildAdapters.ts constructs the build-fleet codex worker via makeCodexHeadlessAdapter.

DeepSeek-backed Codex worker (T682, verified 2026-08-25 against installed codex 0.149.1)

Codex can be pointed at DeepSeek's native Responses API with zero Cyberine code change: makeCodexHeadlessAdapter passes its model straight through as -m <model> (src/adapters/CliAgentAdapter.ts:2387, resolved from CYBERINE_CODEX_MODEL at :2375) and has no model allowlist. It also passes no provider override — there is no -c model_provider=... in buildArgs — so base_url and wire_api come solely from the Codex config the process reads.

DANGER: do not run DeepSeek's setup script

DeepSeek ships a curl-pipe-to-bash installer that rewrites ~/.codex/config.toml globally. That file and the codex binary are SHARED with the global nf-codex skill and (on the homelab) with MONITORING/container-monitor.sh's cron. Running it flips every Codex client — CLI, desktop app, IDE extension — onto API-key billing and hides all existing ChatGPT-subscription session history, because Codex partitions history by login method. A suddenly-empty codex exec resume list is that switch, not data loss. Never run it. Use the scoped recipe below instead.

The scoped recipe — CODEX_HOME fully isolates it

CODEX_HOME relocates the whole Codex config directory, not just a layer. Verified: with CODEX_HOME set, codex exec reported provider: deepseek / model: deepseek-v4-flash while the md5 of ~/.codex/config.toml was unchanged across every probe in the session.

export CODEX_HOME="$PWD/.local/tmp/codex-deepseek"   # any dir; keep it gitignored, it holds a key
mkdir -p "$CODEX_HOME"
cat > "$CODEX_HOME/config.toml" <<'TOML'
model = "deepseek-v4-flash"
model_provider = "deepseek"
preferred_auth_method = "apikey"
forced_login_method = "api"
model_catalog_json = "<ABSOLUTE PATH>/models.json"   # top-level ONLY — see the TOML trap below

[model_providers.deepseek]
name = "DeepSeek"
base_url = "https://api.deepseek.com/"
wire_api = "responses"
experimental_bearer_token = "<DEEPSEEK_API_KEY>"
TOML
chmod 600 "$CODEX_HOME/config.toml"

Then dispatch through Cyberine normally — CODEX_HOME reaches the child untouched (buildScopedChildEnv only filters provider-credential keys, and CODEX_HOME is not one; src/adapters/cliConfigDirs.ts:59 already honors it):

CODEX_HOME="$CODEX_HOME" CYBERINE_CODEX_MODEL=deepseek-v4-flash CYBERINE_BUILD_FLEET=codex \
  npm run wf -- .local/dogfood/t682-codex-deepseek.workflow.ts

The workflow def lives in gitignored .local/dogfood/ (this repo's documented home for defs), so recreate it if it is not on your machine:

// .local/dogfood/t682-codex-deepseek.workflow.ts
import type { WorkflowDef } from '../../src/engine/workflowDef.js';

const def: WorkflowDef = {
  name: 't682-codex-deepseek',
  steps: [{ id: 'probe', title: 'DeepSeek-backed Codex probe',
            prompt: 'Reply with exactly the single line: T682-WF-OK', agentIds: ['codex'] }],
};
export default def;

TOML trap that cost a debugging cycle

model_catalog_json is a top-level key. Appending it after the [model_providers.deepseek] header makes it model_providers.deepseek.model_catalog_json, which Codex silently ignores — no error, and the catalog is simply never read. Put every top-level key BEFORE the first table header.

models.json — the required-field schema, discovered by probe

Without a catalog entry Codex warns Model metadata for <id> not found. Defaulting to fallback metadata; this can degrade performance and cause issues. The schema is undocumented; these are the fields 0.149.1 requires (each one discovered by reading the parser's own missing field error):

{ "models": [ {
  "slug": "deepseek-v4-flash",
  "display_name": "DeepSeek V4 Flash",
  "supported_reasoning_levels": [],
  "shell_type": "default",
  "visibility": "list",
  "supported_in_api": true,
  "priority": 1,
  "support_verbosity": false,
  "experimental_supported_tools": [],
  "truncation_policy": { "mode": "tokens", "limit": 100000 },
  "base_instructions": "You are a coding agent running in the Codex CLI.",
  "context_window": 1000000,
  "tool_mode": "direct",
  "multi_agent_version": "v1"
} ] }

With that file the metadata warning disappears and the model runs. Notes:

  • base_instructions (or model_messages.instructions_template) is REQUIRED — a catalog entry must carry the model's whole system prompt. That is why DeepSeek ships a generator script rather than a snippet, and it is the main cost of hand-maintaining a catalog.
  • truncation_policy takes mode, not type.
  • supported_reasoning_levels is an array of ReasoningEffortPreset structs, not strings; [] is accepted and is the simplest correct value.
  • shell_type variants: default | local | unified_exec | disabled | shell_command.
  • tool_mode variants: direct | code_mode | code_mode_only.
  • multi_agent_version (v1/v2) is a PER-MODEL field in models.json, not a top-level config.toml key. Third-party reports name multi_agent_version = "v1" as the workaround for Codex Multi-Agent V2 silently discarding subagent task payloads on DeepSeek, without saying where it goes; this is where. The underlying bug was NOT re-verified on 0.149.1 — the reports are against 0.148.0 and remain third-party.
  • wire_api = "chat" is rejected outright by 0.149.1 ("wire_api = \"chat\" is no longer supported."), so responses is not a preference here, it is the only option.

Operator gotcha: codex exec blocks on stdin

Run by hand, codex exec prints Reading additional input from stdin... and waits — a 5-minute hang with no output that looks like a network stall. Append </dev/null. Cyberine is immune: CliAgentAdapter spawns with stdio: ['ignore', 'pipe', 'pipe'].

Known limitations and deferred work

  • No usage is recorded for a Codex worker. FIXED 2026-08-25 (T694). The adapter now parses the token summary, so a codex dispatch records usage.outputTokens instead of usage: null. Corrections to the original report, from reading codex-rs/exec/src/event_processor_with_human_output.rs at tag rust-v0.149.1 (print_final_output), which is the code that emits it:

    eprintln!("{}\n{}", "tokens used".style(self.dimmed), format_with_separators(blended_total(usage)))
    • It is stderr, not stdout (eprintln!). The earlier note said stdout.
    • The label and the count are on separate lines, and the label is bare — there is no tokens used: N form to match.
    • The label may be ANSI-dim wrapped (ESC[2m…ESC[0m) even when stderr is a pipe: --color auto defers to supports_color on the stderr stream, which honours env such as FORCE_COLOR. Observed on a real 0.149.1 run on the homelab box: its stderr came back carrying ESC[2m…ESC[0m around every dim-styled line even though stderr was a pipe, so "piped, therefore no ANSI" is not a safe assumption.
    • The count is locale-grouped: format_with_separators is icu_decimal over sys_locale::get_locale() (en-US only as a fallback), so the separator is , on en-US, . on de-DE/vi-VN, U+00A0/U+202F on fr-FR, ' on de-CH. parseCodexHumanUsage accepts all of them and requires the last group to be exactly 3 digits, which is what keeps 18.12 from being read as a count.
    • It is one blended number: blended_total() is (input_tokens - cached_input_tokens) + output_tokens. No input/output split exists in the human output, so the total is reported as outputTokens (conservative — estimateCostUsd prices output above input) and inputTokens is left absent rather than invented.
    • It is the cumulative session total, not a per-turn delta — harmless because the adapter cold-spawns a fresh codex exec per dispatch, but a future resume-based warm session would have to diff it.

    Pinned by npm run selftest:codex-usage (captured stderr fixtures + an end-to-end dispatch against a stub codex on PATH, no credentials needed).

  • cyberine run (the single-step path) prints a runId and state: done but writes no run.json — orchestrate() does not persist. Only the workflow engine (npm run wf) does. Use a workflow def when you need a recorded artifact.

  • A step's agentIds: ['codex'] did not restrict the fleet in the T682 probe — six workers ran. Observed, not diagnosed.

go/no-go as a standing build-fleet worker

NO-GO for now, on cost-shape grounds rather than capability. The path works end-to-end (recorded run 44320a3b, status=success), but a DeepSeek-backed Codex worker is token-metered against DEEPSEEK_API_KEY, whereas the default codex worker is subscription-billed against the user's ChatGPT plan. Registering it as a standing worker would silently move a lane from flat-rate to metered, and the spend it generates would not reach the daily cap.

Still NO-GO after T694, for the second half of that reason only. Usage parsing landed (usage.outputTokens is now recorded), but recorded usage is not the same as capped spend: WorkflowEngine's recordRealizedSpend() call is gated on adapter.info.kind === 'loop', so NO CLI worker's cost enters CYBERINE_SPEND_CAP_USD's cumulative tally. That gate is right for the default codex worker (subscription-billed, no metered cost to count) and wrong for a DeepSeek-backed one — and the adapter cannot currently tell the two apart, since the provider comes from whatever CODEX_HOME config the subprocess reads, not from anything Cyberine passes. Teaching the cap to distinguish a metered CLI worker from a subscription-funded one is the remaining prerequisite; the invisible-usage half is done.

Sources