bxctl: reindex, and memory log, undo, pending, accept, reject
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
+13
-1
@@ -28,7 +28,13 @@ usage: bxctl <command> [options]
|
||||
Check that the grants load.
|
||||
|
||||
bxctl audit verify [--home DIR]
|
||||
Check the audit log's hash chain, reading the files directly.";
|
||||
Check the audit log's hash chain, reading the files directly.
|
||||
|
||||
bxctl reindex [--memory-socket PATH]
|
||||
Rebuild memoryd's index from the session logs and notes.
|
||||
|
||||
bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> [--memory-socket PATH]
|
||||
List memory edits, undo the latest edit of a file, or answer writes waiting for review.";
|
||||
|
||||
// The options every `chat` call carries. `socket` is the loop socket; `admin_socket` is the admin
|
||||
// socket the other subcommands use. Both default to the paths under `$BOXMAKER_HOME`.
|
||||
@@ -90,6 +96,10 @@ pub enum Command {
|
||||
AuditVerify {
|
||||
home: PathBuf,
|
||||
},
|
||||
Memory {
|
||||
socket: PathBuf,
|
||||
ask: crate::memory::Ask,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn parse(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
@@ -104,6 +114,8 @@ pub fn parse(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
"refuse" | "deny" => parse_refuse(rest, home),
|
||||
"grants" => parse_grants(rest, home),
|
||||
"audit" => parse_audit(rest, home),
|
||||
"reindex" => crate::memory::parse_reindex(rest, home),
|
||||
"memory" => crate::memory::parse_memory(rest, home),
|
||||
// A flag before the command, or any unknown command, is not valid.
|
||||
_ => Err(UsageError),
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ pub mod admin;
|
||||
pub mod chat;
|
||||
pub mod cli;
|
||||
pub mod escape;
|
||||
pub mod memory;
|
||||
pub mod verify;
|
||||
|
||||
@@ -49,6 +49,16 @@ fn main() -> ExitCode {
|
||||
reason,
|
||||
} => exit(|| cmd_refuse(&admin_socket, approval, reason.as_deref())),
|
||||
Command::GrantsCheck { admin_socket } => exit(|| cmd_grants_check(&admin_socket)),
|
||||
Command::Memory { socket, ask } => {
|
||||
let mut out = std::io::stdout().lock();
|
||||
match bxctl::memory::run(&socket, &ask, bxctl::memory::MEMORY_TIMEOUT, &mut out) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("bxctl: {e}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::AuditVerify { home } => {
|
||||
let mut out = std::io::stdout().lock();
|
||||
match bxctl::verify::run(&home, &mut out) {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! The owner's memory commands over `memoryd`'s owner socket: `reindex`, and `memory log`,
|
||||
//! `undo`, `pending`, `accept`, `reject` (M5a spec, section 10).
|
||||
|
||||
use std::io::Write;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use proto::{
|
||||
Empty, Envelope, MemoryAdmin, MemoryCommand, Message, PROTOCOL_VERSION, read_frame, write_frame,
|
||||
};
|
||||
|
||||
use crate::cli::{Command, UsageError};
|
||||
use crate::escape::escape_json_text;
|
||||
|
||||
/// How long `bxctl` waits for `memoryd`: a reindex reads every session log.
|
||||
pub const MEMORY_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
pub fn default_socket(home: &Path) -> PathBuf {
|
||||
home.join("run").join("owner-memory").join("admin.sock")
|
||||
}
|
||||
|
||||
/// What to ask `memoryd`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Ask {
|
||||
Reindex,
|
||||
Admin(MemoryCommand, String),
|
||||
}
|
||||
|
||||
/// `bxctl reindex [--memory-socket PATH]`.
|
||||
pub fn parse_reindex(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
let (socket, rest) = socket_flag(args, home)?;
|
||||
if !rest.is_empty() {
|
||||
return Err(UsageError);
|
||||
}
|
||||
Ok(Command::Memory {
|
||||
socket,
|
||||
ask: Ask::Reindex,
|
||||
})
|
||||
}
|
||||
|
||||
/// `bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id>`, each with an optional
|
||||
/// `--memory-socket PATH`.
|
||||
pub fn parse_memory(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
let (socket, rest) = socket_flag(args, home)?;
|
||||
let rest: Vec<&str> = rest.iter().map(String::as_str).collect();
|
||||
let ask = match rest.as_slice() {
|
||||
["log"] => Ask::Admin(MemoryCommand::Log, String::new()),
|
||||
["log", n] => {
|
||||
if n.parse::<usize>().is_err() {
|
||||
return Err(UsageError);
|
||||
}
|
||||
Ask::Admin(MemoryCommand::Log, n.to_string())
|
||||
}
|
||||
["pending"] => Ask::Admin(MemoryCommand::Pending, String::new()),
|
||||
["undo", id] => Ask::Admin(MemoryCommand::Undo, id.to_string()),
|
||||
["accept", id] => Ask::Admin(MemoryCommand::Accept, id.to_string()),
|
||||
["reject", id] => Ask::Admin(MemoryCommand::Reject, id.to_string()),
|
||||
_ => return Err(UsageError),
|
||||
};
|
||||
Ok(Command::Memory { socket, ask })
|
||||
}
|
||||
|
||||
/// Take `--memory-socket PATH` out of `args`, once at most; the rest in order.
|
||||
fn socket_flag(args: &[String], home: &Path) -> Result<(PathBuf, Vec<String>), UsageError> {
|
||||
let mut socket = default_socket(home);
|
||||
let mut rest = Vec::with_capacity(args.len());
|
||||
let mut seen = false;
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
if args[i] == "--memory-socket" {
|
||||
if seen {
|
||||
return Err(UsageError);
|
||||
}
|
||||
seen = true;
|
||||
socket = PathBuf::from(args.get(i + 1).cloned().ok_or(UsageError)?);
|
||||
i += 2;
|
||||
} else {
|
||||
rest.push(args[i].clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
Ok((socket, rest))
|
||||
}
|
||||
|
||||
/// `n` with commas between thousands.
|
||||
fn grouped(n: u64) -> String {
|
||||
let digits = n.to_string();
|
||||
let mut out = String::with_capacity(digits.len() + digits.len() / 3);
|
||||
for (i, c) in digits.chars().enumerate() {
|
||||
if i > 0 && (digits.len() - i).is_multiple_of(3) {
|
||||
out.push(',');
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Ask `memoryd` and print its answer. `Err` is the line for stderr.
|
||||
pub fn run(socket: &Path, ask: &Ask, timeout: Duration, out: &mut dyn Write) -> Result<(), String> {
|
||||
let mut stream = UnixStream::connect(socket)
|
||||
.map_err(|e| format!("cannot reach memoryd at {}: {e}", socket.display()))?;
|
||||
stream
|
||||
.set_read_timeout(Some(timeout))
|
||||
.map_err(|e| format!("cannot reach memoryd at {}: {e}", socket.display()))?;
|
||||
stream
|
||||
.set_write_timeout(Some(timeout))
|
||||
.map_err(|e| format!("cannot reach memoryd at {}: {e}", socket.display()))?;
|
||||
|
||||
let msg = match ask {
|
||||
Ask::Reindex => Message::Reindex(Empty {}),
|
||||
Ask::Admin(command, arg) => Message::MemoryAdmin(MemoryAdmin {
|
||||
command: *command,
|
||||
arg: arg.clone(),
|
||||
}),
|
||||
};
|
||||
let env = Envelope {
|
||||
v: PROTOCOL_VERSION,
|
||||
id: 1,
|
||||
r#final: true,
|
||||
msg,
|
||||
};
|
||||
write_frame(&mut stream, &env)
|
||||
.map_err(|e| format!("cannot reach memoryd at {}: {e}", socket.display()))?;
|
||||
|
||||
let reply = read_frame(&mut stream).map_err(|e| format!("memoryd did not answer: {e}"))?;
|
||||
let text = match reply.msg {
|
||||
Message::Reindexed(r) => format!(
|
||||
"reindexed: {} logs, {} records, {} notes",
|
||||
grouped(r.logs),
|
||||
grouped(r.records),
|
||||
grouped(r.notes)
|
||||
),
|
||||
Message::MemoryAdminReply(reply) => reply.text,
|
||||
Message::Error(e) => return Err(format!("memoryd: {}", escape_json_text(&e.detail))),
|
||||
_ => return Err("memoryd answered with the wrong message".to_string()),
|
||||
};
|
||||
|
||||
writeln!(out, "{text}").map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//! `bxctl reindex` and `bxctl memory`: parsing, what is sent to `memoryd`, and what is printed
|
||||
//! (M5a spec, section 10). Do not edit.
|
||||
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bxctl::cli::{Command, UsageError, parse};
|
||||
use bxctl::memory::{Ask, run};
|
||||
use proto::{
|
||||
Envelope, ErrorCode, MemoryAdminReply, MemoryCommand, Message, Reindexed, WireError,
|
||||
read_frame, write_frame,
|
||||
};
|
||||
|
||||
fn args(line: &str) -> Vec<String> {
|
||||
line.split_whitespace().map(str::to_string).collect()
|
||||
}
|
||||
|
||||
fn home() -> PathBuf {
|
||||
PathBuf::from("/srv/bx")
|
||||
}
|
||||
|
||||
fn memory(ask: Ask) -> Result<Command, UsageError> {
|
||||
Ok(Command::Memory {
|
||||
socket: PathBuf::from("/srv/bx/run/owner-memory/admin.sock"),
|
||||
ask,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_commands_parse() {
|
||||
let h = home();
|
||||
assert_eq!(parse(&args("reindex"), &h), memory(Ask::Reindex));
|
||||
assert_eq!(
|
||||
parse(&args("memory log"), &h),
|
||||
memory(Ask::Admin(MemoryCommand::Log, String::new()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&args("memory log 3"), &h),
|
||||
memory(Ask::Admin(MemoryCommand::Log, "3".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&args("memory undo 2026-09-24-0007"), &h),
|
||||
memory(Ask::Admin(
|
||||
MemoryCommand::Undo,
|
||||
"2026-09-24-0007".to_string()
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&args("memory pending"), &h),
|
||||
memory(Ask::Admin(MemoryCommand::Pending, String::new()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&args("memory accept p-1"), &h),
|
||||
memory(Ask::Admin(MemoryCommand::Accept, "p-1".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&args("memory reject p-1"), &h),
|
||||
memory(Ask::Admin(MemoryCommand::Reject, "p-1".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&args("reindex --memory-socket /r/m.sock"), &h),
|
||||
Ok(Command::Memory {
|
||||
socket: PathBuf::from("/r/m.sock"),
|
||||
ask: Ask::Reindex
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse(&args("memory --memory-socket /r/m.sock undo x"), &h),
|
||||
Ok(Command::Memory {
|
||||
socket: PathBuf::from("/r/m.sock"),
|
||||
ask: Ask::Admin(MemoryCommand::Undo, "x".to_string())
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_lines_are_usage_errors() {
|
||||
for line in [
|
||||
"memory",
|
||||
"memory log ten",
|
||||
"memory log 1 2",
|
||||
"memory undo",
|
||||
"memory undo a b",
|
||||
"memory forget x",
|
||||
"memory pending x",
|
||||
"reindex now",
|
||||
"reindex --memory-socket",
|
||||
"reindex --memory-socket /a --memory-socket /b",
|
||||
] {
|
||||
assert_eq!(parse(&args(line), &home()), Err(UsageError), "{line}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A fake `memoryd` answering one request with `answer`; hands back what it was sent.
|
||||
fn fake(socket: &Path, answer: Message) -> mpsc::Receiver<Message> {
|
||||
let listener = UnixListener::bind(socket).unwrap();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().unwrap();
|
||||
let env = read_frame(&mut stream).unwrap();
|
||||
tx.send(env.msg).unwrap();
|
||||
let reply = Envelope {
|
||||
v: 1,
|
||||
id: env.id,
|
||||
r#final: true,
|
||||
msg: answer,
|
||||
};
|
||||
write_frame(&mut stream, &reply).unwrap();
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
fn dir(tag: &str) -> PathBuf {
|
||||
let path = std::env::temp_dir().join(format!("bx-mem-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
std::fs::create_dir_all(&path).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reindex_prints_the_counts() {
|
||||
let d = dir("reindex");
|
||||
let socket = d.join("admin.sock");
|
||||
let seen = fake(
|
||||
&socket,
|
||||
Message::Reindexed(Reindexed {
|
||||
logs: 42,
|
||||
records: 1208,
|
||||
notes: 7,
|
||||
}),
|
||||
);
|
||||
let mut out = Vec::new();
|
||||
run(&socket, &Ask::Reindex, Duration::from_secs(5), &mut out).unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8(out).unwrap(),
|
||||
"reindexed: 42 logs, 1,208 records, 7 notes\n"
|
||||
);
|
||||
assert!(matches!(seen.recv().unwrap(), Message::Reindex(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_admin_command_prints_the_reply_and_an_error_is_escaped() {
|
||||
let d = dir("admin");
|
||||
let socket = d.join("admin.sock");
|
||||
let seen = fake(
|
||||
&socket,
|
||||
Message::MemoryAdminReply(MemoryAdminReply {
|
||||
text: "undone: 2026-09-24-0007".to_string(),
|
||||
}),
|
||||
);
|
||||
let mut out = Vec::new();
|
||||
let ask = Ask::Admin(MemoryCommand::Undo, "2026-09-24-0007".to_string());
|
||||
run(&socket, &ask, Duration::from_secs(5), &mut out).unwrap();
|
||||
assert_eq!(String::from_utf8(out).unwrap(), "undone: 2026-09-24-0007\n");
|
||||
match seen.recv().unwrap() {
|
||||
Message::MemoryAdmin(m) => {
|
||||
assert_eq!(
|
||||
(m.command, m.arg.as_str()),
|
||||
(MemoryCommand::Undo, "2026-09-24-0007")
|
||||
)
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
let socket = d.join("refuses.sock");
|
||||
let _seen = fake(
|
||||
&socket,
|
||||
Message::Error(WireError {
|
||||
code: ErrorCode::Internal,
|
||||
detail: "user.md has changed\u{1b}[31m".to_string(),
|
||||
}),
|
||||
);
|
||||
let err = run(&socket, &ask, Duration::from_secs(5), &mut Vec::new()).unwrap_err();
|
||||
assert!(err.starts_with("memoryd: user.md has changed"), "{err}");
|
||||
assert!(!err.contains('\u{1b}'), "escaped: {err}");
|
||||
let err = run(
|
||||
&d.join("nobody.sock"),
|
||||
&ask,
|
||||
Duration::from_secs(1),
|
||||
&mut Vec::new(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.starts_with("cannot reach memoryd at "), "{err}");
|
||||
}
|
||||
@@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
|
||||
|
||||
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| M5a/14-bxctl-memory | 2026-09-24 | done | 1 | pass | none | Copied the skeleton `crates/bxctl/src/memory.rs` and the test `crates/bxctl/tests/cli_memory.rs` into place, and added `pub mod memory;` after `pub mod escape;` in `lib.rs`. In `cli.rs` opened the last USAGE entry (the audit line lost its closing `";`) and appended the `reindex` and `memory` blocks, added the `Memory { socket, ask }` variant to `Command` after `AuditVerify`, and the `reindex`/`memory` arms to `parse`. In `main.rs` added the `Command::Memory` arm before `AuditVerify`. Filled the five functions the skeleton left as `todo!()` in `memory.rs`: `parse_reindex` (`socket_flag`, then any argument left is a usage error, else `Ask::Reindex`); `parse_memory` (`socket_flag`, then a slice match on the rest — `["log"]`, `["log", n]` when `n` parses as usize, `["pending"]`, `["undo"|"accept"|"reject", id]`, anything else a usage error); `socket_flag` (walk args, `--memory-socket` takes the next argument as the socket, a second one or a missing value is a usage error, every other argument kept in order, the socket defaults to `default_socket(home)`); `grouped` (the digits of n with a comma before each char at index i>0 where `len-i` is a multiple of 3); and `run` (connect to the socket, set read and write timeouts, write the id-1 final envelope, read the reply, print the reindexed counts or the reply text, escape an error detail, a write error as a String). `cargo fmt --all`; `cli_memory` 4 passed (they failed on the `todo!()`s first), the full bxctl suite green; `make gate` printed `gate: ok` on the first run. No new dependency, so `Cargo.lock` is unchanged. | ? |
|
||||
| M5a/13-loopd-memory | 2026-09-24 | done | 1 | pass | none | Copied the skeleton `crates/loopd/src/memory.rs` and the tests `crates/loopd/tests/memory.rs` plus new versions of `crates/loopd/tests/baseline.rs`, `crates/loopd/tests/pointers.rs` and `crates/loopd/tests/channel.rs` into place, and made the runbook changes (replaced `## core-memory-unreadable` with the contents of `runbook-memory-unavailable.md`, appended `runbook-memory-index-damaged.md` to the end). Added `pub mod memory;` after `pub mod llama;` in `lib.rs`. In `config.rs` added `#[serde(default)] pub memory: Memory,` after `pub broker: Broker,` in `Config`, the `Memory` struct (empty socket means the default under `home`) after `struct Config`, and `memory_socket()` before `channel_socket`. In `baseline.rs` changed the module comment ("the memory block and core tool schemas"), removed the `Core(PathBuf, io::Error)` variant and its Display arm (the `#core-memory-unreadable` one), and replaced `assemble` whole with `assemble` (calls `assemble_with(cfg, registry, "")`) and `assemble_with` (system trimmed, then a blank line and the trimmed block when non-empty). In `tools.rs` added `m5a()` before `core_schemas`: `m3a().entries` with `recall` (id 1) and `memory` (id 2) inserted as core. In `channel.rs` added `use crate::memory::MemorySource;`, the `memory: Box<dyn MemorySource>` field on `Context` and a last `memory` param on `Context::new`; in `handle` replaced the `Baseline::assemble` call with `ctx.memory.block().and_then(|block| Baseline::assemble_with(...))`, and after `drop(held)` in step 5 added the best-effort `ctx.memory.updated(&turn.session)` for a finished turn. In `main.rs` imported `MEMORY_WAIT, MemoryPort` and built `MemoryPort::new(cfg.memory_socket(), MEMORY_WAIT)` with `Registry::m5a()`. Appended the memory sentence to `config/system.md`. Filled the three `todo!()`s in `memory.rs`: `MemoryPort::ask` (connect, set read and write timeouts to `self.wait`, write the final id-1 envelope, read it — `Ok(its msg)`; every error `format!("memoryd at {}: {e}; {UNAVAILABLE}", socket)`); `block` (`MemoryBlock(Empty)` -> `MemoryBlockReply.reply.text`; `Error` -> `memoryd: {detail}; {UNAVAILABLE}`; else `wrong message; {UNAVAILABLE}`); `updated` (`SessionUpdated` -> `Ok(())`; `Error` -> `memoryd: {detail}`; else `wrong message`). `cargo fmt --all`; `memory` 4 and `pointers` 7 passed, the full loopd suite green; `make gate` printed `gate: ok` on the first run. No new dependency, so `Cargo.lock` is unchanged. | ? |
|
||||
| M5a/12-brokerd-memory-port | 2026-09-24 | done | 2 | fail | none | Copied the skeleton `crates/brokerd/src/memory_port.rs` and the test `crates/brokerd/tests/memory_port.rs` into place, a new `support/rig.rs` (its `Config` literal gains `memory: Default::default()`), and appended the `memoryd-unreachable` runbook entry. Added `pub mod memory_port;` after `pub mod ledger;` in `lib.rs`. In `policy/mod.rs` added the private `state` field to `Decision`, the `state()` accessor after `request()`, and threaded `state` through the `decide`/`redecide` literals (both already take it as a parameter). In `runner.rs` widened the `use` to `::{Decision, Label, SessionState}`; `RunSpec` gained a `state` field and `state()`; `Runtime` gained a default `run_labelled` that calls `run` and returns no label so existing runtimes are unchanged; `run` now calls `run_labelled` and keeps the stricter of the found label and the grant's (higher class via `.max`, untrusted if either). In `container.rs` added the `IN_MEMORYD` const and a `run` guard that refuses a memory tool before spawning. In `config.rs` added the `Memory` struct before `default_podman`, the `memory` field on `Config`, and `memory_socket()` before `audit_dir`. In `main.rs` imported `MemoryPort`/`Routed`/`MEMORY_WAIT` and wrapped the runtime in `Routed { memory: MemoryPort::new(cfg.memory_socket(), MEMORY_WAIT), other }`. Filled `MemoryPort::ask` (build the `MemoryCall` with the spec's session/call/tool/canonical arguments and the state's taint/untrusted, connect, set read and write timeouts to `wait`, write the final envelope, read it — `Some` only on a `Message::ToolResponse`, else `None`); `MemoryPort::run_labelled` (`Result` -> `Ok((output, Some(label)))`, `Failed` -> `Failed`, anything else or `None` -> `Unreachable`); `Routed::run_labelled` (memory tools to `memoryd`, everything else to `other`). `cargo fmt --all`; `memory_port` 5 passed and the full brokerd suite green; `make gate` printed `gate: ok` on the second run — the first failed on rustfmt import order in `memory_port.rs`, fixed by re-running `cargo fmt --all`. No new dependency, so `Cargo.lock` is unchanged. | ? |
|
||||
| M5a/11-brokerd-memory-tools | 2026-09-24 | done | 2 | pass | none | Copied the skeleton `crates/brokerd/src/builtin.rs` and the tests `crates/brokerd/tests/memory_tools.rs` and a new `grants.rs` (the empty-directory test now asserts `matchable()` sees the two built-ins rather than comparing to an empty set), and added `pub mod builtin;` after `pub mod broker;` in `lib.rs`. Filled `builtin::grants`, the only `todo!()`: for each `(id, text)` of `[("builtin-memory", MEMORY), ("builtin-recall", RECALL)]`, `toml::from_str::<Grant>` and `sha256(text.as_bytes())` each error as a `GrantProblem { file: id, line: None, problem: e }`, then a `LoadedGrant { id, grant, sha256 }`, collected into `Result<Vec<LoadedGrant>, GrantProblem>` (first error wins); the tests failed on the `todo!()` first, then passed. In `args.rs` added `use proto::{MemoryArgs, RecallArgs}`; the `Recall`/`Memory` variants of `ToolName` with their `parse`/`as_str` arms, the `is_memory` method, and the `Recall`/`Memory` variants of `ToolArgs` with their `tool`/`canonical_json`/`parse` arms. In `grants.rs` added the `builtin` field to `GrantSet`, the `builtin` join at the end of `from_grants` (only when the set passed), `matchable`, Rule 2b for ids starting `builtin-`, and the built-in sentence for a memory tool in `check_grant`. In `policy/matching.rs` matched against `matchable()` and gave the memory tools a constraint-free `covers` arm; in `runner.rs` routed them to no mounts/egress; in `config.rs` gave them the read_file time limit. `cargo fmt --all`; `memory_tools` 4 and `grants` 17 passed; the full brokerd suite green; `make gate` printed `gate: ok` on the first run. No new dependency, so `Cargo.lock` is unchanged. | ? |
|
||||
|
||||
Reference in New Issue
Block a user