Metadata-Version: 2.4
Name: codecapsules-sandbox
Version: 0.1.0
Summary: Ephemeral isolated Linux sandboxes via REST API — create, exec, upload, and delete Firecracker microVM environments for AI agents and code execution
Author-email: Code Capsules <hello@codecapsules.io>
License: MIT
Project-URL: Homepage, https://www.codecapsules.io/sandbox/
Project-URL: Documentation, https://docs.codecapsules.io/sandbox
Project-URL: Repository, https://github.com/codecapsules-io/codecapsules-sandbox-py
Project-URL: Issues, https://github.com/codecapsules-io/codecapsules-sandbox-py/issues
Keywords: sandbox,code-execution,code-interpreter,ai-agent,ai-agents,microvm,vm,isolation,security,ephemeral,cloud-sandbox,sdk,firecracker,linux,docker-sandbox,python-sandbox,llm,claude,openai,mcp
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Security
Classifier: Topic :: Internet
Classifier: Topic :: System :: Emulators
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.25.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Dynamic: license-file

<div align="center">
  <a href="https://www.codecapsules.io/sandbox/?utm_campaign=sandbox_sdk&utm_content=readme">
    <img src="https://www.codecapsules.io/logo/Code%20Capsules%20Logo%20-%20yellow-black.svg" alt="Code Capsules Logo" width="300"/>
  </a>
</div>
<br/>

# codecapsules-sandbox

Ephemeral isolated Linux environments via REST API. Create a sandbox, run code, delete it — in three lines.

Powered by [Firecracker microVMs](https://firecracker-microvm.github.io/). Built for AI agents, code execution APIs, and any workload that needs strong isolation without managing infrastructure.

```bash
pip install codecapsules-sandbox
```

---

## Quick Start

```python
from codecapsules_sandbox import Sandbox

# Create → exec → auto-delete
with Sandbox.create(flavor="python-3.12") as sb:
    result = sb.exec("python --version")
    print(result.stdout)    # "Python 3.12.3\n"
    print(result.exit_code) # 0
```

Set your API key:

```bash
export CODECAPSULES_API_KEY=your_api_key
```

Or pass it directly:

```python
sb = Sandbox.create(api_key="your_api_key")
```

---

## Install

```bash
pip install codecapsules-sandbox
# or
uv add codecapsules-sandbox
# or
poetry add codecapsules-sandbox
```

Requires Python ≥ 3.9.

---

## Usage

### Create a sandbox

```python
from codecapsules_sandbox import Sandbox

# Default: Python 3.12, 512MB RAM, 1 vCPU, 60-minute TTL
sb = Sandbox.create()

# With options
sb = Sandbox.create(
    flavor="node-20",    # python-3.12 | node-20 | browser | full
    memory=2048,         # MB
    cpu=2,               # vCPUs
    ttl=30,              # minutes
    metadata={"project": "my-agent"},
)

print(sb.id)     # "sb_01hx..."
print(sb.status) # "running"

# Always clean up
sb.delete()
```

### Context manager (recommended)

```python
with Sandbox.create(flavor="python-3.12") as sb:
    result = sb.exec("python --version")
    print(result.stdout)
# Sandbox is automatically deleted here — even if an exception was raised
```

### Execute commands

```python
with Sandbox.create() as sb:
    result = sb.exec("python --version")
    print(result.stdout)     # "Python 3.12.3\n"
    print(result.stderr)     # ""
    print(result.exit_code)  # 0
    print(result.duration_ms) # 82

    # With options
    result = sb.exec(
        "python /workspace/train.py",
        timeout=120,                          # seconds (default: 30)
        env={"EPOCHS": "10", "LR": "0.001"}, # environment variables
        stdin="input data",                   # stdin content
    )

    # Run multiple commands
    sb.exec("pip install numpy pandas")
    sb.exec('python -c "import numpy; print(numpy.__version__)"')
```

### Stream long-running commands

```python
with Sandbox.create() as sb:
    for chunk in sb.exec_stream("python train.py"):
        print(chunk, end="", flush=True)
```

### Upload and download files

```python
with Sandbox.create() as sb:
    # Upload
    sb.upload("/workspace/script.py", open("script.py", "rb").read())
    sb.upload("/workspace/config.json", '{"learning_rate": 0.001}')

    # Run
    result = sb.exec("python /workspace/script.py")

    # Download the output
    output = sb.download("/workspace/output.json")
    import json
    data = json.loads(output)
```

### Fetch an existing sandbox

```python
sb = Sandbox.get("sb_01hx...")
print(sb.status)  # 'running' | 'starting' | 'stopping' | 'stopped' | 'error'
```

### Sandbox logs

```python
with Sandbox.create() as sb:
    sb.exec("echo hello")
    entries = sb.logs()
    for e in entries:
        print(f"[{e.ts.isoformat()}] [{e.source}] {e.message}")

    # Since a timestamp
    from datetime import datetime, timedelta
    entries = sb.logs(since=datetime.utcnow() - timedelta(minutes=5), limit=50)
```

### Resource metrics

```python
with Sandbox.create() as sb:
    m = sb.metrics()
    print(f"CPU:  {m.cpu_percent}%")
    print(f"RAM:  {m.memory_used_mb}MB / {m.memory_limit_mb}MB")
    print(f"Disk: {m.disk_used_mb}MB / {m.disk_limit_mb}MB")
```

---

## Async Usage

```python
import asyncio
from codecapsules_sandbox import AsyncSandbox

async def main():
    # Async context manager
    async with AsyncSandbox.create(flavor="python-3.12") as sb:
        result = await sb.exec("python --version")
        print(result.stdout)

    # Upload and exec
    async with AsyncSandbox.create() as sb:
        await sb.upload("/workspace/script.py", open("script.py", "rb").read())
        result = await sb.exec("python /workspace/script.py")
        output = await sb.download("/workspace/output.json")

    # Stream output
    async with AsyncSandbox.create() as sb:
        async for chunk in sb.exec_stream("python train.py"):
            print(chunk, end="", flush=True)

asyncio.run(main())
```

---

## Environments (Flavors)

| Flavor | Pre-installed |
|---|---|
| `python-3.12` | Python 3.12, pip, numpy, pandas, requests, git |
| `node-20` | Node.js 20, npm, yarn, git |
| `browser` | Chromium, Playwright, xvfb, Python 3.12 |
| `full` | All of the above + jq, ffmpeg, ImageMagick |

---

## AI Agent Integration

### Anthropic Claude

```python
import anthropic
from codecapsules_sandbox import Sandbox

client = anthropic.Anthropic()

tools = [{
    "name": "run_python",
    "description": (
        "Execute Python code in an isolated sandbox and return the output. "
        "Use this for calculations, data processing, testing code, or any task "
        "requiring code execution."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "code": {"type": "string", "description": "Python code to execute"},
        },
        "required": ["code"],
    },
}]


def run_python(code: str) -> str:
    with Sandbox.create() as sb:
        r = sb.exec(f"python -c {repr(code)}")
        return r.stdout + (f"\nSTDERR: {r.stderr}" if r.stderr else "")


def process_tool_call(tool_name: str, tool_input: dict) -> str:
    if tool_name == "run_python":
        return run_python(tool_input["code"])
    raise ValueError(f"Unknown tool: {tool_name}")
```

### OpenAI

```python
import openai
from codecapsules_sandbox import Sandbox

client = openai.OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "execute_python",
        "description": "Run Python code in an isolated sandbox. Returns stdout and stderr.",
        "parameters": {
            "type": "object",
            "properties": {
                "code": {"type": "string"},
            },
            "required": ["code"],
        },
    },
}]


def execute_python(code: str) -> str:
    with Sandbox.create() as sb:
        r = sb.exec(f"python -c {repr(code)}")
        return f"stdout: {r.stdout}\nstderr: {r.stderr}\nexit_code: {r.exit_code}"
```

### LangChain

```python
from langchain.tools import tool
from codecapsules_sandbox import Sandbox


@tool
def execute_python(code: str) -> str:
    """Execute Python code in an isolated sandbox. Returns stdout, stderr, and exit code."""
    with Sandbox.create() as sb:
        result = sb.exec(f"python -c {repr(code)}")
        return f"stdout: {result.stdout}\nstderr: {result.stderr}\nexit_code: {result.exit_code}"
```

### smolagents (HuggingFace)

```python
from smolagents import tool
from codecapsules_sandbox import Sandbox


@tool
def python_interpreter(code: str) -> str:
    """Execute Python code in a secure isolated environment."""
    with Sandbox.create() as sb:
        result = sb.exec(f"python -c {repr(code)}")
        if result.exit_code != 0:
            return f"Error (exit {result.exit_code}):\n{result.stderr}"
        return result.stdout
```

---

## Error Handling

```python
from codecapsules_sandbox import (
    Sandbox,
    SandboxAuthError,
    SandboxNotFoundError,
    SandboxExecTimeoutError,
    SandboxQuotaError,
    SandboxRateLimitError,
    SandboxAPIError,
)

try:
    with Sandbox.create() as sb:
        result = sb.exec("python script.py", timeout=10)
except SandboxAuthError:
    print("Invalid API key — set CODECAPSULES_API_KEY")
except SandboxExecTimeoutError as e:
    print(f"Command timed out after {e.timeout_seconds}s")
except SandboxNotFoundError as e:
    print(f"Sandbox {e.sandbox_id} was deleted or expired")
except SandboxQuotaError:
    print("Too many concurrent sandboxes — delete one first")
except SandboxRateLimitError as e:
    print(f"Rate limited — retry after {e.retry_after_ms}ms")
except SandboxAPIError as e:
    print(f"API error {e.status}: {e}")
```

All exceptions inherit from `SandboxError`.

---

## Configuration

| Environment variable | Description | Default |
|---|---|---|
| `CODECAPSULES_API_KEY` | API key | — (required) |
| `CODECAPSULES_SANDBOX_URL` | Override API base URL | `https://sandbox.codecapsules.io/v1` |

Alternatively, pass keyword arguments to `Sandbox.create()`:

```python
sb = Sandbox.create(
    api_key="your_api_key",
    base_url="https://...",
    timeout=60.0,       # seconds (default: 30)
    max_retries=3,      # on transient errors (default: 2)
)
```

---

## API Reference

### `Sandbox.create(...) -> Sandbox`

Creates a new sandbox and returns when it is `running`.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `flavor` | str | `'python-3.12'` | Environment preset |
| `memory` | int | `512` | Memory in MB (max 8192) |
| `cpu` | int | `1` | vCPU count (max 4) |
| `ttl` | int | `60` | Lifetime in minutes (max 480) |
| `metadata` | dict | — | Arbitrary key-value metadata |

### `Sandbox.get(sandbox_id) -> Sandbox`

Fetches an existing sandbox by ID.

### `sb.exec(command, *, timeout, env, stdin) -> ExecResult`

Runs a shell command. Returns `ExecResult` with `stdout`, `stderr`, `exit_code`, and timing.

### `sb.exec_stream(command, *, timeout, env) -> Iterator[str]`

Runs a command and yields output chunks. Use in a `for` loop.

### `sb.upload(sandbox_path, content) -> FileInfo`

Uploads `bytes` or `str` content to a path inside the sandbox.

### `sb.download(sandbox_path) -> bytes`

Downloads a file from the sandbox.

### `sb.logs(*, since, limit) -> list[LogEntry]`

Returns system and exec log entries.

### `sb.metrics() -> SandboxMetrics`

Returns live CPU, memory, and disk usage.

### `sb.refresh() -> Sandbox`

Re-fetches sandbox info from the API (updates cached `status`).

### `sb.wait_until_running(*, timeout, interval) -> Sandbox`

Polls until status is `running`. Raises `SandboxNotReadyError` on timeout.

### `sb.stop() -> None`

Gracefully stops the sandbox.

### `sb.delete() -> None`

Deletes the sandbox and releases all resources.

---

## Types

```python
from codecapsules_sandbox import (
    SandboxInfo,     # dataclass: id, status, flavor, cpu, memory, created_at, expires_at
    ExecResult,      # dataclass: exec_id, command, stdout, stderr, exit_code, duration_ms
    FileInfo,        # dataclass: path, size_bytes, created_at
    LogEntry,        # dataclass: ts, source, message, exec_id
    SandboxMetrics,  # dataclass: id, status, cpu_percent, memory_used_mb, ...
    SandboxStatus,   # Literal["starting", "running", "stopping", "stopped", "error"]
    SandboxFlavor,   # Literal["python-3.12", "node-20", "browser", "full"]
)
```

All types are fully typed — compatible with mypy, pyright, and Pylance.
