use crate::groups::card::AgentCard;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
const X0X_TRANSPORT: &str = "x0x";
fn default_modes() -> Vec<String> {
vec!["text/plain".to_string(), "application/json".to_string()]
}
#[derive(Debug, Clone)]
pub struct A2aContext {
pub version: String,
pub exec_enabled: bool,
pub certificate_b64: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct A2aAgentCard {
pub name: String,
pub description: String,
pub version: String,
pub url: String,
pub preferred_transport: String,
pub supported_interfaces: Vec<A2aInterface>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<A2aProvider>,
pub skills: Vec<A2aSkill>,
pub security_schemes: BTreeMap<String, A2aSecurityScheme>,
pub capabilities: A2aCapabilities,
pub default_input_modes: Vec<String>,
pub default_output_modes: Vec<String>,
#[serde(rename = "x0xAgentId")]
pub x0x_agent_id: String,
#[serde(rename = "x0xMachineId")]
pub x0x_machine_id: String,
#[serde(rename = "x0xUserId", skip_serializing_if = "Option::is_none")]
pub x0x_user_id: Option<String>,
#[serde(rename = "x0xAgentPublicKey", skip_serializing_if = "Option::is_none")]
pub x0x_agent_public_key: Option<String>,
#[serde(rename = "x0xSignature", skip_serializing_if = "Option::is_none")]
pub x0x_signature: Option<String>,
#[serde(rename = "x0xCertificate", skip_serializing_if = "Option::is_none")]
pub x0x_certificate: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct A2aInterface {
pub transport: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct A2aProvider {
pub organization: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct A2aSkill {
pub id: String,
pub name: String,
pub description: String,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct A2aCapabilities {
pub streaming: bool,
pub push_notifications: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct A2aSecurityScheme {
#[serde(rename = "type")]
pub scheme_type: String,
#[serde(rename = "in", skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[must_use]
pub fn a2a_card_from(card: &AgentCard, ctx: &A2aContext) -> A2aAgentCard {
let id_prefix: String = card.agent_id.chars().take(8).collect();
let name = if card.display_name.is_empty() {
format!("x0x-agent-{id_prefix}")
} else {
card.display_name.clone()
};
let mut skills: Vec<A2aSkill> = Vec::new();
for store in &card.stores {
skills.push(A2aSkill {
id: format!("kv:{}", store.topic),
name: store.name.clone(),
description: format!("Replicated x0x KV store on topic '{}'", store.topic),
tags: vec!["kv".to_string(), "storage".to_string()],
});
}
for group in &card.groups {
skills.push(A2aSkill {
id: format!("group:{}", group.name),
name: group.name.clone(),
description: format!("x0x group '{}'", group.name),
tags: vec!["group".to_string(), "messaging".to_string()],
});
}
if ctx.exec_enabled {
skills.push(A2aSkill {
id: "exec".to_string(),
name: "Remote exec".to_string(),
description: "ACL-gated remote command execution".to_string(),
tags: vec!["exec".to_string(), "command".to_string()],
});
}
let mut security_schemes = BTreeMap::new();
security_schemes.insert(
"x0x-agent-signature".to_string(),
A2aSecurityScheme {
scheme_type: "apiKey".to_string(),
location: Some("header".to_string()),
name: Some("X-X0X-Agent-Signature".to_string()),
description: Some(
"ML-DSA-65 detached signature over the canonical agent card; \
verify against x0xAgentPublicKey (SHA-256 of which equals x0xAgentId)."
.to_string(),
),
},
);
let url = card.to_link();
A2aAgentCard {
name,
description: format!("x0x agent {id_prefix}"),
version: ctx.version.clone(),
url: url.clone(),
preferred_transport: X0X_TRANSPORT.to_string(),
supported_interfaces: vec![A2aInterface {
transport: X0X_TRANSPORT.to_string(),
url,
}],
provider: Some(A2aProvider {
organization: "x0x".to_string(),
url: Some("https://github.com/saorsa-labs".to_string()),
}),
skills,
security_schemes,
capabilities: A2aCapabilities {
streaming: false,
push_notifications: false,
},
default_input_modes: default_modes(),
default_output_modes: default_modes(),
x0x_agent_id: card.agent_id.clone(),
x0x_machine_id: card.machine_id.clone(),
x0x_user_id: card.user_id.clone(),
x0x_agent_public_key: card.agent_public_key.clone(),
x0x_signature: card.signature.clone(),
x0x_certificate: ctx.certificate_b64.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::groups::card::{CardGroup, CardStore};
use crate::identity::AgentKeypair;
fn ctx(exec: bool) -> A2aContext {
A2aContext {
version: "9.9.9".to_string(),
exec_enabled: exec,
certificate_b64: None,
}
}
fn signed_card(kp: &AgentKeypair) -> AgentCard {
let mut card = AgentCard::new("Ada".to_string(), &kp.agent_id(), &hex::encode([7u8; 32]));
card.stores.push(CardStore {
name: "docs".to_string(),
topic: "docs-topic".to_string(),
});
card.groups.push(CardGroup {
name: "team".to_string(),
invite_link: "x0x://invite/abc".to_string(),
});
card.sign(kp).expect("sign");
card
}
#[test]
fn maps_core_fields_and_extensions() {
let kp = AgentKeypair::generate().expect("kp");
let card = signed_card(&kp);
let a2a = a2a_card_from(&card, &ctx(false));
assert_eq!(a2a.name, "Ada");
assert_eq!(a2a.version, "9.9.9");
assert_eq!(a2a.preferred_transport, "x0x");
assert_eq!(a2a.supported_interfaces.len(), 1);
assert_eq!(a2a.supported_interfaces[0].transport, "x0x");
assert_eq!(a2a.x0x_agent_id, card.agent_id);
assert!(a2a.x0x_agent_public_key.is_some());
assert!(a2a.x0x_signature.is_some());
}
#[test]
fn derives_skills_from_stores_and_groups() {
let kp = AgentKeypair::generate().expect("kp");
let card = signed_card(&kp);
let a2a = a2a_card_from(&card, &ctx(false));
assert!(a2a.skills.iter().any(|s| s.id == "kv:docs-topic"));
assert!(a2a.skills.iter().any(|s| s.id == "group:team"));
}
#[test]
fn exec_skill_gated_on_config() {
let kp = AgentKeypair::generate().expect("kp");
let card = signed_card(&kp);
let disabled = a2a_card_from(&card, &ctx(false));
assert!(!disabled.skills.iter().any(|s| s.id == "exec"));
let enabled = a2a_card_from(&card, &ctx(true));
assert!(enabled.skills.iter().any(|s| s.id == "exec"));
}
#[test]
fn serializes_with_a2a_and_x0x_keys() {
let kp = AgentKeypair::generate().expect("kp");
let card = signed_card(&kp);
let a2a = a2a_card_from(&card, &ctx(false));
let json = serde_json::to_string(&a2a).expect("serialize");
assert!(json.contains("\"preferredTransport\""));
assert!(json.contains("\"defaultInputModes\""));
assert!(json.contains("\"x0xAgentId\""));
assert!(json.contains("\"x0xSignature\""));
}
}