close
Skip to content

fix: audit deactivate of an already-lapsed safety override grant (#2475) - #2483

Merged
iamwhatever merged 1 commit into
mainfrom
fix/deactivate-audit-lapsed-grant-2475
Aug 10, 2026
Merged

fix: audit deactivate of an already-lapsed safety override grant (#2475)#2483
iamwhatever merged 1 commit into
mainfrom
fix/deactivate-audit-lapsed-grant-2475

Conversation

@bolichen97

Copy link
Copy Markdown
Collaborator

Summary

SafetyOverride.deactivate() returned early when _active was false, and that early return sat above the safety_override:deactivate SEL audit write. Lazy expiry (is_active()) clears _active while leaving _expires_at/_source/_permanent in place, so an operator switching back to normal mode on a grant whose TTL had already elapsed was recorded nowhere — the common path, not an edge case. The SEL stream is the only durable record of who changed the auto-approval posture; an auditor saw the activate event and then nothing.

The contract decision (explicit, per the issue)

The issue frames this as a contract disagreement on the same object. This PR resolves it as:

  • deactivate() logs every operator DECISION — it now emits its SEL event whenever a grant exists in any form, live or lapsed. Only a never-activated instance stays silent.
  • deactivate_scope() stays on "log every state change" — no operator decision is lost there (revoking an absent scope changes nothing), so it is intentionally left untouched.

How

  • The guard now keys on the _expires_at 0.0 sentinel — the one field lazy expiry does not clear (verified against is_active(), and pinned by a test asserting _expires_at > 0 after lazy expiry) — instead of _active, which is exactly what expiry zeroes.
  • Pre-call state is snapshotted under _lock, then the SEL write happens outside the lock (same rule as renew(): never hold the state lock across I/O).
  • resources now carries the pre-call state: source:<who>, was_active:<bool>, was_permanent:<bool>, remaining:<n>s, prior_source:<grant origin>, so a no-change deactivate is distinguishable from one that revoked a live grant. operation/outcome (safety_override:deactivate / disabled) are unchanged; nothing in src/ or website/src/ parses the resources string.
  • Deliberate asymmetry with _commit_activation (and the fail-closed posture in SafetyOverride.renew() extends a grant before auditing it, and swallows audit failure #2453's area): this is a REVOCATION, not a grant. The audit stays non-critical and the state change unconditional — refusing to deactivate because an audit write failed would leave auto-approval ON, which is strictly worse. This is not an oversight; it is stated in the code comment.

Latent bug fixed as a direct consequence

The old early return left _expires_at intact on a lapsed grant, so a renew() inside the 300s grace window could resurrect a grant the operator had just explicitly revoked. Zeroing _expires_at on every existing-grant deactivate closes that; test_renew_after_deactivating_lapsed_grant_fails fails on unfixed main and pins it.

Tests (mutation-verified: 4 of the new tests fail on unfixed code)

All inject at the SEL sink (log_api_access on the patched sel() instance), not _log_sel, so assertions cannot pass vacuously:

  • lapsed grant → deactivate emits, with was_active:False, remaining:0s, prior_source:slack; also pins the discriminator (lazy expiry clears _active but not _expires_at)
  • live grant → still emits with was_active:True (existing signal not diluted)
  • declared/permanent grant → was_permanent:True, remaining:-1s
  • never-activated instance → silent (pre-existing test retained)
  • second deactivate → silent (sentinel restored)
  • renew after deactivating a lapsed grant → refused

Out-of-scope note

src/kiro_crew/dashboard/token_auth.py gets a mechanical import reorder: main HEAD (584bbb0) currently fails the repo-wide isort --check-only gate on that file (a cross-PR merge artifact between the tailnet and revocation_gen import blocks). Without it this PR cannot pass lint. No code change, imports only.

Coordination

Scoped strictly to deactivate(). Sibling issue #2453 (renew()) and PR #2443 touch the same file in disjoint hunks; rebased onto fresh origin/main immediately before pushing. This PR does NOT attempt #2453.

Closes #2475

@bolichen97
bolichen97 requested a review from a team as a code owner August 10, 2026 05:22
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 10, 2026
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 32bdb228a45de8c273d8cdcee3618477340a4e2f — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 32bdb22

Verdict parsed from the review's SHA-scoped output markers for commit 32bdb228a45de8c273d8cdcee3618477340a4e2f.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 32bdb228a45de8c273d8cdcee3618477340a4e2f: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Advisory design-level review of 32bdb228a45de8c273d8cdcee3618477340a4e2f — updated in place on each push; does not block merge.

Design-Verdict: PASS

Guard keyed on the one field lazy expiry preserves fixes the root cause, and zeroing it closes the renew-resurrection hole — sound, proportionate, spec updated in-commit.

[DESIGN-REVIEWED] 32bdb22

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 32bdb228a45de8c273d8cdcee3618477340a4e2f and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 32bdb22

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 32bdb228a45de8c273d8cdcee3618477340a4e2f: <one-sentence reason>

@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 10, 2026
SafetyOverride.deactivate() returned early when _active was false, and
that early return sat above the SEL audit write. Lazy expiry clears
_active while leaving the rest of the grant state in place, so an
operator switching back to normal mode after the TTL elapsed was
recorded nowhere -- the common path, not an edge case.

deactivate() now emits its safety_override:deactivate event for any
grant that exists in any form, keyed on the _expires_at 0.0 sentinel
(which lazy expiry does NOT clear) rather than _active (which it does).
The pre-call state (was_active, was_permanent, remaining, prior_source)
is snapshotted under _lock and written to resources outside the lock;
operation/outcome are unchanged. A never-activated instance stays
silent. Zeroing _expires_at on every existing grant also closes the
renew grace window, so an explicitly revoked lapsed grant can no longer
be resurrected by renew().

Contract decision: deactivate() logs every operator DECISION;
deactivate_scope() stays on log-every-state-change (no decision is
lost there). The revocation is deliberately non-critical, unlike the
fail-closed _commit_activation: refusing to deactivate on a failed
audit write would leave auto-approval ON.

Also sorts imports in dashboard/token_auth.py -- a cross-PR merge
artifact on main that fails the repo-wide isort gate.

Closes #2475
@bolichen97
bolichen97 force-pushed the fix/deactivate-audit-lapsed-grant-2475 branch from 98327bb to 32bdb22 Compare August 10, 2026 05:30
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 10, 2026
@iamwhatever
iamwhatever merged commit cc889b5 into main Aug 10, 2026
52 checks passed
@iamwhatever
iamwhatever deleted the fix/deactivate-audit-lapsed-grant-2475 branch August 10, 2026 16:38
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 10, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
…odotdev#2475) (kirodotdev#2483)

SafetyOverride.deactivate() returned early when _active was false, and
that early return sat above the SEL audit write. Lazy expiry clears
_active while leaving the rest of the grant state in place, so an
operator switching back to normal mode after the TTL elapsed was
recorded nowhere -- the common path, not an edge case.

deactivate() now emits its safety_override:deactivate event for any
grant that exists in any form, keyed on the _expires_at 0.0 sentinel
(which lazy expiry does NOT clear) rather than _active (which it does).
The pre-call state (was_active, was_permanent, remaining, prior_source)
is snapshotted under _lock and written to resources outside the lock;
operation/outcome are unchanged. A never-activated instance stays
silent. Zeroing _expires_at on every existing grant also closes the
renew grace window, so an explicitly revoked lapsed grant can no longer
be resurrected by renew().

Contract decision: deactivate() logs every operator DECISION;
deactivate_scope() stays on log-every-state-change (no decision is
lost there). The revocation is deliberately non-critical, unlike the
fail-closed _commit_activation: refusing to deactivate on a failed
audit write would leave auto-approval ON.

Also sorts imports in dashboard/token_auth.py -- a cross-PR merge
artifact on main that fails the repo-wide isort gate.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SafetyOverride.deactivate() skips its audit event when the grant already lapsed

2 participants