feat(acp): add a dormant Codex ACP backend seam and name the harness-onboarding sequence - #7813
Conversation
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Invisible on public builds; on edition builds a registered backend gets a legible row (policy-id label, generic icon, honest status line) instead of vanishing. [UX-REVIEWED] c214698 |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS The panel now widens selectability server-side, but per-backend disclosure stays a hard-coded frontend literal — capability generalizes, its compensating control doesn't. Watch
Suggestions
[DESIGN-REVIEWED] c214698 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All counts run. Producing the review. First-Principles-Verdict: CONCERNS The dormant seam's only activator is out-of-repo — the RFC the description cites never names Codex — and What this change shipsIntent: let the core name, spawn, and govern a Codex agent without offering it to anyone yet, and fix the dashboard panel that could never show a registered agent — an ADDITION carrying one FIX.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] c214698 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Adds the Codex adapter as an adapted harness: known and driveable, but absent from BASELINE_SELECTABLE_BACKENDS, so no build offers it until an edition registers it with register_selectable_backend. Vocabulary and identifier - acp_backends.py: ACP_BACKEND_CODEX in ACP_BACKENDS_KNOWN and in POLICY_ID_BY_BACKEND, so a governance rule can name the id before an edition registers it. Absent from BASELINE_SELECTABLE_BACKENDS: the spawn path is complete, but backend_install.py has no probe for the adapter, so the switch would render with nothing to say about a session that failed to start. test_agent_backend_editable.py names the exception in NOT_SHIPPED_SELECTABLE rather than relaxing the baseline == known assertion. - acp/types.py: PROVIDER_LABEL_CODEX, plus an explicit non-membership decision with its reason for every Group B capability set (H6/H8): one process per session, no _session/steer, a Node harness so Crew's sandbox is the only OS confinement it gets, and its own credentials store so a kiro logout says nothing about it. Spawn path - acp/client.py: _is_codex; PROTOCOL_VERSION_CODEX as its own literal (H10, so a future divergence is a one-line edit rather than a silent downgrade of whichever harness moved first); _resolve_codex_acp_bin mirroring the claude resolver's order and node-resolution rules; a _spawn branch; the _codex_session_mcp_servers override seam; per-harness log labels; and a session/load arm -- the adapter keeps its own session records, so gating file_ok on a kiro transcript would make an activated codex session start fresh every time instead of resuming. - acp/client.py: each per-harness mcpServers hook is now spliced only for its own backend. Both defaults return [], so the previous shared splice was inert, but an edition overriding both hooks would hand a claude session codex's entries and vice versa -- and an entry whose transport the adapter does not advertise fails the whole session/new, not just that server. - acp/client.py: _vendored_claude_acp_roots -> _vendored_acp_roots. The roots are plain node_modules directories and each resolver joins its own package path onto them, so both adapters share it; the claude name would have made the next reader think codex had its own copy. Tuning channels stated as membership (H6) Six sites in providers/acp.py read `if is_claude_backend: <adapter> else: <kiro>`. That is correct with two harnesses and wrong with three: codex took the kiro arm at every one of them. ACP_BACKENDS_EFFORT_VIA_CONFIG_OPTION and ACP_BACKENDS_KIRO_SLASH_COMMANDS replace the negation. - stream_command and change_effort routed codex into _kiro.dev/commands/execute, a kiro extension the adapter does not implement, so every slash command and every effort push would have returned -32601 and reset the session. - _apply_effort_overlay and _apply_tool_search_overlay wrote the kiro workspace cli.json overlay for codex, which never reads it. The companion clear (_clear_cli_overlay_effort) was ALREADY membership-gated, so those writes left a stale effort level in the user's workspace that no later clear could reach. - clear_effort took the kiro arm: overlay write, then a /effort push. - The effort telemetry label was literally "claude" or "kiro", so a codex session logged backend=kiro. It now resolves through POLICY_ID_BY_BACKEND. - change_effort now reports unsupported for a harness in NEITHER set instead of defaulting to a channel. _set_claude_effort -> _set_effort_config_option: the method is the channel, not the harness, and codex now arrives there too. Tests - test_harness_parity.py: codex is known but not shipped-selectable; codex carries its own provider label; the model-switch channel is opt-in; the effort channel is opt-in and the two channels are disjoint; the overlay is written only for its readers; each mcpServers seam is spliced only for its own harness; codex keeps its own spawn branch and protocol literal. - test_acp_backend_kas.py: is_codex_backend in the mutual-exclusion pin, plus an ACP_BACKENDS_ACP_RUNTIME assertion. The is_acp_runtime_backend == not is_claude_backend equivalence is scoped to the three backends conversion covered rather than ACP_BACKENDS_KNOWN: that identity holds only while claude is the SOLE non-runtime harness, and codex is the first additional one, so widening it would require every future harness to be claude-shaped -- the negation reasoning H5/H6 exist to retire. - test_agent_sdk_backend_install.py: the codex install row reads unknown and names nothing to install, which is the gap that keeps it out of the baseline. Deliberately not changed: a failed adapter resolution stays cached for the process lifetime. That is pre-existing, documented behaviour for the claude resolver, and codex now matches it. Fixing it for codex alone would reintroduce the asymmetry the rest of this change removes -- it should be both or neither, in its own PR. .github/black-baseline.txt loses one entry: test_acp_backend_kas.py became black-clean, and the gate requires a graduated file to be pruned.
`harness-parity.md` says what an added harness may not do to the Kiro path. Nothing said what it must do to land at all, so each onboarding rediscovered the order — and rediscovered it from KAS and Claude Code in retrospect, which is the wrong direction: the two harnesses that predate the invariants cannot show which stage a third one gets stuck at. Derived from the Codex seam instead, which makes the gap visible as a gap. Two stages here did not exist as stages until a third harness needed them. Stage 2 had five capability sets and no tuning channels at all; the Codex work took it to eight, one set per channel because a harness can implement one and not another. Stage 5 (the install probe) was invisible while every known id happened to be selectable, and it is the reason Codex lands dormant — one named stage, not a re-litigation. Also fixes a contradiction the Codex seam left in `harness-parity.md`: the intro correctly describes Codex as a named exception carried in `NOT_SHIPPED_SELECTABLE`, while point 4 of "Adding or changing an invariant" still asserted `ACP_BACKENDS_KNOWN` and `BASELINE_SELECTABLE_BACKENDS` were equal and that any id outside the baseline fails the narrowing check. They are no longer equal, and the check now accepts a named exception.
ff0ec5e to
c214698
Compare
Codex could be spelled by the core but not chosen. The vocabulary, the spawn path and the adapter resolution landed in #7813; what was missing was everything that decides whether offering the switch is honest. Nothing established how a harness is made to ask before it runs a tool, and backend_install.py had no probe that could explain a failed session. acp_tool_gate resolves one routing verdict per harness, so the refusal message, a doctor row and any later surface cannot disagree, and it REFUSES a session whose tool calls would bypass HookManager.on_tool_call. For Codex the client verifies that session/new advertised mode=read-only and applies it after session/new|load, before any prompt can run. Not advertised gives INDETERMINATE, an observed write rejection gives BYPASSED, and both refuse identically: a guarantee that lapses when evidence is missing is not a guarantee. There is no local opt-out, because a config bool could undercut a governance ceiling set above the operator. It also puts both ACP adapter OAuth token stores -- ~/.codex/auth.json and ~/.claude/.credentials.json -- on the sensitive-home read-gate floor. Neither was classified before, so the agent's own fs_read could lift either one. Kiro Crew never reads them and only ever checks that one exists so it can name the right sign-in command, so nothing legitimate loses access, and only the token leaf is classified -- the adapter's readable siblings stay readable. The credential mask handed to an enforced adapter's child is DERIVED from security.sensitive_home_dirs() minus that harness's own leaf, re-anchored under every override root and under both home spellings, so a leaf added to the floor is covered with no edit in the ACP layer. The macOS profile emits both a subpath and a literal deny for each entry, because most of that list is plain files and a subpath rule over a non-directory was asserted in three comments here and contradicted in a fourth, with no test exercising sandbox-exec either way. Selecting Codex with agent.sandbox="off" now refuses to start. wrap_argv returns from its off branch before it applies extra_hidden_dirs, so the mask -- Codex's only compensating control, since ACP v1 cannot prompt on a passive read -- silently evaporated. The refusal keys on the EFFECTIVE tier so a governed min_level floor does not false-refuse, and a host with no sandbox backend fails closed without consulting any mutable policy value, since both the opt-in and the floor can move between the preflight and the spawn. Known gap, documented rather than closed: ACP v1 offers no way to require a prompt for a passive read, so an adapter can still read its own token. That is inherent to running these harnesses at all -- kiro-cli is handed its own model credential in an environment nothing gates, and Claude Code already ships on this baseline with no credential mask at all -- and closing it needs upstream support rather than a change here.
Codex could be spelled by the core but not chosen. The vocabulary, the spawn path and the adapter resolution landed in #7813; what was missing was everything that decides whether offering the switch is honest. Nothing established how a harness is made to ask before it runs a tool, and backend_install.py had no probe that could explain a failed session. acp_tool_gate resolves one routing verdict per harness, so the refusal message, a doctor row and any later surface cannot disagree, and it REFUSES a session whose tool calls would bypass HookManager.on_tool_call. For Codex the client verifies that session/new advertised mode=read-only and applies it after session/new|load, before any prompt can run. Not advertised gives INDETERMINATE, an observed write rejection gives BYPASSED, and both refuse identically: a guarantee that lapses when evidence is missing is not a guarantee. There is no local opt-out, because a config bool could undercut a governance ceiling set above the operator. It also puts both ACP adapter OAuth token stores -- ~/.codex/auth.json and ~/.claude/.credentials.json -- on the sensitive-home read-gate floor. Neither was classified before, so the agent's own fs_read could lift either one. Kiro Crew never reads them and only ever checks that one exists so it can name the right sign-in command, so nothing legitimate loses access, and only the token leaf is classified -- the adapter's readable siblings stay readable. The credential mask handed to an enforced adapter's child is DERIVED from security.sensitive_home_dirs() minus that harness's own leaf, re-anchored under every override root and under both home spellings, so a leaf added to the floor is covered with no edit in the ACP layer. The macOS profile emits both a subpath and a literal deny for each entry, because most of that list is plain files and a subpath rule over a non-directory was asserted in three comments here and contradicted in a fourth, with no test exercising sandbox-exec either way. Selecting Codex with agent.sandbox="off" now refuses to start. wrap_argv returns from its off branch before it applies extra_hidden_dirs, so the mask -- Codex's only compensating control, since ACP v1 cannot prompt on a passive read -- silently evaporated. The refusal keys on the EFFECTIVE tier so a governed min_level floor does not false-refuse, and a host with no sandbox backend fails closed without consulting any mutable policy value, since both the opt-in and the floor can move between the preflight and the spawn. A gate refusal is non-retryable end to end. AcpToolGateUnroutable documents itself so but subclasses AcpError, so ensure_ready's transport ladder used to respawn the adapter once before reaching the identical refusal; it now has its own handler ahead of that ladder. And _sandbox_preflight translates the leaf module's plain ToolGateUnroutable into it at the boundary, the way the session-routing path already did -- raised raw it was neither an AcpError nor the type that handler names, so a sandbox-floor refusal escaped ensure_ready uncaught and skipped the cleanup every other refusal path runs. Known gap, documented rather than closed: ACP v1 offers no way to require a prompt for a passive read, so an adapter can still read its own token. That is inherent to running these harnesses at all -- kiro-cli is handed its own model credential in an environment nothing gates, and Claude Code already ships on this baseline with no credential mask at all -- and closing it needs upstream support rather than a change here.
Codex could be spelled by the core but not chosen. The vocabulary, the spawn path and the adapter resolution landed in #7813; what was missing was everything that decides whether offering the switch is honest. Nothing established how a harness is made to ask before it runs a tool, and backend_install.py had no probe that could explain a failed session. acp_tool_gate resolves one routing verdict per harness, so the refusal message, a doctor row and any later surface cannot disagree, and it REFUSES a session whose tool calls would bypass HookManager.on_tool_call. For Codex the client verifies that session/new advertised mode=read-only and applies it after session/new|load, before any prompt can run. Not advertised gives INDETERMINATE, an observed write rejection gives BYPASSED, and both refuse identically: a guarantee that lapses when evidence is missing is not a guarantee. There is no local opt-out, because a config bool could undercut a governance ceiling set above the operator. It also puts both ACP adapter OAuth token stores -- ~/.codex/auth.json and ~/.claude/.credentials.json -- on the sensitive-home read-gate floor. Neither was classified before, so the agent's own fs_read could lift either one. Kiro Crew never reads them and only ever checks that one exists so it can name the right sign-in command, so nothing legitimate loses access, and only the token leaf is classified -- the adapter's readable siblings stay readable. The credential mask handed to an enforced adapter's child is DERIVED from security.sensitive_home_dirs() minus that harness's own leaf, re-anchored under every override root and under both home spellings, so a leaf added to the floor is covered with no edit in the ACP layer. The macOS profile emits both a subpath and a literal deny for each entry, because most of that list is plain files and a subpath rule over a non-directory was asserted in three comments here and contradicted in a fourth, with no test exercising sandbox-exec either way. Selecting Codex with agent.sandbox="off" now refuses to start. wrap_argv returns from its off branch before it applies extra_hidden_dirs, so the mask -- Codex's only compensating control, since ACP v1 cannot prompt on a passive read -- silently evaporated. The refusal keys on the EFFECTIVE tier so a governed min_level floor does not false-refuse, and a host with no sandbox backend fails closed without consulting any mutable policy value, since both the opt-in and the floor can move between the preflight and the spawn. A gate refusal is non-retryable end to end. AcpToolGateUnroutable documents itself so but subclasses AcpError, so ensure_ready's transport ladder used to respawn the adapter once before reaching the identical refusal; it now has its own handler ahead of that ladder. And _sandbox_preflight translates the leaf module's plain ToolGateUnroutable into it at the boundary, the way the session-routing path already did -- raised raw it was neither an AcpError nor the type that handler names, so a sandbox-floor refusal escaped ensure_ready uncaught and skipped the cleanup every other refusal path runs. Known gap, documented rather than closed: ACP v1 offers no way to require a prompt for a passive read, so an adapter can still read its own token. That is inherent to running these harnesses at all -- kiro-cli is handed its own model credential in an environment nothing gates, and Claude Code already ships on this baseline with no credential mask at all -- and closing it needs upstream support rather than a change here.
Codex could be spelled by the core but not chosen. The vocabulary, the spawn path and the adapter resolution landed in #7813; what was missing was everything that decides whether offering the switch is honest. Nothing established how a harness is made to ask before it runs a tool, and backend_install.py had no probe that could explain a failed session. acp_tool_gate resolves one routing verdict per harness, so the refusal message, a doctor row and any later surface cannot disagree, and it REFUSES a session whose tool calls would bypass HookManager.on_tool_call. For Codex the client verifies that session/new advertised mode=read-only and applies it after session/new|load, before any prompt can run. Not advertised gives INDETERMINATE, an observed write rejection gives BYPASSED, and both refuse identically: a guarantee that lapses when evidence is missing is not a guarantee. There is no local opt-out, because a config bool could undercut a governance ceiling set above the operator. It also puts both ACP adapter OAuth token stores -- ~/.codex/auth.json and ~/.claude/.credentials.json -- on the sensitive-home read-gate floor. Neither was classified before, so the agent's own fs_read could lift either one. Kiro Crew never reads them and only ever checks that one exists so it can name the right sign-in command, so nothing legitimate loses access, and only the token leaf is classified -- the adapter's readable siblings stay readable. The credential mask handed to an enforced adapter's child is DERIVED from security.sensitive_home_dirs() minus that harness's own leaf, re-anchored under every override root and under both home spellings, so a leaf added to the floor is covered with no edit in the ACP layer. The macOS profile emits both a subpath and a literal deny for each entry, because most of that list is plain files and a subpath rule over a non-directory was asserted in three comments here and contradicted in a fourth, with no test exercising sandbox-exec either way. Selecting Codex with agent.sandbox="off" now refuses to start. wrap_argv returns from its off branch before it applies extra_hidden_dirs, so the mask -- Codex's only compensating control, since ACP v1 cannot prompt on a passive read -- silently evaporated. The refusal keys on the EFFECTIVE tier so a governed min_level floor does not false-refuse, and a host with no sandbox backend fails closed without consulting any mutable policy value, since both the opt-in and the floor can move between the preflight and the spawn. A gate refusal is non-retryable end to end. AcpToolGateUnroutable documents itself so but subclasses AcpError, so ensure_ready's transport ladder used to respawn the adapter once before reaching the identical refusal; it now has its own handler ahead of that ladder. And _sandbox_preflight translates the leaf module's plain ToolGateUnroutable into it at the boundary, the way the session-routing path already did -- raised raw it was neither an AcpError nor the type that handler names, so a sandbox-floor refusal escaped ensure_ready uncaught and skipped the cleanup every other refusal path runs. Known gap, documented rather than closed: ACP v1 offers no way to require a prompt for a passive read, so an adapter can still read its own token. That is inherent to running these harnesses at all -- kiro-cli is handed its own model credential in an environment nothing gates, and Claude Code already ships on this baseline with no credential mask at all -- and closing it needs upstream support rather than a change here.
) Codex could be spelled by the core but not chosen. The vocabulary, the spawn path and the adapter resolution landed in #7813; what was missing was everything that decides whether offering the switch is honest. Nothing established how a harness is made to ask before it runs a tool, and backend_install.py had no probe that could explain a failed session. acp_tool_gate resolves one routing verdict per harness, so the refusal message, a doctor row and any later surface cannot disagree, and it REFUSES a session whose tool calls would bypass HookManager.on_tool_call. For Codex the client verifies that session/new advertised mode=read-only and applies it after session/new|load, before any prompt can run. Not advertised gives INDETERMINATE, an observed write rejection gives BYPASSED, and both refuse identically: a guarantee that lapses when evidence is missing is not a guarantee. There is no local opt-out, because a config bool could undercut a governance ceiling set above the operator. It also puts both ACP adapter OAuth token stores -- ~/.codex/auth.json and ~/.claude/.credentials.json -- on the sensitive-home read-gate floor. Neither was classified before, so the agent's own fs_read could lift either one. Kiro Crew never reads them and only ever checks that one exists so it can name the right sign-in command, so nothing legitimate loses access, and only the token leaf is classified -- the adapter's readable siblings stay readable. The credential mask handed to an enforced adapter's child is DERIVED from security.sensitive_home_dirs() minus that harness's own leaf, re-anchored under every override root and under both home spellings, so a leaf added to the floor is covered with no edit in the ACP layer. The macOS profile emits both a subpath and a literal deny for each entry, because most of that list is plain files and a subpath rule over a non-directory was asserted in three comments here and contradicted in a fourth, with no test exercising sandbox-exec either way. Selecting Codex with agent.sandbox="off" now refuses to start. wrap_argv returns from its off branch before it applies extra_hidden_dirs, so the mask -- Codex's only compensating control, since ACP v1 cannot prompt on a passive read -- silently evaporated. The refusal keys on the EFFECTIVE tier so a governed min_level floor does not false-refuse, and a host with no sandbox backend fails closed without consulting any mutable policy value, since both the opt-in and the floor can move between the preflight and the spawn. A gate refusal is non-retryable end to end. AcpToolGateUnroutable documents itself so but subclasses AcpError, so ensure_ready's transport ladder used to respawn the adapter once before reaching the identical refusal; it now has its own handler ahead of that ladder. And _sandbox_preflight translates the leaf module's plain ToolGateUnroutable into it at the boundary, the way the session-routing path already did -- raised raw it was neither an AcpError nor the type that handler names, so a sandbox-floor refusal escaped ensure_ready uncaught and skipped the cleanup every other refusal path runs. Known gap, documented rather than closed: ACP v1 offers no way to require a prompt for a passive read, so an adapter can still read its own token. That is inherent to running these harnesses at all -- kiro-cli is handed its own model credential in an environment nothing gates, and Claude Code already ships on this baseline with no credential mask at all -- and closing it needs upstream support rather than a change here. Co-authored-by: zejiangg <zejiangg@amazon.com>
Problem / Motivation
Two gaps, one in the code and one in the docs.
Code. The core can serve a Codex session over ACP but has no vocabulary for it: no id, no provider label, no policy name, and no adapter resolution. Separately, the dashboard's agent-backend control derived its rows from a hard-coded
[KIRO, CLAUDE, KAS]filtered by the live schema. Filtering a literal narrows correctly but can never widen, so an agent an edition registered throughregister_selectable_backendwas selectable on the wire, valid toPATCH, present in the probe payload — and invisible in the only control that sets it.Docs.
harness-parity.mdsays what an added harness may not do to the Kiro path. Nothing said what it must do to land at all, so each onboarding rediscovered the order, and rediscovered it from KAS and Claude Code in retrospect. That is the wrong direction: the two harnesses that predate the invariants cannot show which stage a third one gets stuck at.Why it matters
Without the vocabulary, every consumer that wants to name Codex has to spell a bare literal, which
harness-parityH5 forbids and the added-line gate rejects. Without the policy-name mapping a governance author cannot deny the id before an edition registers it, which is the wrong order for a deny rule.The frontend defect is the sharper one: it silently contradicted the module's own promise that "a build that ships another agent lights it up here with no frontend change". An edition could register a backend and have no way for an operator to pick it.
What changed (motivation → approach → change)
Vocabulary, in the leaf.
ACP_BACKEND_CODEX, membership inACP_BACKENDS_KNOWN,PROVIDER_LABEL_CODEX, and aPOLICY_ID_BY_BACKENDentry. These live inacp_backends.py, which imports no ACP, so a consumer can name the constant without crossing the SDK boundary gate.Capability decisions, all eight. Codex opts into the model and effort tuning channels and out of the other six. Three of the eight sets are new here —
ACP_BACKENDS_MODEL_VIA_CONFIG_OPTION,ACP_BACKENDS_EFFORT_VIA_CONFIG_OPTION,ACP_BACKENDS_KIRO_SLASH_COMMANDS— one per channel rather than one "tuning" set, because a harness can implement one and not another.Spawn path. The
codexCLI does not serve ACP (it readsacpas a prompt), socodex-acpis the transport, not an optimization: binary and npm-package constants, a hoisted-dependency marker so a broken adapter fails before the child is spawned rather than at ESM import time, an explicitCODEX_ACP_BINoverride, and project-localnode_modulesresolution ahead of global/PATH._vendored_claude_acp_rootsbecomes_vendored_acp_roots— harness-neutral root discovery shared by both resolvers, with each joining its own package path.Handshake.
PROTOCOL_VERSION_CODEXas its own literal, at the same number as Claude's. Per H10 that is not duplication: it makes a future divergence a one-line edit instead of a silent downgrade of whichever harness moved first.Deliberately dormant. Codex is absent from
BASELINE_SELECTABLE_BACKENDS, named inNOT_SHIPPED_SELECTABLEwith its reason:backend_install.pyhas no_probe_codex, so a build offering the switch could not tell an operator what was missing when the session failed to start. One named stage, not a re-litigation.Frontend.
candidatesbecomes a union of server answers — schema enum ∪ probe payload ∪ savedcurrent— withNAMEDunioned in as a floor rather than a ceiling. As a ceiling it capped the panel at three ids forever; as a floor it only guarantees the core agents still have rows while both queries are in flight, since hiding a row on absent information is the same mistake as disabling one. Sorted (KIRO first, then bypolicy_id) so set-iteration order cannot reshuffle the control between renders.New doc.
docs/system-specs/modules/harness-onboarding.mdnames the seven stages a harness walks, written forward from this work so the gaps surface as gaps, and closes with the Codex seam as a worked example that stops at stage 5.One doc fix.
harness-parity.md's intro correctly describes Codex as a named exception inNOT_SHIPPED_SELECTABLE, but point 4 of "Adding or changing an invariant" still assertedACP_BACKENDS_KNOWNandBASELINE_SELECTABLE_BACKENDSwere equal and that any id outside the baseline fails the narrowing check. They are no longer equal, and the check now accepts a named exception. Both statements were in the same file.Tests
test_harness_parity.py(+156):test_codex_is_known_but_not_shipped_selectable,test_codex_carries_its_own_provider_label,test_codex_spawn_keeps_its_own_branch, plus opt-in coverage for each new channel set (test_model_switch_channel_is_opt_in,test_effort_channel_is_opt_in,test_only_overlay_readers_are_written_to).test_agent_backend_editable.py:NOT_SHIPPED_SELECTABLEturns the narrowing assertion into a named allowlist, so a plainbaseline != knownstill fails — an id may sit outside the baseline only by being named there.test_acp_client.py/test_acp_provider.py/test_acp_backend_kas.py/test_agent_sdk_backend_install.py: adapter resolution, protocol literal, and probe coverage follow the renamed helper.AgentBackendTab.test.tsx(+43): pins that a server-registered id this frontend has no translated name for still renders under itspolicy_id, and that theNAMEDfloor keeps rows during loading..github/black-baseline.txtshrinks by one entry (test_acp_backend_kas.pyis now black-clean).734 backend tests pass across the seven affected files.
Manual verification
Gates run locally on this base: agent-sdk-boundary, harness-parity, docs-lint, flake8, isort, mypy, subprocess-encoding, sync-io-in-async, brand, changelog-history, focus-cue, testpaths-coverage, vendor-manifest — all pass.
tsc -bclean.Not run locally, disclosed rather than claimed: the frontend
vitestsuite. This host has Node 20.20.2; the repo requires Node ≥22 (.nvmrcpins 24) and vitest 4 passes--no-experimental-webstorage, which Node 20 rejects. Installing Node 24 failed local gpg verification and I did not bypass that check. CI is therefore the first run ofAgentBackendTab.test.tsx.Screenshots / video
Why no screenshot: on any build CI can produce, the rendered control is unchanged —
candidatesresolves to the same three ids, because Codex is absent from the schema enum and has nobackend_installprobe to appear in the probe payload. The change widens what an edition build can display and guarantees the core rows exist during the loading window; neither state is reachable on a public build, and the loading case is transient.Related Issues
no linked issue: incremental step in the
agent_sdkboundary work, tracked by the RFC atdocs/request-for-change/rfc-crew-agent-sdk-boundary.mdrather than an issue.Checklist
On the two commits: the first is authored by Vamil Gandhi and carries the Codex seam; the second is the docs work. They are deliberately not squashed, so the spawn-path work keeps its author rather than being absorbed under mine. Vamil's commit is taken from PR #6910.