Skip to main content

sandbox-cli for Python

pip install sandbox-cli-sdk      # the distribution
from sandbox_cli import Studio   # the import

Those two names differ on purpose: sandbox-cli on PyPI belongs to an unrelated project, so the distribution takes the same shape as the npm package (@sandbox-cli/sdk) while the module keeps the name you would guess.

Drive sandbox-cli from a program: run commands and agents in isolated containers, and get back the exit code, the output, and which agent actually did the work.

from sandbox_cli import Studio

studio = Studio.connect()                 # finds the local daemon: port and token
repo = studio.project("my-app")           # or project() for the one you are in
ws = repo.workspace("agent-42")           # a branch's git worktree

print(ws.run(["pytest", "-q"]).exit_code)

Async, for the same client:

import asyncio
from sandbox_cli.aio import AsyncStudio

async def main():
    studio = await AsyncStudio.connect()
    ws = await (await studio.project("my-app")).workspace("agent-42")
    a, b = await asyncio.gather(ws.run(["pytest", "-q"]), other_work())

asyncio.run(main())

Before it works

A Studio daemon has to be running: sh studio.sh up in a sandbox-cli checkout. This package finds its port and token in ~/.config/sandbox/studio — the same files the daemon writes — so there is nothing to paste. SANDBOX_API_URL and SANDBOX_STUDIO_TOKEN override, and explicit arguments override those.

What this is, and what it is not

It is a client. Every gate that makes a sandbox a sandbox — the workspace refusals, the fake HOME, default-deny environment, the egress allowlist — is applied where the container is built, on the machine running the daemon. This package holds no docker socket, shells out to nothing, and assembles no argv.

No dependencies, deliberately: it is imported into somebody's agent process, and an HTTP stack is a bad thing to drag in behind them. The async face runs the same calls in a thread rather than duplicating them against a second stack — one implementation, and a test that fails when the two surfaces drift.

Things that will bite you otherwise

A repository is named, never located. project() with no argument asks git which repository the current directory belongs to and matches it against what the daemon knows; it does not register anything. add_project(path) is the sentence that asks, and add_project(init=True) will git init a directory that is not a repository yet.

Studio works from committed state. A repository with files and no commits makes empty worktrees, so add_project refuses it and tells you what to run rather than handing an agent a /workspace with none of your files in it.

stdout is the run's log lines, joined. Right for reading output, wrong for copying a file — a trailing newline cannot survive it. Move artifacts base64-encoded in both directions.

Each run is a new container. Nothing outside the worktree survives between steps: /tmp is gone, /workspace is not.

Error names avoid the builtins. TimeoutError and ConnectionError are Python's own, so this package raises RequestTimeout and DaemonUnreachable instead; ApiError and WaitError mean what they do in the TypeScript client.

A run outliving its deadline is not an error. RequestTimeout means one HTTP request was slow. A timeout= that expires stops the container and returns an Outcome with stopped=True — check that before you read exit_code, because the exit code of a container somebody stopped is not a verdict on the work.

Adding a repository

Three ways, and the difference is who owns the directory:

studio.project("my-app")                        # already registered
studio.add_project("/home/you/code/my-app")     # a directory on the daemon's machine
studio.add_project(init=True)                   # this one, `git init` first
studio.clone("Amitgb14/sandbox-cli", "/home/you/code")   # clone it there, then register

clone takes a full git URL or the GitHub shorthand owner/repo. Everything else is passed through untouched for the daemon to accept or refuse — including ext::, which it refuses, because deciding that here would put the refusal in two places and let them disagree. Private repositories use whatever credentials the daemon's git has; putting a token in the URL would write it into that machine's remote config.

Steps and environment

ws = repo.workspace("ci", env={"CI": "true"})     # applies to every run here
ws.steps([
    ["npm", "ci"],
    ["npm", "test"],
    ["npm", "run", "build"],
], env={"NODE_ENV": "test"})                      # merged over the workspace's, per key

steps stops at the first failure and returns what actually ran. That rule is the reason it exists rather than a for loop: a loop that runs everything reports the last exit code, so a failed install followed by a passing lint looks like success.

Environment from a file, explicitly:

from sandbox_cli.env import read_env_file
ws.run(["python3", "app.py"], env=read_env_file(".env.local"))

Nothing is read unless you name a file, and a malformed line raises with its number rather than being skipped — a silently ignored line in a credentials file is how a run goes out without the key it needed. Values travel in the request body, so against a remote daemon without TLS they cross the network in cleartext.

Examples

examples/stock_price.py — untrusted code fetching a quote, and the two lines that decide what it can do:

$ python3 examples/stock_price.py TSLA
TSLA  362.86 USD  (NasdaqGS)

It is worth reading for allow= rather than for the price. Naming a host turns the egress allowlist on for that run: measured against a daemon with unrestricted egress, example.com answers 200 without allow and is refused with it. Asking for one host means giving up the rest of the internet, which is usually what you want for code you did not write.

examples/travel_planner.py — three agents that hand work to each other, and a gate that decides. Two specialists research in parallel, each in its own worktree, so the coordinator cannot see what they wrote: the artifacts cross through this process, base64-encoded in both directions. The gate asks the filesystem rather than the agent, because an agent that reports success having written nothing is the failure it cannot be asked about:

OK   agent-flights   claude  ok
SKIP agent-hotels    claude  finished without writing hotels.json
handed over: flights.json

Publishing

cd sdk/python
python3 -m pip install --upgrade build twine     # once
python3 -m build                                 # -> dist/*.whl and *.tar.gz
python3 -m twine check dist/*
python3 -m twine upload dist/*                   # asks for a PyPI API token

The distribution is sandbox-cli-sdk; the plain name belongs to somebody else. Use __token__ as the username and a PyPI API token as the password, scoped to this project once it exists. twine upload --repository testpypi dist/* publishes to TestPyPI first, which is worth doing once: a version number cannot be reused, so the first upload is the one that has to be right.

Delete dist/ between builds — twine upload dist/* uploads whatever is there, including artifacts from an older version you did not mean to ship.

Status

Early. The surface above is stable enough to build on; run_code, artifacts and the code-interpreter face described in docs/proposals/python-sdk.md are next.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sandbox_cli_sdk-0.0.1.tar.gz (31.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sandbox_cli_sdk-0.0.1-py3-none-any.whl (24.6 kB view details)

Uploaded Python 3

File details

Details for the file sandbox_cli_sdk-0.0.1.tar.gz.

File metadata

  • Download URL: sandbox_cli_sdk-0.0.1.tar.gz
  • Upload date:
  • Size: 31.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for sandbox_cli_sdk-0.0.1.tar.gz
Algorithm Hash digest
SHA256 62d11617b5bc19c76bd3832f3862d589d1979e2327ea5b4d83daaa358493c175
MD5 c33141052f013e88d1ff031606f62c7c
BLAKE2b-256 c43ff4eff6ac933983030f18c523d419c882ee4a6d2ae68810171482c367fb1c

See more details on using hashes here.

File details

Details for the file sandbox_cli_sdk-0.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for sandbox_cli_sdk-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 be5f0a2305d9129a1ea12f3129a67578e203c6c3f51d48bf61d8e5883a19dbe1
MD5 a1cb917e7afe885633143d7178f9c159
BLAKE2b-256 69ab2e5d359ccbbf76ef9b236dbb7d7e2c408cb764ced4158ac9b88ad2100d33

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page