fix: make session_pid importable, and cover Codex in the host contract - #8791
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root causes fixed the hard way — cycle edge deleted rather than exempted, and the doc gate is honestly scoped, self-checked, and mutation-tested against its own bypasses. Suggestions
[DESIGN-REVIEWED] 3a122c2 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsVerification complete. All grep hits for No findings. [OPUS-REVIEWED] 3a122c2 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All verification done — Codex is in First-Principles-Verdict: PASS Every item traces to a reported defect — an ImportError, a backend shipped with no contract column — and each fix lands at the cause, not the symptom. What this change shipsIntent: make the host contract describe every backend a public build can select, and make
WatchItem 7: the RFC sections beyond the two fixes (PR 3 rescope, §12.5 mirrors, PR 6 gating) are a third job riding in a fix PR. Each corrects a falsifiable stale claim with named harm (a contributor executing PR 3 or PR 6 against premises the tree no longer satisfies), so they earn existence — but they are separable, and AGENTS.md's one-logical-change rule is tight against this shape. Item 9: the [FIRST-PRINCIPLES-REVIEWED] 3a122c2 |
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: |
78ecd26 to
035ced8
Compare
The finding holds exactly as written.
The fix is One thing worth recording, because the obvious pin does not work. A test that diffs Verified independently on |
035ced8 to
4de291b
Compare
The finding holds, and it is the third instance of one overclaim rather than a new
The docstring now separates projection from arrival, which is the distinction that Worth recording for the ledger: the identical claim was corrected in |
4de291b to
8e47c47
Compare
The finding holds. This is the fourth site of one inference, handed out one site per
The invalid step is always the same word: nothing is projected so nothing is Every site of that claim, and its state as of
The three remaining Docstring and rationale-string only; no behaviour change. The 34 Python-side ratchet |
UX Review (Fable 5) — ⏭️ skippedRevision |
|
The Rebasing onto
1 failed / 22,235 passed on the shard. This PR touches nothing under Attribution:
Not fixing it here: Everything else on |
8822007 to
0e26d51
Compare
0e26d51 to
5096507
Compare
|
Holds. The sentence was true before the rebase and false after it:
Holds, and is the same one-line cleanup already applied to |
session_pid.py imported providers.base at module scope for one parameter annotation, closing session_pid -> providers.base -> acp.types -> acp/__init__ -> acp.runtime -> session_pid. That was fatal, not cosmetic: importing kiro_crew.session_pid as the first kiro_crew module raised ImportError on _track_pid, so the module was only ever importable because something else had imported the ACP package first. Deferring the import under TYPE_CHECKING closes the cycle, and the boundary gate refused it -- correctly, since it counts a type-only import as boundary knowledge and deliberately offers no opt-out marker. The refusal exposed a better answer: _sync_kill_provider reads _client, _proc and _active_proc through getattr, and those are alternative private layouts rather than one shared interface, so the provider ABC never described that parameter and a Protocol would not either. It is object now, the edge is deleted rather than exempted, and the baseline shrinks 107 -> 106. session_compaction.py's now-needless LLMProvider = Any goes with it, and test_agent_lifecycle_cycle.py pins both properties in a fresh interpreter (-I -B, so the read-only check writes no bytecode into the tree it inspects). The rest is the documentation half of the same audit. The host contract's nine buckets described three backends. Codex landed in ACP_BACKENDS_KNOWN and BASELINE_SELECTABLE_BACKENDS -- so a public build serves sessions on it -- with a column in none of them. That doc's own checklist says "Silence is not an answer" and nothing enforced it, so the omission shipped and the documented section 5 defect recurred: _codex_session_mcp_servers() returns [], so nothing is projected onto a codex session. Add the fourth column, all 47 cells derived from code (cited by module and symbol, never line number, matching the doc's own rule), with unmeasured rows saying so rather than guessing. Replace the sentence with a ratchet: test_agent_host_contract_parity.py resolves every bucket table's column labels through the Column-meaning table and fails when the set is not exactly ACP_BACKENDS_KNOWN. Its scope is narrow on purpose and the doc says so -- it asserts a column exists, not that a cell is true. Three attacks on a first draft of that ratchet shaped what it checks. A decoy backend-shaped table placed before the real one passed, because the parser took the first table and stopped looking. A continuation table further down a bucket could omit a backend unseen. And a fabricated ACP_BACKEND_FAKE = "" row could impersonate kiro-cli, because only the quoted value was read and a set erases the alias. So the contract table must now be the first table under its heading with a matching rule row, a second backend-shaped table in the same bucket is rejected rather than ignored, every Column-meaning row's CONSTANT NAME is resolved against acp_backends and its documented value checked against the real one, and bucket numbering must be consecutive from 1. Correct the two remaining places that inferred "nothing is mounted" from "nothing is projected" -- the _codex_session_mcp_servers docstring and the NO_MIRROR rationale string. The shared MCP gateway's pooled broker stubs are appended for every backend alike, so the gateway, not the harness, decides whether Crew's own control plane arrives. Re-audit the RFC to v4 against 73d60a8: PR 1 marked landed, the stale 68-edges/42-files census replaced by the baseline it contradicted, section 2.3 rewritten because the capability vocabulary left acp/types.py for acp_backends.py and acp_tool_gate.py, a new section 2.7 recording Codex as the third cost measurement, PR 3 reframed from building the capability mechanism to consolidating the one that exists, PR 4's wrong cycle criterion replaced, and PR 6 blocked on where providers/mirrors/ lives. The v4 edits cite modules and symbols, not line numbers, so the docs-lint line-ref baseline only shrinks.
5096507 to
3a122c2
Compare
|
Holds. Both stubs stay (each subscripts
Holds. Number dropped ("sibling leaves"); the test module's docstring had the same count and is fixed the same way.
Holds. §2.4 now names all three deleted stubs and both surviving ones, with the surviving ones' reason stated as the runtime subscript. Comment-and-prose only; no behaviour change. Boundary baseline unchanged at 106, 34 Python-side gates and 290 tests green. |
Problem / Motivation
Two defects, both found by auditing what the recent provider work actually left
behind.
The host contract does not describe Codex.
docs/system-specs/features/agent-host-contract.mdis the document that tells a provider author what a backend must supply besides
speaking ACP, in nine buckets. Codex landed in
ACP_BACKENDS_KNOWNand inBASELINE_SELECTABLE_BACKENDS, so a plain public build serves sessions on it —with a column in none of the nine buckets. The doc's own New-provider checklist
says "Silence is not an answer", and nothing enforced that, so the omission
shipped. The §5 failure the checklist already described then recurred unchanged
on it:
_codex_session_mcp_servers()returns[], so nothing is projected ontoa codex session. Two places in code still infer "nothing is mounted" from
"nothing is projected" — the
_codex_session_mcp_serversdocstring and theNO_MIRRORrationale — and are corrected here. (Two sibling comments andproviders.mdcarried the older "dormant seam no build offers" claim;mainhas since rewritten all three (#8905), so they dropped out of this diff on
rebase.)
session_pidcould not be imported on its own.import kiro_crew.session_pidas the first
kiro_crewmodule raisedImportError: cannot import name '_track_pid' from partially initialized module.session_pid.pyimportedkiro_crew.providers.baseat module scope for a singleparameter annotation, closing
session_pid → providers.base → acp.types → acp/__init__ → acp.runtime → session_pid.And the RFC that governs this work,
rfc-crew-agent-sdk-boundary.md, still said"nothing proposed here has shipped" while three of its six PRs rested on premises
that are no longer true.
Why it matters
The point of the boundary work is that onboarding a provider should be writing
one driver, not editing the tree. Both defects cut against that directly.
An undocumented column is the expensive kind of missing: the next author
re-derives from code what the previous author knew, and the same class of defect
ships again — which is exactly what happened between CC's empty MCP array and
Codex's.
The import cycle was not cosmetic. It is why four other leaves carry
LLMProvider = Anyruntime stubs and whysession_pidreachesacp.clientthrough function-local imports; and the RFC named the wrong cycle
(
session_pid↔worker_pool, which does not exist), so its PR 4 exit criterionwould have been satisfied without fixing anything.
A stale RFC is worse than no RFC: PR 3 was scoped to build a capability mechanism
that now exists, and PR 6 was scoped to delete a package that has since grown a
new load-bearing layer inside it.
What changed (motivation → approach → change)
Codex gets a column, and a gate replaces the sentence. A stronger "silence is
not an answer" had already been tried and had already failed, so the fix is a
ratchet.
test_agent_host_contract_parity.pyparses the header row of everybucket table, resolves each column label through the Column-meaning table to a
backend constant, and fails when the resolved set is not exactly
ACP_BACKENDS_KNOWN. Its scope is deliberately narrow and the doc says so: itasserts a column exists, not that a cell is true. A column of "unknown" passes.
That is the honest limit of a text gate — it makes the omission visible and a
reviewer makes it answered. All 47 Codex cells were derived from code, and rows
that are genuinely unmeasured say so rather than guessing.
Three attacks on a first draft of that ratchet shaped what it checks, and all
three passed against it. A decoy backend-shaped table placed before the real one,
because the parser took the first table and stopped looking. A continuation table
further down a bucket omitting a backend. And a fabricated
ACP_BACKEND_FAKE = ""row impersonating kiro-cli, which worked because only thequoted value was read and a set erases the alias. So the contract table must now
be the first table under its heading with a rule row of matching width, a second
backend-shaped table in the same bucket is rejected rather than ignored, every
Column-meaning row's CONSTANT NAME is resolved against
acp_backendsand itsdocumented value checked against the real one, and bucket numbering must be
consecutive from 1 — which also stops a legitimately added tenth bucket failing
for the wrong reason.
The cycle is closed by deleting the edge, not by hiding it. Deferring the
import under
if TYPE_CHECKING:was the first attempt, andscripts/check_agent_sdk_boundary.pyrefused it. That refusal is correct andworth stating: the gate counts a type-only import as boundary knowledge by
explicit design, and it deliberately offers no opt-out marker, because "this
consumer legitimately needs the layer" is the claim the boundary exists to
refuse. Being refused exposed a better answer — the annotation was wrong to begin
with.
_sync_kill_providerreads_client,_procand_active_procthroughgetattr(..., None), none of which the provider ABC declares, soLLMProvidernever described that parameter. It is
objectnow, the import is gone rather thanexempted, and the baseline shrinks from 107 edges to 106 — recorded progress, not
an exemption.
session_compaction.py's now-needlessLLMProvider = Anygoes withit.
session_allocation.pykeeps its stub becauseProviderFactory = Callable[..., LLMProvider]is a runtime subscript — notedrather than silently left.
The RFC is re-audited to v4 against
73d60a83d: PR 1 marked landed with whatit actually shipped, the stale 68-edges/42-files census replaced by the baseline
as single source (the doc contradicted itself on that figure), §2.3 rewritten
because the capability vocabulary moved out of
acp/types.pyentirely intoacp_backends.pyplus the newacp_tool_gate.py, §2.4 rewritten against the realcycle, a new §2.7 recording Codex as the third cost measurement, PR 3 reframed
from "build the mechanism" to "consolidate it into the SDK", PR 4's wrong exit
criterion replaced, and PR 6 blocked on deciding where
providers/mirrors/lives.Codex is worth reading as a split verdict, and both halves are recorded. On
mechanism it is the counter-example the boundary work wants: no
ifchain, but aRoutingenum plus identity-keyed tables read through accessors that fail closedon an unknown id, with enforcement dispatching on the mechanism rather than the
harness. On declaration it repeats the old pattern exactly — its capability gaps
live in frontend prose behind
if (value === CODEX), nothing puts them on thewire, and a third harness with a caveat adds a third
if.Tests
test/test_agent_host_contract_parity.py(new, 3 tests) — every id inACP_BACKENDS_KNOWNhas a column in every bucket; the Column-meaning tableresolves every id against the real constants; and a structural self-check on
bucket numbering, so a renamed or dropped heading cannot make the suite pass
vacuously. Verified against seven mutations of a scratch copy of the doc: the
three bypasses above now fail, a legitimate tenth bucket passes, and a deleted
bucket, a narrowed rule row and the pre-Codex doc all fail.
test/test_agent_lifecycle_cycle.py(new, 4 tests) —import kiro_crew.session_pidsucceeds standalone, and pulls in zerokiro_crew.acp/kiro_crew.providersmodules. Both run in a fresh interpreter, because insidepytest the ACP package is already imported and the cycle cannot be observed. A
third test pins that the child measures this checkout — an editable install
pointed a first draft at a sibling worktree. The child's answer is
sentinel-prefixed so a stray write to stdout cannot read as a leaked module.
Mutation-verified against
origin/main'ssession_pid.py: 3 of the 4 fail.That pair is the RFC's PR 4 exit criterion, replacing the one that tested a cycle
which does not exist.
508 tests pass across the touched areas (
test_harness_parity,test_acp_tool_gate,test_acp_backend_credentials,test_agent_sdk_backend_install,test_pid_lifecycle,test_session_pid_*,test_session_pool, the compaction suites, and both new files).The full backend suite was also run locally — 87,785 passed, 89 failed — and every
one of those 89 is attributed rather than assumed. 88 reproduce identically on a
clean
origin/mainworktree in the same environment (this sandbox's/local/homeis owned by uid 65534, and the host has 16 cores where
test_xdist_host_budgetexpects a big one). The 89th,
test_public_repo_chip_status.py::test_force_public_to_private_transition_queues_hide_update,passes in isolation and passes as a whole file on both branches, so it is a
load-order flake under the parallel suite. None of the 15 failing files is touched
by this diff, and CI's own eight backend shards are green on this head.
Manual verification
Both halves of the cycle claim were verified by hand before the tests were
written: on unpatched
main,import kiro_crew.session_pidraises theImportErrorabove; on this branch it loads with zero agent-layer modules insys.modules.scripts/check_agent_sdk_boundary.pypasses with the baseline lowered to 106edges / 58 files, and its own
--testself-check passes (32 violation probesflagged, 13 clean probes ignored).
./scripts/docs-lint.shpasses over 261markdown files.
mypyis clean on both touched source files, as are black,flake8 and isort on every touched Python file.
Reviewed before first push by four local lanes (two models, four charters:
factual verification of the cells, adversarial attack on the ratchet, adversarial
attack on the cycle fix and baseline, and cross-document coherence). Every
finding above Low is fixed; the notable ones were the three ratchet bypasses, one
self-contradicting cell (§5 claimed the control plane is never mounted while also
saying pooled stubs can contribute — resolved by describing what the gateway
decides), and a
providers.mdcontradiction thatmainthen fixed independently.Rebase note. After this PR opened,
mainmoved the host contract todocs/system-specs/modules/, dropped everyfile:linecitation from its cells,and added a docs-lint
line-refrule with a shrink-only baseline. The Codex columnwas re-applied on top of that version (line numbers stripped to match), the RFC's
v4 citations were rewritten to modules and symbols per the doc's own rule, and the
line-ref baseline shrank by 16 entries the v4 edits had already retired.
CI's own lanes then found two more, both real and both in the new cycle test, and
both are fixed here:
Backend Lint & Type Check/ subprocess-encoding gate. The child wasspawned in text mode with no
encoding=, which decodes with the Windows ANSIcode page. It now passes
**UTF8_TEXTfromsrc/kiro_crew/subprocess_utf8.py— the documented correct case for that helper, since the child is a Python
process we spawn and whose output we control.
span=1c56ce2df458, security-class).-Iisolatesthe child but does not imply
-B, so a read-only assertion was compilingkiro_crewintosrc/**/__pycache__— writing into the checkout it existed toinspect.
-Bis now passed and pinned by a test.That pin is worth a note, because the obvious version of it does not work. A test
that diffs
.pycfilenames and mtimes around the child run passes with-Bremoved: pytest imports most of
kiro_crewduring collection, so the bytecodealready exists and a writing child adds no new file. Measured, not assumed. The
test therefore asks the child what it will do (
sys.dont_write_bytecode), whichfails exactly when the flag is dropped.
Related Issues
no linked issue: this is the re-audit that
docs/request-for-change/rfc-crew-agent-sdk-boundary.mdasks for in its ownlast-auditedfrontmatter, plus the two defects that audit surfaced. Nothing wasfiled first, so there is no issue to close.
Pattern harvest
Rule candidate:
review-promptPattern: a module-scope import needed only for a type annotation closes an import
cycle. It is invisible in review because the import looks used, and the module
stays importable as long as something else imports the cycle's other half first —
so the failure surfaces only when that module is imported first, which no test
did. Two follow-ons generalize further. First, the check added here is the
reusable one: for any module a lower layer imports back, assert it imports
standalone and drags in none of that layer. Second, an annotation that forces such
an import is worth re-reading before it is deferred — this one turned out to name
a type the function never required, and two more instances of the same shape sit
in the tree as
LLMProvider = Anyruntime stubs, one of which this PR deletes.Checklist