//! 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 { 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 { vec![ user("look this up", speaker), calls_tool(), usage(), result(untrusted), reply(Some("here it is")), usage(), ] } fn log(turns: &[Vec]) -> Vec { 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, &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:?}"); } }