use std::collections::BTreeSet;
use polyc_crypto::approval::{VerifiedExcision, verify_signed_excision};
use polyc_eventlog_model::{Event, TrustTag};
use polyc_proto::proto::polychrome::agent::v1::{Message, content};
use polyc_proto::{events_decode::decode_event_payload, kinds};
const CASCADE_KINDS: [&str; 6] = [
kinds::OUTPUT_MSG,
kinds::SUMMARY,
kinds::COMPACTION_CHECKPOINT,
kinds::SUMMARY_GATE_REJECTED,
kinds::SUMMARY_GATE_ADMITTED,
kinds::GROUNDED_CONTENT,
];
#[must_use]
pub fn verified_excisions(events: &[(u64, Event)], conversation_id: &str) -> Vec<VerifiedExcision> {
verified_excisions_matching(events, conversation_id, |excision| {
excision.conversation_id == conversation_id
})
}
#[must_use]
pub fn verified_excisions_matching(
events: &[(u64, Event)],
target: &str,
matches: impl Fn(&VerifiedExcision) -> bool,
) -> Vec<VerifiedExcision> {
events
.iter()
.filter(|(_, ev)| kinds::base(&ev.kind) == kinds::TAINT_EXCISION)
.filter_map(|(pos, ev)| {
let verified = verify_signed_excision(&ev.payload);
match &verified {
Some(v) if matches(v) => verified,
Some(_) => {
tracing::warn!(
position = pos,
%target,
"ignoring taint-excision marker bound to a different conversation"
);
None
}
None => {
tracing::warn!(
position = pos,
%target,
"ignoring unverifiable taint-excision marker; taint stays (fail closed)"
);
None
}
}
})
.collect()
}
#[must_use]
pub fn excised_positions(events: &[(u64, Event)], excisions: &[VerifiedExcision]) -> BTreeSet<u64> {
let existing: BTreeSet<u64> = events.iter().map(|(pos, _)| *pos).collect();
let mut excised: BTreeSet<u64> = BTreeSet::new();
for excision in excisions {
let named: BTreeSet<u64> = excision
.positions
.iter()
.copied()
.filter(|p| existing.contains(p))
.collect();
let Some(&earliest) = named.iter().next() else {
continue;
};
if excision.is_cascade() {
let from = earliest;
for (pos, ev) in events {
if *pos >= from && CASCADE_KINDS.contains(&kinds::base(&ev.kind)) {
excised.insert(*pos);
}
}
} else {
for &pos in &named {
if let Some(id) = tool_result_id_at(events, pos)
&& let Some(partner) = tool_use_position(events, &id)
{
excised.insert(partner);
}
}
}
excised.extend(named);
}
excised
}
fn tool_result_id_at(events: &[(u64, Event)], pos: u64) -> Option<String> {
let (_, ev) = events.iter().find(|(p, _)| *p == pos)?;
if kinds::base(&ev.kind) != kinds::OUTPUT_MSG {
return None;
}
let msg = decode_event_payload::<Message>(&ev.payload)?;
match &msg.content.as_option()?.r#type {
Some(content::Type::ToolResult(tr)) => Some(tr.call_id.clone()),
_ => None,
}
}
fn tool_use_position(events: &[(u64, Event)], tool_call_id: &str) -> Option<u64> {
events.iter().find_map(|(pos, ev)| {
if kinds::base(&ev.kind) != kinds::OUTPUT_MSG {
return None;
}
let msg = decode_event_payload::<Message>(&ev.payload)?;
match &msg.content.as_option()?.r#type {
Some(content::Type::ToolCall(tc)) if tc.id == tool_call_id => Some(*pos),
_ => None,
}
})
}
pub fn strip_excised(events: &mut [(u64, Event)], excised: &BTreeSet<u64>) {
for (pos, ev) in events.iter_mut() {
if !excised.contains(pos) {
continue;
}
let (_, turn) = kinds::parse(&ev.kind);
ev.kind = turn.map_or_else(|| "excised".to_owned(), |t| kinds::tagged("excised", &t));
ev.trust = TrustTag::Unspecified;
ev.payload = Vec::new();
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use polyc_crypto::approval::{
ApprovalSigner, EXCISION_SCOPE_CASCADE, EXCISION_SCOPE_SOURCE_ONLY, excision_payload,
};
use polyc_eventlog_model::any_untrusted_excluding;
use super::*;
fn signer() -> ApprovalSigner {
ApprovalSigner::from_seed(7)
}
fn tool_call_msg(id: &str) -> Message {
use polyc_proto::proto::polychrome::agent::v1::{Content, ToolCallContent};
Message {
role: "model".to_owned(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::ToolCall(Box::new(ToolCallContent {
id: id.to_owned(),
..Default::default()
}))),
..Default::default()
}),
..Default::default()
}
}
fn tool_result_msg(id: &str) -> Message {
use polyc_proto::proto::polychrome::agent::v1::{Content, ToolResultContent};
Message {
role: "user".to_owned(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::ToolResult(Box::new(ToolResultContent {
call_id: id.to_owned(),
..Default::default()
}))),
..Default::default()
}),
..Default::default()
}
}
fn marker(conversation: &str, scope: &str, positions: &[u64]) -> Event {
let (payload, _, _) = excision_payload(
conversation,
scope,
positions,
"persona-1",
"test excision",
&signer(),
);
Event::new(kinds::TAINT_EXCISION, payload)
}
fn poisoned_log() -> Vec<(u64, Event)> {
let tool_use = tool_call_msg("call-1");
let tool_result = tool_result_msg("call-1");
use buffa::Message as _;
vec![
(
0,
Event::new("turn_start:0195000000007000800000000000aaaa", Vec::new()),
),
(
1,
Event::trusted(
"user_msg:0195000000007000800000000000aaaa",
b"fetch it".to_vec(),
),
),
(
2,
Event::new(
"output_msg:0195000000007000800000000000aaaa",
tool_use.encode_to_vec(),
),
),
(
3,
Event::with_trust(
"output_msg:0195000000007000800000000000aaaa",
tool_result.encode_to_vec(),
TrustTag::QuarantinedContent,
),
),
(
4,
Event::new(
"output_msg:0195000000007000800000000000aaaa",
Vec::new(), ),
),
(
5,
Event::new("turn_complete:0195000000007000800000000000aaaa", Vec::new()),
),
(
6,
Event::trusted(
"user_msg:0195000000007000800000000000bbbb",
b"thanks".to_vec(),
),
),
(
7,
Event::new("output_msg:0195000000007000800000000000bbbb", Vec::new()),
),
]
}
#[test]
fn cascade_excises_the_source_and_all_model_output_after_it() {
let mut events = poisoned_log();
events.push((8, marker("conv-1", EXCISION_SCOPE_CASCADE, &[3])));
let excisions = verified_excisions(&events, "conv-1");
assert_eq!(excisions.len(), 1);
let excised = excised_positions(&events, &excisions);
assert_eq!(excised, [3, 4, 7].into());
assert!(!any_untrusted_excluding(&events, &excised));
let mut later = events.clone();
later.push((
9,
Event::quarantined(
"output_msg:0195000000007000800000000000cccc",
b"<new>".to_vec(),
),
));
assert!(any_untrusted_excluding(&later, &excised));
}
#[test]
fn cascade_clears_a_grounding_marker_so_the_seed_recovers() {
let mut events = poisoned_log();
events.push((
8,
Event::with_trust(
"grounded_content:0195000000007000800000000000dddd".to_owned(),
Vec::new(),
TrustTag::QuarantinedContent,
),
));
events.push((9, marker("conv-1", EXCISION_SCOPE_CASCADE, &[3])));
let excisions = verified_excisions(&events, "conv-1");
let excised = excised_positions(&events, &excisions);
assert!(
excised.contains(&8),
"a cascade from position 3 must reach the grounding marker at 8: {excised:?}"
);
assert!(
!any_untrusted_excluding(&events, &excised),
"the conversation must recover — a surviving grounding marker gates \
it forever with nothing left to excise"
);
}
#[test]
fn source_only_leaves_an_unnamed_grounding_marker_standing() {
let mut events = poisoned_log();
events.push((
8,
Event::with_trust(
"grounded_content:0195000000007000800000000000dddd".to_owned(),
Vec::new(),
TrustTag::QuarantinedContent,
),
));
events.push((9, marker("conv-1", EXCISION_SCOPE_SOURCE_ONLY, &[3])));
let excisions = verified_excisions(&events, "conv-1");
let excised = excised_positions(&events, &excisions);
assert_eq!(
excised,
[2, 3].into(),
"source-only excises the named pair and nothing else"
);
assert!(
any_untrusted_excluding(&events, &excised),
"a grounding the person never named still taints — narrowing the \
scope narrows the recovery, which is the point of the scope"
);
}
#[test]
fn source_only_excises_the_whole_pair_and_nothing_downstream() {
let mut events = poisoned_log();
events.push((8, marker("conv-1", EXCISION_SCOPE_SOURCE_ONLY, &[3])));
let excisions = verified_excisions(&events, "conv-1");
let excised = excised_positions(&events, &excisions);
assert_eq!(excised, [2, 3].into());
assert!(!any_untrusted_excluding(&events, &excised));
}
#[test]
fn forged_and_foreign_markers_are_ignored() {
let mut events = poisoned_log();
events.push((8, marker("conv-other", EXCISION_SCOPE_CASCADE, &[3])));
let (payload, _, _) = excision_payload(
"conv-1",
EXCISION_SCOPE_CASCADE,
&[3],
"persona-1",
"r",
&signer(),
);
let mut v: serde_json::Value = serde_json::from_slice(&payload).unwrap();
v["positions"] = serde_json::json!([0, 1, 2, 3, 4, 5, 6, 7]);
events.push((
9,
Event::new(kinds::TAINT_EXCISION, v.to_string().into_bytes()),
));
let excisions = verified_excisions(&events, "conv-1");
assert!(excisions.is_empty(), "both markers must be ignored");
let excised = excised_positions(&events, &excisions);
assert!(excised.is_empty());
assert!(
any_untrusted_excluding(&events, &excised),
"taint stays (fail closed)"
);
}
#[test]
fn named_positions_that_do_not_exist_contribute_nothing() {
let mut events = poisoned_log();
events.push((8, marker("conv-1", EXCISION_SCOPE_CASCADE, &[99])));
let excisions = verified_excisions(&events, "conv-1");
assert_eq!(excised_positions(&events, &excisions), BTreeSet::new());
}
#[test]
fn strip_excised_preserves_shape_and_turn_tags() {
let mut events = poisoned_log();
let excised: BTreeSet<u64> = [3, 4].into();
let before = events.len();
strip_excised(&mut events, &excised);
assert_eq!(
events.len(),
before,
"length preserved for positional accounting"
);
let (_, ev3) = &events[3];
assert_eq!(kinds::base(&ev3.kind), "excised");
assert!(
kinds::parse(&ev3.kind).1.is_some(),
"the turn tag survives so committed-turn grouping is unchanged"
);
assert_eq!(ev3.trust, TrustTag::Unspecified);
assert!(ev3.payload.is_empty());
let (_, ev1) = &events[1];
assert_eq!(kinds::base(&ev1.kind), "user_msg");
assert_eq!(ev1.trust, TrustTag::TrustedUser);
}
}