use std::collections::HashSet;
use polyc_crypto::signing_role::{HandoffRole, RoleTrustSet, SignatureVerdict, SigningKeyIdentity};
use polyc_eventlog_model::Event;
use polyc_proto::kinds;
use crate::{HandoffFact, PreparedSource, fold_handoff_event, prepare_conversation_core};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ConversationDelegationFacts {
pub handoffs: Vec<HandoffHistoryFact>,
pub signers: Vec<HandoffSignerFact>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HandoffPhase {
Handoff,
Denied,
}
impl HandoffPhase {
pub const ALL: [Self; 2] = [Self::Handoff, Self::Denied];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Handoff => "handoff",
Self::Denied => "handoff_denied",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HandoffHistoryFact {
pub position: u64,
pub turn_id: String,
pub phase: HandoffPhase,
pub child_conversation_id: String,
pub child_agent_id: String,
pub carried_count: u64,
pub reason: String,
pub parent_agent_id: String,
pub denial_reason: String,
pub allowed: Vec<String>,
pub signature_status: SignatureVerdict,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HandoffSignerFact {
pub position: u64,
pub signed_by: [u8; 32],
pub signer_key_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConversationDelegationError {
#[error("position {position} appears more than once in one source prefix")]
RepeatedPosition {
position: u64,
},
#[error("the {kind} record at position {position} is malformed")]
Malformed {
kind: &'static str,
position: u64,
},
}
pub fn fold_conversation_delegation(
events: &[(u64, Event)],
handoff_trust: &RoleTrustSet<HandoffRole>,
) -> Result<ConversationDelegationFacts, ConversationDelegationError> {
refuse_repeated_positions(events)?;
let mut facts = ConversationDelegationFacts::default();
for (position, event) in events {
let (base, turn) = kinds::parse(&event.kind);
if base != kinds::HANDOFF && base != kinds::HANDOFF_DENIED {
continue;
}
let kind = if base == kinds::HANDOFF {
kinds::HANDOFF
} else {
kinds::HANDOFF_DENIED
};
let fact = fold_handoff_event(base, &event.payload, handoff_trust).ok_or(
ConversationDelegationError::Malformed {
kind,
position: *position,
},
)?;
let turn_id = turn.map_or_else(String::new, |id| id.to_string());
let (row, signed_by) = match fact {
HandoffFact::Handoff(h) => (
HandoffHistoryFact {
position: *position,
turn_id,
phase: HandoffPhase::Handoff,
child_conversation_id: h.child_conversation_id,
child_agent_id: h.child_agent_id,
carried_count: u64::from(h.carried_count),
reason: h.reason,
parent_agent_id: String::new(),
denial_reason: String::new(),
allowed: Vec::new(),
signature_status: h.signature_status,
},
h.signed_by,
),
HandoffFact::Denied(d) => (
HandoffHistoryFact {
position: *position,
turn_id,
phase: HandoffPhase::Denied,
child_conversation_id: String::new(),
child_agent_id: d.child_agent_id,
carried_count: 0,
reason: d.reason,
parent_agent_id: d.parent_agent_id,
denial_reason: d.denial_reason,
allowed: d.allowed,
signature_status: d.signature_status,
},
d.signed_by,
),
};
facts.signers.extend(signer(*position, signed_by));
facts.handoffs.push(row);
}
Ok(facts)
}
pub fn prepare_and_fold_conversation_delegation(
events: &mut [(u64, Event)],
partition: &str,
handoff_trust: &RoleTrustSet<HandoffRole>,
) -> Result<(PreparedSource, ConversationDelegationFacts), ConversationDelegationError> {
let prepared = prepare_conversation_core(events, partition);
let facts = fold_conversation_delegation(events, handoff_trust)?;
Ok((prepared, facts))
}
fn signer(position: u64, signed_by: Vec<u8>) -> Option<HandoffSignerFact> {
let identity = SigningKeyIdentity::for_public_key::<HandoffRole>(signed_by).ok()?;
let signed_by: [u8; 32] = identity.public_key().try_into().ok()?;
Some(HandoffSignerFact {
position,
signed_by,
signer_key_id: identity.key_id().to_owned(),
})
}
fn refuse_repeated_positions(events: &[(u64, Event)]) -> Result<(), ConversationDelegationError> {
let mut positions = HashSet::with_capacity(events.len());
for (position, _) in events {
if !positions.insert(*position) {
return Err(ConversationDelegationError::RepeatedPosition {
position: *position,
});
}
}
Ok(())
}
#[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::handoff::v1::{Handoff, HandoffDenied};
use uuid::Uuid;
use super::*;
#[test]
fn every_phase_appears_in_all() {
for phase in HandoffPhase::ALL {
match phase {
HandoffPhase::Handoff | HandoffPhase::Denied => {}
}
}
assert_eq!(HandoffPhase::ALL.len(), 2);
}
fn trust(signer: &HandoffSigner) -> RoleTrustSet<HandoffRole> {
RoleTrustSet::current(signer)
}
fn handoff_event(signer: &HandoffSigner, turn: Option<&Uuid>) -> Event {
let mut h = Handoff {
child_conversation_id: "child-a".to_owned(),
child_agent_id: "researcher".to_owned(),
carried_count: 2,
reason: "delegate research".to_owned(),
..Default::default()
};
polyc_crypto::handoff::sign_handoff_into(signer, &mut h);
let kind = turn.map_or_else(
|| kinds::HANDOFF.to_owned(),
|turn| kinds::tagged(kinds::HANDOFF, turn),
);
Event::new(kind, h.encode_to_vec())
}
fn denied_event(signer: &HandoffSigner, turn: &Uuid) -> Event {
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 cannot 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);
Event::new(
kinds::tagged(kinds::HANDOFF_DENIED, turn),
d.encode_to_vec(),
)
}
#[test]
fn a_record_whose_turn_never_completed_still_folds() {
let signer = HandoffSigner::from_seed(31);
let turn = Uuid::from_u128(7);
let events = vec![
(9, handoff_event(&signer, Some(&turn))),
(10, denied_event(&signer, &turn)),
];
let facts = fold_conversation_delegation(&events, &trust(&signer)).unwrap();
assert_eq!(
facts.handoffs.len(),
2,
"a committed-turn gate would empty this"
);
assert_eq!(facts.handoffs[0].phase, HandoffPhase::Handoff);
assert_eq!(facts.handoffs[1].phase, HandoffPhase::Denied);
}
#[test]
fn a_malformed_payload_refuses_instead_of_vanishing() {
let signer = HandoffSigner::from_seed(32);
let events = vec![(
4,
Event::new(kinds::HANDOFF.to_owned(), vec![0xff, 0xff, 0xff]),
)];
assert_eq!(
fold_conversation_delegation(&events, &trust(&signer)),
Err(ConversationDelegationError::Malformed {
kind: kinds::HANDOFF,
position: 4,
})
);
}
#[test]
fn a_tampered_record_keeps_every_field_and_reads_invalid() {
let signer = HandoffSigner::from_seed(33);
let mut h = Handoff {
child_conversation_id: "child-a".to_owned(),
child_agent_id: "researcher".to_owned(),
carried_count: 2,
reason: "delegate research".to_owned(),
..Default::default()
};
polyc_crypto::handoff::sign_handoff_into(&signer, &mut h);
h.child_conversation_id = "child-evil".to_owned();
let events = vec![(1, Event::new(kinds::HANDOFF.to_owned(), h.encode_to_vec()))];
let facts = fold_conversation_delegation(&events, &trust(&signer)).unwrap();
assert_eq!(facts.handoffs.len(), 1);
assert_eq!(facts.handoffs[0].child_conversation_id, "child-evil");
assert_eq!(
facts.handoffs[0].signature_status,
SignatureVerdict::Invalid
);
}
#[test]
fn a_key_this_deployment_does_not_trust_reads_untrusted() {
let signer = HandoffSigner::from_seed(34);
let stranger = HandoffSigner::from_seed(99);
let events = vec![(1, handoff_event(&signer, None))];
let facts = fold_conversation_delegation(&events, &trust(&stranger)).unwrap();
assert_eq!(
facts.handoffs[0].signature_status,
SignatureVerdict::Untrusted
);
}
#[test]
fn a_denial_carries_its_allowlist_and_a_handoff_carries_none() {
let signer = HandoffSigner::from_seed(35);
let turn = Uuid::from_u128(3);
let events = vec![
(1, handoff_event(&signer, Some(&turn))),
(2, denied_event(&signer, &turn)),
];
let facts = fold_conversation_delegation(&events, &trust(&signer)).unwrap();
assert!(facts.handoffs[0].allowed.is_empty());
assert_eq!(
facts.handoffs[1].allowed,
vec!["coding".to_owned(), "research".to_owned()]
);
assert_eq!(facts.handoffs[0].carried_count, 2);
assert_eq!(facts.handoffs[1].carried_count, 0);
assert_eq!(facts.handoffs[0].turn_id, turn.to_string());
}
#[test]
fn an_untagged_record_carries_an_empty_turn_id() {
let signer = HandoffSigner::from_seed(36);
let events = vec![(1, handoff_event(&signer, None))];
let facts = fold_conversation_delegation(&events, &trust(&signer)).unwrap();
assert_eq!(facts.handoffs[0].turn_id, "");
}
#[test]
fn every_record_contributes_one_signer_row_at_its_own_position() {
let signer = HandoffSigner::from_seed(37);
let turn = Uuid::from_u128(5);
let events = vec![
(11, handoff_event(&signer, Some(&turn))),
(12, denied_event(&signer, &turn)),
];
let facts = fold_conversation_delegation(&events, &trust(&signer)).unwrap();
assert_eq!(facts.signers.len(), 2);
assert_eq!(facts.signers[0].position, 11);
assert_eq!(facts.signers[1].position, 12);
assert_eq!(
facts.signers[0].signed_by.to_vec(),
signer.public_key_bytes()
);
assert!(!facts.signers[0].signer_key_id.is_empty());
assert_eq!(
facts.signers[0].signer_key_id,
facts.signers[1].signer_key_id
);
}
#[test]
fn a_record_carrying_no_usable_key_reads_invalid_and_publishes_no_signer() {
let signer = HandoffSigner::from_seed(38);
let mut h = Handoff {
child_conversation_id: "child-a".to_owned(),
..Default::default()
};
polyc_crypto::handoff::sign_handoff_into(&signer, &mut h);
h.signed_by = vec![0u8; 31];
let events = vec![(6, Event::new(kinds::HANDOFF.to_owned(), h.encode_to_vec()))];
let facts = fold_conversation_delegation(&events, &trust(&signer))
.expect("an unusable key must not refuse the generation");
assert_eq!(facts.handoffs.len(), 1, "the record still publishes");
assert_eq!(facts.handoffs[0].position, 6);
assert_eq!(facts.handoffs[0].child_conversation_id, "child-a");
assert_eq!(
facts.handoffs[0].signature_status,
SignatureVerdict::Invalid,
"a signature cannot verify against an unusable key"
);
assert!(
facts.signers.is_empty(),
"there is no usable key to publish in the Fleet-only signer table"
);
}
#[test]
fn a_repeated_source_position_is_refused() {
let signer = HandoffSigner::from_seed(39);
let events = vec![
(2, handoff_event(&signer, None)),
(2, handoff_event(&signer, None)),
];
assert_eq!(
fold_conversation_delegation(&events, &trust(&signer)),
Err(ConversationDelegationError::RepeatedPosition { position: 2 })
);
}
#[test]
fn an_unrelated_kind_contributes_no_row() {
let signer = HandoffSigner::from_seed(40);
let events = vec![(1, Event::new(kinds::USAGE.to_owned(), vec![]))];
let facts = fold_conversation_delegation(&events, &trust(&signer)).unwrap();
assert!(facts.handoffs.is_empty());
assert!(facts.signers.is_empty());
}
}