1use std::sync::Arc;
13use std::time::Duration;
14
15use a2a_protocol_types::agent_card::AgentCard;
16use a2a_protocol_types::extensions::AgentExtension;
17use a2a_protocol_types::idempotency;
18
19use crate::error::ServerResult;
20use crate::executor::AgentExecutor;
21use crate::handler::{HandlerLimits, RequestHandler};
22use crate::interceptor::{ServerInterceptor, ServerInterceptorChain};
23use crate::metrics::{Metrics, NoopMetrics};
24use crate::push::{InMemoryPushConfigStore, PushConfigStore, PushSender};
25use crate::store::{InMemoryTaskStore, TaskStore, TaskStoreConfig};
26use crate::streaming::EventQueueManager;
27use crate::tenant_config::PerTenantConfig;
28use crate::tenant_resolver::TenantResolver;
29
30pub const DEFAULT_MAX_CONCURRENT_STREAMS: usize = 1024;
39
40pub const DEFAULT_EXECUTOR_TIMEOUT: Duration = Duration::from_secs(3600);
72
73pub struct RequestHandlerBuilder {
97 executor: Arc<dyn AgentExecutor>,
98 task_store: Option<Arc<dyn TaskStore>>,
99 task_store_config: TaskStoreConfig,
100 push_config_store: Option<Arc<dyn PushConfigStore>>,
101 push_sender: Option<Arc<dyn PushSender>>,
102 interceptors: ServerInterceptorChain,
103 agent_card: Option<AgentCard>,
104 executor_timeout: Option<Duration>,
105 event_queue_capacity: Option<usize>,
106 max_event_size: Option<usize>,
107 max_concurrent_streams: Option<usize>,
108 metrics: Arc<dyn Metrics>,
109 handler_limits: HandlerLimits,
110 tenant_resolver: Option<Arc<dyn TenantResolver>>,
111 tenant_config: Option<PerTenantConfig>,
112 require_resolved_tenant: bool,
113 inbound_trace_policy: crate::handler::InboundTracePolicy,
114 allow_unauthenticated_extended_card: bool,
115 allow_undeclared_input_modes: bool,
116}
117
118impl RequestHandlerBuilder {
119 #[must_use]
123 pub fn new(executor: impl AgentExecutor) -> Self {
124 Self {
125 executor: Arc::new(executor),
126 task_store: None,
127 task_store_config: TaskStoreConfig::default(),
128 push_config_store: None,
129 push_sender: None,
130 interceptors: ServerInterceptorChain::new(),
131 agent_card: None,
132 executor_timeout: Some(DEFAULT_EXECUTOR_TIMEOUT),
133 event_queue_capacity: None,
134 max_event_size: None,
135 max_concurrent_streams: None,
136 metrics: Arc::new(NoopMetrics),
137 handler_limits: HandlerLimits::default(),
138 tenant_resolver: None,
139 tenant_config: None,
140 require_resolved_tenant: false,
141 inbound_trace_policy: crate::handler::InboundTracePolicy::Continue,
142 allow_unauthenticated_extended_card: false,
143 allow_undeclared_input_modes: false,
144 }
145 }
146
147 #[must_use]
149 pub fn with_task_store(mut self, store: impl TaskStore + 'static) -> Self {
150 self.task_store = Some(Arc::new(store));
151 self
152 }
153
154 #[must_use]
159 pub fn with_task_store_arc(mut self, store: Arc<dyn TaskStore>) -> Self {
160 self.task_store = Some(store);
161 self
162 }
163
164 #[must_use]
172 pub fn with_task_store_config(mut self, config: TaskStoreConfig) -> Self {
173 debug_assert!(
174 self.task_store.is_none(),
175 "with_task_store_config() called after with_task_store(); \
176 the config will be ignored because a custom store was already set"
177 );
178 self.task_store_config = config;
179 self
180 }
181
182 #[must_use]
184 pub fn with_push_config_store(mut self, store: impl PushConfigStore + 'static) -> Self {
185 self.push_config_store = Some(Arc::new(store));
186 self
187 }
188
189 #[must_use]
191 pub fn with_push_sender(mut self, sender: impl PushSender + 'static) -> Self {
192 self.push_sender = Some(Arc::new(sender));
193 self
194 }
195
196 #[must_use]
198 pub fn with_interceptor(mut self, interceptor: impl ServerInterceptor + 'static) -> Self {
199 self.interceptors.push(Arc::new(interceptor));
200 self
201 }
202
203 #[must_use]
213 pub const fn with_executor_timeout(mut self, timeout: Duration) -> Self {
214 self.executor_timeout = Some(timeout);
215 self
216 }
217
218 #[must_use]
229 pub const fn without_executor_timeout(mut self) -> Self {
230 self.executor_timeout = None;
231 self
232 }
233
234 #[must_use]
236 pub fn with_agent_card(mut self, card: AgentCard) -> Self {
237 self.agent_card = Some(card);
238 self
239 }
240
241 #[must_use]
249 pub const fn with_event_queue_capacity(mut self, capacity: usize) -> Self {
250 self.event_queue_capacity = Some(capacity);
251 self
252 }
253
254 #[must_use]
259 pub const fn with_max_event_size(mut self, max_event_size: usize) -> Self {
260 self.max_event_size = Some(max_event_size);
261 self
262 }
263
264 #[must_use]
271 pub const fn with_max_concurrent_streams(mut self, max: usize) -> Self {
272 self.max_concurrent_streams = Some(max);
273 self
274 }
275
276 #[must_use]
280 pub const fn with_handler_limits(mut self, limits: HandlerLimits) -> Self {
281 self.handler_limits = limits;
282 self
283 }
284
285 #[must_use]
289 pub fn with_metrics(mut self, metrics: impl Metrics + 'static) -> Self {
290 self.metrics = Arc::new(metrics);
291 self
292 }
293
294 #[must_use]
310 pub fn with_tenant_resolver(mut self, resolver: impl TenantResolver) -> Self {
311 self.tenant_resolver = Some(Arc::new(resolver));
312 self
313 }
314
315 #[must_use]
327 pub const fn require_resolved_tenant(mut self) -> Self {
328 self.require_resolved_tenant = true;
329 self
330 }
331
332 #[must_use]
352 pub const fn with_inbound_trace_policy(
353 mut self,
354 policy: crate::handler::InboundTracePolicy,
355 ) -> Self {
356 self.inbound_trace_policy = policy;
357 self
358 }
359
360 #[must_use]
373 pub const fn allow_unauthenticated_extended_card(mut self) -> Self {
374 self.allow_unauthenticated_extended_card = true;
375 self
376 }
377
378 #[must_use]
388 pub const fn allow_undeclared_input_modes(mut self) -> Self {
389 self.allow_undeclared_input_modes = true;
390 self
391 }
392
393 #[must_use]
407 pub fn with_tenant_config(mut self, config: PerTenantConfig) -> Self {
408 self.tenant_config = Some(config);
409 self
410 }
411
412 #[allow(clippy::too_many_lines)]
420 pub fn build(self) -> ServerResult<RequestHandler> {
421 if let Some(ref card) = self.agent_card
423 && card.supported_interfaces.is_empty()
424 {
425 return Err(crate::error::ServerError::InvalidParams(
426 "agent card must have at least one supported interface".into(),
427 ));
428 }
429
430 if let Some(timeout) = self.executor_timeout
432 && timeout.is_zero()
433 {
434 return Err(crate::error::ServerError::InvalidParams(
435 "executor timeout must be greater than zero".into(),
436 ));
437 }
438
439 if self.handler_limits.max_id_length == 0 {
441 return Err(crate::error::ServerError::InvalidParams(
442 "max_id_length must be greater than zero".into(),
443 ));
444 }
445 if self.handler_limits.max_metadata_size == 0 {
446 return Err(crate::error::ServerError::InvalidParams(
447 "max_metadata_size must be greater than zero".into(),
448 ));
449 }
450 if self.handler_limits.push_delivery_timeout.is_zero() {
451 return Err(crate::error::ServerError::InvalidParams(
452 "push_delivery_timeout must be greater than zero".into(),
453 ));
454 }
455 let task_store = self
458 .task_store
459 .unwrap_or_else(|| Arc::new(InMemoryTaskStore::with_config(self.task_store_config)));
460
461 let mut agent_card = self.agent_card;
477 if let Some(card) = agent_card.as_mut() {
478 let mut wanted: Vec<AgentExtension> = Vec::new();
479 if task_store.supports_idempotency() {
480 wanted.push(AgentExtension {
481 uri: idempotency::IDEMPOTENCY_EXTENSION_URI.to_owned(),
482 description: Some(
483 "Client-supplied idempotency keys on message/send: a \
484 retried send returns the task the first one created \
485 instead of starting a second."
486 .to_owned(),
487 ),
488 required: Some(false),
489 params: None,
490 });
491 }
492 wanted.push(AgentExtension {
493 uri: a2a_protocol_types::failure::FAILURE_EXTENSION_URI.to_owned(),
494 description: Some(
495 "A failed task says why in a form a caller can match on, \
496 rather than only in prose."
497 .to_owned(),
498 ),
499 required: Some(false),
500 params: None,
501 });
502
503 let declared = card.capabilities.extensions.as_deref().unwrap_or(&[]);
504 let missing: Vec<AgentExtension> = wanted
505 .into_iter()
506 .filter(|w| !declared.iter().any(|have| have.uri == w.uri))
507 .collect();
508
509 if !missing.is_empty()
518 && card
519 .signatures
520 .as_ref()
521 .is_some_and(|sigs| !sigs.is_empty())
522 {
523 let uris: Vec<&str> = missing.iter().map(|e| e.uri.as_str()).collect();
524 return Err(crate::error::ServerError::InvalidParams(format!(
525 "the agent card carries a signature but does not declare {}. \
526 Advertising an extension edits `capabilities.extensions`, \
527 which is inside the signed bytes, so the card this server \
528 would serve no longer verifies against its own signature. \
529 Declare the extension on the card before signing it, or \
530 supply the card unsigned.",
531 uris.join(", ")
532 )));
533 }
534
535 if !missing.is_empty() {
536 card.capabilities
537 .extensions
538 .get_or_insert_with(Vec::new)
539 .extend(missing);
540 }
541 }
542
543 let (required_extensions, declared_extensions) = agent_card
546 .as_ref()
547 .and_then(|c| c.capabilities.extensions.as_ref())
548 .map(|exts| {
549 let declared: Vec<String> = exts.iter().map(|e| e.uri.clone()).collect();
550 let required: Vec<String> = exts
551 .iter()
552 .filter(|e| e.required == Some(true))
553 .map(|e| e.uri.clone())
554 .collect();
555 (required, declared)
556 })
557 .unwrap_or_default();
558
559 let accepted_input_modes = crate::handler::input_modes::accepted_input_modes(
560 agent_card.as_ref(),
561 self.allow_undeclared_input_modes,
562 );
563
564 Ok(RequestHandler {
565 executor: self.executor,
566 task_store,
567 push_config_store: self
568 .push_config_store
569 .unwrap_or_else(|| Arc::new(InMemoryPushConfigStore::new())),
570 push_sender: self.push_sender,
571 event_queue_manager: {
572 let mut mgr = self
573 .event_queue_capacity
574 .map_or_else(EventQueueManager::new, EventQueueManager::with_capacity);
575 if let Some(max_size) = self.max_event_size {
576 mgr = mgr.with_max_event_size(max_size);
577 }
578 mgr = mgr.with_max_concurrent_queues(
579 self.max_concurrent_streams
580 .unwrap_or(DEFAULT_MAX_CONCURRENT_STREAMS),
581 );
582 mgr = mgr.with_metrics(Arc::clone(&self.metrics));
583 mgr
584 },
585 interceptors: self.interceptors,
586 agent_card,
587 executor_timeout: self.executor_timeout,
588 metrics: self.metrics,
589 limits: self.handler_limits,
590 tenant_resolver: self.tenant_resolver,
591 require_resolved_tenant: self.require_resolved_tenant,
592 inbound_trace_policy: self.inbound_trace_policy,
593 allow_unauthenticated_extended_card: self.allow_unauthenticated_extended_card,
594 accepted_input_modes,
595 required_extensions,
596 declared_extensions,
597 tenant_config: self.tenant_config,
598 cancellation_tokens: Arc::new(tokio::sync::RwLock::new(
599 std::collections::HashMap::new(),
600 )),
601 context_locks: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
602 tenant_slots: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
603 in_flight: crate::handler::InFlight::default(),
604 })
605 }
606}
607
608impl std::fmt::Debug for RequestHandlerBuilder {
609 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
610 f.debug_struct("RequestHandlerBuilder")
611 .field("executor", &"<dyn AgentExecutor>")
612 .field("task_store", &self.task_store.is_some())
613 .field("task_store_config", &self.task_store_config)
614 .field("push_config_store", &self.push_config_store.is_some())
615 .field("push_sender", &self.push_sender.is_some())
616 .field("interceptors", &self.interceptors)
617 .field("agent_card", &self.agent_card.is_some())
618 .field("executor_timeout", &self.executor_timeout)
619 .field("event_queue_capacity", &self.event_queue_capacity)
620 .field("max_event_size", &self.max_event_size)
621 .field("max_concurrent_streams", &self.max_concurrent_streams)
622 .field("metrics", &"<dyn Metrics>")
623 .field("handler_limits", &self.handler_limits)
624 .field("tenant_resolver", &self.tenant_resolver.is_some())
625 .field("tenant_config", &self.tenant_config)
626 .field("require_resolved_tenant", &self.require_resolved_tenant)
627 .field("inbound_trace_policy", &self.inbound_trace_policy)
628 .field(
629 "allow_unauthenticated_extended_card",
630 &self.allow_unauthenticated_extended_card,
631 )
632 .field(
633 "allow_undeclared_input_modes",
634 &self.allow_undeclared_input_modes,
635 )
636 .finish()
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use crate::agent_executor;
644
645 struct TestExecutor;
646
647 agent_executor!(TestExecutor, |_ctx, _queue| async { Ok(()) });
648
649 #[test]
650 fn builder_defaults_build_ok() {
651 let handler = RequestHandlerBuilder::new(TestExecutor).build();
652 let h = handler.expect("default builder should succeed");
653 assert!(
654 h.tenant_resolver().is_none(),
655 "default builder should have no tenant resolver"
656 );
657 assert!(
658 h.tenant_config().is_none(),
659 "default builder should have no tenant config"
660 );
661 }
662
663 #[test]
664 fn builder_zero_executor_timeout_errors() {
665 let result = RequestHandlerBuilder::new(TestExecutor)
666 .with_executor_timeout(Duration::ZERO)
667 .build();
668 assert!(result.is_err());
669 }
670
671 #[test]
672 fn builder_empty_agent_card_interfaces_errors() {
673 use a2a_protocol_types::{AgentCapabilities, AgentCard};
674
675 let card = AgentCard {
676 url: None,
677 name: "empty".into(),
678 version: "1.0".into(),
679 description: "No interfaces".into(),
680 supported_interfaces: vec![],
681 provider: None,
682 icon_url: None,
683 documentation_url: None,
684 capabilities: AgentCapabilities::none(),
685 security_schemes: None,
686 security_requirements: None,
687 default_input_modes: vec![],
688 default_output_modes: vec![],
689 skills: vec![],
690 signatures: None,
691 };
692
693 let result = RequestHandlerBuilder::new(TestExecutor)
694 .with_agent_card(card)
695 .build();
696 assert!(result.is_err());
697 }
698
699 #[test]
700 fn builder_with_all_options() {
701 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
702
703 let card = AgentCard {
704 url: None,
705 name: "test".into(),
706 version: "1.0".into(),
707 description: "Test agent".into(),
708 supported_interfaces: vec![AgentInterface {
709 url: "http://localhost:8080".into(),
710 protocol_binding: "JSONRPC".into(),
711 protocol_version: "1.0.0".into(),
712 tenant: None,
713 }],
714 provider: None,
715 icon_url: None,
716 documentation_url: None,
717 capabilities: AgentCapabilities::none(),
718 security_schemes: None,
719 security_requirements: None,
720 default_input_modes: vec![],
721 default_output_modes: vec![],
722 skills: vec![],
723 signatures: None,
724 };
725
726 let result = RequestHandlerBuilder::new(TestExecutor)
727 .with_agent_card(card)
728 .with_executor_timeout(Duration::from_secs(30))
729 .with_event_queue_capacity(128)
730 .with_max_event_size(1024 * 1024)
731 .with_max_concurrent_streams(10)
732 .with_handler_limits(HandlerLimits::default().with_max_id_length(2048))
733 .build();
734 let h = result.expect("builder with all options should succeed");
735 assert!(h.tenant_resolver().is_none(), "no tenant resolver set");
736 }
737
738 #[test]
739 fn builder_with_tenant_resolver_and_config() {
740 use crate::tenant_config::{PerTenantConfig, TenantLimits};
741 use crate::tenant_resolver::HeaderTenantResolver;
742
743 let handler = RequestHandlerBuilder::new(TestExecutor)
744 .with_tenant_resolver(HeaderTenantResolver::default())
745 .with_tenant_config(
746 PerTenantConfig::builder()
747 .default_limits(TenantLimits::builder().rate_limit_rps(100).build())
748 .with_override(
749 "premium",
750 TenantLimits::builder().rate_limit_rps(1000).build(),
751 )
752 .build(),
753 )
754 .build();
755 let handler = handler.expect("builder with tenant resolver and config should succeed");
756 assert!(handler.tenant_resolver().is_some());
757 assert!(handler.tenant_config().is_some());
758 assert_eq!(
759 handler
760 .tenant_config()
761 .unwrap()
762 .get("premium")
763 .rate_limit_rps,
764 Some(1000)
765 );
766 assert_eq!(
767 handler
768 .tenant_config()
769 .unwrap()
770 .get("unknown")
771 .rate_limit_rps,
772 Some(100)
773 );
774 }
775
776 #[test]
777 fn builder_without_tenant_fields() {
778 let handler = RequestHandlerBuilder::new(TestExecutor).build().unwrap();
779 assert!(handler.tenant_resolver().is_none());
780 assert!(handler.tenant_config().is_none());
781 }
782
783 #[test]
787 fn builder_default_caps_concurrent_streams() {
788 let handler = RequestHandlerBuilder::new(TestExecutor).build().unwrap();
789 let debug = format!("{:?}", handler.event_queue_manager);
790 assert!(
791 debug.contains(&format!(
792 "max_concurrent_queues: Some({DEFAULT_MAX_CONCURRENT_STREAMS})"
793 )),
794 "default builder should cap concurrent streams at \
795 {DEFAULT_MAX_CONCURRENT_STREAMS}, got: {debug}"
796 );
797 }
798
799 #[test]
801 fn builder_max_concurrent_streams_override_wins() {
802 let handler = RequestHandlerBuilder::new(TestExecutor)
803 .with_max_concurrent_streams(7)
804 .build()
805 .unwrap();
806 let debug = format!("{:?}", handler.event_queue_manager);
807 assert!(
808 debug.contains("max_concurrent_queues: Some(7)"),
809 "explicit cap should override the default, got: {debug}"
810 );
811 }
812
813 #[test]
814 fn builder_debug_does_not_panic() {
815 let builder = RequestHandlerBuilder::new(TestExecutor);
816 let debug = format!("{builder:?}");
817 assert!(debug.contains("RequestHandlerBuilder"));
818 }
819
820 #[test]
821 fn builder_with_push_config_store_builds_ok() {
822 use crate::push::InMemoryPushConfigStore;
823 let result = RequestHandlerBuilder::new(TestExecutor)
824 .with_push_config_store(InMemoryPushConfigStore::new())
825 .build();
826 let _h = result.expect("builder with push config store should succeed");
827 }
828
829 #[test]
830 fn builder_zero_max_id_length_errors() {
831 let result = RequestHandlerBuilder::new(TestExecutor)
832 .with_handler_limits(HandlerLimits::default().with_max_id_length(0))
833 .build();
834 assert!(result.is_err(), "zero max_id_length should be rejected");
835 }
836
837 #[test]
838 fn builder_zero_max_metadata_size_errors() {
839 let result = RequestHandlerBuilder::new(TestExecutor)
840 .with_handler_limits(HandlerLimits::default().with_max_metadata_size(0))
841 .build();
842 assert!(result.is_err(), "zero max_metadata_size should be rejected");
843 }
844
845 #[test]
846 fn builder_zero_push_delivery_timeout_errors() {
847 let result = RequestHandlerBuilder::new(TestExecutor)
848 .with_handler_limits(
849 HandlerLimits::default().with_push_delivery_timeout(Duration::ZERO),
850 )
851 .build();
852 assert!(
853 result.is_err(),
854 "zero push_delivery_timeout should be rejected"
855 );
856 }
857
858 #[test]
863 fn the_failure_extension_is_advertised_and_never_required() {
864 use a2a_protocol_types::failure::FAILURE_EXTENSION_URI;
865
866 let card = a2a_protocol_types::agent_card::AgentCard::new(
867 "t",
868 "0.0.0",
869 a2a_protocol_types::agent_card::AgentInterface::jsonrpc("http://127.0.0.1:1"),
870 );
871 let handler = RequestHandlerBuilder::new(TestExecutor)
872 .with_agent_card(card)
873 .build()
874 .expect("handler");
875
876 let entry = handler
877 .agent_card
878 .as_ref()
879 .expect("a card was configured")
880 .capabilities
881 .extensions
882 .as_ref()
883 .expect("extensions")
884 .iter()
885 .find(|e| e.uri == FAILURE_EXTENSION_URI)
886 .expect("the failure extension must be advertised");
887 assert_eq!(entry.required, Some(false));
888 assert!(
889 entry.description.is_some(),
890 "a bare URI tells a reader nothing"
891 );
892 }
893
894 #[test]
897 fn an_operators_own_failure_declaration_is_left_alone() {
898 use a2a_protocol_types::extensions::AgentExtension;
899 use a2a_protocol_types::failure::FAILURE_EXTENSION_URI;
900
901 let mut card = a2a_protocol_types::agent_card::AgentCard::new(
902 "t",
903 "0.0.0",
904 a2a_protocol_types::agent_card::AgentInterface::jsonrpc("http://127.0.0.1:1"),
905 );
906 let mut mine = AgentExtension::new(FAILURE_EXTENSION_URI);
907 mine.required = Some(true);
908 mine.description = Some("mine".to_owned());
909 card.capabilities.extensions = Some(vec![mine]);
910
911 let handler = RequestHandlerBuilder::new(TestExecutor)
912 .with_agent_card(card)
913 .build()
914 .expect("handler");
915
916 let entries: Vec<_> = handler
917 .agent_card
918 .as_ref()
919 .expect("card")
920 .capabilities
921 .extensions
922 .as_ref()
923 .expect("extensions")
924 .iter()
925 .filter(|e| e.uri == FAILURE_EXTENSION_URI)
926 .collect();
927 assert_eq!(entries.len(), 1, "the entry must not be duplicated");
928 assert_eq!(entries[0].required, Some(true));
929 assert_eq!(entries[0].description.as_deref(), Some("mine"));
930 }
931
932 fn unsigned_card() -> a2a_protocol_types::agent_card::AgentCard {
934 a2a_protocol_types::agent_card::AgentCard::new(
935 "t",
936 "0.0.0",
937 a2a_protocol_types::agent_card::AgentInterface::jsonrpc("http://127.0.0.1:1"),
938 )
939 }
940
941 fn a_signature() -> a2a_protocol_types::extensions::AgentCardSignature {
942 a2a_protocol_types::extensions::AgentCardSignature {
943 protected: "eyJhbGciOiJFUzI1NiJ9".to_owned(),
944 signature: "c2ln".to_owned(),
945 header: None,
946 }
947 }
948
949 #[test]
955 fn a_signed_card_missing_an_advertisement_is_refused_not_edited() {
956 let mut card = unsigned_card();
957 card.signatures = Some(vec![a_signature()]);
958
959 let err = RequestHandlerBuilder::new(TestExecutor)
960 .with_agent_card(card)
961 .build()
962 .expect_err("a signed card that would be edited must not build");
963
964 let msg = err.to_string();
965 assert!(
966 msg.contains(a2a_protocol_types::failure::FAILURE_EXTENSION_URI),
967 "the error must name the extension to declare: {msg}"
968 );
969 assert!(
970 msg.contains("signature"),
971 "the error must say why refusing is right: {msg}"
972 );
973 }
974
975 #[test]
979 fn a_signed_card_that_declares_everything_is_served_unchanged() {
980 use a2a_protocol_types::extensions::AgentExtension;
981
982 let mut card = unsigned_card();
983 card.capabilities.extensions = Some(vec![
984 AgentExtension::new(idempotency::IDEMPOTENCY_EXTENSION_URI),
985 AgentExtension::new(a2a_protocol_types::failure::FAILURE_EXTENSION_URI),
986 ]);
987 card.signatures = Some(vec![a_signature()]);
988
989 let signed = serde_json::to_value(&card).expect("serialize");
990
991 let handler = RequestHandlerBuilder::new(TestExecutor)
992 .with_agent_card(card)
993 .build()
994 .expect("a fully-declared signed card must build");
995
996 let served =
997 serde_json::to_value(handler.agent_card.as_ref().expect("card")).expect("serialize");
998
999 assert_eq!(
1000 signed, served,
1001 "the served card must be what was signed, field for field"
1002 );
1003 }
1004
1005 #[test]
1008 fn an_empty_signature_list_does_not_count_as_signed() {
1009 let mut card = unsigned_card();
1010 card.signatures = Some(Vec::new());
1011
1012 let handler = RequestHandlerBuilder::new(TestExecutor)
1013 .with_agent_card(card)
1014 .build()
1015 .expect("an unsigned card still gets its advertisements");
1016
1017 assert!(
1018 handler
1019 .agent_card
1020 .as_ref()
1021 .expect("card")
1022 .capabilities
1023 .extensions
1024 .as_ref()
1025 .is_some_and(|e| e
1026 .iter()
1027 .any(|x| x.uri == a2a_protocol_types::failure::FAILURE_EXTENSION_URI)),
1028 "the failure extension must still be advertised"
1029 );
1030 }
1031}