Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
18 KiB
M5c-2 task 02: brokerd knows ask_memory, a built-in memory tool
Branch: m5c2. Check it: git branch --show-current prints m5c2 and git status --short
is empty; otherwise stop. Never switch branch.
Commit subject: brokerd: ask_memory, a built-in memory tool that memoryd runs
Log row: the Task cell is M5c2/02-brokerd-ask-memory.
Goal
Task 01 gave bxcore the arguments of a new tool, ask_memory (bxcore::AskMemoryArgs, with
check()). This task makes brokerd accept it exactly as it accepts recall (M5c spec,
section 9):
- Its name.
ToolName::AskMemory, writtenask_memory. It is a memory tool:is_memory()is true, somemorydruns it, never a container. - Its arguments.
parsedecodes them intoToolArgs::AskMemory(AskMemoryArgs)and runscheck(); a refusal isArgsError::Shapewithcheck's sentence, as forrecall. The canonical form isserde_json::to_stringof the value, as forrecall. - Its policy is built in. A third built-in grant,
builtin-ask-memory, allows it in every session,secretand untrusted ones too:mode = "auto",max_taint = "secret",result_class = "private",untrusted = false. A question is not written to Honcho (spec, section 8), and the answer's own labels, whichmemorydgives, raise the session, asrecall's do. An owner's grant file namingask_memoryis refused with theBUILT_INsentence, which now names the three tools.
brokerd's wait for memoryd stays 30 s for every memory tool in this task; task 03 gives
ask_memory its own. Nothing yet asks brokerd for ask_memory: loopd declares it in task 04
and memoryd answers it from task 09. Until then, a call reaching memoryd is answered
memoryd runs only recall and memory, a failed result.
Every place that changes
This is the complete list.
| Place | What happens there |
|---|---|
crates/brokerd/src/args.rs |
ToolName::AskMemory, ToolArgs::AskMemory, and their arms in parse, as_str, is_memory, tool, canonical_json and parse (edits A1 to A9) |
crates/brokerd/src/builtin.rs |
the grant ASK_MEMORY, its id in sources, BUILT_IN's sentence, two comments (edits B1, B2) |
crates/brokerd/src/config.rs, Runner::time_limit |
AskMemory beside Recall and Memory (edit C1) |
crates/brokerd/src/policy/matching.rs |
ToolArgs::AskMemory takes no constraints, as Recall (edit P1) |
crates/brokerd/src/runner.rs |
ToolArgs::AskMemory has no mounts and no egress, as Recall (edit R1) |
crates/brokerd/src/grants.rs (rule 3), container.rs (Podman::run), memory_port.rs (Routed) |
no change: each asks tool.is_memory(), which edit A5 makes true for AskMemory |
crates/brokerd/tests/ask_memory_tool.rs |
new, given |
crates/brokerd/tests/grants.rs, memory_tools.rs |
new versions, given: three built-in grants, not two |
| everything else | no change: not bxcore, not loopd, not memoryd, no Cargo.toml |
Files
- Copy, from
docs/plans/M5c2/files/(the same path under it): the new testcrates/brokerd/tests/ask_memory_tool.rs, and new versions of (they replace the files in the tree)crates/brokerd/tests/grants.rsandcrates/brokerd/tests/memory_tools.rs. - Modify:
crates/brokerd/src/args.rs,crates/brokerd/src/builtin.rs,crates/brokerd/src/config.rs,crates/brokerd/src/policy/matching.rs,crates/brokerd/src/runner.rs,docs/implementer-log.md. - Do not touch: any other file. The test support files under
crates/brokerd/tests/support/are used as they are.
Facts you need
-
AskMemoryArgs(task 01, inbxcore::memory, exported asbxcore::AskMemoryArgs):#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AskMemoryArgs { pub question: String, #[serde(default)] pub workspaces: Vec<String>, } impl AskMemoryArgs { pub fn check(&self) -> Result<(), String>; pub fn asked(&self) -> Vec<&str>; }It is used here exactly as
RecallArgsis:serde_json::from_str, thencheck(), each error asArgsError::Shape;serde_json::to_stringfor the canonical form. Its canonical form writes every field:{"question":"q","workspaces":[]}. -
The order of the built-in grants is the order of
sourcesinbuiltin.rs:memory,recall, then the newask-memorylast. The givengrants.rslists them in that order. -
Why the grant is
secretand trusted.max_taint = "secret"lets asecretsession ask (spec, section 8: a query is not stored by Honcho).result_class = "private"anduntrusted = falseare the floor of the answer's label;memorydlabels each answer by the workspaces asked, andbrokerdkeeps the stricter of the two (runner::run, unchanged). The given tests send anask_memorycall throughbroker::handleto a fakememorydthat answersuntrustedorsecret, and read the session's state afterwards. -
The long lines. The three grant constants are string literals longer than 100 characters;
rustfmtleaves them as they are, and the gate accepts them, as it doesRECALLandMEMORYtoday. Do not split them. -
The given tests:
ask_memory_tool.rs(9 tests): the name,is_memory, parsing and the canonical form; each refusal withbxcore's sentence and itsdetail(); the built-in grant, its exact text and its hash, allowing the call in aprivate, asecretand an untrusted session; an owner's grant file refused with the newBUILT_IN; the call routed tomemorydwith the session's state and canonical arguments; a container runtime refusing it; and, throughbroker::handle, an answer labelled untrusted making the session untrusted, one labelledsecretraising it tosecret, and one labelledpublicleaving itprivate.grants.rs: one line changes, the list of built-in ids.memory_tools.rs: one line changes, the number of grantsmatchablegives beside one owner grant (4, not 3).
-
The items the edits use. This table is complete; nothing from a crate's source (the Cargo registry,
~/.cargo/registry) is needed or allowed.Item Where bxcore::AskMemoryArgs,AskMemoryArgs::check(&self) -> Result<(), String>A1, A9 serde_json::from_str,serde_json::to_stringA8, A9, as the Recallarms beside themArgsError::Shape(String)A9
The edits, exactly
Nothing else in these files changes. Each quoted "before" block occurs exactly once in its file when its turn comes (make them in order). Keep the variants in the order shown.
crates/brokerd/src/args.rs
A1. Add one line. The line
use bxcore::tools::{HttpFetchArgs, ReadFileArgs, ShellArgs, WriteFileArgs};
becomes
use bxcore::AskMemoryArgs;
use bxcore::tools::{HttpFetchArgs, ReadFileArgs, ShellArgs, WriteFileArgs};
A2. The lines
/// The tools the broker can dispatch to: four that run in a container, and the two memory
/// tools that `memoryd` runs (M5a spec, section 8).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolName {
ReadFile,
WriteFile,
Shell,
HttpFetch,
Recall,
Memory,
}
become
/// 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).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolName {
ReadFile,
WriteFile,
Shell,
HttpFetch,
Recall,
Memory,
AskMemory,
}
A3. In ToolName::parse, the lines (its spaces are checked: the name is ask_memory, with an
underscore)
"memory" => Some(ToolName::Memory),
_ => None,
become
"memory" => Some(ToolName::Memory),
"ask_memory" => Some(ToolName::AskMemory),
_ => None,
A4. In ToolName::as_str, the lines (its spaces are checked)
ToolName::Memory => "memory",
}
become
ToolName::Memory => "memory",
ToolName::AskMemory => "ask_memory",
}
A5. In ToolName::is_memory, the line
matches!(self, ToolName::Recall | ToolName::Memory)
becomes
matches!(
self,
ToolName::Recall | ToolName::Memory | ToolName::AskMemory
)
A6. At the end of enum ToolArgs, the lines
Recall(RecallArgs),
Memory(MemoryArgs),
}
become
Recall(RecallArgs),
Memory(MemoryArgs),
AskMemory(AskMemoryArgs),
}
A7. In ToolArgs::tool, the line
ToolArgs::Memory(_) => ToolName::Memory,
becomes
ToolArgs::Memory(_) => ToolName::Memory,
ToolArgs::AskMemory(_) => ToolName::AskMemory,
A8. At the end of ToolArgs::canonical_json, the lines
ToolArgs::Memory(value) => {
serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string())
}
become
ToolArgs::Memory(value) => {
serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string())
}
ToolArgs::AskMemory(value) => {
serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string())
}
A9. At the end of parse, the lines
Ok(ToolArgs::Memory(value))
}
become
Ok(ToolArgs::Memory(value))
}
ToolName::AskMemory => {
let value: AskMemoryArgs = serde_json::from_str(arguments)
.map_err(|error| ArgsError::Shape(error.to_string()))?;
value.check().map_err(ArgsError::Shape)?;
Ok(ToolArgs::AskMemory(value))
}
crates/brokerd/src/builtin.rs
B1. The module's doc comment, the lines
//! The built-in grants of `recall` and `memory` (M5a spec, section 8). Reading and writing memory
//! is what an agent is, so no grant file can turn it off or put questions in front of it; the
//! prompt is protected by `memoryd`, which lets only clean sessions edit identity.
become
//! The built-in grants of `recall`, `memory` and `ask_memory` (M5a spec, section 8; M5c spec,
//! section 9). Reading and writing memory is what an agent is, so no grant file can turn it off or
//! put questions in front of it; the prompt is protected by `memoryd`, which lets only clean
//! sessions edit identity.
B2. The lines (its spaces are checked: the grant's text is hashed into the audit log, and the
given test compares it and the sentence letter for letter; every \n and \" is as shown)
pub const MEMORY: &str = "tool = \"memory\"\nmode = \"auto\"\nmax_taint = \"secret\"\nresult_class = \"private\"\nuntrusted = false\n";
/// The sentence for an owner's grant naming a memory tool.
pub const BUILT_IN: &str = "the policy of recall and memory is built in; remove this grant";
/// The two built-in grants, `builtin-memory` and `builtin-recall`.
pub fn grants() -> Result<Vec<LoadedGrant>, GrantProblem> {
// For each (id, text) of [("builtin-memory", MEMORY), ("builtin-recall", RECALL)]:
// `toml::from_str::<Grant>(text)` and `sha256(text.as_bytes())`, each error as a GrantProblem {
// file: id, line: None, problem: e.to_string() }; build LoadedGrant { id, grant, sha256 }.
// Collect into Result<Vec<LoadedGrant>, GrantProblem> (the first error wins).
let sources: &[(&str, &str)] = &[("builtin-memory", MEMORY), ("builtin-recall", RECALL)];
become
pub const MEMORY: &str = "tool = \"memory\"\nmode = \"auto\"\nmax_taint = \"secret\"\nresult_class = \"private\"\nuntrusted = false\n";
pub const ASK_MEMORY: &str = "tool = \"ask_memory\"\nmode = \"auto\"\nmax_taint = \"secret\"\nresult_class = \"private\"\nuntrusted = false\n";
/// The sentence for an owner's grant naming a memory tool.
pub const BUILT_IN: &str =
"the policy of recall, memory and ask_memory is built in; remove this grant";
/// The three built-in grants, `builtin-memory`, `builtin-recall` and `builtin-ask-memory`.
pub fn grants() -> Result<Vec<LoadedGrant>, GrantProblem> {
// For each (id, text) of `sources`:
// `toml::from_str::<Grant>(text)` and `sha256(text.as_bytes())`, each error as a GrantProblem {
// file: id, line: None, problem: e.to_string() }; build LoadedGrant { id, grant, sha256 }.
// Collect into Result<Vec<LoadedGrant>, GrantProblem> (the first error wins).
let sources: &[(&str, &str)] = &[
("builtin-memory", MEMORY),
("builtin-recall", RECALL),
("builtin-ask-memory", ASK_MEMORY),
];
The id is builtin-ask-memory, with a hyphen; the tool in the grant's text is ask_memory, with
an underscore.
crates/brokerd/src/config.rs
C1. In Runner::time_limit, the line
crate::args::ToolName::Recall | crate::args::ToolName::Memory => self.read_file_ms,
becomes
crate::args::ToolName::Recall
| crate::args::ToolName::Memory
| crate::args::ToolName::AskMemory => self.read_file_ms,
crates/brokerd/src/policy/matching.rs
P1. The line
ToolArgs::Recall(_) | ToolArgs::Memory(_) => Some(None),
becomes
ToolArgs::Recall(_) | ToolArgs::Memory(_) | ToolArgs::AskMemory(_) => Some(None),
crates/brokerd/src/runner.rs
R1. The line
ToolArgs::Recall(_) | ToolArgs::Memory(_) => (Vec::new(), None),
becomes
ToolArgs::Recall(_) | ToolArgs::Memory(_) | ToolArgs::AskMemory(_) => (Vec::new(), None),
Steps
-
1. Copy the given tests. From the repository root, three commands:
cp docs/plans/M5c2/files/crates/brokerd/tests/ask_memory_tool.rs crates/brokerd/tests/ cp docs/plans/M5c2/files/crates/brokerd/tests/grants.rs crates/brokerd/tests/ cp docs/plans/M5c2/files/crates/brokerd/tests/memory_tools.rs crates/brokerd/tests/Then
git status --shortlistsgrants.rsandmemory_tools.rsas modified andask_memory_tool.rsas new, all undercrates/brokerd/tests/. -
2. See them fail. Two commands, each with a shell timeout of 600000 ms:
cargo build --tests --keep-going -p brokerd: one test target,ask_memory_tool, does not compile, with 11 errors: oneE0432(unresolved import `brokerd::builtin::ASK_MEMORY`), oneE0599(no variant, associated function, or constant named `AskMemory` found for enum `ToolArgs`) and nineE0599(the same for enumToolName). Every other target compiles.cargo test --no-fail-fast -p brokerd --test grants --test memory_tools: two tests fail, each on its changed line:an_empty_directory_is_a_valid_empty_setingrantsandgrant_files_cannot_name_the_memory_tools_or_take_a_built_in_idinmemory_tools.
-
3. The edits. Make edits A1 to A9, B1, B2, C1, P1 and R1, in order.
cargo check -p brokerdprints no error and no warning. -
4. The tests. With a shell timeout of 600000 ms, every test passes: 9 in
ask_memory_tool, 17 ingrants, 4 inmemory_tools, 5 inmemory_port, 8 inrunner:cargo test -p brokerd --test ask_memory_tool --test grants --test memory_tools \ --test memory_port --test runner -
5. Check. Two commands:
git diff --stat -- crates/brokerd/srclists exactly the five files of "Modify" undercrates/brokerd/src: notgrants.rs,container.rsormemory_port.rs.git diff --stat -- crates/bxcore crates/loopd crates/memoryd Cargo.toml Cargo.lockprints nothing.
-
6. Run the gate.
cargo fmt --all, thenmake gate, with a shell timeout of 600000 ms. Expected last line:gate: ok. The test count is the passed total ofcargo test --workspaceplus one, for the ignoredend_to_endtest that the gate runs on its own: 1381 after this task, 1372 before it; the 9 new ones are the tests ofask_memory_tool.rs(the new versions ofgrants.rsandmemory_tools.rskeep their counts). To count, add up theN passedof everytest result:line the gate'scargo test --workspaceprinted, and add 1. Never run the gate again only to count. -
7. Log and commit. Add your row to
docs/implementer-log.md; its Task cell isM5c2/02-brokerd-ask-memory.git status --shortlists the five source files, the two replaced tests and your log as modified, andask_memory_tool.rsas new. Stage withgit add crates/brokerd docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.Cargo.lockdoes not change in this task; thegit addis harmless.
Done when
make gateprintsgate: okwith 1381 tests.git diff HEAD~1 --statlists only the five source files, the three test files of step 1 anddocs/implementer-log.md.
Stop and report if
- The branch check at the top fails.
- A "before" block is not in its file exactly as shown when its turn comes, or is there more than once.
- Step 2 shows a different result: other targets not compiling, other errors, or other tests failing.
cargo checkin step 3 prints an error or a warning. Quote it, and stop.- A test fails in step 4. Do not change a test: stop, and give the failing tests' names and their messages.
- The fix seems to need a change anywhere else:
grants.rs,container.rs,memory_port.rs,bxcore, a test support file, a per-tool wait. It does not; stop and say why you think so. - A test outside the three given files fails in the gate: check once whether it fails without
your change (
git stash -u, run it,git stash pop), then stop and report both results. - The gate passes with a count other than 1381.
- A comment in an edit does not fit the code under it. Quote it and stop.
What the given tests pin
Each of these changes to the finished code makes at least one given test fail (17 tried, all
caught): ask_memory not parsed as a name, or written another way; AskMemory not a memory
tool; check skipped; tool() giving another tool; the canonical form lost; the built-in grant
missing, under another id, asking instead of allowing, capped at private, or labelling its
results untrusted; BUILT_IN's old sentence; AskMemory matching no grant; an owner's grant
naming it accepted; a container running it; Routed sending it to the container runtime;
memoryd's label dropped, so its answer no longer raises the session.