Skip to main content

arcbox

Python SDK for ArcBox sandboxes: isolated microVMs on your Mac, driven over the local daemon's Unix socket with the Connect protocol. Requires Python ≥ 3.10.

uv add arcbox        # or: pip install arcbox

Hello world

With the daemon running (abctl daemon start):

from arcbox import Sandbox

# Local daemon over ~/.arcbox/run/arcbox.sock — zero config.
with Sandbox.create("", ttl=300) as sandbox:
    sandbox.files.write_text("/tmp/hello.txt", "hello from arcbox\n")

    check = sandbox.commands.run(["/bin/cat", "/tmp/hello.txt"])
    print(check.expect().stdout, "→ exit", check.exit_code)

    job = sandbox.commands.run("for i in 1 2 3; do echo line$i; done", background=True)
    for chunk in job.output:
        print(chunk.data.decode(), end="")
    print("background job exited", job.wait_for_exit().exit_code)
# context exit: sandbox killed, nothing leaked

Async is a first-class mirror (AsyncSandbox, async with, async for):

from arcbox import AsyncSandbox


async def main() -> None:
    sandbox = await AsyncSandbox.create("", ttl=300)
    async with sandbox:
        await sandbox.files.write_text("/tmp/hello.txt", "hello from arcbox\n")
        result = await sandbox.commands.run(["/bin/cat", "/tmp/hello.txt"])
        print(result.expect().stdout)

Non-zero exit is data (result.exit_code), never an exception — result.expect() (or run(..., check=True), subprocess.run-style) is the opt-in raise. Every daemon error maps to a typed class in arcbox.errors (SandboxNotFoundError, CapabilityError, ConnectionFailedError, ...) carrying a machine-readable code, an actionable suggestion, and the failed operation. Time arguments are seconds (floats) everywhere.

Connection

Resolution order: explicit option > environment > default.

Environment Meaning
ARCBOX_SOCKET daemon Unix socket (default $ARCBOX_DATA_DIR/run/arcbox.sock; data dir default ~/.arcbox, or ~/.arcbox-dev under ARCBOX_PROFILE=development)
ARCBOX_API_URL remote daemon / cloud front door; setting it selects the remote tier (reserved, CORE-63)
ARCBOX_API_KEY bearer credential, attached as Authorization when set; unused by the local daemon

Every entry point takes a connection=Connection(...) slot (socket_path / api_url / api_key / request_timeout / injected http_client for mocking — pass an httpx.Client to the sync surface, an httpx.AsyncClient to the async one).

The Sandbox / AsyncSandbox classmethods resolve a hidden connection per call, and the returned handle closes its HTTP client on context exit. Long-lived programs should hold an ArcBox / AsyncArcBox instead: it is a context manager (or call .close() / .aclose()), and every handle it creates shares its client. An injected http_client always belongs to the caller and is never closed by the SDK.

E2B-compatible surface

arcbox.e2b serves the e2b SDK's shape, so existing E2B code runs against the local daemon with one import change:

from arcbox.e2b import Sandbox  # was: from e2b import Sandbox

sandbox = Sandbox.create(timeout_ms=300_000)
sandbox.files.write("/tmp/hello.txt", "hello\n")
print(sandbox.commands.run("cat /tmp/hello.txt").stdout)
sandbox.kill()

AsyncSandbox mirrors it, as in e2b. No API key and no network: E2B's cloud arguments (api_key, domain, access_token, ...) are accepted and ignored, so an app that reads E2B_API_KEY from its environment keeps working.

Most of the surface is a rename. These behaviours genuinely differ:

  • Time is milliseconds here, as e2b counts it, converted to this SDK's float seconds at that one boundary.
  • get_host(port) needs the port exposed first. E2B fronts every sandbox port with an edge proxy at {port}-{id}.e2b.app and so can answer for a port nobody declared; ArcBox forwards on request, so sandbox.expose_port(3000) comes first and get_host(3000) is synchronous from there on. An un-exposed port raises rather than returning an address nothing is listening on.
  • set_timeout replaces rather than extends. E2B only pushes the deadline out; ArcBox re-arms it from now.
  • 'base' and an omitted template both resolve to the built-in minimal template. Other names come from the local catalog, not E2B's registry.
  • files.get_info().owner/.group are numeric — the daemon reports uids and gids, and inventing a name would not be honest.
  • commands.list() reports ids only — the daemon does not report argv, so cmd/args/envs are empty rather than invented.
  • watch_dir(...).stop() is best-effort in the sync flavor — a pumping thread cannot be interrupted mid-read; the daemon thread ends when the watch stream does. The async flavor cancels for real.
  • The exception classes are aliases of arcbox.errors', not new subclasses, so except SandboxException matches what the SDK raises. CommandExitException is real and is raised by commands.run() on a non-zero exit, as in E2B.
  • Only the instance form of kill() exists. e2b carries Sandbox.kill(sandbox_id) and sandbox.kill() under one name, which Python cannot express; reach a sandbox you do not hold through Sandbox.connect(sandbox_id) first.

Anything with no local counterpart raises UnsupportedException on the first call rather than failing quietly: fork, volumes, signed upload/download URLs, get_metrics, the MCP gateway, the Template build DSL, and git.dangerously_authenticate. NotEnoughSpaceException, RateLimitException, GitAuthException, GitUpstreamException, BuildException, FileUploadException and VolumeException are exported so existing imports resolve, but a local daemon has no quota, no registry build and no volumes, so nothing raises them.

Development

Inside the arcbox repo (sdk/python):

uv sync                                  # create .venv from uv.lock
uv run python scripts/gen_proto.py       # regenerate src/arcbox/_gen from ../../rpc/arcbox-protocol/proto
uv run python scripts/gen_sync.py        # regenerate src/arcbox/_sync from src/arcbox/_async
uv run ruff check . && uv run ruff format --check .
uv run pyright
uv run pytest                            # includes the sync-tree lockstep + parity checks

Generated code under src/arcbox/_gen/ is committed and is never exported from the package — public shapes are hand-written and mapped at the transport boundary.

The async tree (src/arcbox/_async/) is the source of truth; the sync tree (src/arcbox/_sync/) is generated from it by an unasync token transform and committed. Edit only the async tree, then rerun scripts/gen_sync.py. Lockstep is CI-enforced twice: the transform is rerun and diffed (scripts/gen_sync.py --check, also wired into pytest), and a parity test asserts identical public surfaces modulo async markers.

Optional pre-commit hooks (scoped to sdk/python), via prek or classic pre-commit:

prek install -c sdk/python/prek.yaml

The end-to-end hello-world loop runs only against a live daemon and is opt-in:

ARCBOX_SDK_E2E=1 uv run pytest tests/test_e2e.py

Toolchain notes

  • uv is the package/project manager (uv_build backend, uv.lock committed). Publishing is CI-only: uv build, then upload via pypa/gh-action-pypi-publish under PyPI trusted publishing (OIDC, with PEP 740 attestations — uv publish emits none, astral-sh/uv#15618) — see Releasing; no UV_PUBLISH_TOKEN anywhere.
  • ruff is both linter and formatter (E,F,W,I,UP,B,SIM,RUF).
  • pyright (strict) is the authoritative type checker. Evaluated alternatives (2026-08): ty 0.0.65 reports 16 false positives here (all unresolved-attribute on protobuf generated-module members) — kept in dev-deps for uv run ty check, may replace pyright when it stabilizes; pyrefly 1.2.0 passes cleanly (it imports the pyright config) and serves as an informational second opinion — one authoritative checker avoids double-suppression drift.
  • msgspec parses the SDK's one JSON seam — Connect error bodies and EndStreamResponse frames — as typed, validated Structs at the untrusted-input boundary (chosen for the typed decoding, not speed).
  • Message types are upstream protobuf runtime code generated by the protoc bundled with grpcio-tools (dev-dep); the bundled protoc version matches the pinned runtime.
  • A future native-acceleration path (if profiling ever demands one) is a maturin/PyO3 extension crate in this repo's workspace; nothing in the current SDK needs it.

The development gates (ruff check + format, pyright, the gen_sync.py --check lockstep gate, pytest) run on every PR as the sdk-python job in .github/workflows/ci.yml, from a uv sync --locked environment.

Releasing

The SDK is a release-please component (sdk-python in release-please-config.json), released on its own cadence, independent of the main arcbox release train:

  1. Conventional commits touching sdk/python accumulate on master.
  2. release-please maintains a dedicated release PR for the component (separate from the root, fleet-agent, and sdk-typescript PRs) that bumps the pyproject.toml version and updates CHANGELOG.md.
  3. Merging that PR creates the GitHub release and the tag sdk-python-vX.Y.Z (same convention as sdk-typescript-vX.Y.Z).
  4. The tag is what a PyPI publish workflow (.github/workflows/release-sdk-python.yml) triggers on: it checks out the tag's tree, re-runs the full gate suite (ruff check, ruff format --check, pyright, pytest, gen_sync.py --check), builds with uv build, and publishes via pypa/gh-action-pypi-publish under trusted publishing (OIDC, PEP 740 attestations included) — tokenless: the job's id-token: write permission is exchanged for a short-lived PyPI credential. The job skips cleanly if the version is already on PyPI, so a re-dispatch never fails on an already-published release.

A tag minted while the publish workflow was absent (or a tag whose run failed) is not replayed by a later push — re-dispatch the workflow against the existing tag instead (it takes the tag as a workflow_dispatch input), never publish by hand: a hand publish would need an API token, which the pending-publisher bootstrap below exists to avoid.

One-time bootstrap — unlike npm, PyPI supports pending publishers: the trusted publisher is registered before the first upload and CI does the first publish, so there is no local bootstrap publish and no API token at any point:

  1. On pypi.org → account → Publishing → "Add a new pending publisher" (GitHub): PyPI project name arcbox, owner arcboxlabs, repository arcbox, workflow filename release-sdk-python.yml, environment left empty. Empty is deliberate: PyPI calls the environment "optional but strongly recommended", but its value is the protection rules an environment can carry (required reviewers gating a publish), and this repo configures none — naming one today would add a label, not a gate. Add the environment and a matching environment: key in the workflow together with the reviewer rule, not before.
  2. The first tag-triggered run then creates the arcbox project on PyPI as it publishes, and the pending publisher becomes the project's regular trusted publisher.

A pending publisher does not hold the name: PyPI "does not create a project or reserve a project's name until it is actually used to publish", and if someone else registers arcbox first the pending publisher is invalidated. arcbox is short, generic, and still unclaimed — do the first publish promptly after registering, and re-check the name is free if the bootstrap has been sitting for a while.

Status

Phases 2a and 2b of CORE-58, on both the sync and async surfaces. Shipped:

Surface Notes
Sandbox create/connect/list, kill/pause/info pause/resume are live daemon-side (CORE-21); data-plane calls auto-resume a paused sandbox
commands.run foreground result + background handle; stdin=str|bytes (write-then-close) or stdin=True (keep open); pty=PtySize(cols, rows)
CommandHandle streamed output with transparent offset-resume across stream death (bounded retries → ConnectionLostError), wait_for_exit, kill, write_stdin/close_stdin/stdin_status (offset-idempotent), resize
handle.wait_for_log(pattern) first log line matching a substring/compiled regex, SDK-side over the replayed offset-addressed output (resumes across drops); deadline → TimeoutError naming the knob
commands.get(id) / commands.list() re-attach a handle by execution id (stdin cursor seeded from the daemon); list running and exited executions as CommandInfo summaries
sandbox.events() typed lifecycle events, keepalives filtered; sync reads genuinely block; a mid-stream drop is ConnectionLostError
sandbox.set_lifecycle() tri-state: omitted (UNCHANGED) = unchanged, None = restore default (as on create), value = replace
arcbox.capabilities() daemon handshake (version/protocol/features/nested virt), cached per client
files whole-file read/write; path verbs stat/list/mkdir/remove/move (frozen FileStat dataclass, mkdir -p semantics, FileNotFoundError with the path in context); every path accepts str or PurePosixPath
files.watch(path) typed FsEvent stream via AsyncFileWatch/FileWatch context managers (renames paired, keepalives filtered); ends cleanly on sandbox stop; a mid-stream drop is ConnectionLostError — never auto-reconnected (events cannot be replayed)
ports.wait_for_port(port) guest-side listen-table wait (no client polling); expiry → TimeoutError naming the timeout knob (daemon default 30 s, cap 600 s)
ports.expose(port) / unexpose / list publish a guest port on host loopback (daemon-allocated or specific host port, tcp/udp); list reads the daemon's authoritative live listener table
sandbox.checkpoint() pause + snapshot + resume under the same id; returns the frozen Snapshot catalog row
ArcBox.restore(snapshot_id) new READY sandbox from a snapshot (fresh client-minted id; fresh_network=True for concurrent restores); plus list_snapshots (auto-paginating) and delete_snapshot
Template.build(name, ...) build a catalog template from exactly one source (image= local Docker ref, dockerfile= inline content, snapshot= checkpoint promotion) with default limits/cmd/env, a ready probe (ready_probe_port= or ready_probe_command=), and prewarm=True for a boot-to-READY warm snapshot; no client deadline — builds block as long as they need
Template.get/list/delete_reference, instance publish(version)/delete() resolve name[:version] (bare name = newest published, else draft), auto-paginating list, delete a version or the whole template; publish freezes the draft immutably
create(template=Template | "name[:version]") a Template instance pins its exact reference; template defaults are inherited unless overridden (vcpus/memory_mib replace limits wholesale; no_default_cmd/no_default_env suppress inherited cmd/env)

Deferred: the SDK-side default idle-reaping policy (design decision 4).

Download files

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

Source Distribution

arcbox-0.1.2.tar.gz (108.6 kB view details)

Uploaded Source

Built Distribution

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

arcbox-0.1.2-py3-none-any.whl (136.5 kB view details)

Uploaded Python 3

File details

Details for the file arcbox-0.1.2.tar.gz.

File metadata

  • Download URL: arcbox-0.1.2.tar.gz
  • Upload date:
  • Size: 108.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for arcbox-0.1.2.tar.gz
Algorithm Hash digest
SHA256 82785fa1de6f372d68fec212dea4bd8cc6c54dda8c9914205913dc5a505657f9
MD5 e6c13aa0ad56dc82903d054651d95e24
BLAKE2b-256 81dc0714515e7f48c700abf52904d717456a57790a237668929bf5061eee3a6c

See more details on using hashes here.

Provenance

The following attestation bundles were made for arcbox-0.1.2.tar.gz:

Publisher: release-sdk-python.yml on arcboxlabs/arcbox

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file arcbox-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: arcbox-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 136.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for arcbox-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 2a37878cceb8a710a1d20043e11ce030a7687b6ef5c1173b7f2af095833a5418
MD5 09de396918688241f7c2c64b0ff19c03
BLAKE2b-256 99c6afe77297faf6b174ac348cba6553aab906db0a14f8caf5f00d90827f04f6

See more details on using hashes here.

Provenance

The following attestation bundles were made for arcbox-0.1.2-py3-none-any.whl:

Publisher: release-sdk-python.yml on arcboxlabs/arcbox

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page