1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct AgentCard {
5 pub metadata: AgentCardMetadata,
6 pub spec: AgentCardSpec,
7 #[serde(skip_serializing_if = "Option::is_none")]
8 pub status: Option<AgentStatus>,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AgentCardMetadata {
13 pub id: String,
14 pub name: String,
15 pub version: String,
16 pub owner: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct AgentCardSpec {
21 pub domains: Vec<String>,
22 pub capabilities: Vec<CapabilitySpec>,
23 pub endpoints: AgentEndpoints,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CapabilitySpec {
28 pub id: String,
29 pub description: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AgentEndpoints {
34 pub control_plane: ControlPlaneEndpoint,
35 pub data_plane: DataPlaneEndpoint,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ControlPlaneEndpoint {
40 pub nats_subject: String,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct DataPlaneEndpoint {
45 pub grpc: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct AgentStatus {
50 pub health: Option<String>,
51 pub last_heartbeat: Option<String>,
52}
53
54pub fn validate_agent_card(card: &AgentCard) -> Result<(), String> {
55 if card.metadata.id.is_empty() {
56 return Err("metadata.id is empty".into());
57 }
58 if card.spec.domains.is_empty() {
59 return Err("spec.domains is empty".into());
60 }
61 if card.spec.capabilities.is_empty() {
62 return Err("spec.capabilities is empty".into());
63 }
64 Ok(())
65}
66
67use serde_json::Value;
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub struct PartiesData {
71 pub consumer: String,
72 pub provider: String,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76pub struct SessionData {
77 pub session_id: String,
78 pub created_at: Option<String>,
79 pub expires_at: Option<String>,
80 pub session_token: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84pub struct AgreedTerms {
85 pub max_latency_ms: Option<u64>,
86 pub security_level: Option<String>,
87 pub cost_usd: Option<f64>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub struct CapabilityMatchData {
92 pub request_id: String,
93 pub parties: PartiesData,
94 pub session: SessionData,
95 pub agreed_terms: Option<AgreedTerms>,
96 #[serde(flatten)]
97 pub extra: Option<Value>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct CapabilityRejectData {
102 pub request_id: String,
103 pub reason: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107#[serde(tag = "kind", rename_all = "lowercase")]
108pub enum MatchOrReject {
109 Match { data: CapabilityMatchData },
110 Reject { data: CapabilityRejectData },
111}
112
113pub fn parse_match_or_reject(v: &Value) -> Result<MatchOrReject, String> {
114 if let Some(kind) = v.get("kind").and_then(|k| k.as_str()) {
116 match kind {
117 "match" => {
118 let data = v.get("data").ok_or("missing data")?;
119 let m: CapabilityMatchData = serde_json::from_value(data.clone()).map_err(|e| e.to_string())?;
120 return Ok(MatchOrReject::Match { data: m });
121 }
122 "reject" => {
123 let data = v.get("data").ok_or("missing data")?;
124 let r: CapabilityRejectData = serde_json::from_value(data.clone()).map_err(|e| e.to_string())?;
125 return Ok(MatchOrReject::Reject { data: r });
126 }
127 _ => return Err("unknown kind".into()),
128 }
129 }
130
131 if let Some(ty) = v.get("type").and_then(|t| t.as_str()) {
133 match ty {
134 "amp.capability.match" => {
135 let data = v.get("data").ok_or("missing data")?;
136 let m: CapabilityMatchData = serde_json::from_value(data.clone()).map_err(|e| e.to_string())?;
137 return Ok(MatchOrReject::Match { data: m });
138 }
139 "amp.capability.reject" => {
140 let data = v.get("data").ok_or("missing data")?;
141 let r: CapabilityRejectData = serde_json::from_value(data.clone()).map_err(|e| e.to_string())?;
142 return Ok(MatchOrReject::Reject { data: r });
143 }
144 _ => return Err("unknown cloud event type".into()),
145 }
146 }
147
148 Err("unrecognized message format".into())
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use serde_json::json;
155
156 #[test]
157 fn parse_match_cloud_event() {
158 let msg = json!({
159 "specversion": "1.0",
160 "type": "amp.capability.match",
161 "source": "did:mesh:agent:matcher",
162 "id": "evt-1",
163 "data": {
164 "request_id": "req-1",
165 "parties": { "consumer": "did:mesh:agent:cons", "provider": "did:mesh:agent:prov" },
166 "session": { "session_id": "s-1", "session_token": "tok" }
167 }
168 });
169
170 let parsed = parse_match_or_reject(&msg).expect("should parse");
171 match parsed {
172 MatchOrReject::Match { data } => {
173 assert_eq!(data.request_id, "req-1");
174 assert_eq!(data.parties.consumer, "did:mesh:agent:cons");
175 }
176 _ => panic!("expected match"),
177 }
178 }
179
180 #[test]
181 fn parse_reject_cloud_event() {
182 let msg = json!({
183 "specversion": "1.0",
184 "type": "amp.capability.reject",
185 "source": "did:mesh:agent:matcher",
186 "id": "evt-2",
187 "data": { "request_id": "req-2", "reason": "no-candidates" }
188 });
189
190 let parsed = parse_match_or_reject(&msg).expect("should parse");
191 match parsed {
192 MatchOrReject::Reject { data } => {
193 assert_eq!(data.request_id, "req-2");
194 assert_eq!(data.reason.unwrap(), "no-candidates");
195 }
196 _ => panic!("expected reject"),
197 }
198 }
199}