fix(update): fail closed when approval nonce persists - #7615
Conversation
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]: This PR has been inactive for 7+ days with its readiness gate stuck. I've assessed the blockers and they appear resolvable — I'll push fixes directly to this branch as a co-author. Assessment: All CI checks are green on head 33afaf4 and the PR is mergeable, but all five Stage-2 fork review lanes (GPT/Opus/Design/First Principles/UX) skipped off the original CI completion and never restamp on reruns, leaving PR Readiness pending on 7 checks. Fix plan: rebase-only push onto current main to mint a new head, which re-fires CI and the review lanes, then drive any resulting findings to green. If you'd prefer I don't touch this PR, add the |
33afaf4 to
2332c89
Compare
|
Rebased onto main Clean rebase: no conflicts in any of the three files. The diff is byte-identical to the original (35 insertions, 2 deletions), including the RFC §1 sentence, which main's #7532 rewrite of §5 did not touch. Gates run locally on the rebased head, scoped to the changed files: @leonlaiyc please review the rebase. Note that a maintainer push makes the maintainer the last pusher, so under this repo's last-push rule a second approver is needed before merge. Reply here if anything looks wrong. |
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]: Drive-to-green status on head What changed by this drive:
What did not change:
The maintainer push minted the new head, which re-dispatched CI and all five Stage-2 review lanes (the sole triage blocker — lanes stuck |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of I've verified the key claims against the base tree: on base, First-Principles-Verdict: PASS Only unreadable claim: the mutation-probe "red-before" on the serialization test — confirm the test actually fails with What this change shipsInventory (5 items) — 5 justifiedIntent: make consuming the host-local update-approval nonce genuinely at-most-once — fail closed when the file can't be removed, and atomic against a concurrent re-arm. This is a FIX (the new fail-closed test demonstrably fails on base, where
Writer count grepped ( [FIRST-PRINCIPLES-REVIEWED] 4faa01c |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsThe candidate list contains no candidates, and I've independently verified the change. This is a concurrency fix: it serializes the nonce read-validate-remove under a reentrant module mutex, makes expiry cleanup an explicit opt-in so the out-of-process CLI reader can never unlink a fresh request, and makes the consume unlink fail-closed. I confirmed:
No behavioral defect, security hole, or blocking-rule violation is groundable on the changed lines. No findings. [OPUS-REVIEWED] 4faa01c |
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of The consume path, both callers, and the approve endpoint's Design-Verdict: PASS Fail-closed consumption and an in-process mutex land at the exact layer that owns the nonce, matching the real writer topology — sound and proportionate. [DESIGN-REVIEWED] 4faa01c |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
2332c89 to
e24762d
Compare
|
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]: Pushed What changed:
Local gates: isort / flake8 / mypy clean, |
82cd360 to
7739e27
Compare
|
|
|
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]: Pushed What changed:
Housekeeping: deleted one of my earlier disposition comments whose span ( |
7739e27 to
4f96f5f
Compare
|
|
|
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]: Pushed What changed:
Gates green (isort/flake8/mypy, 28 passed). Dispositions posted: GPT 00859f067f65 fixed, Design c4615150a7bb fixed, First Principles 6a4fb2306a04 rebutted (third same-class rides-along tag; class-level ruling recorded). |
A failed nonce unlink left an approved request replayable. Reject approval unless the single-use file is removed successfully. The nonce file's writers are also serialized: consume() runs its read-validate-remove as one critical section under a module mutex, and arm()'s atomic swap, the expiry cleanup, and clear_pending() take the same lock. Without it, an approve that validated request A could unlink a fresh request B that arm swapped in mid-window (GPT review finding). Reading never writes by default: read_pending() removes an expired file only on the explicit clear_expired=True opt-in used by gateway callers under the mutex, so the one out-of-process reader (kirocrew update approve) cannot write at all and the lock covers every writer that exists. Regression tests drive the two-thread interleave and the default-read no-write contract, both red on the pre-fix code. Original work by Leon (leonlaiyc). Rebased onto current main by a maintainer; serialization hardening added in review. Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
4f96f5f to
4faa01c
Compare
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]: Pushed |
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]: Review-ready at head |
bolichen97
left a comment
There was a problem hiding this comment.
Maintainer review (agent-assisted, on behalf of @bolichen97), head 4faa01c1f.
Verified: consume() in src/kiro_crew/platform/update_stepup.py:180-190 now raises StepUpError on unlink failure instead of the old best-effort clear_pending(), and dashboard/handlers/updates.py:2430-2432 turns that into a 403 approve_refused with nothing swallowed. Read-validate-remove runs as one critical section under _PENDING_MUTEX, so the stale-nonce, unreadable-file and check/consume race paths all fail closed. Negative tests cover each path. No findings.
Problem / Motivation
The host-local update approval nonce is documented as single-use, but
consume()delegated deletion to a best-effort cleanup helper. If unlinking the nonce file failed, the helper swallowed the error andconsume()still returned the approved request, leaving the same nonce on disk and replayable.Why it matters
The nonce is the authority boundary between dashboard arming and host-local approval. Accepting an approval without durably consuming that nonce breaks the at-most-once guarantee precisely when the filesystem reports that it could not enforce it.
What changed (motivation → approach → change)
Approval consumption now uses a strict removal path that raises
StepUpErroron any unlink failure. General cleanup remains best-effort, so expired-state cleanup keeps its existing behavior while the security-sensitive transition fails closed. The update architecture RFC now states that removal must succeed before approval is accepted.Review hardening: consume() runs its read → validate → remove as one critical section under a new module mutex (
_PENDING_MUTEX), and arm()'s atomic swap, the expiry cleanup, and clear_pending() take the same lock. This closes a window the GPT review lane flagged: an approve that validated request A could unlink a fresh request B a concurrent arm swapped in — accepting the stale approval while silently destroying the new one. Reading never writes by default:read_pending()removes an expired file only on the explicitclear_expired=Trueopt-in, which only gateway callers use (under the mutex, serialized against arm). The one out-of-process reader (kirocrew update approve) uses the no-write default, so the lock covers every writer that exists. A stale file that lingers grants nothing (every reader checks expiry) and the next arm replaces it.Tests
PermissionError, verifies approval is rejected, and verifies the request remains pending.DID NOT RAISE StepUpErroron currentmain.read_pending()on an expired file reports absent and leaves the file untouched; cleanup happens only via the explicitclear_expired=Truegateway opt-in.test/test_update_stepup.py: 28 passed.src/kiro_crew/) pass on the final tree.Manual verification
N/A — the unit test directly controls the filesystem failure at the consumption boundary and the existing endpoint tests cover
StepUpErrorresponse handling.Related Issues
no linked issue: found through direct source inspection; no matching open issue or pull request.
Pattern harvest
Rule candidate: review-prompt
Pattern: single-use credentials must fail closed unless durable consumption succeeds.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)