1use serde::Deserialize;
14use spine_agentic::swe::{SweArtifact, SweArtifactKind};
15use spine_agentic::{AgentCapability, AgentId, AgentProfile, Goal, SwarmTask, TrustLevel};
16use uuid::Uuid;
17
18#[derive(Debug, Clone, Deserialize)]
21pub struct AblAgentEnvelope {
22 pub name: String,
24 pub capabilities: Vec<AgentCapability>,
26 #[serde(default)]
28 pub trust_level: Option<TrustLevel>,
29 #[serde(default)]
31 pub requires_approval: Vec<String>,
32}
33
34impl AblAgentEnvelope {
35 pub fn from_json(s: &str) -> Result<Self, String> {
37 serde_json::from_str(s).map_err(|e| format!("bad agent envelope: {e}"))
38 }
39 pub fn into_profile(self) -> AgentProfile {
41 AgentProfile::new(self.name)
42 .with_capabilities(self.capabilities)
43 .with_trust(self.trust_level.unwrap_or(TrustLevel::Unknown))
44 }
45}
46
47#[derive(Debug, Clone, Deserialize)]
49pub struct AblSwarmEnvelope {
50 pub description: String,
52 pub required_capabilities: Vec<AgentCapability>,
54 pub min_members: usize,
56 pub max_members: usize,
58 #[serde(default)]
60 pub topology: Option<String>,
61 #[serde(default)]
63 pub consensus: Option<String>,
64}
65
66impl AblSwarmEnvelope {
67 pub fn from_json(s: &str) -> Result<Self, String> {
69 serde_json::from_str(s).map_err(|e| format!("bad swarm envelope: {e}"))
70 }
71 pub fn into_task(self) -> SwarmTask {
74 SwarmTask {
75 id: Uuid::new_v4(),
76 description: self.description,
77 goal: Box::new(Goal::FindAgents { capabilities: self.required_capabilities.clone() }),
78 min_members: self.min_members,
79 max_members: self.max_members,
80 required_capabilities: self.required_capabilities,
81 deadline: None,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Deserialize)]
88pub struct AblArtifactFrame {
89 pub byte_len: usize,
91 pub content_digest: String,
93 pub exec: bool,
95 pub payload_hex: String,
97}
98
99fn fnv1a64(bytes: &[u8]) -> u64 {
101 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
102 for &b in bytes {
103 h ^= b as u64;
104 h = h.wrapping_mul(0x0000_0100_0000_01b3);
105 }
106 h
107}
108
109fn from_hex(s: &str) -> Result<Vec<u8>, String> {
110 if s.len() % 2 != 0 {
111 return Err("odd-length hex".into());
112 }
113 (0..s.len())
114 .step_by(2)
115 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| format!("bad hex: {e}")))
116 .collect()
117}
118
119impl AblArtifactFrame {
120 pub fn from_json(s: &str) -> Result<Self, String> {
122 serde_json::from_str(s).map_err(|e| format!("bad frame: {e}"))
123 }
124
125 pub fn into_artifact(self, producer: AgentId) -> Result<SweArtifact, String> {
130 if self.exec {
131 return Err("frame.exec must be false (ABL load is no-exec)".into());
132 }
133 let bytes = from_hex(&self.payload_hex)?;
134 if bytes.len() != self.byte_len {
135 return Err(format!("byte_len {} != decoded {}", self.byte_len, bytes.len()));
136 }
137 let got = format!("{:016x}", fnv1a64(&bytes));
138 if got != self.content_digest {
139 return Err(format!("digest mismatch: frame={} computed={}", self.content_digest, got));
140 }
141 Ok(SweArtifact::new(&bytes, SweArtifactKind::Other("abl-artifact".into()), producer))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
151 fn agent_envelope_maps_to_profile() {
152 let json = r#"{"name":"Builder","capabilities":["AgentCommunication","SwarmParticipation","ContentExtraction","CodeExecution"],"trust_level":"Verified","miras_variant":"Titans","requires_approval":["write_files"]}"#;
153 let env = AblAgentEnvelope::from_json(json).expect("parse");
154 assert!(env.capabilities.contains(&AgentCapability::CodeExecution));
156 assert_eq!(env.requires_approval, vec!["write_files".to_string()]);
157 let profile = env.into_profile();
158 assert_eq!(profile.name, "Builder");
159 assert_eq!(profile.trust_level, TrustLevel::Verified);
160 assert!(profile.capabilities.contains(&AgentCapability::SwarmParticipation));
161 }
162
163 #[test]
164 fn custom_capability_round_trips_through_the_enum() {
165 let json = r#"{"name":"Reviewer","capabilities":[{"Custom":"review_pr"}]}"#;
166 let env = AblAgentEnvelope::from_json(json).expect("parse");
167 assert_eq!(env.capabilities, vec![AgentCapability::Custom("review_pr".into())]);
168 }
169
170 #[test]
171 fn swarm_envelope_maps_to_task() {
172 let json = r#"{"description":"Reviewers","required_capabilities":[{"Custom":"Reviewer"}],"min_members":5,"max_members":5,"topology":"ring","consensus":"quorum"}"#;
173 let env = AblSwarmEnvelope::from_json(json).expect("parse");
174 let task = env.into_task();
175 assert_eq!(task.description, "Reviewers");
176 assert_eq!(task.min_members, 5);
177 assert!(matches!(*task.goal, Goal::FindAgents { .. }));
178 assert_eq!(task.required_capabilities, vec![AgentCapability::Custom("Reviewer".into())]);
179 }
180
181 #[test]
182 fn frame_cross_validates_and_yields_artifact() {
183 let bytes = b"ABL1\x02\x00demo-net";
185 let digest = format!("{:016x}", fnv1a64(bytes));
186 let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
187 let json = format!(
188 r#"{{"kind":"abl-artifact","byte_len":{},"content_digest":"{}","exec":false,"signed":false,"payload_hex":"{}"}}"#,
189 bytes.len(), digest, hex
190 );
191 let frame = AblArtifactFrame::from_json(&json).expect("parse");
192 let art = frame.into_artifact(AgentId::new()).expect("valid frame");
193 assert_eq!(art.size, bytes.len());
194 assert!(!art.content_hash.is_empty(), "sha-256 content address assigned");
195 }
196
197 #[test]
198 fn frame_rejects_digest_mismatch() {
199 let json = r#"{"kind":"abl-artifact","byte_len":3,"content_digest":"deadbeefdeadbeef","exec":false,"signed":false,"payload_hex":"414243"}"#;
200 let frame = AblArtifactFrame::from_json(json).unwrap();
201 assert!(frame.into_artifact(AgentId::new()).is_err(), "tampered digest must be rejected");
202 }
203}