use buffa::Message as _;
use polyc_proto::proto::polychrome::handoff::v1::{Handoff, HandoffDenied, HandoffReturn};
use crate::{Signer, verify};
fn handoff_canonical_bytes(handoff: &Handoff) -> Vec<u8> {
let mut canonical = handoff.clone();
canonical.signature_hex.clear();
canonical.encode_to_vec()
}
fn handoff_return_canonical_bytes(ret: &HandoffReturn) -> Vec<u8> {
let mut canonical = ret.clone();
canonical.signature_hex.clear();
canonical.encode_to_vec()
}
fn handoff_denied_canonical_bytes(denied: &HandoffDenied) -> Vec<u8> {
let mut canonical = denied.clone();
canonical.signature_hex.clear();
canonical.encode_to_vec()
}
pub fn sign_handoff_into(signer: &Signer, handoff: &mut Handoff) {
handoff.signed_by = signer.public_key_bytes();
handoff.signature_hex.clear();
let sig = signer.sign(&handoff_canonical_bytes(handoff));
handoff.signature_hex = crate::hex::lower(&sig);
}
pub fn sign_handoff_return_into(signer: &Signer, ret: &mut HandoffReturn) {
ret.signed_by = signer.public_key_bytes();
ret.signature_hex.clear();
let sig = signer.sign(&handoff_return_canonical_bytes(ret));
ret.signature_hex = crate::hex::lower(&sig);
}
pub fn sign_handoff_denied_into(signer: &Signer, denied: &mut HandoffDenied) {
denied.signed_by = signer.public_key_bytes();
denied.signature_hex.clear();
let sig = signer.sign(&handoff_denied_canonical_bytes(denied));
denied.signature_hex = crate::hex::lower(&sig);
}
#[must_use]
pub fn verify_handoff(public_key: &[u8], handoff: &Handoff) -> bool {
let Some(sig) = crate::hex::decode(&handoff.signature_hex) else {
return false;
};
verify(public_key, &handoff_canonical_bytes(handoff), &sig)
}
#[must_use]
pub fn verify_handoff_return(public_key: &[u8], ret: &HandoffReturn) -> bool {
let Some(sig) = crate::hex::decode(&ret.signature_hex) else {
return false;
};
verify(public_key, &handoff_return_canonical_bytes(ret), &sig)
}
#[must_use]
pub fn verify_handoff_denied(public_key: &[u8], denied: &HandoffDenied) -> bool {
let Some(sig) = crate::hex::decode(&denied.signature_hex) else {
return false;
};
verify(public_key, &handoff_denied_canonical_bytes(denied), &sig)
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use polyc_proto::proto::polychrome::agent::v1::{Content, Message, TextContent, content};
use super::*;
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 sample_handoff() -> Handoff {
Handoff {
child_conversation_id: "child-7".to_owned(),
child_agent_id: "researcher".to_owned(),
carried_count: 2,
carried_context: vec![text_msg("user", "find prior art"), text_msg("model", "ok")],
reason: "delegate research".to_owned(),
..Default::default()
}
}
fn sample_return() -> HandoffReturn {
HandoffReturn {
child_conversation_id: "child-7".to_owned(),
final_message: buffa::MessageField::some(text_msg("model", "result: 42")),
..Default::default()
}
}
fn sample_denied() -> HandoffDenied {
HandoffDenied {
parent_conversation_id: "parent-7".to_owned(),
parent_agent_id: "assistant".to_owned(),
child_agent_id: "researcher".to_owned(),
reason: "delegate research".to_owned(),
denial_reason: "this agent can't hand off to that agent".to_owned(),
allowed: vec!["coding".to_owned()],
..Default::default()
}
}
#[test]
fn handoff_round_trips() {
let signer = Signer::from_seed(11);
let mut h = sample_handoff();
sign_handoff_into(&signer, &mut h);
assert!(!h.signature_hex.is_empty());
assert_eq!(h.signed_by, signer.public_key_bytes());
assert!(verify_handoff(&h.signed_by, &h));
}
#[test]
fn handoff_tampered_child_id_fails() {
let signer = Signer::from_seed(11);
let mut h = sample_handoff();
sign_handoff_into(&signer, &mut h);
h.child_conversation_id = "child-evil".to_owned();
assert!(!verify_handoff(&h.signed_by, &h));
}
#[test]
fn handoff_tampered_carried_context_fails() {
let signer = Signer::from_seed(11);
let mut h = sample_handoff();
sign_handoff_into(&signer, &mut h);
h.carried_context.push(text_msg("user", "leaked"));
assert!(!verify_handoff(&h.signed_by, &h));
}
#[test]
fn handoff_wrong_key_fails() {
let signer = Signer::from_seed(11);
let other = Signer::from_seed(12);
let mut h = sample_handoff();
sign_handoff_into(&signer, &mut h);
assert!(!verify_handoff(&other.public_key_bytes(), &h));
}
#[test]
fn handoff_unsigned_fails() {
let signer = Signer::from_seed(11);
let h = sample_handoff();
assert!(!verify_handoff(&signer.public_key_bytes(), &h));
}
#[test]
fn handoff_return_round_trips() {
let signer = Signer::from_seed(13);
let mut r = sample_return();
sign_handoff_return_into(&signer, &mut r);
assert!(verify_handoff_return(&signer.public_key_bytes(), &r));
}
#[test]
fn handoff_return_tampered_final_message_fails() {
let signer = Signer::from_seed(13);
let mut r = sample_return();
sign_handoff_return_into(&signer, &mut r);
r.final_message = buffa::MessageField::some(text_msg("model", "tampered"));
assert!(!verify_handoff_return(&signer.public_key_bytes(), &r));
}
#[test]
fn handoff_denied_round_trips() {
let signer = Signer::from_seed(14);
let mut d = sample_denied();
sign_handoff_denied_into(&signer, &mut d);
assert!(!d.signature_hex.is_empty());
assert_eq!(d.signed_by, signer.public_key_bytes());
assert!(verify_handoff_denied(&d.signed_by, &d));
}
#[test]
fn handoff_denied_tampered_allowed_fails() {
let signer = Signer::from_seed(14);
let mut d = sample_denied();
sign_handoff_denied_into(&signer, &mut d);
d.allowed.push("evil".to_owned());
assert!(!verify_handoff_denied(&d.signed_by, &d));
}
#[test]
fn handoff_denied_tampered_child_agent_id_fails() {
let signer = Signer::from_seed(14);
let mut d = sample_denied();
sign_handoff_denied_into(&signer, &mut d);
d.child_agent_id = "evil-agent".to_owned();
assert!(!verify_handoff_denied(&d.signed_by, &d));
}
#[test]
fn handoff_denied_wrong_key_fails() {
let signer = Signer::from_seed(14);
let other = Signer::from_seed(15);
let mut d = sample_denied();
sign_handoff_denied_into(&signer, &mut d);
assert!(!verify_handoff_denied(&other.public_key_bytes(), &d));
}
#[test]
fn handoff_denied_unsigned_fails() {
let signer = Signer::from_seed(14);
let d = sample_denied();
assert!(!verify_handoff_denied(&signer.public_key_bytes(), &d));
}
#[test]
fn handoff_garbage_signature_hex_returns_false() {
let mut h = sample_handoff();
h.signature_hex = "not-hex!".to_owned();
assert!(!verify_handoff(b"any-key", &h));
h.signature_hex = "abcd".to_owned();
assert!(!verify_handoff(b"any-key", &h));
}
#[test]
fn hex_round_trip() {
for bytes in [
&b""[..],
&[0x00, 0xff, 0x10, 0xab][..],
&(0u8..=255).collect::<Vec<_>>()[..],
] {
let s = crate::hex::lower(bytes);
assert_eq!(crate::hex::decode(&s).as_deref(), Some(bytes));
}
assert!(crate::hex::decode("abc").is_none(), "odd length rejected");
assert!(crate::hex::decode("zz").is_none(), "non-hex rejected");
}
}