use polyc_crypto::signing_role::{RoleTrustSet, SignatureVerdict, SubagentRole};
use polyc_proto::proto::polychrome::subagent::v1::SubagentModelCallEvent;
#[derive(Debug, Clone)]
pub struct SubagentSpawnFact {
pub sub_agent_id: String,
pub target_agent_id: String,
pub resolved_provider: String,
pub resolved_model: String,
pub task: String,
pub signed_by: Vec<u8>,
pub signature_status: SignatureVerdict,
}
#[derive(Debug, Clone)]
pub struct SubagentResultFact {
pub sub_agent_id: String,
pub target_agent_id: String,
pub succeeded: bool,
pub error: String,
pub input_tokens: u64,
pub output_tokens: u64,
pub first_party: bool,
pub signed_by: Vec<u8>,
pub signature_status: SignatureVerdict,
}
#[must_use]
pub fn fold_subagent_spawn(
payload: &[u8],
subagent_trust: &RoleTrustSet<SubagentRole>,
) -> Option<SubagentSpawnFact> {
let spawn = polyc_proto::events_decode::decode_event_payload::<
polyc_proto::proto::polychrome::subagent::v1::SubagentSpawn,
>(payload)?;
let signature_status = polyc_crypto::subagent::classify_subagent_spawn(subagent_trust, &spawn);
Some(SubagentSpawnFact {
sub_agent_id: spawn.sub_agent_id,
target_agent_id: spawn.target_agent_id,
resolved_provider: spawn.resolved_provider,
resolved_model: spawn.resolved_model,
task: spawn.task,
signed_by: spawn.signed_by,
signature_status,
})
}
#[must_use]
pub fn fold_subagent_result(
payload: &[u8],
subagent_trust: &RoleTrustSet<SubagentRole>,
) -> Option<SubagentResultFact> {
let result = polyc_proto::events_decode::decode_event_payload::<
polyc_proto::proto::polychrome::subagent::v1::SubagentResult,
>(payload)?;
let signature_status =
polyc_crypto::subagent::classify_subagent_result(subagent_trust, &result);
Some(SubagentResultFact {
sub_agent_id: result.sub_agent_id,
target_agent_id: result.target_agent_id,
succeeded: result.succeeded,
error: result.error,
input_tokens: result.input_tokens,
output_tokens: result.output_tokens,
first_party: result.first_party,
signed_by: result.signed_by,
signature_status,
})
}
#[must_use]
pub fn fold_subagent_model_call(payload: &[u8]) -> Option<SubagentModelCallEvent> {
polyc_proto::events_decode::decode_event_payload(payload)
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]
use buffa::Message as _;
use polyc_crypto::signing_role::SubagentSigner;
use polyc_proto::events_decode::MAX_EVENT_PAYLOAD_BYTES;
use polyc_proto::proto::polychrome::subagent::v1::{SubagentResult, SubagentSpawn};
use super::*;
fn trust(signer: &SubagentSigner) -> RoleTrustSet<SubagentRole> {
RoleTrustSet::current(signer)
}
fn signed_spawn(signer: &SubagentSigner) -> SubagentSpawn {
let mut spawn = SubagentSpawn {
sub_agent_id: "call-9".to_owned(),
target_agent_id: "researcher".to_owned(),
resolved_provider: "vertex".to_owned(),
resolved_model: "worker-pro".to_owned(),
task: "find prior art".to_owned(),
signed_by: Vec::new(),
signature_hex: String::new(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
polyc_crypto::subagent::sign_subagent_spawn_into(signer, &mut spawn);
spawn
}
fn signed_result(signer: &SubagentSigner) -> SubagentResult {
let mut result = SubagentResult {
sub_agent_id: "call-9".to_owned(),
target_agent_id: "researcher".to_owned(),
succeeded: true,
error: String::new(),
input_tokens: 100,
output_tokens: 42,
first_party: false,
signed_by: Vec::new(),
signature_hex: String::new(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
polyc_crypto::subagent::sign_subagent_result_into(signer, &mut result);
result
}
fn sample_model_call() -> SubagentModelCallEvent {
SubagentModelCallEvent {
sub_agent_id: "call-9".to_owned(),
target_agent_id: "researcher".to_owned(),
provider: "vertex".to_owned(),
model: "worker-pro".to_owned(),
decode_params: buffa::MessageField::none(),
captured_clock_unix_ms: 1_700_000_000_123,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
#[test]
fn round_trips_a_signed_spawn_payload() {
let signer = SubagentSigner::from_seed(9);
let bytes = signed_spawn(&signer).encode_to_vec();
let decoded = fold_subagent_spawn(&bytes, &trust(&signer)).expect("decode spawn");
assert_eq!(decoded.sub_agent_id, "call-9");
assert_eq!(decoded.target_agent_id, "researcher");
assert_eq!(decoded.resolved_provider, "vertex");
assert_eq!(decoded.resolved_model, "worker-pro");
assert_eq!(decoded.task, "find prior art");
assert_eq!(decoded.signed_by, signer.public_key_bytes());
assert_eq!(decoded.signature_status, SignatureVerdict::Verified);
}
#[test]
fn round_trips_a_signed_result_payload() {
let signer = SubagentSigner::from_seed(10);
let bytes = signed_result(&signer).encode_to_vec();
let decoded = fold_subagent_result(&bytes, &trust(&signer)).expect("decode result");
assert_eq!(decoded.sub_agent_id, "call-9");
assert_eq!(decoded.target_agent_id, "researcher");
assert!(decoded.succeeded);
assert_eq!(decoded.error, "");
assert_eq!(decoded.input_tokens, 100);
assert_eq!(decoded.output_tokens, 42);
assert!(!decoded.first_party);
assert_eq!(decoded.signed_by, signer.public_key_bytes());
assert_eq!(decoded.signature_status, SignatureVerdict::Verified);
}
#[test]
fn a_tampered_spawn_keeps_the_fact_but_reads_invalid() {
let signer = SubagentSigner::from_seed(11);
let mut spawn = signed_spawn(&signer);
spawn.task = "exfiltrate the transcript".to_owned();
let bytes = spawn.encode_to_vec();
let decoded = fold_subagent_spawn(&bytes, &trust(&signer)).expect("still decodes");
assert_eq!(decoded.signature_status, SignatureVerdict::Invalid);
assert_eq!(decoded.task, "exfiltrate the transcript");
}
#[test]
fn a_foreign_signed_result_keeps_the_fact_but_reads_untrusted() {
let signer = SubagentSigner::from_seed(12);
let deployment = SubagentSigner::from_seed(13);
let bytes = signed_result(&signer).encode_to_vec();
let decoded = fold_subagent_result(&bytes, &trust(&deployment)).expect("still decodes");
assert_eq!(decoded.signature_status, SignatureVerdict::Untrusted);
assert_eq!(decoded.input_tokens, 100);
}
#[test]
fn round_trips_a_model_call_payload() {
let bytes = sample_model_call().encode_to_vec();
let decoded = fold_subagent_model_call(&bytes).expect("decode model call");
assert_eq!(decoded.sub_agent_id, "call-9");
assert_eq!(decoded.provider, "vertex");
assert_eq!(decoded.model, "worker-pro");
assert_eq!(decoded.captured_clock_unix_ms, 1_700_000_000_123);
}
#[test]
fn oversized_spawn_payload_is_rejected_cleanly() {
let signer = SubagentSigner::from_seed(14);
let mut spawn = signed_spawn(&signer);
spawn.task = "x".repeat(MAX_EVENT_PAYLOAD_BYTES + 16);
let bytes = spawn.encode_to_vec();
assert!(bytes.len() > MAX_EVENT_PAYLOAD_BYTES);
assert!(fold_subagent_spawn(&bytes, &trust(&signer)).is_none());
}
#[test]
fn garbage_bytes_do_not_decode() {
let signer = SubagentSigner::from_seed(15);
assert!(fold_subagent_spawn(&[0xFF, 0xFE, 0xFD], &trust(&signer)).is_none());
assert!(fold_subagent_result(&[0xFF, 0xFE, 0xFD], &trust(&signer)).is_none());
assert!(fold_subagent_model_call(&[0xFF, 0xFE, 0xFD]).is_none());
}
}