Files
stack/docs/architecture/compaction-revocation.md
jason.woltje 0582a8912b
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
WI-7 #834: T-C server-side branch-protection posture + R1 honesty amendment (#847)
2026-07-20 04:20:02 +00:00

60 lines
7.7 KiB
Markdown

# 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. |
## T-C server-side branch-protection posture
The required posture is that `main` is push-blocked and PR-only-merge is **MANDATORY**, regardless
of client-gate state. The client-side gate narrows the exposure window only; it is not the T-C
guarantee. The server-side protected-branch configuration is the irreducible guarantee for protected
repository actions. Status-check enforcement and approval enforcement are **RECOMMENDED**.
## Current-vs-required gap (recorded, not enacted)
The current empirical configuration is recorded here without re-probing or mutating live branch
protection. `enable_push=False` (push-block present), so the mandatory push-block/PR-only-merge core
holds. `require_approvals=0` (approvals not enforced), `enable_status_check=False` (status checks not
enforced), and `block_on_official_review=False` (official review not enforced). Those recommended
merge-quality controls are the current gap; changing them is a separate, owner-gated operations
decision and is not enacted by this documentation change.
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.