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
e2bcounts 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.appand so can answer for a port nobody declared; ArcBox forwards on request, sosandbox.expose_port(3000)comes first andget_host(3000)is synchronous from there on. An un-exposed port raises rather than returning an address nothing is listening on.set_timeoutreplaces 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/.groupare 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, socmd/args/envsare 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, soexcept SandboxExceptionmatches what the SDK raises.CommandExitExceptionis real and is raised bycommands.run()on a non-zero exit, as in E2B. - Only the instance form of
kill()exists.e2bcarriesSandbox.kill(sandbox_id)andsandbox.kill()under one name, which Python cannot express; reach a sandbox you do not hold throughSandbox.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_buildbackend,uv.lockcommitted). Publishing is CI-only:uv build, then upload viapypa/gh-action-pypi-publishunder PyPI trusted publishing (OIDC, with PEP 740 attestations —uv publishemits none, astral-sh/uv#15618) — see Releasing; noUV_PUBLISH_TOKENanywhere. - 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-attributeon protobuf generated-module members) — kept in dev-deps foruv 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
EndStreamResponseframes — 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:
- Conventional commits touching
sdk/pythonaccumulate onmaster. - release-please maintains a dedicated release PR for the component
(separate from the root, fleet-agent, and sdk-typescript PRs) that
bumps the
pyproject.tomlversion and updatesCHANGELOG.md. - Merging that PR creates the GitHub release and the tag
sdk-python-vX.Y.Z(same convention assdk-typescript-vX.Y.Z). - 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 withuv build, and publishes viapypa/gh-action-pypi-publishunder trusted publishing (OIDC, PEP 740 attestations included) — tokenless: the job'sid-token: writepermission 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_dispatchinput), 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:
- On pypi.org → account → Publishing → "Add a new pending publisher"
(GitHub): PyPI project name
arcbox, ownerarcboxlabs, repositoryarcbox, workflow filenamerelease-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 matchingenvironment:key in the workflow together with the reviewer rule, not before. - The first tag-triggered run then creates the
arcboxproject 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82785fa1de6f372d68fec212dea4bd8cc6c54dda8c9914205913dc5a505657f9
|
|
| MD5 |
e6c13aa0ad56dc82903d054651d95e24
|
|
| BLAKE2b-256 |
81dc0714515e7f48c700abf52904d717456a57790a237668929bf5061eee3a6c
|
Provenance
The following attestation bundles were made for arcbox-0.1.2.tar.gz:
Publisher:
release-sdk-python.yml on arcboxlabs/arcbox
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arcbox-0.1.2.tar.gz -
Subject digest:
82785fa1de6f372d68fec212dea4bd8cc6c54dda8c9914205913dc5a505657f9 - Sigstore transparency entry: 2420205121
- Sigstore integration time:
-
Permalink:
arcboxlabs/arcbox@26b9a1e4c28f173af760d2f7051b62dfd59d40f0 -
Branch / Tag:
refs/tags/sdk-python-v0.1.2 - Owner: https://github.com/arcboxlabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-sdk-python.yml@26b9a1e4c28f173af760d2f7051b62dfd59d40f0 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a37878cceb8a710a1d20043e11ce030a7687b6ef5c1173b7f2af095833a5418
|
|
| MD5 |
09de396918688241f7c2c64b0ff19c03
|
|
| BLAKE2b-256 |
99c6afe77297faf6b174ac348cba6553aab906db0a14f8caf5f00d90827f04f6
|
Provenance
The following attestation bundles were made for arcbox-0.1.2-py3-none-any.whl:
Publisher:
release-sdk-python.yml on arcboxlabs/arcbox
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arcbox-0.1.2-py3-none-any.whl -
Subject digest:
2a37878cceb8a710a1d20043e11ce030a7687b6ef5c1173b7f2af095833a5418 - Sigstore transparency entry: 2420205306
- Sigstore integration time:
-
Permalink:
arcboxlabs/arcbox@26b9a1e4c28f173af760d2f7051b62dfd59d40f0 -
Branch / Tag:
refs/tags/sdk-python-v0.1.2 - Owner: https://github.com/arcboxlabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-sdk-python.yml@26b9a1e4c28f173af760d2f7051b62dfd59d40f0 -
Trigger Event:
push
-
Statement type: