bxcore, loopd: TurnDone says whether the session has read untrusted content

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-27 19:08:42 -07:00
co-authored by ornith-1.5-35b-a3b
parent 91992b9743
commit 0d3048c3c4
12 changed files with 287 additions and 2 deletions
+4 -1
View File
@@ -187,12 +187,15 @@ pub enum TurnEvent {
},
}
// JSON: {"kind":"turn_done","body":{"content":"…","usage":{…}}}
// JSON: {"kind":"turn_done","body":{"content":"…","usage":{…},"untrusted":false}}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TurnDone {
pub content: String,
pub usage: Usage,
/// True once the session has read untrusted content, in this turn or an earlier one (M5b
/// spec, section 9). It has no default: a frame without it is refused, never read as clean.
pub untrusted: bool,
}
// The admin messages on admin.sock. JSON: {"kind":"approvals","body":{}}
+1 -1
View File
@@ -1 +1 @@
{"v":1,"id":3,"final":true,"msg":{"kind":"turn_done","body":{"content":"It is noon.","usage":{"cache_n":539,"prompt_n":27,"predicted_n":24,"reasoning_tokens":8,"thinking_capped":false}}}}
{"v":1,"id":3,"final":true,"msg":{"kind":"turn_done","body":{"content":"It is noon.","usage":{"cache_n":539,"prompt_n":27,"predicted_n":24,"reasoning_tokens":8,"thinking_capped":false},"untrusted":false}}}
@@ -0,0 +1 @@
{"v":1,"id":4,"final":true,"msg":{"kind":"turn_done","body":{"content":"The page says to run the script.","usage":{"cache_n":566,"prompt_n":412,"predicted_n":31,"reasoning_tokens":12,"thinking_capped":false},"untrusted":true}}}
+37
View File
@@ -127,6 +127,7 @@ fn turn_done_and_the_new_error_codes() {
Message::TurnDone(TurnDone {
content: "It is noon.".to_string(),
usage: usage(),
untrusted: false,
}),
),
);
@@ -154,6 +155,42 @@ fn turn_done_and_the_new_error_codes() {
}
}
/// A reply from a session that has read untrusted content says so, and `untrusted` is written
/// whether it is true or false (M5b spec, section 9).
#[test]
fn turn_done_from_an_untrusted_session() {
check(
"turn_done_untrusted.json",
env(
4,
true,
Message::TurnDone(TurnDone {
content: "The page says to run the script.".to_string(),
usage: Usage {
cache_n: 566,
prompt_n: 412,
predicted_n: 31,
reasoning_tokens: 12,
thinking_capped: false,
},
untrusted: true,
}),
),
);
}
/// `untrusted` has no default: a `turn_done` without it, as a `loopd` from before M5b sent, is
/// refused, never read as a reply from a clean session.
#[test]
fn a_turn_done_without_untrusted_is_refused() {
let old = r#"{"v":1,"id":3,"final":true,"msg":{"kind":"turn_done","body":{"content":"It is noon.","usage":{"cache_n":539,"prompt_n":27,"predicted_n":24,"reasoning_tokens":8,"thinking_capped":false}}}}"#;
let error = serde_json::from_str::<Envelope>(old).unwrap_err();
assert!(
error.to_string().contains("missing field `untrusted`"),
"{error}"
);
}
#[test]
fn every_turn_event_kind_round_trips() {
let all = vec![