Skip to content

fix(chat): refuse a linked session binding on an app-scoped slot - #2783

Open
leonlaiyc wants to merge 1 commit into
kirodotdev:mainfrom
leonlaiyc:fix/app-slot-channel-autobind
Open

fix(chat): refuse a linked session binding on an app-scoped slot#2783
leonlaiyc wants to merge 1 commit into
kirodotdev:mainfrom
leonlaiyc:fix/app-slot-channel-autobind

Conversation

@leonlaiyc

@leonlaiyc leonlaiyc commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

A slot's linked_session_key is session authority. App routes authorize against slot._app, then dispatch on effective_session_key(slot). Before this change, an app could request a channel-shaped slot name and receive an app-owned slot bound to a channel session it did not own.

The invariant established here is stronger than blocking name inference: an app-scoped slot cannot carry a linked-session binding at all. That covers explicit arguments, bindings applied after creation, poisoned metadata written by an older build, and authorization rechecks after an await.

Why it matters

This closes a cross-surface authorization path. Slot ownership must not imply authority over a Slack, Discord, cron, or other session merely because the app chose a matching slot name or restored stale metadata.

What changed

  • linked_session_key is a property with a shared mutation boundary. A non-empty binding on an app-owned slot is refused and audited without aborting slot creation or restore.
  • A refused binding is retained separately as an authorization-only linked_session_claim; it never becomes the effective route, transcript target, provider key, or serialized live binding.
  • Reset, note, and context authorization treat both accepted and refused claims as foreign and return the same 404 as a missing slot.
  • Immediate/deferred note delivery, pending-context drain, and save snapshots compare the authorization claim again at their late-use seams, dropping content accepted before a refused rebind attempt rather than delivering it to a different session.
  • Explicitly clearing the binding clears the refused claim.
  • Channel-shaped name inference is skipped for app-owned slots.
  • Restore honors explicit channel_origin, but no longer infers channel provenance from a refused legacy linked key on an app-owned row.
  • Dashboard-owned channel tabs and unscoped cron/workflow late binding keep their existing behavior.
  • The invariant and upgrade behavior are documented in docs/system-specs/modules/session.md.

The rule lives at the attribute boundary because bindings are assigned by the factory, restore paths, cron/workflow injectors, and other late-binding callers. A per-call-site guard would leave the next assignment path as a bypass.

Verification

  • Focused security and compatibility set: 18 passed, including the four failures exposed by the previous CI run.
  • Related state, gateway app-kit, and chat-handler files: 168 passed.
  • Expanded cron/channel/chat-utility set: 259 passed.
  • Five key security cases repeated serially ten times: 50/50 passed.
  • Mypy: 1167 source files, 0 issues.
  • Flake8, isort, Black baseline, subprocess-encoding, docs-lint, brand, and changed-source diff gates passed.
  • After the final rebase onto current main, the focused 18 tests and static gates passed again.

No flaky failure was hidden with a retry, sleep, or longer timeout. A separate Windows SEL test failure observed in CI is being fixed at its own shared lock boundary and is not mixed into this security PR.

Screenshots / video

Why no screenshot: This is a backend authorization, routing, persistence, and test change; it does not alter rendered UI.

@leonlaiyc
leonlaiyc requested a review from a team as a code owner August 11, 2026 08:31
@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 readiness: checking Automated validation is still running labels Aug 11, 2026
@leonlaiyc
leonlaiyc force-pushed the fix/app-slot-channel-autobind branch from dfd83de to 7a3afdb Compare August 11, 2026 09:09
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 11, 2026
@leonlaiyc
leonlaiyc force-pushed the fix/app-slot-channel-autobind branch from 7a3afdb to bf28b5e Compare August 11, 2026 10:12
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 11, 2026
@iamwhatever

Copy link
Copy Markdown
Collaborator

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Tests

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Aug 15, 2026
@leonlaiyc
leonlaiyc force-pushed the fix/app-slot-channel-autobind branch from bf28b5e to f014465 Compare August 17, 2026 13:55
@github-actions github-actions Bot removed the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Aug 17, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

Hi @leonlaiyc, a maintainer nudge on this one: it is carrying the readiness: action required label and has had no activity for about 4 days, so it is not moving toward merge.

Current state:

  • Base: 410 commits behind main
  • Merge state: mergeable, but the readiness gate is still red
  • Red signals:
    • Backend Tests (3.12, 4)
    • Coverage Gate
    • Publish readiness signal

Could you rebase onto the latest main and push a fix?

git fetch upstream           # or: git fetch origin, if this branch lives here
git rebase upstream/main
# resolve any conflicts, run the local gates below, then update the branch with
# a force-with-lease so the rebase lands without clobbering anyone else's work

The local gates to run before updating the branch:

black src/kiro_crew test && isort src/kiro_crew test
flake8 src/kiro_crew test && mypy src/kiro_crew
python -m pytest
cd website && npm run build && npm run test

A good part of this branch's redness is likely stale rather than a real defect: it predates a lot of what is now on main, and several of these gates (the Coverage Gate, the Windows shards, the AI review lanes) have changed since the last run here. A rebase alone often clears them. If something still fails afterwards and you believe it is a false positive, say so in a comment and we will take a look. If the change is no longer needed, feel free to close the PR.

@bolichen97
bolichen97 enabled auto-merge (squash) August 24, 2026 07:01
iamwhatever pushed a commit that referenced this pull request Aug 25, 2026
The self-heal sweep rescues a `pending` verdict on age and a `failure` verdict on
later check evidence. Two freeze modes fell outside both tests and had no other
recovery path, because the sweep is the only thing that recomputes an unchanged
commit once its `workflow_run` events are spent.

An UNPUBLISHED verdict was the live one. `pr-readiness.yml` deliberately does not
retry its status POST -- a retry can overwrite a concurrent run's newer verdict --
and instead instructs a human to re-run the workflow. When that POST fails the SHA
carries no readiness status at all, so there is no `pending` to age out, and the
sweep skipped the PR on the assumption that its own run was still coming. The one
case the publisher delegates to a re-run was the one case nothing re-ran. PR #2783
proves it: both readiness runs for f014465 failed on `gh: HTTP 503` from the POST
on 2026-08-17, and eight days later the PR was still MERGEABLE with no readiness
verdict of any kind. Today's #5282 hit the same POST with a 502 and survived only
because an unrelated later event happened to republish. Rescued now on the PR's
own last-activity age, which a push bumps, so the grace window restarts on every
new revision and a genuinely queueing run is left alone.

A STALE GREEN was the unsafe-direction gap. The re-run mechanism behind mode 2 is
direction-blind: a job re-run that flips a lane red after a green verdict emits no
`workflow_run: completed` either, leaving the required aggregate green over a
now-red revision -- which PERMITS a merge, where a stale red only blocks one. The
`*)` arm skipped it as "success needs no rescue". `success` and `error` now share
mode 2's evidence test, narrowed to failure-class conclusions: housekeeping
check-runs (`Strip stale workflow-change override`, `Fork workflow-change guard`)
legitimately complete days after a verdict, so an unnarrowed "any check completed
later" test would re-fire most green PRs every sweep while proving nothing. No
live poisoned green was found in the current 250 open PRs -- the one candidate,
#4272, turned out to be exactly that housekeeping false positive -- so this arm
closes the mechanism, not an observed incident.

`--paginate` WITHOUT `--slurp` was a latent defect in the pre-existing `failure`
arm, found in review and fixed in both arms. gh concatenates one JSON document per
page and jq evaluates each separately, so `max` emits one timestamp PER PAGE;
`date -d` then rejects the multi-line string, the epoch reads 0, and the PR is
skipped. That silently exempted every PR with more than 100 check-runs, which on
this repo is any PR whose lanes have been re-run -- #4718 alone carries ~90.
Verified against the live API: three pages produced three `max` lines unslurped
and one slurped.

The statuses read is now checked for FAILURE before its filter runs. A 503 and a
genuinely absent status both yield empty jq output, and the new unpublished arm
dispatches on the latter -- so without this, transient GitHub trouble would re-fire
arbitrary old PRs every 15 minutes, on a shared token budget, which is the same
exhaustion that causes the missing verdict in the first place.

`--limit 300` on the open-PR listing is raised to 900 with a loud warning when the
ceiling is hit, here and in the two sibling sweeps that share the defect
(fork-pr-label.yml, pr-merge-conflict-label.yml, both found in review). `gh pr
list` returns newest-first and truncates silently, so a ceiling near the real count
drops the OLDEST PRs -- precisely the frozen or unlabelled ones each sweep exists
to catch. At 250 open the old value was already at 83% of its ceiling. Same
truncation class as the `pr_status_report` limit bug. `gh label list --limit` in
those files counts labels, not PRs, and is untouched.

The unpublished case is carried as a sentinel `state` rather than an early record
write, so it reuses the single staleness comparison and the single stale-file
record writer instead of restating either. GitHub's status states are
error/failure/pending/success, so the sentinel cannot collide.

Not addressed here, because the fix is a policy decision rather than a defect: a
required lane that did not exist when the head SHA was pushed can never be
satisfied, and a recompute re-derives `not started` forever. #3089 and #3037 are
frozen on First Principles Review, which became blocking in #4253 but only exists
from #3436. Auto-skipping the lane would let an unreviewed PR pass, so it wants a
rebase or a deliberate eligibility rule. Filed as #5957.

Test plan:
- test/test_pr_readiness_sweep.py: 31 passed (18 pre-existing, 13 added).
  - unpublished: rescued past the window, left alone inside it, respects
    MAX_DISPATCH, fails closed on an unparseable timestamp, still reads as
    unpublished when only a foreign-context status exists, and is NOT triggered by
    a failed statuses read.
  - stale green: rescued for all six failure-class conclusions, never rescued by
    a later pass, self-terminating on republish, untouched with no evidence.
  - pagination: evidence on a SECOND page is found by both arms, which is what
    an unslurped `--paginate` cannot do.
  - truncation: the ceiling is a variable, and a hit is reported.
  - the context-isolation test now proves a foreign status cannot DRIVE the
    decision, rather than relying on the absent-status shape it used to assert.
  - the gh stub models `--paginate --slurp` (outer array of pages) and can fail a
    statuses read on demand, so both fixes are exercised rather than assumed.
- Revert-verify: undoing each of the seven properties independently (unpublished
  arm, green arm, ceiling, failure-class narrowing, slurp on each arm, statuses
  transport guard) fails a named test; tree restored to 31 green.
- test_workflows_conformance / test_workflow_permissions / test_github_workflow_security
  / test_pr_quality_gates / test_ai_review_workflows + the sweep suite: 289 passed.
- check_black_formatting.py and flake8 clean; all three workflows parse as YAML.
- test_pr_readiness_publish.py has 4 pre-existing failures on this host (local jq
  rejects `label` as an identifier); identical on pristine main, untouched here.
bolichen97 pushed a commit that referenced this pull request Aug 26, 2026
…#5955)

The self-heal sweep rescues a `pending` verdict on age and a `failure` verdict on
later check evidence. Two freeze modes fell outside both tests and had no other
recovery path, because the sweep is the only thing that recomputes an unchanged
commit once its `workflow_run` events are spent.

An UNPUBLISHED verdict was the live one. `pr-readiness.yml` deliberately does not
retry its status POST -- a retry can overwrite a concurrent run's newer verdict --
and instead instructs a human to re-run the workflow. When that POST fails the SHA
carries no readiness status at all, so there is no `pending` to age out, and the
sweep skipped the PR on the assumption that its own run was still coming. The one
case the publisher delegates to a re-run was the one case nothing re-ran. PR #2783
proves it: both readiness runs for f014465 failed on `gh: HTTP 503` from the POST
on 2026-08-17, and eight days later the PR was still MERGEABLE with no readiness
verdict of any kind. Today's #5282 hit the same POST with a 502 and survived only
because an unrelated later event happened to republish. Rescued now on the PR's
own last-activity age, which a push bumps, so the grace window restarts on every
new revision and a genuinely queueing run is left alone.

A STALE GREEN was the unsafe-direction gap. The re-run mechanism behind mode 2 is
direction-blind: a job re-run that flips a lane red after a green verdict emits no
`workflow_run: completed` either, leaving the required aggregate green over a
now-red revision -- which PERMITS a merge, where a stale red only blocks one. The
`*)` arm skipped it as "success needs no rescue". `success` and `error` now share
mode 2's evidence test, narrowed to failure-class conclusions: housekeeping
check-runs (`Strip stale workflow-change override`, `Fork workflow-change guard`)
legitimately complete days after a verdict, so an unnarrowed "any check completed
later" test would re-fire most green PRs every sweep while proving nothing. No
live poisoned green was found in the current 250 open PRs -- the one candidate,
#4272, turned out to be exactly that housekeeping false positive -- so this arm
closes the mechanism, not an observed incident.

`--paginate` WITHOUT `--slurp` was a latent defect in the pre-existing `failure`
arm, found in review and fixed in both arms. gh concatenates one JSON document per
page and jq evaluates each separately, so `max` emits one timestamp PER PAGE;
`date -d` then rejects the multi-line string, the epoch reads 0, and the PR is
skipped. That silently exempted every PR with more than 100 check-runs, which on
this repo is any PR whose lanes have been re-run -- #4718 alone carries ~90.
Verified against the live API: three pages produced three `max` lines unslurped
and one slurped.

The statuses read is now checked for FAILURE before its filter runs. A 503 and a
genuinely absent status both yield empty jq output, and the new unpublished arm
dispatches on the latter -- so without this, transient GitHub trouble would re-fire
arbitrary old PRs every 15 minutes, on a shared token budget, which is the same
exhaustion that causes the missing verdict in the first place.

`--limit 300` on the open-PR listing is raised to 900 with a loud warning when the
ceiling is hit, here and in the two sibling sweeps that share the defect
(fork-pr-label.yml, pr-merge-conflict-label.yml, both found in review). `gh pr
list` returns newest-first and truncates silently, so a ceiling near the real count
drops the OLDEST PRs -- precisely the frozen or unlabelled ones each sweep exists
to catch. At 250 open the old value was already at 83% of its ceiling. Same
truncation class as the `pr_status_report` limit bug. `gh label list --limit` in
those files counts labels, not PRs, and is untouched.

The unpublished case is carried as a sentinel `state` rather than an early record
write, so it reuses the single staleness comparison and the single stale-file
record writer instead of restating either. GitHub's status states are
error/failure/pending/success, so the sentinel cannot collide.

Not addressed here, because the fix is a policy decision rather than a defect: a
required lane that did not exist when the head SHA was pushed can never be
satisfied, and a recompute re-derives `not started` forever. #3089 and #3037 are
frozen on First Principles Review, which became blocking in #4253 but only exists
from #3436. Auto-skipping the lane would let an unreviewed PR pass, so it wants a
rebase or a deliberate eligibility rule. Filed as #5957.

Test plan:
- test/test_pr_readiness_sweep.py: 31 passed (18 pre-existing, 13 added).
  - unpublished: rescued past the window, left alone inside it, respects
    MAX_DISPATCH, fails closed on an unparseable timestamp, still reads as
    unpublished when only a foreign-context status exists, and is NOT triggered by
    a failed statuses read.
  - stale green: rescued for all six failure-class conclusions, never rescued by
    a later pass, self-terminating on republish, untouched with no evidence.
  - pagination: evidence on a SECOND page is found by both arms, which is what
    an unslurped `--paginate` cannot do.
  - truncation: the ceiling is a variable, and a hit is reported.
  - the context-isolation test now proves a foreign status cannot DRIVE the
    decision, rather than relying on the absent-status shape it used to assert.
  - the gh stub models `--paginate --slurp` (outer array of pages) and can fail a
    statuses read on demand, so both fixes are exercised rather than assumed.
- Revert-verify: undoing each of the seven properties independently (unpublished
  arm, green arm, ceiling, failure-class narrowing, slurp on each arm, statuses
  transport guard) fails a named test; tree restored to 31 green.
- test_workflows_conformance / test_workflow_permissions / test_github_workflow_security
  / test_pr_quality_gates / test_ai_review_workflows + the sweep suite: 289 passed.
- check_black_formatting.py and flake8 clean; all three workflows parse as YAML.
- test_pr_readiness_publish.py has 4 pre-existing failures on this host (local jq
  rejects `label` as an identifier); identical on pristine main, untouched here.

Co-authored-by: Joe Guo <zejiangg@amazon.com>
@iamwhatever iamwhatever added the needs-pr-triage PR scanner: awaiting automated triage label Aug 26, 2026
@bolichen97 bolichen97 removed the needs-pr-triage PR scanner: awaiting automated triage label Aug 26, 2026
@bolichen97 bolichen97 added the drive-to-green PR claimed by drive-to-green pipeline label Aug 26, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

🤖 Kiro Crew [operator: bolichen97#bb3ad1ca]: This PR has been inactive for 7+ days with failing CI. I've assessed the blockers and they appear resolvable — I'll push fixes directly to this branch as a co-author.

Assessment: Reds are Backend Tests (3.12, shard 4) — test/test_slack_options_lifecycle.py::TestLinkTimeBackfill::test_the_newest_reply_stays_answerable_and_is_recorded (AttributeError: 'NoneType' object has no attribute 'args', with turn-deadline ContextVar cross-context noise) — plus the Coverage Gate failing closed on that shard. The failing test is outside this PR's touched files, so this looks like base drift/interference; plan is rebase onto current main, reproduce locally, fix or confirm resolved, and drive CI green.

If you'd prefer I don't touch this PR, add the pr-no-autofix label.

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Aug 28, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

This PR shares DashboardState, _ChatSlot, chat_persistence.py, and session rehydration metadata with #3139 and #4904. Its distinct invariant is identity isolation: an app slot/name must not auto-bind an app to an unrelated channel session. #3139 adds general-repository worktree state; #4904 adds fork-to-parent merge state.

Suggested coordination order is to land/replay this identity-binding floor first, then #3139's worktree metadata, then #4904's parent/fork merge metadata. At each step, round-trip persistence/rehydration tests should prove app/channel identity is not overwritten while worktree and parent-session fields survive.

@bolichen97
bolichen97 force-pushed the fix/app-slot-channel-autobind branch from f014465 to b96c4e6 Compare August 29, 2026 19:53
@bolichen97 bolichen97 changed the title fix(chat): stop a slot name from binding an app to a channel session fix(chat): refuse a linked session binding on an app-scoped slot Aug 29, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Aug 29, 2026
Treat a slot binding as session authority and reject it for app-owned slots at the shared attribute boundary. This covers name inference, explicit and late binding, and legacy persistence restore while leaving dashboard/channel flows unchanged.

Do not infer channel provenance from a refused legacy app binding, document the invariant, and pin creation, restoration, serialization, SEL, and route behavior with focused tests.
@bolichen97
bolichen97 force-pushed the fix/app-slot-channel-autobind branch from b96c4e6 to 752de48 Compare August 29, 2026 20:48
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — ✅ PASS

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

All current _app mutation sites are safe: both restore paths (chat_persistence.py:942→989, :1409→1454) stamp _app before assigning the binding, and spec_builder only re-stamps a slot it already owns (routes.py:2979 refuses foreign and unscoped slots). The quarantine also self-heals: the claim is never serialized as a live binding, so the poisoned metadata is rewritten on the next save and the row restores clean thereafter. The enforcement layer (property setter as the single mutation boundary), the refuse-don't-raise restore trade-off, backward compat for legacy rows, and the spec update in the same commit are all sound. One genuine gap in the boundary is worth flagging as a suggestion: the invariant has two mutation axes (linked_session_key and _app) and only one is guarded — a future adoption path stamping _app onto an already-bound slot would recreate the forbidden state silently.

Design-Verdict: PASS

Enforcing the invariant at the single shared mutation boundary, with a retained refused claim failing closed, is the right shape for a cross-surface authorization fix.

Suggestions

  • The invariant has a second unguarded mutation axis: slot._app = ... on an already-bound slot (the adoption pattern in spec_builder/backend/routes.py:3033) would recreate the app-owned-with-binding state with no refusal or SEL audit. Current sites are safe (restores stamp _app before the binding; spec_builder refuses foreign/unscoped slots), but guarding or asserting at that axis too would make the boundary complete rather than convention-dependent.

[DESIGN-REVIEWED] 752de48

@github-actions

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — 🔴 changes requested (blocking)

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

BLOCKING -- src/kiro_crew/dashboard/state.py:3531 -- Refused legacy bindings still restore foreign transcripts
return
Legacy app-owned channel row -> restore hydrates the channel transcript before this refusal -> the owning app’s slot-detail request returns foreign messages.
Anchor: backend-security-controls
Fix: Raise here so rehydration rolls back and skips the poisoned row.
[BLOCK-MERGE] 752de48
[GPT-REVIEWED] 752de48

@github-actions

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5, fork) — 🟡 CONCERNS

Premise-level review of 752de486af0740e1661b48bf2e70908a85724142 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Analysis complete. Key facts from the base tree: notes are stamped with effective_session_key(slot) at accept time (chat_handlers.py:7060,7080,7089), the factory sets _app before applying any binding (state.py:7086:7138), and both restore paths pass app= through the factory — but spec_builder/backend/routes.py:3033 sets slot._app = APP_NAME on an adopted slot after creation, outside the new boundary.

First-Principles-Verdict: CONCERNS

The note-seam claim comparison guards against "delivery to a different session" that the setter itself makes unreachable, and _app adoption bypasses the invariant entirely.

What this change ships

Intent: stop an app-owned chat slot from ever holding authority over another surface's session — a FIX.

  1. App-owned slot refuses any linked-session binding at the setter, with SEL denial — justified
  2. Channel-name inference skipped for app-owned slots at creation — justified
  3. Restore stops inferring channel_origin from a legacy linked key on app rows — justified
  4. Refused binding kept as linked_session_claim; app reset/note/context/cancel 404 a quarantined slot — justified (legacy-restore hydration)
  5. Note drain/flush/save compare against the claim via new note_authorization_session_key — duplicate of effective_session_key
  6. Save snapshot double-confirms the claim — rides along with 5
  7. Empty binding clears the claim — justified, part of 4
  8. New SEL slot_session_bind denial event — justified
  9. session.md section — mandated same-commit doc rule
  10. New test module — justified

Watch

  • The invariant is "app-owned slots carry no binding", but only the binding conjunct is guarded. Grep \._app\s*= in src/: one post-creation assignment outside restore — spec_builder/backend/routes.py:3033 adopts an existing slot; adopting an already-bound slot recreates the forbidden state with no setter fired.
  • The description says late seams drop content "rather than delivering it to a different session", but the setter guarantees an app slot's effective session never changes, so no different session exists; the only content those seams newly drop is stamped for the slot's own session.

Subtractions

  • Delete note_authorization_session_key (chat_utils.py) and keep the three call sites on the existing effective_session_key (chat_utils.py:667). Stamps come from effective_session_key at accept (chat_handlers.py:7060/7080/7089), which the setter freezes on app slots, so the claim-keyed comparison changes nothing that can occur — except for legacy rows stamped with the refused foreign key, where it keeps (stamp == claim) what the effective-key comparison dropped. The quarantine gates in _check_slot_app_ownership / _app_cancel_denied already carry the fail-closed behavior. This also removes the snapshot double-compare in _save_slot_to_history.

[FIRST-PRINCIPLES-REVIEWED] 752de48

@github-actions

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

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

Review details

The binding-assignment sites (restore, cron inject, workflow inject, factory) all route foreign channel/cron sessions — exactly what app-slot isolation is meant to refuse. No legitimate app-slot routing depends on linked_session_key, and for a normal (non-quarantined) app slot there is no claim, so note_authorization_session_key equals effective_session_key and notes behave exactly as before the diff.

Assessing CANDIDATE 1 against the survival bar:

  • (a) input requires a quarantined app slot — one that attempted (and was refused) a foreign binding, i.e. an attack, a bug, or a restored legacy-poisoned row. That is not an ordinary-operation input; a normal app worker slot never carries a claim.
  • (c) wrong outcome is not clean: on such a slot the drop is the documented fail-closed intent ("app gates keep refusing the quarantined slot… note content stamped before the attempt is dropped"). Whether a dashboard user legitimately posts a recoverable note to an already-quarantined app slot and expects delivery cannot be established from the code — the candidate itself rates this "low" and could not confirm it occurs in practice.

The mismatch between the (unchanged) effective_session_key write stamp and the new note_authorization_session_key authorization key is real, but it only bites on anomalous quarantined slots, where dropping fail-closed is defensible design rather than a re-derivable wrong outcome. I cannot re-derive (a) and (c) at 80+, so the candidate does not survive.

No cross-session leak is introduced anywhere in the diff: every changed gate (_check_slot_app_ownership, _app_cancel_denied, the drain/save auth key, the channel-autobind skip, the channel_origin restore inference) denies or drops strictly more than before, never routes or keeps more.

No findings.

[OPUS-REVIEWED] 752de48

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 29, 2026
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Aug 30, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #6813 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6813: KEEP. Same attribute and same authorization seam, with a real semantic interaction (a refused app-slot binding is precisely the case 6813's queue filter and held-entry parking react to). Coordinate the two rather than resolving textually. Files: src/kiro_crew/dashboard/state.py, src/kiro_crew/dashboard/chat_persistence.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

@bolichen97

Copy link
Copy Markdown
Collaborator

@leonlaiyc Thanks for this. The invariant is still absent from main (src/kiro_crew/dashboard/state.py still declares linked_session_key as a plain attribute, and both the channel-name inference and the channel_origin restore inference are still unguarded), so the fix is still wanted. Before it can land, it needs reconciling with open PR #6813 (cc @rnoack1).

The two PRs collide on src/kiro_crew/dashboard/state.py, chat_persistence.py, chat_utils.py, chat_handlers.py and docs/system-specs/modules/session.md. Both add a refusal path plus a retained refusal record to the same attribute at the same restore assignment in _rehydrate_slot_from_history, under different names and different criteria: yours refuses any binding on an app-owned slot and keeps it as linked_session_claim; #6813 keeps a plain attribute, gates adoption on persisted_binding_is_adoptable at three hydration sites, and records binding_refused. Neither covers the other, and landing both leaves two disjoint records of one fact. #6813 also ships census tests that enumerate those three call sites, so moving the decision into a property setter, as this PR does, removes them from the enumerated functions and fails that census.

Suggested path: agree on one refusal mechanism first, ideally as its own small PR that both then rebase onto, rather than merging both records. This PR should keep the parts #6813 does not touch, notably the app guard on get_or_create_slot's channel-name inference.

Also still open here: rebase off the drift (the PR is currently conflicted), the GPT BLOCK-MERGE finding on refusing rather than skipping an already-hydrated poisoned row, and the missing ## Tests section.

Posted from the 2026-09-08 open-PR relationship audit (read-only, one auditor per PR); reply here if any of this is wrong.

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

Labels

drive-to-green PR claimed by drive-to-green pipeline fork Pull request from a fork (external contributor) merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants