Specify M3a (the decision path) and write the runbook
Split M3 into M3a and M3b, record the M3 design decisions, propose P13 (tool image built by Nix, named by digest), and add docs/runbook.md with an entry for every fail-closed state, including loopd's existing ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-2
@@ -6,6 +6,15 @@ Newest first. A decision that changes `docs/design.md` lands in the same commit
|
||||
|
||||
| Date | Decision | Reason |
|
||||
|---|---|---|
|
||||
| 2026-09-18 | M3 is split. M3a is the decision path: grants and matching, session taint, the hash-chained audit log, approvals through `bxctl`, the `broker.sock` and `admin.sock` protocol, and `loopd`'s `BrokerPort`; tools do not run (the runner is a trait with a refusing implementation). M3b is the Podman runner, the four tools and the egress proxy. Review happens after each. | The security logic is reviewed before real tools run on it, as with M2a and M2b. Spec `docs/specs/2026-09-18-m3a-decision-path.md`. |
|
||||
| 2026-09-18 | Grant loading fails closed: one invalid grant file denies every call (`grants_invalid`) until it is fixed. M3 grants may not set `secret` or `patterns`; each tool has fixed rules for which constraints it takes. Among matching grants the most restrictive mode wins (deny, ask, auto). An approval re-decides against the current grants and taint, and runs only if the outcome is still ask or auto. | A skipped, mistyped `deny` grant would silently become an allow. Pattern-matching shell commands is a false comfort; the container is the boundary. |
|
||||
| 2026-09-18 | Every fail-closed state has an entry in `docs/runbook.md` with what you see, why, how to confirm, how to fix and how to check; its message ends with `see docs/runbook.md#<entry>`, and a gate script checks that every referenced entry exists. | Owner's requirement: a refusing system must come with clear, actionable remediation. |
|
||||
| 2026-09-18 | `brokerd` listens on two sockets: `broker.sock` (tool requests only, for `loopd`) and `admin.sock` (approvals and grant checks, for `bxctl`). Each refuses the other's messages with `forbidden`. Replaces the single `broker.sock` of the pre-M1 spec. | Anything that could reach the approving socket could approve its own calls. Until M7 all roles run as the owner's user, so the split is enforced in code; M7 enforces it with mounts. |
|
||||
| 2026-09-18 | The audit log records events (`decision`, `approval`, `result`, `recovery`, `accepted_break`) in one chain. A decision is on disk before anything runs; results are recorded by hash and size, not content. A broken chain stops `brokerd` until the owner runs it once with `--accept-break`, which records the break; nothing is ever repaired or deleted. Chain verification is a pure function in `proto` shared by `brokerd` and `bxctl audit verify`. Approval ids are the `seq` of the decision record. | A secret read must not be copied into the audit log. Verification must work when `brokerd` refuses to start. |
|
||||
| 2026-09-18 | Secrets move to M4. In M3 a grant that sets `secret` is invalid. | None of M3's tools needs a secret; the first is the Mattermost bot token. |
|
||||
| 2026-09-18 | Until M7, `brokerd` and its containers run as the owner's user through rootless Podman; a container escape is the owner's user. Containers are hardened (`--network=none` unless granted, `--read-only`, `--cap-drop=all`, `no-new-privileges`, process and memory limits). | Separate users per role are M7's work. Measured on straylight: rootless Podman 5.8.6 with crun, 40 to 80 ms per container. |
|
||||
| 2026-09-18 | Tool containers run from one OCI image built by Nix (`dockerTools`) holding static `toolkit`, `busybox`, `curl` and the CA bundle, loaded with `podman load` and named by digest. No registry pull at call time. The owner's Gitea registry is the route when there is more than one host. | Pinned contents with no egress. Nix already builds the three binaries statically. |
|
||||
| 2026-09-18 | `http_fetch` runs `curl` in a container with no network, through a per-call SOCKS5 proxy in its own container on a mounted Unix socket. The proxy is ours (in `toolkit`), accepts host names only, checks each against the grant's hosts, and copies bytes; TLS stays end to end in the tool container. `clock` moves into `loopd`. | No TLS stack of our own. Redirects are checked at the proxy. Measured on straylight 2026-09-18: allowed host 200 in 0.26 s; other host and IP literal refused. |
|
||||
| 2026-09-18 | M2b: the baseline (system prompt, core tool schemas, `memory/core.md`) is snapshotted per epoch into `sessions/<id>/<epoch>.baseline.json`; resume uses the snapshot, so edits apply only to later sessions. The agent is called Boxmaker; the first `system.md` is drafted by the design model and edited by the owner. A `Usage` log record follows each `Assistant` record. `bxctl chat` shows reasoning dimmed by default. The core tool set is `clock`, `find_tool`, `call_tool`; `echo` is the first discoverable tool. | Owner's choices during the M2b design review; spec `docs/specs/2026-09-18-m2b-agent-loop.md`. Dimmed thinking shows progress and helps spot stalls. |
|
||||
| 2026-09-17 | A compromised `loopd` can degrade the shared `llama-server` for other clients (large prompts, unpinned requests). Accepted for v0 and written into the threat model. `inferproxy` stays a byte forwarder with a connection cap and an accept-rate limit; it does not enforce slot or model policy. If isolation is wanted later, do it on the server side. | The harm is availability only, the owner would notice, and a policy proxy would put a parser for untrusted input into the component meant to have none. |
|
||||
| 2026-09-17 | M2a limits: 10 min wait for a busy slot, 3 min for a model load, 30 s liveness after the first byte, thinking cap 4,096 tokens with `max_tokens` 8,192 as backstop. All are config values. The slot gate is held per request, never across a tool call. | Owner's choices during the M2a design review; measurements (j) to (n) in `docs/inference-contract.md`. |
|
||||
@@ -31,7 +40,7 @@ Newest first. A decision that changes `docs/design.md` lands in the same commit
|
||||
| Thinking cap size. It starts at 4,096 tokens so that the right size can be found from data. From M2b every completion logs its reasoning-token count and whether the cap fired. Revisit once 200 completions are logged, or sooner if the owner sees answers cut short: raise the cap if more than 5% of completions hit it. Each hit also costs a re-read of that turn. | after M2b has run for a while |
|
||||
| Repository licence. Crates are `publish = false` until it is chosen. | before publishing |
|
||||
| Audit integrity beyond a self-contained hash chain (for example anchoring the chain head outside `brokerd`). Deferred by the owner on 2026-09-17 to a later revision. | after v0 |
|
||||
| Secret store backend, and where the v0 file's key lives. straylight has no secrets manager today. | M3 |
|
||||
| Secret store backend, and where the v0 file's key lives. straylight has no secrets manager today. Moved from M3 on 2026-09-18. | M4 |
|
||||
| Whether cloud-led sessions are ever allowed, and for which data classes. | M6 |
|
||||
| Where the M5b embedding model runs. The brief says a separate `llama-server` instance, but under the shared router it would be a third model against `--models-max 2` and could unload Ornith or Laguna. Owner's note, 2026-09-17: more compute is likely by then (possibly one to three DGX Spark machines), so do not design around today's memory limit. | M5b |
|
||||
|
||||
@@ -62,4 +71,6 @@ Later changes to the brief:
|
||||
|
||||
## Proposed changes to the design brief
|
||||
|
||||
None pending.
|
||||
| # | Change | Reason |
|
||||
|---|---|---|
|
||||
| P13 | Authority contract 4: add "Tool containers run from one image built from source by Nix and named by digest; nothing is pulled at call time." | The brief does not say where tool images come from; a pull at call time would be unlisted egress. Decided 2026-09-18 above. |
|
||||
|
||||
Reference in New Issue
Block a user