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>
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 istools/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)
- Given tests use an in-process fake. The device check starts a loopback dummy on straylight. No live vendor in M6.
- Cloud-led sessions are later, not in M6.
consultstays text-in, text-out, no tools.
What the code already has, and what it lacks
subagentis inm5c(), not core.dispatchPorts every other non-core name tobrokerd. There is noconsulttool.BrokerPort::timeout_forspecial-cases onlyask_memory.[broker] timeout_msdefaults to 120_000; do not change that default.brokerd.tomlhas no[consult]and no[secrets].brokerddoes not enablebxcore'ssecretsfeature.ToolNamehas noConsult.Grant.untrustedalready defaults true.AuditEvent::Decisionhas nobody.
Global constraints
- Roles depend on
bxcoreonly. Nounsafe, 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-configuredandconsult-secret-unreadablehave 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.lockis in thegit addline (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.rsrule_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
consultis not in the epochtoolsarray (task 01).[broker] timeout_msstill 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;messageshas one user item, notools(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:
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.