1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8fn slugify(s: &str) -> String {
12 let lowered = s.to_lowercase();
13 let replaced: String = lowered
14 .chars()
15 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
16 .collect();
17 replaced
18 .trim_matches('-')
19 .split('-')
20 .filter(|s| !s.is_empty())
21 .collect::<Vec<_>>()
22 .join("-")
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct AgentSkill {
31 pub id: String,
33 pub name: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub description: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub tags: Option<Vec<String>>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub examples: Option<Vec<String>>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub input_modes: Option<Vec<String>>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub output_modes: Option<Vec<String>>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct AgentInterface {
54 pub url: String,
56 pub protocol_binding: String,
58 pub protocol_version: String,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct A2ACapabilities {
66 pub streaming: bool,
67 pub push_notifications: bool,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub state_transition_history: Option<bool>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub extended_agent_card: Option<bool>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct A2AProvider {
80 pub name: String,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub url: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub email: Option<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(tag = "type", rename_all = "camelCase")]
92pub enum SecurityScheme {
93 #[serde(rename = "apiKey")]
95 ApiKey {
96 name: String,
97 #[serde(rename = "in")]
98 location: String, #[serde(skip_serializing_if = "Option::is_none")]
100 description: Option<String>,
101 },
102 #[serde(rename = "http")]
104 Http {
105 scheme: String,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 bearer_format: Option<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 description: Option<String>,
110 },
111 #[serde(rename = "oauth2")]
113 OAuth2 {
114 flows: serde_json::Value,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 description: Option<String>,
117 },
118 #[serde(rename = "openIdConnect")]
120 OpenIdConnect {
121 open_id_connect_url: String,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 description: Option<String>,
124 },
125}
126
127pub type SecuritySchemes = HashMap<String, SecurityScheme>;
129
130pub type SecurityRequirement = HashMap<String, Vec<String>>;
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct JwsSignature {
139 #[serde(rename = "protected")]
141 pub protected_header: String,
142 pub signature: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub header: Option<serde_json::Value>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct AgentExtension {
153 pub uri: String,
155 #[serde(flatten)]
157 pub data: serde_json::Map<String, serde_json::Value>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct TrustModel {
165 pub proof_type: String,
166 pub sybil_resistant: bool,
167 pub ofac_screened: bool,
168 pub minimum_age_verified: u64,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct CardCredentials {
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub nationality: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub issuing_state: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub older_than: Option<u64>,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub ofac_clean: Option<bool>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub has_name: Option<bool>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub has_date_of_birth: Option<bool>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub has_gender: Option<bool>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub document_expiry: Option<String>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct SelfProtocolExtension {
195 pub agent_id: u64,
196 pub registry: String,
197 pub chain_id: u64,
198 pub proof_provider: String,
199 pub provider_name: String,
200 pub verification_strength: u8,
201 pub trust_model: TrustModel,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub credentials: Option<CardCredentials>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct Erc8004Service {
212 pub name: String, pub endpoint: String,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub version: Option<String>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase")]
223pub struct Erc8004Registration {
224 pub agent_id: u64,
225 pub agent_registry: String, }
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct Erc8004AgentDocument {
235 #[serde(rename = "type")]
237 pub doc_type: String,
238 pub name: String,
239 pub description: String,
240 pub image: String,
241 pub services: Vec<Erc8004Service>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub active: Option<bool>,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub registrations: Option<Vec<Erc8004Registration>>,
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub supported_trust: Option<Vec<String>>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub version: Option<String>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub url: Option<String>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub provider: Option<A2AProvider>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub capabilities: Option<A2ACapabilities>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub security_schemes: Option<SecuritySchemes>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub security: Option<Vec<SecurityRequirement>>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub default_input_modes: Option<Vec<String>>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub default_output_modes: Option<Vec<String>>,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub supported_interfaces: Option<Vec<AgentInterface>>,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub icon_url: Option<String>,
281
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub documentation_url: Option<String>,
285
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub signatures: Option<Vec<JwsSignature>>,
289
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub extensions: Option<Vec<AgentExtension>>,
293
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub self_protocol: Option<SelfProtocolExtension>,
297
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub skills: Option<Vec<AgentSkill>>,
301}
302
303
304pub fn get_provider_label(strength: u8) -> &'static str {
307 match strength {
308 100..=u8::MAX => "passport",
309 80..=99 => "kyc",
310 60..=79 => "govt_id",
311 40..=59 => "liveness",
312 _ => "unknown",
313 }
314}
315
316pub fn get_strength_color(strength: u8) -> &'static str {
317 match strength {
318 80..=u8::MAX => "green",
319 60..=79 => "blue",
320 40..=59 => "amber",
321 _ => "gray",
322 }
323}
324
325#[derive(Debug, Clone)]
329pub struct A2AOptions {
330 pub version: String,
331 pub url: String,
332 pub provider: Option<A2AProvider>,
333 pub capabilities: Option<A2ACapabilities>,
334 pub security_schemes: Option<SecuritySchemes>,
335 pub security: Option<Vec<SecurityRequirement>>,
336 pub default_input_modes: Option<Vec<String>>,
337 pub default_output_modes: Option<Vec<String>>,
338 pub skills: Option<Vec<AgentSkill>>,
339 pub supported_interfaces: Option<Vec<AgentInterface>>,
340 pub icon_url: Option<String>,
341 pub documentation_url: Option<String>,
342 pub signatures: Option<Vec<JwsSignature>>,
343 pub extensions: Option<Vec<AgentExtension>>,
344}
345
346#[derive(Debug, Clone)]
348pub struct GenerateRegistrationJsonOptions {
349 pub name: String,
350 pub description: String,
351 pub image: String,
352 pub services: Vec<Erc8004Service>,
353 pub active: Option<bool>,
354 pub registrations: Option<Vec<Erc8004Registration>>,
355 pub supported_trust: Option<Vec<String>>,
356 pub a2a: Option<A2AOptions>,
357}
358
359pub fn generate_registration_json(
365 options: GenerateRegistrationJsonOptions,
366) -> Erc8004AgentDocument {
367 let a2a = &options.a2a;
368
369 let skills = a2a.as_ref().and_then(|a| {
371 a.skills.as_ref().map(|skills| {
372 skills
373 .iter()
374 .map(|s| AgentSkill {
375 id: if s.id.is_empty() {
376 slugify(&s.name)
377 } else {
378 s.id.clone()
379 },
380 ..s.clone()
381 })
382 .collect()
383 })
384 });
385
386 let supported_interfaces = a2a.as_ref().map(|a| {
388 a.supported_interfaces.clone().unwrap_or_else(|| {
389 vec![AgentInterface {
390 url: a.url.clone(),
391 protocol_binding: "JSONRPC".to_string(),
392 protocol_version: "0.3.0".to_string(),
393 }]
394 })
395 });
396
397 let mut services = options.services;
399 if let Some(a) = a2a.as_ref() {
400 if !services.iter().any(|s| s.name == "A2A") {
401 services.push(Erc8004Service {
402 name: "A2A".to_string(),
403 endpoint: a.url.clone(),
404 version: Some(a.version.clone()),
405 });
406 }
407 }
408
409 let (version, url, provider, capabilities, security_schemes, security,
410 default_input_modes, default_output_modes, icon_url, documentation_url,
411 signatures, extensions) = match a2a.as_ref() {
412 Some(a) => (
413 Some(a.version.clone()),
414 Some(a.url.clone()),
415 a.provider.clone(),
416 Some(a.capabilities.clone().unwrap_or(A2ACapabilities {
417 streaming: false,
418 push_notifications: false,
419 state_transition_history: Some(false),
420 extended_agent_card: Some(false),
421 })),
422 a.security_schemes.clone(),
423 a.security.clone(),
424 a.default_input_modes.clone(),
425 a.default_output_modes.clone(),
426 Some(a.icon_url.clone().unwrap_or_else(|| options.image.clone())),
427 a.documentation_url.clone(),
428 a.signatures.clone(),
429 a.extensions.clone(),
430 ),
431 None => (None, None, None, None, None, None, None, None, None, None, None, None),
432 };
433
434 Erc8004AgentDocument {
435 doc_type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1".to_string(),
436 name: options.name,
437 description: options.description,
438 image: options.image,
439 services,
440 active: options.active,
441 registrations: options.registrations,
442 supported_trust: options.supported_trust,
443 version,
444 url,
445 provider,
446 capabilities,
447 security_schemes,
448 security,
449 default_input_modes,
450 default_output_modes,
451 supported_interfaces,
452 icon_url,
453 documentation_url,
454 signatures,
455 extensions,
456 self_protocol: None,
457 skills,
458 }
459}