# Decisions Newest first. A decision that changes `docs/design.md` lands in the same commit as the change. ## Decided | Date | Decision | Reason | |---|---|---| | 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`. | | 2026-09-17 | SHA-256 comes from the owner's `emsha` crate, version 1.0.4 or later, not `sha2` and not hand-written code in this repo. It is wrapped behind one function in `proto`, whose tests carry their own vectors: `abc`, the million-`a` message, and lengths 55, 56, 63, 64 and 65. The crate's custom licence is not an issue: the owner is its author. | No dependencies, no `unsafe`, `no_std`, owner-maintained. Version 1.0.3 hashed every message of length 63 mod 64 wrongly; a differential test against `sha256sum` found it while the crate was being vetted, and the owner fixed it in 1.0.4 (3,204 cases pass). | | 2026-09-17 | M2 is split. M2a is the inference path (`inferproxy`, HTTP and SSE client, llama client, fake server, startup self-test). M2b is the agent loop (session log, turn loop, channel protocol, `bxctl chat`). Review happens after each. | The risky, timing-dependent work gets reviewed before the turn loop is built on it. | | 2026-09-17 | Tool results are untrusted by default: each grant has `untrusted`, default true, and `brokerd` tracks a per-session untrusted flag beside taint. Review of Laguna's work is once per milestone, as an experiment for M1 to be revisited after the M1 review. Laguna keeps `docs/implementer-log.md`. | Owner's choices during the pre-M1 design review. Details in `docs/specs/2026-09-17-pre-m1-design.md`. | | 2026-09-17 | Threat model: the main adversary is injected text steering the model. Secondary: any one role process or tool container is compromised, and the goal is containment. `brokerd` trusts nothing `loopd` reports beyond the request itself and tracks session taint on its own. | Owner's choice. It matches the role split the brief already has. | | 2026-09-17 | Mattermost and the tailnet are trusted. A Mattermost compromise presumes the whole machine is compromised and is out of scope. Forged approvals are therefore out of scope, and there is no per-grant approval-path field. An accidental secret or PII leak into Mattermost is an accepted risk in v0. | Owner: Mattermost sits behind a single-user tailnet with ACLs. | | 2026-09-17 | Data classes: every session starts at `private`. A `secret` result raises it to `secret`. `public` is a provenance label and never lowers a session. The grant that authorises a call names the class of its results (default `private`); tools never label their own output. A grant's allowed data classes are the highest session taint under which it applies. Unattended egress is controlled by grant mode, not by lowering the floor. | `memory/core.md` is in every baseline, so no session is really public. Job-scoped grants are left to M5. | | 2026-09-17 | IPC: Unix stream sockets, strict JSON bodies in frames with a 4-byte length prefix and a size cap checked before allocation. Unknown fields rejected, tagged enums, no floats, golden-file fixtures as the wire spec. CBOR and protobuf were considered. | Every role needs a JSON parser anyway (llama-server, Mattermost, model-written tool arguments), so a binary envelope adds a parser without removing one. Both ends share the `proto` crate. | | 2026-09-17 | Each daemon listens on one socket named after itself (`infer.sock`, `broker.sock`, `loop.sock`, `gateway.sock`). Peer authentication is by directory permissions per pair of roles. Blocking I/O with threads; no async runtime. | Small audited dependency tree, and fewer ways for the implementing model to go wrong. | | 2026-09-17 | Audit and session logs stay JSONL. Type safety comes from the `proto` record types and strict decoding; the files are never built by string formatting. Writes go through one writer module so the format can change later. | Volume is one record per tool decision; `fsync` and inference dominate the cost by orders of magnitude. | | 2026-09-17 | Boxmaker uses the shared `llama-server` router and the shared Ornith instance. No dedicated instance. `inferproxy` stays. | The owner runs coding agents against the same router, and a second resident copy of Ornith does not fit beside Laguna. | | 2026-09-17 | The harness runs on straylight, the same host as `llama-server`. | Owner's choice. One host to secure, and no inference traffic crosses the tailnet. | | 2026-09-17 | Container runtime is rootless Podman. | It is already the standard runtime on straylight and the owner's other hosts. | | 2026-09-17 | Implementation is done by Laguna S 2.1, served by straylight, through OpenCode on the owner's development machine; builds are then deployed to straylight. Design, specs, measurement and review are done by a stronger model. Ornith-1.5-35B-A3B remains the model the harness serves. | The owner wants to test a local model on real implementation work. Plans must be written as small closed tasks with tests specified up front. | | 2026-09-17 | M0 is run by the design model, not by Laguna. | M0 is measurement and interpretation, and its findings bind the design. | ## Open | Decision | Needed by | |---|---| | 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 | | 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 | ## Applied to the design brief Approved by the owner on 2026-09-17. One commit each. | # | Change | Evidence | |---|---|---| | P1 | Inference contract 6: the `tools` array is fixed per epoch. `find_tool` returns schemas as a tool result and the model calls them through a `call_tool(name, arguments)` meta-tool. Never instruct the model to call an undeclared tool. | M0 (h): adding a tool re-read the whole prompt; undeclared calls were coerced into `write_file`. | | P2 | Inference contract 2: the session log stores assistant messages exactly as returned, including `reasoning_content`, and replays them unchanged. Remove the "known risk" about dropped thinking blocks. | M0 (e): Ornith's template keeps every think block. | | P3 | Inference contract 7: requests set `return_progress: true`; progress events count as liveness. | M0 (i): otherwise the stream is silent during prefill. | | P4 | Inference contract 8: the thinking cap uses `reasoning_control` and the control endpoint. | README b10809. Exercised 2026-09-17, `docs/inference-contract.md` (k): it works, and a forced end costs a re-read of that one turn. | | P5 | Settle the open question: chat-completions with server-side tool parsing. | M0 (b), (c). | | P6 | Code constraints: `Decision` lives in `brokerd`, has a private field and does not implement `Deserialize`. `proto` carries a plain `DecisionRecord` for the audit log. | Rust privacy is per crate, and a deserializable type can be built by anyone. | | P7 | Inference contract 1: the baseline budget test needs the server's tokenizer, so `make gate` has an offline part and an on-device part (`make verify-device`). | `/tokenize` is a server endpoint. | | P8 | Target environment: describe the shared router as deployed (router mode, `--models-max 2`, other clients on the same Ornith instance, q8_0 KV, six-hour idle unload). Drop "memory is abundant". | `docs/inference-contract.md`, "What is running". | | P9 | Inference contract 5: slots are pinned with `id_slot` but not reserved. Another client can evict a harness slot, and the router can unload Ornith. Cache loss is an expected event: `loopd` detects it (`cache_n` far below the previous request's total), logs it in the session log, and carries on. It is never an error. A session always uses the same slot; threads may share the main slot. | Shared router decision. M0 (d): evicted sessions came back from the host-RAM prompt cache in under a second on the same slot, and cost a full re-read on another slot. | | P10 | Inference contract 7: a pinned request can queue behind another client's request on the same slot, and the router can spend tens of seconds reloading the model. The liveness timer starts at the first byte received, and a separate, longer "waiting for slot or model" limit covers the time before it. | Shared router decision. Measured 2026-09-17, `docs/inference-contract.md` (j): a queued request receives no bytes at all; `/slots` shows whether the slot is busy. | | P11 | Inference contract 10: the serving flags live in `~/src/nixos`, not here. This repo records the expected values (template hash, per-slot `n_ctx`, slot count, KV type, sampling) and the startup self-test compares them with `/props` and `/slots`. KV type is recorded as q8_0. | Shared router decision. | | P12 | Network isolation 2: `inferproxy` is kept. Remove the conditional about dropping it. Its upstream is the router's TCP listener on the host. | Shared router decision; M0 (g). | Later changes to the brief: | Date | Change | Reason | |---|---|---| | 2026-09-17 | The roles table gained a one-sentence Purpose column, and the crates that are not roles got a purpose table under Code constraints. No behaviour changed. | Owner's request: a reader, human or agent, should learn what each piece is for before what it holds. | ## Proposed changes to the design brief None pending.