use serde::Deserialize;
use spine_agentic::swe::{SweArtifact, SweArtifactKind};
use spine_agentic::{AgentCapability, AgentId, AgentProfile, Goal, SwarmTask, TrustLevel};
use uuid::Uuid;
#[derive(Debug, Clone, Deserialize)]
pub struct AblAgentEnvelope {
pub name: String,
pub capabilities: Vec<AgentCapability>,
#[serde(default)]
pub trust_level: Option<TrustLevel>,
#[serde(default)]
pub requires_approval: Vec<String>,
}
impl AblAgentEnvelope {
pub fn from_json(s: &str) -> Result<Self, String> {
serde_json::from_str(s).map_err(|e| format!("bad agent envelope: {e}"))
}
pub fn into_profile(self) -> AgentProfile {
AgentProfile::new(self.name)
.with_capabilities(self.capabilities)
.with_trust(self.trust_level.unwrap_or(TrustLevel::Unknown))
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AblSwarmEnvelope {
pub description: String,
pub required_capabilities: Vec<AgentCapability>,
pub min_members: usize,
pub max_members: usize,
#[serde(default)]
pub topology: Option<String>,
#[serde(default)]
pub consensus: Option<String>,
}
impl AblSwarmEnvelope {
pub fn from_json(s: &str) -> Result<Self, String> {
serde_json::from_str(s).map_err(|e| format!("bad swarm envelope: {e}"))
}
pub fn into_task(self) -> SwarmTask {
SwarmTask {
id: Uuid::new_v4(),
description: self.description,
goal: Box::new(Goal::FindAgents { capabilities: self.required_capabilities.clone() }),
min_members: self.min_members,
max_members: self.max_members,
required_capabilities: self.required_capabilities,
deadline: None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AblArtifactFrame {
pub byte_len: usize,
pub content_digest: String,
pub exec: bool,
pub payload_hex: String,
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
fn from_hex(s: &str) -> Result<Vec<u8>, String> {
if s.len() % 2 != 0 {
return Err("odd-length hex".into());
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| format!("bad hex: {e}")))
.collect()
}
impl AblArtifactFrame {
pub fn from_json(s: &str) -> Result<Self, String> {
serde_json::from_str(s).map_err(|e| format!("bad frame: {e}"))
}
pub fn into_artifact(self, producer: AgentId) -> Result<SweArtifact, String> {
if self.exec {
return Err("frame.exec must be false (ABL load is no-exec)".into());
}
let bytes = from_hex(&self.payload_hex)?;
if bytes.len() != self.byte_len {
return Err(format!("byte_len {} != decoded {}", self.byte_len, bytes.len()));
}
let got = format!("{:016x}", fnv1a64(&bytes));
if got != self.content_digest {
return Err(format!("digest mismatch: frame={} computed={}", self.content_digest, got));
}
Ok(SweArtifact::new(&bytes, SweArtifactKind::Other("abl-artifact".into()), producer))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_envelope_maps_to_profile() {
let json = r#"{"name":"Builder","capabilities":["AgentCommunication","SwarmParticipation","ContentExtraction","CodeExecution"],"trust_level":"Verified","miras_variant":"Titans","requires_approval":["write_files"]}"#;
let env = AblAgentEnvelope::from_json(json).expect("parse");
assert!(env.capabilities.contains(&AgentCapability::CodeExecution));
assert_eq!(env.requires_approval, vec!["write_files".to_string()]);
let profile = env.into_profile();
assert_eq!(profile.name, "Builder");
assert_eq!(profile.trust_level, TrustLevel::Verified);
assert!(profile.capabilities.contains(&AgentCapability::SwarmParticipation));
}
#[test]
fn custom_capability_round_trips_through_the_enum() {
let json = r#"{"name":"Reviewer","capabilities":[{"Custom":"review_pr"}]}"#;
let env = AblAgentEnvelope::from_json(json).expect("parse");
assert_eq!(env.capabilities, vec![AgentCapability::Custom("review_pr".into())]);
}
#[test]
fn swarm_envelope_maps_to_task() {
let json = r#"{"description":"Reviewers","required_capabilities":[{"Custom":"Reviewer"}],"min_members":5,"max_members":5,"topology":"ring","consensus":"quorum"}"#;
let env = AblSwarmEnvelope::from_json(json).expect("parse");
let task = env.into_task();
assert_eq!(task.description, "Reviewers");
assert_eq!(task.min_members, 5);
assert!(matches!(*task.goal, Goal::FindAgents { .. }));
assert_eq!(task.required_capabilities, vec![AgentCapability::Custom("Reviewer".into())]);
}
#[test]
fn frame_cross_validates_and_yields_artifact() {
let bytes = b"ABL1\x02\x00demo-net";
let digest = format!("{:016x}", fnv1a64(bytes));
let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
let json = format!(
r#"{{"kind":"abl-artifact","byte_len":{},"content_digest":"{}","exec":false,"signed":false,"payload_hex":"{}"}}"#,
bytes.len(), digest, hex
);
let frame = AblArtifactFrame::from_json(&json).expect("parse");
let art = frame.into_artifact(AgentId::new()).expect("valid frame");
assert_eq!(art.size, bytes.len());
assert!(!art.content_hash.is_empty(), "sha-256 content address assigned");
}
#[test]
fn frame_rejects_digest_mismatch() {
let json = r#"{"kind":"abl-artifact","byte_len":3,"content_digest":"deadbeefdeadbeef","exec":false,"signed":false,"payload_hex":"414243"}"#;
let frame = AblArtifactFrame::from_json(json).unwrap();
assert!(frame.into_artifact(AgentId::new()).is_err(), "tampered digest must be rejected");
}
}