Skip to content

Admit codex-cli 0.147.0 after repeating the pin's policy checks - #272

Merged
schickling merged 1 commit into
mainfrom
schickling/admit-codex-0147
Aug 18, 2026
Merged

Admit codex-cli 0.147.0 after repeating the pin's policy checks#272
schickling merged 1 commit into
mainfrom
schickling/admit-codex-0147

Conversation

@schickling

@schickling schickling commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Why

SUPPORTED_CODEX_CLI_VERSIONS admitted 0.145.0 and 0.146.0 only, and ensure_supported_version (src/codex_app_server.rs:2101) is a hard gate: on a machine where codex --version reports codex-cli 0.147.0 — the current npm latest, and what codex update installs — every controlled Codex launch aborts before it spawns the app-server, so native delivery is out of service for that agent until the constant is edited. The constant's doc comment is right to be strict, and this PR does not relax it: it repeats the two checks the policy demands for 0.147.0 and records exactly how far each one got.

What

  • Admit codex-cli 0.147.0 alongside 0.145.0 and 0.146.0. Nothing else about the gate changes: the comparison stays an exact string match against the allowlist.
  • Extend the constant's doc comment to name the part of the policy that 0.147.0 does not yet satisfy. A version whose live evidence is partial must say so at the constant, or the comment asserts something false about one of its own entries.
  • Rewrite protocol_version_gate_accepts_only_the_exact_allowlist to derive its accepted cases from SUPPORTED_CODEX_CLI_VERSIONS itself, so a future bump cannot widen the allowlist without the test following it. It pins rejection of an older unadmitted release (0.144.0) and of 0.148.0-alpha.21.
  • 0.148.0-alpha.21 stays rejected deliberately. An unreleased prerelease cannot carry live evidence, so admitting it would assert a check that is impossible to perform.
  • st2's control connection is an observer; the --remote TUI is the interactive client that answers server→client requests. The ServerRequest arm list is byte-identical across the two versions, so no new request class appeared that st2's observer could stall on by ignoring it.

Impact

  • No behaviour changes on 0.145.0 or 0.146.0. Both remain admitted, and the gate's accept/reject logic, error text, and call site are untouched.
  • No wire format, run loop, teardown, or presence change. INVARIANTS.md is unchanged; no invariant is claimed, relaxed, or added.
  • No generated schema or digest is committed — see the last item under Checks for why.
  • One source file changes. No new dependency, no new file, no CLI surface.

Checks

Check 1 — delivery-critical schema comparison, complete.

codex app-server generate-json-schema --out <dir> for both versions (0.146.0 installed to a scratch npm prefix; 0.147.0 is the system binary). The raw diff is unusable — 0.147.0 emits object keys in sorted order, so all 246 files "differ":

diff -rq schema/v146 schema/v147
  → every file reported as differing, e.g.
    3d2 <   "title": "RequestId"
    9,10c8,9 <   "type": "integer" / "format": "int64"  →  "format": "int64" / "type": "integer"

After canonicalizing every file with sorted keys, the real diff is ~40 files. Comparing the two aggregate definitions maps:

total defs: 537 (0.146.0) → 557 (0.147.0)
REMOVED defs: []
ADDED defs:   ThreadSection, ThreadSection{Create,Delete,List,Move,Update}{Params,Response},
              PluginSearchResult, PluginSearchScope, PluginDisabledReason, ThreadSearchSortKey,
              DesktopOnboardingEntrypoint, ExternalAgent* (6)
CHANGED defs: AccountLoginCompletedNotification, ClientRequest, CommandAction,
              ExternalAgentConfigDetectResponse, ExternalAgentConfigImportHistoryRecordParams,
              ExternalAgentConfigImportItemTypeSuccess, InitializeCapabilities, McpAuthStatus,
              Model, PlanType, PluginSummary, ResponseItem, Thread, ThreadItem,
              ThreadListParams, ThreadMetadataUpdateParams, ThreadSortKey

Every definition st2 parses, checked individually against the code rather than against a supplied list — src/codex_app_server.rs reads initialize, initialized, hooks/list, thread/loaded/list, thread/resume, thread/started, thread/status/changed, turn/started, turn/completed, item/started, item/completed, turn/start, turn/steer:

ThreadStatus: identical                    ThreadResumeParams: identical
ThreadActiveFlag: identical                ThreadResumeResponse: identical
ThreadStartedNotification: identical       TurnStartParams: identical
ThreadStatusChangedNotification: identical TurnStartResponse: identical
TurnStartedNotification: identical         TurnSteerParams: identical
TurnCompletedNotification: identical       TurnSteerResponse: identical
ItemStartedNotification: identical         ThreadLoadedListParams: identical
ItemCompletedNotification: identical       ThreadLoadedListResponse: identical
HooksListParams: identical                 HooksListResponse: identical
ServerRequestResolvedNotification: identical                Turn: identical
Thread: CHANGED    ThreadItem: CHANGED

ThreadStatus and ThreadActiveFlag are byte-identical, activeFlags included: arms notLoaded / idle / systemError / active, with active still carrying required activeFlags: ThreadActiveFlag[] and the enum still exactly waitingOnApproval, waitingOnUserInput.

ServerRequest arms, byte-identical in both versions — no arm added, removed, or renamed:

account/chatgptAuthTokens/refresh, applyPatchApproval, attestation/generate,
execCommandApproval, item/commandExecution/requestApproval, item/fileChange/requestApproval,
item/permissions/requestApproval, item/tool/call, item/tool/requestUserInput,
mcpServer/elicitation/request

ServerNotification (70 arms) and ClientNotification (1) are unchanged. ClientRequest gained 5 arms, all ThreadSection*, none of which st2 calls.

Every difference found in the surface st2 consumes, classified:

Change Where Class
Object keys emitted in sorted order all 246 files cosmetic — serializer output order only, no wire change
Thread.isPinned removed Thread breaking for a reader of isPinned; st2 reads only /result/thread/{id,status/type,turns}
Thread.section, Thread.sectionEnteredAt added (both nullable, default null) Thread additive
ThreadItem.mcpToolCall.readOnlyHint added (nullable) ThreadItem additive
ThreadItem.imageGeneration.transparentBackground added (nullable, default null) ThreadItem additive
CommandAction.read.path $ref AbsolutePathBufLegacyAppPathString CommandAction cosmetic on the wire — both are {"type":"string"}, and LegacyAppPathString already existed in 0.146.0; st2 never reads it
InitializeCapabilities.extensions added (nullable object) InitializeCapabilities additive; experimentalApi is unchanged and is the only capability st2 sends
ToolRequestUserInputParams.isBlocking added, required ToolRequestUserInputParams additive for st2 — a server→client request the observer never answered in either version; the --remote TUI is the client that answers it
ResponseItem.encrypted_function_args added (nullable array) ResponseItem additive
10 new ThreadSection* RPCs ClientRequest additive; st2 never calls them

Nothing st2 parses changed shape. No definition was removed.

Check 2 — live remote-TUI evidence, partial. See Limits.

A real controlled launch, st2 codex-app-server driving the real codex-cli 0.147.0 under a PTY, with the version gate patched to admit it. Reproduced twice; artifacts are the wrapper's own diagnostics.

codex --version
  → codex-cli 0.147.0

wrapper.log
  → ownerAcquired / runtimePublished / appServerStarting / appServerStarted
    waitingForControlSocket / controlSocketConnected / controlInitialized
    tuiStarted / waitingForThreadBinding / threadBound

binding.json
  → {"schema":"st2.codex-thread-binding.v1", ..., "threadId":"01a01388-3867-73e0-9e56-e9dd0822cc1f"}

control-state.json
  → {..., "subscribed":true, "observed":{"kind":"idle"}}

That exercises, against the real binary: the version gate, the controlled app-server spawn, the control socket, the WebSocket handshake, initialize + initialized, and thread/started parsed for /params/thread/id and /params/thread/status/type — the binding carries a real thread id and the observed status resolved to idle.

The resume path polls thread/loaded/list and parses /result/data as a string array without bailing:

codex control: requesting TUI-loaded thread list      (×5, no parse error)

With a message queued through st2 message send, the delivery path selected the inbox head and submitted turn/start:

delivery-state.json
  → {"schema":"st2.codex-delivery-state.v1", ...,
     "filename":"1787034315897-0ny64d.md",
     "clientId":"st2:efda01056149e16e08e8a6f0eca48b9c9c595230c23d351af368b4ad40256cef",
     "phase":"attempted"}

remote TUI screen
  → "[id: 0ny64d] ■ You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage
     to purchase more credits or try again at Aug 20th, 2026 5:31 AM."

control-state.json
  → {..., "observed":{"kind":"held","reason":"systemError","turnId":null}}

The TUI rendered [id: 0ny64d], matching the inbox filename 1787034315897-0ny64d.md: st2's delivered prompt reached the thread and was submitted to the model on 0.147.0. The turn then failed on the account's usage limit, and thread/status/changedsystemError was parsed from /params/threadId and /params/status/type.

The independent confirmation of the limit:

codex exec --skip-git-repo-check 'Reply with exactly: st2-live-evidence-ok'
  → ERROR: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage
    to purchase more credits or try again at Aug 20th, 2026 5:31 AM.

Test suite.

nix develop --command cargo test --locked --lib codex_app_server::tests::protocol_version_gate
  → test codex_app_server::tests::protocol_version_gate_accepts_only_the_exact_allowlist ... ok
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 320 filtered out
nix develop --command cargo test --locked --lib codex_app_server
  → test result: ok. 34 passed; 0 failed; 0 ignored; 0 measured; 287 filtered out

nix develop --command cargo test --locked --no-fail-fast reports 8 failing targets: catalog_diff, eval_run_e2e, eval_up, materialize, native_only, nomad_survival, targeted_reconcile, transport_isolation. None is introduced here, and the honest reason is twofold.

catalog_diff::classification_only_and_nested_agent_filename_changes_are_exact is the known stale-KDL-fixture failure. native_only::tracked_product_surface_contains_only_native_names fails on tracked repo content unrelated to this branch:

retired product surface returned:
docs/vrs/spec.md contains "st2 remove"

The rest are fleet- and process-level targets — eval_up boots real supervised agents, nomad_survival crosses a systemd scope — and they were run while three sibling agents executed the same suite concurrently on this machine, which is enough to explain them:

st2_up_boots_a_specs_team
  → t.a not running; sessions=[("dev3.t.a", "running"), ("dev3.t.b", "running")]

Because this branch changes exactly one file, the classifying run is the same eight targets with src/codex_app_server.rs reverted to its parent state, run serially in the same worktree. The failure sets are identical — same eight targets, same thirteen test names, in both directions:

diff <(failing tests, parent state) <(failing tests, this branch)
  → no differences

catalog_diff          classification_only_and_nested_agent_filename_changes_are_exact
eval_run_e2e          canonical_agents_freeze_the_admitted_route_across_post_boot_catalog_mutation
eval_up               st2_down_tears_down_a_spec_fleet
                      st2_up_boots_a_specs_team
                      st2_up_once_atomically_respawns_a_hard_killed_agent
                      st2_up_spec_supervises_and_respawns_a_killed_agent
materialize           up_materialize_only_writes_the_overlay_without_needing_pty
native_only           clean_path_supports_help_validate_env_and_doctor
                      tracked_product_surface_contains_only_native_names
nomad_survival        managed_agent_color_contract_crosses_systemd_scope
targeted_reconcile    targeted_once_real_pty_preserves_sibling_generation_across_selected_lifecycle
transport_isolation   exec_task_survives_transport_cgroup_cascade
                      pty_task_survives_transport_cgroup_cascade

Every one of those thirteen fails identically with this branch's single file reverted, so none is introduced here. The scoped signal is the one that actually covers the change: all 34 codex_app_server unit tests pass, including the rewritten gate test.

Formatting: cargo fmt --check is dirty across the repo and is not a usable gate. rustfmt --check src/codex_app_server.rs reports the same 10 pre-existing hunks before and after this change (lines 1851, 1881, 1896, 1903, 1915, 1925, 1940, 1950, 3275, 4119); none is in an edited region.

On the issue's optional fourth item. Not done, and this bump is the argument against the simple form of it. A digest of the raw generator output would have flagged 100% of files as changed here — 0.147.0 sorts its object keys — while telling a reviewer nothing. A digest of the canonicalized output changes on every additive field too, so it cannot separate additive from breaking either. What made this comparison mechanical was canonicalization plus selecting the definitions st2 actually parses; that is a small tool with its own tests, not a committed blob, and it does not belong in a version-pin PR.

Limits

The live half of the policy is not fully met for 0.147.0, and the reviewer should weigh that before merging.

The account was over its usage limit for the whole of this work, with the reset estimated at Aug 20th, 2026 5:31 AM, so no model turn could complete. Evidence stops at a submitted turn/start. Specifically, these remain unproven on 0.147.0:

  • The turn/start response body. delivery-state.json is written at phase: "attempted" before the request goes out, so its presence does not prove a response came back; /result/turn/id was never observed to parse.
  • turn/started — the observed state never reached Active { turn_id }, so this notification was not seen.
  • turn/completed.
  • The typed item/completed / userMessage receipt. Delivery never advanced past attempted to accepted.
  • turn/steer and the whole steer path, which needs an active turn.
  • The thread/resume subscription path — /result/thread/id, /result/thread/status/type, /result/thread/turns — and reconcile_resume. A resume needs a persisted rollout, and a thread with zero completed turns has none. The attempt failed with, from the real binary:
ERROR: No saved session found with ID 01a01388-3867-73e0-9e56-e9dd0822cc1f.
Run `codex resume` without an ID to choose from existing sessions.

These are the same schema definitions the comparison above found byte-identical to 0.146.0, which is the basis for admitting the version now — but that is a schema argument, not the live evidence the doc comment asks for. Re-running the live half after the quota resets would close it.

Unrelated to this bump, and not a regression. With --listen — the only mode st2's controlled launch uses — the app-server refuses a symlinked $CODEX_HOME/app-server-control:

Error: socket directory path exists and is not a directory: <CODEX_HOME>/app-server-control

This reproduces identically on 0.146.0, an already-admitted version, so it is a live failure on the current pin rather than anything admitting 0.147.0 introduces. If it argues either way about this PR, it argues mildly for it: the bug is present today on a version st2 already launches. (0.145.0 untested.)

The scope is narrow, and bare codex app-server is unaffected — a symlinked control directory only refuses on the --listen path. It is not the --listen socket path (a real directory there still refuses), not CODEX_HOME itself being symlinked (a real CODEX_HOME with a symlinked app-server-control child is enough), and not a broken link (the target directory exists in every failing case). ~/.codex/app-server-control is symlinked on this machine, which is why the live runs above used a scratch CODEX_HOME rather than disturbing the operator's setup.

Filed as #275. This is a provider-side behaviour, not an st2 contract, and it is not addressed here.

Ready for review.

Related

Closes #267. Its first and third acceptance boxes are met; the second — live remote-TUI evidence — is partial, as set out above, and is the reason this is a judgment call rather than a clean tick. The fourth was explicitly optional and separable, and is argued against on the evidence of this bump.

#263 read activeFlags off the 0.147.0 schema while st2 still refused that version. The comparison here confirms ThreadActiveFlag is byte-identical across 0.146.0 and 0.147.0, so that reading also holds on a supported version — but on 0.145.0/0.146.0 it remains schema-confirmed only, not live-captured.

st2's controlled Codex launch refused the installed 0.147.0, taking native
delivery out of service on any machine that ran `codex update`.

The schema comparison 0.146.0 -> 0.147.0 is complete: no definition was
removed, no `ServerRequest` arm was added or renamed, and every message
envelope st2 parses is byte-identical after canonicalization. The raw diff
is noise because 0.147.0 emits object keys in sorted order.

Live evidence reached a submitted `turn/start` against the real binary: the
delivered prompt rendered in the remote TUI, which then failed on the
account's usage limit, with a reset estimated at 2026-08-20. Everything
gated behind a completed model turn is therefore unproven on 0.147.0. The
constant's doc comment names that gap rather than asserting a check that did
not finish.

The existing gate test asserted 0.147.0 was rejected. It now derives its
admitted cases from the constant, and pins rejection of an older unadmitted
release and of the 0.148.0 alpha.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@schickling schickling added type:chore Maintenance, cleanup, dependencies, CI, or refactoring · Set: manual area:driver Harness drivers: launch, MCP, app-server, native delivery · Set: manual harness:codex Codex-specific behavior · Set: manual origin:agent Filed or primarily produced by an AI agent · Set: manual labels Aug 18, 2026
@schickling
schickling marked this pull request as ready for review August 18, 2026 06:57
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@schickling
schickling merged commit 02fbed8 into main Aug 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:driver Harness drivers: launch, MCP, app-server, native delivery · Set: manual harness:codex Codex-specific behavior · Set: manual origin:agent Filed or primarily produced by an AI agent · Set: manual type:chore Maintenance, cleanup, dependencies, CI, or refactoring · Set: manual

Projects

None yet

Development

Successfully merging this pull request may close these issues.

st2 refuses the locally installed codex-cli, and the version pin is now behind by one release

1 participant