refactor(agents): route all KAS derives through the shared wrapper - #7586
Conversation
…sions Fixes #7513 PR #7238 introduced derived_agent_permissions as the shared spelling of derive-plus-fallback but migrated only one of three call sites, so the {"rules": []} fallback existed in three copies and a divergence between them would have been invisible to any behavioural test. Migrate the two remaining inline sites (_seed_kas_permissions and the goal-conductor installer) onto the wrapper, following the pipeline-conductor installer's in-tree shape. Both substitutions are behaviour-preserving: the inline fallbacks were byte-identical to what the wrapper does. One real difference surfaced by the migration (reported, not papered over): the wrapper's ``allowed_tools: list`` annotation was narrower than the derive it wraps, whose parameter is ``Any`` with fail-closed validation (a non-list, including the absent-key None a caller reads off a config dict, yields no policy). Site A passes exactly that possibly-absent value, so mypy rejected the pure substitution. Widened the wrapper's annotation to ``object`` to match the domain of the function it wraps; runtime behaviour is untouched. The migration removes agent.py's last two direct kiro_crew.acp imports, so the shrink-only agent-sdk boundary baseline prunes its agent.py entry (2 -> 0, via --update-baseline; the gate itself mandates the prune for a touched file whose count shrank). A new structural guard (test_derived_agent_permissions_consumers.py) pins the derive to the boundary: no src/ module outside kiro_crew/acp/ and the agent_sdk driver may import allowed_tools_to_permissions or respell the inline fallback. Born red against unmodified main naming agent.py; green after the migration. The two pipeline-conductor probe scripts read three on-disk formats the package itself writes -- the transcript JSONL entry shape, the dashboard_ filename prefix, and the usage-shard token-row schema -- and their existing tests use hand-authored fixtures, which cannot detect the owning writer changing its format (drift would misclassify sessions while every test stayed green; the conductor's reclaim decision runs off that classification, so the live failure is a running session read as GONE and double-dispatched). Add round-trip contract tests (test_pipeline_conductor_probe_roundtrip.py) that drive the REAL writers (ConversationLog.append, _history_key_for, persist_token_record) and let the scripts classify the output, keeping the scripts' subprocess-free design. Mutation-verified: each drifted writer reds exactly the new round-trip test while every fixture test stays green.
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Mechanical dedup onto an existing, gate-enforced seam plus round-trip tests that pin real writer output — right shape, no new surface, fully reversible. [DESIGN-REVIEWED] 313c001 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe refactor is behavior-preserving: No findings. [OPUS-REVIEWED] 313c001 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: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All evidence checked. Composing the review now. First-Principles-Verdict: PASS Completes the migration #7238 left two-thirds undone and closes the fixture-blindness gap behind it; every item traces to a named defect and counted consumers. What this change shipsIntent: finish #7513 — make the KAS derive-plus-fallback exist in exactly one place, and make the conductor probes' readers red when the real writers drift. This is a FIX.
Counts I ran: [FIRST-PRINCIPLES-REVIEWED] 313c001 |
…y gate Review-driven (First Principles, advisory): after this PR prunes agent.py from the shrink-only baseline, scripts/check_agent_sdk_boundary.py already reds any re-grown kiro_crew.acp.kas_permissions import in application code -- including dynamic-import spellings the deleted test could not see. Two scanners for one symbol drift, and the weaker one loses. The textual fallback-spelling guard stays: that half nothing else covers. Empirically re-verified: restoring one inline site reds BOTH the surviving guard and the boundary gate.
Fixes #7513
Symptom
PR #7238 introduced
derived_agent_permissions(insrc/kiro_crew/agent_sdk/drivers/acp.py) as the shared spelling of the KAS derive-plus-fallback, but migrated only one of the three call sites. The other two still spelled the same derive inline, so the{"rules": []}fallback existed in three byte-identical copies — a future divergence between them would have been invisible to any behavioural test. Separately, the two pipeline-conductor probe scripts re-implement readers for three on-disk formats this package itself writes, and their tests used hand-authored fixtures, which cannot detect the owning writer changing its format. The conductor's reclaim decision runs off that classification, so the live failure mode is a running session read as GONE and its work item dispatched twice.Note: every line number in the issue body had drifted; sites below are located by symbol at main
1d705a03f. The issue calls site A_write_agent_permissions— the enclosing function on main is_seed_kas_permissions(its corrected location matches the issue's described site).Root cause
_seed_kas_permissions(src/kiro_crew/agent.py, import was at:3285, fallback at:3292) — inlineallowed_tools_to_permissions+{"rules": []}fallback._install_conductor_agent, the goal-conductor installer (import was at:5153, fallback at:5160) — same inline spelling.fleet_probe.py,credit_spend.py) read the session-transcript JSONL shape, thedashboard_session-filename prefix, and the usage-shard token-row schema with re-implemented readers; only hand-authored fixtures covered them.Fix
Item 1 (mechanical migration). Replaced both inline derive-plus-fallback sites with a call to the existing
derived_agent_permissionswrapper, following the in-tree precedent already at_install_pipeline_conductor_agent(agent.py:5370, the site #7238 migrated). Pure substitution at both sites — argument values and fallback are identical.One real difference surfaced by the migration (reported, not papered over): the wrapper's
allowed_tools: listannotation was narrower than the derive it wraps, whose parameter isAnywith fail-closed validation (a non-list — including the absent-keyNonesite A reads viaconfig.get("allowedTools")— yields no policy, and the wrapper then supplies{"rules": []}). mypy rejected the pure substitution at site A on that annotation alone. Widened the wrapper's annotation toobjectto match the domain of the function it wraps; runtime behaviour is untouched (the derive owns validation, per itsTestUnclassifiableEntriesFailClosed/ non-list tests).Boundary baseline. The migration removes
agent.py's last two directkiro_crew.acpimports — the boundary checker counted exactly 2 edges foragent.py, and both were these imports. The count drops to a provable floor of 0, and the shrink-only gate itself mandates the prune for a touched file whose count shrank (--update-baseline, which only lowers/deletes). Theagent.pyline is therefore removed from.github/agent-sdk-boundary-baseline.txt. No open PR modifies that entry (PR #7325 adds a different file's line; disjoint hunks).Item 2 (round-trip contract tests). New
test/test_pipeline_conductor_probe_roundtrip.pydrives the REAL writer for each format and lets the script (loaded viaload_skill_script) classify the output:ConversationLog.appendinto the same<data home>/sessionsdirectory the probe derives;dashboard_filename prefix — the slot key through the real derivation chain (chat_utils._history_key_for→history._safe_keyviaConversationLog), probe given only the RAW slot key must not read GONE;persist_token_record→_build_token_record→_write_token_recordwith a realTurnUsage, summed bycredit_spend; includes the rows-are-turns contract and the negative (unmetered) half.All three real writers were drivable in-process, so no format needed a hand-rolled substitute. The scripts themselves are untouched — their subprocess-free design survives.
Item-1 guard. New
test/test_derived_agent_permissions_consumers.py: a textual guard that the inline fallback spelling does not re-grow outside the boundary (kiro_crew/acp/and theagent_sdkdriver). The IMPORT half of the invariant is held by the live CI boundary gate: after this PR's baseline prune,scripts/check_agent_sdk_boundary.pyreds any re-grownkiro_crew.acp.kas_permissionsimport in application code, including dynamic-import spellings a second scanner would miss.Verification
- Red before green (item 1): the guard was born red against unmodified main, naming
- Mutant (a), re-verified after the review-driven guard subtraction: restoring one inline fallback in
- Red against drifted writers (item 2): each round-trip test was mutation-verified against a deliberately drifted writer, and in every case the sibling hand-fixture tests STAYED GREEN — demonstrating the exact gap being closed:
- transcript
- token row
- Mutants killed (both directions recorded):
- (a) restore one inline fallback at site A → guard tests red; restore fix → green.
- (b) wrapper fallback
- Focused suites:
- Zero-regression proof: full backend suite on this branch vs a
No frontend surface is touched; there is no visual delta to screenshot. Evidence is the test/gate output above.src/kiro_crew/agent.pyas the only offender (after exemptingacp/kas_agents.py, package-internal use inside the boundary). Green after the migration.agent.pyreds BOTH the surviving textual guard ANDscripts/check_agent_sdk_boundary.py(agent.py:3291flagged; the baseline no longer lists the file). Restore → both green.contentfield renamed inConversationLog.append→ round-trip red,TestFleetProbefixtures green;_history_key_forprefix changeddashboard:→dash:→ round-trip red (false GONE), fixtures green;_typerenamed in_build_token_record→ round-trip red,TestCreditSpendfixtures green.{"rules": []}→None→ behavioural tests red at BOTH migrated sites (test_kas_permissions.py::TestTheDiskWriterfor site A,test_conductor_agent.py::TestConductorInstaller::test_a_fully_governed_host_still_emits_the_permissions_keyfor site B) — the divergence the issue said was invisible is now visible at every consumer at once; restore → green.test_derived_agent_permissions_consumers.py,test_pipeline_conductor_probe_roundtrip.py,test_kas_permissions.py,test_conductor_agent.py,test_pipeline_conductor_agent.py— 145/145 pass.mypy src/kiro_crew: no issues in 1237 files. black/isort/flake8 gates clean.scripts/local-gate.py --base origin/main: backend (full)-n auto --dist loadgroup— 225 failed, 78615 passed, 376 skipped, 5 xfailed, 2 errors. The failure set is this host's known environmental baseline (sandbox userns EPERM, AF_UNIX path-length, host-budget/xdist assumptions), proven below.git worktreeatorigin/main(1d705a03f), sorted failing-test id sets diffed both directions: branch 227 failing ids vs origin/main worktree 227 failing ids;comm -23= 0 lines,comm -13= 0 lines — the sets are byte-identical in both directions (main run: 225 failed, 78607 passed, 2 errors in 510s).Review-driven changes
test_no_application_code_imports_the_raw_derive. The lane is right that after the baseline prune the CI boundary gate (ci.yml,scripts/check_agent_sdk_boundary.py) permanently holds the import half of the invariant — AST-scanning the same tree, catching dynamic-import spellings the deleted test could not, and reddingagent.pyon any re-grown import. Two scanners for one symbol drift, and the weaker one loses. The textual fallback-spelling guard (the half nothing else covers) stays and still kills mutant (a); the gate's coverage of the import half was verified empirically, not assumed (see mutant (a) above). This also resolves the lane's Watch item.