Skip to content

Tools Module

Pydantic-AI compatible tools for sandboxed command execution in MCP servers and AI agents.

Overview

The tools module provides type-safe, reusable tools that integrate Shannot's sandbox capabilities with Model Context Protocol (MCP) servers and Pydantic-AI agents. These tools handle command execution, file reading, and output streaming with proper error handling.

Key Components:

  • SandboxDeps - Dependency container for sandbox configuration and executors
  • run_sandbox_command - Execute arbitrary commands in the sandbox
  • read_sandbox_file - Read file contents from sandboxed paths
  • stream_sandbox_output - Stream command output (for long-running processes)

Common Usage Patterns

With MCP Servers

from shannot.tools import SandboxDeps, run_sandbox_command
from shannot.executors import LocalExecutor

# Create dependencies
executor = LocalExecutor()
deps = SandboxDeps(profile_name="diagnostics", executor=executor)

# Use in MCP tool
result = await run_sandbox_command(
    deps,
    command=["df", "-h"],
    args=[]
)
print(result.stdout)

With Pydantic-AI

from pydantic_ai import Agent
from shannot.tools import SandboxDeps, run_sandbox_command

# Create agent with sandbox tools
agent = Agent(
    "openai:gpt-4",
    deps_type=SandboxDeps,
    system_prompt="You can execute read-only system commands."
)

# Register tool
@agent.tool
async def check_disk_usage(ctx) -> str:
    result = await run_sandbox_command(
        ctx.deps,
        command=["df", "-h"],
        args=[]
    )
    return result.stdout

# Run agent
deps = SandboxDeps(profile_name="diagnostics")
result = await agent.run("What's the disk usage?", deps=deps)

Remote Execution

from shannot.tools import SandboxDeps
from shannot.executors import SSHExecutor

# Configure SSH executor
executor = SSHExecutor(
    host="prod.example.com",
    username="readonly",
    key_filename="/path/to/key"
)

# Create deps with remote executor
deps = SandboxDeps(
    profile_name="diagnostics",
    executor=executor
)

# Commands now execute on remote host
result = await run_sandbox_command(deps, command=["uptime"], args=[])

File Reading

from shannot.tools import read_sandbox_file

# Read a configuration file
result = await read_sandbox_file(
    deps,
    filepath="/etc/os-release"
)
print(result.content)

# With line limits for large files
result = await read_sandbox_file(
    deps,
    filepath="/var/log/syslog",
    max_lines=100
)

Custom Profiles

from pathlib import Path

# Use custom profile path
deps = SandboxDeps(
    profile_path=Path("/etc/shannot/custom.json"),
    executor=executor
)

# Or specify profile name from standard locations
deps = SandboxDeps(
    profile_name="minimal",  # Loads ~/.config/shannot/minimal.json
    executor=executor
)

Error Handling

The tools convert SandboxError exceptions to failed ProcessResult objects for compatibility with MCP and AI frameworks:

result = await run_sandbox_command(deps, command=["forbidden"], args=[])

if result.returncode != 0:
    print(f"Command failed: {result.stderr}")
else:
    print(f"Success: {result.stdout}")

API Reference

tools

Pydantic-AI tools for sandbox operations.

This module provides type-safe, reusable tools for interacting with the Shannot sandbox. These tools can be used standalone, in MCP servers, or with Pydantic-AI agents.

Classes

SandboxDeps

Dependencies for sandbox tools.

Supports both legacy mode (with bubblewrap_path) and new executor mode.

Examples: Legacy mode (backward compatible): >>> deps = SandboxDeps(profile_name="minimal")

With LocalExecutor:
    >>> from shannot.executors import LocalExecutor
    >>> executor = LocalExecutor()
    >>> deps = SandboxDeps(profile_name="minimal", executor=executor)

With SSHExecutor (for remote execution):
    >>> from shannot.executors import SSHExecutor
    >>> executor = SSHExecutor(host="prod.example.com")
    >>> deps = SandboxDeps(profile_name="minimal", executor=executor)
Source code in shannot/tools.py
class SandboxDeps:
    """Dependencies for sandbox tools.

    Supports both legacy mode (with bubblewrap_path) and new executor mode.

    Examples:
        Legacy mode (backward compatible):
            >>> deps = SandboxDeps(profile_name="minimal")

        With LocalExecutor:
            >>> from shannot.executors import LocalExecutor
            >>> executor = LocalExecutor()
            >>> deps = SandboxDeps(profile_name="minimal", executor=executor)

        With SSHExecutor (for remote execution):
            >>> from shannot.executors import SSHExecutor
            >>> executor = SSHExecutor(host="prod.example.com")
            >>> deps = SandboxDeps(profile_name="minimal", executor=executor)
    """

    def __init__(
        self,
        profile_name: str = "readonly",
        profile_path: Path | None = None,
        bwrap_path: Path | None = None,
        executor: SandboxExecutor | None = None,
    ):
        """Initialize sandbox dependencies.

        Args:
            profile_name: Name of profile to load from ~/.config/shannot/
            profile_path: Explicit path to profile (overrides profile_name)
            bwrap_path: Path to bubblewrap executable (legacy mode, optional if executor provided)
            executor: Optional executor instance (LocalExecutor or SSHExecutor)
                     If provided, bwrap_path is not required.

        Raises:
            ValueError: If neither bwrap_path nor executor is provided
        """

        # Load profile
        if profile_path:
            self.profile: SandboxProfile = load_profile_from_path(profile_path)
        else:
            # Try user config first
            user_profile = Path.home() / ".config" / "shannot" / f"{profile_name}.json"
            if user_profile.exists():
                self.profile = load_profile_from_path(user_profile)
            else:
                # Fall back to bundled profiles
                bundled_profile = Path(__file__).parent.parent / "profiles" / f"{profile_name}.json"
                self.profile = load_profile_from_path(bundled_profile)

        # Store executor for later use
        self.executor: SandboxExecutor | None = executor

        # Create manager
        if executor is not None:
            # New mode: use executor
            self.manager: SandboxManager = SandboxManager(self.profile, executor=executor)
        else:
            # Legacy mode: use bwrap_path
            if bwrap_path is None:
                bwrap_path = Path("/usr/bin/bwrap")
            self.manager = SandboxManager(self.profile, bwrap_path)

    async def cleanup(self):
        """Cleanup executor resources (e.g., SSH connections).

        Should be called when done using the dependencies, especially
        when using SSHExecutor to ensure connections are closed.

        Example:
            >>> deps = SandboxDeps(profile_name="minimal", executor=ssh_executor)
            >>> try:
            ...     result = await run_command(deps, CommandInput(command=["ls", "/"]))
            ... finally:
            ...     await deps.cleanup()
        """
        if self.executor is not None:
            await self.executor.cleanup()
Functions
cleanup() async

Cleanup executor resources (e.g., SSH connections).

Should be called when done using the dependencies, especially when using SSHExecutor to ensure connections are closed.

Example: >>> deps = SandboxDeps(profile_name="minimal", executor=ssh_executor) >>> try: ... result = await run_command(deps, CommandInput(command=["ls", "/"])) ... finally: ... await deps.cleanup()

Source code in shannot/tools.py
async def cleanup(self):
    """Cleanup executor resources (e.g., SSH connections).

    Should be called when done using the dependencies, especially
    when using SSHExecutor to ensure connections are closed.

    Example:
        >>> deps = SandboxDeps(profile_name="minimal", executor=ssh_executor)
        >>> try:
        ...     result = await run_command(deps, CommandInput(command=["ls", "/"]))
        ... finally:
        ...     await deps.cleanup()
    """
    if self.executor is not None:
        await self.executor.cleanup()

CommandInput

Bases: BaseModel

Input for running a command in the sandbox.

Source code in shannot/tools.py
class CommandInput(BaseModel):
    """Input for running a command in the sandbox."""

    command: list[str] = Field(
        description="Command and arguments to execute (e.g., ['ls', '-l', '/'])"
    )

CommandOutput

Bases: BaseModel

Output from sandbox command execution.

Source code in shannot/tools.py
class CommandOutput(BaseModel):
    """Output from sandbox command execution."""

    stdout: str = Field(description="Standard output from the command")
    stderr: str = Field(description="Standard error from the command")
    returncode: int = Field(description="Exit code (0 = success)")
    duration: float = Field(description="Execution time in seconds")
    succeeded: bool = Field(description="Whether the command succeeded")

FileReadInput

Bases: BaseModel

Input for reading a file.

Source code in shannot/tools.py
class FileReadInput(BaseModel):
    """Input for reading a file."""

    path: str = Field(description="Absolute path to the file to read")

DirectoryListInput

Bases: BaseModel

Input for listing a directory.

Source code in shannot/tools.py
class DirectoryListInput(BaseModel):
    """Input for listing a directory."""

    path: str = Field(description="Absolute path to the directory to list")
    long_format: bool = Field(default=False, description="Show detailed information (ls -l)")
    show_hidden: bool = Field(default=False, description="Show hidden files (ls -a)")

Functions

run_command(deps, input) async

Execute a command in the read-only sandbox.

The sandbox provides: - Read-only access to system files - Network isolation - Ephemeral /tmp (changes lost after command exits) - Command allowlisting (only approved commands run)

Use this for: - Inspecting files: cat, head, tail, grep - Listing directories: ls, find - Checking system status: df, free, ps

Args: deps: Sandbox dependencies (profile, manager) input: Command to execute

Returns: CommandOutput with stdout, stderr, returncode, duration, and success status

Source code in shannot/tools.py
async def run_command(deps: SandboxDeps, input: CommandInput) -> CommandOutput:
    """Execute a command in the read-only sandbox.

    The sandbox provides:
    - Read-only access to system files
    - Network isolation
    - Ephemeral /tmp (changes lost after command exits)
    - Command allowlisting (only approved commands run)

    Use this for:
    - Inspecting files: cat, head, tail, grep
    - Listing directories: ls, find
    - Checking system status: df, free, ps

    Args:
        deps: Sandbox dependencies (profile, manager)
        input: Command to execute

    Returns:
        CommandOutput with stdout, stderr, returncode, duration, and success status
    """
    result = await _run_manager_command(deps, input.command)

    return CommandOutput(
        stdout=result.stdout,
        stderr=result.stderr,
        returncode=result.returncode,
        duration=result.duration,
        succeeded=result.succeeded(),
    )

read_file(deps, input) async

Read the contents of a file from the system.

Args: deps: Sandbox dependencies input: Path to file

Returns: File contents as string, or error message if failed

Source code in shannot/tools.py
async def read_file(deps: SandboxDeps, input: FileReadInput) -> str:
    """Read the contents of a file from the system.

    Args:
        deps: Sandbox dependencies
        input: Path to file

    Returns:
        File contents as string, or error message if failed
    """
    result = await _run_manager_command(deps, ["cat", input.path])
    if result.succeeded():
        return result.stdout
    else:
        return f"Error reading file: {result.stderr}"

list_directory(deps, input) async

List contents of a directory.

Args: deps: Sandbox dependencies input: Directory path and options

Returns: Directory listing as string, or error message if failed

Source code in shannot/tools.py
async def list_directory(deps: SandboxDeps, input: DirectoryListInput) -> str:
    """List contents of a directory.

    Args:
        deps: Sandbox dependencies
        input: Directory path and options

    Returns:
        Directory listing as string, or error message if failed
    """
    cmd = ["ls"]
    if input.long_format:
        cmd.append("-l")
    if input.show_hidden:
        cmd.append("-a")
    cmd.append(input.path)

    result = await _run_manager_command(deps, cmd)
    return result.stdout if result.succeeded() else result.stderr

check_disk_usage(deps) async

Get disk usage information for all mounted filesystems.

Returns: Human-readable disk usage output (df -h), or error message if failed

Source code in shannot/tools.py
async def check_disk_usage(deps: SandboxDeps) -> str:
    """Get disk usage information for all mounted filesystems.

    Returns:
        Human-readable disk usage output (df -h), or error message if failed
    """
    result = await _run_manager_command(deps, ["df", "-h"])
    return result.stdout if result.succeeded() else result.stderr

check_memory(deps) async

Get memory usage information.

Returns: Human-readable memory info (free -h), or error message if failed

Source code in shannot/tools.py
async def check_memory(deps: SandboxDeps) -> str:
    """Get memory usage information.

    Returns:
        Human-readable memory info (free -h), or error message if failed
    """
    result = await _run_manager_command(deps, ["free", "-h"])
    return result.stdout if result.succeeded() else result.stderr

search_files(deps, pattern=Field(description='Filename pattern to search for')) async

Find files matching a pattern.

Args: deps: Sandbox dependencies pattern: Pattern to search for (e.g., "*.log")

Returns: List of matching file paths, or error message if failed

Source code in shannot/tools.py
async def search_files(
    deps: SandboxDeps, pattern: str = Field(description="Filename pattern to search for")
) -> str:
    """Find files matching a pattern.

    Args:
        deps: Sandbox dependencies
        pattern: Pattern to search for (e.g., "*.log")

    Returns:
        List of matching file paths, or error message if failed
    """
    result = await _run_manager_command(deps, ["find", "/", "-name", pattern])
    return result.stdout if result.succeeded() else result.stderr

grep_content(deps, pattern=Field(description='Text pattern to search for'), path=Field(description='File or directory to search in'), recursive=Field(default=False, description='Search recursively in directories')) async

Search for text pattern in files.

Args: deps: Sandbox dependencies pattern: Text pattern to search for path: File or directory to search recursive: Whether to search recursively

Returns: Matching lines, or error message if failed

Source code in shannot/tools.py
async def grep_content(
    deps: SandboxDeps,
    pattern: str = Field(description="Text pattern to search for"),
    path: str = Field(description="File or directory to search in"),
    recursive: bool = Field(default=False, description="Search recursively in directories"),
) -> str:
    """Search for text pattern in files.

    Args:
        deps: Sandbox dependencies
        pattern: Text pattern to search for
        path: File or directory to search
        recursive: Whether to search recursively

    Returns:
        Matching lines, or error message if failed
    """
    cmd = ["grep"]
    if recursive:
        cmd.append("-r")
    cmd.extend([pattern, path])

    result = await _run_manager_command(deps, cmd)
    return result.stdout if result.succeeded() else result.stderr