Skip to content

Latest commit

 

History

History
600 lines (497 loc) · 67 KB

codex-home.md

File metadata and controls

600 lines (497 loc) · 67 KB

Codex Home

Quota activation restores deadlines from OpenCodex settings; its retry backoff remains process-local. See the quota activation contract.

Catalog HTTP acquisition follows the proxy-routing contract.

A lock in the Codex credential store is governed by descriptor identity and age, so the mere presence of its filename is neither acquisition nor release authority. Failed path-identity probes leave the lock for stale recovery and preserve the refresh callback outcome. Cooperating lock metadata changes serialize through the existing SQLite mutation transaction; release keeps the descriptor open through identity comparison and any unlink, then closes it. Failed metadata writes remove only a matching owned path after successful coordination; unknown identity, failed probes or unavailable coordination retain the path for stale recovery. Async refresh work holds no metadata transaction.

CLI installation inspection reason codes, including Windows deferral, follow the runtime inspection contract.

Explicit Codex CLI installation observation does not discover a Codex home or read/write its state. See the read-only observation contract.

Orca source-owned account import

src/codex/orca-import.ts reads only accounts listed in an explicitly selected Orca registry. Each host-managed home must match its UUID directory and ownership marker. Preview is offline and read-only; applying requires an initialized target and a stopped proxy. Existing main, configured and orphan credential identities participate in account-ID deduplication. Retry can complete an interrupted registration only for a sole, untouched importer-owned pending credential matching the registered source path and identity; it retains that record's ID.

Imported pool records retain an access-token snapshot and a local source reference, with no refresh token. src/codex/orca-auth-source.ts bounds reads and rejects linked or nonlocal paths. src/codex/account-store.ts rereads the source before returning credentials, pins account and subject identity, and fails closed for missing, expired or changed-identity sources. Orca owns refresh; even forced refresh never exchanges its refresh token. A new source bearer advances the target generation while retaining validation state and the same account's quota-history identity. Writers captured under the old generation become stale; already retained history is not reset. New imports remain validation-pending.

src/codex/auth-api/pool-quota-probe.ts also rechecks linked sources after asynchronous quota validation before issuing deferred inference warmups. Already dispatched requests retain their captured bearer.

Codex home

src/codex/paths.ts resolves Codex state from CODEX_HOME when set and valid, otherwise from ~/.codex. An unset CODEX_HOME falls back to ~/.codex, including WSL discovery. On WSL, discovery of a Windows Desktop home applies when the Linux ~/.codex is absent, is not a directory, or is a directory holding no Codex state (none of config.toml, auth.json, sessions, history.jsonl; defaultCodexHome in src/codex/home.ts). A fresh local home Codex is already using stays the home before config.toml exists (issue 5441), a bare directory does not move an existing user off a Windows home they were running against, and a stat failure other than absence keeps the local home rather than switching to a different one. Codex runtime discovery (src/codex/runtime.ts) also reads this home on Linux: after an explicit runtime, PATH, and the ordinary install locations, it enumerates the direct children of /bin/wsl//codex newest first, probes each through the isolated --version seam, and re-enumerates on every resolve so a Desktop update that replaces the hash directory is picked up (issue 5635). An explicitly set path that is unreadable or not a directory is an error, not a fallback: silently using a different home than the operator named would write provider state where nobody is looking for it. A proven missing home (including explicit CODEX_HOME) resolves lexically; native restore, remove and journal reconcile succeed without creating a directory or acquiring the config write lock. Dangling links do not prove absence, and existing homes retain ownership checks. Lock-path resolution never creates parents; unresolved parents yield a typed, handled refusal. Scalar and multi-agent feature edits refuse missing homes; prompt commits and native feature commands retain their directory creation before locking. The management native feature toggle resolves and retains the spawn invocation before preparing a home; unavailable executables return the toggle failure without creating it. Failed native toggles and prompt commits keep a home they created (src/codex/prepared-home.ts): a pathname-based removal cannot prove it still names that directory, and an empty home is what Codex itself creates. A fresh install can have that directory but no config.toml yet; applying the integration then creates an empty config.toml there (never overwriting an existing file) and continues, while a missing home directory is refused with instructions to start Codex once or set CODEX_HOME (issue 5422). On macOS, src/codex/runtime.ts tries the system then user ChatGPT.app bundle after environment, persisted selection, shim, PATH, established installs and a runnable bare fallback, preferring Contents/Resources/codex-cli/bin/codex over Contents/Resources/codex. Bundle membership compares the normalized and canonical (symlink-resolved) forms of the path and bundle with a segment boundary, including backing executables; candidate ordering keys on the generated app commands, so an established install path that links into the app keeps its precedence. Discovery and probe prechecks require exactly one top-level CFBundleIdentifier equal to com.openai.codex in a regular XML Contents/Info.plist, read in-process with a 64 KiB limit; the whole document must be valid plist XML, so binary, malformed (anywhere, not only in the identity pair), oversized or unavailable metadata fails closed. App candidates never supersede a runnable persisted selection, including legacy/discovered records and the fast path, but full discovery can report them in newerAvailable; invalid selections retain replacedConfigured recovery. When a verified app candidate exists, deferred discovery skips the bare fallback as a candidate and retains it only as the no-candidate sentinel. src/codex/home.ts imports path expansion directly from src/config/paths.ts, so a fresh WSL process can resolve its Codex home without re-entering the config facade before the resolver initializes. The managed files are:

$CODEX_HOME/config.toml
$CODEX_HOME/opencodex.config.toml
$CODEX_HOME/opencodex-catalog.json
$CODEX_HOME/opencodex-journal.json
$CODEX_HOME/models_cache.json
$CODEX_HOME/.opencodex-native-main-profiles/

Never assume macOS-only paths. Windows, service installs, and app-launched Codex can all depend on the resolved CODEX_HOME.

Log Guard reclaim in src/codex/log-guard/maintenance.ts refuses a non-regular or redirected database path. It compares canonical path and full-width filesystem device and file identity from path observations before and immediately after SQLite opens the database, before maintenance statements. Reclaim is unavailable where the filesystem does not report a usable stable file identity: missing or zero inode values return unsafe_path. File size and timestamps are measurements, not identity evidence. These path observations do not attest SQLite's opened handle or continuous file identity between the observations.

Observed catalog/cache rows and restore output follow the retired-native policy. Restore filters retired bare and trusted account-qualified native rows from its output with or without a backup. The original backup, historical user-selected settings and session records remain intact.

Journal restoration compares config and profile independently against their saved originals and recorded injected hashes. If either changed artifact lacks its injected hash, the config/profile pair and journal remain untouched and the result is explicitly unverified; callers must not convert that refusal into successful fallback cleanup. Already-original bytes need no rewrite, and absence is distinct from an empty file. The injector checks a retained hashless journal against the same baselineContent it snapshots, plus the current profile, before writing or assigning a new injected hash. Native content can establish a fresh snapshot; routed content cannot promote an unverified older original. Existing hash-backed edit preservation and external-provider opt-out remain separate paths. Removal with preserveProfile: false refuses a final-component profile symlink before changing config or journal state. Profile cleanup uses the held configuration lock, canonical parent and identity rechecks; its pathname unlink is not atomic against non-cooperating writers. A matching journal refuses profiles eligible for restoration or removal when they are symlinks, including dangling generated links, before changing either artifact or recovery evidence. Config publication is covered by withConfigWriteRecovery if later profile removal throws.

Standalone removal derives its transform and rollback preimage from the same raw bytes. Removal refuses before publication if the recovery capture no longer matches its input bytes. Nested removal also requires the outer scope's last observed file identity to remain current; its transaction preimage is retained. Advisory locking does not serialize non-cooperating edits after these checks.

src/codex/journal.ts distinguishes restored state from actual writes with configRewritten and profileRewritten; removing a generated profile counts as a write, while already-original state and an ENOENT removal outcome do not. reconcileJournal checks unverified recovery first, then incomplete recovery, before its silent no-write return. Both dead-process and mismatched-client recovery warn without file contents or client identity when either artifact remains unrestored, return false and retain the journal, including when config was rewritten but profile removal failed. Only complete recovery with an actual write reports a restore; complete already-original recovery removes the stale journal silently, and warns instead when the journal file is still present afterwards (removeJournal ignores unlink errors such as a Windows lock). Ownership/hash checks and profileRestoreFailed remain intact. tests/codex-integration/codex-journal-recovery.test.ts covers these diagnostics and write flags.

The source-built Docker image explicitly keeps CODEX_HOME=/home/bun/.codex separate from OPENCODEX_HOME=/home/bun/.opencodex. Compose persists them in codex-state and ocx-state respectively, retaining a read-only root. The image creates owner-only writable homes for bun; existing volume ownership and permissions are not repaired. The catalog resolver is unchanged; a writable empty home is not a materialized catalog.

Decision record: ADR-0005

docker compose down retains both volumes. docker compose down --volumes deletes both ocx-state and codex-state, including their credentials and catalog/state; treat it as destructive, not as an upgrade or restart command.

Service install-state ownership uses this same resolver. In WSL, an unset CODEX_HOME may resolve to the single discoverable Windows Desktop home; recording Linux ~/.codex instead would make a later repair or uninstall look foreign even though the service and runtime were started from the same environment. A record written before that discovery still names Linux ~/.codex; service commands refuse it and name the recorded home to rerun with, because stop and repair would otherwise restore a different home. A recorded spelling that still resolves to the same physical directory — a junction or symlink alias — counts as the same home for the ownership check, the recorded SQLite home, and the unattended ownership preflight. Access or transient I/O errors are unknown rather than foreign in preflight; distinct spellings with missing/non-directory paths remain mismatches. Lifecycle guards still fail closed on unknown and can throw ServiceOwnershipError. An explicit CODEX_HOME remains authoritative; nothing migrates implicitly.

Decision record: ADR-0006

SQLite-backed thread state may live outside CODEX_HOME. The one resolver in src/codex/paths.ts uses Codex's precedence: root sqlite_home in the effective config.toml, then CODEX_SQLITE_HOME, then the effective CODEX_HOME; relative SQLite homes resolve from the current working directory. History jobs resolve the database and its hashed backup identity together at call time, and admission/residue checks consume the same database path. Storage retention still owns the Codex-home tree separately and does not gain deletion authority over an external SQLite root from this resolver alone. Durable service launchers preserve an explicitly supplied CODEX_SQLITE_HOME so a background service resolves the same split state as the installing shell. Service install state records that effective SQLite home beside CODEX_HOME and OPENCODEX_HOME, and a lifecycle command requires the recorded value to match the current effective resolution; a legacy record without the field keeps its existing behavior. An absent config.toml or absent root sqlite_home permits the environment/home fallback. Any other read failure, malformed TOML, wrong-typed or blank sqlite_home is indeterminate and fails closed so history code cannot select a different database by accident. This strict parse is scoped to SQLite ownership; the tolerant root-string helper used by injection and catalog reads is unchanged.

Decision record: ADR-0007

Native-main profile ownership is bound to the real CODEX_HOME, not to an OpenCodex instance. Its encrypted vault, transaction journal, recovery marker, and referenced quarantine files live in the owner-only .opencodex-native-main-profiles directory. The unchanged .opencodex-native-profile.lock.sqlite beside that directory serializes every process sharing the home. Only plaintext login staging is instance-local under $OPENCODEX_HOME/native-main-profile-staging; a stage from one instance is invalid in another. These paths and the OS keyring are owner-only: the operating-system account that owns them is the trust boundary and already has direct access to active native credentials. OpenCodex detects and fails closed on file identities that change during an operation, but it does not claim isolation from a malicious process already running as that same trusted OS account.

Startup and the periodic stage cleaner do not acquire the profile transaction lock when both the stage registry and this instance's staging tree are proven absent. This keeps an unused profile subsystem from fencing native traffic or creating lock contention. Presence, an unsafe entry type, or any observation error still takes the locked sweep and fails closed; the fast path is based only on proven absence, never on an unreadable path.

Native-main admission is one process-global gate. Releasing the last reference to a startup entry returns a snapshot published by that startup generation to the process-initial ready state synchronously, before the release awaits anything, so a server stopped in the middle of startup convergence cannot leave the process fenced for the servers that follow it. A same-home recovery fence published independently by a profile transaction survives release. An entry created afterwards for the same home arms its own gate, and the retired generation's late convergence writes are ignored. Recovery-completion provenance is separate from the convergence promise: release retains the owner through the pending recovery and its following stage sweep. tests/codex-integration/native-profile-startup-release.test.ts pins that ordering.

A sibling instance is ocx start --port <other> while a live proxy serves the configured port, or a start where the cross-home owner check proves a different live proxy at a managed client destination. It gets past the spend-ledger lease only with its own OPENCODEX_HOME, and still shares this Codex home, ~/.claude, ~/.grok and the launchd domain with the live owner. handleStart marks the process through src/codex/sibling-start.ts before the server binds, and the mark is one-way for the process's lifetime. The cross-home check follows same-home discovery and precedes journal reconciliation. It reads the default home's protected runtime record plus every home nominated by the shared owner registry, plus managed Grok and Codex loopback URLs as location hints. The registry (~/.opencodex/ocx-homes/.json, home path only, written beside runtime-port.json publication; OCX_OWNER_REGISTRY_DIR overrides the anchor for tests only) is the locator that lets one custom home find another; it lives in OpenCodex's own namespace, never inside a protected client home, and pointers whose home no longer publishes a record are pruned before the reader's entry cap while a truncated listing fails closed as indeterminate. It accepts a different process only when the listener's PID matches a record and a fresh /healthz challenge proves possession of its attestation secret; an unauthenticated listener at a stale managed destination is not an owner, and a sole custom-home start still syncs. When a managed destination answers but ownership cannot be decided - a live listener no registered record names, a legacy record without an attestation secret, or an unreadable transport - the verdict is indeterminate and the start still takes the sibling mark, so a competing owner never rewrites shared clients on unverifiable evidence. The mark closes localClientSyncAllowed in src/codex/desired-state.ts with its own skip reason sibling, so startup sync, cache invalidation, Grok, the retained catalog writers and the native-main lifecycle stand down (the sibling runs the no-op lifecycle, so it never contends for the owner lease; its data-plane auth.json refresh still runs under the machine-wide exclusive claim). Owner-level checks cover what the gate reads backwards or never reaches: both restore entry points and the injector return before their external-provider journal cleanup, the management catalog funnel, src/integrations/catalog-refresh.ts, connectClient/syncConnectedClient/disconnectClient, the Claude roster and system env refuse, the exit teardown comes from decideStartExitTeardown, and POST /api/stop answers sharedTeardown: "not-owned" without touching the service manager. The guard refuses the native-main profile and reauth routes among the others listed in gui-and-management-api.md. The runtime record carries siblingOfPort, and ocx stop of such a runtime, live or left behind by a hard kill, claims no receipt, runs no shared teardown, does not revert the system env and does not ask the service manager: a sibling never runs under one, so an installed service is the live owner's. When the recorded sibling no longer answers and discovery reaches the owner instead (siblingStopFoundOwner), the stop leaves that proxy running, clears the stale sibling records and exits 0. A clean sibling exit removes that record; a later ocx stop refuses a discovered listener unless it proves possession of this home's runtime-record secret through a fresh /healthz challenge, so the configured-port fallback cannot stop the owner. The sibling's own drain-and-restart (src/server/management/system-restart.ts) and standalone recycle (src/client/runtime.ts) hand the mark to their replacement through OCX_SIBLING_OF_PORT and OCX_SIBLING_HANDOFF_NONCE, backed by a one-use src/codex/sibling-handoff.ts record bound to the prior sibling runtime and OPENCODEX_HOME. Connected-client recycle issues it before stopping the listener or removing that runtime record. handleStart consumes the record before any probe; a forged port env or replay grants no sibling status. A valid replacement stays a sibling while the owner is down, and its journal recovery remains skipped. Every other detached ocx start (ocx ensure, the tray, the ocx claude/opencode/minimax auto-start and the updater's restart) starts an ordinary owner and strips an inherited marker through withoutSiblingMarker.

The startup sync is not the owner's last look at config.toml. Once it settles, handleStart starts src/codex/routing-healer.ts in an unmarked owner (never in a sibling or the connected-client runtime), and the exit cleanup stops it before any teardown. An unref'd timer reads config.toml every 10 s; unchanged bytes cost nothing more. src/codex/routing-drift.ts calls routing foreign only when it is opencodex-owned (the marker line, the journaled value, or the opencodex provider table), names a loopback endpoint with an explicit port that is neither the bound port nor the loopback listener's, and no external model_provider is selected, so native, user, custom, external, restored, LAN and admission-token routing never is. Every distinct foreign hostname and port is probed with probeEndpointLiveness: a live opencodex is left alone with one log line per endpoint, unknown never advances the streak, and a heal needs dead on at least two probes spanning 20 s, dead again on a final probe, and every gate open (no sibling mark, no recycle or drain, the runtime record names this process, Codex ON and not hub-gated, no admission-token routing, a write target this process serves, no client connection and no client-owned journal). The heal is injectCodexConfig with no catalog path, a 1 s lock timeout and a synchronous beforeClientWrite guard that re-reads config.toml under the lock and aborts if any admitted bytes or destination endpoint changed. A coordinated home re-reads its admission under the lock before that guard runs, so a rewrite between plan and lock comes back as a stale-admission refusal instead: any refused or failed write whose config.toml bytes moved since the proof is the same abort, with no wait and no warning, and the next heal needs a fresh streak. A busy lock retries on the next tick. A refusal over the proven bytes waits 10 minutes, and that wait ends once routing is no longer foreign. Six attempts, or a fourth heal, within an hour pause probing and writing until the oldest one leaves the hour; the loop then resumes with a fresh streak and never stops for good. An unknown answer is asked again every 30 s, not every tick. Each heal prints one warning; a failure line carries only the first message line, with home paths masked. Detection, gates and probes read the journal only through the read-only accessors, journalOwner({ readOnly: true }) included, so watching never deletes an unreadable journal; only a heal write goes through the injector's ordinary journal handling. Threads Codex opened while routing named the dead port keep that address until they are reopened. ocx status reports the same drift, also while the loop is gated off, through codexRoutingDriftWarning in src/cli/status.ts, which reads the journal the same read-only way. It says nothing in a sibling's home, whose routing names the owner beside it, and it promises only ocx sync, because it cannot see whether a healer runs, is gated, or is paused.

Native-main refresh follows the rotation contract. Same-identity device reauth (#3898) instead obtains a new login grant through /api/codex-auth/main/reauth-device (start/status/cancel) plus ocx account main reauth. It uses the OpenAI deviceauth grant already used for pool accounts, but nothing routes through the pool login surface — /api/codex-auth/login keeps rejecting __main__ — and the commit is a sibling of the refresh write: same-identity check against the snapshot captured at start, owner-independent exclusive claim (a headless hub runs no owner lifecycle), path/hash/inode re-verification inside the claim, and one atomic write of access/refresh/id token + account_id. The old identity token is never retained beside the new grant. No claim is held while the human completes the device page, and no DTO, log, or error carries tokens, emails, or raw account ids. Cancellation is bound to the commit's exclusive claim and is rechecked immediately before publication, so a cancel delivered while the claim is contended aborts the wait and a cancelled flow cannot replace auth.json or clear its reauthentication quarantine. A cancel that arrives after the write still reports succeeded: the credential was replaced, so that is the honest terminal.

Decision record: ADR-0008

The native-write coordinator is keyed by the canonical CODEX_HOME in the effective-user runtime namespace. A pathname alone is not authority: SQLite can expose a zero-byte file before its first schema write, and a terminated process can leave that remnant behind. Eligibility treats the file as non-authoritative only after an immutable SQLite read proves version zero with no tables, the filesystem identity remains unchanged, and the file has been settled for at least one second; a fresh zero-byte creator stays on the coordinated path so its lock cannot be bypassed. ocx doctor inspects the coordinator with immutable read-only SQLite flags so diagnosis never creates WAL/SHM sidecars. It distinguishes absent, zero-byte, unversioned, rowless, valid, unsupported, changed, unsafe, and unreadable states and prints the exact path. Explicit recovery is available only after the proxy is stopped and only for a proven zero-byte state. The command revalidates the same private regular-file identity under a non-blocking SQLite write lock and moves it to a same-directory backup; it never deletes or auto-adopts legacy routed residue.

Decision record: ADR-0009

OpenCodex never overrides an explicit CODEX_HOME. On Windows, ocx doctor and ocx status nevertheless diagnose the high-confidence Orca dual-home case: both CODEX_HOME and ORCA_CODEX_HOME select Orca's orca/codex-runtime-home/home, while the ChatGPT/Codex app uses the default %USERPROFILE%\\.codex. Sync and restore output always prints the exact target Codex home; display and JSON paths redact the OS username. The diagnostic tells users to invoke OpenCodex with the app home explicitly rather than silently claiming that an unrelated app was configured. If a service was installed under the Orca home, it must first be uninstalled from that original Orca environment and then reinstalled under the app home; changing only the current shell cannot migrate the recorded service ownership.

Decision record: ADR-0010

atomicWriteFile uses a temp file named {path}.ocx.{pid}.{seq}.tmp (process ID + incrementing sequence number) to avoid collisions when concurrent writers (e.g. ocx stop and the proxy's own shutdown handler) both restore Codex config simultaneously. The temp is renamed atomically into place. Storage cleanup run metadata uses the field-scoped persisted-config mutation path, so a background Worker cannot restore unrelated API keys or provider settings from a snapshot read before the lock. If that metadata write is unavailable after cleanup has already completed, the job retains the cleanup outcome and exposes a bounded persistence error instead of relabeling the run as a Worker failure.

Cleanup manifests and satellite backups share the stage-local atomic publisher: an exclusive private temporary file is fully written and file-synced before the existing Windows-tolerant rename replaces the destination. Handled publication failures retain the previous record; directory syncing remains best-effort. This does not make a partial permanent purge reversible: restore still fails closed when a recorded logical entry has no surviving file.

OpenCodex config writers share config.toml.ocx-write.lock: feature scalar and CLI-toggle batches, injection, journal replay, removal, and restore/compensation read and write while holding it. Injection and coordinated restore take config before SQLite N; prompt writes take prompt-store before config. Nested writers receive an explicit live handle for the same canonical config destination; implicit nesting refuses fast. Symlink aliases contend on the real destination's lock. Each publication revalidates canonical identity and inode immediately before rename, including each Windows retry; the confirmed publication hook advances that witness. Async acquisition uses a monotonic deadline, defaults to two seconds, and accepts only integer timeouts from zero to ten seconds. Zero permits one immediate attempt. Native feature commands in src/cli/v2.ts receive an explicit child environment with CODEX_HOME bound to the canonical home recorded by the held config lock, without an inherited Orca home override or case-variant home key. Native feature paths retain the selected home alias, including the default .codex, for drift validation. The held destination is validated immediately before spawning and after exit, including failed exits. Native commands require the held canonical destination to be the canonical child home's config.toml; a link to a differently named file refuses before transition staging or child writes. The recovery scope captures exact bytes or proven absence and file identity before any writes. Standalone toggles refresh this preimage immediately before spawn; multi-agent transitions retain it from before staging. All later validation, publication, child failures and postconditions run inside that scope, including wrapper entry and the initial spawn validator. Alias drift restores that preimage through the canonical path while the lock remains held. Recovery validates the canonical file identity immediately before publication and every rename retry. A changed canonical target refuses recovery, preserves existing journal evidence, and retains a private preimage file beside the canonical config with its location in the diagnostic. Drift is non-retryable and stops further parent publication or alias-based compensation; a successful child replacement advances the inode witness before subsequent parent writes. Prompt journal recovery uses the same prompt-store then config order as a normal prompt commit. A busy config lock preserves config/store/journal bytes; explicit retry after release replays only the recorded images, including a missing custom config parent. Both leases release on refusal or completion.

src/codex/prompt-lock-claim.ts reserves the acquisition/takeover interval with a unique PID/token file and bakery ticket. A contender still choosing makes peers refuse, and a later contender sees an earlier published ticket. Dead reservations are removed by their unique filename; stale observation cannot rename a live successor's reusable lock path. A peer's cleanup removes the empty claims directory, so a claims directory that vanishes between a contender's mkdir and its read is contention (locked), never an unsafe namespace or a crash. That single read is both the ownership/type check and the parent witness, so a replacement that appears after a missing read is never trusted. Lock and claim records include host evidence and optional process-start evidence. Takeover requires a matching local host and two dead-PID observations; a reused PID with a different known start identity remains busy. Unknown start identity disables only that comparison: live or unknown PID liveness remains busy, and a dead lock owner is recoverable only past the grace window. Foreign-host, unknown-host, legacy and malformed records are unsafe and preserved for deliberate removal. Unknown process liveness remains busy. The lock file, claims directory and entries must have the expected type, current-user ownership on POSIX, and no symlinks. On Windows, namespace ownership relies on the per-user profile ACL; acquisition does not run a separate ACL hardener for claims directories. Machine identity is read lazily once per process, including unavailable results. Linux reads /etc/machine-id, falling back to /var/lib/dbus/machine-id, without spawning. Windows queries MachineGuid through trusted System32 reg.exe with argv, no shell, a three-second timeout and strict registry-key, REG_SZ and UUID parsing. Executable resolution uses the same OS system-directory resolver as trusted PowerShell and ignores PATH, SystemRoot and WINDIR. macOS retains its one-second sysctl boot-session lookup. If host discovery fails, a free lock can still be acquired, but its hostless record is unsafe for every later contender and requires deliberate removal. This process's start identity is cached where available; on Windows it is unknown without spawning. Another PID's start lookup runs only for a dead-owner takeover decision when the record has start evidence, with a one-second command timeout; Windows retains trusted PowerShell for that lookup. Choosing evidence is published atomically before the bakery scan. Lock filesystem operations reuse src/lib/windows-atomic-replace.ts: Windows EPERM, EBUSY and EACCES retry after 25ms and 50ms; exhaustion returns busy. Unreadable evidence or a contended exclusive open followed by EEXIST never authorizes takeover. src/codex/prompt-lock-evidence.ts shares the claim and reusable-lock guards. Every destructive attempt revalidates captured parent/entry dev/ino and exact evidence bytes; replacements and parent retargets survive retry sleeps. Stale quarantine rename binds the dead-owner observation to those bytes and identity; cleanup deletes only the moved entry, on both acquisition and failure paths. Exclusive creation also revalidates the captured parent on every attempt. Completed local reservations whose cleanup exhausts are tracked by path/token and retried before acquisition, without reclaiming active live-self reservations. Release binds the matching token to the captured file identity, contents and parent before every unlink attempt; protected writer errors propagate. The default stat provider retains device, inode and UID evidence as bigints, so distinct 64-bit file IDs remain distinct across retry fences. POSIX ownership still compares the exact UID to the current user; injected numeric stat providers remain supported. tests/codex-integration/codex-prompt-lock-sharing.test.ts covers sharing violations and distinct 64-bit file IDs that collide when converted to numbers. The last reservation removes its directory only with atomic empty-directory rmdir. Unsafe acquisition is non-retryable and carries the lock-path diagnostic through writer results. Automatic recovery is a no-op when the Codex home is proven absent and creates nothing. Journal replay selects its current evidence under the config lock; automatic recovery also decides owner liveness there and passes the held handle to replay, so stale evidence cannot authorize restoration of a replacement journal owned by a live session. Absence handling, profile removal, courtesy cleanup and compensation share this section. Prompt commits acquire through the held-handle helper and release the store lock even if config acquisition throws. Recovery and compensation revalidate the destination before writing and retain recovery evidence if the destination or selected journal changes. Incomplete config/profile compensation preserves the current journal as recovery authority. tests/codex-integration/codex-prompt-lock.test.ts uses separate synthetic processes to cover stale observation, initialization, and a killed reservation owner.

This is advisory serialization among cooperating OpenCodex writers. Native Codex and manual writes ignore it; injection witnesses and journal byte comparisons retain their separate role. It is not a kernel lock, a cross-host lock, or protection against an external writer between byte verification and rename. A rolling mix of old writers that do not reserve takeover has the same cooperation limit.

Windows secret-file hardening resolves the effective token SID through an absolute, trusted PowerShell path before granting the owner and removing inherited broad ACL entries. The normal path obtains System32 from GetSystemDirectoryW. Windows ARM64 Bun builds that cannot execute bun:ffi use a narrower ACL-only fallback to the fixed protected default installation path C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe. The fallback never applies to UAC or Task Scheduler launch, never consults environment variables or PATH, and fails closed when the fixed executable is absent.

Direct PowerShell children rely on the process launcher's windowsHide/hidden-host mechanism and must not also receive the PowerShell CLI pair -WindowStyle Hidden. On affected Windows 11 systems, Bun 1.3.14 exits that direct invocation before the command runs, which turns a valid SID or process lookup into EACLIDENTITY or a failed sync. This does not apply to Start-Process -WindowStyle Hidden inside an already-running PowerShell script, nor to .NET/VBS process-window settings.

Decision record: ADR-0011

Decision record: ADR-0012

The durable response-spill directory ~/.opencodex/responses-state-spill/ is bounded in aggregate, not only per file. Continuation state demoted out of the in-memory cap (MAX_STORED_RESPONSE_BYTES) is written there, and eviction past MAX_SPILLED_RESPONSE_BYTES removes oldest-first through the same deletion point that serves TTL and count eviction, so an evicted entry unlinks its file. One function owns that ceiling; mutation pruning, the post-restart lazy load, the periodic sweep and spill admission drive it. The periodic caller is not redundant — the mutation path runs only when traffic arrives, so a process that comes up over budget from a snapshot written under a larger ceiling would otherwise stay over it while idle.

The ceiling bounds what the store can account for, which is every entry in the map plus the superseded generations queued for unlink, and deliberately not the directory as a whole. Spill files orphaned by a crash are absent from the map, so this accounting can neither see nor price them; they are reclaimed by recoverOrphanedResponseSpills, a bounded scan that unlinks owned names past RESPONSE_SPILL_ORPHAN_GRACE_MS unless the process still references them — referenced means a live spill stub, a superseded generation queued for unlink, or a queued or in-flight publication's temp, destination, or superseded file. The sweep runs once inside the lazy load and again on each liveness tick with the tighter PERIODIC_SPILL_SWEEP_OPTS bounds, so orphans created mid-run do not wait for a restart. A host that crashes repeatedly can therefore hold spill bytes above this ceiling for up to RESPONSE_SPILL_ORPHAN_GRACE_MS past each crash. Without that aggregate bound the directory was limited only per file (256 MiB) and per entry (1000) — a 250 GiB product — which left RESPONSE_TTL_MS as the only effective limit and made disk use a function of client request rate rather than of anything the process controls. The durable snapshot that re-establishes these references after a restart is budgeted too, and its bounded stub and tombstone rows are selected before resident payloads under that budget, so a full resident cohort cannot crowd a spill reference out of responses-state.json and strand a file the store still owns.

Decision record: ADR-0013

Response-state loading performs a bounded recovery pass for interrupted snapshot writes. It only matches regular files named responses-state.json.ocx.<pid>.<sequence>.tmp, waits at least 15 minutes, and skips the current or any live PID. Eligible files are truncated before unlinking so a matching stale path is unlinked without following it. Path-based truncation is intentionally avoided: a same-user replacement could otherwise turn cleanup into a write through a symlink. Unrelated temporary files, symlinks, directories, and young/active writes are never touched; directory entries are consumed incrementally and at most 512 stale files are attempted per process start.

Decision record: ADR-0014

Windows runtime response spills never wait on icacls through Bun.spawnSync. Linux and macOS retain the immediate synchronous publication path. On Windows, the resident continuation enters one serialized publication queue and remains replayable while hardenSecretDirAsync and hardenSecretPathAsync run. Publication installs a spill stub only when the map still contains the same resident object; a superseded job deletes its newly published file instead of overwriting newer state. Pending payloads are pinned and capped at 256 MiB, so an ACL outage cannot grow an unbounded queue or be misreported as evictable memory. One caller-owned retry is allowed after a real ETIMEDOUT; the first timeout does not install a spill-failed tombstone. Required ACL failures remain fail-closed after that bounded recovery. Optional config-directory hardening uses a separate per-directory async single-flight, while required config mutation writers retain their existing awaited or synchronous fail-closed boundary. Snapshot persistence serializes the entire background pass, including its follow-up timer decision. Shutdown awaits that gate before consuming a pending pass; a stable bounded rewrite cancels the mutation timer whose bytes it incorporated. tests/responses/responses-state-flush-race.test.ts covers overlap with a background rewrite and leaves no redundant persistence timer after flush.

Each ordinary async spill write attempt owns one 30-second ACL budget shared across directory, temp, and exclusive-copy destination hardening; the single timeout retry receives one fresh whole-attempt budget. No harden step may reopen an independent 30-second window inside either attempt. Both icacls and effective-principal subprocess waits are settlement-bounded: at deadline the child is killed, unref'd, and abandoned without awaiting proc.exited. The caller-level deadline also bounds injected/shared runners, so a child that ignores termination cannot pin the serialized spill queue.

Graceful shutdown drains that serialized publication queue to a stable fixed point before snapshot serialization. The drain has a wall-clock cap with a reserved synchronous fallback budget; expiry supersedes the async writer, claims and removes any temp or destination it still owns, and only then starts fallback publication. The writer rechecks supersession before no-replace publication, while the fallback splits its reserve across the directory and file ACL hardens. This ordering is load-bearing because resident entries over 2 MiB are deliberately excluded from responses-state.json: serializing first could omit the resident before its durable spill stub exists, losing the continuation on restart. Cleanup is attempted for every abandoned writer; any failure is retained while fallback and snapshot persistence continue, then returned through the shutdown status so process exit is non-zero without sacrificing unrelated replay state. If the fallback reserve expires, every remaining resident candidate is terminalized as a bounded spill-failed tombstone before pruning, so no payload remains eligible for shutdown requeue and the snapshot flush always regains control. The terminalization pass itself is hard-capped at MAX_STORED_RESPONSES + 1; exceeding that structural bound records a bounded failure, fail-closes every remaining resident, and returns control to snapshot persistence instead of relying on the progress argument alone.

Decision record: ADR-0015

Config-backed catalog removals

A refresh removes a provider's OpenCodex-authored routed rows wholesale only when config.json agrees the provider is gone (#6529). src/codex/catalog/routed-removal.ts lists the namespaces whose rows are in the active catalog, missing from the candidate, and not enabled by the driving config. Native aliases and combo-owned rows with the generated description prefix count as combo, including bare and slashed aliases; ownership alone does not establish authorship. Foreign and trusted account-bound rows never count. Both catalog writers — retained sync (ocx start/ensure/sync) and the convergence commit (management writes, login, auto-refresh) — hold the catalog lock K, then config mutation lock C, from the config.json re-read through replacement. A busy C also refuses the retained sync write. When the file is missing, unreadable or salvaged, or still enables one of those namespaces, the write is refused and the active catalog and models cache keep their bytes, so a config-less process (an empty OPENCODEX_HOME reads defaults, which route nothing) cannot publish a native-only catalog into this home. Removals inside an enabled namespace (discovery, disabled models, deleted custom rows) and catalogs without routed rows are unaffected; see the shared catalog. ocx sync reports the refusal as one count-only warning, and ocx restore stays the deliberate way to drop routed rows. Coverage: tests/codex-integration/codex-catalog-routed-removal.test.ts, tests/codex-integration/codex-catalog-sync-hardening.test.ts, and tests/codex-integration/codex-convergence-contract.test.ts.

Catalog writer ownership and intent

src/codex/codex-home-owner.ts reads the injection journal's opencodexHome binding without modifying recovery evidence. A readable legacy journal has no binding; the next injection records its owner. Canonical aliases of the same physical OpenCodex home remain owned. A proven vanished home is stale and can be adopted, while foreign active or unknown ownership refuses writes. Every acquisition rechecks ownership under K before issuing its live permit; later calls re-evaluate the evidence.

Injection and both native-restore paths check ownership before cleanup and at publication. The exported journal snapshot and injected-state writers also check ownership themselves before mutation, including direct calls; a foreign binding protects both its identity and recovery metadata. Successful deliberate config/profile restoration releases ownership at the native restoration boundary. Exact restoration removes the journal; successful field-level restoration preserves its recovery data and releases only the home binding. Catalog-only restore retains the binding. A later catalog failure remains a failed artifact. This does not add a new transaction across native and catalog restoration or guarantee custom-path recovery after the original journal was released.

Each withCatalogWriteSerialization call supplies refresh, cache, pull or restore intent and a writer label. The existing permit registry binds that context to one live acquisition. src/codex/internal/catalog-writer.ts skips identical catalog/cache bytes and preserves their mtime. A refresh cannot clear all routed rows without readable file-backed config; restore deliberately clears them, pull keeps the hub's authority, and a cache permit cannot replace the catalog. Remote pull accepts an unchanged derived cache as success. Sync and sync-cache report owner refusals without private paths.

These checks coordinate cooperating processes running as the same OS user. They do not isolate files from an arbitrary process with that user's direct filesystem access.

Catalog write audit

Catalog and cache writes and admission refusals leave bounded diagnostic records through src/codex/catalog/write-audit.ts. The catalog serialization owner coordinates every append and compaction under K; denied writers never receive a publication permit. Identical bytes produce no event. Audit failure preserves the catalog operation's outcome.

Records contain fixed categories, time and process identifiers, redacted bounded home identity, routed counts and config provenance. They contain no model/provider names or raw command arguments. Each event is at most 2 KiB; retention keeps complete newest records within both 256 KiB and 400 records. Reads are bounded to the tail. On POSIX, owner creation is private and regular-file-only; a foreign writer can append only to an existing valid file within capacity, without creating or compacting it. Windows owner creation uses an exclusive blank file and required NTFS ACL hardening with a one-second deadline before diagnostic bytes, checking descriptor/path identity before and after hardening and refreshing metadata after legitimate ACL changes. ACL failure or timeout leaves the new file blank and skips the diagnostic. Existing Windows files are skipped by owner and foreign callers without ACL mutation because no read-only NTFS privacy verifier exists; mode 0600 does not establish NTFS privacy. Consequently Windows records at most the first event in a newly created audit file: subsequent events are skipped in the same process and after restart. This fallback does not provide a complete Windows audit stream; a failed initial harden leaves a blank residual and later events remain skipped.

The audit artifact is opencodex-catalog-audit.jsonl under the Codex home. New owner-created files with file-backed config attempt existing uninstall registration. A separate Codex home lies outside the config manifest's ownership root, so its audit remains an explicit cleanup residual; this does not broaden uninstall deletion authority.

Owner catalog healing

src/codex/catalog-self-heal.ts is preloaded without timers before the owner's startup sync. src/cli/claude-agent-startup-sync.ts forwards the sync readiness verdict and starts observation synchronously on successful settlement, before deferred Claude roster or Desktop registry awaits. Successful no-op and OFF syncs establish observation; OFF remains gated until enabled, and failed sync never adopts a baseline. Observation stops before teardown. Its unreferenced 30-second timer observes the selected catalog's file identity and parses bounded regular-file content only after a change. Importing admission or catalog writers starts no timer.

A lost routed namespace is repaired through ordinary management convergence only while its provider remains enabled and the owner is idle: no sibling, shutdown/recycle, connected client or client-owned journal; integration remains enabled, runtime ownership is current, and the Codex-home binding permits this writer. A synchronous precommit guard rechecks lifecycle, path and ownership inside K→C after gathering. Observation reuse, target changes, publication/release, precommit/postcommit and expected-target admission compare native realpaths with samePath; if either realpath is unavailable they use lexical samePath. Null selections remain unavailable. Directory symlink aliases and Windows casing refer to the same target; distinct paths and hardlinks do not. Missing/corrupt selected custom catalogs remain unavailable; healing does not select another target implicitly.

A matching committed owner publication establishes the baseline even while lifecycle gates prevent healing; baseline acceptance performs no write. Publications during this healer's own convergence remain fenced until its terminal result. Deliberate restore/native release fences pending work and clears retry state; successful owner empty publication is accepted. Failed or refused convergence retains an explicit pending retry, including catalog-success/cache-failure when no missing namespace remains on disk. Retries wait five minutes and obey rolling limits of six attempts and three successful heals per hour. Successful convergence accepts its result, while configuration deletion or a target change retires the old obligation. These are local cooperative recovery rules, not proof of installed Desktop or live-provider behavior.

Codex-home diagnostics

Desktop executable membership uses the discovered installation root, independently of the Codex state directory resolved here.

Some Codex-home conditions are reported rather than repaired, because repairing them would overwrite a deliberate user choice:

  • Bundled-plugin marketplace state on Windows (src/codex/plugins-doctor.ts), surfaced by ocx status.
  • Project-level Codex config that bypasses managed routing (src/codex/project-config-warnings.ts), surfaced by ocx doctor as a warning rather than an override. Project candidates and opened handles must be regular files of at most 1 MiB; nonblocking descriptor reads reject changed size or timestamps. Global config reads are unchanged.

Codex display-cache expiry, retained blocking main-policy evidence, and reset history follow the quota cache contract.

Plan-based automatic exclusions leave native credential files untouched and preserve the native-main exemption in the selection policy.

Base prompt import

The prompt contract governs base imports and previews.

Prompt text probe

The probe contract governs prompt capture and bounded sources.

Paginated history writer boundary

src/codex/history-provider.ts rejects provider-history changes with history_paginated_requires_native_writer when a target begins with an ordinal-bearing record, later contains a paginated record after a legacy start (#4311), or declares history_mode=paginated. The first line alone is not sufficient: a rollout that started unnumbered and was later migrated is also refused. Apply, manifest-backed restore, and explicit legacy recovery preflight all selected targets before changing database rows or manifests. The append boundary checks again. Codex owns ordinal allocation and the live projection cursor; reading the last ordinal and appending N+1 is not safe concurrent coordination. Legacy unnumbered rollouts retain their existing behavior. History Worker targets resolve the canonical manifest first and an existing pre-normalization Windows filename second, so passing an explicit target cannot bypass upgrade recovery. This guard prevents the observed stable-format corruption; it does not implement native-writer integration.

Injection preflights affected history using the normalized config candidate before writing config/profile/journal, then checks again after the complete artifact write. Native restore also rechecks after successful journal restoration or fallback removal, while exact config/profile/journal preimages and any coordinated remove transaction remain available for compensation.

The preflight opens the state store read-write-free and in that order deliberately. { readonly: true } is the primary open and the only one that joins a live writer's WAL shared memory, so a thread another process just migrated to paginated history is visible and refuses here. A WAL store whose last writer closed cleanly has no -shm to join and a read-only connection may not create one, so that open fails SQLITE_CANTOPEN on a perfectly healthy store and the catch-all turned it into history_injection_preflight_unavailable on every attempt (#4943). The immutable fallback (immutable=1 over a file: URI, the same idiom as the storage scanner and the log-guard inspector) is admitted only when neither -wal nor -shm is on disk, because that is the state in which the main database is the whole store and an immutable read is exact rather than stale. Either sidecar present, or any other open failure, keeps the original error and the refusal that follows: an immutable read is a snapshot, and a refusal this preflight fails to observe is a config transition over history Codex owns. The guard covers the whole primary attempt, not only the constructor. sqlite3_open_v2 never reads page 1, so a WAL header is not inspected until the first prepare, and on macOS that is where the absent -shm is raised; the first read therefore happens inside the attempt, where the error can still be classified. Bun's bundled SQLite on Linux materializes both sidecars on that same read and never fails, which is why Linux and Windows evidence could not see this gap.

The guardian and ocx doctor pending-history probe (countPendingOpencodexHistory) use the same opener in src/codex/history-state-open.ts. A checkpointed WAL store without sidecars is readable through that fallback and does not become a false history integrity failure; manifest and restore-target validation still run. What a detected migration does depends on which refusal it is, and on direction. The reason that stands down is one exported constant, HISTORY_RELABEL_STANDS_DOWN in src/codex/history-provider.ts, because apply and restore have to agree on it exactly and once did not.

On apply, that reason retires the relabel unit and the config/profile/journal write stands when the admitted candidate preserves any existing provider table. Retention is decided before witness construction and does not depend on history preflight passing: apply keeps any existing provider definition while selecting the requested root provider. This also protects references when native migration begins after artifact commit or during worker startup, without compensating over newer native writes. Background worker failures remain reported, and candidate bytes never change after admission. Any other reason there — an unreadable state database, a changed rollout identity, a preflight that could not run — may succeed on a later attempt, so it still restores all three preimages before returning a structured refusal, including on legacy-uncoordinated homes.

history_paginated_openai_requires_native_writer is the one apply-side reason that is neither of those. It means a provider-table transition found an openai-tagged row already paginated, and the transition as planned would take the root openai_base_url out from under it. applyPaginatedOpenaiCompat in src/codex/inject/paginated-openai-compat.ts resolves it in the same window as the provider-table retention above, before the witness: it keeps the marker-owned root override beside the table and downgrades the reason to the stand-down constant, so the relabel unit never starts and the paginated row is neither read nor written. The retained line is journaled as OpenCodex's own, which is what lets restore remove it later; a line the user owns is left in place and journaled as theirs. Only an admission-token form still refuses, because Codex's built-in openai entry cannot carry x-opencodex-api-key, and that refusal names the configuration that resolves it rather than telling the operator not to retry (#5321).

On restore and removal, that same reason no longer refuses the config half. It selects a degraded restore: every OpenCodex root routing key comes out, [model_providers.opencodex] is retained verbatim including its ownership marker, and the history relabel is skipped rather than attempted. The retained table is captured from the pre-transform bytes and re-appended into the same buffer, so the write is one atomic transformation — a config carrying root model_provider = "opencodex" without a matching table fails the whole Codex config load, not one thread, which makes that intermediate state strictly worse than the routing it replaces. If an exact journal restore brings back a same-named provider table, retention accepts it only when the parsed provider values match. src/codex/inject/provider-table.ts captures lossless table spans using the shared lexical lines in src/codex/toml-source-lines.ts, also consumed by the native defaults editor. Header-shaped text and blank lines inside multiline values stay intact; only the isolated provider block is parsed so unrelated large integers do not prevent retention. Capture and removal own only an adjacent structural comment whose trimmed text is exactly the bare ownership marker or its routing undo-hint form; marker substrings in user comments and multiline values remain untouched. Root URL and web-search ownership likewise requires an exact adjacent structural marker and a complete root assignment, with quoted keys decoded as TOML keys. Journal-recorded injected values are separate ownership evidence for marker-less app rewrites. Managed root assignment edits consume their complete validated lexical spans; value-free removal and capture accept scalar TOML integers throughout the signed 64-bit range without JavaScript number decoding, while value-based ownership still requires decoded values and malformed assignments refuse; the web-search journal field retains the whole replaced assignment, including multiline values, for re-enabling or fallback restore. Catalog and feature edits exclude header/key-shaped multiline data. Fallback blank-line normalization applies only to structural document lines, preserving opaque multiline values. Cosmetic spacing and comments do not change equality, but multiline string contents do. Invalid, duplicate, array-root or unsupported inline provider definitions refuse; retained bytes are never regenerated from parsed values. A different table fails the restore and compensation reinstates the pre-restore files rather than rebinding tagged threads to another destination. resolveRestoreHistoryDisposition in src/codex/inject/restore.ts is the single place that reads the preflight reason and answers the separate question of whether routing may come out. Every other reason keeps the hard refusal and compensates on every artifact, because retiring a provider definition its thread rows still name would orphan them. A failed config restore stops catalog/history work; coordinated restore rolls back its published remove transition. Legacy first-line provider patches are bound to the validated file identity before and after writing. These compensating checks do not provide a native-writer lock or authorize external ordinal allocation.

The legacy external writer is now refused for affected rows in any store whose schema includes history_mode, even while their row mode is still legacy. This deliberately sacrifices automatic relabeling on migration-capable stores rather than racing native conversion. It no longer costs the home its ability to be uninstalled: synchronous and asynchronous restore, inline journal restore, and direct config removal all take routing down on that reason while keeping the provider table, so an already-paginated home can be stopped and uninstalled and plain codex returns to the built-in provider. Rows naming opencodex still resolve through the retained table; their requests reach a proxy that is gone and fail with an ordinary connection error, which is a per-conversation failure rather than a broken config. ocx restore --remove-codex-provider-table removes the table for a user who accepts that those conversations stop opening; nothing selects it implicitly.

Provider-table routing can leave retained rows tagged openai while new rows use opencodex. Some native app-server/mobile versions scope an omitted thread/list.modelProviders filter to the default provider and therefore hide those retained rows; an explicit empty provider list includes all providers. The native app-server owns this RPC and the remote client talks to it directly, so the inference proxy cannot repair the filter. A successful sync/start and the dashboard's active provider-table setting show the warning; failed root routing and inactive settings do not, and the dashboard renders it once when both preferences are stored. They do not relabel history for presentation: preserving Codex's paginated-writer boundary is more important than making an incompatible remote list complete.

Decision record: provider-table remote history visibility

A degraded restore reports artifacts.config.state = "partial" with action = "routing-restored-provider-retained", carries the retained lines and the follow-up command, and leaves historyPreflightRefusal unset — that field still means nothing was attempted at all, and a stop obligation that was in fact discharged must release its receipt rather than preserve it.

Native restore preflight also checks manifest-owned targets whose rows already returned to openai, including interrupted restores. Preimage capture distinguishes absent files from unreadable artifacts and aborts before mutation when a complete snapshot cannot be read. A config restoration that was attempted and failed retains its failed artifact in the restore result; unattempted catalog and history artifacts remain skipped. Successful preimage compensation preserves config/profile/journal bytes without relabeling the failure as a skipped operation. Incomplete compensation still raises the explicit partial-write error.

The explicit model-capability contract preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Exact model input declarations now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send.

Provider-scoped approval reviewer settings are projected by the catalog owner; this surface retains its existing routing, transport and account-selection behavior.

Private pool credential metadata follows the quota-history publication identity contract; credential-only and account DTO projections omit it.

Pool quota producers and account commands follow the bounded raw-observation contract, separate from the latest display snapshot and capacity estimates.

The account history response can include a low-confidence effective capacity estimate; usage normalization retains local-answer provenance so local responses cannot supply samples.

Codex pool settings and their consumers follow the reset-first ordering contract, including independent-quota fallback, preserved affinity, strategy-specific threshold summaries, and shared short-observation freshness for switch warnings.

Upstream API-key usage follows the physical-attempt account attribution contract, independently of subscription quota observations.

Stored Direct substitution follows the credential identity contract: both synchronous and asynchronous materializers discard the caller account header before applying the stored credential; ordinary native Direct passthrough is unchanged.

Native-main owner claims and credential-generation backoff remain authoritative during priority failback priming; the preference grants no access through a fenced main profile.

Automatic account exhaustion and recovery use the spendable Codex credit evidence contract, including independent freshness, upstream refusal, and reset-ticket separation.