Codex launcher — architecture proposal¶
Package: habemus-papadum-codex-launcher (pdum.codex_launcher)
CLI: pdum-codex
Status: partially implemented; see current capabilities and validation policy. The design principles record the agreed trust/config stance and lifecycle, inspection and CLI direction. Detailed APIs and compatibility remain proposals. This supersedes earlier defensive defaults, capture caps and plugin-worker requirements. See the implementation plan.
Executive decisions¶
- Profiles are self-contained directories. Python takes a directory; the CLI resolves names. Keep native Codex configuration native, including unknown fields.
- Launcher metadata covers only executable selection, environment, launch and integration concerns; do not maintain a second native configuration schema.
- Persistent and temporary profiles are first-class. Copy settings directly; make credential reuse convenient and warning-based, not permission-gated.
- Own each launched app-server with
asyncio, foreground/application-owned, recording PID/process identity per run. Do not gate a profile to one server: Codex coordinates concurrent processes in a home natively, and read-only inspection reports every process using it. No launcher daemon; attaching to a foreign server is a testing affordance. Initial hosts are macOS/Linux on local filesystems. - Default sandbox off, approval prompts off and all-interface TCP listeners. Trust clients inside the operator's network; no required grants or timed reconnects.
- Support Unix and WebSocket endpoints. The inspected Unix transport is WebSocket-over-Unix, not raw JSONL. Verify the selected executable.
- Keep raw-protocol forwarding thin. Copy messages to an independent async inspection/export path; observer failure must not stop healthy forwarding.
- No MITM sidecar by default. When selected, interception starts at launch; recording may start off and be enabled later. No initial capture size quotas.
- Export a reusable Typer CLI and explicit processor hooks. An optional FastAPI management adapter can reuse the runtime; defer daemon mode, cloud migration, lockdown and a general thread SDK.
This page describes the wider target; the implementation-status page owns current command/API availability and validated limitations.
1. Responsibilities and implementation boundaries¶
| Component | Responsibility | Does not own |
|---|---|---|
profiles |
Paths, small launcher-metadata validation, atomic creation/copying and temporary state | Native schema maintenance or credential renewal |
auth |
Credential-source configuration and isolated operator login | Client authentication RPC handling or a new OAuth implementation |
runtime |
Binary checks, lease, environment, subprocess, readiness, cleanup | Thread/turn semantics or automatic turn retries |
relay |
One-to-one forwarding and asynchronous message copying | Client authorization machinery, upstream login, RPC rewriting or tenant isolation |
observation |
Processor selection, independent inspection tasks, local output and optional metric/export hooks | Blocking forwarding, rewriting traffic or treating missing usage as zero |
integrations.mitm |
Codex-specific proxy/trust adapter and optional interpretation plugin | Generic proxy engine/capture implementation |
cli |
Exported Typer app/factory, argument resolution, interview, human/JSON output and optional provider commands | A second implementation of library behavior or hidden process startup on import |
Use Typer for the CLI, plus asyncio, typed dataclasses and tomllib.
Other proposed small dependencies are platformdirs for locations, a round-trip TOML
editor for the few requested edits, and the modern asyncio websockets API. Select compatible
versions during implementation, not in this proposal. MITM is an optional package
extra; importing or running the launcher without it must work. FastAPI and Logfire
are optional adapters, not core import requirements or prerequisites for local use.
2. Profile identity, selection, and disk layout¶
Locations and selection¶
The default root is platformdirs.user_data_path("pdum-codex", appauthor=False) /
"profiles": conventionally ~/Library/Application Support/pdum-codex/profiles on
macOS and $XDG_DATA_HOME/pdum-codex/profiles on Linux. A library call never consults
this default; it receives a Path.
CLI selection is deliberately exclusive:
| Input | Resolution |
|---|---|
--default |
The Codex home the user's own CLI resolves: $CODEX_HOME or ~/.codex. Environment isolation off; never created or disposed |
--profile-dir /absolute/path |
Exact directory; cannot combine with either selector below |
--profile NAME |
Name under the default root |
--profiles-dir ROOT --profile NAME |
Name under that explicit root |
profile list [--profiles-dir ROOT] |
Enumerate that root, without selecting a profile |
launch --temp |
New private temporary profile; cannot select a persistent target |
--default is not a special code path: it resolves a directory and selects the
bare-home defaults below. Any Codex home is launchable without launcher metadata. Names match
[A-Za-z0-9][A-Za-z0-9._-]{0,63}, excluding . and ..; no separators or traversal.
Explicit paths may contain spaces. Resolve existing path aliases before locking;
use the resolved directory's device/inode as an additional identity check. Reject
ambiguous metadata/lock aliases that could create competing ownership. Do not
silently create a missing profile while launching it. These checks prevent path
collisions and unintended writes, not every theoretically untrusted filesystem arrangement.
<profile>/ a Codex home; CODEX_HOME points here
config.toml native Codex model/provider/permission settings
auth.json native file-backed credentials when used
skills/ sessions/ ... Codex-owned history, databases, caches and other state
.pdum/
launcher.toml small launcher-owned configuration; absent for a bare home
home/ isolated HOME and XDG roots when isolation is on
work/ neutral working directory when isolation is on
runtime/
mutation.lock short lock serializing launcher edits, logins and disposal
runs/<run-id>/ owner.json run record plus stdout/stderr logs
tmp/ TMPDIR when isolation is on
A bare home without .pdum/launcher.toml launches with environment isolation off
and the parent environment inherited, so the server behaves like the user's CLI.
A managed profile defaults to isolation on and a clean environment. Nothing in
Codex enumerates the home root, and Codex keeps dotted state of its own there.
Create directories with mode 0700 and files with 0600. Profile identity is the
directory, not a copied UUID; cloned directories are new profiles. Native sockets
can use a short private runtime directory when platform socket path limits would
be exceeded; store the resolved path in the run record, not a predictable /tmp
filename. Network filesystems, cross-machine leases, Windows, and bind-mount aliases
that cannot be identified reliably are outside the first support contract.
Configuration ownership¶
Use a small launcher.toml with schema_version = 2 only for fields we own:
executable selection, process environment and its isolate flag, selected
authentication setup, endpoints/timeouts and optional MITM configuration. Validate those fields for
typos/incompatible values, not native Codex config against a second schema.
Do not introduce a configuration inheritance/merge framework.
Illustrative launcher metadata, not a final API or Codex syntax:
schema_version = 1
[executable]
path = "/absolute/path/to/codex"
[launch]
transport = "websocket"
bind = "0.0.0.0"
startup_timeout_seconds = 30
shutdown_timeout_seconds = 15
[mitm]
enabled = false
Model, reasoning, MCP, provider and permissions stay in native config.toml.
Copy that file verbatim when possible. Preserve unknown keys; use a round-trip
TOML editor or the native config interface for small requested edits. Codex
validates its own configuration. Launch never writes it: --model and -c
become codex app-server -c key=value arguments for that process only, which
is why a permissive launcher run never changes the user's daily TUI settings.
Explicit create/configure/auth commands edit the selected profile, the default included.
Record observed binary version/digest and effective launch choices. Store helper or environment/file references only where needed, avoiding duplication of values already in native config. Relative launcher paths resolve against the profile. No launcher secret-classification, Origin-policy or grant tables.
Show state-location/auth overrides without routinely dumping credentials. New profiles start with sandbox off and approvals off unless the operator supplies different native settings. Do not continuously regenerate config, police native changes or revert edits made through clients.
3. Creation, import, and temporary profiles¶
Creation and copying¶
profile create NAME uses staging and atomic rename; an existing target is an
error, not permission to merge/overwrite state. The terminal interview covers the
source, executable, model, auth and launch choices with a short file/category
preview. Non-interactive configuration/flags do not prompt or substitute models.
Selecting a source copies its native configuration, skills and launcher choices
by default. Categories such as model, skills, mcp, permissions,
environment, executable and native-config are optional convenience filters.
Their shortcuts touch only the small known subset we edit; whole-file copying is
the forward-compatible escape hatch. Preserve unknown keys, MCP environment
values, native credential literals/references and helper commands.
Explicit creation values override copied ones; report overrides without another
approval ceremony. Preview files/categories rather than every value, and note
that config may carry authentication or shared external references. No per-field
auth opt-in, secret scanner, schema classification or disabling of copied services.
Persistent creation can copy auth.json with one --copy-auth switch and a
warning. Independent login for long-lived profiles is available, not mandatory.
A settings clone excludes sessions, caches, databases, locks, sockets, logs and captures. Do not write to the source. Copy profile-local files instead of linking destination state back to them. Preserve intentional external references with a shared-resource warning. Cyclic/dangling symlinks or ambiguous destinations are copy errors; offer explicit materialization when needed, not a security interview.
First-class temporary profiles¶
pdum-codex launch --temp --model gpt-6-luna
pdum-codex launch --temp --from-profile research
pdum-codex launch --temp --clean
pdum-codex launch --temp --from-profile-dir /private/profiles/research --no-auth-copy
Recommended convenience behavior:
| Selection | Initial state |
|---|---|
--temp |
Fresh native settings plus available file-backed auth from the caller's normal Codex home, with one warning. |
--temp --from-profile NAME / --from-profile-dir PATH |
Source settings/skills/launcher choices and available auth.json, with one reuse warning. |
--temp --clean |
No source settings or credentials; conflicts with source/borrowing options. |
--no-auth-copy |
Omit the separate auth.json copy; does not scrub authentication already present in copied native config. |
--borrow-auth-from current/source/PATH |
Override the convenient auth-file source with one choice, without another consent gate. |
Use --exclude CATEGORY to narrow a clone. These are settings clones, not thread
snapshots. If an auth file is missing, report it; copied API/helper configuration
may still work. If no usable auth remains, give a direct next step, not an account
search. Library results expose warnings as data; the CLI prints them once without
blocking quick or automated use.
Resolve current from the caller's original CODEX_HOME or HOME/.codex
before constructing the child environment. Snapshot a regular file into the
destination with ordinary private-file permissions. Never symlink auth back,
write back, or initiate source login/refresh. Codex may refresh its destination
copy. A copied refresh token creates another renewal owner and can invalidate
either session: an accepted warning-level tradeoff, not a launch prohibition.
Do not build a broker just to remove that tradeoff. Keyring/external credentials
need their supported login/reference path; do not fabricate a missing auth file.
Create a unique temporary directory outside the checkout; remove it only after
app-server, relay and MITM stop and pending output flushes. --keep-temp retains
and reports it. Failed cleanup reports retained state, not secure erase.
temp list/temp prune operate on known profiles and acquire their lease,
refusing live/uncertain owners instead of deleting by filename pattern.
After launch releases its lease, cleanup reacquires it, marks the profile disposed, then closes the lease before removal. Launch checks that marker after acquisition. If another owner won the race, retain/report the directory. This prevents accidental state reuse/deletion, not trusted-user access.
4. Library and CLI surface¶
Proposed Python boundaries use paths and immutable request/result objects:
| Interface | Result and behavior |
|---|---|
create_profile(directory, specification) |
Async atomic creation; no interview |
inspect_profile(directory) |
Paths/config sources and state summary; no credential refresh or routine secret dump |
list_profiles(root) |
Per-directory summaries; one malformed profile does not hide others |
login_chatgpt(directory, options) |
Async isolated Codex CLI login; profile lease required |
temporary_profile(specification) |
Async context manager; exposes its resolved directory |
launch(directory, options) |
Async context manager yielding RunningAppServer after readiness |
RunningAppServer.endpoint |
Typed Unix/WS endpoint, not a guessed URL string |
RunningAppServer.wait() / aclose() |
Structured exit result / idempotent bounded shutdown |
RunningAppServer.status() |
Lifecycle, process identity, endpoint, optional capture health; no tokens |
The library does not install global signal handlers; the CLI translates SIGINT and
SIGTERM into cancellation. Large imports/hashing and blocking filesystem operations
run in bounded worker threads; subprocesses use asyncio.create_subprocess_exec,
never shell command interpolation. All cleanup paths must work under cancellation.
pdum-codex profile list
pdum-codex profile create research
pdum-codex --profile research profile show
pdum-codex --profile research profile validate
pdum-codex --profile research auth login-chatgpt
pdum-codex --profile research auth configure-api-key --env OPENAI_API_KEY
pdum-codex --profile research auth configure-api-federation --helper /trusted/token-helper
pdum-codex --profile research auth status
pdum-codex --profile research launch
pdum-codex --profile-dir /private/profile launch --dry-run
pdum-codex --profile research status
pdum-codex --profile research stop
launch stays in the foreground. No daemon/detach or attach-to-existing mode
initially; tmux, Docker or the caller can provide external supervision. The Python
async context manager owns the process lifetime. Keep start/status/stop operations
separate enough for future hosts without implementing a service manager now.
--json emits
structured lifecycle/results without dumping credentials; diagnostics go to stderr.
Show login ceremony data to the operator rather than routinely persisting it.
Deliberate protocol captures are a separate output.
launch --dry-run resolves config and binary but neither loads secret values nor
starts a sidecar/app-server. Edit/rename/delete are deferred rather than disguised as
unsafe filesystem operations; profile show gives paths for deliberate offline edits.
Composable Typer CLI and processor selection¶
Propose pdum.codex_launcher.cli.app and create_app(...) as public mounting
surfaces. The standalone pdum-codex entry point invokes the same app another
Typer application can mount as a subcommand. A factory returns a fresh command
tree, so different hosts can register different provider options without mutating
global CLI state. Importing/mounting does not start processes, install signal
handlers or run an event loop. CLI execution bridges to the existing async API;
applications with an event loop call that API directly.
Callers can supply observation processors explicitly at launch. Saved profiles
and CLI arguments use selected factory references/names plus ordinary configuration,
not serialized live objects. The CLI can list installed providers through optional
entry-point metadata; discovering them does not activate their processors. Selected
providers may register namespaced options/subcommands when the app is constructed,
before argument parsing. Reject command/option collisions clearly. Plain explicit
configuration works without discovery, and no shared root pdum CLI is required.
Use small filesystem and test processors to exercise this boundary initially. Logfire or Grafana-compatible metric exporters can be separate optional adapters; do not build their backend, retries or dependency graph into the launch command.
5. Binary selection, state isolation, and permissions¶
Creation precedence: explicit executable, copied executable, then PATH.
Resolve/store an absolute path and record observed version/hash. Missing binaries
fail; changed binaries warn and record their new identity rather than demanding
security approval. Offer exact-version/digest pinning for requested reproducibility.
--executable PATH overrides one run; profile executable set PATH updates the
stopped profile. A dotslash wrapper may be selected; record its actual artifact
where possible, because its own hash does not identify the binary it runs.
Automatic binary distribution is outside v1.
Environment controls prevent accidental home/credential/proxy reuse. A clean
baseline uses profile-local HOME, CODEX_HOME, XDG/temp paths, explicit PATH
and locale values; add profile settings, one-run overrides, then auth/MITM overlays.
Report conflicting process-owned home/proxy values. An inherit mode is available
without a permission ceremony. Copied credential references do not populate a
clean child's environment automatically: report known missing inputs or propagate
the native failure, then let the operator inherit/supply them. Do not scan all
native fields to infer a secret allowlist.
Start in profile work/, not an accidental caller repository. The fresh minimal
preset avoids importing normal user MCP/plugins/skills; a settings clone honors
its copied integrations instead of disabling them as a precaution. Qualify actual
loaded state for the selected binary. Project/system settings can still apply.
This is state isolation, not a security sandbox.
Default sandbox is off; approval prompts are off. Use the binary's native unrestricted/no-approval settings for new profiles unless explicitly overridden. Display actual settings and managed-policy/binary incompatibilities; do not pretend unrestricted operation is active or patch the installed CLI/host policy. Thread/turn overrides remain native behavior. Do not rewrite RPCs to enforce a continuing policy.
Exposed TCP binds default to 0.0.0.0, with configurable addresses/tested IPv6.
No compulsory client tokens, Origin allowlists, TLS layer or connection-age limit.
Unix/loopback may still be internal wiring. Lockdown is a separate future mode.
6. Run records, inspection and asynchronous process lifecycle¶
Many runs per home, each recorded¶
Codex coordinates concurrent processes in one home itself: per-thread writer locks,
WAL-mode SQLite with a busy timeout, and maintenance locks for compression,
migration and vacuuming. The user's ordinary home is routinely written by the
managed daemon, the TUI and editor extensions at once. The launcher therefore
holds no exclusive per-profile lease. Each launch or login writes its own record
under .pdum/runtime/runs/<run-id>/owner.json: run nonce, owner identity, child
PID with process birth identity, executable digest, the exact native arguments,
endpoint and control socket. status lists every record with liveness; stop
accepts a run selector and otherwise stops every live run the launcher recorded.
A short nonblocking flock on .pdum/runtime/mutation.lock serializes launcher
mutations only: edits, imports, logins and disposal. It never gates launching and
never stops a running server; Codex reads configuration per thread, so a saved
edit applies to the next thread. Do not implement ownership as exists(lockfile),
a PID file, or delete-to-unlock.
Orphan recovery is per run and explicit: validate current UID, process birth
identity, recorded command or endpoint and process-group leadership before
signaling the verified owned process. PID reuse or unverifiable ownership produces
OwnershipUncertain, never a blind kill(pid). Disposal of a temporary profile
refuses while any recorded run is live or any process of any origin still holds
files under the directory. This is hygiene, not a defense against a malicious
same-user process.
Inspection instead of enforcement¶
inspect is a read-only library function, CLI command and management route that
reports what is using a Codex home: every process touching it with kind, origin
(launcher run, managed daemon, editor extension, standalone CLI or foreign),
version and listen endpoint; launcher run records with liveness; the shared
daemon's identity, settings and, optionally, an initialize probe of its control
socket; per-thread writer locks and their holders; each SQLite file's size, WAL
size, holders, read-open latency and applied migrations; recent native log
activity per process; and configured feature flags. SQLite exposes no contention
counter, so the database fields are proxies and are labelled as such. Inspection
starts, stops and modifies nothing.
State machine¶
NEW → RECORDED → PREPARING → STARTING → READY → STOPPING → CLOSED; any stage may
enter FAILED, which still performs reverse-order cleanup. A run identifier is
assigned before resources are allocated. Use an AsyncExitStack and a task group
for drainers, process wait, relay and optional sidecar health; record which resources
actually reached each stage so partial startup is safe.
- Validate profile/binary, write the run record, prepare directories/environment
and render native
-coverrides; a bare home inherits the user's environment. - Start MITM if configured and await its ready handle before obtaining child env.
- Spawn Codex in a new process group with continuous bounded stdout/stderr drainers.
- Connect to the expected native endpoint, initialize a short-lived private probe,
send
initialized, and perform a read-only protocol check. Close that connection; do not reuse its initialized session for a client. No model call or login is needed. - Start optional relay, publish endpoint metadata, then return
READY. Readiness means a speaking app-server, not working credentials or model entitlement. - On close, stop accepting relay clients, close relay connections, terminate the owned Codex process group with SIGTERM, await the configured deadline, then use SIGKILL only for verified owned survivors. Reap and finish drainers.
- Flush/close MITM, remove owned sockets and finalize the run record. The enclosing temporary-profile context performs its separate disposal check. Preserve failures and incomplete-output flags in the result.
Unexpected child exit closes relays and the sidecar. Unexpected forwarding/control failure of a configured MITM stops Codex; there is no automatic direct-network fallback. Capture degradation alone follows the MITM capture policy and is surfaced without pretending forwarding failed. Cancellation is re-raised after bounded cleanup; cleanup errors are attached, not allowed to erase the original failure. No automatic app-server restart or turn replay in v1.
7. Upstream OpenAI authentication¶
Authentication is shared by an app-server, not a tenant boundary between its threads. These modes concern upstream inference, not permission to connect to the relay. They are setup conveniences, not a mandatory classification of every native provider. An existing native configuration may be launched unchanged without the launcher understanding its authentication scheme. Only rewrite provider settings when the operator requests that setup operation, not on every launch.
| Mode | Setup | Credential/renewal owner | Persistence |
|---|---|---|---|
chatgpt |
auth login-chatgpt runs the chosen codex login with isolated home/store |
Codex | Profile's file-backed credentials |
api_key |
Reference one operator environment variable or private file | Operator/secret manager rotates; restart child to reread | Reference only in launcher config; key in child environment |
api_federation |
Trusted executable gets workload identity and exchanges it for an API token | Helper obtains fresh tokens; Codex invokes/caches helper output | Non-secret helper configuration; no copied refresh token |
ChatGPT login owns the same lease as launch, so an operator must stop the profile before running launcher login again. Browser/device ceremony support follows the selected binary's capability; a login URL is not successful login. Cancellation stops the login process without touching the developer's ordinary home.
The API-key convenience setup uses a distinct custom Responses provider with env_key and
requires_openai_auth=false, rather than assuming stock app-server consumes an
OPENAI_API_KEY shortcut. Read the chosen reference at launch; do not write the
secret into additional config, argv, manifests, or routine diagnostics. Existing
copied native credential values remain native; no secret scrubber is implied. Environment
secrets can be inherited by Codex tools, so this is not isolation from the agent.
Federation means API-platform federation, mapping to an API project service
account, not native managed-ChatGPT Codex WIF. The launcher configures a distinct
Responses provider using auth.command, args, cwd, timeout_ms and
refresh_interval_ms. The first contract delegates identity acquisition/exchange
to an operator-supplied trusted executable; it does not implement every cloud SDK.
No shell interpolation. Its stdout contains only a bearer token. Avoid needless
credential dumps on stderr because Codex can include helper errors in diagnostics;
deliberate internal debug capture is allowed and is not public evidence.
Recommend a 25-second helper timeout and 60-second refresh interval only when the
helper guarantees at least 120 seconds of remaining token lifetime. Validate the
chosen lifetime/interval relationship instead of treating those numbers as universal.
Codex's inspected refresh is demand-driven and caches helper output; it is not a
guaranteed background refresh timer. Do not combine helper auth with env_key or
Codex-managed OpenAI auth. AWS acquisition, real API exchange and renewal on a
long-lived upstream WebSocket are explicit integration gates, not proven by a local
dummy-helper test. Select HTTP/SSE for the first federation compatibility profile;
enable WebSocket capability only after the live renewal/reconnect gate passes.
The model name alone does not decide the upstream transport.
Client-initiated authentication has no special launcher handling. The relay forwards account requests, responses, callbacks and notifications like other RPCs, including server requests requiring a client response. It neither injects logins nor answers external-token refresh requests. Codex's selected provider still determines whether a client login affects inference. A client can change the active identity for the shared process; report config/auth state without treating the profile's initial mode as an enforcement rule. Restrict clients accordingly. Do not dump account secrets into routine status. Deliberately enabled protocol recording may retain account payloads under the trusted capture policy. Optional masking must not alter forwarding.
8. Native endpoints and browser relay¶
The client-facing WebSocket listener defaults to all host interfaces (0.0.0.0).
Another bind or a Unix endpoint is selectable. Distinguish bind from connectable
advertised address; a remote client uses a selected host address, not a wildcard.
Native Unix framing is WebSocket-over-Unix, not JSONL.
A Codex binary may require auth for non-loopback native listeners or reject browser Origin headers. Where needed, provide the trusted network/browser endpoint using a thin relay to a native Unix or loopback backend. Honor backend requirements on that internal connection and show the actual frontend/backend layout. A requested direct native mode reports its requirements instead of silently disabling them or changing mode. This is transport compatibility, not a second client-login system; verify the selected binary rather than requiring a fork.
Use one upstream WebSocket per downstream connection. Clients initialize their own sessions; no shared initialized connection, multiplexing, ID rewrite or replay. Preserve message contents and directional order. Framing, compression, ping/pong and closes are transport-local. Use normal transport backpressure for the actual destination, never recorder backpressure. Do not introduce launcher message-size ceilings as capture policy; configure library defaults deliberately and report unavoidable native/backend limits. A real transport failure closes the affected connection with a reason, not silent RPC loss or automatic reconnection/replay.
Trusted browser clients use ordinary new WebSocket(endpoint). No launcher
capability, Origin allowlist, grant file, token subprotocol or 15-minute expiry.
The separate backend handshake omits browser Origin/cookies as needed; do not
blindly forward transport headers. Healthy connections may last as long as needed.
Keepalive/failure detection must not become an arbitrary idle or maximum-age cutoff.
Network clients can use the full native protocol, including account/config changes. The operator's network is the access boundary. Optional TLS/restrictive binds can be chosen, but complete lockdown is deferred. HTTP CORS for any later HTTP routes is separate compatibility configuration, not authentication.
Inspection-only observation stream¶
Treat the relay as a tap, not a protocol controller. For each selected message, copy an immutable observation containing payload, run/connection identity, direction, sequence and timestamp, then enqueue it without awaiting an observer. Forward the original message unchanged. No JSON parsing, metric extraction, filesystem access or remote export belongs on the forwarding path. An observation means the message was seen, not that its peer accepted it; keep delivery/close outcomes distinct when available.
Use an independent asynchronous dispatch path with a queue/task per selected processor. Slow sinks do not gate sending or unrelated processors. Offload blocking file writes or synchronous exporters from the relay event loop. Catch/report processor failures; disabling a failed observer must not disconnect healthy clients or stop Codex. Do not add a process per plugin initially. Trusted plugins must cooperate with the execution contract; a blocking CPU loop can still affect the shared process, so do not claim hard resource/fault isolation.
Recording/export are unselected by default. A metrics processor can run without a local archive, and a file processor need not depend on remote observability. Enabled capture preserves complete observed messages, including unknown methods and account payloads, without initial payload caps, truncation thresholds, queue admission quotas or per-run storage limits. Monitor bytes, queue depth/age, processor latency, storage and errors. Real disk failures, unfinished shutdowns and omissions are explicit; no finite machine can guarantee unlimited retention. Future guardrails need a separate decision, not an arbitrary default hidden in this implementation. Recording failure never opts the agent into a strict-stop mode.
Processors may emit derived records and metrics alongside the untouched message stream. Codex-aware processors live here or in an adjacent integration package, not the generic MITM core. Examples are input/output/cached-token usage, context observations and estimated cost for a configured pricing table. Include source message/sequence, thread/turn identifiers where present, units and pricing/model provenance. Determine incremental versus cumulative usage before aggregating; do not double-count repeated terminal totals or invent missing usage/cost values. Metric interpretation is read-only and must never inject app-server requests.
Optional simple masking affects only observer output and is labeled; schema-wide redaction is not a prerequisite. Captures may contain tokens and belong in trusted destinations, not Git/public artifacts. Configured local/remote processors receive the same data without a second export approval. Start with file/test processors and hooks; a small Logfire or Grafana-compatible exporter can be added independently.
9. MITM integration, control, and deferred services¶
The MITM proposal owns the canonical generic contract. The launcher uses
open_sidecar(config) as an async context manager; after readiness, the handle's
connection supplies proxy_url, ca_cert_path, and run_id. A launcher-owned
Codex adapter chooses the tested proxy/trust environment overlay and rejects
collisions. No global proxy change or certificate installation. Local relay/native
endpoint connections must bypass upstream proxy discovery explicitly.
Three launch choices are explicit:
| Choice | Behavior |
|---|---|
| No sidecar (default) | No interception process, CA or proxy overlay. Later interception requires a new launch. |
| Sidecar, recording off | Configure proxy/trust before starting Codex. Eligible HTTPS/WSS traffic is intercepted from connection establishment, but no payload archive is kept. |
| Sidecar, recording on | Same network path, with selected observations sent to configured processors/destinations. |
Recording-off does not mean opaque TLS tunneling: the interception and trust setup must already exist if later recording is to inspect an established encrypted connection. Explicit tunnel exclusions remain outside that capture capability. No claim of retroactively intercepting a direct or excluded connection is made.
status(), configure_capture(change, expected_revision=...), flush(),
wait_failure() and aclose() are the sidecar control surface. The launcher passes
through revisioned capture controls without inventing another capture schema.
Sidecar defaults remain recording-off; enable/disable applies to already-open
connections according to MITM's event-boundary and partial-capture contract. Fatal
forwarding/control failure and capture sink degradation are different statuses.
Initially the local launcher control socket exposes status, stop and capture
status/configuration with bounded versioned requests. It is not the app-server
RPC endpoint and has no grant/revoke service. An optional FastAPI adapter is a
reasonable small follow-on: async status/stop/capture operations delegate to the
existing owner, never blocking request handlers or launching a second supervisor.
It uses the same trusted-network defaults and remains optional for CLI/library
use. Lockdown access policy belongs to a separate later design.
Cloud snapshots/restoration need a separate design: quiescent
consistency, native databases/history, which credentials/state to include, external
executable references and restore ownership.
Neither remote home upload nor thread-storage replacement is part of v1.
10. Failure contract, evidence, and remaining gates¶
Library failures carry a stable code, lifecycle stage, run ID if assigned, and a
actionable message. Proposed codes include InvalidProfile,
ProfileBusy, OwnershipUncertain, BinaryMismatch, UnsupportedCapability,
CredentialUnavailable, StartupTimeout, ChildExited, SidecarFailed,
RelayFailed, and CleanupIncomplete. Avoid complete credential/environment dumps
in routine errors; detailed local diagnostics and deliberate captures may retain
upstream payloads. No generalized
secret-free-error detector is required. CLI exits: 0 success, 2 invalid arguments/config,
3 busy/uncertain ownership, 4 unsupported binary/capability, 5 startup/auth failure,
6 runtime failure, 7 incomplete cleanup; signal exits follow normal 128+signal
conventions after cleanup. Do not report a ready endpoint after startup failure.
This proposal adds no live observations. Evidence was checked on 12 October 2026:
- Official app-server documentation describes initialization and account RPCs; use the selected executable's generated schema for implementation rather than relying on a moving documentation page.
- Configuration reference documents native provider/environment settings. Reserved launcher overrides still need a real isolated-config inventory test for each supported binary.
- API workload identity documentation distinguishes API Platform service-account federation from managed ChatGPT Codex federation. Only the API-platform route is selected here.
- Local source pin
6c372fbef45f730b6d8aa727b0ec8332365588dbwas inspected inagent-spike/vendor/codex: transport parser, Unix WebSocket transport, provider validation, and helper refresh. The local checkout contains a separate browser-Origin experiment; this proposal does not require that patch or assume it exists in the installed binary. - Prior local spike reports used installed
codex-cli 0.162.1independently of that source pin: 003 checked isolated configuration, 005 checked dummy helper renewal over HTTP/SSE, 008 observed live upstream WebSockets, and 013 exercised browser Origin behavior. These are historical evidence, not tests of the new packages. The reports are preserved underexperiments/docs/; see the research index and import notes. The vendor checkout itself was not copied; its pin and source patch remain reproducible through the import's vendor instructions.
The blocking implementation gates are inherited-lease behavior, exact native transport/readiness compatibility, loaded-state isolation, loss-accounted capture, live proxy routing, and API-federation renewal. Do not convert a design choice into a supported feature until its plan acceptance criteria pass. The original proposal used source inspection and prior spikes; subsequent package tests and live validation are recorded in the implementation status.