arcbox 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. arcbox-0.1.1/PKG-INFO +231 -0
  2. arcbox-0.1.1/README.md +218 -0
  3. arcbox-0.1.1/pyproject.toml +84 -0
  4. arcbox-0.1.1/pyproject.toml.orig +69 -0
  5. arcbox-0.1.1/src/arcbox/__init__.py +56 -0
  6. arcbox-0.1.1/src/arcbox/_async/__init__.py +5 -0
  7. arcbox-0.1.1/src/arcbox/_async/_client.py +235 -0
  8. arcbox-0.1.1/src/arcbox/_async/commands.py +322 -0
  9. arcbox-0.1.1/src/arcbox/_async/files.py +105 -0
  10. arcbox-0.1.1/src/arcbox/_async/sandbox.py +497 -0
  11. arcbox-0.1.1/src/arcbox/_boundary.py +56 -0
  12. arcbox-0.1.1/src/arcbox/_connection.py +115 -0
  13. arcbox-0.1.1/src/arcbox/_envelope.py +157 -0
  14. arcbox-0.1.1/src/arcbox/_gen/__init__.py +1 -0
  15. arcbox-0.1.1/src/arcbox/_gen/errors_pb2.py +42 -0
  16. arcbox-0.1.1/src/arcbox/_gen/errors_pb2.pyi +66 -0
  17. arcbox-0.1.1/src/arcbox/_gen/filesystem_pb2.py +71 -0
  18. arcbox-0.1.1/src/arcbox/_gen/filesystem_pb2.pyi +173 -0
  19. arcbox-0.1.1/src/arcbox/_gen/process_pb2.py +83 -0
  20. arcbox-0.1.1/src/arcbox/_gen/process_pb2.pyi +245 -0
  21. arcbox-0.1.1/src/arcbox/_gen/sandbox_pb2.py +126 -0
  22. arcbox-0.1.1/src/arcbox/_gen/sandbox_pb2.pyi +427 -0
  23. arcbox-0.1.1/src/arcbox/_gen/snapshot_pb2.py +70 -0
  24. arcbox-0.1.1/src/arcbox/_gen/snapshot_pb2.pyi +119 -0
  25. arcbox-0.1.1/src/arcbox/_gen/template_pb2.py +75 -0
  26. arcbox-0.1.1/src/arcbox/_gen/template_pb2.pyi +148 -0
  27. arcbox-0.1.1/src/arcbox/_sync/__init__.py +6 -0
  28. arcbox-0.1.1/src/arcbox/_sync/_client.py +234 -0
  29. arcbox-0.1.1/src/arcbox/_sync/commands.py +321 -0
  30. arcbox-0.1.1/src/arcbox/_sync/files.py +106 -0
  31. arcbox-0.1.1/src/arcbox/_sync/sandbox.py +496 -0
  32. arcbox-0.1.1/src/arcbox/_types.py +248 -0
  33. arcbox-0.1.1/src/arcbox/errors.py +253 -0
  34. arcbox-0.1.1/src/arcbox/py.typed +0 -0
arcbox-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcbox
3
+ Version: 0.1.1
4
+ Summary: ArcBox Sandbox SDK for Python — run isolated microVM sandboxes on your Mac
5
+ Author: ArcBox, Inc.
6
+ License-Expression: MIT OR Apache-2.0
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: msgspec>=0.21.1
9
+ Requires-Dist: protobuf>=7.35.1
10
+ Requires-Python: >=3.10
11
+ Project-URL: Repository, https://github.com/arcboxlabs/arcbox
12
+ Description-Content-Type: text/markdown
13
+
14
+ # arcbox
15
+
16
+ Python SDK for ArcBox sandboxes: isolated microVMs on your Mac, driven
17
+ over the local daemon's Unix socket with the Connect protocol. Requires
18
+ Python ≥ 3.10.
19
+
20
+ ```sh
21
+ uv add arcbox # or: pip install arcbox
22
+ ```
23
+
24
+ ## Hello world
25
+
26
+ With the daemon running (`abctl daemon start`):
27
+
28
+ ```python
29
+ from arcbox import Sandbox
30
+
31
+ # Local daemon over ~/.arcbox/run/arcbox.sock — zero config.
32
+ with Sandbox.create("", ttl=300) as sandbox:
33
+ sandbox.files.write_text("/tmp/hello.txt", "hello from arcbox\n")
34
+
35
+ check = sandbox.commands.run(["/bin/cat", "/tmp/hello.txt"])
36
+ print(check.expect().stdout, "→ exit", check.exit_code)
37
+
38
+ job = sandbox.commands.run("for i in 1 2 3; do echo line$i; done", background=True)
39
+ for chunk in job.output:
40
+ print(chunk.data.decode(), end="")
41
+ print("background job exited", job.wait_for_exit().exit_code)
42
+ # context exit: sandbox killed, nothing leaked
43
+ ```
44
+
45
+ Async is a first-class mirror (`AsyncSandbox`, `async with`, `async for`):
46
+
47
+ ```python
48
+ from arcbox import AsyncSandbox
49
+
50
+
51
+ async def main() -> None:
52
+ sandbox = await AsyncSandbox.create("", ttl=300)
53
+ async with sandbox:
54
+ await sandbox.files.write_text("/tmp/hello.txt", "hello from arcbox\n")
55
+ result = await sandbox.commands.run(["/bin/cat", "/tmp/hello.txt"])
56
+ print(result.expect().stdout)
57
+ ```
58
+
59
+ Non-zero exit is data (`result.exit_code`), never an exception —
60
+ `result.expect()` (or `run(..., check=True)`, `subprocess.run`-style) is
61
+ the opt-in raise. Every daemon error maps to a typed class in
62
+ `arcbox.errors` (`SandboxNotFoundError`, `CapabilityError`,
63
+ `ConnectionFailedError`, ...) carrying a machine-readable `code`, an
64
+ actionable `suggestion`, and the failed `operation`. Time arguments are
65
+ seconds (floats) everywhere.
66
+
67
+ ## Connection
68
+
69
+ Resolution order: explicit option > environment > default.
70
+
71
+ | Environment | Meaning |
72
+ | ---------------- | --------------------------------------------------------------------------------------------- |
73
+ | `ARCBOX_SOCKET` | daemon Unix socket (default `$ARCBOX_DATA_DIR/run/arcbox.sock`; data dir default `~/.arcbox`, or `~/.arcbox-dev` under `ARCBOX_PROFILE=development`) |
74
+ | `ARCBOX_API_URL` | remote daemon / cloud front door; setting it selects the remote tier (reserved, CORE-63) |
75
+ | `ARCBOX_API_KEY` | bearer credential, attached as `Authorization` when set; unused by the local daemon |
76
+
77
+ Every entry point takes a `connection=Connection(...)` slot
78
+ (`socket_path` / `api_url` / `api_key` / `request_timeout` / injected
79
+ `http_client` for mocking — pass an `httpx.Client` to the sync surface,
80
+ an `httpx.AsyncClient` to the async one).
81
+
82
+ The `Sandbox` / `AsyncSandbox` classmethods resolve a hidden connection
83
+ per call, and the returned handle closes its HTTP client on context
84
+ exit. Long-lived programs should hold an `ArcBox` / `AsyncArcBox`
85
+ instead: it is a context manager (or call `.close()` / `.aclose()`),
86
+ and every handle it creates shares its client. An injected
87
+ `http_client` always belongs to the caller and is never closed by the
88
+ SDK.
89
+
90
+ ## Development
91
+
92
+ Inside the arcbox repo (`sdk/python`):
93
+
94
+ ```sh
95
+ uv sync # create .venv from uv.lock
96
+ uv run python scripts/gen_proto.py # regenerate src/arcbox/_gen from ../../rpc/arcbox-protocol/proto
97
+ uv run python scripts/gen_sync.py # regenerate src/arcbox/_sync from src/arcbox/_async
98
+ uv run ruff check . && uv run ruff format --check .
99
+ uv run pyright
100
+ uv run pytest # includes the sync-tree lockstep + parity checks
101
+ ```
102
+
103
+ Generated code under `src/arcbox/_gen/` is committed and is **never**
104
+ exported from the package — public shapes are hand-written and mapped
105
+ at the transport boundary.
106
+
107
+ The async tree (`src/arcbox/_async/`) is the source of truth; the sync
108
+ tree (`src/arcbox/_sync/`) is generated from it by an unasync token
109
+ transform and committed. Edit only the async tree, then rerun
110
+ `scripts/gen_sync.py`. Lockstep is CI-enforced twice: the transform is
111
+ rerun and diffed (`scripts/gen_sync.py --check`, also wired into
112
+ pytest), and a parity test asserts identical public surfaces modulo
113
+ async markers.
114
+
115
+ Optional pre-commit hooks (scoped to `sdk/python`), via
116
+ [prek](https://github.com/j178/prek) or classic pre-commit:
117
+
118
+ ```sh
119
+ prek install -c sdk/python/prek.yaml
120
+ ```
121
+
122
+ The end-to-end hello-world loop runs only against a live daemon and is
123
+ opt-in:
124
+
125
+ ```sh
126
+ ARCBOX_SDK_E2E=1 uv run pytest tests/test_e2e.py
127
+ ```
128
+
129
+ ## Toolchain notes
130
+
131
+ - **uv** is the package/project manager (`uv_build` backend, `uv.lock`
132
+ committed). Publishing is CI-only: `uv build`, then upload via
133
+ `pypa/gh-action-pypi-publish` under PyPI trusted publishing (OIDC,
134
+ with PEP 740 attestations — `uv publish` emits none, astral-sh/uv#15618)
135
+ — see [Releasing](#releasing); no `UV_PUBLISH_TOKEN` anywhere.
136
+ - **ruff** is both linter and formatter (`E,F,W,I,UP,B,SIM,RUF`).
137
+ - **pyright** (strict) is the authoritative type checker. Evaluated
138
+ alternatives (2026-08): **ty** 0.0.65 reports 16 false positives here
139
+ (all `unresolved-attribute` on protobuf generated-module members) —
140
+ kept in dev-deps for `uv run ty check`, may replace pyright when it
141
+ stabilizes; **pyrefly** 1.2.0 passes cleanly (it imports the pyright
142
+ config) and serves as an informational second opinion — one
143
+ authoritative checker avoids double-suppression drift.
144
+ - **msgspec** parses the SDK's one JSON seam — Connect error bodies and
145
+ `EndStreamResponse` frames — as typed, validated Structs at the
146
+ untrusted-input boundary (chosen for the typed decoding, not speed).
147
+ - Message types are upstream **protobuf** runtime code generated by the
148
+ protoc bundled with grpcio-tools (dev-dep); the bundled protoc version
149
+ matches the pinned runtime.
150
+ - A future native-acceleration path (if profiling ever demands one) is a
151
+ **maturin**/PyO3 extension crate in this repo's workspace; nothing in
152
+ the current SDK needs it.
153
+
154
+ TODO(CI): wire the gates above into `.github/workflows` as an
155
+ `sdk-python` job (follow-up; workflow changes are intentionally not part
156
+ of this branch).
157
+
158
+ ## Releasing
159
+
160
+ The SDK is a release-please component (`sdk-python` in
161
+ `release-please-config.json`), released on its own cadence, independent
162
+ of the main arcbox release train:
163
+
164
+ 1. Conventional commits touching `sdk/python` accumulate on `master`.
165
+ 2. release-please maintains a dedicated release PR for the component
166
+ (separate from the root, fleet-agent, and sdk-typescript PRs) that
167
+ bumps the `pyproject.toml` version and updates `CHANGELOG.md`.
168
+ 3. Merging that PR creates the GitHub release and the tag
169
+ `sdk-python-vX.Y.Z` (same convention as `sdk-typescript-vX.Y.Z`).
170
+ 4. The tag is what a PyPI publish workflow
171
+ (`.github/workflows/release-sdk-python.yml`) triggers on: it checks
172
+ out the tag's tree, re-runs the full gate suite (`ruff check`, `ruff
173
+ format --check`, `pyright`, `pytest`, `gen_sync.py --check`), builds
174
+ with `uv build`, and publishes via `pypa/gh-action-pypi-publish` under
175
+ [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC,
176
+ PEP 740 attestations included) —
177
+ tokenless: the job's `id-token: write` permission is exchanged for a
178
+ short-lived PyPI credential. The job skips cleanly if the version is
179
+ already on PyPI, so a re-dispatch never fails on an
180
+ already-published release.
181
+
182
+ > A tag minted while the publish workflow was absent (or a tag whose
183
+ > run failed) is **not replayed** by a later push — re-dispatch the
184
+ > workflow against the existing tag instead (it takes the tag as a
185
+ > `workflow_dispatch` input), never publish by hand: a hand publish
186
+ > would need an API token, which the pending-publisher bootstrap below
187
+ > exists to avoid.
188
+
189
+ One-time bootstrap — unlike npm, PyPI supports [pending
190
+ publishers](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/):
191
+ the trusted publisher is registered *before* the first upload and CI
192
+ does the first publish, so there is no local bootstrap publish and no
193
+ API token at any point:
194
+
195
+ 1. On pypi.org → account → Publishing → "Add a new pending publisher"
196
+ (GitHub): PyPI project name `arcbox`, owner `arcboxlabs`, repository
197
+ `arcbox`, workflow filename `release-sdk-python.yml`, environment
198
+ left empty. Empty is deliberate: PyPI calls the environment
199
+ ["optional but strongly
200
+ recommended"](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/),
201
+ but its value is the [protection
202
+ rules](https://docs.pypi.org/trusted-publishers/security-model/) an
203
+ environment can carry (required reviewers gating a publish), and
204
+ this repo configures none — naming one today would add a label, not
205
+ a gate. Add the environment and a matching `environment:` key in the
206
+ workflow together with the reviewer rule, not before.
207
+ 2. The first tag-triggered run then creates the `arcbox` project on
208
+ PyPI as it publishes, and the pending publisher becomes the
209
+ project's regular trusted publisher.
210
+
211
+ A pending publisher does not hold the name: PyPI ["does not create a
212
+ project or reserve a project's name until it is actually used to
213
+ publish"](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/),
214
+ and if someone else registers `arcbox` first the pending publisher is
215
+ invalidated. `arcbox` is short, generic, and still unclaimed — do the
216
+ first publish promptly after registering, and re-check the name is free
217
+ if the bootstrap has been sitting for a while.
218
+
219
+ ## Status
220
+
221
+ Phase 1 of CORE-58 — the hello-world closed loop: `Sandbox` /
222
+ `AsyncSandbox` create/connect/list, `kill`/`pause`/`info` (`pause` and
223
+ the paused-sandbox reconnect path are wire-complete but reject with an
224
+ unimplemented error until the daemon's CORE-21 lands), `commands.run`
225
+ (foreground result + background handle with streamed output,
226
+ `wait_for_exit`, `kill`), and whole-file `files` read/write. Deferred:
227
+ PTY, `ports`, `wait_for_port`/`wait_for_log`, stdin, filesystem path
228
+ verbs (stat/list/mkdir/...), `Template` statics, `events()`,
229
+ `set_lifecycle`, the capabilities handshake, and the SDK-side default
230
+ idle-reaping policy (design decision 4 — applied once the daemon
231
+ enforces the lifecycle knobs).
arcbox-0.1.1/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # arcbox
2
+
3
+ Python SDK for ArcBox sandboxes: isolated microVMs on your Mac, driven
4
+ over the local daemon's Unix socket with the Connect protocol. Requires
5
+ Python ≥ 3.10.
6
+
7
+ ```sh
8
+ uv add arcbox # or: pip install arcbox
9
+ ```
10
+
11
+ ## Hello world
12
+
13
+ With the daemon running (`abctl daemon start`):
14
+
15
+ ```python
16
+ from arcbox import Sandbox
17
+
18
+ # Local daemon over ~/.arcbox/run/arcbox.sock — zero config.
19
+ with Sandbox.create("", ttl=300) as sandbox:
20
+ sandbox.files.write_text("/tmp/hello.txt", "hello from arcbox\n")
21
+
22
+ check = sandbox.commands.run(["/bin/cat", "/tmp/hello.txt"])
23
+ print(check.expect().stdout, "→ exit", check.exit_code)
24
+
25
+ job = sandbox.commands.run("for i in 1 2 3; do echo line$i; done", background=True)
26
+ for chunk in job.output:
27
+ print(chunk.data.decode(), end="")
28
+ print("background job exited", job.wait_for_exit().exit_code)
29
+ # context exit: sandbox killed, nothing leaked
30
+ ```
31
+
32
+ Async is a first-class mirror (`AsyncSandbox`, `async with`, `async for`):
33
+
34
+ ```python
35
+ from arcbox import AsyncSandbox
36
+
37
+
38
+ async def main() -> None:
39
+ sandbox = await AsyncSandbox.create("", ttl=300)
40
+ async with sandbox:
41
+ await sandbox.files.write_text("/tmp/hello.txt", "hello from arcbox\n")
42
+ result = await sandbox.commands.run(["/bin/cat", "/tmp/hello.txt"])
43
+ print(result.expect().stdout)
44
+ ```
45
+
46
+ Non-zero exit is data (`result.exit_code`), never an exception —
47
+ `result.expect()` (or `run(..., check=True)`, `subprocess.run`-style) is
48
+ the opt-in raise. Every daemon error maps to a typed class in
49
+ `arcbox.errors` (`SandboxNotFoundError`, `CapabilityError`,
50
+ `ConnectionFailedError`, ...) carrying a machine-readable `code`, an
51
+ actionable `suggestion`, and the failed `operation`. Time arguments are
52
+ seconds (floats) everywhere.
53
+
54
+ ## Connection
55
+
56
+ Resolution order: explicit option > environment > default.
57
+
58
+ | Environment | Meaning |
59
+ | ---------------- | --------------------------------------------------------------------------------------------- |
60
+ | `ARCBOX_SOCKET` | daemon Unix socket (default `$ARCBOX_DATA_DIR/run/arcbox.sock`; data dir default `~/.arcbox`, or `~/.arcbox-dev` under `ARCBOX_PROFILE=development`) |
61
+ | `ARCBOX_API_URL` | remote daemon / cloud front door; setting it selects the remote tier (reserved, CORE-63) |
62
+ | `ARCBOX_API_KEY` | bearer credential, attached as `Authorization` when set; unused by the local daemon |
63
+
64
+ Every entry point takes a `connection=Connection(...)` slot
65
+ (`socket_path` / `api_url` / `api_key` / `request_timeout` / injected
66
+ `http_client` for mocking — pass an `httpx.Client` to the sync surface,
67
+ an `httpx.AsyncClient` to the async one).
68
+
69
+ The `Sandbox` / `AsyncSandbox` classmethods resolve a hidden connection
70
+ per call, and the returned handle closes its HTTP client on context
71
+ exit. Long-lived programs should hold an `ArcBox` / `AsyncArcBox`
72
+ instead: it is a context manager (or call `.close()` / `.aclose()`),
73
+ and every handle it creates shares its client. An injected
74
+ `http_client` always belongs to the caller and is never closed by the
75
+ SDK.
76
+
77
+ ## Development
78
+
79
+ Inside the arcbox repo (`sdk/python`):
80
+
81
+ ```sh
82
+ uv sync # create .venv from uv.lock
83
+ uv run python scripts/gen_proto.py # regenerate src/arcbox/_gen from ../../rpc/arcbox-protocol/proto
84
+ uv run python scripts/gen_sync.py # regenerate src/arcbox/_sync from src/arcbox/_async
85
+ uv run ruff check . && uv run ruff format --check .
86
+ uv run pyright
87
+ uv run pytest # includes the sync-tree lockstep + parity checks
88
+ ```
89
+
90
+ Generated code under `src/arcbox/_gen/` is committed and is **never**
91
+ exported from the package — public shapes are hand-written and mapped
92
+ at the transport boundary.
93
+
94
+ The async tree (`src/arcbox/_async/`) is the source of truth; the sync
95
+ tree (`src/arcbox/_sync/`) is generated from it by an unasync token
96
+ transform and committed. Edit only the async tree, then rerun
97
+ `scripts/gen_sync.py`. Lockstep is CI-enforced twice: the transform is
98
+ rerun and diffed (`scripts/gen_sync.py --check`, also wired into
99
+ pytest), and a parity test asserts identical public surfaces modulo
100
+ async markers.
101
+
102
+ Optional pre-commit hooks (scoped to `sdk/python`), via
103
+ [prek](https://github.com/j178/prek) or classic pre-commit:
104
+
105
+ ```sh
106
+ prek install -c sdk/python/prek.yaml
107
+ ```
108
+
109
+ The end-to-end hello-world loop runs only against a live daemon and is
110
+ opt-in:
111
+
112
+ ```sh
113
+ ARCBOX_SDK_E2E=1 uv run pytest tests/test_e2e.py
114
+ ```
115
+
116
+ ## Toolchain notes
117
+
118
+ - **uv** is the package/project manager (`uv_build` backend, `uv.lock`
119
+ committed). Publishing is CI-only: `uv build`, then upload via
120
+ `pypa/gh-action-pypi-publish` under PyPI trusted publishing (OIDC,
121
+ with PEP 740 attestations — `uv publish` emits none, astral-sh/uv#15618)
122
+ — see [Releasing](#releasing); no `UV_PUBLISH_TOKEN` anywhere.
123
+ - **ruff** is both linter and formatter (`E,F,W,I,UP,B,SIM,RUF`).
124
+ - **pyright** (strict) is the authoritative type checker. Evaluated
125
+ alternatives (2026-08): **ty** 0.0.65 reports 16 false positives here
126
+ (all `unresolved-attribute` on protobuf generated-module members) —
127
+ kept in dev-deps for `uv run ty check`, may replace pyright when it
128
+ stabilizes; **pyrefly** 1.2.0 passes cleanly (it imports the pyright
129
+ config) and serves as an informational second opinion — one
130
+ authoritative checker avoids double-suppression drift.
131
+ - **msgspec** parses the SDK's one JSON seam — Connect error bodies and
132
+ `EndStreamResponse` frames — as typed, validated Structs at the
133
+ untrusted-input boundary (chosen for the typed decoding, not speed).
134
+ - Message types are upstream **protobuf** runtime code generated by the
135
+ protoc bundled with grpcio-tools (dev-dep); the bundled protoc version
136
+ matches the pinned runtime.
137
+ - A future native-acceleration path (if profiling ever demands one) is a
138
+ **maturin**/PyO3 extension crate in this repo's workspace; nothing in
139
+ the current SDK needs it.
140
+
141
+ TODO(CI): wire the gates above into `.github/workflows` as an
142
+ `sdk-python` job (follow-up; workflow changes are intentionally not part
143
+ of this branch).
144
+
145
+ ## Releasing
146
+
147
+ The SDK is a release-please component (`sdk-python` in
148
+ `release-please-config.json`), released on its own cadence, independent
149
+ of the main arcbox release train:
150
+
151
+ 1. Conventional commits touching `sdk/python` accumulate on `master`.
152
+ 2. release-please maintains a dedicated release PR for the component
153
+ (separate from the root, fleet-agent, and sdk-typescript PRs) that
154
+ bumps the `pyproject.toml` version and updates `CHANGELOG.md`.
155
+ 3. Merging that PR creates the GitHub release and the tag
156
+ `sdk-python-vX.Y.Z` (same convention as `sdk-typescript-vX.Y.Z`).
157
+ 4. The tag is what a PyPI publish workflow
158
+ (`.github/workflows/release-sdk-python.yml`) triggers on: it checks
159
+ out the tag's tree, re-runs the full gate suite (`ruff check`, `ruff
160
+ format --check`, `pyright`, `pytest`, `gen_sync.py --check`), builds
161
+ with `uv build`, and publishes via `pypa/gh-action-pypi-publish` under
162
+ [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC,
163
+ PEP 740 attestations included) —
164
+ tokenless: the job's `id-token: write` permission is exchanged for a
165
+ short-lived PyPI credential. The job skips cleanly if the version is
166
+ already on PyPI, so a re-dispatch never fails on an
167
+ already-published release.
168
+
169
+ > A tag minted while the publish workflow was absent (or a tag whose
170
+ > run failed) is **not replayed** by a later push — re-dispatch the
171
+ > workflow against the existing tag instead (it takes the tag as a
172
+ > `workflow_dispatch` input), never publish by hand: a hand publish
173
+ > would need an API token, which the pending-publisher bootstrap below
174
+ > exists to avoid.
175
+
176
+ One-time bootstrap — unlike npm, PyPI supports [pending
177
+ publishers](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/):
178
+ the trusted publisher is registered *before* the first upload and CI
179
+ does the first publish, so there is no local bootstrap publish and no
180
+ API token at any point:
181
+
182
+ 1. On pypi.org → account → Publishing → "Add a new pending publisher"
183
+ (GitHub): PyPI project name `arcbox`, owner `arcboxlabs`, repository
184
+ `arcbox`, workflow filename `release-sdk-python.yml`, environment
185
+ left empty. Empty is deliberate: PyPI calls the environment
186
+ ["optional but strongly
187
+ recommended"](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/),
188
+ but its value is the [protection
189
+ rules](https://docs.pypi.org/trusted-publishers/security-model/) an
190
+ environment can carry (required reviewers gating a publish), and
191
+ this repo configures none — naming one today would add a label, not
192
+ a gate. Add the environment and a matching `environment:` key in the
193
+ workflow together with the reviewer rule, not before.
194
+ 2. The first tag-triggered run then creates the `arcbox` project on
195
+ PyPI as it publishes, and the pending publisher becomes the
196
+ project's regular trusted publisher.
197
+
198
+ A pending publisher does not hold the name: PyPI ["does not create a
199
+ project or reserve a project's name until it is actually used to
200
+ publish"](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/),
201
+ and if someone else registers `arcbox` first the pending publisher is
202
+ invalidated. `arcbox` is short, generic, and still unclaimed — do the
203
+ first publish promptly after registering, and re-check the name is free
204
+ if the bootstrap has been sitting for a while.
205
+
206
+ ## Status
207
+
208
+ Phase 1 of CORE-58 — the hello-world closed loop: `Sandbox` /
209
+ `AsyncSandbox` create/connect/list, `kill`/`pause`/`info` (`pause` and
210
+ the paused-sandbox reconnect path are wire-complete but reject with an
211
+ unimplemented error until the daemon's CORE-21 lands), `commands.run`
212
+ (foreground result + background handle with streamed output,
213
+ `wait_for_exit`, `kill`), and whole-file `files` read/write. Deferred:
214
+ PTY, `ports`, `wait_for_port`/`wait_for_log`, stdin, filesystem path
215
+ verbs (stat/list/mkdir/...), `Template` statics, `events()`,
216
+ `set_lifecycle`, the capabilities handshake, and the SDK-side default
217
+ idle-reaping policy (design decision 4 — applied once the daemon
218
+ enforces the lifecycle knobs).
@@ -0,0 +1,84 @@
1
+ [project]
2
+ name = "arcbox"
3
+ version = "0.1.1"
4
+ description = "ArcBox Sandbox SDK for Python — run isolated microVM sandboxes on your Mac"
5
+ readme = "README.md"
6
+ license = "MIT OR Apache-2.0"
7
+ requires-python = ">=3.10"
8
+ dependencies = [
9
+ "httpx>=0.28.1",
10
+ "msgspec>=0.21.1",
11
+ "protobuf>=7.35.1",
12
+ ]
13
+
14
+ [[project.authors]]
15
+ name = "ArcBox, Inc."
16
+
17
+ [project.urls]
18
+ Repository = "https://github.com/arcboxlabs/arcbox"
19
+
20
+ [build-system]
21
+ requires = ["uv_build>=0.12.1,<0.13.0"]
22
+ build-backend = "uv_build"
23
+
24
+ [tool.ruff]
25
+ target-version = "py310"
26
+ line-length = 100
27
+ extend-exclude = ["src/arcbox/_gen"]
28
+
29
+ [tool.ruff.lint]
30
+ select = [
31
+ "E",
32
+ "F",
33
+ "W",
34
+ "I",
35
+ "UP",
36
+ "B",
37
+ "SIM",
38
+ "RUF",
39
+ ]
40
+
41
+ [tool.ruff.lint.per-file-ignores]
42
+ "src/arcbox/_sync/*" = ["UP028"]
43
+
44
+ [tool.pyright]
45
+ include = [
46
+ "src",
47
+ "tests",
48
+ "scripts",
49
+ ]
50
+ exclude = [
51
+ "src/arcbox/_gen",
52
+ ".venv",
53
+ ]
54
+ typeCheckingMode = "strict"
55
+
56
+ [[tool.pyright.executionEnvironments]]
57
+ root = "scripts"
58
+ reportMissingTypeStubs = false
59
+ reportUnknownMemberType = false
60
+ reportUnknownVariableType = false
61
+ reportUnknownArgumentType = false
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+
66
+ [tool.ty.src]
67
+ include = [
68
+ "src",
69
+ "tests",
70
+ "scripts",
71
+ ]
72
+ exclude = ["src/arcbox/_gen"]
73
+
74
+ [dependency-groups]
75
+ dev = [
76
+ "anyio>=4.14.2",
77
+ "grpcio-tools>=1.83.0",
78
+ "pyrefly>=1.2.0",
79
+ "pyright>=1.1.411",
80
+ "pytest>=9.1.1",
81
+ "ruff>=0.16.1",
82
+ "ty>=0.0.65",
83
+ "unasync>=0.6.0",
84
+ ]
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "arcbox"
3
+ version = "0.1.1"
4
+ description = "ArcBox Sandbox SDK for Python — run isolated microVM sandboxes on your Mac"
5
+ readme = "README.md"
6
+ authors = [{ name = "ArcBox, Inc." }]
7
+ license = "MIT OR Apache-2.0"
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "httpx>=0.28.1",
11
+ "msgspec>=0.21.1",
12
+ "protobuf>=7.35.1",
13
+ ]
14
+
15
+ [project.urls]
16
+ Repository = "https://github.com/arcboxlabs/arcbox"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.12.1,<0.13.0"]
20
+ build-backend = "uv_build"
21
+
22
+ [tool.ruff]
23
+ target-version = "py310"
24
+ line-length = 100
25
+ extend-exclude = ["src/arcbox/_gen"]
26
+
27
+ [tool.ruff.lint]
28
+ select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
29
+
30
+ [tool.ruff.lint.per-file-ignores]
31
+ # The sync tree is a mechanical unasync mirror: `async for … yield` has
32
+ # no `yield from` form, so its transform cannot satisfy UP028.
33
+ # gen_sync.py mirrors this ignore for its scratch directory, which sits
34
+ # outside the project root where this pattern cannot anchor.
35
+ "src/arcbox/_sync/*" = ["UP028"]
36
+
37
+ [tool.pyright]
38
+ include = ["src", "tests", "scripts"]
39
+ exclude = ["src/arcbox/_gen", ".venv"]
40
+ typeCheckingMode = "strict"
41
+
42
+ # The codegen scripts drive tools that ship no type information
43
+ # (grpcio-tools' protoc entry point, unasync); only the unknown-type
44
+ # diagnostics that follow from those imports are relaxed there.
45
+ [[tool.pyright.executionEnvironments]]
46
+ root = "scripts"
47
+ reportMissingTypeStubs = false
48
+ reportUnknownMemberType = false
49
+ reportUnknownVariableType = false
50
+ reportUnknownArgumentType = false
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+
55
+ [tool.ty.src]
56
+ include = ["src", "tests", "scripts"]
57
+ exclude = ["src/arcbox/_gen"]
58
+
59
+ [dependency-groups]
60
+ dev = [
61
+ "anyio>=4.14.2",
62
+ "grpcio-tools>=1.83.0",
63
+ "pyrefly>=1.2.0",
64
+ "pyright>=1.1.411",
65
+ "pytest>=9.1.1",
66
+ "ruff>=0.16.1",
67
+ "ty>=0.0.65",
68
+ "unasync>=0.6.0",
69
+ ]
@@ -0,0 +1,56 @@
1
+ """arcbox — run isolated microVM sandboxes on the local ArcBox daemon.
2
+
3
+ Two mirrored surfaces: the sync classes (`Sandbox`, `ArcBox`, ...) and
4
+ their `Async*` counterparts. Public shapes are hand-written and mapped
5
+ at the transport boundary; everything under `arcbox._gen` is generated
6
+ wire code and is deliberately NOT exported. The error hierarchy lives
7
+ in :mod:`arcbox.errors`.
8
+ """
9
+
10
+ from arcbox._async._client import AsyncConnectClient
11
+ from arcbox._async.commands import AsyncCommandHandle, AsyncCommands, AsyncOutputStream
12
+ from arcbox._async.files import AsyncFiles
13
+ from arcbox._async.sandbox import AsyncArcBox, AsyncSandbox
14
+ from arcbox._connection import Connection
15
+ from arcbox._sync._client import ConnectClient
16
+ from arcbox._sync.commands import CommandHandle, Commands, OutputStream
17
+ from arcbox._sync.files import Files
18
+ from arcbox._sync.sandbox import ArcBox, Sandbox
19
+ from arcbox._types import (
20
+ MAX_FILE_BYTES,
21
+ CommandResult,
22
+ IdlePolicy,
23
+ OutputChannel,
24
+ OutputChunk,
25
+ SandboxInfo,
26
+ SandboxState,
27
+ SandboxSummary,
28
+ SignalName,
29
+ )
30
+
31
+ __all__ = [
32
+ "MAX_FILE_BYTES",
33
+ "ArcBox",
34
+ "AsyncArcBox",
35
+ "AsyncCommandHandle",
36
+ "AsyncCommands",
37
+ "AsyncConnectClient",
38
+ "AsyncFiles",
39
+ "AsyncOutputStream",
40
+ "AsyncSandbox",
41
+ "CommandHandle",
42
+ "CommandResult",
43
+ "Commands",
44
+ "ConnectClient",
45
+ "Connection",
46
+ "Files",
47
+ "IdlePolicy",
48
+ "OutputChannel",
49
+ "OutputChunk",
50
+ "OutputStream",
51
+ "Sandbox",
52
+ "SandboxInfo",
53
+ "SandboxState",
54
+ "SandboxSummary",
55
+ "SignalName",
56
+ ]
@@ -0,0 +1,5 @@
1
+ """One flavor of the SDK surface.
2
+
3
+ The async core lives in `arcbox/_async`; `arcbox/_sync` is generated
4
+ from it by scripts/gen_sync.py — edit only the async tree.
5
+ """