6.6 KiB
Compaction observer revocation and runtime generations
WI-3 connects Claude and Pi compaction/session lifecycle events to the existing authenticated lease-broker state machine. It does not add a second lease store or let runtime hooks assert identity. Each observer inherits the broker-minted session, resolves the current private runtime generation, and sends the existing revoke_lease action over the authenticated Unix socket.
Observer matrix
| Runtime | Lifecycle signal | Action |
|---|---|---|
| Claude / Claudex | PreCompact |
Revoke the current lease before compaction. A non-zero hook result blocks the lifecycle transition. |
| Claude / Claudex | SessionStart with matcher compact |
Revoke again after compacted context starts. |
| Claude / Claudex | SessionStart with matcher resume|clear |
Atomically advance the private generation, then revoke the replacement incarnation. |
| Pi | session_before_compact |
Revoke before compaction; return { cancel: true } if revocation cannot be confirmed. |
| Pi | session_compact then the first context |
Arm and run an independent post-compaction revoke. A failed post observer blocks later tools locally until a retry succeeds. |
| Pi | session_start with reason reload, new, resume, or fork |
Atomically advance the private generation, then revoke the replacement incarnation before reuse. |
The first observer that reaches the broker deletes pending promotion tokens and makes the lease UNVERIFIED. The second compaction observer is deliberate redundancy, not a prerequisite for the first. Claudex receives the same mandatory hooks in its isolated CLAUDE_CONFIG_DIR; hook merging preserves unrelated isolated settings and rejects malformed or symlinked settings fail-closed.
Private generation authority
launch-runtime.py still registers before exec, preserving the kernel-authenticated PID/starttime anchor. It now also creates generation-<broker-session>.state beside the broker socket. The file is owner-only mode 0600 under the broker's mode-0700 directory. Hook descendants read that file instead of relying only on an immutable inherited environment value.
Generation changes use an exclusive file lock, validate owner/type/mode/size, increment monotonically, truncate and write the complete new value, and fsync before contacting the broker. Therefore reload, new-session, resume, and fork events may retain the same PID/starttime while still becoming a new broker incarnation. The higher generation causes the broker to atomically discard prior tokens and lease authority; the replacement generation inherits no VERIFIED lease.
If an observer fires while broker transport is unavailable, revoke-lease.py advances the private generation as a local fence before returning non-zero. Every later all-tools gate reads that higher value. When the broker is reachable again, authentication of that value performs the same old-generation revocation before authorization. Pi also keeps a process-local post-compaction/rollover failure latch that blocks tool calls. An unsafe or unreadable generation file itself makes both lifecycle revocation and tool authorization fail closed.
Threat contract and stopping boundary
BOUNDED RESIDUAL STALE WINDOW
If both pre- and post-compaction observers are missed entirely, no revocation signal exists. During the remaining unexpired lease, within-TTL consequential actions are allowed. Their count and timing are bounded by lease expiry, not by the mutator gate. WI-3 makes no claim that it bounds mutator actions inside this stale interval. The broker's monotonic lease TTL is capped at 300 seconds; after expiry, the next consequential tool is denied with LEASE_EXPIRED.
This is the named D2-v5 T-A residual. It is distinct from an observer that fires but cannot contact the broker: the latter creates a local generation fence and fails closed. It is also distinct from T-C total rot, where the lifecycle observers and the all-tools gate are both absent or replaced. Server-side branch protection, required CI, and independent review remain the irreducible backstop for T-C.
| Condition | Result |
|---|---|
| Either compaction observer succeeds | Existing lease and pending promotion tokens are revoked immediately. |
| Observer runs but broker confirmation fails | Lifecycle transition is denied where supported; local generation fence and runtime latch prevent inherited authority. |
| Both observers are missed, lease unexpired | ALLOWED inside the bounded residual stale window. No within-window mutator bound is claimed. |
| Both observers are missed, lease expired | DENIED by monotonic TTL expiry. |
| Generation advances on reload/new/resume/fork | Prior incarnation revoked; replacement starts UNVERIFIED. |
| Lifecycle observers and all-tools gate both fail or are removed | T-C total-hook-miss residual; protected-branch controls remain required. |
The permanent T12b/T30 acceptance case prints both required outcomes: dual-hook miss within TTL is ALLOWED, and the same lease after TTL is DENIED. Separate real-socket tests prove each Claude observer and same-PID generation rollover; Pi lifecycle tests exercise pre/post observers, all four replacement reasons, and local failure closure.