memoryd: where a finished turn goes in Honcho, decided from brokerd and the log
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,191 @@
|
|||||||
|
//! Where a finished turn goes in Honcho (M5c spec, sections 4 and 5): this agent's trusted or
|
||||||
|
//! untrusted workspace, or nowhere, and why. Pure: the session's state as `brokerd` answered it
|
||||||
|
//! and the session's log are given, and nothing is read or written here.
|
||||||
|
|
||||||
|
use bxcore::sanitize::refuse;
|
||||||
|
use bxcore::{DataClass, LogRecord, SessionStateReply};
|
||||||
|
|
||||||
|
use crate::honcho::config::is_name;
|
||||||
|
use crate::honcho::ledger::{Action, Workspace};
|
||||||
|
|
||||||
|
/// Why a turn goes nowhere: the `why` of its `Action::Skip`.
|
||||||
|
pub const SECRET_CLASS: &str = "the session is secret";
|
||||||
|
pub const NO_SPEAKER: &str = "the turn has no single speaker";
|
||||||
|
pub const BAD_SPEAKER: &str = "the speaker is not a Honcho peer name";
|
||||||
|
pub const AGENT_SPEAKER: &str = "the speaker has the agent's peer name";
|
||||||
|
pub const NO_REPLY: &str = "the turn has no reply";
|
||||||
|
pub const SECRET_TEXT: &str = "the turn's text holds a secret";
|
||||||
|
|
||||||
|
/// The turn asked for is not in the log.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct NoSuchTurn {
|
||||||
|
pub turn: u64,
|
||||||
|
pub turns: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for NoSuchTurn {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"turn {} is not in the session's log, which has {} turns",
|
||||||
|
self.turn, self.turns
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for NoSuchTurn {}
|
||||||
|
|
||||||
|
/// How many turns the log holds: its `User` records.
|
||||||
|
pub fn turns(records: &[LogRecord]) -> u64 {
|
||||||
|
let count = records
|
||||||
|
.iter()
|
||||||
|
.filter(|r| matches!(r, LogRecord::User { .. }))
|
||||||
|
.count();
|
||||||
|
u64::try_from(count).unwrap_or(u64::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where turn `turn` (counted from 1) lies in the log: the index of its `User` record, and the
|
||||||
|
/// index just past its last record (the next `User` record's, or the log's length). `None` when
|
||||||
|
/// the log has no such turn.
|
||||||
|
fn bounds(records: &[LogRecord], turn: u64) -> Option<(usize, usize)> {
|
||||||
|
let mut seen: u64 = 0;
|
||||||
|
let mut start: Option<usize> = None;
|
||||||
|
for (index, record) in records.iter().enumerate() {
|
||||||
|
if let LogRecord::User { .. } = record {
|
||||||
|
if let Some(start) = start {
|
||||||
|
return Some((start, index));
|
||||||
|
}
|
||||||
|
seen += 1;
|
||||||
|
if seen == turn {
|
||||||
|
start = Some(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start.map(|start| (start, records.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn `turn`'s records, from its `User` record to the record before the next `User`.
|
||||||
|
pub fn turn_records(records: &[LogRecord], turn: u64) -> Option<&[LogRecord]> {
|
||||||
|
let (start, end) = bounds(records, turn)?;
|
||||||
|
records.get(start..end)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Mattermost username as a Honcho peer name: each `.` becomes `_` (M5c spec, section 3).
|
||||||
|
pub fn peer_name(speaker: &str) -> String {
|
||||||
|
speaker.replace('.', "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `text` holds a known-prefix secret, by `refuse`'s secret rules alone. `refuse` gives
|
||||||
|
/// only the first rule a text breaks, and its character rules come first, so each character they
|
||||||
|
/// match is first replaced by a space: it is not a key character and not a letter, so a secret's
|
||||||
|
/// prefix and run are found as before, and a carriage return cannot hide a key.
|
||||||
|
pub fn has_secret(text: &str) -> bool {
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
let masked: String = text
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
if refuse(c.encode_utf8(&mut buf)).is_some() {
|
||||||
|
' '
|
||||||
|
} else {
|
||||||
|
c
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
refuse(&masked).is_some_and(|r| r.rule.starts_with("secret-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The turn's user text and its speaker, from its first record, the `User` one.
|
||||||
|
fn user_text(turn: &[LogRecord]) -> (&str, Option<&str>) {
|
||||||
|
match turn.first() {
|
||||||
|
Some(LogRecord::User {
|
||||||
|
content, speaker, ..
|
||||||
|
}) => (content.as_str(), speaker.as_deref()),
|
||||||
|
_ => ("", None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The turn's final reply: the content of its last `Assistant` record, when that record asks for
|
||||||
|
/// no tool. A turn that stopped in a tool round, or before any answer, has none.
|
||||||
|
fn final_reply(turn: &[LogRecord]) -> Option<&str> {
|
||||||
|
let last = turn
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|r| matches!(r, LogRecord::Assistant { .. }));
|
||||||
|
match last {
|
||||||
|
Some(LogRecord::Assistant {
|
||||||
|
content,
|
||||||
|
tool_calls,
|
||||||
|
..
|
||||||
|
}) if tool_calls.is_empty() => content.as_deref(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether any tool result up to the turn's end is labelled untrusted.
|
||||||
|
fn labelled_untrusted(records: &[LogRecord]) -> bool {
|
||||||
|
records.iter().any(|r| {
|
||||||
|
matches!(
|
||||||
|
r,
|
||||||
|
LogRecord::ToolResult {
|
||||||
|
untrusted: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where turn `turn` of the session whose log is `records` goes. `state` is `brokerd`'s answer
|
||||||
|
/// for the session; `agent` is `[honcho] agent`. The checks run in this order, and the first that
|
||||||
|
/// holds decides: the class `secret`; no speaker; a speaker that is not a peer name, or is the
|
||||||
|
/// agent's; no reply; a secret in the user's text or the reply. Otherwise the turn goes to the
|
||||||
|
/// untrusted workspace if `brokerd` or any tool result up to the turn's end says untrusted, else
|
||||||
|
/// to the trusted one.
|
||||||
|
pub fn decide(
|
||||||
|
state: &SessionStateReply,
|
||||||
|
records: &[LogRecord],
|
||||||
|
turn: u64,
|
||||||
|
agent: &str,
|
||||||
|
) -> Result<Action, NoSuchTurn> {
|
||||||
|
let missing = NoSuchTurn {
|
||||||
|
turn,
|
||||||
|
turns: turns(records),
|
||||||
|
};
|
||||||
|
let (start, end) = bounds(records, turn).ok_or(missing)?;
|
||||||
|
let upto = records.get(..end).ok_or(missing)?;
|
||||||
|
let this = records.get(start..end).ok_or(missing)?;
|
||||||
|
let skip = |why: &str| {
|
||||||
|
Ok(Action::Skip {
|
||||||
|
why: why.to_string(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.class == DataClass::Secret {
|
||||||
|
return skip(SECRET_CLASS);
|
||||||
|
}
|
||||||
|
let (text, speaker) = user_text(this);
|
||||||
|
let Some(speaker) = speaker else {
|
||||||
|
return skip(NO_SPEAKER);
|
||||||
|
};
|
||||||
|
let peer = peer_name(speaker);
|
||||||
|
if !is_name(&peer) {
|
||||||
|
return skip(BAD_SPEAKER);
|
||||||
|
}
|
||||||
|
if peer == agent {
|
||||||
|
return skip(AGENT_SPEAKER);
|
||||||
|
}
|
||||||
|
let Some(reply) = final_reply(this).filter(|r| !r.trim().is_empty()) else {
|
||||||
|
return skip(NO_REPLY);
|
||||||
|
};
|
||||||
|
if has_secret(text) || has_secret(reply) {
|
||||||
|
return skip(SECRET_TEXT);
|
||||||
|
}
|
||||||
|
let workspace = if state.untrusted || labelled_untrusted(upto) {
|
||||||
|
Workspace::Untrusted
|
||||||
|
} else {
|
||||||
|
Workspace::Trusted
|
||||||
|
};
|
||||||
|
Ok(Action::Turn {
|
||||||
|
workspace,
|
||||||
|
speaker: peer,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
//! Honcho, the memory this agent shares with the fleet, reached through `roncho` (M5c spec).
|
//! Honcho, the memory this agent shares with the fleet, reached through `roncho` (M5c spec).
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod decide;
|
||||||
pub mod ledger;
|
pub mod ledger;
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
//! Where a finished turn goes in Honcho (M5c spec, sections 4 and 5): the trusted or untrusted
|
||||||
|
//! workspace, or nowhere and why, from `brokerd`'s answer and the session's log; the order of the
|
||||||
|
//! checks; and the helpers that find a turn in the log. Do not edit.
|
||||||
|
|
||||||
|
use bxcore::sanitize::TAGS;
|
||||||
|
use bxcore::{
|
||||||
|
CallId, DataClass, Epoch, Hash32, LogRecord, SessionId, SessionStateReply, Timestamp, ToolCall,
|
||||||
|
};
|
||||||
|
use memoryd::honcho::decide::{
|
||||||
|
AGENT_SPEAKER, BAD_SPEAKER, NO_REPLY, NO_SPEAKER, NoSuchTurn, SECRET_CLASS, SECRET_TEXT,
|
||||||
|
decide, has_secret, peer_name, turn_records, turns,
|
||||||
|
};
|
||||||
|
use memoryd::honcho::ledger::{Action, Workspace};
|
||||||
|
|
||||||
|
const AGENT: &str = "boxmaker0";
|
||||||
|
|
||||||
|
fn time() -> Timestamp {
|
||||||
|
Timestamp::parse("2026-09-28T10:00:00.000Z").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start() -> LogRecord {
|
||||||
|
LogRecord::SessionStart {
|
||||||
|
time: time(),
|
||||||
|
session: SessionId::new("chat-1").unwrap(),
|
||||||
|
epoch: Epoch(0),
|
||||||
|
slot: 0,
|
||||||
|
baseline: Hash32::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user(content: &str, speaker: Option<&str>) -> LogRecord {
|
||||||
|
LogRecord::User {
|
||||||
|
time: time(),
|
||||||
|
content: content.to_string(),
|
||||||
|
speaker: speaker.map(str::to_string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reply(content: Option<&str>) -> LogRecord {
|
||||||
|
LogRecord::Assistant {
|
||||||
|
time: time(),
|
||||||
|
content: content.map(str::to_string),
|
||||||
|
reasoning_content: Some("thinking".to_string()),
|
||||||
|
tool_calls: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calls_tool() -> LogRecord {
|
||||||
|
calls_tool_saying(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tool round; a model may say something beside its tool calls.
|
||||||
|
fn calls_tool_saying(content: Option<&str>) -> LogRecord {
|
||||||
|
LogRecord::Assistant {
|
||||||
|
time: time(),
|
||||||
|
content: content.map(str::to_string),
|
||||||
|
reasoning_content: None,
|
||||||
|
tool_calls: vec![ToolCall {
|
||||||
|
id: "c1".to_string(),
|
||||||
|
name: "fetch".to_string(),
|
||||||
|
arguments: "{}".to_string(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn result(untrusted: bool) -> LogRecord {
|
||||||
|
LogRecord::ToolResult {
|
||||||
|
time: time(),
|
||||||
|
call: CallId(1),
|
||||||
|
tool_call_id: "c1".to_string(),
|
||||||
|
content: "the page".to_string(),
|
||||||
|
class: DataClass::Public,
|
||||||
|
untrusted,
|
||||||
|
truncated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> LogRecord {
|
||||||
|
LogRecord::Usage {
|
||||||
|
time: time(),
|
||||||
|
cache_n: 0,
|
||||||
|
prompt_n: 10,
|
||||||
|
predicted_n: 5,
|
||||||
|
reasoning_tokens: 2,
|
||||||
|
thinking_capped: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One finished turn: its `User` record, then the reply and its usage.
|
||||||
|
fn turn(text: &str, speaker: Option<&str>, answer: &str) -> Vec<LogRecord> {
|
||||||
|
vec![user(text, speaker), reply(Some(answer)), usage()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A finished turn with a tool round: the tool's result is labelled `untrusted`.
|
||||||
|
fn turn_with_tool(speaker: Option<&str>, untrusted: bool) -> Vec<LogRecord> {
|
||||||
|
vec![
|
||||||
|
user("look this up", speaker),
|
||||||
|
calls_tool(),
|
||||||
|
usage(),
|
||||||
|
result(untrusted),
|
||||||
|
reply(Some("here it is")),
|
||||||
|
usage(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(turns: &[Vec<LogRecord>]) -> Vec<LogRecord> {
|
||||||
|
let mut records = vec![start()];
|
||||||
|
for t in turns {
|
||||||
|
records.extend(t.iter().cloned());
|
||||||
|
}
|
||||||
|
records
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(class: DataClass, untrusted: bool) -> SessionStateReply {
|
||||||
|
SessionStateReply { class, untrusted }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean() -> SessionStateReply {
|
||||||
|
state(DataClass::Private, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to(workspace: Workspace, speaker: &str) -> Action {
|
||||||
|
Action::Turn {
|
||||||
|
workspace,
|
||||||
|
speaker: speaker.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip(why: &str) -> Action {
|
||||||
|
Action::Skip {
|
||||||
|
why: why.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `decide` for the last turn of `records`.
|
||||||
|
fn last(state: &SessionStateReply, records: &[LogRecord]) -> Action {
|
||||||
|
decide(state, records, turns(records), AGENT).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `n` characters of `A1b2`…, which the secret rules take for a key's run.
|
||||||
|
fn mixed(n: usize) -> String {
|
||||||
|
"A1b2".chars().cycle().take(n).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GitHub token, built from pieces so that no file holds one whole.
|
||||||
|
fn github_token() -> String {
|
||||||
|
format!("{}{}", ["gh", "p_"].concat(), mixed(36))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_skip_reasons_are_these_texts() {
|
||||||
|
assert_eq!(SECRET_CLASS, "the session is secret");
|
||||||
|
assert_eq!(NO_SPEAKER, "the turn has no single speaker");
|
||||||
|
assert_eq!(BAD_SPEAKER, "the speaker is not a Honcho peer name");
|
||||||
|
assert_eq!(AGENT_SPEAKER, "the speaker has the agent's peer name");
|
||||||
|
assert_eq!(NO_REPLY, "the turn has no reply");
|
||||||
|
assert_eq!(SECRET_TEXT, "the turn's text holds a secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn turns_counts_the_user_records() {
|
||||||
|
assert_eq!(turns(&[]), 0);
|
||||||
|
assert_eq!(turns(&[start()]), 0);
|
||||||
|
let records = log(&[
|
||||||
|
turn("one", Some("kyle"), "a"),
|
||||||
|
turn_with_tool(Some("kyle"), false),
|
||||||
|
vec![user("failed before any answer", Some("kyle"))],
|
||||||
|
turn("four", None, "d"),
|
||||||
|
]);
|
||||||
|
assert_eq!(turns(&records), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_turn_runs_from_its_user_record_to_the_next() {
|
||||||
|
let one = turn("one", Some("kyle"), "a");
|
||||||
|
let two = turn_with_tool(Some("ana"), true);
|
||||||
|
let three = vec![user("three", None), calls_tool()];
|
||||||
|
let records = log(&[one.clone(), two.clone(), three.clone()]);
|
||||||
|
assert_eq!(turn_records(&records, 1), Some(&one[..]));
|
||||||
|
assert_eq!(turn_records(&records, 2), Some(&two[..]));
|
||||||
|
assert_eq!(turn_records(&records, 3), Some(&three[..]));
|
||||||
|
for n in [0, 4, u64::MAX] {
|
||||||
|
assert_eq!(turn_records(&records, n), None, "{n}");
|
||||||
|
}
|
||||||
|
assert_eq!(turn_records(&[start()], 1), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_turn_not_in_the_log_is_an_error() {
|
||||||
|
let records = log(&[
|
||||||
|
turn("one", Some("kyle"), "a"),
|
||||||
|
turn("two", Some("kyle"), "b"),
|
||||||
|
]);
|
||||||
|
for n in [0, 3, u64::MAX] {
|
||||||
|
let err = decide(&clean(), &records, n, AGENT).unwrap_err();
|
||||||
|
assert_eq!(err, NoSuchTurn { turn: n, turns: 2 });
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
NoSuchTurn { turn: 3, turns: 2 }.to_string(),
|
||||||
|
"turn 3 is not in the session's log, which has 2 turns"
|
||||||
|
);
|
||||||
|
let err = decide(&clean(), &[], 1, AGENT).unwrap_err();
|
||||||
|
assert_eq!(err, NoSuchTurn { turn: 1, turns: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_clean_turn_goes_to_the_trusted_workspace() {
|
||||||
|
let records = log(&[turn("hello", Some("kyle"), "hi")]);
|
||||||
|
for class in [DataClass::Private, DataClass::Public] {
|
||||||
|
assert_eq!(
|
||||||
|
last(&state(class, false), &records),
|
||||||
|
to(Workspace::Trusted, "kyle")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let records = log(&[turn_with_tool(Some("kyle"), false)]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Trusted, "kyle"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_speaker_becomes_a_peer_name() {
|
||||||
|
assert_eq!(peer_name("first.last"), "first_last");
|
||||||
|
assert_eq!(peer_name("a.b.c-d_e"), "a_b_c-d_e");
|
||||||
|
assert_eq!(peer_name("kyle"), "kyle");
|
||||||
|
let records = log(&[turn("hello", Some("ana.b"), "hi")]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Trusted, "ana_b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn untrusted_by_brokerd_or_by_a_label_goes_to_the_untrusted_workspace() {
|
||||||
|
let plain = turn("hello", Some("kyle"), "hi");
|
||||||
|
let records = log(std::slice::from_ref(&plain));
|
||||||
|
assert_eq!(
|
||||||
|
last(&state(DataClass::Private, true), &records),
|
||||||
|
to(Workspace::Untrusted, "kyle")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
last(&state(DataClass::Public, true), &records),
|
||||||
|
to(Workspace::Untrusted, "kyle")
|
||||||
|
);
|
||||||
|
// A label in this turn, or in an earlier one, with `brokerd` saying clean.
|
||||||
|
let records = log(&[turn_with_tool(Some("kyle"), true)]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Untrusted, "kyle"));
|
||||||
|
let records = log(&[turn_with_tool(None, true), plain.clone()]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Untrusted, "kyle"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_label_after_the_turn_does_not_count() {
|
||||||
|
let records = log(&[
|
||||||
|
turn("hello", Some("kyle"), "hi"),
|
||||||
|
turn_with_tool(Some("kyle"), true),
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
decide(&clean(), &records, 1, AGENT).unwrap(),
|
||||||
|
to(Workspace::Trusted, "kyle")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
decide(&clean(), &records, 2, AGENT).unwrap(),
|
||||||
|
to(Workspace::Untrusted, "kyle")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_secret_session_goes_nowhere() {
|
||||||
|
for untrusted in [false, true] {
|
||||||
|
let records = log(&[turn("hello", Some("kyle"), "hi")]);
|
||||||
|
assert_eq!(
|
||||||
|
last(&state(DataClass::Secret, untrusted), &records),
|
||||||
|
skip(SECRET_CLASS)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_speaker_goes_nowhere() {
|
||||||
|
let records = log(&[turn("hello", None, "hi")]);
|
||||||
|
assert_eq!(last(&clean(), &records), skip(NO_SPEAKER));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_speaker_that_is_not_a_peer_name_goes_nowhere() {
|
||||||
|
let long = "a".repeat(513);
|
||||||
|
for name in [
|
||||||
|
"",
|
||||||
|
"a b",
|
||||||
|
"caf\u{e9}",
|
||||||
|
"a/b",
|
||||||
|
"a@b",
|
||||||
|
"\u{661}",
|
||||||
|
long.as_str(),
|
||||||
|
] {
|
||||||
|
let records = log(&[turn("hello", Some(name), "hi")]);
|
||||||
|
assert_eq!(last(&clean(), &records), skip(BAD_SPEAKER), "{name:?}");
|
||||||
|
}
|
||||||
|
let longest = "a".repeat(512);
|
||||||
|
let records = log(&[turn("hello", Some(&longest), "hi")]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Trusted, &longest));
|
||||||
|
let records = log(&[turn("hello", Some("."), "hi")]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Trusted, "_"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_speaker_with_the_agents_name_goes_nowhere() {
|
||||||
|
let records = log(&[turn("hello", Some(AGENT), "hi")]);
|
||||||
|
assert_eq!(last(&clean(), &records), skip(AGENT_SPEAKER));
|
||||||
|
let records = log(&[turn("hello", Some("box.maker"), "hi")]);
|
||||||
|
assert_eq!(
|
||||||
|
decide(&clean(), &records, 1, "box_maker").unwrap(),
|
||||||
|
skip(AGENT_SPEAKER)
|
||||||
|
);
|
||||||
|
let records = log(&[turn("hello", Some("Boxmaker0"), "hi")]);
|
||||||
|
assert_eq!(
|
||||||
|
last(&clean(), &records),
|
||||||
|
to(Workspace::Trusted, "Boxmaker0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_turn_without_a_reply_goes_nowhere() {
|
||||||
|
let cases = [
|
||||||
|
vec![user("hello", Some("kyle"))],
|
||||||
|
vec![user("hello", Some("kyle")), usage()],
|
||||||
|
vec![user("hello", Some("kyle")), calls_tool(), usage()],
|
||||||
|
vec![
|
||||||
|
user("hello", Some("kyle")),
|
||||||
|
calls_tool_saying(Some("let me look")),
|
||||||
|
usage(),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
user("hello", Some("kyle")),
|
||||||
|
calls_tool(),
|
||||||
|
usage(),
|
||||||
|
result(false),
|
||||||
|
],
|
||||||
|
vec![user("hello", Some("kyle")), reply(None), usage()],
|
||||||
|
vec![user("hello", Some("kyle")), reply(Some("")), usage()],
|
||||||
|
vec![user("hello", Some("kyle")), reply(Some(" \n\t")), usage()],
|
||||||
|
vec![user("hello", Some("kyle")), reply(Some("hi")), calls_tool()],
|
||||||
|
];
|
||||||
|
for case in cases {
|
||||||
|
let records = log(std::slice::from_ref(&case));
|
||||||
|
assert_eq!(last(&clean(), &records), skip(NO_REPLY), "{case:?}");
|
||||||
|
}
|
||||||
|
// A later turn is decided on its own records.
|
||||||
|
let records = log(&[
|
||||||
|
vec![user("hello", Some("kyle"))],
|
||||||
|
turn("again", Some("kyle"), "hi"),
|
||||||
|
]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Trusted, "kyle"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_secret_in_the_text_or_the_reply_goes_nowhere() {
|
||||||
|
let key = github_token();
|
||||||
|
let texts = [
|
||||||
|
format!("my token is {key}, keep it"),
|
||||||
|
key.clone(),
|
||||||
|
format!("line one\r\n{key}\r\n"),
|
||||||
|
format!("\u{7}{key}"),
|
||||||
|
format!("\u{200b}{key}"),
|
||||||
|
format!("{}{}", ["AK", "IA"].concat(), "IOSFODNN7EXAMPLE"),
|
||||||
|
format!("{}{}", ["sk", "-proj-"].concat(), mixed(40)),
|
||||||
|
];
|
||||||
|
for text in &texts {
|
||||||
|
let records = log(&[turn(text, Some("kyle"), "noted")]);
|
||||||
|
assert_eq!(last(&clean(), &records), skip(SECRET_TEXT), "{text:?}");
|
||||||
|
let records = log(&[turn("what is my token?", Some("kyle"), text)]);
|
||||||
|
assert_eq!(last(&clean(), &records), skip(SECRET_TEXT), "{text:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_secret_where_nothing_is_sent_does_not_count() {
|
||||||
|
let key = github_token();
|
||||||
|
let mut records = log(&[turn_with_tool(Some("kyle"), false)]);
|
||||||
|
for record in &mut records {
|
||||||
|
match record {
|
||||||
|
LogRecord::ToolResult { content, .. } => *content = key.clone(),
|
||||||
|
LogRecord::Assistant {
|
||||||
|
reasoning_content, ..
|
||||||
|
} => *reasoning_content = Some(key.clone()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Trusted, "kyle"));
|
||||||
|
let records = log(&[
|
||||||
|
turn(&key, Some("kyle"), "noted"),
|
||||||
|
turn("hello", Some("kyle"), "hi"),
|
||||||
|
]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Trusted, "kyle"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_the_secret_rules_count() {
|
||||||
|
let tag = TAGS[0].text;
|
||||||
|
for text in [
|
||||||
|
"a bell \u{7} and a carriage return \r".to_string(),
|
||||||
|
"a zero width\u{200b}space".to_string(),
|
||||||
|
format!("the model's {tag} marker"),
|
||||||
|
"ghp_ is how GitHub tokens start; sk-learn is a library".to_string(),
|
||||||
|
format!("{}{}", ["gh", "p_"].concat(), mixed(35)),
|
||||||
|
] {
|
||||||
|
assert!(!has_secret(&text), "{text:?}");
|
||||||
|
let records = log(&[turn(&text, Some("kyle"), &text)]);
|
||||||
|
assert_eq!(last(&clean(), &records), to(Workspace::Trusted, "kyle"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn has_secret_sees_past_the_character_rules() {
|
||||||
|
let key = github_token();
|
||||||
|
assert!(has_secret(&key));
|
||||||
|
for mark in ["\r", "\u{7}", "\u{200b}", "\u{e000}", "\u{fdd0}", "\u{7f}"] {
|
||||||
|
assert!(has_secret(&format!("{mark}{key}")), "{mark:?} before");
|
||||||
|
assert!(has_secret(&format!("{key}{mark}")), "{mark:?} after");
|
||||||
|
assert!(has_secret(&format!("x{mark}{mark}{key}")), "{mark:?} twice");
|
||||||
|
}
|
||||||
|
// A letter joined to the prefix still hides it, as it does for `refuse`.
|
||||||
|
assert!(!has_secret(&format!("x{key}")));
|
||||||
|
let tag = TAGS[1].text;
|
||||||
|
assert!(has_secret(&format!("{tag}{key}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_checks_run_in_their_order() {
|
||||||
|
let key = github_token();
|
||||||
|
let secret = state(DataClass::Secret, true);
|
||||||
|
let untrusted = state(DataClass::Private, true);
|
||||||
|
// Each case breaks the rule named and every rule after it; the first one decides.
|
||||||
|
let cases: Vec<(SessionStateReply, Vec<LogRecord>, &str)> = vec![
|
||||||
|
(
|
||||||
|
secret.clone(),
|
||||||
|
vec![user(&key, None), calls_tool()],
|
||||||
|
SECRET_CLASS,
|
||||||
|
),
|
||||||
|
(secret.clone(), vec![user("hi", Some("a b"))], SECRET_CLASS),
|
||||||
|
(
|
||||||
|
untrusted.clone(),
|
||||||
|
vec![user(&key, None), calls_tool()],
|
||||||
|
NO_SPEAKER,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
untrusted.clone(),
|
||||||
|
vec![user(&key, Some("a b"))],
|
||||||
|
BAD_SPEAKER,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
untrusted.clone(),
|
||||||
|
vec![user(&key, Some(AGENT))],
|
||||||
|
AGENT_SPEAKER,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
untrusted.clone(),
|
||||||
|
vec![user(&key, Some("kyle")), calls_tool()],
|
||||||
|
NO_REPLY,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
untrusted.clone(),
|
||||||
|
vec![user("hi", Some("kyle")), reply(Some(&key))],
|
||||||
|
SECRET_TEXT,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (state, turn, why) in cases {
|
||||||
|
let records = log(std::slice::from_ref(&turn));
|
||||||
|
assert_eq!(last(&state, &records), skip(why), "{turn:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
|
|||||||
|
|
||||||
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|
||||||
|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| M5c1/08-memoryd-honcho-decide | 2026-09-28 | done | 1 | pass | none | Copied the given test `crates/memoryd/tests/honcho_decide.rs` and the skeleton `crates/memoryd/src/honcho/decide.rs` from `docs/plans/M5c1/files/`. Step 2 showed exactly the named failure: `honcho_decide` with one E0432 (`unresolved import memoryd::honcho::decide`); every other target compiled. Made H1 (`pub mod decide;` after `pub mod config;` in `honcho/mod.rs`) and copied the skeleton; `cargo check` gave 11 warnings (one unused import `bxcore::sanitize::refuse`, ten unused variables: `records` four, `turn` four, `speaker` and `text` once each). Step 3 `cargo test -p memoryd --test honcho_decide` gave 1 pass (`the_skip_reasons_are_these_texts`) and 17 fail with `not yet implemented`. Filled its eight bodies in the order of the file, deleting each instruction comment as it went and keeping the doc comments, each from its comment and `cargo check` after: `turns` (filter `User`, `u64::try_from(count).unwrap_or(u64::MAX)`); `bounds` (walk `enumerate`, when a second `User` follows the tracked turn-th one return `(start, index)`, else count and set `start` at `seen == turn`, then `start.map(|s| (s, len))`); `turn_records` (`bounds` then `get`); `peer_name` (`replace('.', "_")`); `has_secret` (map each char `refuse` refuses on its own to a space via a 4-byte `encode_utf8` buffer, then keep only a `secret-` rule); `user_text` (first record's `User`, `speaker.as_deref()`); `final_reply` (last `Assistant` with empty `tool_calls`, `content.as_deref()`); `labelled_untrusted` (any `ToolResult { untrusted: true }`). After the eighth `cargo check` is clean with no warning; `cargo test` gives all 18 pass. Step 6 all pass: the grep for `fs::`/`std::io`/`Timestamp::now` prints 0, the two `refuse(` lines (87, 94) are both in `has_secret`, and the other-crates `git diff --stat` is empty. `make gate` printed `gate: ok` with 1230 tests (1229 in `cargo test --workspace`, plus the 1 ignored `end_to_end` the gate runs separately with `BOXMAKER_BROKERD` set). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5c1/07-memoryd-honcho-ledger | 2026-09-28 | done | 1 | pass | none | Copied the three given files from `docs/plans/M5c1/files/`: the tests `crates/memoryd/tests/honcho_ledger.rs`, `honcho_ledger_file.rs` and the support `crates/memoryd/tests/support/ledger.rs`. Step 2 showed exactly the named failures: two test targets (`honcho_ledger` and `honcho_ledger_file`) with two E0432 each (`unresolved import memoryd::honcho::ledger`), one in each test file and one in `support/ledger.rs`; every other target compiled. Made H1 (`pub mod ledger;` after `pub mod config;` in `honcho/mod.rs`) and copied the skeleton `crates/memoryd/src/honcho/ledger.rs`; `cargo check` gave 20 warnings from the unfilled bodies. Step 3 `cargo test -p memoryd --test honcho_ledger` gave 2 pass (`each_record_is_one_line_exactly_as_given`, `an_unknown_field_or_value_is_refused`) and 10 fail with `not yet implemented`. Filled its twelve bodies in the order of the file, each from its comment and `cargo check` after: `is_decided` (`contains_key`); `outbox` (walk `order` with `.iter().filter_map`, keeping `(item, action)` when `state == Waiting`); `sent_ids` (match `State::Sent(ids)` -> `ids.as_slice()`); `conclusion` (walk `sends` in order, pushing a sent `Conclude`'s first id and popping on a matching sent `Unconclude`, returning `live.pop()` so only workspace and text match and a `Sent` with no ids pushes nothing); `counts` (one per entry, matching `&entry.state` since `State` is not `Copy`); `check` (`AlreadyDecided` for a second `Decided`, `NotWaiting` unless a `Sent`/`Failed` finds its entry `Waiting`); `apply` (`Skip` -> `Skipped` else `Waiting`, push to `order` and `entries`; `Sent` sets `Sent(ids)` and pushes to `sends`, `Failed` sets `Failed` without pushing); `write_line` (append, create, mode 0600, one `write_all` then `sync_data`); then `read` (NotFound -> None, else Io), `complete` (rposition of `\n`), `parse` (serde error -> Damaged), `cut` (`set_len` then `sync_data`). Removed the `mut` in `cut` that clippy flagged (`set_len` and `sync_data` take `&self`); after the twelfth `cargo check` was clean and `cargo fmt` left the file. Both test files passed 12 and 12; appended `runbook-honcho-ledger-damaged.md` (`grep -c '^## honcho-ledger-damaged$'` printed 1). Step 7: `sync_data` 2, `write_all` 1, one runbook pointer line in `src`, no `create_dir`, no diff in the other crates. `make gate` printed `gate: ok` with 1212 tests (1211 in `cargo test --workspace`, plus the ignored `end_to_end` the gate runs with `BOXMAKER_BROKERD` set). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5c1/07-memoryd-honcho-ledger | 2026-09-28 | done | 1 | pass | none | Copied the three given files from `docs/plans/M5c1/files/`: the tests `crates/memoryd/tests/honcho_ledger.rs`, `honcho_ledger_file.rs` and the support `crates/memoryd/tests/support/ledger.rs`. Step 2 showed exactly the named failures: two test targets (`honcho_ledger` and `honcho_ledger_file`) with two E0432 each (`unresolved import memoryd::honcho::ledger`), one in each test file and one in `support/ledger.rs`; every other target compiled. Made H1 (`pub mod ledger;` after `pub mod config;` in `honcho/mod.rs`) and copied the skeleton `crates/memoryd/src/honcho/ledger.rs`; `cargo check` gave 20 warnings from the unfilled bodies. Step 3 `cargo test -p memoryd --test honcho_ledger` gave 2 pass (`each_record_is_one_line_exactly_as_given`, `an_unknown_field_or_value_is_refused`) and 10 fail with `not yet implemented`. Filled its twelve bodies in the order of the file, each from its comment and `cargo check` after: `is_decided` (`contains_key`); `outbox` (walk `order` with `.iter().filter_map`, keeping `(item, action)` when `state == Waiting`); `sent_ids` (match `State::Sent(ids)` -> `ids.as_slice()`); `conclusion` (walk `sends` in order, pushing a sent `Conclude`'s first id and popping on a matching sent `Unconclude`, returning `live.pop()` so only workspace and text match and a `Sent` with no ids pushes nothing); `counts` (one per entry, matching `&entry.state` since `State` is not `Copy`); `check` (`AlreadyDecided` for a second `Decided`, `NotWaiting` unless a `Sent`/`Failed` finds its entry `Waiting`); `apply` (`Skip` -> `Skipped` else `Waiting`, push to `order` and `entries`; `Sent` sets `Sent(ids)` and pushes to `sends`, `Failed` sets `Failed` without pushing); `write_line` (append, create, mode 0600, one `write_all` then `sync_data`); then `read` (NotFound -> None, else Io), `complete` (rposition of `\n`), `parse` (serde error -> Damaged), `cut` (`set_len` then `sync_data`). Removed the `mut` in `cut` that clippy flagged (`set_len` and `sync_data` take `&self`); after the twelfth `cargo check` was clean and `cargo fmt` left the file. Both test files passed 12 and 12; appended `runbook-honcho-ledger-damaged.md` (`grep -c '^## honcho-ledger-damaged$'` printed 1). Step 7: `sync_data` 2, `write_all` 1, one runbook pointer line in `src`, no `create_dir`, no diff in the other crates. `make gate` printed `gate: ok` with 1212 tests (1211 in `cargo test --workspace`, plus the ignored `end_to_end` the gate runs with `BOXMAKER_BROKERD` set). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5c1/06-memoryd-honcho-config | 2026-09-28 | done | 1 | pass | none | Copied the three given tests from `docs/plans/M5c1/files/crates/memoryd/tests/`: `honcho_config.rs`, `honcho_reasons.rs`, `honcho_start.rs`. Step 2 showed exactly the named failures: `honcho_config` with eight (E0433 cannot find `honcho` in `memoryd`, E0432 unresolved import `bxcore::secrets`, two E0599 for `honcho_config`, three for `state_socket`, one for the `Honcho` variant) and `honcho_reasons` with two (the same E0433 and E0432); `honcho_start` compiled. Made K1 (`bxcore` gains `features = ["secrets"]`) and L1 (`pub mod honcho;` after `pub mod files;`), created `crates/memoryd/src/honcho/mod.rs` with the given one-line module, and copied the skeleton `crates/memoryd/src/honcho/config.rs`. Filled its seven bodies in the given order, each from its comment and `cargo check` after: `is_name` (not empty, byte length `<= MAX_NAME`, every byte `is_ascii_alphanumeric()` or `_` or `-`, bytes not chars so `é` and U+0660 fail); `check_url` (strip `https://` then `http://`, `split_once(':')` into host and port, a `host_ok` helper for the non-empty ASCII-alnum/`.`/`-` host, a port that is non-empty, all digits and parses to `u16` `> 0` via `ok().is_some_and`, the `URL_REASON` which never quotes the URL); `check_own_workspaces` (the three pairs in order, first equal pair is R5 with its name); `trust_of` (match on `as_deref`, `None`/`Some("untrusted")` is `Untrusted`, `Some("trusted")` is `Trusted`, else R9 with the written text); `class_of` (same shape, `Private`/`Public`/`Secret`, else R10); `read_workspace` (check agent then workspace names, R6 the agent is this agent, R7 the agent is listed in `earlier`, R8 the workspace is one of the three, then the `ReadWorkspace` with `trust_of`/`class_of`); `check_keys` (a name other than the three is a read key only when `strip_prefix(READ_KEY)` names an agent in `read`, else R11 with the suffix or R12 without the prefix, then `spec.source()` with R13). Made C1-C8 in `config.rs` (module comment, imports with `BTreeMap`/`SecretSpec`/`honcho`, `Sockets.state`, `Config.honcho` and `.secrets`, `ConfigError::Honcho` and its Display arm, `load` calls `honcho_config` before returning, `honcho_config` and `state_socket`), and M1-M2 in `main.rs` (imports and the pointer matched by error kind on the `Config::load` arm); the pointer edit first landed on the `instance::lock` arm and I moved it to the `Config::load` arm, the diff being exactly M1-M2. Appended `runbook-honcho-config-invalid.md` to `docs/runbook.md` (`grep -c '^## honcho-config-invalid$'` printed 1) and made D1 in `docs/dependencies.md`. Step 7: `cargo build -p memoryd` builds, the runbook grep prints one line, the `secrets::load`/`read_checked` grep prints nothing, the other-crates diff is empty. `cargo test -p memoryd` every suite passes (config 4, honcho_config 8, honcho_reasons 5, honcho_start 4). `make gate` printed `gate: ok` with 1188 tests (1187 in `cargo test --workspace` plus the ignored `end_to_end` the gate runs separately with `BOXMAKER_BROKERD`; 1171 before this task plus the 17 new). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5c1/06-memoryd-honcho-config | 2026-09-28 | done | 1 | pass | none | Copied the three given tests from `docs/plans/M5c1/files/crates/memoryd/tests/`: `honcho_config.rs`, `honcho_reasons.rs`, `honcho_start.rs`. Step 2 showed exactly the named failures: `honcho_config` with eight (E0433 cannot find `honcho` in `memoryd`, E0432 unresolved import `bxcore::secrets`, two E0599 for `honcho_config`, three for `state_socket`, one for the `Honcho` variant) and `honcho_reasons` with two (the same E0433 and E0432); `honcho_start` compiled. Made K1 (`bxcore` gains `features = ["secrets"]`) and L1 (`pub mod honcho;` after `pub mod files;`), created `crates/memoryd/src/honcho/mod.rs` with the given one-line module, and copied the skeleton `crates/memoryd/src/honcho/config.rs`. Filled its seven bodies in the given order, each from its comment and `cargo check` after: `is_name` (not empty, byte length `<= MAX_NAME`, every byte `is_ascii_alphanumeric()` or `_` or `-`, bytes not chars so `é` and U+0660 fail); `check_url` (strip `https://` then `http://`, `split_once(':')` into host and port, a `host_ok` helper for the non-empty ASCII-alnum/`.`/`-` host, a port that is non-empty, all digits and parses to `u16` `> 0` via `ok().is_some_and`, the `URL_REASON` which never quotes the URL); `check_own_workspaces` (the three pairs in order, first equal pair is R5 with its name); `trust_of` (match on `as_deref`, `None`/`Some("untrusted")` is `Untrusted`, `Some("trusted")` is `Trusted`, else R9 with the written text); `class_of` (same shape, `Private`/`Public`/`Secret`, else R10); `read_workspace` (check agent then workspace names, R6 the agent is this agent, R7 the agent is listed in `earlier`, R8 the workspace is one of the three, then the `ReadWorkspace` with `trust_of`/`class_of`); `check_keys` (a name other than the three is a read key only when `strip_prefix(READ_KEY)` names an agent in `read`, else R11 with the suffix or R12 without the prefix, then `spec.source()` with R13). Made C1-C8 in `config.rs` (module comment, imports with `BTreeMap`/`SecretSpec`/`honcho`, `Sockets.state`, `Config.honcho` and `.secrets`, `ConfigError::Honcho` and its Display arm, `load` calls `honcho_config` before returning, `honcho_config` and `state_socket`), and M1-M2 in `main.rs` (imports and the pointer matched by error kind on the `Config::load` arm); the pointer edit first landed on the `instance::lock` arm and I moved it to the `Config::load` arm, the diff being exactly M1-M2. Appended `runbook-honcho-config-invalid.md` to `docs/runbook.md` (`grep -c '^## honcho-config-invalid$'` printed 1) and made D1 in `docs/dependencies.md`. Step 7: `cargo build -p memoryd` builds, the runbook grep prints one line, the `secrets::load`/`read_checked` grep prints nothing, the other-crates diff is empty. `cargo test -p memoryd` every suite passes (config 4, honcho_config 8, honcho_reasons 5, honcho_start 4). `make gate` printed `gate: ok` with 1188 tests (1187 in `cargo test --workspace` plus the ignored `end_to_end` the gate runs separately with `BOXMAKER_BROKERD`; 1171 before this task plus the 17 new). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5c1/05-brokerd-state-socket | 2026-09-28 | done | 2 | pass | none | Copied five files from `docs/plans/M5c1/files/`: `state_socket.rs` and `serve_state.rs` (new) and new versions of `config.rs`, `serve.rs` and `tests/fixtures/config/full.toml` (all now set and check `state`). Step 2 showed exactly the named failures: `config` with four E0599 (`no method named state_socket`) and `state_socket` with one E0432 (`unresolved import brokerd::state_socket`); the new `serve.rs` compiled. `cargo build --tests` reported those two targets only. Made the edits exactly as the task gives: C1 `Sockets` gains `state: PathBuf` with the doc comment, C2 `Config::state_socket` after `admin_socket` returning the default `run/memory-broker/state.sock` when empty; L1 `pub mod state_socket;` after `pub mod state;`. Created `crates/brokerd/src/state_socket.rs` with the given body: `read_request`, match `Message::SessionState(question)` (no `SessionState` import), `StateStore::new(&broker.cfg().state_dir())`, `Ok` -> `SessionStateReply { class: state.taint, untrusted: state.untrusted }`, `Err` -> log `brokerd: {e}` and send `internal` with `e.to_string()`, `forbid` for any other kind; `send` the final frame. V1-V11 in `serve.rs`: doc comment, `use crate::state_socket`, `Started` gains `state: UnixListener`, `state.sock` bound first then tool and admin, `Which::State`, the `run` destructure, the second `accept_loop` with `tx` moved, the `accept-state` name, and `state_socket::handle` in `serve_one`; M1-M3 in `main.rs`: doc comment, `state_path = cfg.state_socket()`, the `serving session state on <path>` notice. R1-R3 in `docs/runbook.md`: `accept-broker (or accept-admin, accept-state)`, the `state.sock` line in `socket-forbidden`, and `pgrep -a 'loopd|bxctl|memoryd'`. After the edits `cargo check -p brokerd` is clean with no warning; `config` 7, `state_socket` 8, `serve_state` 5, `instance` 2, `serve` 8, `serve_pointers` 10 pass. Step 7: `grep -n 'listen(&cfg'` prints three lines with `state_socket` first, `grep -n '^use' state_socket.rs` prints four and the `bxcore` import carries `ErrorCode`, `Message`, `SessionStateReply`, `WireError` with no `SessionState`, `git diff --stat -- crates/brokerd/src` lists exactly config.rs/lib.rs/main.rs/serve.rs, the other-crates diff is empty. `make gate` printed `gate: ok` with 1171 tests (1158 before this task plus the 13 new: eight in `state_socket.rs`, five in `serve_state.rs`; the new `config.rs` and `serve.rs` kept their counts, 7 and 8). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5c1/05-brokerd-state-socket | 2026-09-28 | done | 2 | pass | none | Copied five files from `docs/plans/M5c1/files/`: `state_socket.rs` and `serve_state.rs` (new) and new versions of `config.rs`, `serve.rs` and `tests/fixtures/config/full.toml` (all now set and check `state`). Step 2 showed exactly the named failures: `config` with four E0599 (`no method named state_socket`) and `state_socket` with one E0432 (`unresolved import brokerd::state_socket`); the new `serve.rs` compiled. `cargo build --tests` reported those two targets only. Made the edits exactly as the task gives: C1 `Sockets` gains `state: PathBuf` with the doc comment, C2 `Config::state_socket` after `admin_socket` returning the default `run/memory-broker/state.sock` when empty; L1 `pub mod state_socket;` after `pub mod state;`. Created `crates/brokerd/src/state_socket.rs` with the given body: `read_request`, match `Message::SessionState(question)` (no `SessionState` import), `StateStore::new(&broker.cfg().state_dir())`, `Ok` -> `SessionStateReply { class: state.taint, untrusted: state.untrusted }`, `Err` -> log `brokerd: {e}` and send `internal` with `e.to_string()`, `forbid` for any other kind; `send` the final frame. V1-V11 in `serve.rs`: doc comment, `use crate::state_socket`, `Started` gains `state: UnixListener`, `state.sock` bound first then tool and admin, `Which::State`, the `run` destructure, the second `accept_loop` with `tx` moved, the `accept-state` name, and `state_socket::handle` in `serve_one`; M1-M3 in `main.rs`: doc comment, `state_path = cfg.state_socket()`, the `serving session state on <path>` notice. R1-R3 in `docs/runbook.md`: `accept-broker (or accept-admin, accept-state)`, the `state.sock` line in `socket-forbidden`, and `pgrep -a 'loopd|bxctl|memoryd'`. After the edits `cargo check -p brokerd` is clean with no warning; `config` 7, `state_socket` 8, `serve_state` 5, `instance` 2, `serve` 8, `serve_pointers` 10 pass. Step 7: `grep -n 'listen(&cfg'` prints three lines with `state_socket` first, `grep -n '^use' state_socket.rs` prints four and the `bxcore` import carries `ErrorCode`, `Message`, `SessionStateReply`, `WireError` with no `SessionState`, `git diff --stat -- crates/brokerd/src` lists exactly config.rs/lib.rs/main.rs/serve.rs, the other-crates diff is empty. `make gate` printed `gate: ok` with 1171 tests (1158 before this task plus the 13 new: eight in `state_socket.rs`, five in `serve_state.rs`; the new `config.rs` and `serve.rs` kept their counts, 7 and 8). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
|
|||||||
Reference in New Issue
Block a user