feat(#828): add authenticated lease broker
Bind broker sessions to Linux SO_PEERCRED and /proc starttime ancestry, fence runtime generations, persist cryptographic single-use cycle tokens, and enforce protected Unix socket/state posture.\n\ncloses #828
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Documentation Sitemap
|
||||
|
||||
## Compaction refresh lease broker
|
||||
|
||||
- [Internal broker protocol](architecture/lease-broker-protocol.md) — kernel identity, ancestry and generation invariants, framed requests, responses, and persisted cycle bindings.
|
||||
- [Broker operations](guides/lease-broker-operations.md) — protected paths, startup, fail-closed recovery posture, distinct-principal deployment, and residual risk.
|
||||
- [WI-1 security notes](architecture/lease-broker-security.md) — threat-boundary summary and coordinator review requirements.
|
||||
|
||||
## CLI and skill management
|
||||
|
||||
- [Skill registration user guide](guides/user-guide.md#claude-code-skill-registration) — register, unregister, list statuses, automatic install/update reconciliation, and Claude reload behavior.
|
||||
|
||||
18
docs/architecture/lease-broker-protocol.md
Normal file
18
docs/architecture/lease-broker-protocol.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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`.
|
||||
|
||||
A higher generation for the same anchor atomically replaces the stored incarnation and consumes all prior tokens for that session. A lower generation is stale. Tokens are 256-bit values from the operating-system cryptographic RNG and are single use. Their persisted binding is the WI-1 substrate for later receipt work; WI-1 does not implement receipts, promotion, hooks, payload builders, mutator gates, or recovery.
|
||||
|
||||
State replacement uses a mode-`0600` temporary file, `fsync`, atomic rename, and parent-directory `fsync`. 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. 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.
|
||||
11
docs/architecture/lease-broker-security.md
Normal file
11
docs/architecture/lease-broker-security.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# WI-1 lease broker security notes
|
||||
|
||||
- Trusted identity comes only from Linux `SO_PEERCRED` plus `/proc` starttime, never request identity fields.
|
||||
- Descendant authorization is anchored to `(pid,starttime)` and uses a complete second starttime pass to fail closed on disappearance or PID-reuse races.
|
||||
- Runtime generations are monotonic per anchor; a bump revokes prior-incarnation tokens before persistence commits.
|
||||
- Session IDs and cycle tokens use the OS cryptographic RNG. `Math.random` and model output are not token sources.
|
||||
- Framing and persistence failures fail closed. Sensitive tokens are not logged.
|
||||
- Same-principal filesystem modes are minimum hardening, not socket authenticity against the same UID. Distinct-principal service isolation is required for that stronger claim.
|
||||
- WI-2+ security surfaces—receipts, promotion transactions, mutator gates, hooks, payload construction, and recovery—are explicitly out of scope.
|
||||
|
||||
Coordinator security review must rerun the real socket/peercred acceptance suite on an unrestricted Linux runner and obtain the mandated independent Opus-SECREV review before integration.
|
||||
19
docs/guides/lease-broker-operations.md
Normal file
19
docs/guides/lease-broker-operations.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Lease broker operations
|
||||
|
||||
Place the socket and state file in a dedicated directory with mode `0700`. Start the packaged daemon with:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
There is no automated recovery workflow in WI-1. After a crash, preserve the protected state file and restart only after verifying that no broker owns the socket. 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 minimum same-principal hardening: 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. The stronger deployment runs the broker as a distinct principal behind a protected service boundary whose clients cannot unlink or rebind the socket; that is the T-C-closing option. Server-side branch protection remains the irreducible backstop.
|
||||
66
docs/scratchpads/828-lease-broker.md
Normal file
66
docs/scratchpads/828-lease-broker.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# WI-1 Scratchpad — Authenticated external lease broker
|
||||
|
||||
- **Issue:** Gitea #828
|
||||
- **Milestone:** 188 — Compaction-Refresh Mechanism (M1: Claude + Pi)
|
||||
- **Branch:** `feat/828-lease-broker`
|
||||
- **Starting HEAD:** `d801d6c4c8a984d6a95033c49714210018d3d9a8`
|
||||
- **Session role:** Orchestrator coordinating implementation; Mos retains merge authority.
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the ratified WI-1 product lease broker under `packages/mosaic/`: Linux `SO_PEERCRED` identity, broker-minted logical session IDs, `(pid,starttime)` launcher anchors with per-hop `/proc` starttime revalidation, sibling-substitution rejection, same-PID runtime-generation revocation, crypto-RNG single-use token persistence, and protected Unix-socket posture.
|
||||
|
||||
## Authority verification
|
||||
|
||||
Verified before code on session start; all exact SHA-256 values matched:
|
||||
|
||||
- BUILD-BRIEF: `89fdbc27ed0e5050dc7b52f3ef2ddaea691edf17fd89d51b15e26fb5ed47171b`
|
||||
- SPEC-v5: `a6d07ade835758e8488ca10d3b0631caf0beb93ea3a6733631f151b0c2f01433`
|
||||
- Ratification: `bac58319c9c4028b5b40e1129e0033cdb5a6b7b02033c25f06f4cb77d7779c67`
|
||||
- P6 planner ruling: `b7bbb6ea6e8d9a5c3366993642ab4e4f65b961af04936dcac20bfbcdcbaf1a09`
|
||||
- WI-0 Gate0 evidence: `5d418306fcc597fd514e500bee40d1509f0bf467e46ee13fc5c280ed8274759d`
|
||||
|
||||
## Locked constraints
|
||||
|
||||
- Build against the ratified design; do not re-derive it.
|
||||
- Product code only in `packages/mosaic`; Gate0 Python probes are reference prototypes and are not shipped.
|
||||
- Caller-supplied/asserted `session_id` is refused.
|
||||
- Tokens use the operating-system CSPRNG via Python `secrets`; never `Math.random` or model output.
|
||||
- Socket parent directory mode `0700`, socket mode `0600` minimum; document distinct-principal deployment as the stronger T-C-closing posture.
|
||||
- Red-first TDD for six named cases; new-code coverage >=85%.
|
||||
- No merge. PR must say `closes #828`; exact 40-character head handed to Mos for Opus-SECREV and independent review.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Load security/testing/docs guidance and inspect existing `packages/mosaic` architecture.
|
||||
2. Write the six required tests first and capture RED evidence.
|
||||
3. Implement minimal broker modules and CLI/runtime integration necessary for product use.
|
||||
4. Run focused tests with coverage, package gates, then full repository gates/suite.
|
||||
5. Run author-side review/remediation, commit `closes #828`, queue guard, push, and open PR through Mosaic wrappers.
|
||||
6. Send PR number + exact head SHA to `web1:mosaic-100`; stop without merging.
|
||||
|
||||
## Risks / boundaries
|
||||
|
||||
- Same-UID counterfeit socket replacement remains the disclosed T-C residual unless broker runs under a distinct principal; filesystem modes alone are minimum hardening, not a complete authenticity proof.
|
||||
- `.mosaic/orchestrator/mission.json` and `.mosaic/orchestrator/session.lock` were already modified at session start and must not be included in this PR.
|
||||
- Repository Woodpecker pipelines exist; CI is the canonical build path. No manual image build/deploy is in scope.
|
||||
|
||||
## Progress / evidence
|
||||
|
||||
- 2026-07-18 session start: mandatory mission files and orchestration guides loaded.
|
||||
- STEP 0: all four authority hashes matched; artifacts read in full.
|
||||
- Branch/HEAD confirmed; issue #828 open; Gate0 evidence hash confirmed.
|
||||
- Initial RED: focused Vitest acceptance suite failed 11/11 because the product daemon did not exist; the expected missing-product failure was observed before implementation.
|
||||
- Review-remediation RED: partial/zero-progress state writes, nested corrupt state, symlink state, canonical starttime, and duplicate-anchor generation behavior failed before their fixes. Real socket RED/GREEN runs were executed by the unrestricted parent harness because the delegated worker sandbox denies `AF_UNIX.bind()`.
|
||||
- Product implementation added at `packages/mosaic/framework/tools/lease-broker/daemon.py`; Gate0 probe scripts were read as references but not copied or shipped.
|
||||
- Independent Codex code review round 1 found 2 blockers + 1 should-fix (connection stall/crash, partial writes, packet-dependent framing); all were remediated with tests.
|
||||
- Independent Codex code review round 2 found 2 blockers + 1 relevant should-fix (half-close contract ambiguity, incomplete persisted-state validation, symlink/non-regular state); all were remediated with tests and documentation. Pre-existing `.mosaic/*` session dirt remains excluded from the PR.
|
||||
- Unrestricted focused situational suite: `35/35` GREEN.
|
||||
- New Python product module coverage: `90%` (`356` statements, `36` missed), above the user-required 85%.
|
||||
- Root typecheck and lint gates passed after remediation; final package/full-suite/format evidence is recorded below before push.
|
||||
|
||||
## Coordinator handoff requirements
|
||||
|
||||
1. Mandatory Opus-SECREV on the exact PR head; no GPT/terra substitute.
|
||||
2. Independent exact-head code review and exact-head RoR before Mos-authorized merge.
|
||||
3. Mos retains merge authority; this WI author stops after PR + full 40-character head handoff.
|
||||
Reference in New Issue
Block a user