# Sandbox SDK

A Python SDK for interacting with the Sandbox Orchestrator. Supports both
**gRPC** and **HTTP** transports with an identical public interface, so you can swap protocols without changing application logic.

## Installation

```bash
pip install flash-sandbox
```

The package pulls in `grpcio`, `protobuf`, and `requests` automatically.

## Quick Start

### HTTP transport (recommended for simplicity)

```python
from flash_sandbox import HTTPClient

client = HTTPClient(host="localhost", port=8080)

# Start a Docker sandbox
sandbox_id = client.start_sandbox(
    type="docker",
    image="alpine:latest",
    command=["tail", "-f", "/dev/null"],
    memory_mb=128,
    cpu_cores=0.5,
)
print(f"Sandbox ID: {sandbox_id}")

# Execute a command
result = client.exec_command(sandbox_id, ["echo", "Hello from HTTP!"])
print(f"Output: {result.stdout.strip()}")
print(f"Exit code: {result.exit_code}")

# Check status
status = client.get_status(sandbox_id)
print(f"Status: {status}")

# Get resource metrics
metrics = client.get_metrics(sandbox_id)
print(f"Memory: {metrics.memory_usage_bytes} / {metrics.memory_limit_bytes}")
print(f"CPU: {metrics.cpu_percent}%")

# Snapshot and resume
snap = client.snapshot_sandbox(sandbox_id)
print(f"Snapshot: {snap['snapshot_path']}")
client.resume_sandbox(sandbox_id)

# Stop
client.stop_sandbox(sandbox_id)
client.close()
```

### gRPC transport

```python
from flash_sandbox import SandboxClient

client = SandboxClient(host="localhost", port=50051)

# Start a Docker sandbox
docker_id = client.start_sandbox(
    type="docker",
    image="alpine:latest",
    command=["tail", "-f", "/dev/null"],
    memory_mb=128,
    cpu_cores=0.5,
)
print(f"Docker Sandbox ID: {docker_id}")

# Start a Firecracker sandbox
fc_id = client.start_sandbox(
    type="firecracker",
    image="alpine:latest",
    command=["tail", "-f", "/dev/null"],
    memory_mb=512,
    cpu_cores=1.0,
)
print(f"Firecracker Sandbox ID: {fc_id}")

# Start a gVisor sandbox
gvisor_id = client.start_sandbox(
    type="gvisor",
    image="alpine:latest",
    command=["tail", "-f", "/dev/null"],
    memory_mb=256,
    cpu_cores=1.0,
)
print(f"gVisor Sandbox ID: {gvisor_id}")

# Check status
status = client.get_status(fc_id)
print(f"Firecracker Status: {status}")

# Snapshot and Resume
snap_res = client.snapshot_sandbox(fc_id)
print(f"Snapshot Data: {snap_res}")
client.resume_sandbox(fc_id)

# Execute commands
exec_res = client.exec_command(gvisor_id, ["echo", "Hello from gVisor!"])
print(f"gVisor Output: {exec_res.stdout.strip()}")
print(f"gVisor Exit Code: {exec_res.exit_code}")

# Stop sandboxes
client.stop_sandbox(docker_id)
client.stop_sandbox(fc_id)
client.stop_sandbox(gvisor_id)
client.close()
```

## API Reference

Both `HTTPClient` and `SandboxClient` expose the same set of methods:

| Method | Description |
|---|---|
| `start_sandbox(type, image, command, memory_mb, cpu_cores, ...)` | Start a new sandbox and return its ID. |
| `stop_sandbox(sandbox_id)` | Stop and remove a running sandbox. |
| `exec_command(sandbox_id, command)` | Execute a command in a sandbox. Returns an object with `stdout`, `stderr`, and `exit_code`. |
| `get_status(sandbox_id)` | Return the status string (`"running"`, `"stopped"`, etc.). |
| `get_metrics(sandbox_id)` | Return point-in-time resource-usage metrics (memory, CPU, network, block I/O). |
| `snapshot_sandbox(sandbox_id)` | Create a snapshot. Returns `{"snapshot_path": ..., "mem_file_path": ...}`. |
| `resume_sandbox(sandbox_id)` | Resume a paused / snapshotted sandbox. |
| `close()` | Release the underlying connection / session. |

### Constructor options

#### HTTPClient

```python
HTTPClient(
    host="localhost",       # Orchestrator hostname
    port=8080,              # HTTP port (default 8080)
    address=None,           # Full URL, overrides host/port (e.g. "http://proxy:9090/v1/service/sandbox")
    timeout=30.0,           # Request timeout in seconds (None = no timeout)
    session=None,           # Optional requests.Session for custom TLS / auth / retries
)
```

#### SandboxClient (gRPC)

```python
SandboxClient(
    host="localhost",       # Orchestrator hostname
    port=50051,             # gRPC port (default 50051)
    address=None,           # Full address for proxy routing (e.g. "localhost:8092/v1/service/sandbox")
)
```

### HTTPClient-only extras

The HTTP transport includes a few additional features:

- **Firecracker fields** on `start_sandbox`: `kernel_image`, `initrd_path`, `snapshot_path`, `mem_file_path`.
- **Custom timeouts** per-client via the `timeout` parameter.
- **Session injection** – pass your own `requests.Session` for connection pooling, mutual TLS, retry policies, or authentication headers.
- **Typed exceptions**: `SandboxHTTPError` (with `.status_code` and `.detail`) and the more specific `SandboxNotFoundError` for 404 responses.

### Response types (HTTP client)

| Class | Fields |
|---|---|
| `ExecResult` | `stdout: str`, `stderr: str`, `exit_code: int` |
| `MetricsResult` | `memory_usage_bytes`, `memory_limit_bytes`, `memory_percent`, `cpu_percent`, `pids_current`, `net_rx_bytes`, `net_tx_bytes`, `block_read_bytes`, `block_write_bytes` |
| `SnapshotResult` | `snapshot_path: str`, `mem_file_path: str` |

All response dataclasses are **frozen** (immutable).

## Context manager

Both clients support the context-manager protocol:

```python
from flash_sandbox import HTTPClient

with HTTPClient(host="localhost") as client:
    sid = client.start_sandbox(type="docker", image="alpine:latest")
    client.stop_sandbox(sid)
# Connection is automatically closed here.
```

## Proxy / reverse-proxy support

Both clients support routing through a reverse proxy that uses path-based
routing. Pass the full address including the path prefix:

```python
# HTTP through a proxy
http_client = HTTPClient(address="http://proxy.example.com:8092/v1/service/sandbox")

# gRPC through a proxy
grpc_client = SandboxClient(address="proxy.example.com:8092/v1/service/sandbox")
```

## Running tests

```bash
pip install -e ".[dev]"
pytest tests/
```
