1#![forbid(unsafe_code)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::path::PathBuf;
6use std::pin::Pin;
7use std::process::ExitStatus;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10use std::time::Duration;
11
12use futures_core::Stream;
13
14mod agent_kind;
15mod runtime_support;
16
17use crate::mcp::{
18 normalize_mcp_add_request, normalize_mcp_get_request, normalize_mcp_remove_request,
19 AgentWrapperMcpAddRequest, AgentWrapperMcpCommandOutput, AgentWrapperMcpGetRequest,
20 AgentWrapperMcpListRequest, AgentWrapperMcpRemoveRequest, CAPABILITY_MCP_ADD_V1,
21 CAPABILITY_MCP_GET_V1, CAPABILITY_MCP_LIST_V1, CAPABILITY_MCP_REMOVE_V1,
22};
23use agent_kind::validate_agent_kind;
24
25#[cfg(any(
26 feature = "codex",
27 feature = "claude_code",
28 feature = "aider",
29 feature = "gemini_cli",
30 feature = "opencode"
31))]
32mod bounds;
33
34#[cfg(any(
35 feature = "codex",
36 feature = "claude_code",
37 feature = "aider",
38 feature = "gemini_cli",
39 feature = "opencode"
40))]
41mod run_handle_gate;
42
43#[cfg(any(
44 feature = "codex",
45 feature = "claude_code",
46 feature = "aider",
47 feature = "gemini_cli",
48 feature = "opencode"
49))]
50mod backend_harness;
51
52pub mod backends;
53pub mod mcp;
54pub use runtime_support::{list_runtime_support, resolve_runtime_support, RuntimeSupportRecord};
55
56const CAPABILITY_CONTROL_CANCEL_V1: &str = "agent_api.control.cancel.v1";
57#[allow(dead_code)]
58#[cfg(any(
59 feature = "codex",
60 feature = "claude_code",
61 feature = "aider",
62 feature = "gemini_cli",
63 feature = "opencode",
64 test
65))]
66pub(crate) const EXT_AGENT_API_CONFIG_MODEL_V1: &str = "agent_api.config.model.v1";
67
68#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
69pub struct AgentWrapperKind(String);
70
71impl AgentWrapperKind {
72 pub fn new(value: impl Into<String>) -> Result<Self, AgentWrapperError> {
76 let value = value.into();
77 validate_agent_kind(&value)?;
78 Ok(Self(value))
79 }
80
81 pub fn as_str(&self) -> &str {
83 &self.0
84 }
85}
86
87#[derive(Clone, Debug, Default, Eq, PartialEq)]
88pub struct AgentWrapperCapabilities {
89 pub ids: BTreeSet<String>,
91}
92
93impl AgentWrapperCapabilities {
94 pub fn contains(&self, capability_id: &str) -> bool {
95 self.ids.contains(capability_id)
96 }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum AgentWrapperEventKind {
101 TextOutput,
102 ToolCall,
103 ToolResult,
104 Status,
105 Error,
106 Unknown,
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct AgentWrapperEvent {
111 pub agent_kind: AgentWrapperKind,
112 pub kind: AgentWrapperEventKind,
113 pub channel: Option<String>,
114 pub text: Option<String>,
116 pub message: Option<String>,
118 pub data: Option<serde_json::Value>,
119}
120
121#[derive(Clone, Debug, Default)]
122pub struct AgentWrapperRunRequest {
123 pub prompt: String,
124 pub working_dir: Option<PathBuf>,
125 pub timeout: Option<Duration>,
126 pub env: BTreeMap<String, String>,
127 pub extensions: BTreeMap<String, serde_json::Value>,
129}
130
131pub type DynAgentWrapperEventStream = Pin<Box<dyn Stream<Item = AgentWrapperEvent> + Send>>;
132pub type DynAgentWrapperCompletion =
133 Pin<Box<dyn Future<Output = Result<AgentWrapperCompletion, AgentWrapperError>> + Send>>;
134
135pub struct AgentWrapperRunHandle {
136 pub events: DynAgentWrapperEventStream,
137 pub completion: DynAgentWrapperCompletion,
138}
139
140impl std::fmt::Debug for AgentWrapperRunHandle {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 f.debug_struct("AgentWrapperRunHandle")
143 .field("events", &"<stream>")
144 .field("completion", &"<future>")
145 .finish()
146 }
147}
148
149struct AgentWrapperCancelInner {
150 called: AtomicBool,
151 cancel: Box<dyn Fn() + Send + Sync + 'static>,
152}
153
154#[derive(Clone)]
155pub struct AgentWrapperCancelHandle {
156 inner: Arc<AgentWrapperCancelInner>,
157}
158
159impl std::fmt::Debug for AgentWrapperCancelHandle {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 f.debug_struct("AgentWrapperCancelHandle")
162 .finish_non_exhaustive()
163 }
164}
165
166impl AgentWrapperCancelHandle {
167 #[allow(dead_code)]
168 pub(crate) fn new(cancel: impl Fn() + Send + Sync + 'static) -> Self {
169 Self {
170 inner: Arc::new(AgentWrapperCancelInner {
171 called: AtomicBool::new(false),
172 cancel: Box::new(cancel),
173 }),
174 }
175 }
176
177 pub fn cancel(&self) {
187 if self.inner.called.swap(true, Ordering::SeqCst) {
188 return;
189 }
190
191 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
192 (self.inner.cancel)();
193 }));
194 }
195}
196
197#[derive(Debug)]
198pub struct AgentWrapperRunControl {
199 pub handle: AgentWrapperRunHandle,
200 pub cancel: AgentWrapperCancelHandle,
201}
202
203#[derive(Clone, Debug)]
204pub struct AgentWrapperCompletion {
205 pub status: ExitStatus,
206 pub final_text: Option<String>,
208 pub data: Option<serde_json::Value>,
213}
214
215#[derive(Clone, Debug)]
216pub struct AgentWrapperRunResult {
217 pub completion: AgentWrapperCompletion,
218}
219
220#[derive(Debug, thiserror::Error)]
221pub enum AgentWrapperError {
222 #[error("unknown backend: {agent_kind}")]
223 UnknownBackend { agent_kind: String },
224 #[error("unknown runtime family: {runtime_family}")]
225 UnknownRuntimeFamily { runtime_family: String },
226 #[error("unsupported target triple for {runtime_family}: {target_triple}")]
227 UnsupportedTargetTriple {
228 runtime_family: String,
229 target_triple: String,
230 },
231 #[error("missing validated runtime for {runtime_family}: {target_triple}")]
232 MissingValidatedRuntime {
233 runtime_family: String,
234 target_triple: String,
235 },
236 #[error("unsupported capability for {agent_kind}: {capability}")]
237 UnsupportedCapability {
238 agent_kind: String,
239 capability: String,
240 },
241 #[error("invalid agent kind: {message}")]
242 InvalidAgentKind { message: String },
243 #[error("invalid request: {message}")]
244 InvalidRequest { message: String },
245 #[error("backend error: {message}")]
246 Backend { message: String },
247}
248
249pub trait AgentWrapperBackend: Send + Sync {
250 fn kind(&self) -> AgentWrapperKind;
251 fn capabilities(&self) -> AgentWrapperCapabilities;
252
253 fn run(
257 &self,
258 request: AgentWrapperRunRequest,
259 ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunHandle, AgentWrapperError>> + Send + '_>>;
260
261 fn run_control(
267 &self,
268 _request: AgentWrapperRunRequest,
269 ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunControl, AgentWrapperError>> + Send + '_>>
270 {
271 let agent_kind = self.kind().as_str().to_string();
272 Box::pin(async move {
273 Err(AgentWrapperError::UnsupportedCapability {
274 agent_kind,
275 capability: CAPABILITY_CONTROL_CANCEL_V1.to_string(),
276 })
277 })
278 }
279
280 fn mcp_list(
286 &self,
287 _request: AgentWrapperMcpListRequest,
288 ) -> Pin<
289 Box<
290 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
291 + Send
292 + '_,
293 >,
294 > {
295 let agent_kind = self.kind().as_str().to_string();
296 Box::pin(async move {
297 Err(AgentWrapperError::UnsupportedCapability {
298 agent_kind,
299 capability: CAPABILITY_MCP_LIST_V1.to_string(),
300 })
301 })
302 }
303
304 fn mcp_get(
310 &self,
311 _request: AgentWrapperMcpGetRequest,
312 ) -> Pin<
313 Box<
314 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
315 + Send
316 + '_,
317 >,
318 > {
319 let agent_kind = self.kind().as_str().to_string();
320 Box::pin(async move {
321 Err(AgentWrapperError::UnsupportedCapability {
322 agent_kind,
323 capability: CAPABILITY_MCP_GET_V1.to_string(),
324 })
325 })
326 }
327
328 fn mcp_add(
334 &self,
335 _request: AgentWrapperMcpAddRequest,
336 ) -> Pin<
337 Box<
338 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
339 + Send
340 + '_,
341 >,
342 > {
343 let agent_kind = self.kind().as_str().to_string();
344 Box::pin(async move {
345 Err(AgentWrapperError::UnsupportedCapability {
346 agent_kind,
347 capability: CAPABILITY_MCP_ADD_V1.to_string(),
348 })
349 })
350 }
351
352 fn mcp_remove(
358 &self,
359 _request: AgentWrapperMcpRemoveRequest,
360 ) -> Pin<
361 Box<
362 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
363 + Send
364 + '_,
365 >,
366 > {
367 let agent_kind = self.kind().as_str().to_string();
368 Box::pin(async move {
369 Err(AgentWrapperError::UnsupportedCapability {
370 agent_kind,
371 capability: CAPABILITY_MCP_REMOVE_V1.to_string(),
372 })
373 })
374 }
375}
376
377#[derive(Clone, Default)]
378pub struct AgentWrapperGateway {
379 backends: BTreeMap<AgentWrapperKind, Arc<dyn AgentWrapperBackend>>,
380}
381
382impl AgentWrapperGateway {
383 pub fn new() -> Self {
384 Self::default()
385 }
386
387 pub fn register(
391 &mut self,
392 backend: Arc<dyn AgentWrapperBackend>,
393 ) -> Result<(), AgentWrapperError> {
394 let kind = backend.kind();
395 if self.backends.contains_key(&kind) {
396 return Err(AgentWrapperError::InvalidRequest {
397 message: format!("backend already registered: {}", kind.as_str()),
398 });
399 }
400 self.backends.insert(kind, backend);
401 Ok(())
402 }
403
404 pub fn backend(&self, agent_kind: &AgentWrapperKind) -> Option<Arc<dyn AgentWrapperBackend>> {
406 self.backends.get(agent_kind).cloned()
407 }
408
409 pub fn run(
413 &self,
414 agent_kind: &AgentWrapperKind,
415 request: AgentWrapperRunRequest,
416 ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunHandle, AgentWrapperError>> + Send + '_>>
417 {
418 let backend = self.backends.get(agent_kind).cloned();
419 let agent_kind = agent_kind.as_str().to_string();
420 Box::pin(async move {
421 let backend = backend.ok_or(AgentWrapperError::UnknownBackend { agent_kind })?;
422 backend.run(request).await
423 })
424 }
425
426 pub fn run_control(
440 &self,
441 agent_kind: &AgentWrapperKind,
442 request: AgentWrapperRunRequest,
443 ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunControl, AgentWrapperError>> + Send + '_>>
444 {
445 let backend = self.backends.get(agent_kind).cloned();
446 let agent_kind = agent_kind.as_str().to_string();
447 Box::pin(async move {
448 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
449 agent_kind: agent_kind.clone(),
450 })?;
451
452 if !backend
453 .capabilities()
454 .contains(CAPABILITY_CONTROL_CANCEL_V1)
455 {
456 return Err(AgentWrapperError::UnsupportedCapability {
457 agent_kind,
458 capability: CAPABILITY_CONTROL_CANCEL_V1.to_string(),
459 });
460 }
461
462 backend.run_control(request).await
463 })
464 }
465
466 pub fn mcp_list(
477 &self,
478 agent_kind: &AgentWrapperKind,
479 request: AgentWrapperMcpListRequest,
480 ) -> Pin<
481 Box<
482 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
483 + Send
484 + '_,
485 >,
486 > {
487 let backend = self.backends.get(agent_kind).cloned();
488 let agent_kind = agent_kind.as_str().to_string();
489 Box::pin(async move {
490 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
491 agent_kind: agent_kind.clone(),
492 })?;
493
494 if !backend.capabilities().contains(CAPABILITY_MCP_LIST_V1) {
495 return Err(AgentWrapperError::UnsupportedCapability {
496 agent_kind,
497 capability: CAPABILITY_MCP_LIST_V1.to_string(),
498 });
499 }
500
501 backend.mcp_list(request).await
502 })
503 }
504
505 pub fn mcp_get(
516 &self,
517 agent_kind: &AgentWrapperKind,
518 request: AgentWrapperMcpGetRequest,
519 ) -> Pin<
520 Box<
521 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
522 + Send
523 + '_,
524 >,
525 > {
526 let backend = self.backends.get(agent_kind).cloned();
527 let agent_kind = agent_kind.as_str().to_string();
528 Box::pin(async move {
529 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
530 agent_kind: agent_kind.clone(),
531 })?;
532
533 if !backend.capabilities().contains(CAPABILITY_MCP_GET_V1) {
534 return Err(AgentWrapperError::UnsupportedCapability {
535 agent_kind,
536 capability: CAPABILITY_MCP_GET_V1.to_string(),
537 });
538 }
539
540 let request = normalize_mcp_get_request(request)?;
541 backend.mcp_get(request).await
542 })
543 }
544
545 pub fn mcp_add(
556 &self,
557 agent_kind: &AgentWrapperKind,
558 request: AgentWrapperMcpAddRequest,
559 ) -> Pin<
560 Box<
561 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
562 + Send
563 + '_,
564 >,
565 > {
566 let backend = self.backends.get(agent_kind).cloned();
567 let agent_kind = agent_kind.as_str().to_string();
568 Box::pin(async move {
569 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
570 agent_kind: agent_kind.clone(),
571 })?;
572
573 if !backend.capabilities().contains(CAPABILITY_MCP_ADD_V1) {
574 return Err(AgentWrapperError::UnsupportedCapability {
575 agent_kind,
576 capability: CAPABILITY_MCP_ADD_V1.to_string(),
577 });
578 }
579
580 let request = normalize_mcp_add_request(request)?;
581 backend.mcp_add(request).await
582 })
583 }
584
585 pub fn mcp_remove(
596 &self,
597 agent_kind: &AgentWrapperKind,
598 request: AgentWrapperMcpRemoveRequest,
599 ) -> Pin<
600 Box<
601 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
602 + Send
603 + '_,
604 >,
605 > {
606 let backend = self.backends.get(agent_kind).cloned();
607 let agent_kind = agent_kind.as_str().to_string();
608 Box::pin(async move {
609 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
610 agent_kind: agent_kind.clone(),
611 })?;
612
613 if !backend.capabilities().contains(CAPABILITY_MCP_REMOVE_V1) {
614 return Err(AgentWrapperError::UnsupportedCapability {
615 agent_kind,
616 capability: CAPABILITY_MCP_REMOVE_V1.to_string(),
617 });
618 }
619
620 let request = normalize_mcp_remove_request(request)?;
621 backend.mcp_remove(request).await
622 })
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use std::collections::BTreeSet;
629 use std::future::Future;
630 use std::pin::Pin;
631 use std::sync::atomic::{AtomicUsize, Ordering};
632 use std::sync::{Arc, Mutex};
633 use std::task::{Context, Poll, Wake, Waker};
634
635 use super::*;
636 use crate::mcp::{
637 AgentWrapperMcpAddRequest, AgentWrapperMcpAddTransport, AgentWrapperMcpCommandContext,
638 AgentWrapperMcpGetRequest, CAPABILITY_MCP_ADD_V1, CAPABILITY_MCP_GET_V1,
639 };
640
641 #[test]
642 fn cancel_handle_is_idempotent() {
643 let calls = Arc::new(AtomicUsize::new(0));
644 let calls_for_cancel = Arc::clone(&calls);
645 let cancel = AgentWrapperCancelHandle::new(move || {
646 calls_for_cancel.fetch_add(1, Ordering::SeqCst);
647 });
648
649 cancel.cancel();
650 cancel.cancel();
651 cancel.cancel();
652
653 assert_eq!(calls.load(Ordering::SeqCst), 1);
654 }
655
656 #[test]
657 fn mcp_add_returns_unknown_backend_before_validation() {
658 let gateway = AgentWrapperGateway::new();
659 let agent_kind = AgentWrapperKind::new("codex").expect("valid kind");
660 let secret = "SECRET_BACKEND_VALUE";
661
662 let err = block_on_immediate(gateway.mcp_add(
663 &agent_kind,
664 AgentWrapperMcpAddRequest {
665 name: format!(" {secret} "),
666 transport: AgentWrapperMcpAddTransport::Url {
667 url: format!("relative/{secret}"),
668 bearer_token_env_var: None,
669 },
670 context: AgentWrapperMcpCommandContext::default(),
671 },
672 ))
673 .expect_err("unknown backend should fail first");
674
675 match err {
676 AgentWrapperError::UnknownBackend { agent_kind } => {
677 assert_eq!(agent_kind, "codex");
678 }
679 other => panic!("expected UnknownBackend, got {other:?}"),
680 }
681 }
682
683 #[test]
684 fn mcp_add_returns_unsupported_capability_before_validation_and_without_hook_call() {
685 let backend = Arc::new(TestBackend::new(BTreeSet::new()));
686 let mut gateway = AgentWrapperGateway::new();
687 gateway.register(backend.clone()).expect("register backend");
688 let agent_kind = backend.kind();
689 let secret = "SECRET_UNSUPPORTED_VALUE";
690
691 let err = block_on_immediate(gateway.mcp_add(
692 &agent_kind,
693 AgentWrapperMcpAddRequest {
694 name: format!(" {secret} "),
695 transport: AgentWrapperMcpAddTransport::Url {
696 url: format!("relative/{secret}"),
697 bearer_token_env_var: None,
698 },
699 context: AgentWrapperMcpCommandContext::default(),
700 },
701 ))
702 .expect_err("unsupported capability should fail before validation");
703
704 match err {
705 AgentWrapperError::UnsupportedCapability {
706 agent_kind,
707 capability,
708 } => {
709 assert_eq!(agent_kind, "test_backend");
710 assert_eq!(capability, CAPABILITY_MCP_ADD_V1);
711 }
712 other => panic!("expected UnsupportedCapability, got {other:?}"),
713 }
714
715 assert_eq!(backend.mcp_add_calls.load(Ordering::SeqCst), 0);
716 }
717
718 #[test]
719 fn mcp_add_returns_invalid_request_before_hook_when_capability_is_advertised() {
720 let backend = Arc::new(TestBackend::new(BTreeSet::from([
721 CAPABILITY_MCP_ADD_V1.to_string()
722 ])));
723 let mut gateway = AgentWrapperGateway::new();
724 gateway.register(backend.clone()).expect("register backend");
725 let agent_kind = backend.kind();
726 let secret = "SECRET_INVALID_VALUE";
727
728 let err = block_on_immediate(gateway.mcp_add(
729 &agent_kind,
730 AgentWrapperMcpAddRequest {
731 name: " example ".to_string(),
732 transport: AgentWrapperMcpAddTransport::Url {
733 url: format!("https:{secret}.example.com"),
734 bearer_token_env_var: None,
735 },
736 context: AgentWrapperMcpCommandContext::default(),
737 },
738 ))
739 .expect_err("invalid request should fail before hook");
740
741 match err {
742 AgentWrapperError::InvalidRequest { message } => {
743 assert_eq!(message, "mcp add url must be an absolute http or https URL");
744 assert!(!message.contains(secret));
745 }
746 other => panic!("expected InvalidRequest, got {other:?}"),
747 }
748
749 assert_eq!(backend.mcp_add_calls.load(Ordering::SeqCst), 0);
750 }
751
752 #[test]
753 fn mcp_get_passes_normalized_request_to_hook() {
754 let backend = Arc::new(TestBackend::new(BTreeSet::from([
755 CAPABILITY_MCP_GET_V1.to_string()
756 ])));
757 let mut gateway = AgentWrapperGateway::new();
758 gateway.register(backend.clone()).expect("register backend");
759 let agent_kind = backend.kind();
760
761 let output = block_on_immediate(gateway.mcp_get(
762 &agent_kind,
763 AgentWrapperMcpGetRequest {
764 name: " normalized-name ".to_string(),
765 context: AgentWrapperMcpCommandContext::default(),
766 },
767 ))
768 .expect("normalized request should reach hook");
769
770 assert_eq!(output.stdout, "ok");
771 assert_eq!(backend.mcp_get_calls.load(Ordering::SeqCst), 1);
772 assert_eq!(
773 backend
774 .last_get_name
775 .lock()
776 .expect("get name mutex")
777 .as_deref(),
778 Some("normalized-name")
779 );
780 }
781
782 struct NoopWake;
783
784 impl Wake for NoopWake {
785 fn wake(self: Arc<Self>) {}
786 }
787
788 fn block_on_immediate<F>(future: F) -> F::Output
789 where
790 F: Future,
791 {
792 let waker = Waker::from(Arc::new(NoopWake));
793 let mut future = Box::pin(future);
794 let mut context = Context::from_waker(&waker);
795
796 loop {
797 match future.as_mut().poll(&mut context) {
798 Poll::Ready(output) => return output,
799 Poll::Pending => std::thread::yield_now(),
800 }
801 }
802 }
803
804 fn success_exit_status() -> std::process::ExitStatus {
805 #[cfg(unix)]
806 {
807 use std::os::unix::process::ExitStatusExt;
808 std::process::ExitStatus::from_raw(0)
809 }
810 #[cfg(windows)]
811 {
812 use std::os::windows::process::ExitStatusExt;
813 std::process::ExitStatus::from_raw(0)
814 }
815 }
816
817 fn success_output() -> AgentWrapperMcpCommandOutput {
818 AgentWrapperMcpCommandOutput {
819 status: success_exit_status(),
820 stdout: "ok".to_string(),
821 stderr: String::new(),
822 stdout_truncated: false,
823 stderr_truncated: false,
824 }
825 }
826
827 struct TestBackend {
828 capabilities: AgentWrapperCapabilities,
829 mcp_add_calls: AtomicUsize,
830 mcp_get_calls: AtomicUsize,
831 last_get_name: Mutex<Option<String>>,
832 }
833
834 impl TestBackend {
835 fn new(capabilities: BTreeSet<String>) -> Self {
836 Self {
837 capabilities: AgentWrapperCapabilities { ids: capabilities },
838 mcp_add_calls: AtomicUsize::new(0),
839 mcp_get_calls: AtomicUsize::new(0),
840 last_get_name: Mutex::new(None),
841 }
842 }
843 }
844
845 impl AgentWrapperBackend for TestBackend {
846 fn kind(&self) -> AgentWrapperKind {
847 AgentWrapperKind("test_backend".to_string())
848 }
849
850 fn capabilities(&self) -> AgentWrapperCapabilities {
851 self.capabilities.clone()
852 }
853
854 fn run(
855 &self,
856 _request: AgentWrapperRunRequest,
857 ) -> Pin<
858 Box<dyn Future<Output = Result<AgentWrapperRunHandle, AgentWrapperError>> + Send + '_>,
859 > {
860 Box::pin(async {
861 Err(AgentWrapperError::Backend {
862 message: "run not used in tests".to_string(),
863 })
864 })
865 }
866
867 fn mcp_get(
868 &self,
869 request: AgentWrapperMcpGetRequest,
870 ) -> Pin<
871 Box<
872 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
873 + Send
874 + '_,
875 >,
876 > {
877 self.mcp_get_calls.fetch_add(1, Ordering::SeqCst);
878 *self.last_get_name.lock().expect("last get name mutex") = Some(request.name);
879 Box::pin(async move { Ok(success_output()) })
880 }
881
882 fn mcp_add(
883 &self,
884 _request: AgentWrapperMcpAddRequest,
885 ) -> Pin<
886 Box<
887 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
888 + Send
889 + '_,
890 >,
891 > {
892 self.mcp_add_calls.fetch_add(1, Ordering::SeqCst);
893 Box::pin(async move { Ok(success_output()) })
894 }
895 }
896}