sandboxer-sdk 0.1.0__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.
- sandboxer_sdk-0.1.0/.gitignore +8 -0
- sandboxer_sdk-0.1.0/CHANGELOG.md +14 -0
- sandboxer_sdk-0.1.0/LICENSE +21 -0
- sandboxer_sdk-0.1.0/PKG-INFO +351 -0
- sandboxer_sdk-0.1.0/README.md +316 -0
- sandboxer_sdk-0.1.0/pyproject.toml +80 -0
- sandboxer_sdk-0.1.0/scripts/generate.py +739 -0
- sandboxer_sdk-0.1.0/scripts/unasync.py +122 -0
- sandboxer_sdk-0.1.0/src/sandboxer/__init__.py +128 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_config.py +45 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_generated_api.py +6100 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_helpers_async.py +481 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_helpers_sync.py +481 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_results.py +104 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_retry.py +97 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_sandbox_async.py +906 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_sandbox_sync.py +908 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_streaming.py +108 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_transport_async.py +232 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_transport_sync.py +234 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_types.py +47 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_utils.py +128 -0
- sandboxer_sdk-0.1.0/src/sandboxer/_version.py +1 -0
- sandboxer_sdk-0.1.0/src/sandboxer/aio.py +61 -0
- sandboxer_sdk-0.1.0/src/sandboxer/errors.py +256 -0
- sandboxer_sdk-0.1.0/src/sandboxer/models.py +3666 -0
- sandboxer_sdk-0.1.0/src/sandboxer/py.typed +0 -0
- sandboxer_sdk-0.1.0/src/sandboxer/webhooks.py +198 -0
- sandboxer_sdk-0.1.0/tests/conftest.py +147 -0
- sandboxer_sdk-0.1.0/tests/test_api.py +242 -0
- sandboxer_sdk-0.1.0/tests/test_async.py +164 -0
- sandboxer_sdk-0.1.0/tests/test_docs_snippets.py +44 -0
- sandboxer_sdk-0.1.0/tests/test_errors.py +120 -0
- sandboxer_sdk-0.1.0/tests/test_retries.py +178 -0
- sandboxer_sdk-0.1.0/tests/test_sandbox.py +740 -0
- sandboxer_sdk-0.1.0/tests/test_streaming.py +39 -0
- sandboxer_sdk-0.1.0/tests/test_webhooks.py +97 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `sandboxer-sdk` (imported as `sandboxer`). The Python and TypeScript SDKs are released together under one version.
|
|
4
|
+
|
|
5
|
+
## 0.1.0
|
|
6
|
+
|
|
7
|
+
First public release.
|
|
8
|
+
|
|
9
|
+
- `Sandboxer` / `Sandbox` (sync) and `AsyncSandboxer` / `AsyncSandbox` (asyncio): create (waiting until ready),
|
|
10
|
+
exec, streaming exec, background commands, files, hosting, lifecycle (stop, resume, fork, snapshot), agents and events.
|
|
11
|
+
- `SandboxerApi` / `AsyncSandboxerApi`: one typed method for each of the 93 operations of the public API v1.
|
|
12
|
+
- Typed pydantic models, envelope-aware errors, automatic retries with jittered backoff and automatic
|
|
13
|
+
`Idempotency-Key` on create and fork.
|
|
14
|
+
- Webhook signature verification.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sandboxer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sandboxer-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Sandboxer: cloud sandboxes for AI agents and humans.
|
|
5
|
+
Project-URL: Homepage, https://sandboxer.com
|
|
6
|
+
Project-URL: Documentation, https://sandboxer.com/docs/sdk-python
|
|
7
|
+
Author-email: Sandboxer <support@sandboxer.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,ai,cloud,microvm,sandbox,sandboxer,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx<1,>=0.27
|
|
25
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: datamodel-code-generator==0.83.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: pyyaml>=6; extra == 'dev'
|
|
32
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff==0.17.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# sandboxer-sdk — Python SDK
|
|
37
|
+
|
|
38
|
+
Python client for [Sandboxer](https://sandboxer.com): persistent cloud sandboxes (Ubuntu microVMs with root,
|
|
39
|
+
Docker, SSH, HTTPS port hosting, stop/resume, forks, snapshots, a streamed desktop and integrated coding agents).
|
|
40
|
+
|
|
41
|
+
- **High-level API** — `Sandboxer` / `Sandbox` (sync) and `AsyncSandboxer` / `AsyncSandbox` (asyncio).
|
|
42
|
+
- **Low-level API** — `SandboxerApi` / `AsyncSandboxerApi`: one typed method for every operation of the public
|
|
43
|
+
API (`/api/v1`), wire-compatible with boat's API v1 so boat-sdk code ports by renaming imports.
|
|
44
|
+
- Typed pydantic models, envelope-aware errors, automatic retries with jittered backoff, automatic
|
|
45
|
+
`Idempotency-Key` on create/fork, NDJSON command streaming, event streaming (polling and SSE) and webhook
|
|
46
|
+
signature verification.
|
|
47
|
+
|
|
48
|
+
Requires Python 3.10+.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install sandboxer-sdk # imported as `import sandboxer`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configure
|
|
55
|
+
|
|
56
|
+
| Option | Default |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `api_key` | `SANDBOXER_API_KEY`, then `SANDBOXER_TOKEN` (the per-sandbox key injected inside sandboxes) |
|
|
59
|
+
| `base_url` | `SANDBOXER_API_URL`, else `https://sandboxer.com/api/v1` |
|
|
60
|
+
| `org` | `SANDBOXER_ORG`; sent as `X-Sandboxer-Org` (billing wallet) |
|
|
61
|
+
| `timeout` | `60` seconds (`None` disables it) |
|
|
62
|
+
| `max_retries` | `3` |
|
|
63
|
+
| `http_client` | your own `httpx.Client` / `httpx.AsyncClient` (not closed by the SDK) |
|
|
64
|
+
| `default_headers` | extra headers for every request |
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from sandboxer import Sandboxer
|
|
68
|
+
|
|
69
|
+
client = Sandboxer() # reads SANDBOXER_API_KEY
|
|
70
|
+
client = Sandboxer(api_key="sbx_live_...", org="team_acme", timeout=30)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Keep API keys, secrets, SSH keys and desktop/hosted URLs (they can carry access tokens) out of your logs.
|
|
74
|
+
|
|
75
|
+
## Quickstart
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from sandboxer import Sandboxer
|
|
79
|
+
|
|
80
|
+
with Sandboxer() as client:
|
|
81
|
+
sandbox = client.create(type="small", ttl_seconds=1800) # waits until it is ready
|
|
82
|
+
|
|
83
|
+
result = sandbox.exec("python3 --version")
|
|
84
|
+
print(result.exit_code, result.stdout)
|
|
85
|
+
|
|
86
|
+
sandbox.files.write("app/main.py", "print('hi')\n")
|
|
87
|
+
print(sandbox.files.read("app/main.py"))
|
|
88
|
+
|
|
89
|
+
sandbox.start("python3 -m http.server 8000", cwd="app") # detached
|
|
90
|
+
print(sandbox.host(8000, public=True).url) # https://<slug>-8000.box.sandboxer.com
|
|
91
|
+
|
|
92
|
+
sandbox.stop() # archive (resume later)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`Sandboxer.create(...)`, `Sandboxer.get(...)` and `Sandboxer.list(...)` also work on the class itself, using a
|
|
96
|
+
default client configured from the environment:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
sandbox = Sandboxer.create(ttl_seconds=600)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Create options
|
|
103
|
+
|
|
104
|
+
`client.create(type=, ttl_seconds=, env=, no_env=, from_=, setup_script=, environment=, snapshots=, monitoring=,
|
|
105
|
+
fail_fast=, org=, name=, idempotency_key=, wait=True, wait_for_setup=False, timeout=300, on_exit=None)`
|
|
106
|
+
|
|
107
|
+
- `ttl_seconds=None` disables auto-stop (omitting it uses the server default of one hour).
|
|
108
|
+
- `from_="web-stack"` starts from a named snapshot (or a `bsh_...` share code).
|
|
109
|
+
- `no_env=True` withholds every account secret — use it for sandboxes you hand to your own users.
|
|
110
|
+
- `wait_for_setup=True` also waits for `setup_script` and raises if it failed.
|
|
111
|
+
- Waiting raises `SandboxStateError` if the sandbox ends in `error`/`cancelled`, `WaitTimeoutError` on timeout.
|
|
112
|
+
|
|
113
|
+
## Commands
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
r = sandbox.exec(["ls", "-la", "my dir"], cwd="project", env={"DEBUG": "1"}, timeout_seconds=120)
|
|
117
|
+
r.stdout, r.stderr, r.exit_code, r.success, r.timed_out
|
|
118
|
+
|
|
119
|
+
sandbox.exec("make test", check=True) # raises CommandError on a non-zero exit
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Commands run through the shell; a list is quoted for you. The API has no `env` field for commands, so `env` is
|
|
123
|
+
exported in front of the command line.
|
|
124
|
+
|
|
125
|
+
### Streaming output
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
stream = sandbox.exec_stream("npm install")
|
|
129
|
+
for frame in stream: # started, stdout/stderr chunks, then exit or error
|
|
130
|
+
if frame.type in ("stdout", "stderr"):
|
|
131
|
+
print(frame.data, end="")
|
|
132
|
+
result = stream.wait() # CommandResult with the collected output
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Detached commands
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
server = sandbox.start("node server.js", cwd="app")
|
|
139
|
+
server.process_id, server.pid, server.command_id
|
|
140
|
+
|
|
141
|
+
server.status() # running?, exit code, log tails
|
|
142
|
+
for frame in server.attach(): # live output; reconnects transparently
|
|
143
|
+
print(frame.type, frame.data)
|
|
144
|
+
server.kill() # SIGTERM, then SIGKILL after 5 s
|
|
145
|
+
server.wait() # follow to the end -> CommandResult
|
|
146
|
+
server.logs() # full stored stdout from the command history
|
|
147
|
+
|
|
148
|
+
sandbox.command_status(42); sandbox.attach(42); sandbox.kill(42) # by process id
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Files
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
sandbox.files.write("notes.txt", "hello\n") # text
|
|
155
|
+
sandbox.files.write("logo.png", png_bytes) # bytes (sent base64)
|
|
156
|
+
sandbox.files.read("notes.txt") # -> str (up to 5 MiB)
|
|
157
|
+
sandbox.files.read_bytes("logo.png") # -> bytes (up to 5 MiB)
|
|
158
|
+
sandbox.files.download("build/app.tar.gz") # -> bytes (up to 50 MiB)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Paths are absolute or relative to `/home/user`, and must resolve under `/home/user` or `/tmp`.
|
|
162
|
+
|
|
163
|
+
## Hosting, SSH and desktop
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
sandbox.host(3000) # private URL with a _token query parameter
|
|
167
|
+
sandbox.host(3000, public=True) # or private=False
|
|
168
|
+
sandbox.host(5173, localhost=True) # service bound to 127.0.0.1 only
|
|
169
|
+
sandbox.ssh_key(Path.home().joinpath(".ssh/id_ed25519.pub").read_text()) # then ssh user@<machine_ip>
|
|
170
|
+
sandbox.desktop(public_access=True).desktop_url # waits while VNC is provisioning
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Lifecycle
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
sandbox.stop() # waits until archived (wait=False to return at once)
|
|
177
|
+
sandbox.resume(type="large") # waits until ready again
|
|
178
|
+
child = sandbox.fork() # new Sandbox from the current state; the source is untouched
|
|
179
|
+
sandbox.snapshot("web-stack") # named snapshot, waits until ready -> deploy with create(from_="web-stack")
|
|
180
|
+
sandbox.extend(7200) # auto-stop in 2 h from now (None disables auto-stop)
|
|
181
|
+
sandbox.update(name="demo", subdomain="acme-demo")
|
|
182
|
+
sandbox.usage(); sandbox.processes(); sandbox.history(); sandbox.snapshots(); sandbox.latest_snapshot()
|
|
183
|
+
sandbox.delete() # PERMANENT: deletes the sandbox and its snapshots
|
|
184
|
+
|
|
185
|
+
for sb in client.list(state=["ready", "idle"]):
|
|
186
|
+
print(sb.id, sb.state)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Context managers
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
with client.create(on_exit="delete") as sandbox: # or on_exit="stop"; default None leaves it running
|
|
193
|
+
sandbox.exec("pytest", check=True)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`on_exit` can also be set later: `sandbox.on_exit = "stop"`.
|
|
197
|
+
|
|
198
|
+
## Agents: prompts and events
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
run = sandbox.prompt("Run the tests and fix what fails.", provider="codex", model="gpt-5.4")
|
|
202
|
+
for event in run.events(): # this run's events, ending when it is done
|
|
203
|
+
if event.type == "response" and event.data.get("content"):
|
|
204
|
+
print(event.data["content"])
|
|
205
|
+
run.wait() # PromptRun: finished / failed / interrupted
|
|
206
|
+
print(run.text()) # final response text
|
|
207
|
+
run.steer("Skip the integration tests.")
|
|
208
|
+
run.interrupt()
|
|
209
|
+
|
|
210
|
+
for event in sandbox.events(type="prompt,response"): # every conversation, follows new events
|
|
211
|
+
...
|
|
212
|
+
sandbox.conversations(); sandbox.steer("..."); sandbox.interrupt(conversation="...")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`sandbox.events(mode=...)`: `"poll"` long-polls `GET /events?sort=asc` with the page cursor, `"sse"` uses
|
|
216
|
+
`text/event-stream` (reconnecting with `Last-Event-ID`), `"auto"` (default) tries SSE and falls back to polling.
|
|
217
|
+
Repeated pages are de-duplicated, while an event whose payload changed (a streaming partial becoming final) is
|
|
218
|
+
yielded again.
|
|
219
|
+
|
|
220
|
+
## Async
|
|
221
|
+
|
|
222
|
+
Everything above exists with `async`/`await` on `AsyncSandboxer` / `AsyncSandbox`:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
import asyncio
|
|
226
|
+
from sandboxer import AsyncSandboxer
|
|
227
|
+
|
|
228
|
+
async def main() -> None:
|
|
229
|
+
async with AsyncSandboxer() as client:
|
|
230
|
+
async with await client.create(on_exit="delete") as sandbox:
|
|
231
|
+
print((await sandbox.exec("uname -a")).stdout)
|
|
232
|
+
async for frame in sandbox.exec_stream("ls -R /etc"):
|
|
233
|
+
...
|
|
234
|
+
|
|
235
|
+
asyncio.run(main())
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Async versions of the helper functions are in `sandboxer.aio`.
|
|
239
|
+
|
|
240
|
+
## Low-level client
|
|
241
|
+
|
|
242
|
+
`client.api` is a `SandboxerApi` with one method per API operation (snake_case operation ids):
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from sandboxer import models
|
|
246
|
+
|
|
247
|
+
api = client.api
|
|
248
|
+
api.me(); api.limits(); api.sandboxes(state="ready,idle")
|
|
249
|
+
created = api.create(ttl_seconds=None, no_env=True) # keyword body fields...
|
|
250
|
+
api.update(created.sandbox.id, models.UpdateSandboxRequest(name="x")) # ...or a body model / dict
|
|
251
|
+
api.events(sandbox_id, sort="asc", limit=50, type="prompt,response")
|
|
252
|
+
for frame in api.command_stream(sandbox_id, command="make"): ...
|
|
253
|
+
api.request("GET", "/provider-models") # escape hatch
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- Keyword arguments left out are not sent; `None` is sent as JSON `null` (`ttl_seconds=None` disables auto-stop).
|
|
257
|
+
- `request_options={"timeout": 5, "max_retries": 0, "headers": {...}, "org": "team_x", "idempotency_key": "..."}`
|
|
258
|
+
works on every method.
|
|
259
|
+
- Deletes fill `X-Sandboxer-Confirm-Delete` with the target id for you (override with `confirm_delete=`).
|
|
260
|
+
- Responses are pydantic models with snake_case attributes (`sandbox.memory_gb`); unknown fields are kept
|
|
261
|
+
(`model_extra`) and unexpected shapes are logged instead of raising.
|
|
262
|
+
|
|
263
|
+
## Errors, retries and idempotency
|
|
264
|
+
|
|
265
|
+
Every failure raises `SandboxerError` (subclasses: `BadRequestError`, `AuthenticationError`,
|
|
266
|
+
`PaymentRequiredError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `RateLimitError`,
|
|
267
|
+
`InternalServerError`, `APIConnectionError`, `APITimeoutError`, plus `WaitTimeoutError`, `SandboxStateError`,
|
|
268
|
+
`CommandError`, `WebhookVerificationError`).
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
from sandboxer import SandboxerError
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
sandbox.exec("true")
|
|
275
|
+
except SandboxerError as e:
|
|
276
|
+
print(e.status, e.code, e.message, e.details, e.request_id, e.retryable)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Automatic retries (default 3, jittered exponential backoff, `Retry-After` honored):
|
|
280
|
+
|
|
281
|
+
| Request | Retried on |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| `GET` | network errors, timeouts, 5xx, 429 with `Retry-After`, `sandbox_starting`, retryable `sandbox_direct_failed` |
|
|
284
|
+
| `POST /commands` | only `sandbox_starting`, `sandbox_direct_failed` + `sandbox_restoring` (the command provably did not run) and 429 with `Retry-After` — never network errors or 5xx, since the command may already be running |
|
|
285
|
+
| create / fork | network errors, timeouts, 5xx, 429 with `Retry-After`, `409 idempotency_in_progress` — safe because each call carries an `Idempotency-Key` (a UUID, reused across retries; pass `idempotency_key=` to choose it) |
|
|
286
|
+
| other writes | 429 with `Retry-After`, `sandbox_starting`, retryable `sandbox_direct_failed` |
|
|
287
|
+
|
|
288
|
+
## Webhooks
|
|
289
|
+
|
|
290
|
+
Deliveries carry `Sandboxer-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`. Verify the raw
|
|
291
|
+
bytes before parsing:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from sandboxer import construct_event, verify_webhook, WebhookVerificationError
|
|
295
|
+
|
|
296
|
+
# FastAPI
|
|
297
|
+
@app.post("/hooks/sandbox")
|
|
298
|
+
async def hook(request: Request):
|
|
299
|
+
raw = await request.body()
|
|
300
|
+
try:
|
|
301
|
+
event = construct_event(raw, request.headers, os.environ["SANDBOXER_WEBHOOK_SECRET"])
|
|
302
|
+
except WebhookVerificationError:
|
|
303
|
+
raise HTTPException(400)
|
|
304
|
+
if event.type == "sandbox.ready":
|
|
305
|
+
...
|
|
306
|
+
return {"ok": True}
|
|
307
|
+
|
|
308
|
+
# Flask
|
|
309
|
+
@app.post("/hooks/sandbox")
|
|
310
|
+
def hook():
|
|
311
|
+
if not verify_webhook(request.get_data(), request.headers.get("Sandboxer-Signature"), SECRET):
|
|
312
|
+
abort(400)
|
|
313
|
+
...
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
- `tolerance_seconds` (default 300) rejects stale timestamps; pass a list of secrets while rotating.
|
|
317
|
+
- `verify_webhook_headers(raw, headers, secret)` / `construct_event(raw, headers, secret)` also accept the
|
|
318
|
+
`X-Sandboxer-Signature` / `X-Sandboxer-Delivery` / `X-Sandboxer-Timestamp` header set.
|
|
319
|
+
- Deliveries are at-least-once and may arrive out of order: de-duplicate on `event.id`.
|
|
320
|
+
|
|
321
|
+
## Porting from boat-sdk
|
|
322
|
+
|
|
323
|
+
| boat-sdk | sandboxer |
|
|
324
|
+
| --- | --- |
|
|
325
|
+
| `from boat_sdk.api.boat_api import BoatApi` | `from sandboxer import SandboxerApi` (or `Sandboxer().api`) |
|
|
326
|
+
| `Configuration(host=..., access_token=...)` | `Sandboxer(base_url=..., api_key=...)` |
|
|
327
|
+
| `BOAT_API_KEY`, `X-Boat-Org` | `SANDBOXER_API_KEY`, `X-Sandboxer-Org` (`org=`) |
|
|
328
|
+
| `boat_sdk.models.CreateSandboxRequest(var_from=...)` | `sandboxer.models.CreateSandboxRequest(from_=...)` |
|
|
329
|
+
| `sandbox.create(CreateSandboxRequest(ttl_seconds=1800))` | `api.create(CreateSandboxRequest(ttl_seconds=1800))` or `api.create(ttl_seconds=1800)` |
|
|
330
|
+
| `sandbox.get(id)`, `update`, `stop`, `resume`, `fork`, `delete_sandbox`, `prompt`, `events`, `read_file`, `write_file`, `command`, `artifact`, `desktop`, `ssh_key`, snapshot methods ... | same names on `SandboxerApi` |
|
|
331
|
+
| `ApiException` (`.status`, `.body`) | `SandboxerError` (`.status`, `.code`, `.message`, `.details`, `.request_id`) |
|
|
332
|
+
| `wait_until_ready`, `wait_until_idle`, `wait_for_desktop`, `wait_for_prompt`, `wait_for_prompt_done`, `stream_events`, `stream_prompt`, `stop_and_remove`, `read_text`, `write_text`, `exec_command` | same names in `sandboxer` (async versions in `sandboxer.aio`) |
|
|
333
|
+
|
|
334
|
+
## Development
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
338
|
+
.venv/bin/python scripts/generate.py # regenerate models + low-level client from ../../api/openapi.yaml
|
|
339
|
+
.venv/bin/python scripts/unasync.py # regenerate the sync modules from the async sources
|
|
340
|
+
.venv/bin/pytest -q && .venv/bin/ruff check src tests && .venv/bin/mypy --strict src
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`models.py`, `_generated_api.py` and the `*_sync.py` modules are generated: edit the generator or the async sources.
|
|
344
|
+
|
|
345
|
+
## Support
|
|
346
|
+
|
|
347
|
+
Questions and bug reports: [support@sandboxer.com](mailto:support@sandboxer.com). Full docs: [sandboxer.com/docs/sdk-python](https://sandboxer.com/docs/sdk-python).
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT
|