stackbox 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.
@@ -0,0 +1,288 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackbox
3
+ Version: 0.1.0
4
+ Summary: One API for every sandbox provider: an OpenSandbox-compatible gateway that routes sandboxes across E2B, Modal, Daytona, Vercel and Cloudflare
5
+ Keywords: sandbox,opensandbox,e2b,modal,daytona,vercel,cloudflare,agents
6
+ Author: Stacktrace AI, Inc
7
+ Author-email: Stacktrace AI, Inc <founders@stacktrace.ai>
8
+ License-Expression: LicenseRef-Proprietary
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: fastapi>=0.162.2
14
+ Requires-Dist: httpx>=0.28.1
15
+ Requires-Dist: pydantic>=2.14.3
16
+ Requires-Dist: uvicorn>=0.54.0
17
+ Requires-Dist: stackbox[e2b,modal,daytona,vercel] ; extra == 'all'
18
+ Requires-Dist: daytona>=0.231.0 ; extra == 'daytona'
19
+ Requires-Dist: e2b>=2.63.0 ; extra == 'e2b'
20
+ Requires-Dist: aiohttp-socks>=0.12.0 ; extra == 'modal'
21
+ Requires-Dist: modal>=1.6.3 ; extra == 'modal'
22
+ Requires-Dist: python-socks>=3.2.1 ; extra == 'modal'
23
+ Requires-Dist: vercel-sandbox>=0.11.0 ; extra == 'vercel'
24
+ Requires-Python: >=3.13
25
+ Project-URL: Homepage, https://stacktrace.ai
26
+ Provides-Extra: all
27
+ Provides-Extra: daytona
28
+ Provides-Extra: e2b
29
+ Provides-Extra: modal
30
+ Provides-Extra: vercel
31
+ Description-Content-Type: text/markdown
32
+
33
+ # stackbox
34
+
35
+ One OpenSandbox-compatible endpoint for **E2B, Modal, Daytona, Vercel, Cloudflare** and your own
36
+ OpenSandbox servers. Each sandbox goes to the cheapest or fastest provider that can run it, quotas
37
+ are pooled across accounts, and failures fail over without duplicates or leaked sandboxes.
38
+
39
+ Clients don't change. The stock OpenSandbox SDKs (Python, Go, Kotlin, TS, C#), the `osb` CLI, and
40
+ Harbor's `opensandbox` environment all talk to it as if it were one OpenSandbox server.
41
+
42
+ ## Quickstart
43
+
44
+ ```bash
45
+ uv tool install "stackbox[all]" # or: pip install "stackbox[all]"
46
+
47
+ export E2B_API_KEY=... # any provider; `stackbox providers` lists them all
48
+ stackbox doctor # checks each provider end to end, says what to fix
49
+ stackbox prepare ubuntu:24.04 # makes the image fast to start on every provider
50
+ stackbox serve # http://127.0.0.1:8090
51
+ ```
52
+
53
+ No config file is needed:
54
+ - providers are detected from their credentials;
55
+ - prices and plan limits come from a built-in catalog;
56
+ - the `execd` binary each sandbox runs is downloaded automatically.
57
+
58
+ `stackbox init` writes a `stackbox.toml` when you want to customise.
59
+
60
+ **Python** (`pip install opensandbox`; the snippet uses top-level `await`, as in Jupyter or `python -m asyncio`, and `examples/quickstart.py` is the same code as a script):
61
+
62
+ ```python
63
+ from opensandbox import Sandbox
64
+ from opensandbox.config import ConnectionConfig
65
+
66
+ gateway = ConnectionConfig(domain="http://127.0.0.1:8090", protocol="https")
67
+ sandbox = await Sandbox.create("ubuntu:24.04", connection_config=gateway)
68
+ print((await sandbox.commands.run("echo hello")).logs.stdout[0].text)
69
+ await sandbox.kill()
70
+ ```
71
+
72
+ **Harbor** (`examples/harbor.yaml`): set the environment to `opensandbox`, `domain` to the
73
+ gateway, and `protocol: https`. Then:
74
+
75
+ ```bash
76
+ harbor run -c examples/harbor.yaml
77
+ ```
78
+
79
+ ## Routing
80
+
81
+ Pick a default strategy with `[routing] strategy` or `serve --strategy`, and override it per
82
+ request, OpenRouter-style:
83
+
84
+ | Strategy | Picks |
85
+ |---|---|
86
+ | `balanced` (default) | the best mix of price and startup time |
87
+ | `cost` | the lowest list price for what the provider would bill |
88
+ | `latency` | the lowest learned time-to-sandbox for this image on this provider |
89
+ | `priority` | backends in configured `priority` order |
90
+
91
+ **Per-request preferences** go in headers (SDK: `ConnectionConfig(headers=…)`) or in sandbox
92
+ metadata (Harbor: `kwargs.metadata`):
93
+
94
+ | Header | Metadata key | Meaning |
95
+ |---|---|---|
96
+ | `X-Stackbox-Strategy: cost` | `stackbox.dev/strategy` | override the strategy |
97
+ | `X-Stackbox-Providers: e2b,modal` | `stackbox.dev/providers` (`e2b.modal`) | only these providers; without a strategy, try them in this order |
98
+ | `X-Stackbox-Max-Price: 0.10` | `stackbox.dev/max-price` | skip providers above this USD per sandbox-hour |
99
+ | `X-Stackbox-Backend: e2b` | `stackbox.dev/backend` | pin to one provider, no fallback |
100
+
101
+ **Why a sandbox goes where it does:** `stackbox route [IMAGE]` (or `GET /v1/route?image=…`) lists
102
+ every provider in order with its price, expected startup time and status, for example:
103
+ - next placement goes here;
104
+ - full: 5/5 (learned from provider refusals);
105
+ - not prepared; run `stackbox prepare …`;
106
+ - too expensive.
107
+
108
+ Errors carry the same per-provider reasons and a `hint`.
109
+
110
+ **How the routing behaves:**
111
+ - **Learns startup time:** the latency estimate is an average of observed creates per provider and
112
+ image, weighted toward recent ones. It survives restarts, and each provider gets 3 measured starts
113
+ before it can lose on its default estimate.
114
+ - **Discovers quotas:** a provider's quota refusal (Daytona's memory cap, E2B's rate limit, Modal's
115
+ resource limits, Cloudflare having no instances to hand out) is not a failure.
116
+ - The gateway caps that provider at its current count, growing the cap back by one every 15 s.
117
+ - New creates spill to other providers or wait.
118
+ - Refusals don't count toward the circuit breaker.
119
+ - **Fails over only when it's safe:**
120
+ - A clean failure (nothing was created) moves on to the next provider.
121
+ - An ambiguous one (timeout, 5xx) first checks the provider for the sandbox and adopts or deletes
122
+ what it finds.
123
+ - If the outcome can't be determined, it returns `503 STACKBOX::OUTCOME_UNKNOWN` rather than risk a
124
+ duplicate.
125
+ - **Breaker and retries:** a per-backend circuit breaker demotes a backend after repeated
126
+ failures, and retries go to other backends first.
127
+
128
+ ## CLI
129
+
130
+ | Command | What it does |
131
+ |---|---|
132
+ | `stackbox providers` | supported providers, list prices, plan limits, which you have set up, and live status |
133
+ | `stackbox init` | write `stackbox.toml` for the providers in your environment |
134
+ | `stackbox doctor` | per provider: SDK, credentials, API reachable, `execd`, image prepared; each failure comes with its fix |
135
+ | `stackbox prepare IMAGE…` | build templates and image layers, push to Vercel's registry; `-p` limits to some providers |
136
+ | `stackbox serve` | run the server (`--host`, `--port`, `--strategy`) |
137
+ | `stackbox route [IMAGE]` | where a sandbox would go right now, and why (`--strategy`, `--providers`, `--max-price`, `--cpu`, `--memory`) |
138
+ | `stackbox images [IMAGE]` | prepared images per provider |
139
+ | `stackbox sandboxes` | sandboxes the running server holds, with provider and cost (`--all` includes ended ones) |
140
+ | `stackbox placement ID` | where one sandbox went, every attempt, and its cost |
141
+ | `stackbox usage` | sandbox-hours, vCPU-hours, GiB-hours and estimated cost per provider (`--since 24h`) |
142
+
143
+ `stackbox providers` also shows live status (held/limit, placements) when a server is running.
144
+
145
+ Every command takes `-c FILE`, and the read commands take `--json`. Config is found in this order:
146
+ 1. `-c`;
147
+ 2. `$STACKBOX_CONFIG`;
148
+ 3. `./stackbox.toml`;
149
+ 4. auto-detection from credentials.
150
+
151
+ `doctor` exits 0 when everything passes, 1 when some providers have problems, and 2 when nothing
152
+ is usable.
153
+
154
+ ## Providers
155
+
156
+ | Kind | Credentials | Images | Notes |
157
+ |---|---|---|---|
158
+ | `e2b` | `E2B_API_KEY` | any image; prepared as an E2B template with `execd` (about 45 s, once) | Firecracker; a template fixes vCPU and memory |
159
+ | `modal` | `modal token set`, or `MODAL_TOKEN_ID` + `MODAL_TOKEN_SECRET` | any image; the `execd` layer is built once | gVisor; 1 vCPU = half a Modal core |
160
+ | `daytona` | `DAYTONA_API_KEY` | any image, as a declarative image Daytona builds once and caches | the account memory quota is discovered automatically |
161
+ | `vercel` | `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID` | Vercel's registry only; `prepare` pushes image + `execd` there (needs Docker once) | bills active CPU only; Hobby caps sandboxes at 45 min |
162
+ | `cloudflare` | `CLOUDFLARE_BRIDGE_URL` + `CLOUDFLARE_BRIDGE_TOKEN` | fixed when the bridge is deployed | deploy the bridge Worker once (`bridges/cloudflare`); bills active CPU only |
163
+ | `opensandbox` | `url` (+ `api_key`) in config | the server pulls them | your own OpenSandbox servers on Docker or Kubernetes |
164
+
165
+ On hosted providers the gateway emulates the OpenSandbox lifecycle on the provider's SDK and runs
166
+ OpenSandbox's `execd` in every sandbox, with a per-sandbox token. `GET /sandboxes/{id}/endpoints/44772`
167
+ returns the provider's URL and the headers to send. Command and file traffic goes from the client
168
+ to the sandbox, not through the gateway. Cloudflare is the exception: it goes through the bridge
169
+ Worker.
170
+
171
+ ## Configuration
172
+
173
+ Every field is optional except `kind`; the provider catalog fills in the rest. See
174
+ `examples/all-providers.toml` for every provider, and `stackbox init` for a starter.
175
+
176
+ ```toml
177
+ [gateway]
178
+ api_key = "env:STACKBOX_API_KEY" # required to listen beyond 127.0.0.1
179
+
180
+ [routing]
181
+ strategy = "balanced"
182
+
183
+ [[backends]]
184
+ kind = "e2b"
185
+ max_active = 50 # your plan's concurrency
186
+
187
+ [[backends]]
188
+ kind = "modal"
189
+ priority = 10
190
+ ```
191
+
192
+ - **Secrets:** `"env:NAME"` values (or `"env:A|B"`) are read from the environment, so secrets stay
193
+ out of config files.
194
+ - **Common backend fields:**
195
+ - `max_active` (concurrency);
196
+ - `priority`;
197
+ - `cost_per_vcpu_hour` and `cost_per_gib_hour`;
198
+ - `cpu_billed_fraction`, for providers that bill only active CPU;
199
+ - `max_timeout_s`;
200
+ - `default_cpu` and `default_memory_gib`;
201
+ - `auto_prepare`;
202
+ - `execd_path`, if you'd rather supply `execd` yourself.
203
+
204
+ ## HTTP API
205
+
206
+ Hosted at `https://stackbox.stacktrace.dev`, or wherever you run `stackbox serve`. Every route
207
+ except `GET /health` needs the API key in `OPEN-SANDBOX-API-KEY`.
208
+
209
+ **OpenSandbox API** (what the SDKs, Harbor and `osb` use, unchanged):
210
+
211
+ | Route | |
212
+ |---|---|
213
+ | `POST/GET /v1/sandboxes`, `GET/DELETE /v1/sandboxes/{id}` | create, list, get, delete |
214
+ | `/v1/sandboxes/{id}/endpoints/{port}`, `…/renew-expiration`, … | forwarded to the provider; hosted providers implement endpoints and renew-expiration and return `501` for the rest |
215
+ | `/v1/snapshots`, `/v1/templates` | `501 STACKBOX::UNSUPPORTED`; prepare images with `POST /v1/images` instead |
216
+
217
+ **stackbox API:**
218
+
219
+ | Route | |
220
+ |---|---|
221
+ | `GET /v1/providers`, `GET /v1/providers/{name}` | pricing, capacity (held, limit, learned quota), health, learned startup per image, prepared images, usage |
222
+ | `GET /v1/route?image=…&strategy=…&providers=…&max_price=…&cpu=…&memory_gib=…` | where a sandbox would go right now, and why, for every provider (dry run) |
223
+ | `GET /v1/placements/{sandbox_id}` | where a sandbox went, every attempt (outcome, error, duration), and its cost |
224
+ | `GET /v1/images?image=…`, `POST /v1/images` `{image, providers?}` | prepared images per provider; prepare an image |
225
+ | `GET /v1/usage?since=24h&until=…&group_by=provider\|sandbox` | sandbox-hours, vCPU-hours, GiB-hours, estimated cost |
226
+ | `POST /v1/admin/reconcile` | run cleanup now |
227
+
228
+ **Errors** are JSON: `{code: "STACKBOX::…", message, details, hint}`. The message includes the hint,
229
+ since SDKs show only the message. `SANDBOX::NOT_FOUND` keeps OpenSandbox's standard code.
230
+
231
+ ## Reliability
232
+
233
+ - **Ledger:** a SQLite ledger records intent before every upstream create, and every attempt's
234
+ outcome.
235
+ - **Reconciler:** runs periodically and on `POST /v1/admin/reconcile`:
236
+ - deletes orphans from ambiguous creates;
237
+ - expires sandboxes that vanished upstream;
238
+ - deletes leaked sandboxes carrying this gateway's managed tag.
239
+ - **Ownership:** the tag holds a per-ledger instance id, so gateways sharing an account never touch
240
+ each other's sandboxes.
241
+
242
+ ## Verified
243
+
244
+ All of these ran on real accounts with Harbor trials (an oracle agent, `ubuntu:24.04`, 1 vCPU,
245
+ 2 GiB). Full write-ups: `bench/LIVE.md` and `bench/LIVE-MORE.md`; the local fault-injection
246
+ benchmark is in `bench/RESULTS.md`.
247
+
248
+ - **Reliability:** every gateway arm passed 48/48 with no leaked sandboxes. That covers E2B, Modal,
249
+ Daytona, Vercel, Cloudflare, cost and latency routing, and quota pooling.
250
+ - **Strategy decides placement:** `cost` chose E2B, 28% cheaper than Modal. `latency` chose the
251
+ fastest-starting provider and followed it as providers sped up and slowed down.
252
+ - **Quotas:**
253
+ - Pooling two 4-sandbox quotas: 36 s, versus 69 s on one provider.
254
+ - Daytona's 10 GiB account quota: 67–71 s through the gateway, versus 199 s with Harbor's own
255
+ Daytona environment; 40–56 s with E2B taking the overflow.
256
+ - **Against Harbor's own provider environments:**
257
+ - E2B: 14% faster and 16% cheaper, traced to Harbor's E2B file transfers.
258
+ - Vercel: 48/48 through the gateway, versus 10/48 native (a per-trial registry-token rate
259
+ limit).
260
+ - Cloudflare: Harbor has no environment of its own; the gateway is how it gets there.
261
+
262
+ Gateway create p50: Modal 0.6–0.8 s, Vercel 0.9 s, E2B 1.1–1.4 s, Cloudflare 1.5 s, Daytona 2.8–3.5 s.
263
+
264
+ ## Limits and next steps
265
+
266
+ - **Latency routing learns startup time, not task runtime.** It picked Vercel for its fast starts,
267
+ but Vercel trials ran longer. Next: learn per-provider runtime from completed sandboxes.
268
+ - **Hosted providers lack some OpenSandbox features:** no egress sidecar (network policy),
269
+ pause/resume or snapshots. Only E2B exposes ports other than `execd`'s, and Modal can't extend a
270
+ running sandbox.
271
+ - **Cost is an estimate** from list prices and allocated resources, not invoices. Active-CPU
272
+ billing is approximated with `cpu_billed_fraction`.
273
+ - **Cloudflare adds a hop:** `execd` traffic goes through the bridge Worker, at about 165 ms per
274
+ command versus about 110 ms elsewhere.
275
+ - **Listing doesn't scale yet:** the list endpoint queries every backend on each call.
276
+ - **Later:** a Kubernetes backend, region and GPU matching, and per-tenant quotas.
277
+
278
+ ## Development
279
+
280
+ ```bash
281
+ git clone https://github.com/stacktrace-ai/stackbox && cd stackbox
282
+ uv sync --all-extras
283
+ uv run pytest -q # all tests run against in-memory fakes
284
+ uv run ruff check src tests bench scripts examples && uv run ruff format
285
+ ```
286
+
287
+ Contributor guidelines and the developer-experience principles are in `CLAUDE.md`. Releases ship by
288
+ pushing a `v<version>` tag; see `docs/releases/` and the `release-stackbox` skill.
@@ -0,0 +1,256 @@
1
+ # stackbox
2
+
3
+ One OpenSandbox-compatible endpoint for **E2B, Modal, Daytona, Vercel, Cloudflare** and your own
4
+ OpenSandbox servers. Each sandbox goes to the cheapest or fastest provider that can run it, quotas
5
+ are pooled across accounts, and failures fail over without duplicates or leaked sandboxes.
6
+
7
+ Clients don't change. The stock OpenSandbox SDKs (Python, Go, Kotlin, TS, C#), the `osb` CLI, and
8
+ Harbor's `opensandbox` environment all talk to it as if it were one OpenSandbox server.
9
+
10
+ ## Quickstart
11
+
12
+ ```bash
13
+ uv tool install "stackbox[all]" # or: pip install "stackbox[all]"
14
+
15
+ export E2B_API_KEY=... # any provider; `stackbox providers` lists them all
16
+ stackbox doctor # checks each provider end to end, says what to fix
17
+ stackbox prepare ubuntu:24.04 # makes the image fast to start on every provider
18
+ stackbox serve # http://127.0.0.1:8090
19
+ ```
20
+
21
+ No config file is needed:
22
+ - providers are detected from their credentials;
23
+ - prices and plan limits come from a built-in catalog;
24
+ - the `execd` binary each sandbox runs is downloaded automatically.
25
+
26
+ `stackbox init` writes a `stackbox.toml` when you want to customise.
27
+
28
+ **Python** (`pip install opensandbox`; the snippet uses top-level `await`, as in Jupyter or `python -m asyncio`, and `examples/quickstart.py` is the same code as a script):
29
+
30
+ ```python
31
+ from opensandbox import Sandbox
32
+ from opensandbox.config import ConnectionConfig
33
+
34
+ gateway = ConnectionConfig(domain="http://127.0.0.1:8090", protocol="https")
35
+ sandbox = await Sandbox.create("ubuntu:24.04", connection_config=gateway)
36
+ print((await sandbox.commands.run("echo hello")).logs.stdout[0].text)
37
+ await sandbox.kill()
38
+ ```
39
+
40
+ **Harbor** (`examples/harbor.yaml`): set the environment to `opensandbox`, `domain` to the
41
+ gateway, and `protocol: https`. Then:
42
+
43
+ ```bash
44
+ harbor run -c examples/harbor.yaml
45
+ ```
46
+
47
+ ## Routing
48
+
49
+ Pick a default strategy with `[routing] strategy` or `serve --strategy`, and override it per
50
+ request, OpenRouter-style:
51
+
52
+ | Strategy | Picks |
53
+ |---|---|
54
+ | `balanced` (default) | the best mix of price and startup time |
55
+ | `cost` | the lowest list price for what the provider would bill |
56
+ | `latency` | the lowest learned time-to-sandbox for this image on this provider |
57
+ | `priority` | backends in configured `priority` order |
58
+
59
+ **Per-request preferences** go in headers (SDK: `ConnectionConfig(headers=…)`) or in sandbox
60
+ metadata (Harbor: `kwargs.metadata`):
61
+
62
+ | Header | Metadata key | Meaning |
63
+ |---|---|---|
64
+ | `X-Stackbox-Strategy: cost` | `stackbox.dev/strategy` | override the strategy |
65
+ | `X-Stackbox-Providers: e2b,modal` | `stackbox.dev/providers` (`e2b.modal`) | only these providers; without a strategy, try them in this order |
66
+ | `X-Stackbox-Max-Price: 0.10` | `stackbox.dev/max-price` | skip providers above this USD per sandbox-hour |
67
+ | `X-Stackbox-Backend: e2b` | `stackbox.dev/backend` | pin to one provider, no fallback |
68
+
69
+ **Why a sandbox goes where it does:** `stackbox route [IMAGE]` (or `GET /v1/route?image=…`) lists
70
+ every provider in order with its price, expected startup time and status, for example:
71
+ - next placement goes here;
72
+ - full: 5/5 (learned from provider refusals);
73
+ - not prepared; run `stackbox prepare …`;
74
+ - too expensive.
75
+
76
+ Errors carry the same per-provider reasons and a `hint`.
77
+
78
+ **How the routing behaves:**
79
+ - **Learns startup time:** the latency estimate is an average of observed creates per provider and
80
+ image, weighted toward recent ones. It survives restarts, and each provider gets 3 measured starts
81
+ before it can lose on its default estimate.
82
+ - **Discovers quotas:** a provider's quota refusal (Daytona's memory cap, E2B's rate limit, Modal's
83
+ resource limits, Cloudflare having no instances to hand out) is not a failure.
84
+ - The gateway caps that provider at its current count, growing the cap back by one every 15 s.
85
+ - New creates spill to other providers or wait.
86
+ - Refusals don't count toward the circuit breaker.
87
+ - **Fails over only when it's safe:**
88
+ - A clean failure (nothing was created) moves on to the next provider.
89
+ - An ambiguous one (timeout, 5xx) first checks the provider for the sandbox and adopts or deletes
90
+ what it finds.
91
+ - If the outcome can't be determined, it returns `503 STACKBOX::OUTCOME_UNKNOWN` rather than risk a
92
+ duplicate.
93
+ - **Breaker and retries:** a per-backend circuit breaker demotes a backend after repeated
94
+ failures, and retries go to other backends first.
95
+
96
+ ## CLI
97
+
98
+ | Command | What it does |
99
+ |---|---|
100
+ | `stackbox providers` | supported providers, list prices, plan limits, which you have set up, and live status |
101
+ | `stackbox init` | write `stackbox.toml` for the providers in your environment |
102
+ | `stackbox doctor` | per provider: SDK, credentials, API reachable, `execd`, image prepared; each failure comes with its fix |
103
+ | `stackbox prepare IMAGE…` | build templates and image layers, push to Vercel's registry; `-p` limits to some providers |
104
+ | `stackbox serve` | run the server (`--host`, `--port`, `--strategy`) |
105
+ | `stackbox route [IMAGE]` | where a sandbox would go right now, and why (`--strategy`, `--providers`, `--max-price`, `--cpu`, `--memory`) |
106
+ | `stackbox images [IMAGE]` | prepared images per provider |
107
+ | `stackbox sandboxes` | sandboxes the running server holds, with provider and cost (`--all` includes ended ones) |
108
+ | `stackbox placement ID` | where one sandbox went, every attempt, and its cost |
109
+ | `stackbox usage` | sandbox-hours, vCPU-hours, GiB-hours and estimated cost per provider (`--since 24h`) |
110
+
111
+ `stackbox providers` also shows live status (held/limit, placements) when a server is running.
112
+
113
+ Every command takes `-c FILE`, and the read commands take `--json`. Config is found in this order:
114
+ 1. `-c`;
115
+ 2. `$STACKBOX_CONFIG`;
116
+ 3. `./stackbox.toml`;
117
+ 4. auto-detection from credentials.
118
+
119
+ `doctor` exits 0 when everything passes, 1 when some providers have problems, and 2 when nothing
120
+ is usable.
121
+
122
+ ## Providers
123
+
124
+ | Kind | Credentials | Images | Notes |
125
+ |---|---|---|---|
126
+ | `e2b` | `E2B_API_KEY` | any image; prepared as an E2B template with `execd` (about 45 s, once) | Firecracker; a template fixes vCPU and memory |
127
+ | `modal` | `modal token set`, or `MODAL_TOKEN_ID` + `MODAL_TOKEN_SECRET` | any image; the `execd` layer is built once | gVisor; 1 vCPU = half a Modal core |
128
+ | `daytona` | `DAYTONA_API_KEY` | any image, as a declarative image Daytona builds once and caches | the account memory quota is discovered automatically |
129
+ | `vercel` | `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID` | Vercel's registry only; `prepare` pushes image + `execd` there (needs Docker once) | bills active CPU only; Hobby caps sandboxes at 45 min |
130
+ | `cloudflare` | `CLOUDFLARE_BRIDGE_URL` + `CLOUDFLARE_BRIDGE_TOKEN` | fixed when the bridge is deployed | deploy the bridge Worker once (`bridges/cloudflare`); bills active CPU only |
131
+ | `opensandbox` | `url` (+ `api_key`) in config | the server pulls them | your own OpenSandbox servers on Docker or Kubernetes |
132
+
133
+ On hosted providers the gateway emulates the OpenSandbox lifecycle on the provider's SDK and runs
134
+ OpenSandbox's `execd` in every sandbox, with a per-sandbox token. `GET /sandboxes/{id}/endpoints/44772`
135
+ returns the provider's URL and the headers to send. Command and file traffic goes from the client
136
+ to the sandbox, not through the gateway. Cloudflare is the exception: it goes through the bridge
137
+ Worker.
138
+
139
+ ## Configuration
140
+
141
+ Every field is optional except `kind`; the provider catalog fills in the rest. See
142
+ `examples/all-providers.toml` for every provider, and `stackbox init` for a starter.
143
+
144
+ ```toml
145
+ [gateway]
146
+ api_key = "env:STACKBOX_API_KEY" # required to listen beyond 127.0.0.1
147
+
148
+ [routing]
149
+ strategy = "balanced"
150
+
151
+ [[backends]]
152
+ kind = "e2b"
153
+ max_active = 50 # your plan's concurrency
154
+
155
+ [[backends]]
156
+ kind = "modal"
157
+ priority = 10
158
+ ```
159
+
160
+ - **Secrets:** `"env:NAME"` values (or `"env:A|B"`) are read from the environment, so secrets stay
161
+ out of config files.
162
+ - **Common backend fields:**
163
+ - `max_active` (concurrency);
164
+ - `priority`;
165
+ - `cost_per_vcpu_hour` and `cost_per_gib_hour`;
166
+ - `cpu_billed_fraction`, for providers that bill only active CPU;
167
+ - `max_timeout_s`;
168
+ - `default_cpu` and `default_memory_gib`;
169
+ - `auto_prepare`;
170
+ - `execd_path`, if you'd rather supply `execd` yourself.
171
+
172
+ ## HTTP API
173
+
174
+ Hosted at `https://stackbox.stacktrace.dev`, or wherever you run `stackbox serve`. Every route
175
+ except `GET /health` needs the API key in `OPEN-SANDBOX-API-KEY`.
176
+
177
+ **OpenSandbox API** (what the SDKs, Harbor and `osb` use, unchanged):
178
+
179
+ | Route | |
180
+ |---|---|
181
+ | `POST/GET /v1/sandboxes`, `GET/DELETE /v1/sandboxes/{id}` | create, list, get, delete |
182
+ | `/v1/sandboxes/{id}/endpoints/{port}`, `…/renew-expiration`, … | forwarded to the provider; hosted providers implement endpoints and renew-expiration and return `501` for the rest |
183
+ | `/v1/snapshots`, `/v1/templates` | `501 STACKBOX::UNSUPPORTED`; prepare images with `POST /v1/images` instead |
184
+
185
+ **stackbox API:**
186
+
187
+ | Route | |
188
+ |---|---|
189
+ | `GET /v1/providers`, `GET /v1/providers/{name}` | pricing, capacity (held, limit, learned quota), health, learned startup per image, prepared images, usage |
190
+ | `GET /v1/route?image=…&strategy=…&providers=…&max_price=…&cpu=…&memory_gib=…` | where a sandbox would go right now, and why, for every provider (dry run) |
191
+ | `GET /v1/placements/{sandbox_id}` | where a sandbox went, every attempt (outcome, error, duration), and its cost |
192
+ | `GET /v1/images?image=…`, `POST /v1/images` `{image, providers?}` | prepared images per provider; prepare an image |
193
+ | `GET /v1/usage?since=24h&until=…&group_by=provider\|sandbox` | sandbox-hours, vCPU-hours, GiB-hours, estimated cost |
194
+ | `POST /v1/admin/reconcile` | run cleanup now |
195
+
196
+ **Errors** are JSON: `{code: "STACKBOX::…", message, details, hint}`. The message includes the hint,
197
+ since SDKs show only the message. `SANDBOX::NOT_FOUND` keeps OpenSandbox's standard code.
198
+
199
+ ## Reliability
200
+
201
+ - **Ledger:** a SQLite ledger records intent before every upstream create, and every attempt's
202
+ outcome.
203
+ - **Reconciler:** runs periodically and on `POST /v1/admin/reconcile`:
204
+ - deletes orphans from ambiguous creates;
205
+ - expires sandboxes that vanished upstream;
206
+ - deletes leaked sandboxes carrying this gateway's managed tag.
207
+ - **Ownership:** the tag holds a per-ledger instance id, so gateways sharing an account never touch
208
+ each other's sandboxes.
209
+
210
+ ## Verified
211
+
212
+ All of these ran on real accounts with Harbor trials (an oracle agent, `ubuntu:24.04`, 1 vCPU,
213
+ 2 GiB). Full write-ups: `bench/LIVE.md` and `bench/LIVE-MORE.md`; the local fault-injection
214
+ benchmark is in `bench/RESULTS.md`.
215
+
216
+ - **Reliability:** every gateway arm passed 48/48 with no leaked sandboxes. That covers E2B, Modal,
217
+ Daytona, Vercel, Cloudflare, cost and latency routing, and quota pooling.
218
+ - **Strategy decides placement:** `cost` chose E2B, 28% cheaper than Modal. `latency` chose the
219
+ fastest-starting provider and followed it as providers sped up and slowed down.
220
+ - **Quotas:**
221
+ - Pooling two 4-sandbox quotas: 36 s, versus 69 s on one provider.
222
+ - Daytona's 10 GiB account quota: 67–71 s through the gateway, versus 199 s with Harbor's own
223
+ Daytona environment; 40–56 s with E2B taking the overflow.
224
+ - **Against Harbor's own provider environments:**
225
+ - E2B: 14% faster and 16% cheaper, traced to Harbor's E2B file transfers.
226
+ - Vercel: 48/48 through the gateway, versus 10/48 native (a per-trial registry-token rate
227
+ limit).
228
+ - Cloudflare: Harbor has no environment of its own; the gateway is how it gets there.
229
+
230
+ Gateway create p50: Modal 0.6–0.8 s, Vercel 0.9 s, E2B 1.1–1.4 s, Cloudflare 1.5 s, Daytona 2.8–3.5 s.
231
+
232
+ ## Limits and next steps
233
+
234
+ - **Latency routing learns startup time, not task runtime.** It picked Vercel for its fast starts,
235
+ but Vercel trials ran longer. Next: learn per-provider runtime from completed sandboxes.
236
+ - **Hosted providers lack some OpenSandbox features:** no egress sidecar (network policy),
237
+ pause/resume or snapshots. Only E2B exposes ports other than `execd`'s, and Modal can't extend a
238
+ running sandbox.
239
+ - **Cost is an estimate** from list prices and allocated resources, not invoices. Active-CPU
240
+ billing is approximated with `cpu_billed_fraction`.
241
+ - **Cloudflare adds a hop:** `execd` traffic goes through the bridge Worker, at about 165 ms per
242
+ command versus about 110 ms elsewhere.
243
+ - **Listing doesn't scale yet:** the list endpoint queries every backend on each call.
244
+ - **Later:** a Kubernetes backend, region and GPU matching, and per-tenant quotas.
245
+
246
+ ## Development
247
+
248
+ ```bash
249
+ git clone https://github.com/stacktrace-ai/stackbox && cd stackbox
250
+ uv sync --all-extras
251
+ uv run pytest -q # all tests run against in-memory fakes
252
+ uv run ruff check src tests bench scripts examples && uv run ruff format
253
+ ```
254
+
255
+ Contributor guidelines and the developer-experience principles are in `CLAUDE.md`. Releases ship by
256
+ pushing a `v<version>` tag; see `docs/releases/` and the `release-stackbox` skill.
@@ -0,0 +1,66 @@
1
+ [project]
2
+ name = "stackbox"
3
+ version = "0.1.0"
4
+ description = "One API for every sandbox provider: an OpenSandbox-compatible gateway that routes sandboxes across E2B, Modal, Daytona, Vercel and Cloudflare"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "LicenseRef-Proprietary"
8
+ keywords = [
9
+ "sandbox",
10
+ "opensandbox",
11
+ "e2b",
12
+ "modal",
13
+ "daytona",
14
+ "vercel",
15
+ "cloudflare",
16
+ "agents",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Environment :: Console",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.13",
23
+ ]
24
+ dependencies = [
25
+ "fastapi>=0.162.2",
26
+ "httpx>=0.28.1",
27
+ "pydantic>=2.14.3",
28
+ "uvicorn>=0.54.0",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "Stacktrace AI, Inc"
33
+ email = "founders@stacktrace.ai"
34
+
35
+ [project.urls]
36
+ Homepage = "https://stacktrace.ai"
37
+
38
+ [project.scripts]
39
+ stackbox = "stackbox:main"
40
+
41
+ [project.optional-dependencies]
42
+ modal = [
43
+ "aiohttp-socks>=0.12.0",
44
+ "modal>=1.6.3",
45
+ "python-socks>=3.2.1",
46
+ ]
47
+ e2b = ["e2b>=2.63.0"]
48
+ vercel = ["vercel-sandbox>=0.11.0"]
49
+ daytona = ["daytona>=0.231.0"]
50
+ all = ["stackbox[e2b,modal,daytona,vercel]"]
51
+
52
+ [build-system]
53
+ requires = ["uv_build>=0.11.32,<0.12.0"]
54
+ build-backend = "uv_build"
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "opensandbox>=1.2.0",
59
+ "pytest>=9.2.0",
60
+ "pytest-asyncio>=1.4.1.post1",
61
+ "ruff>=0.17.4",
62
+ ]
63
+
64
+ [tool.pytest.ini_options]
65
+ asyncio_mode = "auto"
66
+ pythonpath = ["."]
@@ -0,0 +1,64 @@
1
+ [project]
2
+ name = "stackbox"
3
+ version = "0.1.0"
4
+ description = "One API for every sandbox provider: an OpenSandbox-compatible gateway that routes sandboxes across E2B, Modal, Daytona, Vercel and Cloudflare"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ # Proprietary, like stacktrace-cli. SPDX LicenseRef- is the expression form
8
+ # for a licence with no public identifier.
9
+ license = "LicenseRef-Proprietary"
10
+ authors = [{ name = "Stacktrace AI, Inc", email = "founders@stacktrace.ai" }]
11
+ keywords = ["sandbox", "opensandbox", "e2b", "modal", "daytona", "vercel", "cloudflare", "agents"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.13",
17
+ ]
18
+ dependencies = [
19
+ "fastapi>=0.162.2",
20
+ "httpx>=0.28.1",
21
+ "pydantic>=2.14.3",
22
+ "uvicorn>=0.54.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://stacktrace.ai"
27
+
28
+ [project.scripts]
29
+ stackbox = "stackbox:main"
30
+
31
+ [project.optional-dependencies]
32
+ modal = [
33
+ "aiohttp-socks>=0.12.0",
34
+ "modal>=1.6.3",
35
+ "python-socks>=3.2.1",
36
+ ]
37
+ e2b = [
38
+ "e2b>=2.63.0",
39
+ ]
40
+ vercel = [
41
+ "vercel-sandbox>=0.11.0",
42
+ ]
43
+ daytona = [
44
+ "daytona>=0.231.0",
45
+ ]
46
+ all = [
47
+ "stackbox[e2b,modal,daytona,vercel]",
48
+ ]
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.11.32,<0.12.0"]
52
+ build-backend = "uv_build"
53
+
54
+ [dependency-groups]
55
+ dev = [
56
+ "opensandbox>=1.2.0",
57
+ "pytest>=9.2.0",
58
+ "pytest-asyncio>=1.4.1.post1",
59
+ "ruff>=0.17.4",
60
+ ]
61
+
62
+ [tool.pytest.ini_options]
63
+ asyncio_mode = "auto"
64
+ pythonpath = ["."]