Skip to content

test(session-control): pin containment_snapshot keys to authorize_target refusals - #7099

Merged
bolichen97 merged 1 commit into
kirodotdev:mainfrom
raiyanlabs:test/containment-constraint-parity-5994
Aug 30, 2026
Merged

test(session-control): pin containment_snapshot keys to authorize_target refusals#7099
bolichen97 merged 1 commit into
kirodotdev:mainfrom
raiyanlabs:test/containment-constraint-parity-5994

Conversation

@raiyanlabs

Copy link
Copy Markdown

Problem / Motivation

src/kiro_crew/dashboard/session_control.py carries two hand-maintained spellings of the same per-slot containment constraint set:

Nothing ties the two together. A refusal added to authorize_target does not appear in the snapshot by construction, so it is enforced at enqueue and never re-asserted at delivery.

Why it matters

Drift fails open. An eighth refusal added to authorize_target alone reopens the enqueue→drain TOCTOU that #5978 closed — for that one constraint, with nothing red. A queued prompt rides past a boundary the enqueue side enforces. That is the failure class #5911 was about.

The file's existing coverage does not catch it. test_human_typed_enqueue_stamps_admission_snapshot asserts snap == {...} with the seven keys spelled out, which pins the snapshot's shape but is not derived from authorize_target. So it is:

  • red when someone adds a key to containment_snapshot (the safe direction), and
  • silent when someone adds a refusal to authorize_target (the dangerous one).

What changed (motivation → approach → change)

Goal: make divergence between the two constraint sets fail CI instead of failing open.

Approach. The issue offers two shapes and states either is acceptable, with the first preferred. This takes option 2 — the parity test. Option 1 (extracting a shared predicate table both functions consume) is a structural refactor of the admission path through a deny-by-default, SEL-audited chokepoint; it is the better end state but a wider change than closing this axis requires, and it does not need to block the pin.

Change. A new section in test/test_queue_drain_revalidation.py, alongside the drain-re-validation tests it belongs with:

  • The refusal set is read from authorize_target's source by AST, not hand-listed. A hand-listed copy would be a third spelling of the same set, free to drift from the other two — reproducing the exact failure the test exists to pin. _authorize_target_refusal_codes() walks inspect.getsource(sc.authorize_target) for deny(..., code) literals. One deny re-raises a resolution failure with exc.code rather than a literal; it carries no new constraint, so a non-literal code is skipped rather than failing the parse.

  • Two classification tables carry the semantics the parse cannot infer:

    • _TARGET_CONTAINMENT_REFUSALS — the 7 target-side refusals, each mapped to the snapshot key that re-asserts it (linked_session_targetlinked, mirrored_targetmirrored, crew_mode_targetcrew, ephemeral_targetephemeral, app_scoped_targetapp, unattended_targetunattended, workspace_mismatchworkspace).
    • _NON_CONTAINMENT_REFUSALS — the 10 that need no drain-time check, with the reason inline: the 7 caller-side refusals are a property of who is asking rather than of the target slot, and session_control_disabled / target_not_found / self_target are not containment at all.

    Naming alone cannot do this: self_target and target_not_found end in or contain target but are not containment constraints, while workspace_mismatch is one and does not match the pattern. Requiring an explicit classification is the point — a new refusal cannot be added without someone deciding which side it falls on.

  • A header comment tells the next person exactly what to do when they add a refusal, so the test's failure message and the code's guidance agree.

Tests

Five tests, all in test/test_queue_drain_revalidation.py. Each locks in one direction of drift:

Test What it catches
test_the_parse_finds_the_refusals_it_is_asked_to_pin Guards the guard — if refusals stop being spelled deny(..., "code"), the extraction silently returns less and every test below passes while pinning nothing
test_every_refusal_is_classified The fail-open case. A new refusal in authorize_target that nobody classified
test_every_classified_refusal_still_exists A removed or renamed refusal leaving a stale mapping that claims coverage
test_every_containment_refusal_has_a_snapshot_key A containment refusal with no snapshot key — enforced at admission, unchecked at drain
test_the_snapshot_carries_no_unmapped_constraint The reverse drift: a snapshot key with no refusal behind it

mirror_identity and mirror_unverified are excluded via _SNAPSHOT_NON_CONSTRAINT_KEYS: both are set by the mirror probe to tell the drain how to compare mirrored, and neither has its own refusal.

Local results: 32 passed, 0 failed (27 before this change). isort, flake8, and black clean on the CI scope. mypy is unaffected — CI runs it against src/kiro_crew/ only.

Manual verification

The tests were confirmed to fail for the reasons claimed, not to pass vacuously. Each of the four drift directions was injected into session_control.py, the suite run, and the change reverted:

Injected drift Result
New refusal quarantined_target in authorize_target, unclassified — the fail-open case 1 failed. Message named the offending code and both remedies
Same refusal classified as containment, snapshot key not added 3 failed
"crew" deleted from containment_snapshot 1 failed
crew_mode_target renamed in authorize_target, mapping left stale 2 failed

git diff after the sequence confirmed session_control.py byte-identical to HEAD; only the test file is modified in this PR.

Related Issues

Closes #5994
Refs #5978, #5911

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable) — N/A, the classification tables and header comment are the documentation of this invariant, and they live with the test that enforces it
  • No secrets, credentials, or internal references in the diff

…get refusals

`authorize_target` refuses admission from an inline constraint set;
`containment_snapshot` (kirodotdev#5978) re-derives the same predicates so the drain can
re-assert them. Two hand-maintained spellings of one set, with nothing tying
them together.

Drift fails OPEN. A refusal added to `authorize_target` alone is enforced at
enqueue and never re-checked at delivery, reopening the enqueue->drain window
kirodotdev#5978 closed for that constraint only, with nothing red. The existing
`assert snap == {...}` pins the snapshot's shape but is not derived from
`authorize_target`, so it catches a new snapshot key and misses a new refusal --
it is red in the safe direction and silent in the dangerous one.

Takes option 2 from the issue: a parity test rather than the structural refactor
of the admission path, which is a wider change than a test needs to be.

The refusal set is read from `authorize_target`'s source via AST rather than
hand-listed. A hand-listed copy would be a third spelling of the same set, free
to drift from the other two -- the failure this pins, reproduced inside the test
that pins it.

Two classification tables carry the mapping, so a new refusal must be declared
either a containment constraint (with the snapshot key that re-asserts it) or
explicitly not one (with a reason). Five tests: the parse finds what it claims
to, every refusal is classified, no mapping outlives its refusal, every
containment refusal has a snapshot key, and no snapshot key lacks a refusal.

Verified by injecting each of the four drift directions and confirming the suite
goes red for each, then reverting: an unclassified new refusal (the fail-open
case), a containment refusal with no snapshot key, a deleted snapshot key, and a
renamed refusal leaving a stale mapping.

Test-only; no source file is touched.

Refs kirodotdev#5994, kirodotdev#5978
@raiyanlabs
raiyanlabs requested a review from a team as a code owner August 30, 2026 19:44
@raiyanlabs
raiyanlabs requested a review from patrigao August 30, 2026 19:44
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — ✅ no blocking findings

Reviewed 8424b9ed85fcae7241705368b7a73f1faca0246a via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 8424b9e

@github-actions

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — ✅ PASS

Design-level review of 8424b9ed85fcae7241705368b7a73f1faca0246a via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All claims verified against the base tree: 17 literal deny codes in authorize_target (7 target-containment + 10 non-containment, exactly matching the tables), the one non-literal exc.code re-raise is _resolve_slot's resolution failures (409 ambiguity, not containment), snapshot keys and probe extras match the mapping, and the guard test's len >= 17 plus the stale-mapping test make both the vacuous-parse and refactor-away directions red. The chosen shape (parity pin over the shared-predicate refactor) is the issue's sanctioned option 2, test-only, and fully reversible.

Design-Verdict: PASS

A self-guarding, source-derived parity pin that makes the fail-open drift red; test-only, reversible, and every claimed drift direction verifiably fails.

[DESIGN-REVIEWED] 8424b9e

@github-actions

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed 8424b9ed85fcae7241705368b7a73f1faca0246a via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 8424b9e

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 30, 2026
@bolichen97
bolichen97 enabled auto-merge (squash) August 30, 2026 21:03

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tier 1 auto-approve: test (1 file). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: test-only parity test pinning authorize_target refusal codes to containment_snapshot keys via AST parse. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.

@bolichen97
bolichen97 merged commit fcccd28 into kirodotdev:main Aug 30, 2026
70 checks passed
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 30, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tier 1 auto-approve: test (1 file). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: test-only — adds AST-parity guards pinning authorize_target refusals to containment_snapshot keys (#5994), no production code touched. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tier 1 auto-approve: test (1 file). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: test-only addition to test/test_queue_drain_revalidation.py, no production file touched; it parses authorize_target's own source for literal deny(..., code) refusals and asserts each is either mapped to a containment_snapshot key or classified as non-containment, so a refusal added at admission but not re-asserted at drain fails CI instead of failing open. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tier 1 auto-approve: test (1 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: test-file-only additive parity tests pinning authorize_target refusal codes to containment_snapshot keys. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Converge authorize_target and containment_snapshot onto one spelling of the containment constraint set

2 participants