Files
stack/docs/guides/lease-broker-operations.md
jason.woltje 2509eb7646
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
WI-6 (#833): constrained recovery command + mosaic-context-refresh skill wrapper (#846)
2026-07-20 03:33:00 +00:00

6.6 KiB

Lease broker operations

Place the socket and state file in a dedicated directory with mode 0700. Start the packaged daemon with:

python3 "$MOSAIC_HOME/tools/lease-broker/daemon.py" \
  --socket /run/user/1000/mosaic-lease/broker.sock \
  --state /run/user/1000/mosaic-lease/state.json

The broker refuses an existing parent directory whose mode is not exactly 0700, an existing state file not at 0600, corrupt/incompatible state, or an already-existing socket path. After bind it sets the socket to 0600. It never silently unlinks a pre-existing socket. On normal termination it unlinks only the socket inode it created, so it does not remove a replacement path.

Before launching Claude, Claudex, or Pi, export the socket path; mosaic then runs the runtime through the packaged register-and-exec wrapper:

export MOSAIC_LEASE_BROKER_SOCKET=/run/user/1000/mosaic-lease/broker.sock
mosaic claude # or: mosaic claudex, mosaic yolo claudex, mosaic pi

The wrapper obtains a broker-minted session ID, creates a private generation-<session>.state file beside the socket, and execs the runtime without changing its PID/starttime anchor. The all-tools Claude PreToolUse hook and Pi tool_call handler inherit that identity and read the current generation from the file. Claudex retains its isolated proxy environment and config directory; Mosaic merges the mandatory all-tools and compaction-lifecycle hooks into that isolated settings.json before invoking the same wrapper. PRDY init/update, QA remediation, coord, orchestrator, and fleet launchers also converge on this boundary. Broker registration failure, unsafe isolated settings, unsafe generation state, or missing identity denies launch/tool execution fail-closed; broker timeout/unavailability and malformed replies also block tools.

Claude PreCompact and SessionStart(compact) hooks and Pi pre-/post-compaction handlers invoke revoke-lease.py. Pi session_start reload/new/resume/fork and Claude resume/clear advance the locked generation before revocation, so a replacement session inherits no lease even when PID/starttime stay unchanged. Do not invoke the revoker manually as a way to restore authority; it only removes authority. If a lifecycle hook reports failure, stop consequential work and repair broker/generation-state availability before re-verification.

Run the permanent launch inventory locally with:

python3 packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py --root .

The same check runs in the Mosaic package test suite and therefore in root CI. Any direct Claude/Pi binary launch must be replaced with launch-runtime.py, execLeaseGatedRuntime, or the gated mosaic runtime command; do not add static allowlist exceptions.

Clients must complete the request boundary before waiting for a reply. After sending the single JSON object and its terminating newline, the client MUST half-close the socket's write side (shutdown(SHUT_WR) in POSIX clients; socket.end() in Node) and only then await the response. Merely calling write() and waiting is invalid: the broker waits for EOF to enforce the exact-one-frame contract and fails closed at its one-second deadline. Do not replace end() with write() in client helpers. A delayed second frame remains malformed and is rejected.

mosaic_context_recover is the only unverified mutator class. Its durable mosaic-context-refresh skill is a thin wrapper over tools/lease-broker/recover-context.py: begin has the broker rebuild the validated B_payload/H_payload, revoke first, and mint a new PENDING_DELIVERY receipt challenge; complete accepts neither receipt text nor a challenge argument. Claude maps only the exact direct recovery executable/validated arguments to this exempt tool identity; ordinary Bash remains gated. Pi exposes only the mosaic_context_recover custom tool; ordinary bash and all other tools remain gated. A normal-path receipt cannot be replayed through recovery because each retry begins a distinct recovery cycle and recovery completion cannot receive caller-presented evidence.

Production daemon startup creates a separate private observer socket unless a test-only --test-observer-file fixture is selected. Claude's Stop hook sends its exact latest assistant entry and Pi's message_end handler sends only finalized assistant content to that authenticated transport; the broker public socket never accepts message text. This is byte-build and private out-of-process harness wiring only: do not activate it against a live daemon, live socket, systemd service, tmux session, or model-output stream outside the controlled integration procedure.

Receipt honesty is load-bearing: absent, malformed, prefix-truncated, and observable adapter-mutated terminal receipts do not promote. A tail-only case is non-promoting only where the concrete terminal payload is malformed or observably incomplete. A tail-preserving middle drop is not receipt-detectable; it is the disclosed T-C injection-contract residual deferred to WI-7 server-side evidence. The receipt remains a T-A delivery/liveness prerequisite, never a safety, obedience, or residency proof. The framework skill is source-resident and bridge-projected on install/upgrade; do not hand-create a live runtime symlink.

After a runtime exits, its generation-<session>.state file may be removed only after verifying that no process for that broker-minted session remains; stale files carry no lease authority but should be retained during incident analysis. After a broker crash, preserve the protected state file and restart only after verifying that no broker owns the socket. Restart intentionally clears all volatile VERIFIED leases. A leftover socket requires an operator to verify the owning service is stopped and remove that exact socket deliberately. Corrupt, oversized, symlinked, or non-regular state fails closed; do not overwrite it. Preserve it for incident review and establish new state only through an explicit operational decision, which invalidates prior sessions and tokens.

Security posture

Directory 0700 plus socket/state 0600 is built-in same-principal hardening only: it excludes other UIDs but does not stop the same UID from unlinking and counterfeiting the socket. It therefore does not close T-C same-UID replacement. WI-1 does not provide a distinct-principal boundary. A stronger distinct-principal deployment requires an external protected proxy, ACL, or service boundary that clients cannot unlink or rebind and that preserves the authenticated client identity required by the broker's SO_PEERCRED and ancestry checks. Server-side branch protection remains the irreducible backstop.