docs: M6b implementation plan
Six closed tasks for consult: registry, CONSULT_WAIT, [consult]/[secrets], ToolName and grants, Decision body, loopback HTTP. Reference is the git bundle ~/boxmaker-refs/m6b-ref.bundle at 5fb05f0. Gate after task 06 is 1611. Co-Authored-By: Grok 4.6 <noreply@x.ai>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# M6b implementation plan: consult
|
||||
|
||||
> 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:** a `consult` tool, not core, decided by `brokerd`. The grant's `max_taint` is the
|
||||
class gate. `brokerd` POSTs an allowlisted chat-completions body (question only, no
|
||||
history, no tools) over loopback HTTP/1.1. Every decision records that JSON body,
|
||||
including a denial. No vendor names in code.
|
||||
|
||||
**Architecture:** `loopd` registers `consult` after `subagent` and Ports it to `brokerd`.
|
||||
`timeout_for("consult")` is 180 s or `[broker] timeout_ms`, whichever is longer. `brokerd`
|
||||
loads `[consult]` and the named `bxcore::secrets` key at start. No `[consult]` denies with
|
||||
`consult-not-configured`. The HTTP client is not a container and not `http_fetch`.
|
||||
|
||||
**Tech stack:** Rust 1.98.1, edition 2024; no new crate. HTTPS is later.
|
||||
|
||||
**Spec:** `docs/specs/2026-10-04-m6-subagents-consult.md`, sections 4, 5, 6, 7 (M6b tests),
|
||||
and 8 (`consult-not-configured`, `consult-secret-unreadable`).
|
||||
|
||||
## The owner's decisions (2026-10-04)
|
||||
|
||||
1. Given tests use an in-process fake. The device check starts a loopback dummy on
|
||||
straylight. No live vendor in M6.
|
||||
2. Cloud-led sessions are later, not in M6. `consult` stays text-in, text-out, no tools.
|
||||
|
||||
## What the code already has, and what it lacks
|
||||
|
||||
- `subagent` is in `m5c()`, not core. `dispatch` Ports every other non-core name to
|
||||
`brokerd`. There is no `consult` tool.
|
||||
- `BrokerPort::timeout_for` special-cases only `ask_memory`. `[broker] timeout_ms` defaults
|
||||
to 120_000; do not change that default.
|
||||
- `brokerd.toml` has no `[consult]` and no `[secrets]`. `brokerd` does not enable
|
||||
`bxcore`'s `secrets` feature.
|
||||
- `ToolName` has no `Consult`. `Grant.untrusted` already defaults true.
|
||||
- `AuditEvent::Decision` has no `body`.
|
||||
|
||||
## Global constraints
|
||||
|
||||
- Roles depend on `bxcore` only. No `unsafe`, no async runtime, no new crate.
|
||||
- No file the implementer reads holds a chat-template tag whole (T33): build tags from
|
||||
pieces (`concat!("<", "think", ">")`).
|
||||
- Every path a given test creates is removed by a guard (T44); no test depends on timing
|
||||
(T27).
|
||||
- Fail-closed states `consult-not-configured` and `consult-secret-unreadable` have runbook
|
||||
entries, added by the task that adds the pointer (T49).
|
||||
- Each task gives its baseline and its gate count (T50, T53); the branch line is a check,
|
||||
never `git switch` (T51).
|
||||
- `Cargo.lock` is in the `git add` line (T24). Gate timeout 600000 ms (T40).
|
||||
- `docs/implementer-log.md`: at log-and-commit, read only the first 12 lines (T62).
|
||||
- Do not edit existing given tests the task does not list (`broker_port_ask_wait.rs`,
|
||||
`crates/loopd/tests/config.rs`, `crates/brokerd/tests/grants.rs` `rule_3`,
|
||||
`crates/brokerd/tests/config.rs`).
|
||||
|
||||
## Tasks
|
||||
|
||||
Every task below has been written to be replayed from its text. The gate count is the
|
||||
passed total of `cargo test --workspace` plus one, for the ignored `end_to_end` test the
|
||||
gate runs on its own. Before task 01 it is **1590** (straylight, rustc 1.98.1 / clippy
|
||||
0.1.98; M6a).
|
||||
|
||||
| # | Delivers | Crates | Gate after | Tests added | Mutations tried, caught |
|
||||
|---|---|---|---|---|---|
|
||||
| 01 | `consult` in `m5c()`, not core; epoch tools array unchanged | loopd | 1592 | 2 | 1, 1 |
|
||||
| 02 | `CONSULT_WAIT` 180 s via `timeout_for`; default `[broker] timeout_ms` stays 120_000 | loopd | 1593 | 1 | 1, 1 |
|
||||
| 03 | `[consult]` + `[secrets]`; start fails closed; runbook both consult entries | brokerd | 1602 | 9 | 2, 2 |
|
||||
| 04 | `ToolName::Consult`; parse; grants `covers` as `Some(None)`; no paths/hosts | brokerd | 1606 | 4 | 2, 2 |
|
||||
| 05 | Decision `body`; not-configured and `TaintTooHigh` record the body | bxcore, brokerd | 1609 | 3 | 2, 2 |
|
||||
| 06 | Fake HTTP 200; audit body equals posted JSON; tool result is the answer | brokerd | 1611 | 2 | 2, 2 |
|
||||
|
||||
Mutations (reference, one line each, restored after): `core: true` fails both task-01 tests;
|
||||
dropping the `consult` arm of `timeout_for` fails `consult_waits_one_hundred_and_eighty_seconds`
|
||||
and leaves `the_wait_for_each_tool` green; skipping `parse_loopback_http` in `validate` fails
|
||||
`a_non_loopback_url_is_invalid`; skipping the `0.6` check in `de_temperature_milli` fails
|
||||
`temperature_above_zero_point_six_is_invalid`; omitting `Consult` from `parse` fails
|
||||
`consult_parses_and_is_not_a_container_tool`; omitting the paths arm fails
|
||||
`consult_does_not_take_paths_or_hosts`; omitting `decide_forced` when `[consult]` is absent
|
||||
fails `no_consult_table_is_denied_with_the_runbook_and_records_the_body`; omitting `body` on
|
||||
the event fails both deny tests that assert the body; skipping `neutralise` fails the `<`
|
||||
assert in `an_allowed_consult_posts_only_the_question_and_returns_the_answer`; posting a
|
||||
second message in `wire_body_parts` fails `the_consult_body_is_only_the_question`.
|
||||
|
||||
After task 06, the design model runs the M6b device check (spec 7) on straylight: a loopback
|
||||
OpenAI-compatible dummy the check starts; a clean session's audit `body` matches a captured
|
||||
request; a session that first recalled an untrusted note is refused. Slot 1 is left busy. No
|
||||
internet consult.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- `consult` is not in the epoch `tools` array (task 01).
|
||||
- `[broker] timeout_ms` still defaults to 120_000 (task 02).
|
||||
- URL is loopback `http://` only (task 03). Temperature ≤ 0.6.
|
||||
- A consult grant with paths or hosts is invalid; unknown tool names still fail with the
|
||||
four-tool sentence (task 04; do not edit `rule_3`).
|
||||
- A denial still records the body that would have been sent (task 05).
|
||||
- The posted bytes equal the audit `body`; `messages` has one user item, no `tools`
|
||||
(task 06). The answer is tag-neutralised.
|
||||
|
||||
## Known limits
|
||||
|
||||
- **No internet consult in M6.** A live URL later is its own egress row, grant, and secret.
|
||||
- **No HTTPS in M6.** The client is HTTP/1.1 to 127.0.0.1 or localhost.
|
||||
- **Cloud-led sessions are not in M6.**
|
||||
|
||||
## Before hand-over
|
||||
|
||||
Work through `docs/plans/CHECKLIST.md` with a reference in a scratch worktree with its own
|
||||
build directory, replayed from its task text; mutations; `/tmp` compared around a gate run;
|
||||
the reference moved off this machine as a bundle before the run (T18). Task files name their
|
||||
log row (T47).
|
||||
|
||||
## Running it
|
||||
|
||||
The reference (`m6b-ref` at `5fb05f0`, tasks 01 to 06) is a git bundle
|
||||
`~/boxmaker-refs/m6b-ref.bundle`. Copy it to straylight and delete the local worktree
|
||||
(`/home/kyle/boxmaker-refs/m6b-ref`) and branch before the run (T18). Then, from the
|
||||
repository root, with the OpenCode TUI closed, after M6a has landed on master:
|
||||
|
||||
```sh
|
||||
git switch -c m6b master
|
||||
OPENCODE_MODEL=llama.cpp/ornith-1.5-35b-a3b RESUME=1 \
|
||||
SLOTS_URL='http://straylight:11434/slots?model=ornith-1.5-35b-a3b' \
|
||||
tools/run-plan.sh docs/plans/M6b 01
|
||||
```
|
||||
|
||||
After task 06, the gate count is 1611. Then the device check above.
|
||||
Reference in New Issue
Block a user