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.
- arcbox-0.1.1/PKG-INFO +231 -0
- arcbox-0.1.1/README.md +218 -0
- arcbox-0.1.1/pyproject.toml +84 -0
- arcbox-0.1.1/pyproject.toml.orig +69 -0
- arcbox-0.1.1/src/arcbox/__init__.py +56 -0
- arcbox-0.1.1/src/arcbox/_async/__init__.py +5 -0
- arcbox-0.1.1/src/arcbox/_async/_client.py +235 -0
- arcbox-0.1.1/src/arcbox/_async/commands.py +322 -0
- arcbox-0.1.1/src/arcbox/_async/files.py +105 -0
- arcbox-0.1.1/src/arcbox/_async/sandbox.py +497 -0
- arcbox-0.1.1/src/arcbox/_boundary.py +56 -0
- arcbox-0.1.1/src/arcbox/_connection.py +115 -0
- arcbox-0.1.1/src/arcbox/_envelope.py +157 -0
- arcbox-0.1.1/src/arcbox/_gen/__init__.py +1 -0
- arcbox-0.1.1/src/arcbox/_gen/errors_pb2.py +42 -0
- arcbox-0.1.1/src/arcbox/_gen/errors_pb2.pyi +66 -0
- arcbox-0.1.1/src/arcbox/_gen/filesystem_pb2.py +71 -0
- arcbox-0.1.1/src/arcbox/_gen/filesystem_pb2.pyi +173 -0
- arcbox-0.1.1/src/arcbox/_gen/process_pb2.py +83 -0
- arcbox-0.1.1/src/arcbox/_gen/process_pb2.pyi +245 -0
- arcbox-0.1.1/src/arcbox/_gen/sandbox_pb2.py +126 -0
- arcbox-0.1.1/src/arcbox/_gen/sandbox_pb2.pyi +427 -0
- arcbox-0.1.1/src/arcbox/_gen/snapshot_pb2.py +70 -0
- arcbox-0.1.1/src/arcbox/_gen/snapshot_pb2.pyi +119 -0
- arcbox-0.1.1/src/arcbox/_gen/template_pb2.py +75 -0
- arcbox-0.1.1/src/arcbox/_gen/template_pb2.pyi +148 -0
- arcbox-0.1.1/src/arcbox/_sync/__init__.py +6 -0
- arcbox-0.1.1/src/arcbox/_sync/_client.py +234 -0
- arcbox-0.1.1/src/arcbox/_sync/commands.py +321 -0
- arcbox-0.1.1/src/arcbox/_sync/files.py +106 -0
- arcbox-0.1.1/src/arcbox/_sync/sandbox.py +496 -0
- arcbox-0.1.1/src/arcbox/_types.py +248 -0
- arcbox-0.1.1/src/arcbox/errors.py +253 -0
- 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
|
+
]
|