memoryd (files, the check, edits, journal, index, recall, serving), the memory tools and built-in grants in brokerd, the memory block in loopd, bxctl reindex and memory, and the device checks with memoryd. Each task was built as a reference and gated. Each was replayed on its own at its end state (854 to 936 tests) and mutation-checked. Each skeleton compiles and fails its tests. Every todo comment was read beside its signature. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
7.3 KiB
M5a task 12: decided memory calls go to memoryd, and labels only get stricter
Branch: m5a (run git switch m5a; git status --short must be empty, otherwise stop)
Commit subject: brokerd: memory calls to memoryd with the session's state; labels only stricter
Goal
M5a spec, section 8:
- A decided
recallormemorycall goes tomemoryd'sbrokerdsocket as amemory_call, with the session's taint and untrusted flag asbrokerdheld them when it decided. Every other call goes to the configured runtime, as before (Routed). memorydknows the label of what it returns (arecallhit from an untrusted session). The result keeps the stricter of that and the grant's label: the higher class, and untrusted if either says so. A runtime can never make a result laxer.- The container runtime refuses a memory tool rather than start a container for it.
memorydnot answering in 30 seconds is a failed result with a fixed sentence and a runbook pointer.
To carry the session's state, Decision gains a private field (decide and redecide already
receive the state), and RunSpec gains one too. Runtime gains run_labelled, with a default
that calls run and returns no label, so no existing runtime changes.
Files
- Copy (skeleton):
crates/brokerd/src/memory_port.rs - Copy (tests):
crates/brokerd/tests/memory_port.rs, and a new version of the givencrates/brokerd/tests/support/rig.rs(itsConfigliteral gainsmemory) - Append:
docs/plans/M5a/files/runbook-memoryd-unreachable.mdto the end ofdocs/runbook.md(a blank line before it) - Modify:
crates/brokerd/src/lib.rs(pub mod memory_port;afterpub mod ledger;),crates/brokerd/src/policy/mod.rs,crates/brokerd/src/runner.rs,crates/brokerd/src/container.rs,crates/brokerd/src/config.rs,crates/brokerd/src/main.rs,docs/implementer-log.md
The skeleton
Written: UNREACHABLE, MEMORY_WAIT, MemoryPort and new, Routed, and both run methods
(they call run_labelled). Fill: MemoryPort::ask, MemoryPort::run_labelled,
Routed::run_labelled.
policy/mod.rs, exactly
- In
struct Decision, aftermatched: Matched,add:
/// The session's state when the call was decided; `memoryd` needs it (M5a spec, section 8).
state: SessionState,
(Only Decision: Ask is unchanged.)
2. In impl Decision, after request() add:
pub fn state(&self) -> SessionState {
self.state
}
- In
decide, theMode::Auto => Outcome::Allowed(Decision { request, args, matched, })literal gainsstate,aftermatched,; inredecide, theOk(Decision { … matched, })literal gainsstate,aftermatched,. Both functions already havestateas a parameter.
runner.rs, exactly
use crate::policy::Decision;becomesuse crate::policy::{Decision, Label, SessionState};.- In
struct RunSpec, afteregress: Option<Vec<String>>,addstate: SessionState,; inimpl RunSpec, afteregress()add:
/// The session's state when the call was decided.
pub fn state(&self) -> SessionState {
self.state
}
- In
trait Runtime, afterfn run(…);add:
/// `run`, and a label for what came back when the runtime knows one (`memoryd` does: a
/// `recall` hit from an untrusted session). The runner keeps the stricter of this and the
/// grant's label. Container runtimes know none.
fn run_labelled(&self, spec: &RunSpec) -> Result<(RunOutput, Option<Label>), RunError> {
self.run(spec).map(|output| (output, None))
}
- In
run, theRunSpec { … }literal gainsstate: decision.state(),afteregress,, and
match runtime.run(&spec) {
Ok(output) => ToolResponse::Result {
content: output.content,
class: label.class,
untrusted: label.untrusted,
truncated: output.truncated,
},
becomes
match runtime.run_labelled(&spec) {
Ok((output, found)) => {
// Labels only get stricter: the higher class, and untrusted if either says so.
let (class, untrusted) = match found {
Some(found) => (
label.class.max(found.class),
label.untrusted || found.untrusted,
),
None => (label.class, label.untrusted),
};
ToolResponse::Result {
content: output.content,
class,
untrusted,
truncated: output.truncated,
}
}
(the Err arm below it is unchanged).
container.rs, config.rs, main.rs, exactly
container.rs: afterpub type Log = …;add
/// A memory tool that reached the container runtime: fixed, it reaches the model.
pub const IN_MEMORYD: &str = "the memory tools run in memoryd, not in a container";
and at the top of Podman's fn run, before let n = self.next.fetch_add(…), add
// `Routed` sends the memory tools to `memoryd`; one here is refused, never run.
if spec.tool().is_memory() {
return Err(RunError::Unavailable(IN_MEMORYD.to_string()));
}
config.rs: beforefn default_podman()add
/// `[memory]`: where `memoryd` takes the decided `recall` and `memory` calls. The memory tools are
/// always on, so the section is optional; an empty path means the default under `home`.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, default)]
pub struct Memory {
pub socket: PathBuf,
}
in struct Config, after the gateway field, add #[serde(default)] and
pub memory: Memory,; and before pub fn audit_dir add
/// `memoryd`'s socket for `brokerd`.
pub fn memory_socket(&self) -> PathBuf {
if self.memory.socket.as_os_str().is_empty() {
self.paths.home.join("run/broker-memory/memory.sock")
} else {
self.memory.socket.clone()
}
}
main.rs: afteruse brokerd::container::Podman;adduse brokerd::memory_port::{MEMORY_WAIT, MemoryPort, Routed};, and just beforelet started = match serve::start(cfg, accept_break, runtime, log) {add
// The memory tools go to `memoryd`, whatever runs the others (M5a spec, section 8).
let runtime: Box<dyn Runtime> = Box::new(Routed {
memory: MemoryPort::new(cfg.memory_socket(), MEMORY_WAIT),
other: runtime,
});
Steps
- 1. Copy the skeleton, the test and the new
rig.rs; append the runbook entry; make the edits, one file at a time, withcargo check -p brokerdafter each. - 2. See it fail.
cargo test -p brokerd --test memory_port: 4 failed, 1 passed (the one that never reachesmemoryd). - 3. Fill the three functions.
cargo fmt --all. - 4. See it pass. The same command, three times: 5 passed. Then
cargo test -p brokerd: every suite passes. - 5. Run the gate.
make gate:gate: ok, 5 more tests than before (about 929). - 6. Log and commit.
git add -A crates/brokerd docs/runbook.md docs/implementer-log.md Cargo.lock && git commit
Done when
make gateprintsgate: ok.
Stop and report if
- A
policycompile-fail doc test starts to compile, or any otherbrokerdtest fails.