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>
203 lines
7.3 KiB
Markdown
203 lines
7.3 KiB
Markdown
# 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 `recall` or `memory` call goes to `memoryd`'s `brokerd` socket as a `memory_call`,
|
|
with the session's taint and untrusted flag **as `brokerd` held them when it decided**. Every
|
|
other call goes to the configured runtime, as before (`Routed`).
|
|
- `memoryd` knows the label of what it returns (a `recall` hit 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.
|
|
- `memoryd` not 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 given
|
|
`crates/brokerd/tests/support/rig.rs` (its `Config` literal gains `memory`)
|
|
- Append: `docs/plans/M5a/files/runbook-memoryd-unreachable.md` to the end of `docs/runbook.md`
|
|
(a blank line before it)
|
|
- Modify: `crates/brokerd/src/lib.rs` (`pub mod memory_port;` after `pub 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
|
|
|
|
1. In `struct Decision`, after `matched: Matched,` add:
|
|
|
|
```rust
|
|
/// 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:
|
|
|
|
```rust
|
|
pub fn state(&self) -> SessionState {
|
|
self.state
|
|
}
|
|
```
|
|
|
|
3. In `decide`, the `Mode::Auto => Outcome::Allowed(Decision { request, args, matched, })`
|
|
literal gains `state,` after `matched,`; in `redecide`, the `Ok(Decision { … matched, })`
|
|
literal gains `state,` after `matched,`. Both functions already have `state` as a parameter.
|
|
|
|
## `runner.rs`, exactly
|
|
|
|
1. `use crate::policy::Decision;` becomes `use crate::policy::{Decision, Label, SessionState};`.
|
|
2. In `struct RunSpec`, after `egress: Option<Vec<String>>,` add `state: SessionState,`; in
|
|
`impl RunSpec`, after `egress()` add:
|
|
|
|
```rust
|
|
/// The session's state when the call was decided.
|
|
pub fn state(&self) -> SessionState {
|
|
self.state
|
|
}
|
|
```
|
|
|
|
3. In `trait Runtime`, after `fn run(…);` add:
|
|
|
|
```rust
|
|
|
|
/// `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))
|
|
}
|
|
```
|
|
|
|
4. In `run`, the `RunSpec { … }` literal gains `state: decision.state(),` after `egress,`, and
|
|
|
|
```rust
|
|
match runtime.run(&spec) {
|
|
Ok(output) => ToolResponse::Result {
|
|
content: output.content,
|
|
class: label.class,
|
|
untrusted: label.untrusted,
|
|
truncated: output.truncated,
|
|
},
|
|
```
|
|
|
|
becomes
|
|
|
|
```rust
|
|
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`: after `pub type Log = …;` add
|
|
|
|
```rust
|
|
|
|
/// 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
|
|
|
|
```rust
|
|
// `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`: before `fn default_podman()` add
|
|
|
|
```rust
|
|
/// `[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
|
|
|
|
```rust
|
|
/// `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`: after `use brokerd::container::Podman;` add
|
|
`use brokerd::memory_port::{MEMORY_WAIT, MemoryPort, Routed};`, and just before
|
|
`let started = match serve::start(cfg, accept_break, runtime, log) {` add
|
|
|
|
```rust
|
|
// 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, with `cargo check -p brokerd` after each.
|
|
- [ ] **2. See it fail.** `cargo test -p brokerd --test memory_port`: 4 failed, 1 passed (the one
|
|
that never reaches `memoryd`).
|
|
- [ ] **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 gate` prints `gate: ok`.
|
|
|
|
## Stop and report if
|
|
|
|
- A `policy` compile-fail doc test starts to compile, or any other `brokerd` test fails.
|