brokerd: ask_memory, a built-in memory tool that memoryd runs

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-28 22:28:41 -07:00
co-authored by ornith-1.5-35b-a3b
parent 3c54096704
commit e9f103dfdd
9 changed files with 383 additions and 15 deletions
+21 -3
View File
@@ -4,6 +4,7 @@
//! path, a host or a URL is well formed. The module is pure: no I/O, no clock. Everything it reads
//! was written by the model, so it is treated as hostile.
use bxcore::AskMemoryArgs;
use bxcore::tools::{HttpFetchArgs, ReadFileArgs, ShellArgs, WriteFileArgs};
use bxcore::{MemoryArgs, RecallArgs};
use std::fmt;
@@ -15,8 +16,8 @@ pub const MAX_URL: usize = 2048;
/// Maximum length, in characters, of the detail an invalid call is denied with.
pub const MAX_DETAIL: usize = 300;
/// 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).
/// 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,
@@ -25,6 +26,7 @@ pub enum ToolName {
HttpFetch,
Recall,
Memory,
AskMemory,
}
impl ToolName {
@@ -45,6 +47,7 @@ impl ToolName {
"http_fetch" => Some(ToolName::HttpFetch),
"recall" => Some(ToolName::Recall),
"memory" => Some(ToolName::Memory),
"ask_memory" => Some(ToolName::AskMemory),
_ => None,
}
}
@@ -58,12 +61,16 @@ impl ToolName {
ToolName::HttpFetch => "http_fetch",
ToolName::Recall => "recall",
ToolName::Memory => "memory",
ToolName::AskMemory => "ask_memory",
}
}
/// Whether `memoryd` runs this tool rather than a container.
pub fn is_memory(self) -> bool {
matches!(self, ToolName::Recall | ToolName::Memory)
matches!(
self,
ToolName::Recall | ToolName::Memory | ToolName::AskMemory
)
}
}
@@ -84,6 +91,7 @@ pub enum ToolArgs {
HttpFetch(FetchUrl),
Recall(RecallArgs),
Memory(MemoryArgs),
AskMemory(AskMemoryArgs),
}
/// A URL that passed the checks, and its host. Only `parse` makes one, so the host policy matched
@@ -128,6 +136,7 @@ impl ToolArgs {
ToolArgs::HttpFetch(_) => ToolName::HttpFetch,
ToolArgs::Recall(_) => ToolName::Recall,
ToolArgs::Memory(_) => ToolName::Memory,
ToolArgs::AskMemory(_) => ToolName::AskMemory,
}
}
@@ -167,6 +176,9 @@ impl ToolArgs {
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())
}
}
}
}
@@ -284,6 +296,12 @@ pub fn parse(tool: ToolName, arguments: &str) -> Result<ToolArgs, ArgsError> {
value.check().map_err(ArgsError::Shape)?;
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))
}
}
}
+14 -7
View File
@@ -1,6 +1,7 @@
//! 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.
//! 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.
use bxcore::{Grant, GrantProblem, sha256};
@@ -12,17 +13,23 @@ pub const RESERVED: &str = "builtin-";
/// The built-in grants' text, hashed for the audit log like a grant file's bytes.
pub const RECALL: &str = "tool = \"recall\"\nmode = \"auto\"\nmax_taint = \"secret\"\nresult_class = \"private\"\nuntrusted = false\n";
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 and memory is built in; remove this grant";
pub const BUILT_IN: &str =
"the policy of recall, memory and ask_memory is built in; remove this grant";
/// The two built-in grants, `builtin-memory` and `builtin-recall`.
/// 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 [("builtin-memory", MEMORY), ("builtin-recall", RECALL)]:
// 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)];
let sources: &[(&str, &str)] = &[
("builtin-memory", MEMORY),
("builtin-recall", RECALL),
("builtin-ask-memory", ASK_MEMORY),
];
let mut out: Vec<LoadedGrant> = Vec::new();
for (id, text) in sources {
let problem = |error: String| GrantProblem {
+3 -1
View File
@@ -124,7 +124,9 @@ impl Runner {
crate::args::ToolName::Shell => self.shell_ms,
crate::args::ToolName::HttpFetch => self.http_fetch_ms,
// `memoryd` runs these; they never reach a container.
crate::args::ToolName::Recall | crate::args::ToolName::Memory => self.read_file_ms,
crate::args::ToolName::Recall
| crate::args::ToolName::Memory
| crate::args::ToolName::AskMemory => self.read_file_ms,
};
std::time::Duration::from_millis(ms)
}
+1 -1
View File
@@ -94,7 +94,7 @@ fn covers(args: &ToolArgs, grant: &Grant) -> Option<Option<String>> {
}
ToolArgs::Shell { cwd: Some(cwd), .. } => best_path(grant, cwd, true),
// The memory tools take no constraints: their grants are built in (M5a spec, section 8).
ToolArgs::Recall(_) | ToolArgs::Memory(_) => Some(None),
ToolArgs::Recall(_) | ToolArgs::Memory(_) | ToolArgs::AskMemory(_) => Some(None),
ToolArgs::HttpFetch(target) => {
if grant
.constraints
+1 -1
View File
@@ -129,7 +129,7 @@ pub fn run(decision: Decision, runtime: &dyn Runtime) -> bxcore::ToolResponse {
None,
),
ToolArgs::HttpFetch(_) => (Vec::new(), Some(decision.hosts().to_vec())),
ToolArgs::Recall(_) | ToolArgs::Memory(_) => (Vec::new(), None),
ToolArgs::Recall(_) | ToolArgs::Memory(_) | ToolArgs::AskMemory(_) => (Vec::new(), None),
};
let spec = RunSpec {
session: decision.request().session.clone(),