close
Skip to content

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

Description

@chenmingwei23

What

SafetyOverride.deactivate() returns early when the grant is already inactive, and the early return is above its SEL audit. So when an operator switches back to normal mode on a grant that had already lapsed, the action is recorded nowhere: no safety_override:deactivate event is emitted.

def deactivate(self, source: str) -> None:
    with self._lock:
        if not self._active:
            return          # <-- skips the audit below
        ...
    self._log_sel(operation="safety_override:deactivate", outcome="disabled", ...)

Lazy expiry makes this the common case rather than an edge one: it clears _active while leaving the rest of the grant's state in place, so any deactivate arriving after the TTL has elapsed takes the early return.

Why

The audit stream is the only durable record of who changed the auto-approval posture and when. An auditor reconstructing "was auto-approval on at 03:00, and who turned it off?" sees the activate event and then nothing — the operator's explicit decision is invisible, and it is indistinguishable from the grant simply having been left to lapse.

That distinction is not academic. It was load-bearing in #2443: a guard there assumed deactivate() always zeroes _expires_at, which the early return makes false, and the result was that an automatic lease could re-enable auto-approval an operator had just turned off. The guard is fixed (it now requires the grant to be live), but the underlying observation stands — this method's early return makes an operator action leave no trace at all.

Additional Context

  • src/kiro_crew/safety_override.pydeactivate(). Compare deactivate_scope(), which has the same shape but only logs when something was actually removed; the difference there is that no state change happened, whereas here an operator DID make a decision.
  • Same family as SafetyOverride.renew() extends a grant before auditing it, and swallows audit failure #2453 (renew() commits before auditing and swallows audit failure). Both are audit-completeness gaps on the same object; worth deciding together whether the object's contract is "log every decision" or "log every state change", because the two disagree exactly on this case.
  • Suggested shape: emit the event whenever deactivate() is called with a grant that exists in any form (including lapsed), with the pre-call state in resources so a no-change deactivate is distinguishable from one that actually revoked a live grant. Genuinely absent grants (never activated) can stay silent.
  • Test shape: activate, force lapse, call is_active() to trip lazy expiry, then deactivate() and assert a safety_override:deactivate SEL event was emitted. Inject the failure at the SEL sink rather than at _log_sel so the assertion cannot pass vacuously.

Metadata

Metadata

Assignees

Labels

area: coreCore runtime utilities, config/paths, process singletonsauto-fixableTriage: mechanical fix, automated pipeline will handlebugSomething is not workingclaimedAn automated fix is in progress

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions