anyclaw-sdk-tool 0.3.10

Tool SDK for anyclaw — build MCP-compatible tool servers
Documentation
use std::future::Future;
use std::path::Path;
use std::pin::Pin;

use anyclaw_sdk_types::WorkspaceScope;

use crate::error::ToolSdkError;

/// Trait for accessing an agent's workspace from within an MCP tool.
///
/// Implementations provide scoped access to the agent container's filesystem.
/// The sandbox enforces the granted [`WorkspaceScope`] — operations beyond the
/// scope will return an error.
///
/// # Phase 0 Note
///
/// This trait defines the target interface. Implementations will be added in
/// Phase 5 when `DockerWorkspaceExec` wraps Bollard calls behind this trait.
pub trait WorkspaceExec: Send + Sync + 'static {
    /// The scope granted to this workspace handle.
    fn scope(&self) -> WorkspaceScope;

    /// Read a file from the agent's workspace.
    ///
    /// Returns the file contents as bytes. Requires at least [`WorkspaceScope::ReadOnly`].
    fn read_file(&self, path: &Path) -> impl Future<Output = Result<Vec<u8>, ToolSdkError>> + Send;

    /// Write a file to the agent's workspace.
    ///
    /// Creates or overwrites the file at the given path. Requires at least
    /// [`WorkspaceScope::ReadWrite`].
    fn write_file(
        &self,
        path: &Path,
        contents: &[u8],
    ) -> impl Future<Output = Result<(), ToolSdkError>> + Send;

    /// Execute a command in the agent's workspace.
    ///
    /// Returns the command's stdout as bytes. Requires [`WorkspaceScope::Exec`].
    fn exec(
        &self,
        command: &str,
        args: &[&str],
    ) -> impl Future<Output = Result<ExecOutput, ToolSdkError>> + Send;
}

/// Object-safe alias for [`WorkspaceExec`].
///
/// Use `Arc<dyn DynWorkspaceExec>` when the concrete type is not known at compile time
/// (e.g. injecting workspace handles into tool contexts).
pub trait DynWorkspaceExec: Send + Sync + 'static {
    /// The scope granted to this workspace handle.
    fn scope(&self) -> WorkspaceScope;

    /// Read a file (boxed future for object safety).
    fn read_file<'a>(
        &'a self,
        path: &'a Path,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, ToolSdkError>> + Send + 'a>>;

    /// Write a file (boxed future for object safety).
    fn write_file<'a>(
        &'a self,
        path: &'a Path,
        contents: &'a [u8],
    ) -> Pin<Box<dyn Future<Output = Result<(), ToolSdkError>> + Send + 'a>>;

    /// Execute a command (boxed future for object safety).
    fn exec<'a>(
        &'a self,
        command: &'a str,
        args: &'a [&'a str],
    ) -> Pin<Box<dyn Future<Output = Result<ExecOutput, ToolSdkError>> + Send + 'a>>;
}

impl<T: WorkspaceExec> DynWorkspaceExec for T {
    fn scope(&self) -> WorkspaceScope {
        WorkspaceExec::scope(self)
    }

    fn read_file<'a>(
        &'a self,
        path: &'a Path,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, ToolSdkError>> + Send + 'a>> {
        Box::pin(WorkspaceExec::read_file(self, path))
    }

    fn write_file<'a>(
        &'a self,
        path: &'a Path,
        contents: &'a [u8],
    ) -> Pin<Box<dyn Future<Output = Result<(), ToolSdkError>> + Send + 'a>> {
        Box::pin(WorkspaceExec::write_file(self, path, contents))
    }

    fn exec<'a>(
        &'a self,
        command: &'a str,
        args: &'a [&'a str],
    ) -> Pin<Box<dyn Future<Output = Result<ExecOutput, ToolSdkError>> + Send + 'a>> {
        Box::pin(WorkspaceExec::exec(self, command, args))
    }
}

/// Output from a command execution in the workspace.
#[derive(Debug, Clone)]
pub struct ExecOutput {
    /// Process exit code (0 = success).
    pub exit_code: i32,
    /// Standard output bytes.
    pub stdout: Vec<u8>,
    /// Standard error bytes.
    pub stderr: Vec<u8>,
}

impl ExecOutput {
    /// Returns `true` if the command exited successfully (code 0).
    pub fn success(&self) -> bool {
        self.exit_code == 0
    }
}

#[cfg(test)]
mod tests {
    use rstest::rstest;

    use super::*;

    #[rstest]
    fn when_exec_output_exit_zero_then_success() {
        let output = ExecOutput {
            exit_code: 0,
            stdout: vec![],
            stderr: vec![],
        };
        assert!(output.success());
    }

    #[rstest]
    fn when_exec_output_exit_nonzero_then_not_success() {
        let output = ExecOutput {
            exit_code: 1,
            stdout: vec![],
            stderr: b"error".to_vec(),
        };
        assert!(!output.success());
    }

    #[rstest]
    fn when_dyn_workspace_exec_used_as_trait_object_then_compiles() {
        fn _accepts_dyn(_w: &dyn DynWorkspaceExec) {}
    }
}