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 testcrates/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-"), thenrsplit_once('-'). The right piece must be non-empty and all ASCII digits.SessionId::new(parent)on the left piece. Anything else (chat-1,sub-xwith no trailing digits, an empty parent) isNoneandreadreturnsSessionState::default()without writing.- Recursion.
sub-sub-mm-thread-1-2→ parentsub-mm-thread-1→ parentmm-thread.readof a missing middle child inherits from the live parent the same way. Do not special-case depth. raise, not a silent struct.inherit_or_defaultcallsstore.read(&parent)?thenstore.raise(session, SessionState::default(), Label { class: inherited.taint, untrusted: inherited.untrusted }).raisealready maxes class atPrivateand ORsuntrusted, and creates the file. A laterreadof the child hits the file.- Plain sessions.
a_session_with_no_file_is_private_and_trustedinstate.rsstill requires thatread("chat-1")creates nobroker/directory. Only asub-*miss writes. LabelandSessionStateare already imported instate.rs.- The items the edits use. This table is complete.
Item Signature StateStore::readread(&self, session: &SessionId) -> Result<SessionState, StateError>StateStore::raiseraise(&self, session: &SessionId, current: SessionState, label: Label) -> Result<SessionState, StateError>SessionState::defaultprivate + trusted Label{ class: DataClass, untrusted: bool }SessionId::newnew(s: &str) -> Result<SessionId, ValueError>SessionId::as_stras_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::readexists).a_new_subagent_copies_the_parents_untrusted_flagfails: the child is the default (private, trusted) andsub-mm-thread-3.jsondoes not exist.a_nested_subagent_copies_through_a_missing_middle_childfails: the leaf is notSecret/ untrusted.a_plain_session_still_creates_nothing_on_readpasses. -
3. The edits. I1, I2.
cargo check -p brokerdprints 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 instate_subagent.a_session_with_no_file_is_private_and_trustedstill passes. -
5. Run the gate.
cargo fmt --all, thenmake gate, timeout 600000 ms. Last linegate: ok. Test count 1590 (1587 before, plus 3). Aftergate: ok, do not add uptest result:lines. -
6. Log and commit. Task cell
M6a/05-brokerd-state-inherit. Read only the first 12 lines ofdocs/implementer-log.md; insert the row under the header. Stage withgit add crates/brokerd docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.
Done when
make gateprintsgate: okwith 1590 tests.git diff HEAD~1 --statlists onlycrates/brokerd/src/state.rs,crates/brokerd/tests/state_subagent.rsand 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 checkwarns;state.rsexisting tests fail; you write a state file fromloopd; 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.