Files
boxmaker/docs/plans/M5c2/02-brokerd-ask-memory.md
2026-09-28 21:27:10 -07:00

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, 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 12. 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):

    #[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 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

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 --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:

    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.