Files
stack/docs/architecture/lease-broker-protocol.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

7.0 KiB

Authenticated external lease broker protocol

The compaction-refresh lease broker is a Linux-only, newline-framed JSON protocol over a Unix stream socket. It is runtime-neutral; M1 consumers are limited to Claude and Pi. This is an internal process boundary, not an HTTP API, so it is intentionally absent from OpenAPI.

The broker, never the caller, obtains (pid, uid, gid) from kernel SO_PEERCRED. It correlates the PID with /proc/<pid>/stat field 22 (starttime) and mints session_id on register_anchor. Presence of session_id in that request is refused even when its value is null or empty. Later requests must originate from the anchor or a descendant. The broker walks parent PIDs to the (pid,starttime) anchor and then rereads every walked PID's starttime before accepting the chain.

Request and response boundary

Each connection carries exactly one UTF-8 JSON object followed by one newline, capped at 64 KiB. The protocol deliberately uses EOF to prove that there is exactly one frame: immediately after writing the newline, the client MUST half-close its write side with shutdown(SHUT_WR) (or Node socket.end()) before awaiting the response. A client that writes a newline but leaves its write side open receives no successful response; the broker's one-second connection deadline fails closed. Malformed, unterminated, multiple (including a delayed second frame), or oversized frames fail closed. Responses are one JSON object and one newline. Success has {"ok":true,...}; refusal has {"ok":false,"code":"TYPED_CODE"}. Requests are:

  • register_anchor: action, non-negative runtime_generation; no session_id field.
  • authenticate: action, broker-minted session_id, non-negative runtime_generation.
  • mint_token: authenticated identity plus binding containing exactly compaction_epoch, request_epoch, h_source, h_payload, and schema_version.
  • consume_token: authenticated identity plus token.
  • begin_verification: authenticated identity, runtime (claude or pi), cycle binding, and a TTL no greater than 300 seconds. The broker revokes existing authority first, enters PENDING_VERIFICATION, and returns a single-use promotion token.
  • begin_recovery: the constrained recovery entrypoint. It rejects caller-provided receipt/challenge fields and delegates to the same begin_verification transition, but reports PENDING_DELIVERY and marks the volatile cycle as recovery-owned.
  • complete_recovery: authenticated identity only. It rejects caller-provided receipt/challenge fields, obtains the current recovery challenge only from broker state, and delegates to the same trusted-observer → evidence → consume → promote sequence. An observation failure revokes recovery authority; retry starts a fresh challenge.

The daemon owns a second protected production observer socket (mode 0600) unless a private --test-observer-file fixture is selected. That transport accepts only the exact record_runtime_observation schema after kernel SO_PEERCRED plus the existing anchor/ancestry authentication; it validates the pending runtime/generation before storing one finalized assistant entry for the in-process RuntimeReceiptObserver. It is not a broker request action. Claude sends its latest assistant entry from the Stop-hook transport; Pi sends only finalized message_end assistant content. The public broker socket continues to reject request-supplied latest_assistant_message in begin, observe, and complete paths.

  • promote_lease: authenticated identity plus the exact pending promotion token. The broker commits token consumption before making VERIFIED visible.
  • revoke_lease: authenticated observer signal; deletes pending tokens and makes the session UNVERIFIED immediately. WI-3 Claude/Pi hooks send this existing action; runtime and bounded reason fields are diagnostic input only and never identity authority.
  • authorize_tool: authenticated identity, runtime, and exact runtime-reported tool name. The broker returns an explicit allow/deny decision from the whole-class policy and current lease.

A higher generation for the same anchor atomically replaces the stored incarnation and deletes all prior tokens and lease authority for that session. A lower generation is stale. Runtime descendants resolve the current generation from an owner-only, locked generation file created by the register-before-exec launcher; reload/new/resume/fork observers advance and fsync it before broker revocation. This supports generation replacement even when PID/starttime do not change. Tokens are 256-bit values from the operating-system cryptographic RNG and are single use. At most 256 pending tokens may be persisted; another mint fails with TOKEN_CAPACITY before mutation. Successful consumption deletes the token, while a replay still fails with TOKEN_REPLAY. Live v1 token records retain the existing consumed: false schema.

VERIFIED leases are volatile and monotonic-time bounded: broker restart, generation change, explicit observer revocation, or expiry returns the session to UNVERIFIED. begin_verification always revokes before minting a new prerequisite. begin_recovery reuses that exact transition and mints a new challenge, so a normal-path receipt/challenge cannot be replayed through recovery. promote_lease is valid only from the matching pending cycle; persistence failure rolls token and lease state back, while post-rename durability uncertainty terminates the broker. The WI-1 token is the atomic promotion prerequisite substrate.

Receipt boundary and T-C residual (R1)

Receipt evidence is a T-A delivery/liveness prerequisite only; it cannot replace the mechanical mutator gate as safety authority. The receipt detects an ABSENT or PREFIX-TRUNCATED terminal token. A MIDDLE-DROP that preserves the tail is a T-C contract violation that is NOT receipt-detectable. It is covered by server-side protected-branch controls, NOT by the receipt; no category-wide receipt-detection claim is made for that tail-preserving transformation.

State replacement serializes and enforces the 4 MiB maximum before opening a temporary file, then uses a mode-0600 temporary file, fsync, atomic rename, and parent-directory fsync. Every broker mutation snapshots the prior v1 state. A commit failure before rename restores that snapshot and leaves durable state unchanged. A failure after rename makes durability uncertain, so the store is poisoned without rolling memory back and the daemon terminates rather than serving with divergent state. Existing state is opened without following symlinks, must be a bounded regular file at mode 0600, and is fully schema- and invariant-validated before use. Persisted tokens must be unconsumed, match their session's current generation, and remain within the 256-token cap. Session identity is uniquely keyed by (anchor_pid,anchor_starttime); duplicate logical sessions for one anchor refuse startup. State integrity or mode failures refuse startup. The daemon does not log session IDs or tokens.