@cloudflare/sandbox 0.13.0-next.651.1 → 0.13.0-next.709.1

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. package/Dockerfile +12 -17
  2. package/README.md +48 -2
  3. package/dist/bridge/index.d.ts.map +1 -1
  4. package/dist/bridge/index.js +1365 -1054
  5. package/dist/bridge/index.js.map +1 -1
  6. package/dist/{contexts-BS0Bs6IU.d.ts → contexts-1EsLHByO.d.ts} +240 -15
  7. package/dist/contexts-1EsLHByO.d.ts.map +1 -0
  8. package/dist/{dist-DF8sudAg.js → dist-Duor5GbS.js} +38 -147
  9. package/dist/dist-Duor5GbS.js.map +1 -0
  10. package/dist/errors/index.d.ts +4 -0
  11. package/dist/errors/index.js +4 -0
  12. package/dist/{errors-BG6NZiPD.js → errors-CXR0xBpw.js} +82 -13
  13. package/dist/errors-CXR0xBpw.js.map +1 -0
  14. package/dist/errors-QYlSkVGz.js +893 -0
  15. package/dist/errors-QYlSkVGz.js.map +1 -0
  16. package/dist/extensions/index.d.ts +4 -74
  17. package/dist/extensions/index.js +5 -152
  18. package/dist/extensions-CFB2xHqY.js +1023 -0
  19. package/dist/extensions-CFB2xHqY.js.map +1 -0
  20. package/dist/filesystem-BWAZCZER.d.ts +732 -0
  21. package/dist/filesystem-BWAZCZER.d.ts.map +1 -0
  22. package/dist/git/index.d.ts +63 -0
  23. package/dist/git/index.d.ts.map +1 -0
  24. package/dist/git/index.js +338 -0
  25. package/dist/git/index.js.map +1 -0
  26. package/dist/index-Bs4bqXDR.d.ts +438 -0
  27. package/dist/index-Bs4bqXDR.d.ts.map +1 -0
  28. package/dist/index-HNYBk-az.d.ts +444 -0
  29. package/dist/index-HNYBk-az.d.ts.map +1 -0
  30. package/dist/index.d.ts +487 -181
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +12 -43
  33. package/dist/index.js.map +1 -1
  34. package/dist/interpreter/index.d.ts +311 -0
  35. package/dist/interpreter/index.d.ts.map +1 -0
  36. package/dist/interpreter/index.js +292 -0
  37. package/dist/interpreter/index.js.map +1 -0
  38. package/dist/openai/index.d.ts +5 -4
  39. package/dist/openai/index.d.ts.map +1 -1
  40. package/dist/openai/index.js +11 -6
  41. package/dist/openai/index.js.map +1 -1
  42. package/dist/opencode/index.d.ts +133 -161
  43. package/dist/opencode/index.d.ts.map +1 -1
  44. package/dist/opencode/index.js +284 -203
  45. package/dist/opencode/index.js.map +1 -1
  46. package/dist/process-types-GStiZ8f8.d.ts +73 -0
  47. package/dist/process-types-GStiZ8f8.d.ts.map +1 -0
  48. package/dist/sandbox-Auuwfnur.js +10010 -0
  49. package/dist/sandbox-Auuwfnur.js.map +1 -0
  50. package/dist/sandbox-BbAabq93.d.ts +42 -0
  51. package/dist/sandbox-BbAabq93.d.ts.map +1 -0
  52. package/dist/xterm/index.d.ts +11 -7
  53. package/dist/xterm/index.d.ts.map +1 -1
  54. package/dist/xterm/index.js +61 -18
  55. package/dist/xterm/index.js.map +1 -1
  56. package/package.json +28 -4
  57. package/dist/contexts-BS0Bs6IU.d.ts.map +0 -1
  58. package/dist/dist-DF8sudAg.js.map +0 -1
  59. package/dist/errors-BG6NZiPD.js.map +0 -1
  60. package/dist/extensions/index.d.ts.map +0 -1
  61. package/dist/extensions/index.js.map +0 -1
  62. package/dist/rpc-types-PBUY-xXM.d.ts +0 -1679
  63. package/dist/rpc-types-PBUY-xXM.d.ts.map +0 -1
  64. package/dist/sandbox-BgwMBQ7S.js +0 -8273
  65. package/dist/sandbox-BgwMBQ7S.js.map +0 -1
  66. package/dist/sandbox-D_MMqExx.d.ts +0 -1077
  67. package/dist/sandbox-D_MMqExx.d.ts.map +0 -1
@@ -1,6 +1,8 @@
1
- import "../dist-DF8sudAg.js";
2
- import "../errors-BG6NZiPD.js";
3
- import { S as validateTunnelName, r as getSandbox, w as streamFile, x as validatePort, y as SandboxSecurityError } from "../sandbox-BgwMBQ7S.js";
1
+ import "../errors-CXR0xBpw.js";
2
+ import "../errors-QYlSkVGz.js";
3
+ import "../dist-Duor5GbS.js";
4
+ import { g as validateTunnelName, h as validatePort, m as SandboxSecurityError, n as getSandbox$1, x as streamFile } from "../sandbox-Auuwfnur.js";
5
+ import "../extensions-CFB2xHqY.js";
4
6
  import { DurableObject, env } from "cloudflare:workers";
5
7
  import { Hono } from "hono";
6
8
 
@@ -27,16 +29,6 @@ async function primePool(env$1, warmPoolBinding) {
27
29
  /**
28
30
  * Utility functions used by the bridge routes.
29
31
  */
30
- /**
31
- * UTF-8-safe base64 encoding.
32
- * btoa() only handles latin-1; encode to UTF-8 bytes first via TextEncoder.
33
- */
34
- function toBase64(str) {
35
- const bytes = new TextEncoder().encode(str);
36
- let binary = "";
37
- for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
38
- return btoa(binary);
39
- }
40
32
  /** RFC 4648 base32 encoding (lowercase). Returns only [a-z2-7]. */
41
33
  function base32Encode(data) {
42
34
  const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
@@ -94,10 +86,10 @@ function resolveWorkspacePath(userPath) {
94
86
  return null;
95
87
  }
96
88
  /**
97
- * Validate a session ID. Rejects path traversal, control chars, and excessive length.
89
+ * Validate a terminal ID. Rejects path traversal, control chars, and excessive length.
98
90
  * Returns the validated ID or null if invalid.
99
91
  */
100
- function validateSessionId(id) {
92
+ function validateTerminalId(id) {
101
93
  if (!/^[a-zA-Z0-9._-]{1,128}$/.test(id)) return null;
102
94
  if (id.includes("..")) return null;
103
95
  return id;
@@ -108,24 +100,73 @@ function validateSessionId(id) {
108
100
  */
109
101
  function sseToByteStream(sse) {
110
102
  const encoder = new TextEncoder();
111
- return new ReadableStream({ async start(controller) {
112
- try {
113
- for await (const chunk of streamFile(sse)) controller.enqueue(chunk instanceof Uint8Array ? chunk : encoder.encode(chunk));
114
- controller.close();
115
- } catch (err) {
116
- controller.error(err);
103
+ const iterator = streamFile(sse)[Symbol.asyncIterator]();
104
+ let finished = false;
105
+ async function finish(cancelSource) {
106
+ if (finished) return;
107
+ finished = true;
108
+ if (!cancelSource) return;
109
+ await iterator.return?.().catch(() => void 0);
110
+ }
111
+ return new ReadableStream({
112
+ async pull(controller) {
113
+ try {
114
+ const result = await iterator.next();
115
+ if (result.done) {
116
+ await finish(false);
117
+ controller.close();
118
+ return;
119
+ }
120
+ controller.enqueue(result.value instanceof Uint8Array ? result.value : encoder.encode(result.value));
121
+ } catch (error) {
122
+ await finish(true);
123
+ controller.error(error);
124
+ }
125
+ },
126
+ async cancel() {
127
+ await finish(true);
128
+ }
129
+ }, { highWaterMark: 0 });
130
+ }
131
+ function withStreamCleanup(source, cleanup) {
132
+ const reader = source.getReader();
133
+ let finished = false;
134
+ async function finish(cancelSource) {
135
+ if (finished) return;
136
+ finished = true;
137
+ if (cancelSource) await reader.cancel().catch(() => void 0);
138
+ reader.releaseLock();
139
+ await cleanup().catch(() => void 0);
140
+ }
141
+ return new ReadableStream({
142
+ async pull(controller) {
143
+ try {
144
+ const result = await reader.read();
145
+ if (result.done) {
146
+ await finish(false);
147
+ controller.close();
148
+ return;
149
+ }
150
+ controller.enqueue(result.value);
151
+ } catch (error) {
152
+ await finish(true);
153
+ controller.error(error);
154
+ }
155
+ },
156
+ async cancel() {
157
+ await finish(true);
117
158
  }
118
- } });
159
+ }, { highWaterMark: 0 });
119
160
  }
120
161
 
121
162
  //#endregion
122
- //#region src/bridge/openapi.ts
163
+ //#region src/bridge/openapi/common.ts
123
164
  /**
124
165
  * OpenAPI 3.1 schema for the Cloudflare Sandbox Service API.
125
166
  *
126
167
  * Served at GET /v1/openapi.json (requires Bearer token auth).
127
168
  */
128
- const OPENAPI_SCHEMA = {
169
+ const OPENAPI_BASE = {
129
170
  openapi: "3.1.0",
130
171
  info: {
131
172
  title: "Cloudflare Sandbox Service API",
@@ -139,7 +180,7 @@ const OPENAPI_SCHEMA = {
139
180
  description: "API token set via `wrangler secret put SANDBOX_API_KEY`. The /openapi.* routes also accept the token as a `?token=` query parameter."
140
181
  } },
141
182
  schemas: {
142
- ExecRequest: {
183
+ ProcessCreateRequest: {
143
184
  type: "object",
144
185
  required: ["argv"],
145
186
  properties: {
@@ -147,25 +188,121 @@ const OPENAPI_SCHEMA = {
147
188
  type: "array",
148
189
  items: { type: "string" },
149
190
  minItems: 1,
150
- description: "Argv array already shell-expanded by the Python layer if shell=True.",
191
+ description: "Argv array to launch directly.",
151
192
  example: [
152
193
  "sh",
153
194
  "-lc",
154
195
  "echo hello"
155
196
  ]
156
197
  },
157
- timeout_ms: {
198
+ timeout: {
158
199
  type: "integer",
159
- description: "Per-call timeout in milliseconds.",
200
+ description: "Remote process lifetime deadline in milliseconds. When reached, the sandbox may stop the process and the process exit outcome reports `timedOut: true`; this is not caller-local observation cancellation.",
160
201
  example: 3e4
161
202
  },
162
203
  cwd: {
163
204
  type: "string",
164
205
  description: "Working directory for the command (defaults to sandbox cwd).",
165
206
  example: "/workspace"
207
+ },
208
+ env: {
209
+ type: "object",
210
+ additionalProperties: { type: "string" },
211
+ description: "Environment overrides for the process."
166
212
  }
167
213
  }
168
214
  },
215
+ ProcessStatus: {
216
+ oneOf: [
217
+ {
218
+ type: "object",
219
+ required: [
220
+ "id",
221
+ "pid",
222
+ "command",
223
+ "state",
224
+ "startedAt"
225
+ ],
226
+ properties: {
227
+ id: { type: "string" },
228
+ pid: { type: "integer" },
229
+ command: {
230
+ type: "array",
231
+ items: { type: "string" }
232
+ },
233
+ cwd: { type: "string" },
234
+ state: { const: "running" },
235
+ startedAt: { type: "string" }
236
+ }
237
+ },
238
+ {
239
+ type: "object",
240
+ required: [
241
+ "id",
242
+ "pid",
243
+ "command",
244
+ "state",
245
+ "startedAt",
246
+ "endedAt",
247
+ "exit"
248
+ ],
249
+ properties: {
250
+ id: { type: "string" },
251
+ pid: { type: "integer" },
252
+ command: {
253
+ type: "array",
254
+ items: { type: "string" }
255
+ },
256
+ cwd: { type: "string" },
257
+ state: { const: "exited" },
258
+ startedAt: { type: "string" },
259
+ endedAt: { type: "string" },
260
+ exit: {
261
+ type: "object",
262
+ required: ["code", "timedOut"],
263
+ properties: {
264
+ code: { type: "integer" },
265
+ signal: { type: "integer" },
266
+ timedOut: { type: "boolean" }
267
+ }
268
+ }
269
+ }
270
+ },
271
+ {
272
+ type: "object",
273
+ required: [
274
+ "id",
275
+ "pid",
276
+ "command",
277
+ "state",
278
+ "startedAt",
279
+ "endedAt",
280
+ "error"
281
+ ],
282
+ properties: {
283
+ id: { type: "string" },
284
+ pid: { type: "integer" },
285
+ command: {
286
+ type: "array",
287
+ items: { type: "string" }
288
+ },
289
+ cwd: { type: "string" },
290
+ state: { const: "error" },
291
+ startedAt: { type: "string" },
292
+ endedAt: { type: "string" },
293
+ error: {
294
+ type: "object",
295
+ required: ["code", "message"],
296
+ properties: {
297
+ code: { type: "string" },
298
+ message: { type: "string" }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ ],
304
+ discriminator: { propertyName: "state" }
305
+ },
169
306
  WriteResponse: {
170
307
  type: "object",
171
308
  required: ["ok"],
@@ -339,7 +476,6 @@ const OPENAPI_SCHEMA = {
339
476
  "pool_error",
340
477
  "mount_error",
341
478
  "unmount_error",
342
- "session_error",
343
479
  "tunnel_error"
344
480
  ]
345
481
  }
@@ -401,459 +537,106 @@ const OPENAPI_SCHEMA = {
401
537
  code: "invalid_request"
402
538
  }
403
539
  } }
540
+ },
541
+ NotFound: {
542
+ description: "Requested resource was not found.",
543
+ content: { "application/json": {
544
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
545
+ example: {
546
+ error: "Process not found",
547
+ code: "not_found"
548
+ }
549
+ } }
550
+ },
551
+ BadGateway: {
552
+ description: "Sandbox operation failed.",
553
+ content: { "application/json": {
554
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
555
+ example: {
556
+ error: "process launch failed",
557
+ code: "process_error"
558
+ }
559
+ } }
404
560
  }
405
561
  }
406
562
  },
407
- security: [{ BearerAuth: [] }],
408
- paths: {
409
- "/v1/sandbox": { post: {
410
- operationId: "createSandbox",
411
- summary: "Create a new sandbox session",
412
- description: "Generates a new unique sandbox ID. Use this ID with all `/v1/sandbox/{id}/*` routes.",
413
- "x-codeSamples": [{
414
- lang: "curl",
415
- label: "cURL",
416
- source: "curl -X POST https://$HOST/v1/sandbox \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
417
- }],
418
- responses: {
419
- "200": {
420
- description: "New sandbox session created.",
421
- content: { "application/json": { schema: {
422
- type: "object",
423
- required: ["id"],
424
- properties: { id: {
425
- type: "string",
426
- description: "Unique sandbox ID for use with `/v1/sandbox/{id}/*` routes.",
427
- example: "mfrggzdfmy2tqnrzgezdgnbv"
428
- } }
429
- } } }
430
- },
431
- "401": { $ref: "#/components/responses/Unauthorized" }
432
- }
433
- } },
434
- "/v1/sandbox/{id}/exec": { post: {
435
- operationId: "execCommand",
436
- summary: "Execute a command in the sandbox",
437
- description: "Runs a shell command inside the named sandbox and streams output as Server-Sent Events (SSE). Events: `stdout` (base64 chunk), `stderr` (base64 chunk), `exit` (JSON with exit_code), `error` (JSON with error and code). The stream terminates after an `exit` or `error` event.",
563
+ security: [{ BearerAuth: [] }]
564
+ };
565
+
566
+ //#endregion
567
+ //#region src/bridge/openapi/files.ts
568
+ const FILESPaths = {
569
+ "/v1/sandbox/{id}/file/{path}": {
570
+ get: {
571
+ operationId: "readFile",
572
+ summary: "Read a file from the sandbox filesystem",
438
573
  "x-codeSamples": [{
439
574
  lang: "curl",
440
575
  label: "cURL",
441
- source: "curl -N -X POST https://$HOST/v1/sandbox/my-sandbox/exec \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"argv\":[\"sh\",\"-lc\",\"echo hello\"]}'"
576
+ source: "curl -X GET https://$HOST/v1/sandbox/my-sandbox/file/workspace/main.py \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -o main.py"
442
577
  }],
443
578
  parameters: [{
444
579
  name: "id",
445
580
  in: "path",
446
581
  required: true,
447
582
  schema: { type: "string" },
448
- description: "Sandbox instance name (maps to a Durable Object key)."
583
+ description: "Sandbox instance name."
449
584
  }, {
450
- name: "Session-Id",
451
- in: "header",
452
- required: false,
453
- schema: {
454
- type: "string",
455
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
456
- },
457
- description: "Scope this operation to a specific session. Uses the default session if omitted."
458
- }],
459
- requestBody: {
585
+ name: "path",
586
+ in: "path",
460
587
  required: true,
461
- content: { "application/json": { schema: { $ref: "#/components/schemas/ExecRequest" } } }
462
- },
588
+ schema: { type: "string" },
589
+ description: "File path inside the sandbox, without leading slash (e.g. workspace/main.py). Must resolve within /workspace."
590
+ }],
463
591
  responses: {
464
592
  "200": {
465
- description: "SSE stream of command output. Events:\n- `event: stdout` — data is a base64-encoded chunk of stdout\n- `event: stderr` — data is a base64-encoded chunk of stderr\n- `event: exit` — data is JSON `{\"exit_code\": N}` (terminal)\n- `event: error` — data is JSON `{\"error\": \"...\", \"code\": \"...\"}` (terminal)",
466
- content: { "text/event-stream": { schema: { type: "string" } } }
593
+ description: "Raw file bytes.",
594
+ content: { "application/octet-stream": { schema: {
595
+ type: "string",
596
+ format: "binary"
597
+ } } }
467
598
  },
468
599
  "400": { $ref: "#/components/responses/InvalidRequest" },
469
600
  "401": { $ref: "#/components/responses/Unauthorized" },
470
601
  "403": {
471
- description: "cwd resolves outside /workspace.",
602
+ description: "Path resolves outside /workspace.",
472
603
  content: { "application/json": {
473
604
  schema: { $ref: "#/components/schemas/ErrorResponse" },
474
605
  example: {
475
- error: "cwd must resolve to a location within /workspace",
606
+ error: "path must resolve to a location within /workspace",
476
607
  code: "invalid_request"
477
608
  }
478
609
  } }
479
610
  },
480
- "502": {
481
- description: "SDK transport error before the SSE stream could be established. Once the stream is open, errors are delivered as `event: error` SSE events instead.",
482
- content: { "application/json": {
483
- schema: { $ref: "#/components/schemas/ErrorResponse" },
484
- example: {
485
- error: "exec failed: connection reset",
486
- code: "exec_transport_error"
487
- }
488
- } }
489
- }
490
- }
491
- } },
492
- "/v1/sandbox/{id}/tunnel/{port}": {
493
- post: {
494
- operationId: "createTunnel",
495
- summary: "Create or reuse a tunnel for a sandbox port",
496
- description: "Returns an existing tunnel for the port when one is already recorded, or provisions one when needed. The service must already be listening inside the sandbox. Omit `name` for an ephemeral `*.trycloudflare.com` tunnel, or pass `name` to choose the subdomain prefix for a named tunnel. Use a value such as `app`, not a full hostname.",
497
- "x-codeSamples": [{
498
- lang: "curl",
499
- label: "Ephemeral tunnel",
500
- source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/tunnel/8080 \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
501
- }, {
502
- lang: "curl",
503
- label: "Named tunnel",
504
- source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/tunnel/8080 \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"app\"}'"
505
- }],
506
- parameters: [{
507
- name: "id",
508
- in: "path",
509
- required: true,
510
- schema: { type: "string" },
511
- description: "Sandbox instance name."
512
- }, {
513
- name: "port",
514
- in: "path",
515
- required: true,
516
- schema: {
517
- type: "integer",
518
- minimum: 1024,
519
- maximum: 65535
520
- },
521
- description: "Container port to tunnel. Port 3000 is reserved."
522
- }],
523
- requestBody: {
524
- required: false,
525
- content: { "application/json": { schema: { $ref: "#/components/schemas/TunnelRequest" } } }
526
- },
527
- responses: {
528
- "200": {
529
- description: "Tunnel created or reused.",
530
- content: { "application/json": { schema: { $ref: "#/components/schemas/Tunnel" } } }
531
- },
532
- "400": { $ref: "#/components/responses/InvalidRequest" },
533
- "401": { $ref: "#/components/responses/Unauthorized" },
534
- "502": {
535
- description: "Tunnel creation failed.",
536
- content: { "application/json": {
537
- schema: { $ref: "#/components/schemas/ErrorResponse" },
538
- example: {
539
- error: "tunnel failed: cloudflared could not be found",
540
- code: "tunnel_error"
541
- }
542
- } }
543
- }
544
- }
545
- },
546
- delete: {
547
- operationId: "deleteTunnel",
548
- summary: "Delete the tunnel for a sandbox port",
549
- description: "Stops the tunnel process for the port and removes any named-tunnel Cloudflare resources tracked by the sandbox.",
550
- "x-codeSamples": [{
551
- lang: "curl",
552
- label: "Delete tunnel",
553
- source: "curl -X DELETE https://$HOST/v1/sandbox/my-sandbox/tunnel/8080 \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
554
- }],
555
- parameters: [{
556
- name: "id",
557
- in: "path",
558
- required: true,
559
- schema: { type: "string" },
560
- description: "Sandbox instance name."
561
- }, {
562
- name: "port",
563
- in: "path",
564
- required: true,
565
- schema: {
566
- type: "integer",
567
- minimum: 1024,
568
- maximum: 65535
569
- },
570
- description: "Container port whose tunnel should be deleted. Port 3000 is reserved."
571
- }],
572
- responses: {
573
- "204": { description: "Tunnel deleted or already absent." },
574
- "400": { $ref: "#/components/responses/InvalidRequest" },
575
- "401": { $ref: "#/components/responses/Unauthorized" },
576
- "502": {
577
- description: "Tunnel deletion failed.",
578
- content: { "application/json": {
579
- schema: { $ref: "#/components/schemas/ErrorResponse" },
580
- example: {
581
- error: "tunnel failed: cleanup failed",
582
- code: "tunnel_error"
583
- }
584
- } }
585
- }
586
- }
587
- }
588
- },
589
- "/v1/sandbox/{id}/file/{path}": {
590
- get: {
591
- operationId: "readFile",
592
- summary: "Read a file from the sandbox filesystem",
593
- "x-codeSamples": [{
594
- lang: "curl",
595
- label: "cURL",
596
- source: "curl -X GET https://$HOST/v1/sandbox/my-sandbox/file/workspace/main.py \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -o main.py"
597
- }],
598
- parameters: [
599
- {
600
- name: "id",
601
- in: "path",
602
- required: true,
603
- schema: { type: "string" },
604
- description: "Sandbox instance name."
605
- },
606
- {
607
- name: "path",
608
- in: "path",
609
- required: true,
610
- schema: { type: "string" },
611
- description: "File path inside the sandbox, without leading slash (e.g. workspace/main.py). Must resolve within /workspace."
612
- },
613
- {
614
- name: "Session-Id",
615
- in: "header",
616
- required: false,
617
- schema: {
618
- type: "string",
619
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
620
- },
621
- description: "Scope this operation to a specific session. Uses the default session if omitted."
622
- }
623
- ],
624
- responses: {
625
- "200": {
626
- description: "Raw file bytes.",
627
- content: { "application/octet-stream": { schema: {
628
- type: "string",
629
- format: "binary"
630
- } } }
631
- },
632
- "400": { $ref: "#/components/responses/InvalidRequest" },
633
- "401": { $ref: "#/components/responses/Unauthorized" },
634
- "403": {
635
- description: "Path resolves outside /workspace.",
636
- content: { "application/json": {
637
- schema: { $ref: "#/components/schemas/ErrorResponse" },
638
- example: {
639
- error: "path must resolve to a location within /workspace",
640
- code: "invalid_request"
641
- }
642
- } }
643
- },
644
- "404": {
645
- description: "File not found in the sandbox.",
646
- content: { "application/json": {
647
- schema: { $ref: "#/components/schemas/ErrorResponse" },
648
- example: {
649
- error: "File not found: /workspace/foo.txt",
650
- code: "workspace_read_not_found"
651
- }
652
- } }
653
- },
654
- "502": {
655
- description: "SDK read call failed.",
656
- content: { "application/json": {
657
- schema: { $ref: "#/components/schemas/ErrorResponse" },
658
- example: {
659
- error: "read failed: connection reset",
660
- code: "exec_transport_error"
661
- }
662
- } }
663
- }
664
- }
665
- },
666
- put: {
667
- operationId: "writeFile",
668
- summary: "Write a file into the sandbox filesystem",
669
- "x-codeSamples": [{
670
- lang: "curl",
671
- label: "cURL",
672
- source: "curl -X PUT https://$HOST/v1/sandbox/my-sandbox/file/workspace/main.py \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/octet-stream\" \\\n --data-binary @main.py"
673
- }],
674
- parameters: [
675
- {
676
- name: "id",
677
- in: "path",
678
- required: true,
679
- schema: { type: "string" },
680
- description: "Sandbox instance name."
681
- },
682
- {
683
- name: "path",
684
- in: "path",
685
- required: true,
686
- schema: { type: "string" },
687
- description: "File path inside the sandbox, without leading slash (e.g. workspace/main.py). Must resolve within /workspace."
688
- },
689
- {
690
- name: "Session-Id",
691
- in: "header",
692
- required: false,
693
- schema: {
694
- type: "string",
695
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
696
- },
697
- description: "Scope this operation to a specific session. Uses the default session if omitted."
698
- }
699
- ],
700
- requestBody: {
701
- required: true,
702
- description: "Raw file content to write.",
703
- content: { "application/octet-stream": { schema: {
704
- type: "string",
705
- format: "binary"
706
- } } }
707
- },
708
- responses: {
709
- "200": {
710
- description: "File written successfully.",
711
- content: { "application/json": { schema: { $ref: "#/components/schemas/WriteResponse" } } }
712
- },
713
- "400": { $ref: "#/components/responses/InvalidRequest" },
714
- "401": { $ref: "#/components/responses/Unauthorized" },
715
- "403": {
716
- description: "Path resolves outside /workspace.",
717
- content: { "application/json": {
718
- schema: { $ref: "#/components/schemas/ErrorResponse" },
719
- example: {
720
- error: "path must resolve to a location within /workspace",
721
- code: "invalid_request"
722
- }
723
- } }
724
- },
725
- "502": {
726
- description: "SDK write call failed.",
727
- content: { "application/json": {
728
- schema: { $ref: "#/components/schemas/ErrorResponse" },
729
- example: {
730
- error: "write failed: connection reset",
731
- code: "workspace_archive_write_error"
732
- }
733
- } }
734
- }
735
- }
736
- }
737
- },
738
- "/v1/sandbox/{id}/pty": { get: {
739
- operationId: "ptyTerminal",
740
- summary: "Open a PTY terminal session via WebSocket",
741
- description: "Upgrades the HTTP connection to a WebSocket and proxies it to a PTY shell inside the sandbox.\n\n**WebSocket frame protocol:**\n\n| Direction | Frame type | Content |\n|-----------|------------|--------------------------------------------------|\n| Client → Server | Binary | UTF-8 encoded keystrokes / input |\n| Server → Client | Binary | Terminal output (including ANSI escape sequences) |\n| Client → Server | Text (JSON) | Control messages, e.g. `{\"type\":\"resize\",\"cols\":120,\"rows\":30}` |\n| Server → Client | Text (JSON) | Status messages: `ready`, `exit`, `error` |\n\n**Status messages (server → client):**\n- `{\"type\":\"ready\"}` — PTY is accepting input\n- `{\"type\":\"exit\",\"code\":0,\"signal\":\"SIGTERM\"}` — PTY exited\n- `{\"type\":\"error\",\"message\":\"...\"}` — error occurred\n\nIf the client disconnects, the PTY stays alive; reconnecting replays buffered output.",
742
- "x-codeSamples": [{
743
- lang: "JavaScript",
744
- label: "WebSocket",
745
- source: "const ws = new WebSocket(\"wss://$HOST/v1/sandbox/my-sandbox/pty?cols=120&rows=30\");\nws.binaryType = \"arraybuffer\";\nws.onmessage = (e) => { /* handle binary output or JSON status */ };"
746
- }],
747
- parameters: [
748
- {
749
- name: "id",
750
- in: "path",
751
- required: true,
752
- schema: { type: "string" },
753
- description: "Sandbox instance name."
754
- },
755
- {
756
- name: "cols",
757
- in: "query",
758
- required: false,
759
- schema: {
760
- type: "integer",
761
- default: 80
762
- },
763
- description: "Terminal width in columns."
764
- },
765
- {
766
- name: "rows",
767
- in: "query",
768
- required: false,
769
- schema: {
770
- type: "integer",
771
- default: 24
772
- },
773
- description: "Terminal height in rows."
774
- },
775
- {
776
- name: "shell",
777
- in: "query",
778
- required: false,
779
- schema: { type: "string" },
780
- description: "Shell binary to run (e.g. `/bin/bash`). Uses the container default if omitted."
781
- },
782
- {
783
- name: "session",
784
- in: "query",
785
- required: false,
786
- schema: { type: "string" },
787
- description: "SDK session ID. If provided, the PTY is scoped to this session."
788
- },
789
- {
790
- name: "Session-Id",
791
- in: "header",
792
- required: false,
793
- schema: {
794
- type: "string",
795
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
796
- },
797
- description: "Scope this operation to a specific session. Uses the default session if omitted."
798
- }
799
- ],
800
- responses: {
801
- "101": { description: "WebSocket upgrade successful. Binary and text frames flow bidirectionally as described above." },
802
- "400": {
803
- description: "Missing `Upgrade: websocket` header or invalid query parameters.",
611
+ "404": {
612
+ description: "File not found in the sandbox.",
804
613
  content: { "application/json": {
805
614
  schema: { $ref: "#/components/schemas/ErrorResponse" },
806
615
  example: {
807
- error: "WebSocket upgrade required",
808
- code: "invalid_request"
616
+ error: "File not found: /workspace/foo.txt",
617
+ code: "workspace_read_not_found"
809
618
  }
810
619
  } }
811
620
  },
812
- "401": { $ref: "#/components/responses/Unauthorized" },
813
621
  "502": {
814
- description: "SDK terminal() call failed.",
622
+ description: "SDK read call failed.",
815
623
  content: { "application/json": {
816
624
  schema: { $ref: "#/components/schemas/ErrorResponse" },
817
625
  example: {
818
- error: "terminal failed: connection reset",
626
+ error: "read failed: connection reset",
819
627
  code: "exec_transport_error"
820
628
  }
821
629
  } }
822
630
  }
823
631
  }
824
- } },
825
- "/v1/sandbox/{id}/running": { get: {
826
- operationId: "isSandboxRunning",
827
- summary: "Check whether the sandbox container is alive",
828
- description: "Executes a no-op command inside the sandbox. Always returns HTTP 200; inspect the `running` field.",
829
- "x-codeSamples": [{
830
- lang: "curl",
831
- label: "cURL",
832
- source: "curl -X GET https://$HOST/v1/sandbox/my-sandbox/running \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
833
- }],
834
- parameters: [{
835
- name: "id",
836
- in: "path",
837
- required: true,
838
- schema: { type: "string" },
839
- description: "Sandbox instance name."
840
- }],
841
- responses: {
842
- "200": {
843
- description: "Liveness status (always returned, even when not running).",
844
- content: { "application/json": { schema: { $ref: "#/components/schemas/RunningResponse" } } }
845
- },
846
- "401": { $ref: "#/components/responses/Unauthorized" }
847
- }
848
- } },
849
- "/v1/sandbox/{id}/persist": { post: {
850
- operationId: "persistWorkspace",
851
- summary: "Serialize the sandbox workspace to a tar archive",
852
- description: "Archives the /workspace directory inside the sandbox and streams the resulting tar back as raw bytes.",
632
+ },
633
+ put: {
634
+ operationId: "writeFile",
635
+ summary: "Write a file into the sandbox filesystem",
853
636
  "x-codeSamples": [{
854
637
  lang: "curl",
855
638
  label: "cURL",
856
- source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/persist \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -o workspace.tar"
639
+ source: "curl -X PUT https://$HOST/v1/sandbox/my-sandbox/file/workspace/main.py \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/octet-stream\" \\\n --data-binary @main.py"
857
640
  }],
858
641
  parameters: [{
859
642
  name: "id",
@@ -862,63 +645,15 @@ const OPENAPI_SCHEMA = {
862
645
  schema: { type: "string" },
863
646
  description: "Sandbox instance name."
864
647
  }, {
865
- name: "excludes",
866
- in: "query",
867
- required: false,
868
- schema: { type: "string" },
869
- description: "Comma-separated list of relative paths to exclude from the archive.",
870
- example: "__pycache__,.venv"
871
- }],
872
- responses: {
873
- "200": {
874
- description: "Raw tar archive bytes.",
875
- content: { "application/octet-stream": { schema: {
876
- type: "string",
877
- format: "binary"
878
- } } }
879
- },
880
- "400": {
881
- description: "Invalid exclude paths (e.g. path traversal).",
882
- content: { "application/json": {
883
- schema: { $ref: "#/components/schemas/ErrorResponse" },
884
- example: {
885
- error: "exclude paths must not contain \"..\"",
886
- code: "invalid_request"
887
- }
888
- } }
889
- },
890
- "401": { $ref: "#/components/responses/Unauthorized" },
891
- "502": {
892
- description: "tar command failed inside the sandbox.",
893
- content: { "application/json": {
894
- schema: { $ref: "#/components/schemas/ErrorResponse" },
895
- example: {
896
- error: "tar failed (exit 1): ...",
897
- code: "workspace_archive_read_error"
898
- }
899
- } }
900
- }
901
- }
902
- } },
903
- "/v1/sandbox/{id}/hydrate": { post: {
904
- operationId: "hydrateWorkspace",
905
- summary: "Populate the sandbox workspace from a tar archive",
906
- description: "Accepts a raw tar archive as the request body and extracts it into /workspace inside the sandbox.",
907
- "x-codeSamples": [{
908
- lang: "curl",
909
- label: "cURL",
910
- source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/hydrate \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/octet-stream\" \\\n --data-binary @workspace.tar"
911
- }],
912
- parameters: [{
913
- name: "id",
648
+ name: "path",
914
649
  in: "path",
915
650
  required: true,
916
651
  schema: { type: "string" },
917
- description: "Sandbox instance name."
652
+ description: "File path inside the sandbox, without leading slash (e.g. workspace/main.py). Must resolve within /workspace."
918
653
  }],
919
654
  requestBody: {
920
655
  required: true,
921
- description: "Raw tar archive bytes.",
656
+ description: "Raw file content to write.",
922
657
  content: { "application/octet-stream": { schema: {
923
658
  type: "string",
924
659
  format: "binary"
@@ -926,310 +661,707 @@ const OPENAPI_SCHEMA = {
926
661
  },
927
662
  responses: {
928
663
  "200": {
929
- description: "Archive extracted successfully.",
930
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
664
+ description: "File written successfully.",
665
+ content: { "application/json": { schema: { $ref: "#/components/schemas/WriteResponse" } } }
931
666
  },
932
667
  "400": { $ref: "#/components/responses/InvalidRequest" },
933
668
  "401": { $ref: "#/components/responses/Unauthorized" },
934
- "502": {
935
- description: "tar extract failed inside the sandbox.",
669
+ "403": {
670
+ description: "Path resolves outside /workspace.",
936
671
  content: { "application/json": {
937
672
  schema: { $ref: "#/components/schemas/ErrorResponse" },
938
673
  example: {
939
- error: "tar extract failed (exit 1): ...",
940
- code: "workspace_archive_write_error"
674
+ error: "path must resolve to a location within /workspace",
675
+ code: "invalid_request"
941
676
  }
942
677
  } }
943
- }
944
- }
945
- } },
946
- "/v1/sandbox/{id}/mount": { post: {
947
- operationId: "mountBucket",
948
- summary: "Mount an S3-compatible bucket into the container",
949
- description: "Mounts an S3-compatible bucket (R2, S3, GCS, etc.) as a local directory via s3fs-FUSE. Credentials are optional — the SDK auto-detects from Worker secrets when omitted.",
950
- "x-codeSamples": [{
951
- lang: "curl",
952
- label: "cURL",
953
- source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/mount \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"bucket\":\"my-bucket\",\"mountPath\":\"/mnt/data\",\"options\":{\"endpoint\":\"https://ACCT.r2.cloudflarestorage.com\"}}'"
954
- }],
955
- parameters: [{
956
- name: "id",
957
- in: "path",
958
- required: true,
959
- schema: { type: "string" },
960
- description: "Sandbox instance name."
961
- }],
962
- requestBody: {
963
- required: true,
964
- content: { "application/json": { schema: { $ref: "#/components/schemas/MountBucketRequest" } } }
965
- },
966
- responses: {
967
- "200": {
968
- description: "Bucket mounted successfully.",
969
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
970
678
  },
971
- "400": { $ref: "#/components/responses/InvalidRequest" },
972
- "401": { $ref: "#/components/responses/Unauthorized" },
973
679
  "502": {
974
- description: "SDK mount call failed (invalid config, duplicate mount, or s3fs error).",
680
+ description: "SDK write call failed.",
975
681
  content: { "application/json": {
976
682
  schema: { $ref: "#/components/schemas/ErrorResponse" },
977
683
  example: {
978
- error: "mount failed: Mount path already in use",
979
- code: "mount_error"
684
+ error: "write failed: connection reset",
685
+ code: "workspace_archive_write_error"
980
686
  }
981
687
  } }
982
688
  }
983
689
  }
984
- } },
985
- "/v1/sandbox/{id}/unmount": { post: {
986
- operationId: "unmountBucket",
987
- summary: "Unmount a previously mounted bucket",
988
- description: "Unmounts a bucket filesystem that was previously mounted via `/v1/sandbox/{id}/mount`.",
989
- "x-codeSamples": [{
990
- lang: "curl",
991
- label: "cURL",
992
- source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/unmount \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"mountPath\":\"/mnt/data\"}'"
993
- }],
994
- parameters: [{
995
- name: "id",
996
- in: "path",
997
- required: true,
998
- schema: { type: "string" },
999
- description: "Sandbox instance name."
1000
- }],
690
+ }
691
+ },
692
+ "/v1/sandbox/{id}/persist": { post: {
693
+ operationId: "persistWorkspace",
694
+ summary: "Serialize the sandbox workspace to a tar archive",
695
+ description: "Archives the /workspace directory inside the sandbox and streams the resulting tar back as raw bytes.",
696
+ "x-codeSamples": [{
697
+ lang: "curl",
698
+ label: "cURL",
699
+ source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/persist \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -o workspace.tar"
700
+ }],
701
+ parameters: [{
702
+ name: "id",
703
+ in: "path",
704
+ required: true,
705
+ schema: { type: "string" },
706
+ description: "Sandbox instance name."
707
+ }, {
708
+ name: "excludes",
709
+ in: "query",
710
+ required: false,
711
+ schema: { type: "string" },
712
+ description: "Comma-separated list of relative paths to exclude from the archive.",
713
+ example: "__pycache__,.venv"
714
+ }],
715
+ responses: {
716
+ "200": {
717
+ description: "Raw tar archive bytes.",
718
+ content: { "application/octet-stream": { schema: {
719
+ type: "string",
720
+ format: "binary"
721
+ } } }
722
+ },
723
+ "400": {
724
+ description: "Invalid exclude paths (e.g. path traversal).",
725
+ content: { "application/json": {
726
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
727
+ example: {
728
+ error: "exclude paths must not contain \"..\"",
729
+ code: "invalid_request"
730
+ }
731
+ } }
732
+ },
733
+ "401": { $ref: "#/components/responses/Unauthorized" },
734
+ "502": {
735
+ description: "tar command failed inside the sandbox.",
736
+ content: { "application/json": {
737
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
738
+ example: {
739
+ error: "tar failed (exit 1): ...",
740
+ code: "workspace_archive_read_error"
741
+ }
742
+ } }
743
+ }
744
+ }
745
+ } },
746
+ "/v1/sandbox/{id}/hydrate": { post: {
747
+ operationId: "hydrateWorkspace",
748
+ summary: "Populate the sandbox workspace from a tar archive",
749
+ description: "Accepts a raw tar archive as the request body and extracts it into /workspace inside the sandbox.",
750
+ "x-codeSamples": [{
751
+ lang: "curl",
752
+ label: "cURL",
753
+ source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/hydrate \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/octet-stream\" \\\n --data-binary @workspace.tar"
754
+ }],
755
+ parameters: [{
756
+ name: "id",
757
+ in: "path",
758
+ required: true,
759
+ schema: { type: "string" },
760
+ description: "Sandbox instance name."
761
+ }],
762
+ requestBody: {
763
+ required: true,
764
+ description: "Raw tar archive bytes.",
765
+ content: { "application/octet-stream": { schema: {
766
+ type: "string",
767
+ format: "binary"
768
+ } } }
769
+ },
770
+ responses: {
771
+ "200": {
772
+ description: "Archive extracted successfully.",
773
+ content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
774
+ },
775
+ "400": { $ref: "#/components/responses/InvalidRequest" },
776
+ "401": { $ref: "#/components/responses/Unauthorized" },
777
+ "502": {
778
+ description: "tar extract failed inside the sandbox.",
779
+ content: { "application/json": {
780
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
781
+ example: {
782
+ error: "tar extract failed (exit 1): ...",
783
+ code: "workspace_archive_write_error"
784
+ }
785
+ } }
786
+ }
787
+ }
788
+ } },
789
+ "/v1/sandbox/{id}/mount": { post: {
790
+ operationId: "mountBucket",
791
+ summary: "Mount an S3-compatible bucket into the container",
792
+ description: "Mounts an S3-compatible bucket (R2, S3, GCS, etc.) as a local directory via s3fs-FUSE. Credentials are optional — the SDK auto-detects from Worker secrets when omitted.",
793
+ "x-codeSamples": [{
794
+ lang: "curl",
795
+ label: "cURL",
796
+ source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/mount \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"bucket\":\"my-bucket\",\"mountPath\":\"/mnt/data\",\"options\":{\"endpoint\":\"https://ACCT.r2.cloudflarestorage.com\"}}'"
797
+ }],
798
+ parameters: [{
799
+ name: "id",
800
+ in: "path",
801
+ required: true,
802
+ schema: { type: "string" },
803
+ description: "Sandbox instance name."
804
+ }],
805
+ requestBody: {
806
+ required: true,
807
+ content: { "application/json": { schema: { $ref: "#/components/schemas/MountBucketRequest" } } }
808
+ },
809
+ responses: {
810
+ "200": {
811
+ description: "Bucket mounted successfully.",
812
+ content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
813
+ },
814
+ "400": { $ref: "#/components/responses/InvalidRequest" },
815
+ "401": { $ref: "#/components/responses/Unauthorized" },
816
+ "502": {
817
+ description: "SDK mount call failed (invalid config, duplicate mount, or s3fs error).",
818
+ content: { "application/json": {
819
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
820
+ example: {
821
+ error: "mount failed: Mount path already in use",
822
+ code: "mount_error"
823
+ }
824
+ } }
825
+ }
826
+ }
827
+ } },
828
+ "/v1/sandbox/{id}/unmount": { post: {
829
+ operationId: "unmountBucket",
830
+ summary: "Unmount a previously mounted bucket",
831
+ description: "Unmounts a bucket filesystem that was previously mounted via `/v1/sandbox/{id}/mount`.",
832
+ "x-codeSamples": [{
833
+ lang: "curl",
834
+ label: "cURL",
835
+ source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/unmount \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"mountPath\":\"/mnt/data\"}'"
836
+ }],
837
+ parameters: [{
838
+ name: "id",
839
+ in: "path",
840
+ required: true,
841
+ schema: { type: "string" },
842
+ description: "Sandbox instance name."
843
+ }],
844
+ requestBody: {
845
+ required: true,
846
+ content: { "application/json": { schema: { $ref: "#/components/schemas/UnmountBucketRequest" } } }
847
+ },
848
+ responses: {
849
+ "200": {
850
+ description: "Bucket unmounted successfully.",
851
+ content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
852
+ },
853
+ "400": { $ref: "#/components/responses/InvalidRequest" },
854
+ "401": { $ref: "#/components/responses/Unauthorized" },
855
+ "502": {
856
+ description: "SDK unmount call failed (no active mount or unmount error).",
857
+ content: { "application/json": {
858
+ schema: { $ref: "#/components/schemas/ErrorResponse" },
859
+ example: {
860
+ error: "unmount failed: No active mount found",
861
+ code: "unmount_error"
862
+ }
863
+ } }
864
+ }
865
+ }
866
+ } }
867
+ };
868
+
869
+ //#endregion
870
+ //#region src/bridge/openapi/lifecycle.ts
871
+ const LIFECYCLEPaths = {
872
+ "/v1/sandbox": { post: {
873
+ operationId: "createSandbox",
874
+ summary: "Create a new sandbox",
875
+ description: "Generates a new unique sandbox ID. Use this ID with all `/v1/sandbox/{id}/*` routes.",
876
+ "x-codeSamples": [{
877
+ lang: "curl",
878
+ label: "cURL",
879
+ source: "curl -X POST https://$HOST/v1/sandbox \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
880
+ }],
881
+ responses: {
882
+ "200": {
883
+ description: "New sandbox created.",
884
+ content: { "application/json": { schema: {
885
+ type: "object",
886
+ required: ["id"],
887
+ properties: { id: {
888
+ type: "string",
889
+ description: "Unique sandbox ID for use with `/v1/sandbox/{id}/*` routes.",
890
+ example: "mfrggzdfmy2tqnrzgezdgnbv"
891
+ } }
892
+ } } }
893
+ },
894
+ "401": { $ref: "#/components/responses/Unauthorized" }
895
+ }
896
+ } },
897
+ "/v1/sandbox/{id}": { delete: {
898
+ operationId: "deleteSandbox",
899
+ summary: "Destroy a sandbox instance (best-effort)",
900
+ description: "Calls destroy() on the sandbox Durable Object to release container resources. Best-effort: unknown sandbox IDs return 204 without allocating a container.",
901
+ "x-codeSamples": [{
902
+ lang: "curl",
903
+ label: "cURL",
904
+ source: "curl -X DELETE https://$HOST/v1/sandbox/my-sandbox \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
905
+ }],
906
+ parameters: [{
907
+ name: "id",
908
+ in: "path",
909
+ required: true,
910
+ schema: { type: "string" },
911
+ description: "Sandbox instance name."
912
+ }],
913
+ responses: {
914
+ "204": { description: "Sandbox destroyed (best-effort). Container resources are released." },
915
+ "401": { $ref: "#/components/responses/Unauthorized" }
916
+ }
917
+ } },
918
+ "/health": { get: {
919
+ operationId: "healthCheck",
920
+ summary: "Worker health check",
921
+ description: "Simple liveness probe. Not protected by authentication.",
922
+ "x-codeSamples": [{
923
+ lang: "curl",
924
+ label: "cURL",
925
+ source: "curl https://$HOST/health"
926
+ }],
927
+ security: [],
928
+ responses: { "200": {
929
+ description: "Worker is up.",
930
+ content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
931
+ } }
932
+ } },
933
+ "/v1/openapi.json": { get: {
934
+ operationId: "getOpenApiSchema",
935
+ summary: "OpenAPI schema",
936
+ description: "Returns this OpenAPI 3.1 schema document.",
937
+ "x-codeSamples": [{
938
+ lang: "curl",
939
+ label: "cURL",
940
+ source: "curl https://$HOST/v1/openapi.json \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
941
+ }],
942
+ responses: {
943
+ "200": {
944
+ description: "OpenAPI schema document.",
945
+ content: { "application/json": { schema: { type: "object" } } }
946
+ },
947
+ "401": { $ref: "#/components/responses/Unauthorized" }
948
+ }
949
+ } },
950
+ "/v1/pool/stats": { get: {
951
+ operationId: "getPoolStats",
952
+ summary: "Pool statistics",
953
+ description: "Returns current warm pool statistics including warm/assigned counts and configuration.",
954
+ "x-codeSamples": [{
955
+ lang: "curl",
956
+ label: "cURL",
957
+ source: "curl https://$HOST/v1/pool/stats \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
958
+ }],
959
+ responses: {
960
+ "200": {
961
+ description: "Pool statistics.",
962
+ content: { "application/json": { schema: { $ref: "#/components/schemas/PoolStats" } } }
963
+ },
964
+ "401": { $ref: "#/components/responses/Unauthorized" }
965
+ }
966
+ } },
967
+ "/v1/pool/shutdown-prewarmed": { post: {
968
+ operationId: "shutdownPrewarmed",
969
+ summary: "Shutdown pre-warmed containers",
970
+ description: "Destroys all idle (unassigned) warm containers. Does not affect containers assigned to sandbox instances.",
971
+ "x-codeSamples": [{
972
+ lang: "curl",
973
+ label: "cURL",
974
+ source: "curl -X POST https://$HOST/v1/pool/shutdown-prewarmed \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
975
+ }],
976
+ responses: {
977
+ "200": {
978
+ description: "All pre-warmed containers destroyed.",
979
+ content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
980
+ },
981
+ "401": { $ref: "#/components/responses/Unauthorized" }
982
+ }
983
+ } },
984
+ "/v1/pool/prime": { post: {
985
+ operationId: "primePool",
986
+ summary: "Prime the warm pool",
987
+ description: "Pushes the current pool configuration and starts the alarm loop. Called automatically by the cron trigger; can also be called manually after deploy.",
988
+ "x-codeSamples": [{
989
+ lang: "curl",
990
+ label: "cURL",
991
+ source: "curl -X POST https://$HOST/v1/pool/prime \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
992
+ }],
993
+ responses: {
994
+ "200": {
995
+ description: "Pool primed successfully.",
996
+ content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
997
+ },
998
+ "401": { $ref: "#/components/responses/Unauthorized" }
999
+ }
1000
+ } }
1001
+ };
1002
+
1003
+ //#endregion
1004
+ //#region src/bridge/openapi/processes.ts
1005
+ const sandboxIdParameter$1 = {
1006
+ name: "id",
1007
+ in: "path",
1008
+ required: true,
1009
+ schema: { type: "string" },
1010
+ description: "Sandbox instance name."
1011
+ };
1012
+ const processIdParameter = {
1013
+ name: "processId",
1014
+ in: "path",
1015
+ required: true,
1016
+ schema: { type: "string" }
1017
+ };
1018
+ const processStatusResponse = {
1019
+ description: "Process status.",
1020
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ProcessStatus" } } }
1021
+ };
1022
+ const PROCESSESPaths = {
1023
+ "/v1/sandbox/{id}/processes": {
1024
+ post: {
1025
+ operationId: "createProcess",
1026
+ summary: "Launch a sandbox process",
1027
+ parameters: [sandboxIdParameter$1],
1001
1028
  requestBody: {
1002
1029
  required: true,
1003
- content: { "application/json": { schema: { $ref: "#/components/schemas/UnmountBucketRequest" } } }
1030
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ProcessCreateRequest" } } }
1004
1031
  },
1005
1032
  responses: {
1006
- "200": {
1007
- description: "Bucket unmounted successfully.",
1008
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
1009
- },
1033
+ "200": processStatusResponse,
1010
1034
  "400": { $ref: "#/components/responses/InvalidRequest" },
1011
1035
  "401": { $ref: "#/components/responses/Unauthorized" },
1012
- "502": {
1013
- description: "SDK unmount call failed (no active mount or unmount error).",
1014
- content: { "application/json": {
1015
- schema: { $ref: "#/components/schemas/ErrorResponse" },
1016
- example: {
1017
- error: "unmount failed: No active mount found",
1018
- code: "unmount_error"
1019
- }
1020
- } }
1021
- }
1036
+ "403": { $ref: "#/components/responses/InvalidRequest" },
1037
+ "502": { $ref: "#/components/responses/BadGateway" }
1022
1038
  }
1023
- } },
1024
- "/v1/sandbox/{id}": { delete: {
1025
- operationId: "deleteSandbox",
1026
- summary: "Destroy a sandbox instance (best-effort)",
1027
- description: "Calls destroy() on the sandbox Durable Object to release container resources. Best-effort: unknown sandbox IDs return 204 without allocating a container.",
1028
- "x-codeSamples": [{
1029
- lang: "curl",
1030
- label: "cURL",
1031
- source: "curl -X DELETE https://$HOST/v1/sandbox/my-sandbox \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1032
- }],
1033
- parameters: [{
1034
- name: "id",
1039
+ },
1040
+ get: {
1041
+ operationId: "listProcesses",
1042
+ summary: "List sandbox processes",
1043
+ parameters: [sandboxIdParameter$1],
1044
+ responses: {
1045
+ "200": {
1046
+ description: "Process statuses.",
1047
+ content: { "application/json": { schema: {
1048
+ type: "array",
1049
+ items: { $ref: "#/components/schemas/ProcessStatus" }
1050
+ } } }
1051
+ },
1052
+ "401": { $ref: "#/components/responses/Unauthorized" }
1053
+ }
1054
+ }
1055
+ },
1056
+ "/v1/sandbox/{id}/processes/{processId}": { get: {
1057
+ operationId: "getProcess",
1058
+ summary: "Get a sandbox process",
1059
+ parameters: [sandboxIdParameter$1, processIdParameter],
1060
+ responses: {
1061
+ "200": processStatusResponse,
1062
+ "401": { $ref: "#/components/responses/Unauthorized" },
1063
+ "404": { $ref: "#/components/responses/NotFound" }
1064
+ }
1065
+ } },
1066
+ "/v1/sandbox/{id}/processes/{processId}/logs": { get: {
1067
+ operationId: "readProcessLogs",
1068
+ summary: "Stream sandbox process logs",
1069
+ description: "Returns Server-Sent Events. stdout/stderr event data is base64-encoded at the bridge boundary only.",
1070
+ parameters: [
1071
+ sandboxIdParameter$1,
1072
+ processIdParameter,
1073
+ {
1074
+ name: "since",
1075
+ in: "query",
1076
+ schema: { type: "string" }
1077
+ },
1078
+ {
1079
+ name: "replay",
1080
+ in: "query",
1081
+ schema: { type: "boolean" }
1082
+ },
1083
+ {
1084
+ name: "follow",
1085
+ in: "query",
1086
+ schema: { type: "boolean" }
1087
+ }
1088
+ ],
1089
+ responses: {
1090
+ "200": {
1091
+ description: "SSE stream of process log events.",
1092
+ content: { "text/event-stream": { schema: { type: "string" } } }
1093
+ },
1094
+ "401": { $ref: "#/components/responses/Unauthorized" },
1095
+ "404": { $ref: "#/components/responses/NotFound" }
1096
+ }
1097
+ } },
1098
+ "/v1/sandbox/{id}/processes/{processId}/kill": { post: {
1099
+ operationId: "signalSandboxProcess",
1100
+ summary: "Signal a sandbox process",
1101
+ parameters: [sandboxIdParameter$1, processIdParameter],
1102
+ requestBody: {
1103
+ required: false,
1104
+ content: { "application/json": { schema: {
1105
+ type: "object",
1106
+ properties: { signal: {
1107
+ type: "integer",
1108
+ minimum: 1,
1109
+ maximum: 64,
1110
+ default: 15
1111
+ } }
1112
+ } } }
1113
+ },
1114
+ responses: {
1115
+ "204": { description: "Signal sent." },
1116
+ "400": { $ref: "#/components/responses/InvalidRequest" },
1117
+ "401": { $ref: "#/components/responses/Unauthorized" },
1118
+ "404": { $ref: "#/components/responses/NotFound" }
1119
+ }
1120
+ } },
1121
+ "/v1/sandbox/{id}/tunnel/{port}": {
1122
+ post: {
1123
+ operationId: "createTunnel",
1124
+ summary: "Create or reuse a tunnel for a sandbox port",
1125
+ parameters: [sandboxIdParameter$1, {
1126
+ name: "port",
1035
1127
  in: "path",
1036
1128
  required: true,
1037
- schema: { type: "string" },
1038
- description: "Sandbox instance name."
1129
+ schema: {
1130
+ type: "integer",
1131
+ minimum: 1024,
1132
+ maximum: 65535
1133
+ }
1039
1134
  }],
1040
1135
  responses: {
1041
- "204": { description: "Sandbox destroyed (best-effort). Container resources are released." },
1042
- "401": { $ref: "#/components/responses/Unauthorized" }
1136
+ "200": { description: "Tunnel created or reused." },
1137
+ "400": { $ref: "#/components/responses/InvalidRequest" },
1138
+ "401": { $ref: "#/components/responses/Unauthorized" },
1139
+ "502": { $ref: "#/components/responses/BadGateway" }
1043
1140
  }
1044
- } },
1045
- "/v1/sandbox/{id}/session": { post: {
1046
- operationId: "createSession",
1047
- summary: "Create an execution session",
1048
- description: "Sessions isolate working directory and environment variables across commands. The returned session ID is used with the `Session-Id` header on exec, file, and PTY endpoints.",
1049
- parameters: [{
1050
- name: "id",
1141
+ },
1142
+ delete: {
1143
+ operationId: "deleteTunnel",
1144
+ summary: "Delete the tunnel for a sandbox port",
1145
+ parameters: [sandboxIdParameter$1, {
1146
+ name: "port",
1051
1147
  in: "path",
1052
1148
  required: true,
1053
- schema: { type: "string" },
1054
- description: "Sandbox instance name."
1149
+ schema: {
1150
+ type: "integer",
1151
+ minimum: 1024,
1152
+ maximum: 65535
1153
+ }
1055
1154
  }],
1155
+ responses: {
1156
+ "204": { description: "Tunnel deleted or already absent." },
1157
+ "400": { $ref: "#/components/responses/InvalidRequest" },
1158
+ "401": { $ref: "#/components/responses/Unauthorized" },
1159
+ "502": { $ref: "#/components/responses/BadGateway" }
1160
+ }
1161
+ }
1162
+ },
1163
+ "/v1/sandbox/{id}/running": { get: {
1164
+ operationId: "isSandboxRunning",
1165
+ summary: "Check whether the sandbox container is alive",
1166
+ parameters: [sandboxIdParameter$1],
1167
+ responses: {
1168
+ "200": {
1169
+ description: "Liveness status.",
1170
+ content: { "application/json": { schema: { $ref: "#/components/schemas/RunningResponse" } } }
1171
+ },
1172
+ "401": { $ref: "#/components/responses/Unauthorized" }
1173
+ }
1174
+ } }
1175
+ };
1176
+
1177
+ //#endregion
1178
+ //#region src/bridge/openapi/terminals.ts
1179
+ const terminalSnapshotSchema = {
1180
+ type: "object",
1181
+ required: [
1182
+ "id",
1183
+ "command",
1184
+ "status"
1185
+ ],
1186
+ properties: {
1187
+ id: { type: "string" },
1188
+ pid: { type: "integer" },
1189
+ command: {
1190
+ type: "array",
1191
+ items: { type: "string" }
1192
+ },
1193
+ cwd: { type: "string" },
1194
+ status: {
1195
+ type: "string",
1196
+ enum: ["running", "exited"]
1197
+ },
1198
+ exit: {
1199
+ type: "object",
1200
+ properties: {
1201
+ code: {
1202
+ type: "integer",
1203
+ nullable: true
1204
+ },
1205
+ signal: { oneOf: [{ type: "string" }, { type: "integer" }] },
1206
+ timedOut: { type: "boolean" }
1207
+ }
1208
+ }
1209
+ }
1210
+ };
1211
+ const terminalIdParameter = {
1212
+ name: "terminalId",
1213
+ in: "path",
1214
+ required: true,
1215
+ schema: {
1216
+ type: "string",
1217
+ pattern: "^[a-zA-Z0-9._-]{1,128}$"
1218
+ }
1219
+ };
1220
+ const sandboxIdParameter = {
1221
+ name: "id",
1222
+ in: "path",
1223
+ required: true,
1224
+ schema: { type: "string" },
1225
+ description: "Sandbox instance name."
1226
+ };
1227
+ const terminalErrorResponses = {
1228
+ "400": {
1229
+ description: "Invalid request.",
1230
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
1231
+ },
1232
+ "401": { $ref: "#/components/responses/Unauthorized" },
1233
+ "502": {
1234
+ description: "Terminal operation failed.",
1235
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } }
1236
+ }
1237
+ };
1238
+ const TERMINALSPaths = {
1239
+ "/v1/sandbox/{id}/terminals": {
1240
+ post: {
1241
+ operationId: "createTerminal",
1242
+ summary: "Create a terminal",
1056
1243
  requestBody: {
1057
- required: false,
1244
+ required: true,
1058
1245
  content: { "application/json": { schema: {
1059
1246
  type: "object",
1247
+ required: ["argv"],
1060
1248
  properties: {
1061
- id: {
1062
- type: "string",
1063
- description: "Custom session ID. Auto-generated if omitted."
1064
- },
1065
- cwd: {
1066
- type: "string",
1067
- description: "Initial working directory for the session."
1249
+ argv: {
1250
+ type: "array",
1251
+ items: { type: "string" },
1252
+ minItems: 1
1068
1253
  },
1254
+ cwd: { type: "string" },
1069
1255
  env: {
1070
1256
  type: "object",
1071
- additionalProperties: { type: "string" },
1072
- description: "Environment variables scoped to this session."
1073
- }
1257
+ additionalProperties: { type: "string" }
1258
+ },
1259
+ cols: { type: "integer" },
1260
+ rows: { type: "integer" }
1074
1261
  }
1075
1262
  } } }
1076
1263
  },
1264
+ parameters: [sandboxIdParameter],
1077
1265
  responses: {
1078
1266
  "200": {
1079
- description: "Session created.",
1080
- content: { "application/json": { schema: {
1081
- type: "object",
1082
- required: ["id"],
1083
- properties: { id: {
1084
- type: "string",
1085
- description: "Session ID to pass via `Session-Id` header."
1086
- } }
1087
- } } }
1267
+ description: "Terminal created with a generated ID.",
1268
+ content: { "application/json": { schema: terminalSnapshotSchema } }
1088
1269
  },
1089
- "401": { $ref: "#/components/responses/Unauthorized" },
1090
- "502": {
1091
- description: "Session creation failed.",
1092
- content: { "application/json": {
1093
- schema: { $ref: "#/components/schemas/ErrorResponse" },
1094
- example: {
1095
- error: "session creation failed",
1096
- code: "session_error"
1097
- }
1098
- } }
1099
- }
1270
+ ...terminalErrorResponses
1100
1271
  }
1101
- } },
1102
- "/v1/sandbox/{id}/session/{sid}": { delete: {
1103
- operationId: "deleteSession",
1104
- summary: "Delete an execution session",
1105
- description: "Removes a named session. The default session cannot be deleted.",
1106
- parameters: [{
1107
- name: "id",
1108
- in: "path",
1109
- required: true,
1110
- schema: { type: "string" },
1111
- description: "Sandbox instance name."
1112
- }, {
1113
- name: "sid",
1114
- in: "path",
1115
- required: true,
1116
- schema: { type: "string" },
1117
- description: "Session ID to delete."
1118
- }],
1272
+ },
1273
+ get: {
1274
+ operationId: "listTerminals",
1275
+ summary: "List terminals",
1276
+ parameters: [sandboxIdParameter],
1119
1277
  responses: {
1120
1278
  "200": {
1121
- description: "Session deleted.",
1279
+ description: "Terminal snapshots.",
1122
1280
  content: { "application/json": { schema: {
1123
- type: "object",
1124
- required: ["success", "sessionId"],
1125
- properties: {
1126
- success: {
1127
- type: "boolean",
1128
- description: "`true` if the session was deleted."
1129
- },
1130
- sessionId: {
1131
- type: "string",
1132
- description: "ID of the deleted session."
1133
- }
1134
- }
1281
+ type: "array",
1282
+ items: terminalSnapshotSchema
1135
1283
  } } }
1136
1284
  },
1137
- "401": { $ref: "#/components/responses/Unauthorized" },
1138
- "502": {
1139
- description: "Session deletion failed (e.g. cannot delete default session).",
1140
- content: { "application/json": {
1141
- schema: { $ref: "#/components/schemas/ErrorResponse" },
1142
- example: {
1143
- error: "cannot delete the default session",
1144
- code: "session_error"
1145
- }
1146
- } }
1147
- }
1148
- }
1149
- } },
1150
- "/health": { get: {
1151
- operationId: "healthCheck",
1152
- summary: "Worker health check",
1153
- description: "Simple liveness probe. Not protected by authentication.",
1154
- "x-codeSamples": [{
1155
- lang: "curl",
1156
- label: "cURL",
1157
- source: "curl https://$HOST/health"
1158
- }],
1159
- security: [],
1160
- responses: { "200": {
1161
- description: "Worker is up.",
1162
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
1163
- } }
1164
- } },
1165
- "/v1/openapi.json": { get: {
1166
- operationId: "getOpenApiSchema",
1167
- summary: "OpenAPI schema",
1168
- description: "Returns this OpenAPI 3.1 schema document.",
1169
- "x-codeSamples": [{
1170
- lang: "curl",
1171
- label: "cURL",
1172
- source: "curl https://$HOST/v1/openapi.json \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1173
- }],
1174
- responses: {
1175
- "200": {
1176
- description: "OpenAPI schema document.",
1177
- content: { "application/json": { schema: { type: "object" } } }
1178
- },
1179
- "401": { $ref: "#/components/responses/Unauthorized" }
1180
- }
1181
- } },
1182
- "/v1/pool/stats": { get: {
1183
- operationId: "getPoolStats",
1184
- summary: "Pool statistics",
1185
- description: "Returns current warm pool statistics including warm/assigned counts and configuration.",
1186
- "x-codeSamples": [{
1187
- lang: "curl",
1188
- label: "cURL",
1189
- source: "curl https://$HOST/v1/pool/stats \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1190
- }],
1191
- responses: {
1192
- "200": {
1193
- description: "Pool statistics.",
1194
- content: { "application/json": { schema: { $ref: "#/components/schemas/PoolStats" } } }
1195
- },
1196
- "401": { $ref: "#/components/responses/Unauthorized" }
1285
+ ...terminalErrorResponses
1197
1286
  }
1198
- } },
1199
- "/v1/pool/shutdown-prewarmed": { post: {
1200
- operationId: "shutdownPrewarmed",
1201
- summary: "Shutdown pre-warmed containers",
1202
- description: "Stops all idle (unassigned) warm containers. Does not affect containers assigned to sandbox sessions.",
1203
- "x-codeSamples": [{
1204
- lang: "curl",
1205
- label: "cURL",
1206
- source: "curl -X POST https://$HOST/v1/pool/shutdown-prewarmed \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1207
- }],
1208
- responses: {
1209
- "200": {
1210
- description: "All pre-warmed containers stopped.",
1211
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
1212
- },
1213
- "401": { $ref: "#/components/responses/Unauthorized" }
1214
- }
1215
- } },
1216
- "/v1/pool/prime": { post: {
1217
- operationId: "primePool",
1218
- summary: "Prime the warm pool",
1219
- description: "Pushes the current pool configuration and starts the alarm loop. Called automatically by the cron trigger; can also be called manually after deploy.",
1220
- "x-codeSamples": [{
1221
- lang: "curl",
1222
- label: "cURL",
1223
- source: "curl -X POST https://$HOST/v1/pool/prime \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1224
- }],
1225
- responses: {
1226
- "200": {
1227
- description: "Pool primed successfully.",
1228
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
1229
- },
1230
- "401": { $ref: "#/components/responses/Unauthorized" }
1287
+ }
1288
+ },
1289
+ "/v1/sandbox/{id}/terminals/{terminalId}": { get: {
1290
+ operationId: "getTerminal",
1291
+ summary: "Get a terminal snapshot",
1292
+ parameters: [sandboxIdParameter, terminalIdParameter],
1293
+ responses: {
1294
+ "200": {
1295
+ description: "Terminal snapshot.",
1296
+ content: { "application/json": { schema: terminalSnapshotSchema } }
1297
+ },
1298
+ "404": { description: "Terminal not found." },
1299
+ ...terminalErrorResponses
1300
+ }
1301
+ } },
1302
+ "/v1/sandbox/{id}/terminals/{terminalId}/connect": { get: {
1303
+ operationId: "connectTerminal",
1304
+ summary: "Connect to a terminal WebSocket",
1305
+ description: "WebSocket binary client frames are terminal input bytes. JSON client controls are resize, interrupt, and terminate. Server output is a JSON chunk control immediately followed by one binary frame. Cursor advances after the binary frame is consumed.",
1306
+ parameters: [
1307
+ sandboxIdParameter,
1308
+ terminalIdParameter,
1309
+ {
1310
+ name: "cursor",
1311
+ in: "query",
1312
+ required: false,
1313
+ schema: { type: "string" }
1314
+ },
1315
+ {
1316
+ name: "cols",
1317
+ in: "query",
1318
+ required: false,
1319
+ schema: { type: "integer" }
1320
+ },
1321
+ {
1322
+ name: "rows",
1323
+ in: "query",
1324
+ required: false,
1325
+ schema: { type: "integer" }
1231
1326
  }
1232
- } }
1327
+ ],
1328
+ responses: {
1329
+ "101": { description: "WebSocket upgrade successful." },
1330
+ "404": { description: "Terminal not found." },
1331
+ ...terminalErrorResponses
1332
+ }
1333
+ } },
1334
+ "/v1/sandbox/{id}/terminals/{terminalId}/interrupt": { post: {
1335
+ operationId: "interruptTerminal",
1336
+ summary: "Interrupt a terminal",
1337
+ parameters: [sandboxIdParameter, terminalIdParameter],
1338
+ responses: {
1339
+ "204": { description: "Interrupt signal sent." },
1340
+ "404": { description: "Terminal not found." },
1341
+ ...terminalErrorResponses
1342
+ }
1343
+ } },
1344
+ "/v1/sandbox/{id}/terminals/{terminalId}/terminate": { post: {
1345
+ operationId: "terminateTerminal",
1346
+ summary: "Terminate a terminal",
1347
+ parameters: [sandboxIdParameter, terminalIdParameter],
1348
+ responses: {
1349
+ "204": { description: "Terminate signal sent." },
1350
+ "404": { description: "Terminal not found." },
1351
+ ...terminalErrorResponses
1352
+ }
1353
+ } }
1354
+ };
1355
+
1356
+ //#endregion
1357
+ //#region src/bridge/openapi.ts
1358
+ const OPENAPI_SCHEMA = {
1359
+ ...OPENAPI_BASE,
1360
+ paths: {
1361
+ ...LIFECYCLEPaths,
1362
+ ...PROCESSESPaths,
1363
+ ...FILESPaths,
1364
+ ...TERMINALSPaths
1233
1365
  }
1234
1366
  };
1235
1367
 
@@ -1762,10 +1894,20 @@ function esc(s) {
1762
1894
  }
1763
1895
 
1764
1896
  //#endregion
1765
- //#region src/bridge/routes.ts
1766
- /** Typed wrapper around the SDK's getSandbox() that returns a BridgeSandbox. */
1767
- function getSandbox$1(ns, containerUUID) {
1768
- return getSandbox(ns, containerUUID);
1897
+ //#region src/bridge/routes/common.ts
1898
+ function getSandbox(ns, containerUUID) {
1899
+ return getSandbox$1(ns, containerUUID);
1900
+ }
1901
+ function getSandboxNs(env$1, sandboxBinding) {
1902
+ return env$1[sandboxBinding];
1903
+ }
1904
+ function getWarmPoolNs(env$1, warmPoolBinding) {
1905
+ return env$1[warmPoolBinding];
1906
+ }
1907
+ function getWarmPoolStub(env$1, warmPoolBinding) {
1908
+ const poolNs = getWarmPoolNs(env$1, warmPoolBinding);
1909
+ const poolId = poolNs.idFromName("global-pool");
1910
+ return poolNs.get(poolId);
1769
1911
  }
1770
1912
  function hasEndpoint(options) {
1771
1913
  return "endpoint" in options && typeof options.endpoint === "string";
@@ -1810,171 +1952,36 @@ function validateMountOptions(options, binding) {
1810
1952
  return null;
1811
1953
  }
1812
1954
  function resolveMountBucketName(body) {
1813
- if (hasEndpoint(body.options)) {
1814
- if (body.bucket && typeof body.bucket === "string") return body.bucket;
1815
- return errorJson("bucket must be a non-empty string for remote mounts", "invalid_request", 400);
1816
- }
1817
- if (body.binding !== void 0) return body.binding;
1818
- return errorJson("binding must be a non-empty string for R2 binding mounts", "invalid_request", 400);
1819
- }
1820
- function toSDKMountOptions(options) {
1821
- if (hasEndpoint(options)) {
1822
- const remoteOptions = { endpoint: options.endpoint };
1823
- if (options.readOnly !== void 0) remoteOptions.readOnly = options.readOnly;
1824
- if (options.prefix !== void 0) remoteOptions.prefix = options.prefix;
1825
- if (hasCredentials(options)) remoteOptions.credentials = {
1826
- accessKeyId: options.credentials.accessKeyId,
1827
- secretAccessKey: options.credentials.secretAccessKey
1828
- };
1829
- if (options.s3fsOptions !== void 0) remoteOptions.s3fsOptions = options.s3fsOptions;
1830
- if ("credentialProxy" in options && typeof options.credentialProxy === "boolean") remoteOptions.credentialProxy = options.credentialProxy;
1831
- return remoteOptions;
1832
- }
1833
- const r2BindingOptions = {};
1834
- if (options.readOnly !== void 0) r2BindingOptions.readOnly = options.readOnly;
1835
- if (options.prefix !== void 0) r2BindingOptions.prefix = options.prefix;
1836
- if (options.s3fsOptions !== void 0) r2BindingOptions.s3fsOptions = options.s3fsOptions;
1837
- return r2BindingOptions;
1838
- }
1839
- function createBridgeApp(config) {
1840
- const app = new Hono();
1841
- const { sandboxBinding, warmPoolBinding, apiPrefix } = config;
1842
- function getSandboxNs(env$1) {
1843
- return env$1[sandboxBinding];
1844
- }
1845
- function getWarmPoolNs(env$1) {
1846
- return env$1[warmPoolBinding];
1847
- }
1848
- app.use(`${apiPrefix}/sandbox/*`, async (c, next) => {
1849
- const sandboxId = new URL(c.req.url).pathname.split("/")[apiPrefix.split("/").filter(Boolean).length + 2];
1850
- if (sandboxId && !/^[a-z2-7]{1,128}$/.test(sandboxId)) return errorJson("Invalid sandbox ID format", "invalid_request", 400);
1851
- const token = c.env.SANDBOX_API_KEY;
1852
- if (token) {
1853
- const authHeader = c.req.header("Authorization") ?? "";
1854
- if ((authHeader.toLowerCase().startsWith("bearer ") ? authHeader.slice(7) : "") !== token) return errorJson("Unauthorized", "unauthorized", 401);
1855
- }
1856
- return next();
1857
- });
1858
- app.post(`${apiPrefix}/sandbox`, async (c) => {
1859
- const token = c.env.SANDBOX_API_KEY;
1860
- if (token) {
1861
- const authHeader = c.req.header("Authorization") ?? "";
1862
- if ((authHeader.toLowerCase().startsWith("bearer ") ? authHeader.slice(7) : "") !== token) return errorJson("Unauthorized", "unauthorized", 401);
1863
- }
1864
- const bytes = new Uint8Array(16);
1865
- crypto.getRandomValues(bytes);
1866
- const id = base32Encode(bytes);
1867
- return c.json({ id });
1868
- });
1869
- app.use(`${apiPrefix}/sandbox/:id/*`, async (c, next) => {
1870
- const sandboxId = c.req.param("id");
1871
- const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
1872
- const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
1873
- const poolNs = getWarmPoolNs(c.env);
1874
- const poolId = poolNs.idFromName("global-pool");
1875
- const poolStub = poolNs.get(poolId);
1876
- try {
1877
- await poolStub.configure({
1878
- warmTarget,
1879
- refreshInterval
1880
- });
1881
- const containerUUID = await poolStub.getContainer(sandboxId);
1882
- c.set("containerUUID", containerUUID);
1883
- } catch (err) {
1884
- const msg = err instanceof Error ? err.message : String(err);
1885
- if (msg.includes("instance limit reached")) return errorJson(msg, "capacity_exceeded", 503);
1886
- return errorJson(`pool error: ${msg}`, "pool_error", 502);
1887
- }
1888
- return next();
1889
- });
1890
- app.use(`${apiPrefix}/sandbox/:id`, async (c, next) => {
1891
- if (c.req.method !== "DELETE") return next();
1892
- const sandboxId = c.req.param("id");
1893
- const poolNs = getWarmPoolNs(c.env);
1894
- const poolId = poolNs.idFromName("global-pool");
1895
- const poolStub = poolNs.get(poolId);
1896
- try {
1897
- const containerUUID = await poolStub.lookupContainer(sandboxId);
1898
- if (!containerUUID) return new Response(null, { status: 204 });
1899
- c.set("containerUUID", containerUUID);
1900
- } catch (err) {
1901
- return errorJson(`pool error: ${err instanceof Error ? err.message : String(err)}`, "pool_error", 502);
1902
- }
1903
- return next();
1904
- });
1905
- app.post(`${apiPrefix}/sandbox/:id/exec`, async (c) => {
1906
- let body;
1907
- try {
1908
- body = await c.req.json();
1909
- } catch {
1910
- return errorJson("Invalid JSON body", "invalid_request", 400);
1911
- }
1912
- if (!Array.isArray(body.argv) || body.argv.length === 0) return errorJson("argv must be a non-empty array", "invalid_request", 400);
1913
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
1914
- const rawSessionId = c.req.header("Session-Id");
1915
- let executor = sandbox;
1916
- if (rawSessionId) {
1917
- const sessionId = validateSessionId(rawSessionId);
1918
- if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
1919
- executor = await sandbox.getSession(sessionId);
1920
- }
1921
- const command = body.argv.map(shellQuote).join(" ");
1922
- const opts = {};
1923
- if (typeof body.timeout_ms === "number") opts.timeout = body.timeout_ms;
1924
- if (typeof body.cwd === "string") {
1925
- const resolvedCwd = resolveWorkspacePath(body.cwd);
1926
- if (!resolvedCwd) return errorJson("cwd must resolve to a location within /workspace", "invalid_request", 403);
1927
- opts.cwd = resolvedCwd;
1928
- }
1929
- const { readable, writable } = new TransformStream();
1930
- const writer = writable.getWriter();
1931
- const encoder = new TextEncoder();
1932
- let closed = false;
1933
- let lastWrite = Promise.resolve();
1934
- /** Write a single SSE event. Chains on the previous write to respect backpressure. */
1935
- function writeSSE(event, data) {
1936
- if (closed) return;
1937
- const payload = data.split("\n").map((line) => `data: ${line}`).join("\n");
1938
- lastWrite = lastWrite.then(() => writer.write(encoder.encode(`event: ${event}\n${payload}\n\n`)));
1939
- }
1940
- function closeStream() {
1941
- if (closed) return;
1942
- closed = true;
1943
- lastWrite.then(() => writer.close()).catch(() => {});
1944
- }
1945
- executor.exec(command, {
1946
- ...opts,
1947
- stream: true,
1948
- onOutput(stream, data) {
1949
- writeSSE(stream, toBase64(data));
1950
- },
1951
- onComplete(result) {
1952
- writeSSE("exit", JSON.stringify({ exit_code: result.exitCode }));
1953
- closeStream();
1954
- },
1955
- onError(err) {
1956
- writeSSE("error", JSON.stringify({
1957
- error: err.message,
1958
- code: "exec_error"
1959
- }));
1960
- closeStream();
1961
- }
1962
- }).catch((err) => {
1963
- const msg = err instanceof Error ? err.message : String(err);
1964
- writeSSE("error", JSON.stringify({
1965
- error: `exec failed: ${msg}`,
1966
- code: "exec_transport_error"
1967
- }));
1968
- closeStream();
1969
- });
1970
- return new Response(readable, {
1971
- status: 200,
1972
- headers: {
1973
- "Content-Type": "text/event-stream",
1974
- "Cache-Control": "no-cache"
1975
- }
1976
- });
1977
- });
1955
+ if (hasEndpoint(body.options)) {
1956
+ if (body.bucket && typeof body.bucket === "string") return body.bucket;
1957
+ return errorJson("bucket must be a non-empty string for remote mounts", "invalid_request", 400);
1958
+ }
1959
+ if (body.binding !== void 0) return body.binding;
1960
+ return errorJson("binding must be a non-empty string for R2 binding mounts", "invalid_request", 400);
1961
+ }
1962
+ function toSDKMountOptions(options) {
1963
+ if (hasEndpoint(options)) {
1964
+ const remoteOptions = { endpoint: options.endpoint };
1965
+ if (options.readOnly !== void 0) remoteOptions.readOnly = options.readOnly;
1966
+ if (options.prefix !== void 0) remoteOptions.prefix = options.prefix;
1967
+ if (hasCredentials(options)) remoteOptions.credentials = {
1968
+ accessKeyId: options.credentials.accessKeyId,
1969
+ secretAccessKey: options.credentials.secretAccessKey
1970
+ };
1971
+ if (options.s3fsOptions !== void 0) remoteOptions.s3fsOptions = options.s3fsOptions;
1972
+ if ("credentialProxy" in options && typeof options.credentialProxy === "boolean") remoteOptions.credentialProxy = options.credentialProxy;
1973
+ return remoteOptions;
1974
+ }
1975
+ const r2BindingOptions = {};
1976
+ if (options.readOnly !== void 0) r2BindingOptions.readOnly = options.readOnly;
1977
+ if (options.prefix !== void 0) r2BindingOptions.prefix = options.prefix;
1978
+ if (options.s3fsOptions !== void 0) r2BindingOptions.s3fsOptions = options.s3fsOptions;
1979
+ return r2BindingOptions;
1980
+ }
1981
+
1982
+ //#endregion
1983
+ //#region src/bridge/routes/files.ts
1984
+ function registerFileRoutes(app, apiPrefix, sandboxBinding) {
1978
1985
  app.get(`${apiPrefix}/sandbox/:id/file/*`, async (c) => {
1979
1986
  const sandboxId = c.req.param("id");
1980
1987
  const fullPath = c.req.path;
@@ -1983,16 +1990,9 @@ function createBridgeApp(config) {
1983
1990
  if (!relativePath) return errorJson("file path must not be empty", "invalid_request", 400);
1984
1991
  const resolvedPath = resolveWorkspacePath(`/${relativePath}`);
1985
1992
  if (!resolvedPath) return errorJson("path must resolve to a location within /workspace", "invalid_request", 403);
1986
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
1987
- const rawSessionId = c.req.header("Session-Id");
1988
- let executor = sandbox;
1989
- if (rawSessionId) {
1990
- const sessionId = validateSessionId(rawSessionId);
1991
- if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
1992
- executor = await sandbox.getSession(sessionId);
1993
- }
1993
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
1994
1994
  try {
1995
- const stream = await executor.readFileStream(resolvedPath);
1995
+ const stream = await sandbox.readFileStream(resolvedPath);
1996
1996
  return new Response(sseToByteStream(stream), {
1997
1997
  status: 200,
1998
1998
  headers: { "Content-Type": "application/octet-stream" }
@@ -2010,14 +2010,7 @@ function createBridgeApp(config) {
2010
2010
  if (!relativePath) return errorJson("file path must not be empty", "invalid_request", 400);
2011
2011
  const resolvedPath = resolveWorkspacePath(`/${relativePath}`);
2012
2012
  if (!resolvedPath) return errorJson("path must resolve to a location within /workspace", "invalid_request", 403);
2013
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2014
- const rawSessionId = c.req.header("Session-Id");
2015
- let executor = sandbox;
2016
- if (rawSessionId) {
2017
- const sessionId = validateSessionId(rawSessionId);
2018
- if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
2019
- executor = await sandbox.getSession(sessionId);
2020
- }
2013
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2021
2014
  try {
2022
2015
  const buffer = await c.req.arrayBuffer();
2023
2016
  const MAX_WRITE_BYTES = 32 * 1024 * 1024;
@@ -2026,87 +2019,43 @@ function createBridgeApp(config) {
2026
2019
  let b64 = "";
2027
2020
  const CHUNK = 6144;
2028
2021
  for (let i = 0; i < bytes.length; i += CHUNK) b64 += btoa(String.fromCharCode(...bytes.subarray(i, i + CHUNK)));
2029
- await executor.writeFile(resolvedPath, b64, { encoding: "base64" });
2022
+ await sandbox.writeFile(resolvedPath, b64, { encoding: "base64" });
2030
2023
  return c.json({ ok: true });
2031
2024
  } catch (err) {
2032
2025
  return errorJson(`write failed: ${err instanceof Error ? err.message : String(err)}`, "workspace_archive_write_error", 502);
2033
2026
  }
2034
2027
  });
2035
- app.post(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
2036
- const port = Number(c.req.param("port"));
2037
- if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
2038
- const options = parseTunnelOptions(await c.req.text());
2039
- if (options instanceof Response) return options;
2040
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2041
- try {
2042
- const tunnel = await sandbox.tunnels.get(port, options);
2043
- return c.json(tunnel);
2044
- } catch (err) {
2045
- return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
2046
- }
2047
- });
2048
- app.delete(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
2049
- const port = Number(c.req.param("port"));
2050
- if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
2051
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2052
- try {
2053
- await sandbox.tunnels.destroy(port);
2054
- return c.body(null, 204);
2055
- } catch (err) {
2056
- return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
2057
- }
2058
- });
2059
- app.get(`${apiPrefix}/sandbox/:id/running`, async (c) => {
2060
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2061
- try {
2062
- await sandbox.exec("true");
2063
- return c.json({ running: true });
2064
- } catch {
2065
- return c.json({ running: false });
2066
- }
2067
- });
2068
- app.get(`${apiPrefix}/sandbox/:id/pty`, async (c) => {
2069
- const upgrade = c.req.header("Upgrade");
2070
- if (!upgrade || upgrade.toLowerCase() !== "websocket") return errorJson("WebSocket upgrade required", "invalid_request", 400);
2071
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2072
- const colsParam = c.req.query("cols");
2073
- const rowsParam = c.req.query("rows");
2074
- const shell = c.req.query("shell");
2075
- const sessionId = c.req.header("Session-Id") || c.req.query("session");
2076
- const cols = colsParam ? Number(colsParam) : 80;
2077
- const rows = rowsParam ? Number(rowsParam) : 24;
2078
- if (Number.isNaN(cols) || Number.isNaN(rows)) return errorJson("cols and rows must be valid numbers", "invalid_request", 400);
2079
- const opts = {
2080
- cols,
2081
- rows
2082
- };
2083
- if (shell) opts.shell = shell;
2084
- try {
2085
- if (sessionId) {
2086
- const validatedId = validateSessionId(sessionId);
2087
- if (!validatedId) return errorJson("Invalid session ID format", "invalid_request", 400);
2088
- return await (await sandbox.getSession(validatedId)).terminal(c.req.raw, opts);
2089
- }
2090
- return await sandbox.terminal(c.req.raw, opts);
2091
- } catch (err) {
2092
- return errorJson(`terminal failed: ${err instanceof Error ? err.message : String(err)}`, "exec_transport_error", 502);
2093
- }
2028
+ }
2029
+
2030
+ //#endregion
2031
+ //#region src/bridge/routes/lifecycle.ts
2032
+ function registerLifecycleRoutes(app, apiPrefix, healthPath, sandboxBinding, warmPoolBinding) {
2033
+ app.post(`${apiPrefix}/sandbox`, (c) => {
2034
+ const bytes = new Uint8Array(15);
2035
+ crypto.getRandomValues(bytes);
2036
+ return c.json({ id: base32Encode(bytes) });
2094
2037
  });
2095
2038
  app.post(`${apiPrefix}/sandbox/:id/persist`, async (c) => {
2096
2039
  const root = "/workspace";
2097
2040
  const excludesParam = c.req.query("excludes") ?? "";
2098
2041
  const excludes = excludesParam ? excludesParam.split(",").filter((s) => s.length > 0) : [];
2099
2042
  for (const ex of excludes) if (ex.includes("..")) return errorJson("exclude paths must not contain \"..\"", "invalid_request", 400);
2100
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2101
- const tmpPath = `/tmp/sandbox-persist-${Date.now()}.tar`;
2102
- const excludeArgs = excludes.map((rel) => `--exclude ${shellQuote(`./${rel.replace(/^\.\//, "")}`)}`).join(" ");
2103
- const tarCmd = excludeArgs ? `tar cf ${shellQuote(tmpPath)} ${excludeArgs} -C ${shellQuote(root)} .` : `tar cf ${shellQuote(tmpPath)} -C ${shellQuote(root)} .`;
2043
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2104
2044
  try {
2105
- const result = await sandbox.exec(tarCmd);
2106
- if (result.exitCode !== 0) return errorJson(`tar failed (exit ${result.exitCode}): ${result.stderr}`, "workspace_archive_read_error", 502);
2107
- const stream = await sandbox.readFileStream(tmpPath);
2108
- sandbox.exec(`rm -f ${shellQuote(tmpPath)}`).catch(() => {});
2109
- return new Response(sseToByteStream(stream), {
2045
+ const archivePath = await sandbox.createWorkspaceArchive({
2046
+ root,
2047
+ excludes
2048
+ });
2049
+ let stream;
2050
+ try {
2051
+ stream = await sandbox.readFileStream(archivePath);
2052
+ } catch (error) {
2053
+ await sandbox.cleanupWorkspaceArchive(archivePath).catch(() => void 0);
2054
+ throw error;
2055
+ }
2056
+ const cleanupPath = archivePath;
2057
+ const responseStream = withStreamCleanup(sseToByteStream(stream), () => sandbox.cleanupWorkspaceArchive(cleanupPath));
2058
+ return new Response(responseStream, {
2110
2059
  status: 200,
2111
2060
  headers: { "Content-Type": "application/octet-stream" }
2112
2061
  });
@@ -2116,7 +2065,7 @@ function createBridgeApp(config) {
2116
2065
  });
2117
2066
  app.post(`${apiPrefix}/sandbox/:id/hydrate`, async (c) => {
2118
2067
  const root = "/workspace";
2119
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2068
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2120
2069
  let tarBytes;
2121
2070
  try {
2122
2071
  const buffer = await c.req.arrayBuffer();
@@ -2127,18 +2076,21 @@ function createBridgeApp(config) {
2127
2076
  if (tarBytes.byteLength === 0) return errorJson("Empty tar payload", "invalid_request", 400);
2128
2077
  const MAX_HYDRATE_BYTES = 32 * 1024 * 1024;
2129
2078
  if (tarBytes.byteLength > MAX_HYDRATE_BYTES) return errorJson(`tar payload too large: ${tarBytes.byteLength} bytes exceeds the ${MAX_HYDRATE_BYTES}-byte limit`, "invalid_request", 400);
2079
+ const tmpPath = `/tmp/sandbox-hydrate-${Date.now()}-${crypto.randomUUID()}.tar`;
2130
2080
  try {
2131
- await sandbox.exec(`mkdir -p ${shellQuote(root)}`);
2132
- const tmpPath = `/tmp/sandbox-hydrate-${Date.now()}.tar`;
2133
2081
  let b64 = "";
2134
2082
  const CHUNK = 6144;
2135
2083
  for (let i = 0; i < tarBytes.length; i += CHUNK) b64 += btoa(String.fromCharCode(...tarBytes.subarray(i, i + CHUNK)));
2136
2084
  await sandbox.writeFile(tmpPath, b64, { encoding: "base64" });
2137
- const extractResult = await sandbox.exec(`tar xf ${shellQuote(tmpPath)} -C ${shellQuote(root)} && rm -f ${shellQuote(tmpPath)}`);
2138
- if (extractResult.exitCode !== 0) return errorJson(`tar extract failed (exit ${extractResult.exitCode}): ${extractResult.stderr}`, "workspace_archive_write_error", 502);
2085
+ await sandbox.extractWorkspaceArchive({
2086
+ root,
2087
+ archivePath: tmpPath
2088
+ });
2139
2089
  return c.json({ ok: true });
2140
2090
  } catch (err) {
2141
2091
  return errorJson(`hydrate failed: ${err instanceof Error ? err.message : String(err)}`, "workspace_archive_write_error", 502);
2092
+ } finally {
2093
+ await sandbox.cleanupWorkspaceArchive(tmpPath).catch(() => void 0);
2142
2094
  }
2143
2095
  });
2144
2096
  app.post(`${apiPrefix}/sandbox/:id/mount`, async (c) => {
@@ -2156,7 +2108,7 @@ function createBridgeApp(config) {
2156
2108
  if (optionsError) return optionsError;
2157
2109
  const bucketName = resolveMountBucketName(body);
2158
2110
  if (bucketName instanceof Response) return bucketName;
2159
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2111
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2160
2112
  const sdkOptions = toSDKMountOptions(body.options);
2161
2113
  try {
2162
2114
  await sandbox.mountBucket(bucketName, body.mountPath, sdkOptions);
@@ -2176,55 +2128,29 @@ function createBridgeApp(config) {
2176
2128
  if (!body.mountPath.startsWith("/")) return errorJson("mountPath must be an absolute path (start with /)", "invalid_request", 400);
2177
2129
  const normalizedPath = new URL(body.mountPath, "file:///").pathname;
2178
2130
  if (normalizedPath === "/") return errorJson("mountPath must not resolve to / (filesystem root)", "invalid_request", 400);
2179
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2131
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2180
2132
  try {
2181
2133
  await sandbox.unmountBucket(normalizedPath);
2182
- const quoted = shellQuote(normalizedPath);
2183
2134
  try {
2184
- await sandbox.exec(`mountpoint -q ${quoted} || rmdir ${quoted}`);
2135
+ await sandbox.cleanupMountDirectory(normalizedPath);
2185
2136
  } catch {}
2186
2137
  return c.json({ ok: true });
2187
2138
  } catch (err) {
2188
2139
  return errorJson(`unmount failed: ${err instanceof Error ? err.message : String(err)}`, "unmount_error", 502);
2189
2140
  }
2190
2141
  });
2191
- app.post(`${apiPrefix}/sandbox/:id/session`, async (c) => {
2192
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2193
- let body = {};
2194
- try {
2195
- body = await c.req.json();
2196
- } catch {}
2197
- try {
2198
- const session = await sandbox.createSession(body);
2199
- return c.json({ id: session.id });
2200
- } catch (err) {
2201
- return errorJson(`session create failed: ${err instanceof Error ? err.message : String(err)}`, "session_error", 502);
2202
- }
2203
- });
2204
- app.delete(`${apiPrefix}/sandbox/:id/session/:sid`, async (c) => {
2205
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2206
- const sid = c.req.param("sid");
2207
- try {
2208
- const result = await sandbox.deleteSession(sid);
2209
- return c.json(result);
2210
- } catch (err) {
2211
- return errorJson(`session delete failed: ${err instanceof Error ? err.message : String(err)}`, "session_error", 502);
2212
- }
2213
- });
2214
2142
  app.delete(`${apiPrefix}/sandbox/:id`, async (c) => {
2215
2143
  const containerUUID = c.get("containerUUID");
2216
- const sandbox = getSandbox$1(getSandboxNs(c.env), containerUUID);
2144
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), containerUUID);
2217
2145
  try {
2218
2146
  await sandbox.destroy();
2219
2147
  } catch {}
2220
2148
  try {
2221
- const poolNs = getWarmPoolNs(c.env);
2222
- const poolId = poolNs.idFromName("global-pool");
2223
- await poolNs.get(poolId).reportStopped(containerUUID);
2149
+ await getWarmPoolStub(c.env, warmPoolBinding).reportStopped(containerUUID);
2224
2150
  } catch {}
2225
2151
  return new Response(null, { status: 204 });
2226
2152
  });
2227
- app.get(config.healthPath, (c) => {
2153
+ app.get(healthPath, (c) => {
2228
2154
  const errors = [];
2229
2155
  if (!c.env[sandboxBinding]) errors.push(`Missing required Durable Object binding "${sandboxBinding}". Ensure your wrangler.jsonc has a binding named "${sandboxBinding}".`);
2230
2156
  if (!c.env[warmPoolBinding]) errors.push(`Missing required Durable Object binding "${warmPoolBinding}". Ensure your wrangler.jsonc has a binding named "${warmPoolBinding}".`);
@@ -2245,9 +2171,7 @@ function createBridgeApp(config) {
2245
2171
  app.get(`${apiPrefix}/pool/stats`, async (c) => {
2246
2172
  const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
2247
2173
  const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
2248
- const poolNs = getWarmPoolNs(c.env);
2249
- const poolId = poolNs.idFromName("global-pool");
2250
- const poolStub = poolNs.get(poolId);
2174
+ const poolStub = getWarmPoolStub(c.env, warmPoolBinding);
2251
2175
  try {
2252
2176
  await poolStub.configure({
2253
2177
  warmTarget,
@@ -2260,9 +2184,7 @@ function createBridgeApp(config) {
2260
2184
  app.post(`${apiPrefix}/pool/shutdown-prewarmed`, async (c) => {
2261
2185
  const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
2262
2186
  const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
2263
- const poolNs = getWarmPoolNs(c.env);
2264
- const poolId = poolNs.idFromName("global-pool");
2265
- const poolStub = poolNs.get(poolId);
2187
+ const poolStub = getWarmPoolStub(c.env, warmPoolBinding);
2266
2188
  try {
2267
2189
  await poolStub.configure({
2268
2190
  warmTarget,
@@ -2276,6 +2198,399 @@ function createBridgeApp(config) {
2276
2198
  await primePool(c.env, warmPoolBinding);
2277
2199
  return c.json({ ok: true });
2278
2200
  });
2201
+ }
2202
+
2203
+ //#endregion
2204
+ //#region src/bridge/routes/processes.ts
2205
+ function registerProcessRoutes(app, apiPrefix, sandboxBinding) {
2206
+ app.post(`${apiPrefix}/sandbox/:id/processes`, async (c) => {
2207
+ const body = await parseProcessRequest(c.req.raw);
2208
+ if (body instanceof Response) return body;
2209
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2210
+ const options = processOptions(body);
2211
+ if (options instanceof Response) return options;
2212
+ try {
2213
+ const process = await sandbox.exec(body.argv, options);
2214
+ return c.json(await process.status());
2215
+ } catch (err) {
2216
+ return errorJson(`process launch failed: ${err instanceof Error ? err.message : String(err)}`, "process_error", 502);
2217
+ }
2218
+ });
2219
+ app.get(`${apiPrefix}/sandbox/:id/processes`, async (c) => {
2220
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2221
+ return c.json(await sandbox.listProcesses());
2222
+ });
2223
+ app.get(`${apiPrefix}/sandbox/:id/processes/:processId`, async (c) => {
2224
+ const process = await getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID")).getProcess(c.req.param("processId"));
2225
+ if (!process) return errorJson("Process not found", "not_found", 404);
2226
+ return c.json(await process.status());
2227
+ });
2228
+ app.get(`${apiPrefix}/sandbox/:id/processes/:processId/logs`, async (c) => {
2229
+ const process = await getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID")).getProcess(c.req.param("processId"));
2230
+ if (!process) return errorJson("Process not found", "not_found", 404);
2231
+ const replay = parseBoolean(c.req.query("replay"));
2232
+ const follow = parseBoolean(c.req.query("follow"));
2233
+ const logs = await process.logs({
2234
+ since: c.req.query("since"),
2235
+ replay,
2236
+ follow
2237
+ });
2238
+ return new Response(processLogsToSSE(logs), { headers: {
2239
+ "Content-Type": "text/event-stream",
2240
+ "Cache-Control": "no-cache"
2241
+ } });
2242
+ });
2243
+ app.post(`${apiPrefix}/sandbox/:id/processes/:processId/kill`, async (c) => {
2244
+ const process = await getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID")).getProcess(c.req.param("processId"));
2245
+ if (!process) return errorJson("Process not found", "not_found", 404);
2246
+ const signal = await parseKillRequest(c.req.raw);
2247
+ if (signal instanceof Response) return signal;
2248
+ await process.kill(signal);
2249
+ return c.body(null, 204);
2250
+ });
2251
+ app.post(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
2252
+ const port = Number(c.req.param("port"));
2253
+ if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
2254
+ const options = parseTunnelOptions(await c.req.text());
2255
+ if (options instanceof Response) return options;
2256
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2257
+ try {
2258
+ const tunnel = await sandbox.tunnels.get(port, options);
2259
+ return c.json(tunnel);
2260
+ } catch (err) {
2261
+ return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
2262
+ }
2263
+ });
2264
+ app.delete(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
2265
+ const port = Number(c.req.param("port"));
2266
+ if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
2267
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2268
+ try {
2269
+ await sandbox.tunnels.destroy(port);
2270
+ return c.body(null, 204);
2271
+ } catch (err) {
2272
+ return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
2273
+ }
2274
+ });
2275
+ app.get(`${apiPrefix}/sandbox/:id/running`, async (c) => {
2276
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2277
+ try {
2278
+ const response = { running: await sandbox.isRuntimeActive() };
2279
+ return c.json(response);
2280
+ } catch {
2281
+ return c.json({ running: false });
2282
+ }
2283
+ });
2284
+ }
2285
+ async function parseProcessRequest(request) {
2286
+ let body;
2287
+ try {
2288
+ body = await request.json();
2289
+ } catch {
2290
+ return errorJson("Invalid JSON body", "invalid_request", 400);
2291
+ }
2292
+ if (!Array.isArray(body.argv) || body.argv.length === 0) return errorJson("argv must be a non-empty array", "invalid_request", 400);
2293
+ if (body.argv.some((item) => typeof item !== "string")) return errorJson("argv items must be strings", "invalid_request", 400);
2294
+ if (body.argv[0].length === 0) return errorJson("argv executable must be non-empty", "invalid_request", 400);
2295
+ return body;
2296
+ }
2297
+ function processOptions(body) {
2298
+ const options = {};
2299
+ if (body.timeout !== void 0) {
2300
+ if (typeof body.timeout !== "number" || !Number.isFinite(body.timeout) || body.timeout <= 0) return errorJson("timeout must be a positive finite number", "invalid_request", 400);
2301
+ options.timeout = body.timeout;
2302
+ }
2303
+ if (typeof body.cwd === "string") {
2304
+ const cwd = resolveWorkspacePath(body.cwd);
2305
+ if (!cwd) return errorJson("cwd must resolve to a location within /workspace", "invalid_request", 403);
2306
+ options.cwd = cwd;
2307
+ }
2308
+ if ("env" in body && body.env !== void 0) {
2309
+ if (!isStringRecord(body.env)) return errorJson("env must be an object of strings", "invalid_request", 400);
2310
+ options.env = body.env;
2311
+ }
2312
+ return options;
2313
+ }
2314
+ async function parseKillRequest(request) {
2315
+ if (!request.headers.get("content-type")) return 15;
2316
+ let body;
2317
+ try {
2318
+ body = await request.json();
2319
+ } catch {
2320
+ return errorJson("Invalid JSON body", "invalid_request", 400);
2321
+ }
2322
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return errorJson("Request body must be an object", "invalid_request", 400);
2323
+ const signal = body.signal;
2324
+ if (signal === void 0) return 15;
2325
+ if (typeof signal !== "number" || !Number.isInteger(signal) || signal < 1 || signal > 64) return errorJson("signal must be an integer between 1 and 64", "invalid_request", 400);
2326
+ return signal;
2327
+ }
2328
+ function parseBoolean(value) {
2329
+ if (value === void 0) return void 0;
2330
+ return value === "true" || value === "1";
2331
+ }
2332
+ function isStringRecord(value) {
2333
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((item) => typeof item === "string");
2334
+ }
2335
+ function processLogsToSSE(logs) {
2336
+ const reader = logs.getReader();
2337
+ const encoder = new TextEncoder();
2338
+ let finished = false;
2339
+ async function finish(cancelSource) {
2340
+ if (finished) return;
2341
+ finished = true;
2342
+ if (cancelSource) await reader.cancel().catch(() => void 0);
2343
+ reader.releaseLock();
2344
+ }
2345
+ return new ReadableStream({
2346
+ async pull(controller) {
2347
+ try {
2348
+ const result = await reader.read();
2349
+ if (result.done) {
2350
+ await finish(false);
2351
+ controller.close();
2352
+ return;
2353
+ }
2354
+ const event = result.value;
2355
+ const data = event.type === "stdout" || event.type === "stderr" ? {
2356
+ type: event.type,
2357
+ cursor: event.cursor,
2358
+ timestamp: event.timestamp,
2359
+ data: bytesToBase64(event.data)
2360
+ } : event;
2361
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
2362
+ } catch (error) {
2363
+ await finish(true);
2364
+ controller.error(error);
2365
+ }
2366
+ },
2367
+ async cancel() {
2368
+ await finish(true);
2369
+ }
2370
+ }, { highWaterMark: 0 });
2371
+ }
2372
+ function bytesToBase64(data) {
2373
+ let binary = "";
2374
+ for (const byte of data) binary += String.fromCharCode(byte);
2375
+ return btoa(binary);
2376
+ }
2377
+
2378
+ //#endregion
2379
+ //#region src/bridge/routes/terminals.ts
2380
+ function parsePositiveInteger(value, name) {
2381
+ if (value === void 0) return void 0;
2382
+ const parsed = Number(value);
2383
+ if (!Number.isInteger(parsed) || parsed <= 0) return errorJson(`${name} must be a positive integer`, "invalid_request", 400);
2384
+ return parsed;
2385
+ }
2386
+ function isSandboxCommand(value) {
2387
+ return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string");
2388
+ }
2389
+ function parseCreateTerminalBody(rawBody) {
2390
+ let body;
2391
+ try {
2392
+ body = JSON.parse(rawBody);
2393
+ } catch {
2394
+ return errorJson("Invalid JSON body", "invalid_request", 400);
2395
+ }
2396
+ if (!body || typeof body !== "object" || Array.isArray(body)) return errorJson("Request body must be an object", "invalid_request", 400);
2397
+ const record = body;
2398
+ const argv = record.argv;
2399
+ if (!isSandboxCommand(argv)) return errorJson("argv must be a non-empty string array", "invalid_request", 400);
2400
+ const options = { command: argv };
2401
+ if (record.cwd !== void 0) {
2402
+ if (typeof record.cwd !== "string") return errorJson("cwd must be a string", "invalid_request", 400);
2403
+ options.cwd = record.cwd;
2404
+ }
2405
+ if (record.env !== void 0) {
2406
+ if (!record.env || typeof record.env !== "object" || Array.isArray(record.env)) return errorJson("env must be an object", "invalid_request", 400);
2407
+ const env$1 = {};
2408
+ for (const [key, value] of Object.entries(record.env)) {
2409
+ if (typeof value !== "string") return errorJson("env values must be strings", "invalid_request", 400);
2410
+ env$1[key] = value;
2411
+ }
2412
+ options.env = env$1;
2413
+ }
2414
+ if (record.cols !== void 0) {
2415
+ if (typeof record.cols !== "number" || !Number.isInteger(record.cols) || record.cols <= 0) return errorJson("cols must be a positive integer", "invalid_request", 400);
2416
+ options.cols = record.cols;
2417
+ }
2418
+ if (record.rows !== void 0) {
2419
+ if (typeof record.rows !== "number" || !Number.isInteger(record.rows) || record.rows <= 0) return errorJson("rows must be a positive integer", "invalid_request", 400);
2420
+ options.rows = record.rows;
2421
+ }
2422
+ return options;
2423
+ }
2424
+ function terminalError(error) {
2425
+ return errorJson(`terminal failed: ${error instanceof Error ? error.message : String(error)}`, "exec_transport_error", 502);
2426
+ }
2427
+ function registerTerminalRoutes(app, apiPrefix, sandboxBinding) {
2428
+ app.post(`${apiPrefix}/sandbox/:id/terminals`, async (c) => {
2429
+ const parsed = parseCreateTerminalBody(await c.req.text());
2430
+ if (parsed instanceof Response) return parsed;
2431
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2432
+ try {
2433
+ const terminal = await sandbox.createTerminal(parsed);
2434
+ return c.json(await terminal.getSnapshot());
2435
+ } catch (error) {
2436
+ return terminalError(error);
2437
+ }
2438
+ });
2439
+ app.get(`${apiPrefix}/sandbox/:id/terminals`, async (c) => {
2440
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2441
+ try {
2442
+ const terminals = await sandbox.listTerminals();
2443
+ return c.json(await Promise.all(terminals.map((terminal) => terminal.getSnapshot())));
2444
+ } catch (error) {
2445
+ return terminalError(error);
2446
+ }
2447
+ });
2448
+ app.get(`${apiPrefix}/sandbox/:id/terminals/:terminalId`, async (c) => {
2449
+ const terminalId = validateTerminalId(c.req.param("terminalId"));
2450
+ if (!terminalId) return errorJson("Invalid terminal ID format", "invalid_request", 400);
2451
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2452
+ try {
2453
+ const terminal = await sandbox.getTerminal(terminalId);
2454
+ if (!terminal) return errorJson("Terminal not found", "not_found", 404);
2455
+ return c.json(await terminal.getSnapshot());
2456
+ } catch (error) {
2457
+ return terminalError(error);
2458
+ }
2459
+ });
2460
+ app.get(`${apiPrefix}/sandbox/:id/terminals/:terminalId/connect`, async (c) => {
2461
+ const upgrade = c.req.header("Upgrade");
2462
+ if (!upgrade || upgrade.toLowerCase() !== "websocket") return errorJson("WebSocket upgrade required", "invalid_request", 400);
2463
+ const terminalId = validateTerminalId(c.req.param("terminalId"));
2464
+ if (!terminalId) return errorJson("Invalid terminal ID format", "invalid_request", 400);
2465
+ const cols = parsePositiveInteger(c.req.query("cols"), "cols");
2466
+ if (cols instanceof Response) return cols;
2467
+ const rows = parsePositiveInteger(c.req.query("rows"), "rows");
2468
+ if (rows instanceof Response) return rows;
2469
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2470
+ try {
2471
+ const terminal = await sandbox.getTerminal(terminalId);
2472
+ if (!terminal) return errorJson("Terminal not found", "not_found", 404);
2473
+ return await terminal.connect(c.req.raw, {
2474
+ cursor: c.req.query("cursor"),
2475
+ cols,
2476
+ rows
2477
+ });
2478
+ } catch (error) {
2479
+ return terminalError(error);
2480
+ }
2481
+ });
2482
+ app.post(`${apiPrefix}/sandbox/:id/terminals/:terminalId/interrupt`, async (c) => {
2483
+ const terminalId = validateTerminalId(c.req.param("terminalId"));
2484
+ if (!terminalId) return errorJson("Invalid terminal ID format", "invalid_request", 400);
2485
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2486
+ try {
2487
+ const terminal = await sandbox.getTerminal(terminalId);
2488
+ if (!terminal) return errorJson("Terminal not found", "not_found", 404);
2489
+ await terminal.interrupt();
2490
+ return new Response(null, { status: 204 });
2491
+ } catch (error) {
2492
+ return terminalError(error);
2493
+ }
2494
+ });
2495
+ app.post(`${apiPrefix}/sandbox/:id/terminals/:terminalId/terminate`, async (c) => {
2496
+ const terminalId = validateTerminalId(c.req.param("terminalId"));
2497
+ if (!terminalId) return errorJson("Invalid terminal ID format", "invalid_request", 400);
2498
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2499
+ try {
2500
+ const terminal = await sandbox.getTerminal(terminalId);
2501
+ if (!terminal) return errorJson("Terminal not found", "not_found", 404);
2502
+ await terminal.terminate();
2503
+ return new Response(null, { status: 204 });
2504
+ } catch (error) {
2505
+ return terminalError(error);
2506
+ }
2507
+ });
2508
+ }
2509
+
2510
+ //#endregion
2511
+ //#region src/bridge/routes.ts
2512
+ function sandboxRoutePoolMode(method, routePath) {
2513
+ if (method === "GET" && routePath === "/running") return "lookup";
2514
+ if (method === "GET" && (routePath === "/processes" || /^\/processes\/[^/]+(?:\/logs)?$/.test(routePath))) return "lookup";
2515
+ if (method === "POST" && /^\/processes\/[^/]+\/kill$/.test(routePath)) return "lookup";
2516
+ if (method === "GET" && (routePath === "/terminals" || /^\/terminals\/[^/]+(?:\/connect)?$/.test(routePath))) return "lookup";
2517
+ if (method === "POST" && /^\/terminals\/[^/]+\/(interrupt|terminate)$/.test(routePath)) return "lookup";
2518
+ if (method === "POST" && [
2519
+ "/processes",
2520
+ "/terminals",
2521
+ "/persist",
2522
+ "/hydrate",
2523
+ "/mount",
2524
+ "/unmount"
2525
+ ].includes(routePath)) return "allocate";
2526
+ if ((method === "GET" || method === "PUT") && routePath.startsWith("/file/")) return "allocate";
2527
+ if ((method === "POST" || method === "DELETE") && /^\/tunnel\/[^/]+$/.test(routePath)) return "allocate";
2528
+ return "unmatched";
2529
+ }
2530
+ function missingRuntimeResponse(routePath) {
2531
+ if (routePath === "/running") return Response.json({ running: false });
2532
+ if (routePath === "/processes" || routePath === "/terminals") return Response.json([]);
2533
+ if (routePath.startsWith("/terminals/")) return errorJson("Terminal not found", "not_found", 404);
2534
+ return errorJson("Process not found", "not_found", 404);
2535
+ }
2536
+ function createBridgeApp(config) {
2537
+ const app = new Hono();
2538
+ const { sandboxBinding, warmPoolBinding, apiPrefix } = config;
2539
+ app.use(`${apiPrefix}/sandbox/*`, async (c, next) => {
2540
+ const sandboxId = new URL(c.req.url).pathname.split("/")[apiPrefix.split("/").filter(Boolean).length + 2];
2541
+ if (sandboxId && !/^[a-z2-7]{1,128}$/.test(sandboxId)) return errorJson("Invalid sandbox ID format", "invalid_request", 400);
2542
+ const token = c.env.SANDBOX_API_KEY;
2543
+ if (token) {
2544
+ const authHeader = c.req.header("Authorization") ?? "";
2545
+ if ((authHeader.toLowerCase().startsWith("bearer ") ? authHeader.slice(7) : "") !== token) return errorJson("Unauthorized", "unauthorized", 401);
2546
+ }
2547
+ return next();
2548
+ });
2549
+ app.use(`${apiPrefix}/sandbox/:id/*`, async (c, next) => {
2550
+ const url = new URL(c.req.url);
2551
+ const sandboxId = c.req.param("id");
2552
+ const routePath = url.pathname.slice(`${apiPrefix}/sandbox/${sandboxId}`.length);
2553
+ const poolMode = sandboxRoutePoolMode(c.req.method, routePath);
2554
+ if (poolMode === "unmatched") return next();
2555
+ const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
2556
+ const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
2557
+ const poolStub = getWarmPoolStub(c.env, warmPoolBinding);
2558
+ try {
2559
+ await poolStub.configure({
2560
+ warmTarget,
2561
+ refreshInterval
2562
+ });
2563
+ const containerUUID = poolMode === "lookup" ? await poolStub.lookupContainer(sandboxId) : await poolStub.getContainer(sandboxId);
2564
+ if (!containerUUID) return missingRuntimeResponse(routePath);
2565
+ c.set("containerUUID", containerUUID);
2566
+ } catch (err) {
2567
+ const msg = err instanceof Error ? err.message : String(err);
2568
+ if (msg.includes("instance limit reached")) return errorJson(msg, "capacity_exceeded", 503);
2569
+ return errorJson(`pool error: ${msg}`, "pool_error", 502);
2570
+ }
2571
+ return next();
2572
+ });
2573
+ app.use(`${apiPrefix}/sandbox/:id`, async (c, next) => {
2574
+ if (c.req.method !== "DELETE") return next();
2575
+ const sandboxId = c.req.param("id");
2576
+ const poolStub = getWarmPoolStub(c.env, warmPoolBinding);
2577
+ try {
2578
+ const containerUUID = await poolStub.lookupContainer(sandboxId);
2579
+ if (!containerUUID) return new Response(null, { status: 204 });
2580
+ c.set("containerUUID", containerUUID);
2581
+ } catch (err) {
2582
+ return errorJson(`pool error: ${err instanceof Error ? err.message : String(err)}`, "pool_error", 502);
2583
+ }
2584
+ return next();
2585
+ });
2586
+ registerLifecycleRoutes(app, apiPrefix, config.healthPath, sandboxBinding, warmPoolBinding);
2587
+ registerProcessRoutes(app, apiPrefix, sandboxBinding);
2588
+ registerFileRoutes(app, apiPrefix, sandboxBinding);
2589
+ registerTerminalRoutes(app, apiPrefix, sandboxBinding);
2590
+ registerOpenApiRoutes(app, apiPrefix);
2591
+ return app;
2592
+ }
2593
+ function registerOpenApiRoutes(app, apiPrefix) {
2279
2594
  const openapiAuth = async (c, next) => {
2280
2595
  const token = c.env.SANDBOX_API_KEY;
2281
2596
  if (token) {
@@ -2290,7 +2605,6 @@ function createBridgeApp(config) {
2290
2605
  app.get(`${apiPrefix}/openapi.json`, openapiAuth, (c) => c.json(OPENAPI_SCHEMA));
2291
2606
  app.get(`${apiPrefix}/openapi.html`, openapiAuth, openapiHtmlHandler);
2292
2607
  app.get(`${apiPrefix}/openapi`, openapiAuth, openapiHtmlHandler);
2293
- return app;
2294
2608
  }
2295
2609
 
2296
2610
  //#endregion
@@ -2298,12 +2612,9 @@ function createBridgeApp(config) {
2298
2612
  /**
2299
2613
  * WarmPool — Durable Object that maintains a pool of pre-started sandbox containers.
2300
2614
  *
2301
- * Adapted from https://github.com/mikenomitch/cf-container-warm-pool
2302
- * Inlined and tailored for the @cloudflare/sandbox SDK.
2303
- *
2304
- * The pool keeps N idle containers standing by so new sandbox sessions boot
2305
- * instantly. Once a container is assigned to a sandbox ID it is consumed and
2306
- * never returned to the pool.
2615
+ * The pool keeps N idle containers standing by so new sandboxes boot instantly.
2616
+ * Once a container is assigned to a sandbox ID, it is consumed and never returned
2617
+ * to the pool.
2307
2618
  *
2308
2619
  * Configuration is pushed in via `configure()` on every request (idempotent)
2309
2620
  * so changes to wrangler vars take effect without manual intervention.
@@ -2405,11 +2716,11 @@ var WarmPool = class extends DurableObject {
2405
2716
  async shutdownPrewarmed() {
2406
2717
  await this.init();
2407
2718
  for (const containerUUID of [...this.warmContainers]) try {
2408
- await this.getSandboxStub(containerUUID).stop();
2719
+ await this.getSandboxStub(containerUUID).destroy();
2409
2720
  this.warmContainers.delete(containerUUID);
2410
2721
  } catch (error) {
2411
2722
  console.error({
2412
- message: "Failed to stop container",
2723
+ message: "Failed to destroy container",
2413
2724
  component: "warm-pool",
2414
2725
  containerUUID,
2415
2726
  error
@@ -2589,22 +2900,22 @@ var WarmPool = class extends DurableObject {
2589
2900
  console.info({
2590
2901
  message: "Scaling down pool",
2591
2902
  component: "warm-pool",
2592
- stopping: excess
2903
+ destroying: excess
2593
2904
  });
2594
- const toStop = [...this.warmContainers].slice(0, excess);
2595
- const stopped = [];
2596
- for (const uuid of toStop) try {
2597
- await this.getSandboxStub(uuid).stop();
2598
- stopped.push(uuid);
2905
+ const toDestroy = [...this.warmContainers].slice(0, excess);
2906
+ const destroyed = [];
2907
+ for (const uuid of toDestroy) try {
2908
+ await this.getSandboxStub(uuid).destroy();
2909
+ destroyed.push(uuid);
2599
2910
  } catch (error) {
2600
2911
  console.error({
2601
- message: "Failed to stop container",
2912
+ message: "Failed to destroy container",
2602
2913
  component: "warm-pool",
2603
2914
  containerUUID: uuid,
2604
2915
  error
2605
2916
  });
2606
2917
  }
2607
- for (const uuid of stopped) this.warmContainers.delete(uuid);
2918
+ for (const uuid of destroyed) this.warmContainers.delete(uuid);
2608
2919
  await this.persist();
2609
2920
  }
2610
2921
  }