use serde::{Deserialize, Serialize};
use crate::extensions::{AgentCardSignature, AgentExtension};
use crate::security::{NamedSecuritySchemes, SecurityRequirement};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentInterface {
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, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub streaming: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub push_notifications: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extended_agent_card: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Vec<AgentExtension>>,
}
impl AgentCapabilities {
#[must_use]
pub const fn none() -> Self {
Self {
streaming: None,
push_notifications: None,
extended_agent_card: None,
extensions: None,
}
}
#[must_use]
pub const fn with_streaming(mut self, streaming: bool) -> Self {
self.streaming = Some(streaming);
self
}
#[must_use]
pub const fn with_push_notifications(mut self, push: bool) -> Self {
self.push_notifications = Some(push);
self
}
#[must_use]
pub const fn with_extended_agent_card(mut self, extended: bool) -> Self {
self.extended_agent_card = Some(extended);
self
}
}
impl Default for AgentCapabilities {
fn default() -> Self {
Self::none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentProvider {
pub organization: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentSkill {
pub id: String,
pub name: String,
pub description: String,
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_modes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_modes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security_requirements: Option<Vec<SecurityRequirement>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCard {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
pub description: String,
pub version: String,
pub supported_interfaces: Vec<AgentInterface>,
pub default_input_modes: Vec<String>,
pub default_output_modes: Vec<String>,
pub skills: Vec<AgentSkill>,
pub capabilities: AgentCapabilities,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<AgentProvider>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security_schemes: Option<NamedSecuritySchemes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security_requirements: Option<Vec<SecurityRequirement>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signatures: Option<Vec<AgentCardSignature>>,
}
impl AgentCard {
pub const fn validate(&self) -> Result<(), &'static str> {
if self.name.is_empty() {
return Err("agent card name must not be empty");
}
if self.supported_interfaces.is_empty() {
return Err("agent card must have at least one supported interface");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_card() -> AgentCard {
AgentCard {
url: None,
name: "Test Agent".into(),
description: "A test agent".into(),
version: "1.0.0".into(),
supported_interfaces: vec![AgentInterface {
url: "https://agent.example.com/rpc".into(),
protocol_binding: "JSONRPC".into(),
protocol_version: "1.0.0".into(),
tenant: None,
}],
default_input_modes: vec!["text/plain".into()],
default_output_modes: vec!["text/plain".into()],
skills: vec![AgentSkill {
id: "echo".into(),
name: "Echo".into(),
description: "Echoes input".into(),
tags: vec!["echo".into()],
examples: None,
input_modes: None,
output_modes: None,
security_requirements: None,
}],
capabilities: AgentCapabilities::none(),
provider: None,
icon_url: None,
documentation_url: None,
security_schemes: None,
security_requirements: None,
signatures: None,
}
}
#[test]
fn agent_card_roundtrip() {
let card = minimal_card();
let json = serde_json::to_string(&card).expect("serialize");
assert!(json.contains("\"supportedInterfaces\""));
assert!(json.contains("\"protocolBinding\":\"JSONRPC\""));
assert!(json.contains("\"protocolVersion\":\"1.0.0\""));
assert!(
!json.contains("\"preferredTransport\""),
"v1.0 removed this field"
);
let back: AgentCard = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.name, "Test Agent");
assert_eq!(back.supported_interfaces[0].protocol_binding, "JSONRPC");
}
#[test]
fn optional_fields_omitted() {
let card = minimal_card();
let json = serde_json::to_string(&card).expect("serialize");
assert!(!json.contains("\"provider\""), "provider should be absent");
assert!(!json.contains("\"iconUrl\""), "iconUrl should be absent");
assert!(
!json.contains("\"securitySchemes\""),
"securitySchemes should be absent"
);
}
#[test]
fn extended_agent_card_in_capabilities() {
let mut card = minimal_card();
card.capabilities.extended_agent_card = Some(true);
let json = serde_json::to_string(&card).expect("serialize");
assert!(json.contains("\"extendedAgentCard\":true"));
}
#[test]
fn wire_format_security_requirements_field_name() {
use crate::security::{SecurityRequirement, StringList};
use std::collections::HashMap;
let mut card = minimal_card();
card.security_requirements = Some(vec![SecurityRequirement {
schemes: HashMap::from([("bearer".into(), StringList { list: vec![] })]),
}]);
let json = serde_json::to_string(&card).unwrap();
assert!(
json.contains("\"securityRequirements\""),
"field must be securityRequirements: {json}"
);
assert!(
!json.contains("\"security\":"),
"must not have bare 'security' field: {json}"
);
}
#[test]
fn wire_format_skill_security_requirements() {
use crate::security::{SecurityRequirement, StringList};
use std::collections::HashMap;
let skill = AgentSkill {
id: "s1".into(),
name: "Skill".into(),
description: "A skill".into(),
tags: vec![],
examples: None,
input_modes: None,
output_modes: None,
security_requirements: Some(vec![SecurityRequirement {
schemes: HashMap::from([(
"oauth2".into(),
StringList {
list: vec!["read".into()],
},
)]),
}]),
};
let json = serde_json::to_string(&skill).unwrap();
assert!(
json.contains("\"securityRequirements\""),
"skill must use securityRequirements: {json}"
);
}
#[test]
fn wire_format_capabilities_no_state_transition_history() {
let card = minimal_card();
let json = serde_json::to_string(&card).unwrap();
assert!(
!json.contains("stateTransitionHistory"),
"stateTransitionHistory must not appear: {json}"
);
}
#[test]
fn capabilities_none_all_fields_unset() {
let caps = AgentCapabilities::none();
assert!(caps.streaming.is_none());
assert!(caps.push_notifications.is_none());
assert!(caps.extended_agent_card.is_none());
assert!(caps.extensions.is_none());
}
#[test]
fn capabilities_default_equals_none() {
let def = AgentCapabilities::default();
let none = AgentCapabilities::none();
assert_eq!(def.streaming, none.streaming);
assert_eq!(def.push_notifications, none.push_notifications);
assert_eq!(def.extended_agent_card, none.extended_agent_card);
}
#[test]
fn capabilities_with_streaming_sets_field() {
let caps = AgentCapabilities::none().with_streaming(true);
assert_eq!(caps.streaming, Some(true));
assert!(caps.push_notifications.is_none());
assert!(caps.extended_agent_card.is_none());
let caps = AgentCapabilities::none().with_streaming(false);
assert_eq!(caps.streaming, Some(false));
}
#[test]
fn capabilities_with_push_notifications_sets_field() {
let caps = AgentCapabilities::none().with_push_notifications(true);
assert_eq!(caps.push_notifications, Some(true));
assert!(caps.streaming.is_none());
assert!(caps.extended_agent_card.is_none());
let caps = AgentCapabilities::none().with_push_notifications(false);
assert_eq!(caps.push_notifications, Some(false));
}
#[test]
fn capabilities_with_extended_agent_card_sets_field() {
let caps = AgentCapabilities::none().with_extended_agent_card(true);
assert_eq!(caps.extended_agent_card, Some(true));
assert!(caps.streaming.is_none());
assert!(caps.push_notifications.is_none());
let caps = AgentCapabilities::none().with_extended_agent_card(false);
assert_eq!(caps.extended_agent_card, Some(false));
}
#[test]
fn capabilities_builder_chaining() {
let caps = AgentCapabilities::none()
.with_streaming(true)
.with_push_notifications(false)
.with_extended_agent_card(true);
assert_eq!(caps.streaming, Some(true));
assert_eq!(caps.push_notifications, Some(false));
assert_eq!(caps.extended_agent_card, Some(true));
}
#[test]
fn validate_minimal_card_ok() {
let card = minimal_card();
assert!(card.validate().is_ok());
}
#[test]
fn validate_empty_name_returns_error() {
let mut card = minimal_card();
card.name = String::new();
let err = card.validate().unwrap_err();
assert!(err.contains("name"), "error should mention name: {err}");
}
#[test]
fn validate_empty_supported_interfaces_returns_error() {
let mut card = minimal_card();
card.supported_interfaces = vec![];
let err = card.validate().unwrap_err();
assert!(
err.contains("supported interface"),
"error should mention supported interface: {err}"
);
}
}