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>
13 KiB
M6b task 04: ToolName::Consult, parse, grants
Branch: m6b. Check it: git branch --show-current prints m6b and git status --short
is empty; otherwise stop. Never switch branch.
Commit subject: brokerd: consult tool name, parse, grants
Log row: the Task cell is M6b/04-brokerd-consult-tool.
First action. Run this before any Read of crate source. Do not ls
crates/brokerd/tests/. Do not Read broker.rs, ledger/, main.rs,
docs/implementer-log.md (except the first 12 lines at the log step). Do not edit
crates/brokerd/tests/grants.rs (its rule_3 unknown-tool sentence stays the four
container tools). The signatures below are complete.
cp docs/plans/M6b/files/crates/brokerd/tests/grants_consult.rs crates/brokerd/tests/
Goal
consult is a ToolName. parse reads {"question":…} with deny_unknown_fields and
caps question at 16_384 bytes on a character boundary. A grant with empty constraints
loads (untrusted still defaults true). Paths or hosts on a consult grant are invalid.
covers treats consult like the memory tools (Some(None)). ALL stays the four
container tools. is_memory is false. Unknown tool names still fail with the four-tool
sentence.
Every place that changes
Adding ToolName::Consult makes every exhaustive match on ToolName or ToolArgs
fail to compile until it has an arm. List each match and fill it.
| Place | What happens there |
|---|---|
crates/brokerd/src/consult.rs |
ConsultArgs, QUESTION_CAP, cap_bytes, question_of, wire_body |
crates/brokerd/src/args.rs |
Consult on ToolName and ToolArgs; parse |
crates/brokerd/src/config.rs |
time_limit arm (uses read_file_ms; consult never hits a container) |
crates/brokerd/src/policy/matching.rs |
covers: Some(None) |
crates/brokerd/src/grants.rs |
check_tool_constraints: no paths, no hosts |
crates/brokerd/src/runner.rs |
mounts none, egress none |
crates/brokerd/src/container.rs |
refuse Consult with IN_CONSULT |
crates/brokerd/tests/grants_consult.rs |
new, given |
| everything else | no change: not ledger, not broker.rs, not tests/grants.rs |
memory_port::wait_for already has _ => and needs no edit.
Files
- Copy, from
docs/plans/M6b/files/(the same path under it): the new testcrates/brokerd/tests/grants_consult.rs. - Modify:
crates/brokerd/src/consult.rs,crates/brokerd/src/args.rs,crates/brokerd/src/config.rs,crates/brokerd/src/policy/matching.rs,crates/brokerd/src/grants.rs,crates/brokerd/src/runner.rs,crates/brokerd/src/container.rs,docs/implementer-log.md. - Do not touch: any other file. Do not edit
crates/brokerd/tests/grants.rs.
Facts you need
- Unknown keys.
ConsultArgshas#[serde(deny_unknown_fields)].parsemaps a serde error toArgsError::Shape. The given test accepts a detail that containsunknownorhistory. - Cap on parse.
cap_bytes(&value.question, QUESTION_CAP).0is the stored question.floor_char_boundarythen.get(..boundary). wire_bodyis used by task 05's tests; write it now so those tests can call it. No history, notoolskey. WhenconsultisNone, model is""and temperature is 0.6.ALLis[ToolName; 4]. Do not putConsultin it.- Rule 3.
ToolName::parse("consult")isSome, so a consult grant is not "unknown tool". The unknown-tool sentence ingrants.rsstill lists the four container tools. Do not edit that string. - The items the edits use. This table is complete.
Item Signature ToolName::parseparse(name: &str) -> Option<ToolName>ToolName::as_stras_str(self) -> &'static strToolName::is_memoryis_memory(self) -> boolToolName::ALL[ToolName; 4]parseparse(tool: ToolName, arguments: &str) -> Result<ToolArgs, ArgsError>ArgsError::ShapeShape(String)ArgsError::detaildetail(&self) -> StringGrant.untrusteddefaults true str::floor_char_boundaryfloor_char_boundary(&self, index: usize) -> usize(Rust 1.98)
The edits, exactly
Each quoted "before" block occurs exactly once after task 03.
crates/brokerd/src/consult.rs
A1. Add Serialize to the serde import:
use serde::{Deserialize, Deserializer};
becomes
use serde::{Deserialize, Deserializer, Serialize};
A2. After pub const CONSULT: &str = "consult"; add:
/// `question` and the answer are cut to this many bytes, on a character boundary.
pub const QUESTION_CAP: usize = 16_384;
A3. After parse_loopback_http (after its closing }, before SecretStartError) add:
/// The model's `consult` arguments. Unknown keys are errors.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConsultArgs {
pub question: String,
}
/// Cut `text` to `cap` bytes on a character boundary.
pub fn cap_bytes(text: &str, cap: usize) -> (String, bool) {
if text.len() <= cap {
return (text.to_string(), false);
}
let boundary = text.floor_char_boundary(cap);
match text.get(..boundary) {
Some(cut) => (cut.to_string(), true),
None => (String::new(), true),
}
}
/// The `question` field of `arguments`, capped, or empty when it is missing or not a string.
pub fn question_of(arguments: &str) -> String {
let extracted = match serde_json::from_str::<serde_json::Value>(arguments) {
Ok(serde_json::Value::Object(obj)) => obj
.get("question")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
_ => String::new(),
};
cap_bytes(&extracted, QUESTION_CAP).0
}
/// The exact JSON body `brokerd` POSTs (and records). No history, no tools.
pub fn wire_body(consult: Option<&Consult>, arguments: &str) -> String {
let model = consult.map(|c| c.model.as_str()).unwrap_or("");
let temperature = consult
.map(Consult::temperature)
.unwrap_or(f64::from(DEFAULT_TEMPERATURE_MILLI) / 1000.0);
wire_body_parts(model, temperature, &question_of(arguments))
}
fn wire_body_parts(model: &str, temperature: f64, question: &str) -> String {
serde_json::json!({
"model": model,
"temperature": temperature,
"messages": [{"role": "user", "content": question}],
})
.to_string()
}
crates/brokerd/src/args.rs
N1. After the use bxcore::{MemoryArgs, RecallArgs}; line add:
use crate::consult::{ConsultArgs, QUESTION_CAP, cap_bytes};
N2. The comment on ToolName
/// The tools the broker can dispatch to: four that run in a container, and the three memory
/// tools that `memoryd` runs (M5a spec, section 8; M5c spec, section 9).
becomes
/// The tools the broker can dispatch to: four that run in a container, the three memory
/// tools that `memoryd` runs, and `consult` (M6 spec, section 4).
N3. After AskMemory, in ToolName add Consult,.
N4. In ToolName::parse, after "ask_memory" => Some(ToolName::AskMemory), add
"consult" => Some(ToolName::Consult),.
N5. In as_str, after ToolName::AskMemory => "ask_memory", add
ToolName::Consult => "consult",.
N6. After AskMemory(AskMemoryArgs), in ToolArgs add:
Consult {
question: String,
},
N7. In tool(), after ToolArgs::AskMemory(_) => ToolName::AskMemory, add
ToolArgs::Consult { .. } => ToolName::Consult,.
N8. In canonical_json(), after the AskMemory arm add:
ToolArgs::Consult { question } => {
let value = ConsultArgs {
question: question.clone(),
};
serde_json::to_string(&value).unwrap_or_else(|_| "{}".to_string())
}
N9. In parse(), after the AskMemory arm add:
ToolName::Consult => {
let value: ConsultArgs = serde_json::from_str(arguments)
.map_err(|error| ArgsError::Shape(error.to_string()))?;
let question = cap_bytes(&value.question, QUESTION_CAP).0;
Ok(ToolArgs::Consult { question })
}
Do not add Consult to is_memory. Do not put it in ALL.
crates/brokerd/src/config.rs
T1. The time_limit arm
crate::args::ToolName::Recall
| crate::args::ToolName::Memory
| crate::args::ToolName::AskMemory => self.read_file_ms,
becomes
crate::args::ToolName::Recall
| crate::args::ToolName::Memory
| crate::args::ToolName::AskMemory
| crate::args::ToolName::Consult => self.read_file_ms,
crates/brokerd/src/policy/matching.rs
M1. The covers arm
ToolArgs::Recall(_) | ToolArgs::Memory(_) | ToolArgs::AskMemory(_) => Some(None),
becomes
ToolArgs::Recall(_)
| ToolArgs::Memory(_)
| ToolArgs::AskMemory(_)
| ToolArgs::Consult { .. } => Some(None),
crates/brokerd/src/grants.rs
G1. The end of check_tool_constraints (exactly once)
"http_fetch does not take paths",
);
}
}
_ => {}
}
}
becomes
"http_fetch does not take paths",
);
}
}
"consult" => {
if !grant.constraints.paths.is_empty() {
push(
problems,
file.to_string(),
None,
"consult does not take paths".to_string(),
);
}
if !grant.constraints.hosts.is_empty() {
push(
problems,
file.to_string(),
None,
"consult does not take hosts".to_string(),
);
}
}
_ => {}
}
}
Do not edit the unknown-tool sentence.
crates/brokerd/src/runner.rs
U1. The mounts arm
ToolArgs::Recall(_) | ToolArgs::Memory(_) | ToolArgs::AskMemory(_) => (Vec::new(), None),
becomes
ToolArgs::Recall(_)
| ToolArgs::Memory(_)
| ToolArgs::AskMemory(_)
| ToolArgs::Consult { .. } => (Vec::new(), None),
crates/brokerd/src/container.rs
P1. After pub const IN_MEMORYD: &str = "the memory tools run in memoryd, not in a container"; add:
/// A `consult` that reached the container runtime: fixed, it reaches the model.
pub const IN_CONSULT: &str = "consult runs in brokerd, not in a container";
P2. The comment and body at the start of Podman::run
// `Routed` sends the memory tools to `memoryd`; one here is refused, never run.
if spec.tool().is_memory() {
return Err(RunError::Unavailable(IN_MEMORYD.to_string()));
}
become
// `Routed` sends the memory tools to `memoryd`; `ConsultStack` runs `consult`.
if spec.tool().is_memory() {
return Err(RunError::Unavailable(IN_MEMORYD.to_string()));
}
if spec.tool() == crate::args::ToolName::Consult {
return Err(RunError::Unavailable(IN_CONSULT.to_string()));
}
Steps
-
1. Copy the given test.
cp docs/plans/M6b/files/crates/brokerd/tests/grants_consult.rs crates/brokerd/tests/ -
2. See it fail.
cargo test -p brokerd --test grants_consult, timeout 600000 ms: the target does not compile (E0599/ unresolvedToolName::ConsultorConsultArgs). -
3. The edits. A1–A3, N1–N9, T1, M1, G1, U1, P1–P2. After each file,
cargo check -p brokerd. When it prints no error and no warning, go on.wc -lofconsult.rs,args.rs,grants.rs,container.rseach ≤ 500. -
4. The tests.
cargo test -p brokerd --test grants_consult --test grants --test args: every test passes, 4 ingrants_consult. The existing unknown-tool case ingrants.rsstill names the four container tools. -
5. Run the gate.
cargo fmt --all, thenmake gate, timeout 600000 ms. Last linegate: ok. Test count 1606 (1602 before, plus 4). Aftergate: ok, do not add uptest result:lines. -
6. Log and commit. Task cell
M6b/04-brokerd-consult-tool. Read only the first 12 lines ofdocs/implementer-log.md; insert the row under the header. Stage withgit add crates/brokerd docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.
Done when
make gateprintsgate: okwith 1606 tests.git diff HEAD~1 --statlists only the files in the Files section plus the log.
Stop and report if
- The branch check fails; a "before" block is missing or duplicated; step 2 compiles; you
edit
tests/grants.rs; you addConsulttoALLoris_memory;cargo checkwarns; an existinggrantsorargstest fails; a test outside the named files fails in the gate (git stash -u, rerun,git stash pop, report both).
What the given tests pin
ToolName::parse("consult") is Consult; as_str is "consult"; it is not memory and
not in ALL. parse of {"question":"hello"} yields that question. A history key is
invalid-arguments. An empty-constraints consult grant loads with untrusted true. Paths
or hosts on the grant are rejected with does not take paths / does not take hosts.