Skip to main content

anyclaw_sdk_tool/
workspace_exec.rs

1use std::future::Future;
2use std::path::Path;
3use std::pin::Pin;
4
5use anyclaw_sdk_types::WorkspaceScope;
6
7use crate::error::ToolSdkError;
8
9/// Trait for accessing an agent's workspace from within an MCP tool.
10///
11/// Implementations provide scoped access to the agent container's filesystem.
12/// The sandbox enforces the granted [`WorkspaceScope`] — operations beyond the
13/// scope will return an error.
14///
15/// # Phase 0 Note
16///
17/// This trait defines the target interface. Implementations will be added in
18/// Phase 5 when `DockerWorkspaceExec` wraps Bollard calls behind this trait.
19pub trait WorkspaceExec: Send + Sync + 'static {
20    /// The scope granted to this workspace handle.
21    fn scope(&self) -> WorkspaceScope;
22
23    /// Read a file from the agent's workspace.
24    ///
25    /// Returns the file contents as bytes. Requires at least [`WorkspaceScope::ReadOnly`].
26    fn read_file(&self, path: &Path) -> impl Future<Output = Result<Vec<u8>, ToolSdkError>> + Send;
27
28    /// Write a file to the agent's workspace.
29    ///
30    /// Creates or overwrites the file at the given path. Requires at least
31    /// [`WorkspaceScope::ReadWrite`].
32    fn write_file(
33        &self,
34        path: &Path,
35        contents: &[u8],
36    ) -> impl Future<Output = Result<(), ToolSdkError>> + Send;
37
38    /// Execute a command in the agent's workspace.
39    ///
40    /// Returns the command's stdout as bytes. Requires [`WorkspaceScope::Exec`].
41    fn exec(
42        &self,
43        command: &str,
44        args: &[&str],
45    ) -> impl Future<Output = Result<ExecOutput, ToolSdkError>> + Send;
46}
47
48/// Object-safe alias for [`WorkspaceExec`].
49///
50/// Use `Arc<dyn DynWorkspaceExec>` when the concrete type is not known at compile time
51/// (e.g. injecting workspace handles into tool contexts).
52pub trait DynWorkspaceExec: Send + Sync + 'static {
53    /// The scope granted to this workspace handle.
54    fn scope(&self) -> WorkspaceScope;
55
56    /// Read a file (boxed future for object safety).
57    fn read_file<'a>(
58        &'a self,
59        path: &'a Path,
60    ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, ToolSdkError>> + Send + 'a>>;
61
62    /// Write a file (boxed future for object safety).
63    fn write_file<'a>(
64        &'a self,
65        path: &'a Path,
66        contents: &'a [u8],
67    ) -> Pin<Box<dyn Future<Output = Result<(), ToolSdkError>> + Send + 'a>>;
68
69    /// Execute a command (boxed future for object safety).
70    fn exec<'a>(
71        &'a self,
72        command: &'a str,
73        args: &'a [&'a str],
74    ) -> Pin<Box<dyn Future<Output = Result<ExecOutput, ToolSdkError>> + Send + 'a>>;
75}
76
77impl<T: WorkspaceExec> DynWorkspaceExec for T {
78    fn scope(&self) -> WorkspaceScope {
79        WorkspaceExec::scope(self)
80    }
81
82    fn read_file<'a>(
83        &'a self,
84        path: &'a Path,
85    ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, ToolSdkError>> + Send + 'a>> {
86        Box::pin(WorkspaceExec::read_file(self, path))
87    }
88
89    fn write_file<'a>(
90        &'a self,
91        path: &'a Path,
92        contents: &'a [u8],
93    ) -> Pin<Box<dyn Future<Output = Result<(), ToolSdkError>> + Send + 'a>> {
94        Box::pin(WorkspaceExec::write_file(self, path, contents))
95    }
96
97    fn exec<'a>(
98        &'a self,
99        command: &'a str,
100        args: &'a [&'a str],
101    ) -> Pin<Box<dyn Future<Output = Result<ExecOutput, ToolSdkError>> + Send + 'a>> {
102        Box::pin(WorkspaceExec::exec(self, command, args))
103    }
104}
105
106/// Output from a command execution in the workspace.
107#[derive(Debug, Clone)]
108pub struct ExecOutput {
109    /// Process exit code (0 = success).
110    pub exit_code: i32,
111    /// Standard output bytes.
112    pub stdout: Vec<u8>,
113    /// Standard error bytes.
114    pub stderr: Vec<u8>,
115}
116
117impl ExecOutput {
118    /// Returns `true` if the command exited successfully (code 0).
119    pub fn success(&self) -> bool {
120        self.exit_code == 0
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use rstest::rstest;
127
128    use super::*;
129
130    #[rstest]
131    fn when_exec_output_exit_zero_then_success() {
132        let output = ExecOutput {
133            exit_code: 0,
134            stdout: vec![],
135            stderr: vec![],
136        };
137        assert!(output.success());
138    }
139
140    #[rstest]
141    fn when_exec_output_exit_nonzero_then_not_success() {
142        let output = ExecOutput {
143            exit_code: 1,
144            stdout: vec![],
145            stderr: b"error".to_vec(),
146        };
147        assert!(!output.success());
148    }
149
150    #[rstest]
151    fn when_dyn_workspace_exec_used_as_trait_object_then_compiles() {
152        fn _accepts_dyn(_w: &dyn DynWorkspaceExec) {}
153    }
154}