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.
- tangle_sandbox-0.1.0/.gitignore +23 -0
- tangle_sandbox-0.1.0/PKG-INFO +443 -0
- tangle_sandbox-0.1.0/README.md +403 -0
- tangle_sandbox-0.1.0/examples/fleet.py +85 -0
- tangle_sandbox-0.1.0/examples/research_fleet.py +149 -0
- tangle_sandbox-0.1.0/pyproject.toml +69 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/__init__.py +287 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_batch.py +517 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_batch_events.py +285 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_batch_profile.py +49 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_batch_state.py +677 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_call.py +107 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_contract_schema.py +169 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_create_retry.py +186 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_direct_runtime.py +81 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_error_catalog.py +128 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_fleet_ops.py +231 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_fleet_wire.py +557 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_generated_git.py +70 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_generated_operations.py +39 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_http.py +363 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_instance_ops.py +331 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_instance_wire.py +416 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_intelligence.py +35 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_subresources.py +387 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/_wire.py +23 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/auth/__init__.py +166 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/auth/tokens.py +150 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/auth/types.py +72 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/batch_types.py +156 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/capabilities.py +77 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/client.py +872 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/__init__.py +55 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/client.py +233 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/document_id.py +72 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/file_bridge.py +213 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/collaboration/types.py +158 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/errors.py +278 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/fleet.py +485 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/fleet_handle.py +399 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/fleet_types.py +211 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/image.py +528 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/instance.py +588 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/manage_sandboxes.py +310 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/public-batch-request.schema.json +1576 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/py.typed +1 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/session_gateway/__init__.py +71 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/session_gateway/client.py +832 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/session_gateway/types.py +278 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/__init__.py +54 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/abi.py +204 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/chain_client.py +281 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/client.py +500 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/operator_auth.py +127 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/tangle/types.py +66 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/trace_exporter.py +260 -0
- tangle_sandbox-0.1.0/src/tangle_sandbox/types.py +849 -0
- tangle_sandbox-0.1.0/tests/e2e/__init__.py +13 -0
- tangle_sandbox-0.1.0/tests/e2e/conftest.py +245 -0
- tangle_sandbox-0.1.0/tests/e2e/test_auth_e2e.py +105 -0
- tangle_sandbox-0.1.0/tests/e2e/test_sandbox_lifecycle_e2e.py +319 -0
- tangle_sandbox-0.1.0/tests/test_batch_typescript_parity.py +2088 -0
- tangle_sandbox-0.1.0/tests/test_create_retry_peer.py +211 -0
- tangle_sandbox-0.1.0/tests/test_snapshot_operation_peer.py +88 -0
- tangle_sandbox-0.1.0/tests/test_tangle.py +105 -0
- tangle_sandbox-0.1.0/tests/test_timeouts_peer.py +251 -0
- 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
|