aph_core/a2a_extension.rs
1//! A2A Agent Card extension descriptor for APH.
2//!
3//! When an agent advertises APH support on its AgentCard, it adds the
4//! extension declaration produced by `aph_a2a_extension()`. Verifiers can
5//! discover APH support by scanning `agent_card.extensions` for the
6//! `APH_EXTENSION_URI`.
7//!
8//! The `AgentExtension` shape is vendored here from the A2A Agent Card
9//! model: an opaque capability declaration (uri / description / required)
10//! that peers who do not understand it safely ignore.
11
12/// An extension capability declared on an Agent Card.
13#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14pub struct AgentExtension {
15 uri: String,
16 description: String,
17 required: bool,
18}
19
20impl AgentExtension {
21 /// Declares an extension capability with the given URI, description, and
22 /// whether peers must understand it to interoperate.
23 pub fn new(uri: impl Into<String>, description: impl Into<String>, required: bool) -> Self {
24 Self {
25 uri: uri.into(),
26 description: description.into(),
27 required,
28 }
29 }
30
31 /// Opaque extension identifier, compared byte-for-byte by peers.
32 pub fn uri(&self) -> &str {
33 &self.uri
34 }
35 /// Human-readable summary shown when inspecting an Agent Card.
36 pub fn description(&self) -> &str {
37 &self.description
38 }
39 /// Whether a peer must understand this extension to interoperate.
40 pub fn required(&self) -> bool {
41 self.required
42 }
43}
44
45/// URI for the APH notarization extension on the AgentCard.
46pub const APH_EXTENSION_URI: &str = "aph://extensions/notarization/v1";
47
48/// Creates the APH notarization extension declaration.
49pub fn aph_a2a_extension() -> crate::a2a_extension::AgentExtension {
50 crate::a2a_extension::AgentExtension::new(
51 APH_EXTENSION_URI,
52 "APH (Agent per Human) outbound communication notarization",
53 false,
54 )
55}
56
57#[cfg(test)]
58mod tests {
59 #[test]
60 fn aph_uri_pinned() {
61 // The A2A extension spec requires this URI be declared as one exact
62 // constant and compared byte-for-byte, never assembled from parts. A
63 // single character of drift makes peers fail to recognize APH support.
64 std::assert_eq!(super::APH_EXTENSION_URI, "aph://extensions/notarization/v1");
65 }
66
67 #[test]
68 fn aph_extension_is_optional() {
69 // required:false is deliberate — advertising APH must not make peers
70 // that lack it unable to talk to us. Flipping this to true would break
71 // interop with every non-APH A2A agent.
72 let ext = super::aph_a2a_extension();
73 std::assert_eq!(ext.uri(), super::APH_EXTENSION_URI);
74 std::assert!(!ext.required());
75 }
76
77 #[test]
78 fn description_is_non_empty() {
79 // The description is what a human sees when inspecting an AgentCard's
80 // capabilities; empty would leave the extension unexplained there.
81 let ext = super::aph_a2a_extension();
82 std::assert!(!ext.description().is_empty());
83 }
84}