Metadata-Version: 2.5
Name: agentbox-sandbox
Version: 0.8.0
Summary: Self-hosted code execution sandbox for AI agents
Project-URL: Homepage, https://github.com/yashshah9/agentbox
Project-URL: Repository, https://github.com/yashshah9/agentbox
Project-URL: Issues, https://github.com/yashshah9/agentbox/issues
Author-email: Yash Shah <yash376351@gmail.com>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.111
Requires-Dist: pydantic-settings>=2.2
Requires-Dist: pydantic>=2.6
Requires-Dist: structlog>=24.1
Requires-Dist: uvicorn[standard]>=0.29
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: mypy>=1.9; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# agentbox

Self-hosted **code execution sandbox** for AI agents — one `docker compose up` gives you an HTTP API for running untrusted code in isolated environments.

[![PyPI](https://img.shields.io/pypi/v/agentbox-sandbox.svg)](https://pypi.org/project/agentbox-sandbox/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![CI](https://github.com/yashshah9/agentbox/actions/workflows/ci.yml/badge.svg)](https://github.com/yashshah9/agentbox/actions/workflows/ci.yml)

> **Status:** v0.8 — Docker/subprocess sandboxes, **egress allowlists**, timeouts, memory limits, snapshots, TypeScript client.

## 60-second try

```bash
pip install agentbox-sandbox
agentbox serve   # API on :8080
# or: docker compose up agentbox
curl -s http://localhost:8080/health
curl -s -X POST http://localhost:8080/v1/run \
  -H 'Content-Type: application/json' \
  -d '{"code":"print(sum(range(10)))"}'
```

## Why this vs alternatives

| Approach | Strength | Gap |
|----------|----------|-----|
| **agentbox** | Self-hosted HTTP API + SDK; Docker or subprocess backends | Not gVisor/Firecracker (yet) |
| Hosted sandboxes (E2B, etc.) | Strong isolation, managed | Per-second cost; data leaves your network |
| Raw `docker exec` | Familiar | No agent-oriented API / snapshots / limits |
| YOLO in the agent process | Zero infra | Full host compromise risk |

## Problem

Every agent that writes and runs code needs a safe execution environment. Teams either YOLO in shared containers or pay per-second for hosted sandboxes. Self-hosting gVisor/Firecracker is weeks of work.

## Key features (v0.8)

- HTTP API: `POST /v1/run` executes Python or JavaScript
- **Docker backend** (`AGENTBOX_SANDBOX_BACKEND=docker`): ephemeral `docker run --rm`, `--network=none`, optional `--memory`, workspace at `/work`
- Subprocess backend remains the **default** for easy local tests
- Per-request `limits.timeout_seconds` (HTTP 408 on timeout)
- Per-request `limits.memory_mb` (Docker `--memory` or subprocess `RLIMIT_AS`); response includes `limits_applied` + `oom_killed`
- `AGENTBOX_MAX_MEMORY_MB` clamps requested memory
- Default-deny egress: Docker `--network=none` / Linux `unshare`/`bwrap` (`network_isolated`)
- **Egress allowlists**: `AGENTBOX_EGRESS_ALLOWLIST` + per-request `egress_allowlist` (soft userspace filter when non-empty)
- Workspace snapshots: `"snapshot": true` then `"snapshot_id"`
- Python SDK + TypeScript client (`sdk/ts/client.ts`)
- Credential stripping when the backend is not `unrestricted`

## Architecture

```
Agent / SDK
    └── POST /v1/run
            ├── SubprocessSandbox (default — easy tests)
            └── DockerSandbox (recommended — stronger isolation)
```

| Component | Technology | Why |
|-----------|------------|-----|
| API | FastAPI | Async-ready, OpenAPI docs, widely adopted |
| Server | uvicorn | Standard ASGI server |
| Config | pydantic-settings | Typed env config |
| Isolation | Docker / subprocess | Docker for production; subprocess for CI/dev |
| Tests | pytest + httpx TestClient | Fast API testing |

## Installation

```bash
pip install agentbox-sandbox
pip install -e ".[dev]"
```

## Usage

### Start server

```bash
agentbox serve
# or
docker compose up agentbox
```

### Recommended: Docker sandbox backend

Requires the Docker CLI (and a reachable daemon) on the host running agentbox:

```bash
export AGENTBOX_SANDBOX_BACKEND=docker
# optional:
# export AGENTBOX_DOCKER_IMAGE=python:3.12-slim
# export AGENTBOX_DOCKER_NODE_IMAGE=node:20-slim
agentbox serve
```

Each `/v1/run` starts an ephemeral container, mounts a temp workspace at `/work`, applies timeout on `docker run`, and removes the container (`--rm`). Health and run responses report `backend: "docker"`.

### Run code

```bash
curl -X POST http://localhost:8080/v1/run \
  -H 'Content-Type: application/json' \
  -d '{"code": "print(sum(range(10)))"}'

curl -X POST http://localhost:8080/v1/run \
  -H 'Content-Type: application/json' \
  -d '{"code": "x = bytearray(10**9)", "limits": {"memory_mb": 64, "timeout_seconds": 5}}'

# Allowlisted egress (subset of AGENTBOX_EGRESS_ALLOWLIST when set):
curl -X POST http://localhost:8080/v1/run \
  -H 'Content-Type: application/json' \
  -d '{"code":"print(1)","egress_allowlist":["example.com"]}'
```

### Python SDK

```python
from agentbox.sdk.client import AgentboxClient

client = AgentboxClient("http://localhost:8080")
print(client.health())
print(client.run("print('hello')"))
print(client.run("console.log('hello')", language="javascript", timeout_seconds=5))
print(client.run("x = bytearray(10**8)", memory_mb=64))
snap = client.run("open('memo.txt','w').write('kept')", snapshot=True)
print(client.run("print(open('memo.txt').read())", snapshot_id=snap["snapshot_id"]))
client.close()
```

## Docker

```bash
docker compose up agentbox        # start API on :8080 (subprocess backend)
docker compose run --rm test      # unit tests
```

### Compose + Docker sandbox backend

```bash
export AGENTBOX_HOST_TMP="$(pwd)/.agentbox-work"
mkdir -p "$AGENTBOX_HOST_TMP"
docker compose -f compose.yaml -f compose.docker-sandbox.yaml up --build agentbox
```

Uses image target `runtime-docker` (`docker-cli` + `AGENTBOX_SANDBOX_BACKEND=docker`), mounts the Docker socket, and bind-mounts `AGENTBOX_HOST_TMP` at the **same absolute path** so nested `docker run -v` works on Docker Desktop.

## Configuration

| Variable | Default | Description |
|----------|---------|-------------|
| `AGENTBOX_HOST` | `0.0.0.0` | Bind host |
| `AGENTBOX_PORT` | `8080` | Bind port |
| `AGENTBOX_DEFAULT_TIMEOUT_SECONDS` | `30` | Execution timeout |
| `AGENTBOX_DEFAULT_MEMORY_MB` | unset | Optional default memory cap |
| `AGENTBOX_MAX_MEMORY_MB` | `8192` | Clamp requested `memory_mb` to this max |
| `AGENTBOX_SANDBOX_BACKEND` | `subprocess` | `subprocess` \| `docker` \| `unrestricted` |
| `AGENTBOX_DOCKER_IMAGE` | `python:3.12-slim` | Image for Python runs (docker backend) |
| `AGENTBOX_DOCKER_NODE_IMAGE` | `node:20-slim` | Image for JavaScript runs (docker backend) |
| `AGENTBOX_EGRESS_ALLOWLIST` | empty | Comma-separated `host[:port]`; empty = deny-all |
| `AGENTBOX_SNAPSHOT_DIR` | `/tmp/agentbox-snapshots` | Workspace snapshot store |

## Running tests

```bash
pytest tests/ -v
# Docker unit tests mock the CLI; live Docker run is skipped if docker is unavailable
pytest tests/test_docker.py -v
```

## Roadmap

- [x] Node.js runtime + TypeScript client + per-run timeout
- [x] Filesystem snapshot/restore (tar workspaces)
- [x] `limits.memory_mb` via `RLIMIT_AS` / Docker `--memory`
- [x] Default-deny egress via Linux netns (`unshare`/`bwrap`) when available
- [x] Docker ephemeral-container backend
- [x] Egress allowlists (`AGENTBOX_EGRESS_ALLOWLIST` + per-request)
- [ ] gVisor runsc backend with warm pool
- [ ] Kernel/iptables egress enforcement (beyond soft userspace hooks)

## License

MIT

## Known limitations (v0.8)

- **Subprocess** default is convenient for tests — **not production-grade isolation**; use `AGENTBOX_SANDBOX_BACKEND=docker` for stronger isolation
- Docker backend needs a local Docker CLI/daemon; missing CLI returns a clear 400
- Subprocess `RLIMIT_AS` is a soft address-space cap, not a cgroup memory controller (Docker uses `--memory`)
- Subprocess default-deny egress uses Linux `unshare`/`bwrap` when present; **macOS stays credential-scrub only** (`network_isolated: false`) unless an allowlist soft-hook applies
- Allowlists are a **soft** Python/Node userspace filter (not iptables); determined code can bypass
- Single-node, no warm pool
- TypeScript client is source-only (not published to npm)
