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:
@@ -165,6 +165,7 @@ fn every_kind_but_tool_request() -> Vec<Message> {
|
|||||||
Message::TurnDone(TurnDone {
|
Message::TurnDone(TurnDone {
|
||||||
content: String::new(),
|
content: String::new(),
|
||||||
usage,
|
usage,
|
||||||
|
untrusted: false,
|
||||||
}),
|
}),
|
||||||
Message::Approvals(Empty {}),
|
Message::Approvals(Empty {}),
|
||||||
Message::ApprovalList(ApprovalList { items: Vec::new() }),
|
Message::ApprovalList(ApprovalList { items: Vec::new() }),
|
||||||
|
|||||||
@@ -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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct TurnDone {
|
pub struct TurnDone {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub usage: Usage,
|
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":{}}
|
// The admin messages on admin.sock. JSON: {"kind":"approvals","body":{}}
|
||||||
|
|||||||
+1
-1
@@ -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}}}
|
||||||
@@ -127,6 +127,7 @@ fn turn_done_and_the_new_error_codes() {
|
|||||||
Message::TurnDone(TurnDone {
|
Message::TurnDone(TurnDone {
|
||||||
content: "It is noon.".to_string(),
|
content: "It is noon.".to_string(),
|
||||||
usage: usage(),
|
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]
|
#[test]
|
||||||
fn every_turn_event_kind_round_trips() {
|
fn every_turn_event_kind_round_trips() {
|
||||||
let all = vec![
|
let all = vec![
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ fn done() -> End {
|
|||||||
End::Done(TurnDone {
|
End::Done(TurnDone {
|
||||||
content: "It is noon.".to_string(),
|
content: "It is noon.".to_string(),
|
||||||
usage: usage(),
|
usage: usage(),
|
||||||
|
untrusted: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +365,7 @@ fn json_mode_prints_frames_as_json_lines() {
|
|||||||
let last: serde_json::Value = serde_json::from_str(lines[lines.len() - 1]).unwrap();
|
let last: serde_json::Value = serde_json::from_str(lines[lines.len() - 1]).unwrap();
|
||||||
assert_eq!(last["content"], "It is noon.");
|
assert_eq!(last["content"], "It is noon.");
|
||||||
assert_eq!(last["usage"]["cache_n"], 10);
|
assert_eq!(last["usage"]["cache_n"], 10);
|
||||||
|
assert_eq!(last["untrusted"], false, "the done frame is printed whole");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -375,6 +377,7 @@ fn interactive_mode_reads_lines_until_quit() {
|
|||||||
End::Done(TurnDone {
|
End::Done(TurnDone {
|
||||||
content: "ok".to_string(),
|
content: "ok".to_string(),
|
||||||
usage: usage(),
|
usage: usage(),
|
||||||
|
untrusted: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let mut child = Command::new(env!("CARGO_BIN_EXE_bxctl"))
|
let mut child = Command::new(env!("CARGO_BIN_EXE_bxctl"))
|
||||||
@@ -433,6 +436,7 @@ fn interactive_mode_survives_a_failed_turn() {
|
|||||||
End::Done(TurnDone {
|
End::Done(TurnDone {
|
||||||
content: "ok".to_string(),
|
content: "ok".to_string(),
|
||||||
usage: usage(),
|
usage: usage(),
|
||||||
|
untrusted: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let mut child = Command::new(env!("CARGO_BIN_EXE_bxctl"))
|
let mut child = Command::new(env!("CARGO_BIN_EXE_bxctl"))
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ pub fn fake_loopd(events: Vec<TurnEvent>, answer: &str) -> FakeLoopd {
|
|||||||
Message::TurnDone(TurnDone {
|
Message::TurnDone(TurnDone {
|
||||||
content: answer.clone(),
|
content: answer.clone(),
|
||||||
usage: usage(),
|
usage: usage(),
|
||||||
|
untrusted: false,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
for (last, msg) in frames {
|
for (last, msg) in frames {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub fn done(content: &str) -> Reply {
|
|||||||
let msg = Message::TurnDone(TurnDone {
|
let msg = Message::TurnDone(TurnDone {
|
||||||
content: content.to_string(),
|
content: content.to_string(),
|
||||||
usage: USAGE,
|
usage: USAGE,
|
||||||
|
untrusted: false,
|
||||||
});
|
});
|
||||||
Reply::Frame(Envelope {
|
Reply::Frame(Envelope {
|
||||||
v: PROTOCOL_VERSION,
|
v: PROTOCOL_VERSION,
|
||||||
|
|||||||
@@ -217,6 +217,8 @@ pub fn handle(stream: UnixStream, ctx: Arc<Context>) {
|
|||||||
msg: Message::TurnDone(TurnDone {
|
msg: Message::TurnDone(TurnDone {
|
||||||
content: outcome.content,
|
content: outcome.content,
|
||||||
usage: outcome.usage,
|
usage: outcome.usage,
|
||||||
|
// The whole log, this turn's results included (M5b spec, section 8).
|
||||||
|
untrusted: session.untrusted(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -193,6 +193,21 @@ impl Session {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True once any tool result in the log is labelled untrusted: a result under an untrusted
|
||||||
|
/// grant, or a recalled memory-tainted note, which `brokerd` labels so. The labels are in
|
||||||
|
/// the log, so this holds after the session is reopened, and it never becomes false again.
|
||||||
|
pub fn untrusted(&self) -> bool {
|
||||||
|
self.records.iter().any(|record| {
|
||||||
|
matches!(
|
||||||
|
record,
|
||||||
|
LogRecord::ToolResult {
|
||||||
|
untrusted: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn last_usage(&self) -> Option<bxcore::Usage> {
|
pub fn last_usage(&self) -> Option<bxcore::Usage> {
|
||||||
for record in self.records.iter().rev() {
|
for record in self.records.iter().rev() {
|
||||||
if let LogRecord::Usage {
|
if let LogRecord::Usage {
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
//! `TurnDone.untrusted` follows the session's labels (M5b spec, sections 8 and 9): false while
|
||||||
|
//! every tool result in the session's log is labelled trusted, and true from the turn in which one
|
||||||
|
//! is labelled untrusted (a fetched page, or a recalled memory-tainted note) and in every turn
|
||||||
|
//! after it, also after `loopd` restarts. A denial or a failure is never untrusted. Do not edit.
|
||||||
|
|
||||||
|
mod support;
|
||||||
|
#[path = "support/tags.rs"]
|
||||||
|
mod tags;
|
||||||
|
|
||||||
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bxcore::{
|
||||||
|
DataClass, DenyReason, Envelope, Message, PROTOCOL_VERSION, SessionId, ToolResponse, Turn,
|
||||||
|
read_frame, write_frame,
|
||||||
|
};
|
||||||
|
use loopd::channel::{Context, serve};
|
||||||
|
use loopd::llama::Client;
|
||||||
|
use loopd::memory::NoMemory;
|
||||||
|
use loopd::tools::Registry;
|
||||||
|
use support::{FakeServer, Home, Reply, ScriptedPort};
|
||||||
|
use tags::{call, stream};
|
||||||
|
|
||||||
|
const CHAT: &str = "/v1/chat/completions";
|
||||||
|
|
||||||
|
/// A fake inference server and a home that one `loopd` or several, one after the other, serve
|
||||||
|
/// from.
|
||||||
|
struct Setup {
|
||||||
|
server: FakeServer,
|
||||||
|
// Declared last so it is dropped last: fields drop in order, and the home must outlive
|
||||||
|
// everything that may still write into it (tip T44).
|
||||||
|
home: Home,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Setup {
|
||||||
|
/// The server answers the completions in `chat` in order; the last one repeats.
|
||||||
|
fn new(chat: Vec<Reply>) -> Setup {
|
||||||
|
let home = Home::new();
|
||||||
|
let server = FakeServer::start();
|
||||||
|
server.route(CHAT, chat);
|
||||||
|
Setup { server, home }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a channel server on the home, as a new `loopd` process would: its own context and
|
||||||
|
/// tool port, nothing carried over but the files. `name` names its socket; the port answers
|
||||||
|
/// the tool calls with `replies`, in order.
|
||||||
|
fn loopd(&self, name: &str, replies: Vec<ToolResponse>) -> PathBuf {
|
||||||
|
let mut cfg = self.home.config(&self.server.socket);
|
||||||
|
cfg.channel.socket = self.home.dir.join(name);
|
||||||
|
let socket = cfg.channel_socket();
|
||||||
|
let listener = UnixListener::bind(&socket).unwrap();
|
||||||
|
let ctx = Arc::new(Context::new(
|
||||||
|
cfg.clone(),
|
||||||
|
Client::new(cfg),
|
||||||
|
Box::new(ScriptedPort::new(replies)),
|
||||||
|
Registry::m2b(),
|
||||||
|
Box::new(NoMemory),
|
||||||
|
));
|
||||||
|
thread::spawn(move || serve(listener, ctx));
|
||||||
|
socket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends one turn and returns its `TurnDone`'s `untrusted`. Any other final frame fails the test.
|
||||||
|
fn untrusted(socket: &Path, session: &str, content: &str, resume: bool) -> bool {
|
||||||
|
let mut stream = UnixStream::connect(socket).unwrap();
|
||||||
|
stream
|
||||||
|
.set_read_timeout(Some(Duration::from_secs(10)))
|
||||||
|
.unwrap();
|
||||||
|
let msg = Message::Turn(Turn {
|
||||||
|
session: SessionId::new(session).unwrap(),
|
||||||
|
content: content.to_string(),
|
||||||
|
resume,
|
||||||
|
});
|
||||||
|
let turn = Envelope {
|
||||||
|
v: PROTOCOL_VERSION,
|
||||||
|
id: 7,
|
||||||
|
r#final: true,
|
||||||
|
msg,
|
||||||
|
};
|
||||||
|
write_frame(&mut stream, &turn).unwrap();
|
||||||
|
loop {
|
||||||
|
let frame = read_frame(&mut stream).unwrap();
|
||||||
|
if !frame.r#final {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match frame.msg {
|
||||||
|
Message::TurnDone(done) => return done.untrusted,
|
||||||
|
other => panic!("the turn did not end with turn_done: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A result as `brokerd` answers it, with its label.
|
||||||
|
fn result(content: &str, untrusted: bool) -> ToolResponse {
|
||||||
|
ToolResponse::Result {
|
||||||
|
content: content.to_string(),
|
||||||
|
class: DataClass::Private,
|
||||||
|
untrusted,
|
||||||
|
truncated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The model calls `recall`; the fake port stands for `brokerd` and `memoryd` behind it.
|
||||||
|
fn recall() -> Reply {
|
||||||
|
stream(
|
||||||
|
&[call("call-r", "recall", r#"{"query":"deploy"}"#)],
|
||||||
|
"tool_calls",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_clean_session_s_turns_carry_false() {
|
||||||
|
let s = Setup::new(vec![Reply::fixture("turn1"), Reply::fixture("turn2")]);
|
||||||
|
let socket = s.loopd("loop.sock", vec![]);
|
||||||
|
assert!(!untrusted(&socket, "a", "hello", false), "turn 1");
|
||||||
|
assert!(!untrusted(&socket, "a", "and again", true), "turn 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trusted_results_denials_and_failures_keep_it_false() {
|
||||||
|
let s = Setup::new(vec![
|
||||||
|
Reply::fixture("tool_call"),
|
||||||
|
Reply::fixture("plain"),
|
||||||
|
Reply::fixture("tool_call"),
|
||||||
|
Reply::fixture("plain"),
|
||||||
|
Reply::fixture("tool_call"),
|
||||||
|
Reply::fixture("plain"),
|
||||||
|
]);
|
||||||
|
let socket = s.loopd(
|
||||||
|
"loop.sock",
|
||||||
|
vec![
|
||||||
|
result("straylight\n", false),
|
||||||
|
ToolResponse::Denied {
|
||||||
|
reason: DenyReason::NoGrant,
|
||||||
|
detail: None,
|
||||||
|
},
|
||||||
|
ToolResponse::Failed {
|
||||||
|
message: "the container could not start".to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!untrusted(&socket, "a", "hostname?", false),
|
||||||
|
"a trusted result"
|
||||||
|
);
|
||||||
|
assert!(!untrusted(&socket, "a", "again?", true), "a denial");
|
||||||
|
assert!(!untrusted(&socket, "a", "once more?", true), "a failure");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_untrusted_result_makes_its_turn_and_every_later_one_true() {
|
||||||
|
let s = Setup::new(vec![
|
||||||
|
Reply::fixture("plain"),
|
||||||
|
Reply::fixture("tool_call"),
|
||||||
|
Reply::fixture("plain"),
|
||||||
|
Reply::fixture("tool_call"),
|
||||||
|
Reply::fixture("plain"),
|
||||||
|
]);
|
||||||
|
let socket = s.loopd(
|
||||||
|
"loop.sock",
|
||||||
|
vec![
|
||||||
|
result("a fetched page\n", true),
|
||||||
|
result("straylight\n", false),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert!(!untrusted(&socket, "a", "hello", false), "before the page");
|
||||||
|
assert!(untrusted(&socket, "a", "read it", true), "the page's turn");
|
||||||
|
assert!(
|
||||||
|
untrusted(&socket, "a", "hostname?", true),
|
||||||
|
"a later turn whose own result is trusted"
|
||||||
|
);
|
||||||
|
assert!(untrusted(&socket, "a", "thanks", true), "a later turn");
|
||||||
|
assert!(
|
||||||
|
!untrusted(&socket, "b", "hello", false),
|
||||||
|
"another session is not touched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_recalled_memory_tainted_note_makes_it_true() {
|
||||||
|
let s = Setup::new(vec![recall(), Reply::fixture("plain")]);
|
||||||
|
let hit = "[1] memory-tainted · note, 2026-09-25 14:02, session chat-7 · private\n\
|
||||||
|
the deploy runs from ci.example.org";
|
||||||
|
let socket = s.loopd("loop.sock", vec![result(hit, true)]);
|
||||||
|
assert!(
|
||||||
|
untrusted(&socket, "a", "how do we deploy?", false),
|
||||||
|
"the recall's turn"
|
||||||
|
);
|
||||||
|
assert!(untrusted(&socket, "a", "thanks", true), "a later turn");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_recall_of_trusted_memory_keeps_it_false() {
|
||||||
|
let s = Setup::new(vec![recall(), Reply::fixture("plain")]);
|
||||||
|
let hit = "[1] identity · moved 2026-09-25 from memory.md · private\n\
|
||||||
|
deploys go through ci.example.org";
|
||||||
|
let socket = s.loopd("loop.sock", vec![result(hit, false)]);
|
||||||
|
assert!(!untrusted(&socket, "a", "how do we deploy?", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_restarted_loopd_reads_it_from_the_log() {
|
||||||
|
let s = Setup::new(vec![Reply::fixture("tool_call"), Reply::fixture("plain")]);
|
||||||
|
let first = s.loopd("first.sock", vec![result("a fetched page\n", true)]);
|
||||||
|
assert!(untrusted(&first, "a", "read it", false), "the first loopd");
|
||||||
|
let second = s.loopd("second.sock", vec![]);
|
||||||
|
assert!(
|
||||||
|
untrusted(&second, "a", "thanks", true),
|
||||||
|
"the second loopd, on the same session"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!untrusted(&second, "b", "hello", false),
|
||||||
|
"the second loopd, on a new session"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 |
|
||||||
|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| M5b/09-turn-done-untrusted | 2026-09-26 | done | 1 | pass | none | Copied the eight given files from `docs/plans/M5b/files/`: `bxcore/tests/fixtures/wire/turn_done_untrusted.json` (new) and `loopd/tests/turn_done_untrusted.rs` (new), new versions of `bxcore/tests/fixtures/wire/turn_done.json`, `bxcore/tests/turn_wire.rs`, `brokerd/tests/broker.rs`, `bxctl/tests/chat.rs` and `bxctl/tests/support/mod.rs`, and `gatewayd/tests/support/fake_loop.rs` (did not open them). Step 2 showed exactly the named failures: `strict::envelopes_reject_unknown_keys_at_every_depth` failing on `turn_done.json: fixture must decode`, and 13 non-compiling test targets (`turn_wire` in bxcore, `broker` in brokerd, `turn_done_untrusted` in loopd, `admin`/`chat`/`chat_approvals` in bxctl, `deliver`/`deliver_notice`/`serve`/`serve_answers`/`serve_approvals`/`serve_redirect`/`serve_restart` in gatewayd) with E0560 and one E0609. Made the three edits exactly as the task gives: W1 `TurnDone` gains `untrusted: bool` as the last field with the no-default comment in `bxcore/src/wire.rs`; S1 `Session::untrusted` scanning `self.records` for a `LogRecord::ToolResult { untrusted: true, .. }` placed before `last_usage` in `session.rs`; C1 `session.untrusted()` put in the `TurnDone` in `channel.rs` `handle` after `usage`. `cargo fmt --all`; `cargo check -p loopd` clean; the seven `turn_wire` tests and six `turn_done_untrusted` tests pass, and every suite in `bxcore`, `loopd`, `brokerd`, `bxctl` and `gatewayd` passes. `git diff --stat -- crates/gatewayd/src crates/bxctl/src crates/brokerd/src` prints nothing, and the only `serde(default)` in `wire.rs` is line 109, the `Denied` detail added by task 01. `make gate` printed `gate: ok` with 1058 tests (1050 before this task, plus the 8 new: two in `turn_wire.rs`, six in `turn_done_untrusted.rs`). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5b/08-loopd-selftest-tags | 2026-09-26 | done | 2 | pass | none | Copied the five given test files from `docs/plans/M5b/files/`: `selftest_tags.rs` (new) and new versions of `selftest.rs`, `serve.rs`, `pointers.rs` and `support/mod.rs` (did not open them). Step 2 showed exactly the four named failures (`selftest`: three steps without `"tags tokenize as recorded"`; `a_completion_without_the_tool_call_fails_the_tool_call_check` left 2 right 3; `a_cold_second_turn_fails_the_cache_check` left 3 right 4; `serve_runs_the_self_test_then_binds_the_socket_with_mode_0600` left 0 right 66) and `selftest_tags.rs` failing to compile with 7 E0599s (no `TagNotOneToken`/`TagOtherId`/`NeutralisedTooFew`/`Tokenize` variant, no `pointer`). Appended `runbook-loopd-tags-unexpected.md`; `check-runbook.sh` printed nothing and exited 0, `grep -c '^## loopd-tags-unexpected$'` printed 1. Made the edits in order: I1 the private `TokenIds` struct before `CacheOutcome` and I2 the `pub token_ids` method after `tokenize` (left `tokenize` itself unchanged); S1 the `use bxcore::sanitize::{TAGS, Tag, neutralise}` import, S2 the four tag variants plus `SelfTestError::pointer` with each pointer written whole, S3 the four Display arms, S4 the second check `tags tokenize as recorded`, S5 the `tags_tokenize` and `check_tag` functions, S6 check 3 to check 4; M1 `e.pointer()` in `run_selftest_check`. `cargo fmt --all`; `cargo check -p loopd` clean; `selftest_tags` 8, `selftest` 6, `serve` 3, `pointers` 7 passed, every `loopd` suite passes. `grep -rn 'runbook.md#loopd-' crates/loopd/src` prints exactly two lines, both in `selftest.rs`. `make gate` printed `gate: ok` with 1050 tests (1042 before this task, plus the 8 in `selftest_tags.rs`). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5b/08-loopd-selftest-tags | 2026-09-26 | done | 2 | pass | none | Copied the five given test files from `docs/plans/M5b/files/`: `selftest_tags.rs` (new) and new versions of `selftest.rs`, `serve.rs`, `pointers.rs` and `support/mod.rs` (did not open them). Step 2 showed exactly the four named failures (`selftest`: three steps without `"tags tokenize as recorded"`; `a_completion_without_the_tool_call_fails_the_tool_call_check` left 2 right 3; `a_cold_second_turn_fails_the_cache_check` left 3 right 4; `serve_runs_the_self_test_then_binds_the_socket_with_mode_0600` left 0 right 66) and `selftest_tags.rs` failing to compile with 7 E0599s (no `TagNotOneToken`/`TagOtherId`/`NeutralisedTooFew`/`Tokenize` variant, no `pointer`). Appended `runbook-loopd-tags-unexpected.md`; `check-runbook.sh` printed nothing and exited 0, `grep -c '^## loopd-tags-unexpected$'` printed 1. Made the edits in order: I1 the private `TokenIds` struct before `CacheOutcome` and I2 the `pub token_ids` method after `tokenize` (left `tokenize` itself unchanged); S1 the `use bxcore::sanitize::{TAGS, Tag, neutralise}` import, S2 the four tag variants plus `SelfTestError::pointer` with each pointer written whole, S3 the four Display arms, S4 the second check `tags tokenize as recorded`, S5 the `tags_tokenize` and `check_tag` functions, S6 check 3 to check 4; M1 `e.pointer()` in `run_selftest_check`. `cargo fmt --all`; `cargo check -p loopd` clean; `selftest_tags` 8, `selftest` 6, `serve` 3, `pointers` 7 passed, every `loopd` suite passes. `grep -rn 'runbook.md#loopd-' crates/loopd/src` prints exactly two lines, both in `selftest.rs`. `make gate` printed `gate: ok` with 1050 tests (1042 before this task, plus the 8 in `selftest_tags.rs`). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5b/07-loopd-neutralise | 2026-09-26 | done | 2 | fail | none | Copied the three given tests from `docs/plans/M5b/files/`: `neutralise.rs`, `neutralise_replay.rs` and `support/tags.rs` (did not open them). Step 2 showed exactly the seven named failures (all six in `neutralise`, `the_memory_block_is_neutralised_when_the_baseline_is_assembled` in `neutralise_replay`); the two that should pass passed. Made the edits in order: T1 the module comment, T2 the `use bxcore::sanitize::neutralise` import, T3 `content: neutralise(content).into_owned()` on the `LogRecord::User`, T4 the capped-then-neutralised `let text = neutralise(&text).into_owned()` on the `LogRecord::ToolResult`; B1 the import, B2 the `assemble_with` comment, B3 `neutralise(memory.trim())` with `&memory` pushed. `cargo fmt --all`; `cargo check -p loopd` clean; `neutralise` 6 passed, `neutralise_replay` 3 passed, every `loopd` suite passes. `grep -rn 'neutralise(' crates/loopd/src` prints exactly three lines (two in `turn.rs`, one in `baseline.rs`). First `make gate` run failed on a flaky brokerd concurrency test, `a_torn_tail_is_recovered_and_it_serves` (one fail out of the serve suite under full-workspace load); it passes 3/3 in isolation and the serve suite 9/9 three times on the same tree, and the loopd edits touch no brokerd path, so it is a timing flake, not this task. Re-ran `make gate`: `gate: ok` with 1042 tests. No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5b/07-loopd-neutralise | 2026-09-26 | done | 2 | fail | none | Copied the three given tests from `docs/plans/M5b/files/`: `neutralise.rs`, `neutralise_replay.rs` and `support/tags.rs` (did not open them). Step 2 showed exactly the seven named failures (all six in `neutralise`, `the_memory_block_is_neutralised_when_the_baseline_is_assembled` in `neutralise_replay`); the two that should pass passed. Made the edits in order: T1 the module comment, T2 the `use bxcore::sanitize::neutralise` import, T3 `content: neutralise(content).into_owned()` on the `LogRecord::User`, T4 the capped-then-neutralised `let text = neutralise(&text).into_owned()` on the `LogRecord::ToolResult`; B1 the import, B2 the `assemble_with` comment, B3 `neutralise(memory.trim())` with `&memory` pushed. `cargo fmt --all`; `cargo check -p loopd` clean; `neutralise` 6 passed, `neutralise_replay` 3 passed, every `loopd` suite passes. `grep -rn 'neutralise(' crates/loopd/src` prints exactly three lines (two in `turn.rs`, one in `baseline.rs`). First `make gate` run failed on a flaky brokerd concurrency test, `a_torn_tail_is_recovered_and_it_serves` (one fail out of the serve suite under full-workspace load); it passes 3/3 in isolation and the serve suite 9/9 three times on the same tree, and the loopd edits touch no brokerd path, so it is a timing flake, not this task. Re-ran `make gate`: `gate: ok` with 1042 tests. No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5b/06-memoryd-sanitize | 2026-09-26 | done | 1 | pass | none | Copied the five given tests from `docs/plans/M5b/files/`: `sanitize.rs`, `serve_sanitize.rs`, and new versions of `edit.rs`, `files.rs` and `serve.rs` (did not open them). Step 2 showed exactly the seven named failures (`refusals_change_nothing` in `edit`, `an_entry_that_fails_the_check_is_left_out_and_logged` in `serve`, all five in `serve_sanitize`); `files` did not compile (one E0609, no field `rule` on `&memoryd::check::Refused`) and `sanitize` did not compile (13: E0432 for `memoryd::edit::flagged`/`refused`, E0609 for `flag` on `Outcome` and `&Record` and `rule` on `Refused`, E0615 for `sentence`). Appended the three runbook fragments to `docs/runbook.md`; `check-runbook.sh` printed nothing and exited 0. `git rm`'d `crates/memoryd/src/check.rs` and its test. Made the edits in order: E1 removed `pub mod check;` from `lib.rs`; E2-E10 in `edit.rs` (imports, `Outcome` gains `flag`, five functions replacing `checked`, the refuse check in `apply`, the flag check after `edited`, `flag: None`, `text.trim()` in `edited`, `noted` plus `as_notes`, `text.trim()` in `consolidate`); E11-E13 in `files.rs` (imports, `Block`, the two `block` comments, the `refuse` match); E14-E19 in `journal.rs` (`Record` gains `flag` after `untrusted`, `record` sets `flag: outcome.flag.map(str::to_string)`, `log` appends ` flagged {rule}`, `undo` sets `flag: None`); E20 in `review.rs` (`reject` sets `flag: None`); E21-E23 in `serve.rs` (`block` logs by `refusal.rule` with the runbook pointer, the `memory` comment, and `memory` calls `edit::refused` first then `edit::flagged` to keep flagged writes off review and logs the rule). `cargo fmt --all`; `cargo check -p memoryd` clean; `cargo test -p memoryd` every suite passes (sanitize 11, serve_sanitize 5, edit 8, files 8, serve 8); `make gate` printed `gate: ok` with 1033 tests; `grep -rn 'memoryd::check\|crate::check' crates/` prints nothing. No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5b/06-memoryd-sanitize | 2026-09-26 | done | 1 | pass | none | Copied the five given tests from `docs/plans/M5b/files/`: `sanitize.rs`, `serve_sanitize.rs`, and new versions of `edit.rs`, `files.rs` and `serve.rs` (did not open them). Step 2 showed exactly the seven named failures (`refusals_change_nothing` in `edit`, `an_entry_that_fails_the_check_is_left_out_and_logged` in `serve`, all five in `serve_sanitize`); `files` did not compile (one E0609, no field `rule` on `&memoryd::check::Refused`) and `sanitize` did not compile (13: E0432 for `memoryd::edit::flagged`/`refused`, E0609 for `flag` on `Outcome` and `&Record` and `rule` on `Refused`, E0615 for `sentence`). Appended the three runbook fragments to `docs/runbook.md`; `check-runbook.sh` printed nothing and exited 0. `git rm`'d `crates/memoryd/src/check.rs` and its test. Made the edits in order: E1 removed `pub mod check;` from `lib.rs`; E2-E10 in `edit.rs` (imports, `Outcome` gains `flag`, five functions replacing `checked`, the refuse check in `apply`, the flag check after `edited`, `flag: None`, `text.trim()` in `edited`, `noted` plus `as_notes`, `text.trim()` in `consolidate`); E11-E13 in `files.rs` (imports, `Block`, the two `block` comments, the `refuse` match); E14-E19 in `journal.rs` (`Record` gains `flag` after `untrusted`, `record` sets `flag: outcome.flag.map(str::to_string)`, `log` appends ` flagged {rule}`, `undo` sets `flag: None`); E20 in `review.rs` (`reject` sets `flag: None`); E21-E23 in `serve.rs` (`block` logs by `refusal.rule` with the runbook pointer, the `memory` comment, and `memory` calls `edit::refused` first then `edit::flagged` to keep flagged writes off review and logs the rule). `cargo fmt --all`; `cargo check -p memoryd` clean; `cargo test -p memoryd` every suite passes (sanitize 11, serve_sanitize 5, edit 8, files 8, serve 8); `make gate` printed `gate: ok` with 1033 tests; `grep -rn 'memoryd::check\|crate::check' crates/` prints nothing. No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
|
|||||||
Reference in New Issue
Block a user