# 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`, written `ask_memory`. It is a memory tool: `is_memory()` is true, so `memoryd` runs it, never a container. - **Its arguments.** `parse` decodes them into `ToolArgs::AskMemory(AskMemoryArgs)` and runs `check()`; a refusal is `ArgsError::Shape` with `check`'s sentence, as for `recall`. The canonical form is `serde_json::to_string` of the value, as for `recall`. - **Its policy is built in.** A third built-in grant, `builtin-ask-memory`, allows it in every session, `secret` and 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, which `memoryd` gives, raise the session, as `recall`'s do. An owner's grant file naming `ask_memory` is refused with the `BUILT_IN` sentence, 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 test `crates/brokerd/tests/ask_memory_tool.rs`, and **new versions** of (they replace the files in the tree) `crates/brokerd/tests/grants.rs` and `crates/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, in `bxcore::memory`, exported as `bxcore::AskMemoryArgs`): ```rust #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AskMemoryArgs { pub question: String, #[serde(default)] pub workspaces: Vec, } impl AskMemoryArgs { pub fn check(&self) -> Result<(), String>; pub fn asked(&self) -> Vec<&str>; } ``` It is used here exactly as `RecallArgs` is: `serde_json::from_str`, then `check()`, each error as `ArgsError::Shape`; `serde_json::to_string` for the canonical form. Its canonical form writes every field: `{"question":"q","workspaces":[]}`. - **The order of the built-in grants** is the order of `sources` in `builtin.rs`: `memory`, `recall`, then the new `ask-memory` last. The given `grants.rs` lists them in that order. - **Why the grant is `secret` and trusted.** `max_taint = "secret"` lets a `secret` session ask (spec, section 8: a query is not stored by Honcho). `result_class = "private"` and `untrusted = false` are the floor of the answer's label; `memoryd` labels each answer by the workspaces asked, and `brokerd` keeps the stricter of the two (`runner::run`, unchanged). The given tests send an `ask_memory` call through `broker::handle` to a fake `memoryd` that answers `untrusted` or `secret`, and read the session's state afterwards. - **The long lines.** The three grant constants are string literals longer than 100 characters; `rustfmt` leaves them as they are, and the gate accepts them, as it does `RECALL` and `MEMORY` today. Do not split them. - The given tests: - `ask_memory_tool.rs` (9 tests): the name, `is_memory`, parsing and the canonical form; each refusal with `bxcore`'s sentence and its `detail()`; the built-in grant, its exact text and its hash, allowing the call in a `private`, a `secret` and an untrusted session; an owner's grant file refused with the new `BUILT_IN`; the call routed to `memoryd` with the session's state and canonical arguments; a container runtime refusing it; and, through `broker::handle`, an answer labelled untrusted making the session untrusted, one labelled `secret` raising it to `secret`, and one labelled `public` leaving it `private`. - `grants.rs`: one line changes, the list of built-in ids. `memory_tools.rs`: one line changes, the number of grants `matchable` gives 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_string` | A8, A9, as the `Recall` arms beside them | | `ArgsError::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 ```rust use bxcore::tools::{HttpFetchArgs, ReadFileArgs, ShellArgs, WriteFileArgs}; ``` becomes ```rust use bxcore::AskMemoryArgs; use bxcore::tools::{HttpFetchArgs, ReadFileArgs, ShellArgs, WriteFileArgs}; ``` **A2.** The lines ```rust /// 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 ```rust /// 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) ```rust "memory" => Some(ToolName::Memory), _ => None, ``` become ```rust "memory" => Some(ToolName::Memory), "ask_memory" => Some(ToolName::AskMemory), _ => None, ``` **A4.** In `ToolName::as_str`, the lines (its spaces are checked) ```rust ToolName::Memory => "memory", } ``` become ```rust ToolName::Memory => "memory", ToolName::AskMemory => "ask_memory", } ``` **A5.** In `ToolName::is_memory`, the line ```rust matches!(self, ToolName::Recall | ToolName::Memory) ``` becomes ```rust matches!( self, ToolName::Recall | ToolName::Memory | ToolName::AskMemory ) ``` **A6.** At the end of `enum ToolArgs`, the lines ```rust Recall(RecallArgs), Memory(MemoryArgs), } ``` become ```rust Recall(RecallArgs), Memory(MemoryArgs), AskMemory(AskMemoryArgs), } ``` **A7.** In `ToolArgs::tool`, the line ```rust ToolArgs::Memory(_) => ToolName::Memory, ``` becomes ```rust ToolArgs::Memory(_) => ToolName::Memory, ToolArgs::AskMemory(_) => ToolName::AskMemory, ``` **A8.** At the end of `ToolArgs::canonical_json`, the lines ```rust ToolArgs::Memory(value) => { serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()) } ``` become ```rust 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 ```rust Ok(ToolArgs::Memory(value)) } ``` become ```rust 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 ```rust //! 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 ```rust //! 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) ```rust 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, GrantProblem> { // For each (id, text) of [("builtin-memory", MEMORY), ("builtin-recall", RECALL)]: // `toml::from_str::(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, GrantProblem> (the first error wins). let sources: &[(&str, &str)] = &[("builtin-memory", MEMORY), ("builtin-recall", RECALL)]; ``` become ```rust 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, GrantProblem> { // For each (id, text) of `sources`: // `toml::from_str::(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, 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 ```rust crate::args::ToolName::Recall | crate::args::ToolName::Memory => self.read_file_ms, ``` becomes ```rust 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 ```rust ToolArgs::Recall(_) | ToolArgs::Memory(_) => Some(None), ``` becomes ```rust ToolArgs::Recall(_) | ToolArgs::Memory(_) | ToolArgs::AskMemory(_) => Some(None), ``` ### `crates/brokerd/src/runner.rs` **R1.** The line ```rust ToolArgs::Recall(_) | ToolArgs::Memory(_) => (Vec::new(), None), ``` becomes ```rust ToolArgs::Recall(_) | ToolArgs::Memory(_) | ToolArgs::AskMemory(_) => (Vec::new(), None), ``` ## Steps - [ ] **1. Copy the given tests.** From the repository root, three commands: ```sh 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 --short` lists `grants.rs` and `memory_tools.rs` as modified and `ask_memory_tool.rs` as new, all under `crates/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: one `E0432` (``unresolved import `brokerd::builtin::ASK_MEMORY` ``), one `E0599` (``no variant, associated function, or constant named `AskMemory` found for enum `ToolArgs` ``) and nine `E0599` (the same for enum `ToolName`). 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_set` in `grants` and `grant_files_cannot_name_the_memory_tools_or_take_a_built_in_id` in `memory_tools`. - [ ] **3. The edits.** Make edits A1 to A9, B1, B2, C1, P1 and R1, in order. `cargo check -p brokerd` prints no error and no warning. - [ ] **4. The tests.** With a shell timeout of 600000 ms, every test passes: 9 in `ask_memory_tool`, 17 in `grants`, 4 in `memory_tools`, 5 in `memory_port`, 8 in `runner`: ```sh 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/src` lists exactly the five files of "Modify" under `crates/brokerd/src`: not `grants.rs`, `container.rs` or `memory_port.rs`. - `git diff --stat -- crates/bxcore crates/loopd crates/memoryd Cargo.toml Cargo.lock` prints nothing. - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, with a shell timeout of 600000 ms. Expected last line: `gate: ok`. The test count is the passed total of `cargo test --workspace` plus one, for the ignored `end_to_end` test that the gate runs on its own: **1381** after this task, 1372 before it; the 9 new ones are the tests of `ask_memory_tool.rs` (the new versions of `grants.rs` and `memory_tools.rs` keep their counts). To count, add up the `N passed` of every `test result:` line the gate's `cargo test --workspace` printed, 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 is `M5c2/02-brokerd-ask-memory`. `git status --short` lists the five source files, the two replaced tests and your log as modified, and `ask_memory_tool.rs` as new. Stage with `git add crates/brokerd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. `Cargo.lock` does not change in this task; the `git add` is harmless. ## Done when - `make gate` prints `gate: ok` with 1381 tests. - `git diff HEAD~1 --stat` lists only the five source files, the three test files of step 1 and `docs/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 check` in 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.