tangle-sandbox 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.
Files changed (67) hide show
  1. tangle_sandbox-0.1.0/.gitignore +23 -0
  2. tangle_sandbox-0.1.0/PKG-INFO +443 -0
  3. tangle_sandbox-0.1.0/README.md +403 -0
  4. tangle_sandbox-0.1.0/examples/fleet.py +85 -0
  5. tangle_sandbox-0.1.0/examples/research_fleet.py +149 -0
  6. tangle_sandbox-0.1.0/pyproject.toml +69 -0
  7. tangle_sandbox-0.1.0/src/tangle_sandbox/__init__.py +287 -0
  8. tangle_sandbox-0.1.0/src/tangle_sandbox/_batch.py +517 -0
  9. tangle_sandbox-0.1.0/src/tangle_sandbox/_batch_events.py +285 -0
  10. tangle_sandbox-0.1.0/src/tangle_sandbox/_batch_profile.py +49 -0
  11. tangle_sandbox-0.1.0/src/tangle_sandbox/_batch_state.py +677 -0
  12. tangle_sandbox-0.1.0/src/tangle_sandbox/_call.py +107 -0
  13. tangle_sandbox-0.1.0/src/tangle_sandbox/_contract_schema.py +169 -0
  14. tangle_sandbox-0.1.0/src/tangle_sandbox/_create_retry.py +186 -0
  15. tangle_sandbox-0.1.0/src/tangle_sandbox/_direct_runtime.py +81 -0
  16. tangle_sandbox-0.1.0/src/tangle_sandbox/_error_catalog.py +128 -0
  17. tangle_sandbox-0.1.0/src/tangle_sandbox/_fleet_ops.py +231 -0
  18. tangle_sandbox-0.1.0/src/tangle_sandbox/_fleet_wire.py +557 -0
  19. tangle_sandbox-0.1.0/src/tangle_sandbox/_generated_git.py +70 -0
  20. tangle_sandbox-0.1.0/src/tangle_sandbox/_generated_operations.py +39 -0
  21. tangle_sandbox-0.1.0/src/tangle_sandbox/_http.py +363 -0
  22. tangle_sandbox-0.1.0/src/tangle_sandbox/_instance_ops.py +331 -0
  23. tangle_sandbox-0.1.0/src/tangle_sandbox/_instance_wire.py +416 -0
  24. tangle_sandbox-0.1.0/src/tangle_sandbox/_intelligence.py +35 -0
  25. tangle_sandbox-0.1.0/src/tangle_sandbox/_subresources.py +387 -0
  26. tangle_sandbox-0.1.0/src/tangle_sandbox/_wire.py +23 -0
  27. tangle_sandbox-0.1.0/src/tangle_sandbox/auth/__init__.py +166 -0
  28. tangle_sandbox-0.1.0/src/tangle_sandbox/auth/tokens.py +150 -0
  29. tangle_sandbox-0.1.0/src/tangle_sandbox/auth/types.py +72 -0
  30. tangle_sandbox-0.1.0/src/tangle_sandbox/batch_types.py +156 -0
  31. tangle_sandbox-0.1.0/src/tangle_sandbox/capabilities.py +77 -0
  32. tangle_sandbox-0.1.0/src/tangle_sandbox/client.py +872 -0
  33. tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/__init__.py +55 -0
  34. tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/client.py +233 -0
  35. tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/document_id.py +72 -0
  36. tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/file_bridge.py +213 -0
  37. tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/types.py +158 -0
  38. tangle_sandbox-0.1.0/src/tangle_sandbox/errors.py +278 -0
  39. tangle_sandbox-0.1.0/src/tangle_sandbox/fleet.py +485 -0
  40. tangle_sandbox-0.1.0/src/tangle_sandbox/fleet_handle.py +399 -0
  41. tangle_sandbox-0.1.0/src/tangle_sandbox/fleet_types.py +211 -0
  42. tangle_sandbox-0.1.0/src/tangle_sandbox/image.py +528 -0
  43. tangle_sandbox-0.1.0/src/tangle_sandbox/instance.py +588 -0
  44. tangle_sandbox-0.1.0/src/tangle_sandbox/manage_sandboxes.py +310 -0
  45. tangle_sandbox-0.1.0/src/tangle_sandbox/public-batch-request.schema.json +1576 -0
  46. tangle_sandbox-0.1.0/src/tangle_sandbox/py.typed +1 -0
  47. tangle_sandbox-0.1.0/src/tangle_sandbox/session_gateway/__init__.py +71 -0
  48. tangle_sandbox-0.1.0/src/tangle_sandbox/session_gateway/client.py +832 -0
  49. tangle_sandbox-0.1.0/src/tangle_sandbox/session_gateway/types.py +278 -0
  50. tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/__init__.py +54 -0
  51. tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/abi.py +204 -0
  52. tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/chain_client.py +281 -0
  53. tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/client.py +500 -0
  54. tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/operator_auth.py +127 -0
  55. tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/types.py +66 -0
  56. tangle_sandbox-0.1.0/src/tangle_sandbox/trace_exporter.py +260 -0
  57. tangle_sandbox-0.1.0/src/tangle_sandbox/types.py +849 -0
  58. tangle_sandbox-0.1.0/tests/e2e/__init__.py +13 -0
  59. tangle_sandbox-0.1.0/tests/e2e/conftest.py +245 -0
  60. tangle_sandbox-0.1.0/tests/e2e/test_auth_e2e.py +105 -0
  61. tangle_sandbox-0.1.0/tests/e2e/test_sandbox_lifecycle_e2e.py +319 -0
  62. tangle_sandbox-0.1.0/tests/test_batch_typescript_parity.py +2088 -0
  63. tangle_sandbox-0.1.0/tests/test_create_retry_peer.py +211 -0
  64. tangle_sandbox-0.1.0/tests/test_snapshot_operation_peer.py +88 -0
  65. tangle_sandbox-0.1.0/tests/test_tangle.py +105 -0
  66. tangle_sandbox-0.1.0/tests/test_timeouts_peer.py +251 -0
  67. tangle_sandbox-0.1.0/uv.lock +2095 -0
@@ -0,0 +1,23 @@
1
+ # Python build artifacts
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Virtualenvs (local development only)
13
+ .venv/
14
+ .venv-*/
15
+ venv/
16
+ env/
17
+
18
+ # Test/coverage
19
+ .pytest_cache/
20
+ .ruff_cache/
21
+ .mypy_cache/
22
+ .coverage
23
+ htmlcov/
@@ -0,0 +1,443 @@
1
+ Metadata-Version: 2.5
2
+ Name: tangle-sandbox
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Tangle Sandbox platform
5
+ Project-URL: Homepage, https://tangle.tools
6
+ Project-URL: Repository, https://github.com/tangle-network/agent-container/tree/develop/products/sandbox/sdk-python
7
+ Author-email: Tangle Technologies <eng@tangle.tools>
8
+ License-Expression: MIT
9
+ Keywords: ai,containers,development,sandbox,tangle
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.25.0
21
+ Requires-Dist: pydantic<3,>=2.11
22
+ Provides-Extra: all
23
+ Requires-Dist: eth-account>=0.10; extra == 'all'
24
+ Requires-Dist: web3>=6.0; extra == 'all'
25
+ Requires-Dist: websockets>=12.0; extra == 'all'
26
+ Provides-Extra: dev
27
+ Requires-Dist: eth-account>=0.10; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
29
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
30
+ Requires-Dist: pytest>=7.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.4; extra == 'dev'
32
+ Requires-Dist: web3>=6.0; extra == 'dev'
33
+ Requires-Dist: websockets>=12.0; extra == 'dev'
34
+ Provides-Extra: gateway
35
+ Requires-Dist: websockets>=12.0; extra == 'gateway'
36
+ Provides-Extra: tangle
37
+ Requires-Dist: eth-account>=0.10; extra == 'tangle'
38
+ Requires-Dist: web3>=6.0; extra == 'tangle'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # tangle-sandbox
42
+
43
+ Python SDK for the Tangle Sandbox platform. Create isolated dev containers, run AI agents, and build automation workflows.
44
+
45
+ ## Installation
46
+
47
+ Install [`tangle-sandbox` from PyPI](https://pypi.org/project/tangle-sandbox/):
48
+
49
+ ```bash
50
+ pip install tangle-sandbox # extras: [gateway] session gateway, [tangle] on-chain client, [all]
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ The production journey runs these calls against each cell every five minutes (`scripts/production-journey.mjs`, step `python`).
56
+
57
+ ```python
58
+ import os
59
+
60
+ from tangle_sandbox import Sandbox
61
+
62
+ client = Sandbox(
63
+ api_key=os.environ["TANGLE_API_KEY"],
64
+ base_url="https://sandbox.tangle.tools",
65
+ )
66
+
67
+ box = client.create(name="my-project")
68
+ box.wait_for("running")
69
+ try:
70
+ # Execute commands
71
+ result = box.exec("python3 -c 'print(sum(range(1_000_000)))'")
72
+ print(result.stdout)
73
+
74
+ # Prompt the sandbox agent
75
+ result = box.prompt("Reply with exactly the word OK")
76
+ print(result.response if result.success else result.error)
77
+ finally:
78
+ # Clean up
79
+ box.delete()
80
+ client.close()
81
+ ```
82
+
83
+ ## Async Usage
84
+
85
+ ```python
86
+ from tangle_sandbox import AsyncSandbox
87
+
88
+ async with AsyncSandbox(api_key="...", base_url="...") as client:
89
+ box = await client.create(name="my-project")
90
+ await box.wait_for("running")
91
+
92
+ result = await box.exec("npm test")
93
+ print(result.stdout)
94
+
95
+ await box.delete()
96
+ ```
97
+
98
+ ## Batch Execution
99
+
100
+ Each task runs once against each backend.
101
+ Backend IDs distinguish configurations even when they use the same runtime type.
102
+
103
+ ```python
104
+ from tangle_sandbox import (
105
+ BatchBackend,
106
+ BatchBackendModel,
107
+ BatchRunRequest,
108
+ BatchTask,
109
+ Sandbox,
110
+ )
111
+
112
+ request = BatchRunRequest(
113
+ tasks=[
114
+ BatchTask(id="review", message="Review the repository and report the highest-risk bug"),
115
+ BatchTask(id="fix", message="Fix the failing tests"),
116
+ ],
117
+ backends=[
118
+ BatchBackend(id="builder", type="opencode"),
119
+ BatchBackend(
120
+ id="reviewer",
121
+ type="claude-code",
122
+ model=BatchBackendModel(
123
+ provider="anthropic",
124
+ model="claude-sonnet-5-5",
125
+ ),
126
+ ),
127
+ ],
128
+ persistent=True,
129
+ reuse_key="repository-review-workspace",
130
+ )
131
+
132
+ with Sandbox(api_key="...", base_url="...") as client:
133
+ result = client.run_batch(request, idempotency_key="review-and-fix-run-001")
134
+ print(result.total_success, result.total_executions, result.success_rate)
135
+ print(result.cleanup.mode)
136
+ ```
137
+
138
+ Use `stream_batch()` for progress events.
139
+ It yields every valid server event, including `batch.failed`.
140
+ After cleanup events finish, both methods raise `BatchFailedError` for `batch.failed`.
141
+ The error is a `SandboxError` with code `OPERATION_FAILED` whose `cleanup` field reports confirmed releases, pending releases, and completeness.
142
+ `run_batch()` also rejects incomplete or inconsistent results.
143
+ Malformed or inconsistent stream data raises `NetworkError`.
144
+ For immediate cleanup, `result.cleanup` lists confirmed and pending sidecar IDs and reports whether deletion is complete.
145
+ Persistent and grace-period runs return their retention details instead.
146
+
147
+ ```python
148
+ for event in client.stream_batch(request):
149
+ if event.type in {"task.completed", "task.failed"}:
150
+ print(event.data["taskId"], event.data["backendId"])
151
+ ```
152
+
153
+ The async client exposes the same API through `async for client.stream_batch(...)` and `await client.run_batch(...)`.
154
+ `BatchRunRequest` requires tasks and explicitly named backends, and also supports execution class, persistence, lifecycle timeouts, cleanup grace time, and Git workspace setup.
155
+ Billing identity, scaling, and capacity are assigned by the service.
156
+ `persistent=True` requires `reuse_key`, which selects the same caller-owned workspace across separate batch requests.
157
+ `lifecycle` and `git` also require `persistent=True`; `grace_ms` applies only to non-persistent batches and is omitted by default.
158
+ Batch Git repositories must use a public DNS host over HTTPS with no query, fragment, or custom port.
159
+ Supply credentials through `git.auth` rather than embedding them in the URL.
160
+
161
+ Closing a sync stream or cancelling its async consumer disconnects the HTTP stream immediately.
162
+ Without `idempotency_key`, that disconnect cancels the server run.
163
+ With `idempotency_key`, the server continues the exact request so another call with the same key and body can join or replay it.
164
+ Use a key only for retries of one unchanged request body.
165
+ `reuse_key` controls persistent workspace selection, while `idempotency_key` controls retry identity for one batch run.
166
+
167
+ ## Sandbox Fleets
168
+
169
+ Use fleets when one workload should run across several sandboxes. A researcher
170
+ can create a coordinator plus workers, run the same command across the workers,
171
+ compare outputs, then delete the whole fleet.
172
+
173
+ ```python
174
+ from tangle_sandbox import (
175
+ CreateSandboxFleetWithCoordinatorOptions,
176
+ Sandbox,
177
+ SandboxFleetMachineSpec,
178
+ )
179
+
180
+ with Sandbox(api_key="...", base_url="...") as client:
181
+ fleet = client.fleets.create_with_coordinator(
182
+ CreateSandboxFleetWithCoordinatorOptions(
183
+ workers=[
184
+ SandboxFleetMachineSpec(machine_id="worker-1"),
185
+ SandboxFleetMachineSpec(machine_id="worker-2"),
186
+ ],
187
+ )
188
+ )
189
+
190
+ try:
191
+ run = fleet.dispatch_exec_with_insights("python run_experiment.py")
192
+ for result in run["results"]:
193
+ print(result["machineId"], result["ok"])
194
+ print(run["intelligence"]["recommendedActions"])
195
+ finally:
196
+ fleet.delete()
197
+ ```
198
+
199
+ ### Trace Intelligence
200
+
201
+ Sandboxes and fleet runs expose the full raw trace by default. The compact intelligence envelope is opt-in unless you call the explicit intelligence helper. The raw trace is the evidence: lifecycle events, usage, timing metrics, runtime state, and, for fleets, machine events, dispatch results, critical path, and workspace state. The intelligence envelope is the current platform analysis: reliability, parallelism efficiency for fleets, failure classes, resource attribution, bottlenecks, and recommended actions.
202
+
203
+ ```python
204
+ box_bundle = box.trace() # {"trace": ...}
205
+ box_insights = box.intelligence() # intelligence only
206
+ box_bundle_with_insights = box.trace(include_intelligence=True)
207
+
208
+ run = fleet.dispatch_exec_with_insights("pytest -q")
209
+ print(run["trace"]["events"])
210
+ print(run["intelligence"]["signals"])
211
+ print(run["intelligence"]["recommendedActions"])
212
+
213
+ bundle = fleet.trace() # {"trace": ...}
214
+ insights = fleet.intelligence() # intelligence only
215
+ bundle_with_insights = fleet.trace(include_intelligence=True)
216
+ ```
217
+
218
+ Forward the native bundle to your own warehouse or convert it to OTEL JSON for OpenTelemetry-style collectors.
219
+
220
+ The built-in intelligence is deterministic platform analysis. It reports `billing.billable: false` and `billing.costUsd: 0`; customers are not charged for generating these envelopes. Products opt into generated intelligence per trace request with `include_intelligence=True` or by calling `intelligence()`.
221
+
222
+ ```python
223
+ box.export_trace(
224
+ "https://collector.example.com/traces",
225
+ headers={"Authorization": f"Bearer {OBSERVABILITY_TOKEN}"},
226
+ format="otel-json",
227
+ service_name="research-agent",
228
+ )
229
+
230
+ fleet.export_trace(
231
+ "https://collector.example.com/traces",
232
+ headers={"Authorization": f"Bearer {OBSERVABILITY_TOKEN}"},
233
+ format="otel-json",
234
+ service_name="research-agent",
235
+ )
236
+ ```
237
+
238
+ For Braintrust, Lemma, Raindrop, Langfuse, Datadog, or internal tools, keep the vendor credentials on your side. Fetch `box.trace()` or `fleet.trace()` and transform the raw bundle through that platform's SDK/API, or point `export_trace()` at your own ingest endpoint.
239
+
240
+ ## Timeouts and retries
241
+
242
+ [`_http.TIMEOUTS`](src/tangle_sandbox/_http.py) owns request defaults in seconds. Every sync and async request, including `.direct()`, uses the client's `timeout_ms` unless the call supplies a longer operation timeout. Creation shares that client budget across attempts. Exec waits its execution limit plus `margin`; fleet dispatch waits the larger of its requested duration and `fleet_dispatch`, plus `margin`. Snapshot restore and image builds retain their bounded operation waits in `_instance_ops.py` and `image.py`; collaboration, trace export and Tangle operator requests retain their own bounded defaults.
243
+
244
+ Streams use `TIMEOUTS["stream"]` as the maximum read gap; `PromptOptions.timeout_ms` replaces it for a prompt. HTTPX deadlines bound network phases and read gaps, rather than a whole streamed operation's lifetime. A request timeout does not cancel work already accepted by the server. Retries require a catalog-retryable error and an idempotent method, or a connection failure that proves no request was delivered. Request attempts and retry pacing share one budget; a Retry-After beyond it returns the original refusal. Creation reuses its idempotency key through its separate recovery budget.
245
+
246
+ ## API Reference
247
+
248
+ ### Client
249
+
250
+ ```python
251
+ from tangle_sandbox import Sandbox, CreateSandboxOptions, GitConfig
252
+
253
+ client = Sandbox(
254
+ api_key="sk_sandbox_...",
255
+ base_url="https://your-sandbox-api.example.com",
256
+ timeout_ms=30000,
257
+ )
258
+ ```
259
+
260
+ #### `client.create(options)`
261
+
262
+ ```python
263
+ from tangle_sandbox import CreateSandboxOptions, GitConfig
264
+
265
+ box = client.create(CreateSandboxOptions(
266
+ name="my-project",
267
+ runtime="firecracker",
268
+ git=GitConfig(url="https://github.com/user/repo.git", ref="main"),
269
+ ssh_enabled=True,
270
+ env={"NODE_ENV": "development"},
271
+ ))
272
+ ```
273
+
274
+ `runtime` and `environment` mean what the TypeScript SDK README's "Runtime and image" table says; `client.environments.list()` shows each environment's `runtimes`.
275
+
276
+ #### `client.list(options)`
277
+
278
+ ```python
279
+ from tangle_sandbox import ListSandboxOptions, SandboxStatus
280
+
281
+ boxes = client.list(ListSandboxOptions(status=SandboxStatus.RUNNING, limit=10))
282
+ ```
283
+
284
+ #### `client.get(sandbox_id)`
285
+
286
+ ```python
287
+ box = client.get("sandbox_abc123")
288
+ if box:
289
+ print(box.status)
290
+ ```
291
+
292
+ ### Sandbox Instance
293
+
294
+ #### `box.exec(command, options)`
295
+
296
+ ```python
297
+ from tangle_sandbox import ExecOptions
298
+
299
+ result = box.exec("npm install", ExecOptions(cwd="/workspace", timeout_ms=60000))
300
+ print(result.exit_code)
301
+ print(result.stdout)
302
+ ```
303
+
304
+ #### `box.prompt(message, options)`
305
+
306
+ ```python
307
+ from tangle_sandbox import BackendConfig, PromptOptions
308
+
309
+ result = box.prompt("What files are in this project?", PromptOptions(
310
+ session_id="session_123",
311
+ backend=BackendConfig(
312
+ type="claude-code",
313
+ model={
314
+ "provider": "anthropic",
315
+ "model": "claude-sonnet-5-5",
316
+ },
317
+ ),
318
+ ))
319
+ print(result.response)
320
+ ```
321
+
322
+ #### `box.stream_prompt(message, options)`
323
+
324
+ ```python
325
+ for event in box.stream_prompt("Explain this codebase"):
326
+ if event.type == "message.part.updated":
327
+ part = event.data.get("part", {})
328
+ if part.get("type") == "text" and event.data.get("delta"):
329
+ print(event.data["delta"], end="")
330
+ ```
331
+
332
+ #### File Operations
333
+
334
+ ```python
335
+ content = box.read("src/index.ts")
336
+ box.write("src/fix.ts", "export const fix = () => {}")
337
+ ```
338
+
339
+ #### Code Search
340
+
341
+ ```python
342
+ from tangle_sandbox import SearchOptions
343
+
344
+ matches = box.search("TODO:", SearchOptions(glob="**/*.ts"))
345
+ for match in matches:
346
+ print(f"{match.path}:{match.line}: {match.text}")
347
+ ```
348
+
349
+ #### Git
350
+
351
+ ```python
352
+ status = box.git.status()
353
+ if status.is_dirty:
354
+ box.git.add(["."])
355
+ box.git.commit("Update files")
356
+ box.git.push()
357
+ ```
358
+
359
+ #### Snapshots
360
+
361
+ ```python
362
+ from tangle_sandbox import SnapshotOptions
363
+
364
+ snapshot = box.snapshot(SnapshotOptions(tags=["before-deploy"]))
365
+ print(snapshot.snapshot_id)
366
+
367
+ snapshots = box.list_snapshots()
368
+ ```
369
+
370
+ #### SSH
371
+
372
+ ```python
373
+ creds = box.ssh()
374
+ print(f"ssh {creds.username}@{creds.host} -p {creds.port}")
375
+ ```
376
+
377
+ #### Egress
378
+
379
+ ```python
380
+ from tangle_sandbox import EgressPolicy
381
+
382
+ # strict reaches these plus the sandbox's own model endpoint, nothing else.
383
+ policy, source = box.egress_update(EgressPolicy(
384
+ mode="strict",
385
+ allow_domains=["api.github.com", "pypi.org"],
386
+ ))
387
+ ```
388
+
389
+ #### Lifecycle
390
+
391
+ ```python
392
+ from tangle_sandbox import SandboxStatus, WaitForOptions
393
+
394
+ box.wait_for(SandboxStatus.RUNNING, WaitForOptions(timeout_ms=60000))
395
+ box.stop()
396
+ box.resume()
397
+ box.delete()
398
+ ```
399
+
400
+ ### Image Builder
401
+
402
+ ```python
403
+ from tangle_sandbox import Image
404
+
405
+ image = (
406
+ Image.create("ubuntu:22.04")
407
+ .python("3.11")
408
+ .add_packages(["numpy", "pandas", "scikit-learn"])
409
+ .add_apt_packages(["ffmpeg"])
410
+ .with_env({"PYTHONUNBUFFERED": "1"})
411
+ .build(client=client)
412
+ )
413
+
414
+ box = client.create(environment=image.id)
415
+ ```
416
+
417
+ ## Error Handling
418
+
419
+ Every SDK error is one `SandboxError`. `code` is the platform's error code; `status`, `retryable` and `next_step` come from the platform error catalog (`tangle_sandbox/_error_catalog.py`, generated from `packages/runtime-contracts/src/error-catalog.ts`).
420
+ A code's own fields (`timeout_ms`, `current_state`, `quota_type`, `failures`) read as attributes.
421
+
422
+ ```python
423
+ from tangle_sandbox import SandboxError
424
+
425
+ try:
426
+ result = box.exec("npm test")
427
+ except SandboxError as e:
428
+ print(f"{e.code} (HTTP {e.status}): {e.message}")
429
+ print(f"Next: {e.next_step}")
430
+ if e.retryable:
431
+ ... # a later attempt can succeed
432
+ ```
433
+
434
+ `AuthError`, `NotFoundError`, `QuotaError`, `CapabilityError`, `PartialFailureError`, `ValidationError`, `StateError`, `TimeoutError`, `NetworkError`, `ServerError` and `BatchFailedError` remain for one minor release as aliases of `SandboxError`, so existing `except` clauses keep working; catch `SandboxError` and read `code` instead.
435
+
436
+ ## Requirements
437
+
438
+ - Python 3.10+
439
+ - httpx >= 0.25.0
440
+
441
+ ## License
442
+
443
+ MIT