use polyc_crypto::signing_role::{HandoffRole, RoleTrustSet, SignatureVerdict};
use polyc_proto::proto::polychrome::handoff::v1::{Handoff, HandoffDenied};
#[derive(Debug, Clone)]
pub enum HandoffFact {
Handoff(HandoffSpawnFact),
Denied(HandoffDeniedFact),
}
#[derive(Debug, Clone)]
pub struct HandoffSpawnFact {
pub child_conversation_id: String,
pub child_agent_id: String,
pub carried_count: u32,
pub reason: String,
pub signed_by: Vec<u8>,
pub signature_status: SignatureVerdict,
}
#[derive(Debug, Clone)]
pub struct HandoffDeniedFact {
pub parent_agent_id: String,
pub child_agent_id: String,
pub reason: String,
pub denial_reason: String,
pub allowed: Vec<String>,
pub signed_by: Vec<u8>,
pub signature_status: SignatureVerdict,
}
#[must_use]
pub fn fold_handoff_event(
base: &str,
payload: &[u8],
handoff_trust: &RoleTrustSet<HandoffRole>,
) -> Option<HandoffFact> {
if base == polyc_proto::kinds::HANDOFF {
let h = polyc_proto::events_decode::decode_event_payload::<Handoff>(payload)?;
let signature_status = polyc_crypto::handoff::classify_handoff(handoff_trust, &h);
Some(HandoffFact::Handoff(HandoffSpawnFact {
child_conversation_id: h.child_conversation_id,
child_agent_id: h.child_agent_id,
carried_count: h.carried_count,
reason: h.reason,
signed_by: h.signed_by,
signature_status,
}))
} else if base == polyc_proto::kinds::HANDOFF_DENIED {
let d = polyc_proto::events_decode::decode_event_payload::<HandoffDenied>(payload)?;
let signature_status = polyc_crypto::handoff::classify_handoff_denied(handoff_trust, &d);
Some(HandoffFact::Denied(HandoffDeniedFact {
parent_agent_id: d.parent_agent_id,
child_agent_id: d.child_agent_id,
reason: d.reason,
denial_reason: d.denial_reason,
allowed: d.allowed,
signed_by: d.signed_by,
signature_status,
}))
} else {
None
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]
use buffa::Message as _;
use polyc_crypto::signing_role::HandoffSigner;
use polyc_proto::proto::polychrome::agent::v1::{Content, Message, TextContent, content};
use super::*;
fn trust(signer: &HandoffSigner) -> RoleTrustSet<HandoffRole> {
RoleTrustSet::current(signer)
}
fn text_msg(role: &str, text: &str) -> Message {
Message {
role: role.to_owned(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::Text(Box::new(TextContent {
text: text.to_owned(),
..Default::default()
}))),
..Default::default()
}),
internal_only: false,
..Default::default()
}
}
fn signed_handoff(signer: &HandoffSigner, child_conversation_id: &str) -> Handoff {
let mut h = Handoff {
child_conversation_id: child_conversation_id.to_owned(),
child_agent_id: "researcher".to_owned(),
carried_count: 2,
carried_context: vec![text_msg("user", "find prior art")],
reason: "delegate research".to_owned(),
..Default::default()
};
polyc_crypto::handoff::sign_handoff_into(signer, &mut h);
h
}
fn signed_denied(signer: &HandoffSigner) -> HandoffDenied {
let mut d = HandoffDenied {
parent_conversation_id: "parent-7".to_owned(),
parent_agent_id: "assistant".to_owned(),
child_agent_id: "banned-agent".to_owned(),
reason: "delegate weird task".to_owned(),
denial_reason: "this agent can't hand off to that agent".to_owned(),
allowed: vec!["coding".to_owned(), "research".to_owned()],
..Default::default()
};
polyc_crypto::handoff::sign_handoff_denied_into(signer, &mut d);
d
}
#[test]
fn decodes_a_signed_handoff() {
let signer = HandoffSigner::from_seed(21);
let handoff = signed_handoff(&signer, "child-a");
let bytes = handoff.encode_to_vec();
let fact = fold_handoff_event(polyc_proto::kinds::HANDOFF, &bytes, &trust(&signer))
.expect("decode");
let HandoffFact::Handoff(h) = fact else {
panic!("expected Handoff variant");
};
assert_eq!(h.child_conversation_id, "child-a");
assert_eq!(h.child_agent_id, "researcher");
assert_eq!(h.carried_count, 2);
assert_eq!(h.reason, "delegate research");
assert_eq!(h.signature_status, SignatureVerdict::Verified);
}
#[test]
fn decodes_a_signed_denial() {
let signer = HandoffSigner::from_seed(22);
let denied = signed_denied(&signer);
let bytes = denied.encode_to_vec();
let fact = fold_handoff_event(polyc_proto::kinds::HANDOFF_DENIED, &bytes, &trust(&signer))
.expect("decode");
let HandoffFact::Denied(d) = fact else {
panic!("expected Denied variant");
};
assert_eq!(d.child_agent_id, "banned-agent");
assert_eq!(d.parent_agent_id, "assistant");
assert_eq!(d.denial_reason, "this agent can't hand off to that agent");
assert_eq!(d.allowed, vec!["coding".to_owned(), "research".to_owned()]);
assert_eq!(d.signature_status, SignatureVerdict::Verified);
}
#[test]
fn tampered_handoff_keeps_the_fact_but_reads_invalid() {
let signer = HandoffSigner::from_seed(22);
let mut handoff = signed_handoff(&signer, "child-tampered");
handoff.child_conversation_id = "child-evil".to_owned();
let bytes = handoff.encode_to_vec();
let fact = fold_handoff_event(polyc_proto::kinds::HANDOFF, &bytes, &trust(&signer))
.expect("still decodes");
let HandoffFact::Handoff(h) = fact else {
panic!("expected Handoff variant");
};
assert_eq!(h.signature_status, SignatureVerdict::Invalid);
assert_eq!(h.child_conversation_id, "child-evil");
}
#[test]
fn a_foreign_signed_handoff_keeps_the_fact_but_reads_untrusted() {
let signer = HandoffSigner::from_seed(31);
let deployment = HandoffSigner::from_seed(32);
let handoff = signed_handoff(&signer, "child-foreign");
let bytes = handoff.encode_to_vec();
let fact = fold_handoff_event(polyc_proto::kinds::HANDOFF, &bytes, &trust(&deployment))
.expect("still decodes");
let HandoffFact::Handoff(h) = fact else {
panic!("expected Handoff variant");
};
assert_eq!(h.signature_status, SignatureVerdict::Untrusted);
assert_eq!(h.child_conversation_id, "child-foreign");
}
#[test]
fn structurally_malformed_payload_returns_none() {
let signer = HandoffSigner::from_seed(21);
assert!(
fold_handoff_event(
polyc_proto::kinds::HANDOFF,
&[0xFF, 0xFE, 0xFD],
&trust(&signer)
)
.is_none()
);
}
#[test]
fn unrelated_kind_base_returns_none() {
let signer = HandoffSigner::from_seed(21);
let handoff = signed_handoff(&signer, "child-a");
let bytes = handoff.encode_to_vec();
assert!(fold_handoff_event(polyc_proto::kinds::USAGE, &bytes, &trust(&signer)).is_none());
}
}