Files
boxmaker/docs/plans/M5b/README.md
T
2026-09-26 01:49:47 -07:00

7.4 KiB

M5b implementation plan: hardening

For the implementer: each task file is one closed task for one fresh session. Read AGENTS.md, then the task file, and do exactly that task. The driver is tools/run-plan.sh.

Goal: Boxmaker refuses, neutralises or flags hostile text on every path into the model and into memory, keeps its secrets in one shared store, explains refused tool calls, disarms links in replies from untrusted sessions, and never runs two copies of a daemon.

Architecture: proto becomes bxcore, the one crate every role may depend on. It gains secrets (moved from gatewayd) and sanitize (refuse, neutralise, flag). The roles call it: memoryd refuses and flags memory writes, loopd neutralises tags when text is first logged and checks the tag list against the tokenizer at startup, gatewayd escapes images and disarms links, bxctl explains a text. Each daemon holds a lock file for its life.

Tech stack: Rust 1.95, edition 2024, the standard library (File::try_lock for the locks); no new crate.

Spec: docs/specs/2026-09-26-m5b-hardening.md (approved 2026-09-26). Section numbers below are the spec's.

Global constraints

  • rust-version = "1.95"; no unsafe; no async runtime; no new dependency.
  • Roles depend on bxcore only; bxcore depends on no workspace crate (spec 3).
  • The secrets module is behind the Cargo feature secrets; loopd never enables it (spec 5).
  • No file the implementer reads holds a chat-template tag whole; tags are built from pieces (concat!) and named in words (T33).
  • Every path a given test creates is removed by a guard (T44).
  • Rules and lists are compiled in; no rules file (spec 6.5).
  • Every new fail-closed state has a runbook entry and a see docs/runbook.md#… pointer.
  • Commit trailer: the Co-Authored-By line the driver gives; gate with a 600000 ms timeout.

Review focus

Inputs the spec implies but no single task's tests pin down; each line names the task whose tests add it.

  1. Tags in other shapes: a listed tag in upper case, split by a zero-width character, or inside a longer word. Neutralising matches the listed spellings exactly; the zero-width split is refused in memory (spec 6.1) but reaches loopd in a tool result. Task 04 pins what happens to each shape, so the behaviour is decided, not accidental.
  2. Neutralising grows text past a cap: &lt; is longer than <, so a tool result capped at its limit and then neutralised is a few bytes over. Task 08 pins the order (cap, then neutralise) and that the log records the result's true length.
  3. Non-Latin text and the homoglyph map: a note written in Russian or Greek must not be flagged because its letters fold to Latin. Task 05's code-work corpus includes such notes.
  4. Links already in code, and Markdown links: in an untrusted reply, a URL already inside a code span or fence is not wrapped twice, and [text](url) is disarmed as a whole. Task 10 pins both.
  5. A lock file that cannot be created (an unwritable run/, or inferproxy's socket directory read-only): the daemon does not start and names the error. Task 12 pins it.

Tasks

# File Delivers Crates Check
00 (the design model, before the run) proto → bxcore, compiler-driven, with the four layers of spec 3; AGENTS.md, CLAUDE.md, docs/dependencies.md and the brief updated all done: 2b3ae08, 9672cb5, 6798502; gate 944
01 01-invalid-arguments-detail.md ToolResponse::Denied.detail; brokerd keeps the ArgsError; loopd appends it bxcore, brokerd, loopd fixture; reference
02 02-secrets-move.md bxcore::secrets behind the secrets feature; gatewayd uses it bxcore, gatewayd the moved tests; reference
03 03-sanitize-refuse.md sanitize::refuse: control, invisible, private use, noncharacters, known-prefix secrets, and tags in memory bxcore corpora (refuse part); mutations
04 04-sanitize-tags.md the tag list (from the tokenizer, built from pieces), sanitize::neutralise, and the decided handling of review-focus 1 bxcore corpora (neutralise part); mutations
05 05-sanitize-flag.md sanitize::normalise and sanitize::flag, Hermes' strict patterns (MIT) and M5a's phrases bxcore corpora (flag part, incl. non-Latin notes); mutations
06 06-memoryd-sanitize.md memoryd refuses and flags through sanitize; a flag keeps the text as an untrusted note; the block leaves out refused entries by rule name; check.rs goes memoryd given tests; mutations
07 07-loopd-neutralise.md loopd neutralises user messages, tool results (after the cap) and the memory block when they are logged loopd given tests; replay byte-identical
08 08-loopd-selftest-tags.md the self-test's fourth check, against a fake tokenizer loopd given tests
09 09-turn-done-untrusted.md TurnDone.untrusted from the session's labels bxcore, loopd fixture; given tests
10 10-gatewayd-links.md images escaped always; URLs in code spans with the notice when untrusted gatewayd given tests incl. review-focus 4
11 11-bxctl-sanitize-check.md bxctl sanitize check bxctl given tests
12 12-instance-lock.md bxcore::instance::lock, and loopd and inferproxy take it bxcore, loopd, inferproxy given tests incl. review-focus 5
13 13-instance-lock-rest.md brokerd, memoryd and gatewayd take it brokerd, memoryd, gatewayd given tests
14 14-runbook.md the six runbook entries as given fragments docs gate's runbook check

After task 14, the design model runs the device check of spec 13 on straylight and the owner's Mattermost, as for M5a.

Interfaces between tasks

Names later tasks rely on; each task file gives the full signatures.

  • Task 01: bxcore::ToolResponse::Denied { reason: DenyReason, detail: Option<String> }.
  • Task 02: bxcore::secrets::{Secret, SecretSource, load}, feature secrets.
  • Tasks 03 to 05, in bxcore::sanitize:
    • refuse(text: &str) -> Option<Refusal>, where Refusal carries the rule's name and sentence (only memory writes use it in M5b; M5c's outgoing turns may add a narrower variant);
    • neutralise(text: &str) -> Cow<'_, str> and TAGS: &[Tag] (each with its token id);
    • normalise(text: &str) -> String and flag(text: &str) -> Option<Flag> (the rule's name).
  • Task 09: bxcore::TurnDone { content, usage, untrusted: bool }.
  • Task 12: bxcore::instance::lock(path: &Path) -> Result<InstanceLock, LockError>, with LockError::Held and LockError::Io.

Before hand-over

Every task is worked through docs/plans/CHECKLIST.md: a reference replayed through the gate at each task's end state on titan's toolchain, in a scratch worktree with its own build directory; mutations; given tests walked by hand and grepped against earlier given files; /tmp compared around a gate run; the reference moved to straylight as a bundle before the run.

Facts to fetch while writing the tasks, not guessed: the tag list and token ids from Ornith's tokenizer configuration on straylight (task 04); Hermes' tools/threat_patterns.py and its licence (task 05).

Running it

git switch -c m5b master   # after task 00 is committed on master
OPENCODE_MODEL=titan/ornith-1.5-35b-a3b RESUME=1 \
  SLOTS_URL='http://127.0.0.1:8081/slots?model=ornith-1.5-35b-a3b' \
  tools/run-plan.sh docs/plans/M5b 01

RESUME and the watchdog are trials on this run (decisions, 2026-09-26); the review judges them.