Files
boxmaker/docs/plans/M6a/05-brokerd-state-inherit.md
T
2026-10-03 19:39:35 -07:00

7.1 KiB

M6a task 05: brokerd copies parent taint onto a new sub-* session

Branch: m6a. Check it: git branch --show-current prints m6a and git status --short is empty; otherwise stop. Never switch branch. Commit subject: brokerd: inherit parent taint for a new subagent Log row: the Task cell is M6a/05-brokerd-state-inherit.

First action. Run this before any Read of crate source. Do not ls crates/brokerd/tests/. Do not Read policy.rs, loopd, or docs/implementer-log.md (except the first 12 lines at the log step). The quoted block is the only edit in read.

cp docs/plans/M6a/files/crates/brokerd/tests/state_subagent.rs crates/brokerd/tests/

Goal

brokerd does not see the spawn (spec 3.4). The child's later tool calls, and memoryd's session-state question, go through StateStore::read. A missing file for a sub-<parent>-<n> id copies the parent's taint and untrusted flag via raise (so the child file exists). Nested ids recurse through a missing middle child. A missing file for any other id is still a fresh private trusted session and still creates nothing.

Spawn is not a grant. loopd does not write {home}/state/.

Every place that changes

Place What happens there
crates/brokerd/src/state.rs missing sub-* inherits; other missing ids unchanged
crates/brokerd/tests/state_subagent.rs new, given
everything else no change: not loopd, not raise's merge rules

Files

  • Copy, from docs/plans/M6a/files/ (the same path under it): the new test crates/brokerd/tests/state_subagent.rs.
  • Modify: crates/brokerd/src/state.rs, docs/implementer-log.md.
  • Do not touch: any other file. Do not edit crates/brokerd/tests/state.rs.

Facts you need

  • parent_of_sub. session.as_str().strip_prefix("sub-"), then rsplit_once('-'). The right piece must be non-empty and all ASCII digits. SessionId::new(parent) on the left piece. Anything else (chat-1, sub-x with no trailing digits, an empty parent) is None and read returns SessionState::default() without writing.
  • Recursion. sub-sub-mm-thread-1-2 → parent sub-mm-thread-1 → parent mm-thread. read of a missing middle child inherits from the live parent the same way. Do not special-case depth.
  • raise, not a silent struct. inherit_or_default calls store.read(&parent)? then store.raise(session, SessionState::default(), Label { class: inherited.taint, untrusted: inherited.untrusted }). raise already maxes class at Private and ORs untrusted, and creates the file. A later read of the child hits the file.
  • Plain sessions. a_session_with_no_file_is_private_and_trusted in state.rs still requires that read("chat-1") creates no broker/ directory. Only a sub-* miss writes.
  • Label and SessionState are already imported in state.rs.
  • The items the edits use. This table is complete.
    Item Signature
    StateStore::read read(&self, session: &SessionId) -> Result<SessionState, StateError>
    StateStore::raise raise(&self, session: &SessionId, current: SessionState, label: Label) -> Result<SessionState, StateError>
    SessionState::default private + trusted
    Label { class: DataClass, untrusted: bool }
    SessionId::new new(s: &str) -> Result<SessionId, ValueError>
    SessionId::as_str as_str(&self) -> &str

The edits, exactly

Each quoted "before" block occurs exactly once after task 04 (the file is unchanged since master).

crates/brokerd/src/state.rs

I1. The line (exactly once, inside read)

            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(SessionState::default()),

becomes

            Err(err) if err.kind() == io::ErrorKind::NotFound => {
                return inherit_or_default(self, session);
            }

I2. After impl StateStore (after persist's closing } and the impl's closing }, before /// One line on disk), add:

/// A missing `sub-<parent>-<n>` file copies the parent's taint and untrusted flag (M6 spec 3.4).
fn inherit_or_default(store: &StateStore, session: &SessionId) -> Result<SessionState, StateError> {
    let Some(parent) = parent_of_sub(session) else {
        return Ok(SessionState::default());
    };
    let inherited = store.read(&parent)?;
    store.raise(
        session,
        SessionState::default(),
        Label {
            class: inherited.taint,
            untrusted: inherited.untrusted,
        },
    )
}

fn parent_of_sub(session: &SessionId) -> Option<SessionId> {
    let rest = session.as_str().strip_prefix("sub-")?;
    let (parent, n) = rest.rsplit_once('-')?;
    if n.is_empty() || !n.bytes().all(|b| b.is_ascii_digit()) {
        return None;
    }
    SessionId::new(parent).ok()
}

Do not change raise. Do not change persist.

Steps

  • 1. Copy the given test.

    cp docs/plans/M6a/files/crates/brokerd/tests/state_subagent.rs crates/brokerd/tests/
    
  • 2. See it fail. cargo test -p brokerd --test state_subagent, timeout 600000 ms. The tests compile (StateStore::read exists). a_new_subagent_copies_the_parents_untrusted_flag fails: the child is the default (private, trusted) and sub-mm-thread-3.json does not exist. a_nested_subagent_copies_through_a_missing_middle_child fails: the leaf is not Secret / untrusted. a_plain_session_still_creates_nothing_on_read passes.

  • 3. The edits. I1, I2. cargo check -p brokerd prints no error and no warning. wc -l crates/brokerd/src/state.rs ≤ 500.

  • 4. The tests. cargo test -p brokerd --test state_subagent --test state: every test passes, 3 in state_subagent. a_session_with_no_file_is_private_and_trusted still passes.

  • 5. Run the gate. cargo fmt --all, then make gate, timeout 600000 ms. Last line gate: ok. Test count 1590 (1587 before, plus 3). After gate: ok, do not add up test result: lines.

  • 6. Log and commit. Task cell M6a/05-brokerd-state-inherit. Read only the first 12 lines of docs/implementer-log.md; insert the row under the header. Stage with git add crates/brokerd docs/implementer-log.md Cargo.lock, then commit as AGENTS.md says.

Done when

  • make gate prints gate: ok with 1590 tests.
  • git diff HEAD~1 --stat lists only crates/brokerd/src/state.rs, crates/brokerd/tests/state_subagent.rs and the log.

Stop and report if

  • The branch check fails; a "before" block is missing or duplicated; step 2 does not compile, or the plain-session test fails, or a different assertion fails; cargo check warns; state.rs existing tests fail; you write a state file from loopd; a test outside the named files fails in the gate (git stash -u, rerun, git stash pop, report both).

What the given tests pin

read("sub-mm-thread-3") of a parent raised to private+untrusted returns that pair and creates the child file; read("sub-sub-mm-thread-1-2") with only mm-thread on disk returns secret+untrusted; read("chat-1") still creates no broker/ directory.