use serde::{Deserialize, Serialize};
use crate::manifest::Manifest;
pub const WELL_KNOWN_PATH: &str = "/.well-known/agent-card.json";
pub const BINDING: &str = "JSONRPC";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CardSkill {
pub id: String,
pub name: String,
pub description: String,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CardCapabilities {
pub streaming: bool,
pub push_notifications: bool,
pub extended_agent_card: bool,
}
impl CardCapabilities {
const fn implemented() -> Self {
Self {
streaming: true,
push_notifications: false,
extended_agent_card: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CardInterface {
pub url: String,
pub protocol_binding: String,
pub protocol_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tenant: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpAuthSecurityScheme {
pub scheme: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bearer_format: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CardSecurityScheme {
pub http_auth_security_scheme: HttpAuthSecurityScheme,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecurityScopeList {
pub list: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CardSecurityRequirement {
pub schemes: std::collections::BTreeMap<String, SecurityScopeList>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CardSecurity {
name: String,
scheme: CardSecurityScheme,
scopes: Vec<String>,
}
impl CardSecurity {
#[must_use]
pub fn bearer(
name: impl Into<String>,
scopes: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
name: name.into(),
scheme: CardSecurityScheme {
http_auth_security_scheme: HttpAuthSecurityScheme {
scheme: "Bearer".to_owned(),
bearer_format: None,
},
},
scopes: scopes.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn bearer_format(mut self, format: impl Into<String>) -> Self {
self.scheme.http_auth_security_scheme.bearer_format = Some(format.into());
self
}
#[cfg(feature = "a2a-server")]
pub(crate) fn apply(&self, card: &mut AgentCard) {
card.security_schemes
.insert(self.name.clone(), self.scheme.clone());
card.security_requirements.push(CardSecurityRequirement {
schemes: std::collections::BTreeMap::from([(
self.name.clone(),
SecurityScopeList {
list: self.scopes.clone(),
},
)]),
});
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCard {
pub name: String,
pub description: String,
pub version: String,
pub capabilities: CardCapabilities,
pub supported_interfaces: Vec<CardInterface>,
pub default_input_modes: Vec<String>,
pub default_output_modes: Vec<String>,
pub skills: Vec<CardSkill>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub security_schemes: std::collections::BTreeMap<String, CardSecurityScheme>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub security_requirements: Vec<CardSecurityRequirement>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub signatures: Vec<super::card_sig::CardSignature>,
pub manifest_digest: String,
}
impl AgentCard {
pub fn derive(
manifest: &Manifest,
url: impl Into<String>,
) -> Result<Self, crate::manifest::ManifestError> {
let description = manifest.spec.identity.as_ref().map_or_else(
|| format!("The {} agent.", manifest.metadata.name),
|identity| identity.role.clone(),
);
let skills = manifest
.spec
.capabilities
.provides
.iter()
.map(|capability| CardSkill {
id: capability.clone(),
name: capability.clone(),
description: description.clone(),
tags: Vec::new(),
})
.collect();
Ok(Self {
name: manifest.metadata.name.clone(),
description,
version: manifest.metadata.version.clone(),
capabilities: CardCapabilities::implemented(),
supported_interfaces: vec![CardInterface {
url: url.into(),
protocol_binding: BINDING.to_owned(),
protocol_version: crate::peers::PROTOCOL_VERSION.to_owned(),
tenant: None,
}],
default_input_modes: vec!["text/plain".to_owned(), "application/json".to_owned()],
default_output_modes: vec!["text/plain".to_owned(), "application/json".to_owned()],
skills,
security_schemes: std::collections::BTreeMap::new(),
security_requirements: Vec::new(),
signatures: Vec::new(),
manifest_digest: manifest.digest()?.to_hex(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExtendedAgentCard {
#[serde(flatten)]
pub public: AgentCard,
pub tools: Vec<ExtendedTool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub budget: Option<ExtendedBudget>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topology: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExtendedTool {
pub reference: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub mutates: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExtendedBudget {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_steps: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_effects: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u64>,
}
impl ExtendedAgentCard {
pub fn derive(
manifest: &Manifest,
url: impl Into<String>,
) -> Result<Self, crate::manifest::ManifestError> {
let tools = manifest
.spec
.tools
.iter()
.map(|g| ExtendedTool {
reference: g.reference.clone(),
description: g.description.clone(),
mutates: g.mutates,
})
.collect();
let budget = manifest.spec.budgets.as_ref().map(|b| ExtendedBudget {
max_steps: b.max_steps,
max_effects: b.max_effects,
max_tokens: b.max_tokens,
});
let topology = manifest
.spec
.topology
.as_ref()
.map(|t| format!("{:?}/{:?}", t.mode, t.role));
Ok(Self {
public: AgentCard::derive(manifest, url)?,
tools,
budget,
topology,
})
}
}