Metadata-Version: 2.4
Name: airlock-sandbox
Version: 0.8.0
Summary: Python SDK for agent-sandbox: isolated, network-controlled code execution for AI agents.
Author: Farvez Anzam
License-Expression: MIT
Project-URL: Homepage, https://github.com/farvez/agent-sandbox
Project-URL: Source, https://github.com/farvez/agent-sandbox/tree/main/sdk/python
Project-URL: Issues, https://github.com/farvez/agent-sandbox/issues
Project-URL: Changelog, https://github.com/farvez/agent-sandbox/blob/main/sdk/python/CHANGELOG.md
Project-URL: Documentation, https://github.com/farvez/agent-sandbox/blob/main/sdk/python/README.md
Keywords: ai,agents,sandbox,code-execution,gvisor,llm,tools,mcp,langchain,crewai,openai-agents
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: mcp
Requires-Dist: mcp<3,>=2.3; python_version >= "3.10" and extra == "mcp"
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == "langchain"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.2; extra == "openai-agents"
Provides-Extra: crewai
Requires-Dist: crewai>=0.100; python_version >= "3.10" and extra == "crewai"
Dynamic: license-file

# airlock-sandbox

Python SDK for [agent-sandbox](https://github.com/farvez/agent-sandbox): run code from
AI agents in isolated gVisor containers — no network unless you allow it, hard memory,
CPU, process and disk limits, and every outbound connection logged.

```bash
pip install airlock-sandbox
```

No dependencies; Python 3.9+.

## Quick start

```python
from airlock_sandbox import Sandbox

with Sandbox(api_key="...", base_url="https://sandbox.example.com") as sbx:
    sbx.files.write("main.py", "print(2 ** 16)")
    result = sbx.run("python3 main.py")
    print(result.stdout)      # 65536
    print(result.exit_code)   # 0
```

Leaving the `with` block deletes the session and its files. Settings can come from the
environment instead: `SANDBOX_API_URL`, `SANDBOX_API_KEY`, and `SANDBOX_API_INSECURE=1`
for a server with a self-signed certificate (or pass `verify="/path/to/ca.pem"`).

## Running commands

```python
result = sbx.run("python3 -m pytest -q", timeout=60)   # 1–60 s
result.stdout, result.stderr, result.exit_code
result.ok            # exit code 0, not timed out, not killed
result.timed_out     # hit the timeout
result.oom_killed    # killed for exceeding the memory limit
result.warnings      # e.g. over the disk quota
result.check()       # raises CommandError unless ok; returns the result
str(result)          # text form with [STDOUT]/[STDERR]/[EXIT CODE] — handy for an LLM
```

Each command runs in a fresh container; files in the workspace persist between
commands, processes do not.

## Import a GitHub repository

```python
with Sandbox(egress=["pypi"]) as sbx:
    sbx.import_repo("psf/requests")                     # or a URL; ref="v2.32.3", path="src"
    sbx.run("cd requests && pip install -e . pytest", timeout=60).check()
    print(sbx.run("cd requests && python -m pytest -q tests/test_utils.py", timeout=60).output)
```

Public repositories only. The server downloads the archive (up to 100 MB) and unpacks it
inside the sandbox, within the workspace disk quota, so this works without egress.

## Upload, download and snapshots

```python
with Sandbox() as sbx:
    sbx.files.upload("data/input.parquet", open("input.parquet", "rb").read())  # any bytes, up to 50 MB
    sbx.run("python3 analyse.py > out/report.csv").check()
    open("report.csv", "wb").write(sbx.files.download("out/report.csv"))
    open("out.tar.gz", "wb").write(sbx.files.download_archive("out"))           # a folder as .tar.gz

    snap = sbx.snapshot("deps installed")      # outlives the session (kept 30 days on the hosted service)

with Sandbox(snapshot=snap["id"]) as sbx:      # starts with the snapshot's files
    print(sbx.run("ls").stdout)
```

`sbx.restore(id, path=".")` unpacks a snapshot into a running sandbox; `sbx.list_snapshots()` and
`sbx.delete_snapshot(id)` manage them. Snapshots include installed packages under `/workspace/.local`.

## Agent frameworks

Ready-made tools (`write_file`, `read_file`, `run_command`) for the popular frameworks. Install the
framework as usual, or with an extra: `pip install "airlock-sandbox[langchain]"`,
`"[openai-agents]"` or `"[crewai]"`.

```python
# LangChain / LangGraph
from langchain.agents import create_agent
from airlock_sandbox.integrations.langchain import airlock_tools

with Sandbox(egress=["pypi"]) as sbx:
    agent = create_agent("anthropic:claude-opus-5-5", tools=airlock_tools(sbx))
    agent.invoke({"messages": [{"role": "user", "content": "Write fizzbuzz.py and run it"}]})
```

```python
# OpenAI Agents SDK
from agents import Agent, Runner
from airlock_sandbox.integrations.openai_agents import airlock_tools

with Sandbox() as sbx:
    agent = Agent(name="coder", instructions="Write code and run it.", tools=airlock_tools(sbx))
    print(Runner.run_sync(agent, "Write fizzbuzz.py and run it").final_output)
```

```python
# CrewAI
from crewai import Agent, Crew, Task
from airlock_sandbox.integrations.crewai import airlock_tools

with Sandbox() as sbx:
    coder = Agent(role="Developer", goal="Write working code", backstory="A careful engineer.",
                  tools=airlock_tools(sbx))
    task = Task(description="Write fizzbuzz.py and run it", expected_output="Its output", agent=coder)
    print(Crew(agents=[coder], tasks=[task]).kickoff())
```

`airlock_tools(sbx, tools=["run_command"])` picks a subset. Tool errors come back to the model as
text it can act on, and long output is truncated in the middle.

## Long-running sandboxes and previews

```python
with Sandbox(persistent=True, egress=["pypi"]) as sbx:
    sbx.files.write("app.py", "from flask import Flask\napp = Flask(__name__)\n@app.get('/')\ndef hi(): return 'hi'")
    sbx.run("pip install flask", timeout=60).check()
    sbx.run("nohup flask --app app run --host 0.0.0.0 --port 8000 > flask.log 2>&1 &").check()
    print(sbx.expose(8000))        # https://8000-<token>.<preview domain>, open while the sandbox is
```

One container runs for the whole session, so background processes keep running between
commands (redirect their output, as above). Each account can run a few at a time. The server's
interactive terminal is a WebSocket; see the [API reference](https://github.com/farvez/agent-sandbox/blob/main/docs/api.md#long-running-sessions).

## Internet access (egress)

Sandboxes have no network by default. Ask for specific hosts — they must be allowed by
your tenant's policy on the server:

```python
with Sandbox(egress=["pypi"]) as sbx:              # presets: pypi, npm, github, huggingface
    sbx.run("pip install requests", timeout=60).check()
    for event in sbx.egress_log():
        print(event["decision"], event["host"], event.get("reason", ""))
```

Only HTTPS to the listed hosts works; everything else is refused and logged.
`sbx.egress_policy()` lists what your tenant may request.

## Use as tools for an LLM agent

```python
from airlock_sandbox import Sandbox
from airlock_sandbox.tools import openai_tools, anthropic_tools, handle_tool_call

# OpenAI
response = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=openai_tools())
for call in response.choices[0].message.tool_calls or []:
    output = handle_tool_call(sbx, call.function.name, call.function.arguments)

# Anthropic
response = client.messages.create(model="claude-sonnet-5", max_tokens=2048, messages=messages, tools=anthropic_tools())
for block in response.content:
    if block.type == "tool_use":
        output = handle_tool_call(sbx, block.name, block.input)
```

Tools: `write_file`, `read_file`, `run_command`. `handle_tool_call` never raises — errors
come back as text the model can act on — and long output is trimmed to 8,000 characters,
keeping the beginning and the end.

## MCP server (Claude Code, Claude Desktop, other MCP clients)

**Remote (recommended): nothing to install.** The server has an MCP endpoint at `/mcp`;
connect with a URL and your key:

```bash
claude mcp add --transport http --scope user airlock "https://<server>/mcp?egress=pypi" --header "X-API-Key: <key>"
```

- `?egress=pypi` (comma-separated hosts or presets) gives the sandbox internet access, within
  your tenant's policy; leave it out for no network.
- There's one sandbox per key, reused by every call and reconnect; it closes after 30 minutes
  idle. Add `&workspace=<name>` for a separate sandbox.
- Tools: `run_command`, `write_file`, `read_file`, `list_files`, `import_repo`, `egress_log`,
  `activity`, `save_snapshot`, `list_snapshots`, `restore_snapshot`, `sandbox_info`, `reset_sandbox`. Commands show up in the console's Activity page
  as `mcp key <id>`.

**Local:** the package also includes an MCP server that runs on your machine over stdio and
talks to the API. Install with the `mcp` extra (Python 3.10+):

```bash
pip install "airlock-sandbox[mcp]"
```

**Claude Code:**

```bash
claude mcp add airlock --scope user -e SANDBOX_API_URL=https://<server> -e SANDBOX_API_KEY=<key> -e AIRLOCK_EGRESS=pypi -- airlock-sandbox-mcp
```

Add `-e SANDBOX_API_INSECURE=1` if the server uses a self-signed certificate. With
[uv](https://docs.astral.sh/uv/) you can skip the install and use
`-- uvx --from "airlock-sandbox[mcp]" airlock-sandbox-mcp` as the command instead.
Put the server name (`airlock`) right after `add`: `-e` takes several values and would
otherwise swallow the name. If `uvx` reports an old version, run it once with `--refresh`.

**If Claude Code says the server timed out:** it waits 30 seconds for a server to start. On
Windows, importing the `mcp` library alone can take 8–10 seconds (antivirus scanning its many
files), and `uvx` adds an install on the first start after each release. Two fixes, either works:

- Install the tool once, so starting it never installs anything, and point Claude Code at it:

  ```bash
  uv tool install "airlock-sandbox[mcp]"     # later: uv tool upgrade airlock-sandbox
  claude mcp add airlock --scope user -e SANDBOX_API_URL=https://<server> -e SANDBOX_API_KEY=<key> -e AIRLOCK_EGRESS=pypi -- airlock-sandbox-mcp
  ```
- Give servers longer to start, by setting `MCP_TIMEOUT` (milliseconds) before starting Claude Code,
  e.g. `MCP_TIMEOUT=60000 claude` (PowerShell: `$env:MCP_TIMEOUT=60000; claude`).

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "airlock": {
      "command": "airlock-sandbox-mcp",
      "env": {
        "SANDBOX_API_URL": "https://<server>",
        "SANDBOX_API_KEY": "<key>",
        "AIRLOCK_EGRESS": "pypi"
      }
    }
  }
}
```

On Windows, if Claude Desktop can't find the command, use its full path (`where airlock-sandbox-mcp`).

**Tools:** `run_command`, `write_file`, `read_file`, `list_files`, `import_repo`, `egress_log`,
`activity`, `save_snapshot`, `list_snapshots`, `restore_snapshot`, `sandbox_info`, `reset_sandbox`. Each MCP server process gets one sandbox session, created
on first use and deleted when the client disconnects; files persist between commands
within it. `AIRLOCK_EGRESS` (or `--egress`) sets which hosts the sandbox may reach — it
must be allowed by your tenant's policy; leave it unset for no network.

## Errors

All errors inherit from `SandboxError`:

| Exception | When |
|-----------|------|
| `AuthenticationError` | Missing or invalid API key (401) |
| `PermissionDeniedError` | Path outside the workspace, egress outside your policy, someone else's session (403) |
| `NotFoundError` | Session doesn't exist or expired (404) |
| `QuotaExceededError` | Write would exceed the workspace disk quota (413) |
| `RateLimitError` | Tenant limit hit: sessions, requests/minute or concurrent commands (429); see `.retry_after` |
| `CapacityError` | Every workspace on the server is in use (503) |
| `ValidationError` | Invalid request, e.g. unknown template (400/422) |
| `APIConnectionError` | Server unreachable |
| `CommandError` | Raised by `CommandResult.check()` |

Rate limits (429) and a full server (503) are retried automatically, honouring the
server's `Retry-After` (`max_retries=3` by default).

## API keys

Create, list and revoke keys without your operator's help — for example to rotate:

```python
from airlock_sandbox.keys import Keys

keys = Keys()                          # uses SANDBOX_API_URL / SANDBOX_API_KEY
new = keys.create(name="laptop-2026")  # new["api_key"] is shown only once — store it
keys.revoke("<old key_id>")            # stops working immediately
keys.list()
```

Or from the shell: `airlock-sandbox-keys create --name ci`, `airlock-sandbox-keys list`,
`airlock-sandbox-keys revoke <key_id>`. Operators use `--admin` / `Keys(admin=True)` with
`SANDBOX_ADMIN_KEY` to issue keys for any tenant.

## Account

```python
sbx.usage()    # {"limits": {...}, "sessions_open": 1, "commands_running": 0, "requests_available": 118}
sbx.health()

page = sbx.audit(limit=50)          # this tenant's command history, newest first (never output)
for e in page["entries"]:
    print(e["exit_code"], e["command"], e["actor"])
older = sbx.audit(before=page["next"]) if page["next"] else None
```

## License

MIT © 2026 Farvez Anzam. The Airlock server it talks to is AGPL-3.0; see the
[main repository](https://github.com/farvez/agent-sandbox).
