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