@cloudflare/sandbox 0.13.0-next.681.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 (69) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +49 -3
  3. package/dist/bridge/index.d.ts.map +1 -1
  4. package/dist/bridge/index.js +1364 -1072
  5. package/dist/bridge/index.js.map +1 -1
  6. package/dist/{contexts-DY1LHU1v.d.ts → contexts-1EsLHByO.d.ts} +156 -20
  7. package/dist/contexts-1EsLHByO.d.ts.map +1 -0
  8. package/dist/{dist-BStBkGIC.js → dist-Duor5GbS.js} +7 -56
  9. package/dist/dist-Duor5GbS.js.map +1 -0
  10. package/dist/errors/index.d.ts +4 -4
  11. package/dist/errors/index.js +3 -3
  12. package/dist/{errors-k3B8orjH.js → errors-CXR0xBpw.js} +42 -10
  13. package/dist/errors-CXR0xBpw.js.map +1 -0
  14. package/dist/{errors-ewgSNicb.js → errors-QYlSkVGz.js} +402 -250
  15. package/dist/errors-QYlSkVGz.js.map +1 -0
  16. package/dist/extensions/index.d.ts +4 -2
  17. package/dist/extensions/index.js +5 -3
  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 +8 -19
  23. package/dist/git/index.d.ts.map +1 -1
  24. package/dist/git/index.js +16 -23
  25. package/dist/git/index.js.map +1 -1
  26. package/dist/index-Bs4bqXDR.d.ts +438 -0
  27. package/dist/index-Bs4bqXDR.d.ts.map +1 -0
  28. package/dist/{index-Dy6u9r60.d.ts → index-HNYBk-az.d.ts} +233 -179
  29. package/dist/index-HNYBk-az.d.ts.map +1 -0
  30. package/dist/index.d.ts +480 -11
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +6 -6
  33. package/dist/interpreter/index.d.ts +3 -1
  34. package/dist/interpreter/index.d.ts.map +1 -1
  35. package/dist/interpreter/index.js +46 -18
  36. package/dist/interpreter/index.js.map +1 -1
  37. package/dist/openai/index.d.ts +5 -5
  38. package/dist/openai/index.d.ts.map +1 -1
  39. package/dist/openai/index.js +11 -6
  40. package/dist/openai/index.js.map +1 -1
  41. package/dist/opencode/index.d.ts +7 -11
  42. package/dist/opencode/index.d.ts.map +1 -1
  43. package/dist/opencode/index.js +56 -85
  44. package/dist/opencode/index.js.map +1 -1
  45. package/dist/process-types-GStiZ8f8.d.ts +73 -0
  46. package/dist/process-types-GStiZ8f8.d.ts.map +1 -0
  47. package/dist/{sandbox-boKWPIcd.js → sandbox-Auuwfnur.js} +4152 -3359
  48. package/dist/sandbox-Auuwfnur.js.map +1 -0
  49. package/dist/sandbox-BbAabq93.d.ts +42 -0
  50. package/dist/sandbox-BbAabq93.d.ts.map +1 -0
  51. package/dist/xterm/index.d.ts +5 -1
  52. package/dist/xterm/index.d.ts.map +1 -1
  53. package/dist/xterm/index.js +55 -12
  54. package/dist/xterm/index.js.map +1 -1
  55. package/package.json +3 -2
  56. package/dist/codes-y-U76vnQ.d.ts +0 -79
  57. package/dist/codes-y-U76vnQ.d.ts.map +0 -1
  58. package/dist/contexts-DY1LHU1v.d.ts.map +0 -1
  59. package/dist/dist-BStBkGIC.js.map +0 -1
  60. package/dist/errors-ewgSNicb.js.map +0 -1
  61. package/dist/errors-k3B8orjH.js.map +0 -1
  62. package/dist/extensions-CepYdzro.js +0 -191
  63. package/dist/extensions-CepYdzro.js.map +0 -1
  64. package/dist/index-B7QgIs0N.d.ts +0 -1841
  65. package/dist/index-B7QgIs0N.d.ts.map +0 -1
  66. package/dist/index-Dy6u9r60.d.ts.map +0 -1
  67. package/dist/sandbox-DHNO89IF.d.ts +0 -828
  68. package/dist/sandbox-DHNO89IF.d.ts.map +0 -1
  69. package/dist/sandbox-boKWPIcd.js.map +0 -1
@@ -1,8 +1,8 @@
1
- import "../dist-BStBkGIC.js";
2
- import "../errors-k3B8orjH.js";
3
- import "../errors-ewgSNicb.js";
4
- import { S as streamFile, g as validateTunnelName, h as validatePort, m as SandboxSecurityError, n as getSandbox } from "../sandbox-boKWPIcd.js";
5
- import "../extensions-CepYdzro.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";
6
6
  import { DurableObject, env } from "cloudflare:workers";
7
7
  import { Hono } from "hono";
8
8
 
@@ -29,16 +29,6 @@ async function primePool(env$1, warmPoolBinding) {
29
29
  /**
30
30
  * Utility functions used by the bridge routes.
31
31
  */
32
- /**
33
- * UTF-8-safe base64 encoding.
34
- * btoa() only handles latin-1; encode to UTF-8 bytes first via TextEncoder.
35
- */
36
- function toBase64(str) {
37
- const bytes = new TextEncoder().encode(str);
38
- let binary = "";
39
- for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
40
- return btoa(binary);
41
- }
42
32
  /** RFC 4648 base32 encoding (lowercase). Returns only [a-z2-7]. */
43
33
  function base32Encode(data) {
44
34
  const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
@@ -96,10 +86,10 @@ function resolveWorkspacePath(userPath) {
96
86
  return null;
97
87
  }
98
88
  /**
99
- * 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.
100
90
  * Returns the validated ID or null if invalid.
101
91
  */
102
- function validateSessionId(id) {
92
+ function validateTerminalId(id) {
103
93
  if (!/^[a-zA-Z0-9._-]{1,128}$/.test(id)) return null;
104
94
  if (id.includes("..")) return null;
105
95
  return id;
@@ -110,24 +100,73 @@ function validateSessionId(id) {
110
100
  */
111
101
  function sseToByteStream(sse) {
112
102
  const encoder = new TextEncoder();
113
- return new ReadableStream({ async start(controller) {
114
- try {
115
- for await (const chunk of streamFile(sse)) controller.enqueue(chunk instanceof Uint8Array ? chunk : encoder.encode(chunk));
116
- controller.close();
117
- } catch (err) {
118
- 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);
119
158
  }
120
- } });
159
+ }, { highWaterMark: 0 });
121
160
  }
122
161
 
123
162
  //#endregion
124
- //#region src/bridge/openapi.ts
163
+ //#region src/bridge/openapi/common.ts
125
164
  /**
126
165
  * OpenAPI 3.1 schema for the Cloudflare Sandbox Service API.
127
166
  *
128
167
  * Served at GET /v1/openapi.json (requires Bearer token auth).
129
168
  */
130
- const OPENAPI_SCHEMA = {
169
+ const OPENAPI_BASE = {
131
170
  openapi: "3.1.0",
132
171
  info: {
133
172
  title: "Cloudflare Sandbox Service API",
@@ -141,7 +180,7 @@ const OPENAPI_SCHEMA = {
141
180
  description: "API token set via `wrangler secret put SANDBOX_API_KEY`. The /openapi.* routes also accept the token as a `?token=` query parameter."
142
181
  } },
143
182
  schemas: {
144
- ExecRequest: {
183
+ ProcessCreateRequest: {
145
184
  type: "object",
146
185
  required: ["argv"],
147
186
  properties: {
@@ -149,25 +188,121 @@ const OPENAPI_SCHEMA = {
149
188
  type: "array",
150
189
  items: { type: "string" },
151
190
  minItems: 1,
152
- description: "Argv array already shell-expanded by the Python layer if shell=True.",
191
+ description: "Argv array to launch directly.",
153
192
  example: [
154
193
  "sh",
155
194
  "-lc",
156
195
  "echo hello"
157
196
  ]
158
197
  },
159
- timeout_ms: {
198
+ timeout: {
160
199
  type: "integer",
161
- 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.",
162
201
  example: 3e4
163
202
  },
164
203
  cwd: {
165
204
  type: "string",
166
205
  description: "Working directory for the command (defaults to sandbox cwd).",
167
206
  example: "/workspace"
207
+ },
208
+ env: {
209
+ type: "object",
210
+ additionalProperties: { type: "string" },
211
+ description: "Environment overrides for the process."
168
212
  }
169
213
  }
170
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
+ },
171
306
  WriteResponse: {
172
307
  type: "object",
173
308
  required: ["ok"],
@@ -341,7 +476,6 @@ const OPENAPI_SCHEMA = {
341
476
  "pool_error",
342
477
  "mount_error",
343
478
  "unmount_error",
344
- "session_error",
345
479
  "tunnel_error"
346
480
  ]
347
481
  }
@@ -403,445 +537,106 @@ const OPENAPI_SCHEMA = {
403
537
  code: "invalid_request"
404
538
  }
405
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
+ } }
406
560
  }
407
561
  }
408
562
  },
409
- security: [{ BearerAuth: [] }],
410
- paths: {
411
- "/v1/sandbox": { post: {
412
- operationId: "createSandbox",
413
- summary: "Create a new sandbox session",
414
- description: "Generates a new unique sandbox ID. Use this ID with all `/v1/sandbox/{id}/*` routes.",
415
- "x-codeSamples": [{
416
- lang: "curl",
417
- label: "cURL",
418
- source: "curl -X POST https://$HOST/v1/sandbox \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
419
- }],
420
- responses: {
421
- "200": {
422
- description: "New sandbox session created.",
423
- content: { "application/json": { schema: {
424
- type: "object",
425
- required: ["id"],
426
- properties: { id: {
427
- type: "string",
428
- description: "Unique sandbox ID for use with `/v1/sandbox/{id}/*` routes.",
429
- example: "mfrggzdfmy2tqnrzgezdgnbv"
430
- } }
431
- } } }
432
- },
433
- "401": { $ref: "#/components/responses/Unauthorized" }
434
- }
435
- } },
436
- "/v1/sandbox/{id}/exec": { post: {
437
- operationId: "execCommand",
438
- summary: "Execute a command in the sandbox",
439
- 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",
440
573
  "x-codeSamples": [{
441
574
  lang: "curl",
442
575
  label: "cURL",
443
- 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"
444
577
  }],
445
578
  parameters: [{
446
579
  name: "id",
447
580
  in: "path",
448
581
  required: true,
449
582
  schema: { type: "string" },
450
- description: "Sandbox instance name (maps to a Durable Object key)."
583
+ description: "Sandbox instance name."
451
584
  }, {
452
- name: "Session-Id",
453
- in: "header",
454
- required: false,
455
- schema: {
456
- type: "string",
457
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
458
- },
459
- description: "Scope this operation to an explicit session. If omitted, the operation uses top-level sandbox behavior."
460
- }],
461
- requestBody: {
585
+ name: "path",
586
+ in: "path",
462
587
  required: true,
463
- content: { "application/json": { schema: { $ref: "#/components/schemas/ExecRequest" } } }
464
- },
588
+ schema: { type: "string" },
589
+ description: "File path inside the sandbox, without leading slash (e.g. workspace/main.py). Must resolve within /workspace."
590
+ }],
465
591
  responses: {
466
592
  "200": {
467
- 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)",
468
- 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
+ } } }
469
598
  },
470
599
  "400": { $ref: "#/components/responses/InvalidRequest" },
471
600
  "401": { $ref: "#/components/responses/Unauthorized" },
472
601
  "403": {
473
- description: "cwd resolves outside /workspace.",
602
+ description: "Path resolves outside /workspace.",
474
603
  content: { "application/json": {
475
604
  schema: { $ref: "#/components/schemas/ErrorResponse" },
476
605
  example: {
477
- error: "cwd must resolve to a location within /workspace",
606
+ error: "path must resolve to a location within /workspace",
478
607
  code: "invalid_request"
479
608
  }
480
609
  } }
481
610
  },
482
- "502": {
483
- 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.",
484
- content: { "application/json": {
485
- schema: { $ref: "#/components/schemas/ErrorResponse" },
486
- example: {
487
- error: "exec failed: connection reset",
488
- code: "exec_transport_error"
489
- }
490
- } }
491
- }
492
- }
493
- } },
494
- "/v1/sandbox/{id}/tunnel/{port}": {
495
- post: {
496
- operationId: "createTunnel",
497
- summary: "Create or reuse a tunnel for a sandbox port",
498
- 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.",
499
- "x-codeSamples": [{
500
- lang: "curl",
501
- label: "Ephemeral tunnel",
502
- source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/tunnel/8080 \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
503
- }, {
504
- lang: "curl",
505
- label: "Named tunnel",
506
- 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\"}'"
507
- }],
508
- parameters: [{
509
- name: "id",
510
- in: "path",
511
- required: true,
512
- schema: { type: "string" },
513
- description: "Sandbox instance name."
514
- }, {
515
- name: "port",
516
- in: "path",
517
- required: true,
518
- schema: {
519
- type: "integer",
520
- minimum: 1024,
521
- maximum: 65535
522
- },
523
- description: "Container port to tunnel. Port 3000 is reserved."
524
- }],
525
- requestBody: {
526
- required: false,
527
- content: { "application/json": { schema: { $ref: "#/components/schemas/TunnelRequest" } } }
528
- },
529
- responses: {
530
- "200": {
531
- description: "Tunnel created or reused.",
532
- content: { "application/json": { schema: { $ref: "#/components/schemas/Tunnel" } } }
533
- },
534
- "400": { $ref: "#/components/responses/InvalidRequest" },
535
- "401": { $ref: "#/components/responses/Unauthorized" },
536
- "502": {
537
- description: "Tunnel creation failed.",
538
- content: { "application/json": {
539
- schema: { $ref: "#/components/schemas/ErrorResponse" },
540
- example: {
541
- error: "tunnel failed: cloudflared could not be found",
542
- code: "tunnel_error"
543
- }
544
- } }
545
- }
546
- }
547
- },
548
- delete: {
549
- operationId: "deleteTunnel",
550
- summary: "Delete the tunnel for a sandbox port",
551
- description: "Stops the tunnel process for the port and removes any named-tunnel Cloudflare resources tracked by the sandbox.",
552
- "x-codeSamples": [{
553
- lang: "curl",
554
- label: "Delete tunnel",
555
- source: "curl -X DELETE https://$HOST/v1/sandbox/my-sandbox/tunnel/8080 \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
556
- }],
557
- parameters: [{
558
- name: "id",
559
- in: "path",
560
- required: true,
561
- schema: { type: "string" },
562
- description: "Sandbox instance name."
563
- }, {
564
- name: "port",
565
- in: "path",
566
- required: true,
567
- schema: {
568
- type: "integer",
569
- minimum: 1024,
570
- maximum: 65535
571
- },
572
- description: "Container port whose tunnel should be deleted. Port 3000 is reserved."
573
- }],
574
- responses: {
575
- "204": { description: "Tunnel deleted or already absent." },
576
- "400": { $ref: "#/components/responses/InvalidRequest" },
577
- "401": { $ref: "#/components/responses/Unauthorized" },
578
- "502": {
579
- description: "Tunnel deletion failed.",
580
- content: { "application/json": {
581
- schema: { $ref: "#/components/schemas/ErrorResponse" },
582
- example: {
583
- error: "tunnel failed: cleanup failed",
584
- code: "tunnel_error"
585
- }
586
- } }
587
- }
588
- }
589
- }
590
- },
591
- "/v1/sandbox/{id}/file/{path}": {
592
- get: {
593
- operationId: "readFile",
594
- summary: "Read a file from the sandbox filesystem",
595
- "x-codeSamples": [{
596
- lang: "curl",
597
- label: "cURL",
598
- 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"
599
- }],
600
- parameters: [
601
- {
602
- name: "id",
603
- in: "path",
604
- required: true,
605
- schema: { type: "string" },
606
- description: "Sandbox instance name."
607
- },
608
- {
609
- name: "path",
610
- in: "path",
611
- required: true,
612
- schema: { type: "string" },
613
- description: "File path inside the sandbox, without leading slash (e.g. workspace/main.py). Must resolve within /workspace."
614
- },
615
- {
616
- name: "Session-Id",
617
- in: "header",
618
- required: false,
619
- schema: {
620
- type: "string",
621
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
622
- },
623
- description: "Scope this operation to an explicit session. If omitted, the operation uses top-level sandbox behavior."
624
- }
625
- ],
626
- responses: {
627
- "200": {
628
- description: "Raw file bytes.",
629
- content: { "application/octet-stream": { schema: {
630
- type: "string",
631
- format: "binary"
632
- } } }
633
- },
634
- "400": { $ref: "#/components/responses/InvalidRequest" },
635
- "401": { $ref: "#/components/responses/Unauthorized" },
636
- "403": {
637
- description: "Path resolves outside /workspace.",
638
- content: { "application/json": {
639
- schema: { $ref: "#/components/schemas/ErrorResponse" },
640
- example: {
641
- error: "path must resolve to a location within /workspace",
642
- code: "invalid_request"
643
- }
644
- } }
645
- },
646
- "404": {
647
- description: "File not found in the sandbox.",
648
- content: { "application/json": {
649
- schema: { $ref: "#/components/schemas/ErrorResponse" },
650
- example: {
651
- error: "File not found: /workspace/foo.txt",
652
- code: "workspace_read_not_found"
653
- }
654
- } }
655
- },
656
- "502": {
657
- description: "SDK read call failed.",
658
- content: { "application/json": {
659
- schema: { $ref: "#/components/schemas/ErrorResponse" },
660
- example: {
661
- error: "read failed: connection reset",
662
- code: "exec_transport_error"
663
- }
664
- } }
665
- }
666
- }
667
- },
668
- put: {
669
- operationId: "writeFile",
670
- summary: "Write a file into the sandbox filesystem",
671
- "x-codeSamples": [{
672
- lang: "curl",
673
- label: "cURL",
674
- 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"
675
- }],
676
- parameters: [
677
- {
678
- name: "id",
679
- in: "path",
680
- required: true,
681
- schema: { type: "string" },
682
- description: "Sandbox instance name."
683
- },
684
- {
685
- name: "path",
686
- in: "path",
687
- required: true,
688
- schema: { type: "string" },
689
- description: "File path inside the sandbox, without leading slash (e.g. workspace/main.py). Must resolve within /workspace."
690
- },
691
- {
692
- name: "Session-Id",
693
- in: "header",
694
- required: false,
695
- schema: {
696
- type: "string",
697
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
698
- },
699
- description: "Scope this operation to an explicit session. If omitted, the operation uses top-level sandbox behavior."
700
- }
701
- ],
702
- requestBody: {
703
- required: true,
704
- description: "Raw file content to write.",
705
- content: { "application/octet-stream": { schema: {
706
- type: "string",
707
- format: "binary"
708
- } } }
709
- },
710
- responses: {
711
- "200": {
712
- description: "File written successfully.",
713
- content: { "application/json": { schema: { $ref: "#/components/schemas/WriteResponse" } } }
714
- },
715
- "400": { $ref: "#/components/responses/InvalidRequest" },
716
- "401": { $ref: "#/components/responses/Unauthorized" },
717
- "403": {
718
- description: "Path resolves outside /workspace.",
719
- content: { "application/json": {
720
- schema: { $ref: "#/components/schemas/ErrorResponse" },
721
- example: {
722
- error: "path must resolve to a location within /workspace",
723
- code: "invalid_request"
724
- }
725
- } }
726
- },
727
- "502": {
728
- description: "SDK write call failed.",
729
- content: { "application/json": {
730
- schema: { $ref: "#/components/schemas/ErrorResponse" },
731
- example: {
732
- error: "write failed: connection reset",
733
- code: "workspace_archive_write_error"
734
- }
735
- } }
736
- }
737
- }
738
- }
739
- },
740
- "/v1/sandbox/{id}/pty": { get: {
741
- operationId: "ptyTerminal",
742
- summary: "Open a terminal via WebSocket",
743
- description: "Upgrades the HTTP connection to a WebSocket and proxies it to a PTY shell inside the sandbox.\n\n**A terminal ID is required**, supplied via either the `terminalId` query parameter or the `Terminal-Id` header (header takes precedence). The terminal ID is a stable resource name: reconnecting with the same ID reattaches to the same live PTY while the sandbox container is alive. If the sandbox container restarts (for example after it sleeps), connecting with the same ID creates a fresh shell — prior PTY state and process history are not preserved.\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 terminal stays alive while the sandbox container is alive; reconnect with the same terminal ID to reattach.",
744
- "x-codeSamples": [{
745
- lang: "JavaScript",
746
- label: "WebSocket",
747
- source: "const ws = new WebSocket(\"wss://$HOST/v1/sandbox/my-sandbox/pty?terminalId=term-1&cols=120&rows=30\");\nws.binaryType = \"arraybuffer\";\nws.onmessage = (e) => { /* handle binary output or JSON status */ };"
748
- }],
749
- parameters: [
750
- {
751
- name: "id",
752
- in: "path",
753
- required: true,
754
- schema: { type: "string" },
755
- description: "Sandbox instance name."
756
- },
757
- {
758
- name: "cols",
759
- in: "query",
760
- required: false,
761
- schema: {
762
- type: "integer",
763
- default: 80
764
- },
765
- description: "Terminal width in columns."
766
- },
767
- {
768
- name: "rows",
769
- in: "query",
770
- required: false,
771
- schema: {
772
- type: "integer",
773
- default: 24
774
- },
775
- description: "Terminal height in rows."
776
- },
777
- {
778
- name: "shell",
779
- in: "query",
780
- required: false,
781
- schema: { type: "string" },
782
- description: "Shell binary to run (e.g. `/bin/bash`). Uses the container default if omitted."
783
- },
784
- {
785
- name: "cwd",
786
- in: "query",
787
- required: false,
788
- schema: { type: "string" },
789
- description: "Initial working directory for newly created terminal resources."
790
- },
791
- {
792
- name: "terminalId",
793
- in: "query",
794
- required: false,
795
- schema: {
796
- type: "string",
797
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
798
- },
799
- description: "Terminal resource ID. Required unless the `Terminal-Id` request header is supplied. Stable name for this terminal resource: reuse the same ID across reconnects to reattach to the same live PTY. After the sandbox container restarts, the same ID creates a new shell. Ignored when `Terminal-Id` header is also present (header takes precedence)."
800
- },
801
- {
802
- name: "Terminal-Id",
803
- in: "header",
804
- required: false,
805
- schema: {
806
- type: "string",
807
- pattern: "^[a-zA-Z0-9._-]{1,128}$"
808
- },
809
- description: "Terminal resource ID. Required unless the `terminalId` query parameter is supplied. Takes precedence over the `terminalId` query parameter when both are present. Same stable-ID semantics apply: same ID reattaches to the live PTY; after the sandbox container restarts a new shell is created."
810
- }
811
- ],
812
- responses: {
813
- "101": { description: "WebSocket upgrade successful. Binary and text frames flow bidirectionally as described above." },
814
- "400": {
815
- description: "Missing `Upgrade: websocket` header or invalid query parameters.",
611
+ "404": {
612
+ description: "File not found in the sandbox.",
816
613
  content: { "application/json": {
817
614
  schema: { $ref: "#/components/schemas/ErrorResponse" },
818
615
  example: {
819
- error: "WebSocket upgrade required",
820
- code: "invalid_request"
616
+ error: "File not found: /workspace/foo.txt",
617
+ code: "workspace_read_not_found"
821
618
  }
822
619
  } }
823
620
  },
824
- "401": { $ref: "#/components/responses/Unauthorized" },
825
621
  "502": {
826
- description: "SDK terminal() call failed.",
622
+ description: "SDK read call failed.",
827
623
  content: { "application/json": {
828
624
  schema: { $ref: "#/components/schemas/ErrorResponse" },
829
625
  example: {
830
- error: "terminal failed: connection reset",
626
+ error: "read failed: connection reset",
831
627
  code: "exec_transport_error"
832
628
  }
833
629
  } }
834
630
  }
835
631
  }
836
- } },
837
- "/v1/sandbox/{id}/running": { get: {
838
- operationId: "isSandboxRunning",
839
- summary: "Check whether the sandbox container is alive",
840
- description: "Executes a no-op command inside the sandbox. Always returns HTTP 200; inspect the `running` field.",
632
+ },
633
+ put: {
634
+ operationId: "writeFile",
635
+ summary: "Write a file into the sandbox filesystem",
841
636
  "x-codeSamples": [{
842
637
  lang: "curl",
843
638
  label: "cURL",
844
- source: "curl -X GET https://$HOST/v1/sandbox/my-sandbox/running \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
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"
845
640
  }],
846
641
  parameters: [{
847
642
  name: "id",
@@ -849,399 +644,724 @@ const OPENAPI_SCHEMA = {
849
644
  required: true,
850
645
  schema: { type: "string" },
851
646
  description: "Sandbox instance name."
852
- }],
853
- responses: {
854
- "200": {
855
- description: "Liveness status (always returned, even when not running).",
856
- content: { "application/json": { schema: { $ref: "#/components/schemas/RunningResponse" } } }
857
- },
858
- "401": { $ref: "#/components/responses/Unauthorized" }
859
- }
860
- } },
861
- "/v1/sandbox/{id}/persist": { post: {
862
- operationId: "persistWorkspace",
863
- summary: "Serialize the sandbox workspace to a tar archive",
864
- description: "Archives the /workspace directory inside the sandbox and streams the resulting tar back as raw bytes.",
865
- "x-codeSamples": [{
866
- lang: "curl",
867
- label: "cURL",
868
- source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/persist \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -o workspace.tar"
869
- }],
870
- parameters: [{
871
- name: "id",
647
+ }, {
648
+ name: "path",
872
649
  in: "path",
873
650
  required: true,
874
651
  schema: { type: "string" },
875
- description: "Sandbox instance name."
876
- }, {
877
- name: "excludes",
878
- in: "query",
879
- required: false,
880
- schema: { type: "string" },
881
- description: "Comma-separated list of relative paths to exclude from the archive.",
882
- example: "__pycache__,.venv"
652
+ description: "File path inside the sandbox, without leading slash (e.g. workspace/main.py). Must resolve within /workspace."
883
653
  }],
654
+ requestBody: {
655
+ required: true,
656
+ description: "Raw file content to write.",
657
+ content: { "application/octet-stream": { schema: {
658
+ type: "string",
659
+ format: "binary"
660
+ } } }
661
+ },
884
662
  responses: {
885
663
  "200": {
886
- description: "Raw tar archive bytes.",
887
- content: { "application/octet-stream": { schema: {
888
- type: "string",
889
- format: "binary"
890
- } } }
664
+ description: "File written successfully.",
665
+ content: { "application/json": { schema: { $ref: "#/components/schemas/WriteResponse" } } }
891
666
  },
892
- "400": {
893
- description: "Invalid exclude paths (e.g. path traversal).",
667
+ "400": { $ref: "#/components/responses/InvalidRequest" },
668
+ "401": { $ref: "#/components/responses/Unauthorized" },
669
+ "403": {
670
+ description: "Path resolves outside /workspace.",
894
671
  content: { "application/json": {
895
672
  schema: { $ref: "#/components/schemas/ErrorResponse" },
896
673
  example: {
897
- error: "exclude paths must not contain \"..\"",
674
+ error: "path must resolve to a location within /workspace",
898
675
  code: "invalid_request"
899
676
  }
900
677
  } }
901
678
  },
902
- "401": { $ref: "#/components/responses/Unauthorized" },
903
679
  "502": {
904
- description: "tar command failed inside the sandbox.",
680
+ description: "SDK write call failed.",
905
681
  content: { "application/json": {
906
682
  schema: { $ref: "#/components/schemas/ErrorResponse" },
907
683
  example: {
908
- error: "tar failed (exit 1): ...",
909
- code: "workspace_archive_read_error"
684
+ error: "write failed: connection reset",
685
+ code: "workspace_archive_write_error"
910
686
  }
911
687
  } }
912
688
  }
913
689
  }
914
- } },
915
- "/v1/sandbox/{id}/hydrate": { post: {
916
- operationId: "hydrateWorkspace",
917
- summary: "Populate the sandbox workspace from a tar archive",
918
- description: "Accepts a raw tar archive as the request body and extracts it into /workspace inside the sandbox.",
919
- "x-codeSamples": [{
920
- lang: "curl",
921
- label: "cURL",
922
- 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"
923
- }],
924
- parameters: [{
925
- name: "id",
926
- in: "path",
927
- required: true,
928
- schema: { type: "string" },
929
- description: "Sandbox instance name."
930
- }],
931
- requestBody: {
932
- required: true,
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": {
933
717
  description: "Raw tar archive bytes.",
934
718
  content: { "application/octet-stream": { schema: {
935
719
  type: "string",
936
720
  format: "binary"
937
721
  } } }
938
722
  },
939
- responses: {
940
- "200": {
941
- description: "Archive extracted successfully.",
942
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
943
- },
944
- "400": { $ref: "#/components/responses/InvalidRequest" },
945
- "401": { $ref: "#/components/responses/Unauthorized" },
946
- "502": {
947
- description: "tar extract failed inside the sandbox.",
948
- content: { "application/json": {
949
- schema: { $ref: "#/components/schemas/ErrorResponse" },
950
- example: {
951
- error: "tar extract failed (exit 1): ...",
952
- code: "workspace_archive_write_error"
953
- }
954
- } }
955
- }
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
+ } }
956
743
  }
957
- } },
958
- "/v1/sandbox/{id}/mount": { post: {
959
- operationId: "mountBucket",
960
- summary: "Mount an S3-compatible bucket into the container",
961
- 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.",
962
- "x-codeSamples": [{
963
- lang: "curl",
964
- label: "cURL",
965
- 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\"}}'"
966
- }],
967
- parameters: [{
968
- name: "id",
969
- in: "path",
970
- required: true,
971
- schema: { type: "string" },
972
- description: "Sandbox instance name."
973
- }],
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],
974
1028
  requestBody: {
975
1029
  required: true,
976
- content: { "application/json": { schema: { $ref: "#/components/schemas/MountBucketRequest" } } }
1030
+ content: { "application/json": { schema: { $ref: "#/components/schemas/ProcessCreateRequest" } } }
977
1031
  },
978
1032
  responses: {
979
- "200": {
980
- description: "Bucket mounted successfully.",
981
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
982
- },
1033
+ "200": processStatusResponse,
983
1034
  "400": { $ref: "#/components/responses/InvalidRequest" },
984
1035
  "401": { $ref: "#/components/responses/Unauthorized" },
985
- "502": {
986
- description: "SDK mount call failed (invalid config, duplicate mount, or s3fs error).",
987
- content: { "application/json": {
988
- schema: { $ref: "#/components/schemas/ErrorResponse" },
989
- example: {
990
- error: "mount failed: Mount path already in use",
991
- code: "mount_error"
992
- }
993
- } }
994
- }
1036
+ "403": { $ref: "#/components/responses/InvalidRequest" },
1037
+ "502": { $ref: "#/components/responses/BadGateway" }
995
1038
  }
996
- } },
997
- "/v1/sandbox/{id}/unmount": { post: {
998
- operationId: "unmountBucket",
999
- summary: "Unmount a previously mounted bucket",
1000
- description: "Unmounts a bucket filesystem that was previously mounted via `/v1/sandbox/{id}/mount`.",
1001
- "x-codeSamples": [{
1002
- lang: "curl",
1003
- label: "cURL",
1004
- 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\"}'"
1005
- }],
1006
- parameters: [{
1007
- name: "id",
1008
- in: "path",
1009
- required: true,
1010
- schema: { type: "string" },
1011
- description: "Sandbox instance name."
1012
- }],
1013
- requestBody: {
1014
- required: true,
1015
- content: { "application/json": { schema: { $ref: "#/components/schemas/UnmountBucketRequest" } } }
1016
- },
1039
+ },
1040
+ get: {
1041
+ operationId: "listProcesses",
1042
+ summary: "List sandbox processes",
1043
+ parameters: [sandboxIdParameter$1],
1017
1044
  responses: {
1018
1045
  "200": {
1019
- description: "Bucket unmounted successfully.",
1020
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
1046
+ description: "Process statuses.",
1047
+ content: { "application/json": { schema: {
1048
+ type: "array",
1049
+ items: { $ref: "#/components/schemas/ProcessStatus" }
1050
+ } } }
1021
1051
  },
1022
- "400": { $ref: "#/components/responses/InvalidRequest" },
1023
- "401": { $ref: "#/components/responses/Unauthorized" },
1024
- "502": {
1025
- description: "SDK unmount call failed (no active mount or unmount error).",
1026
- content: { "application/json": {
1027
- schema: { $ref: "#/components/schemas/ErrorResponse" },
1028
- example: {
1029
- error: "unmount failed: No active mount found",
1030
- code: "unmount_error"
1031
- }
1032
- } }
1033
- }
1052
+ "401": { $ref: "#/components/responses/Unauthorized" }
1034
1053
  }
1035
- } },
1036
- "/v1/sandbox/{id}": { delete: {
1037
- operationId: "deleteSandbox",
1038
- summary: "Destroy a sandbox instance (best-effort)",
1039
- description: "Calls destroy() on the sandbox Durable Object to release container resources. Best-effort: unknown sandbox IDs return 204 without allocating a container.",
1040
- "x-codeSamples": [{
1041
- lang: "curl",
1042
- label: "cURL",
1043
- source: "curl -X DELETE https://$HOST/v1/sandbox/my-sandbox \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1044
- }],
1045
- parameters: [{
1046
- name: "id",
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",
1047
1127
  in: "path",
1048
1128
  required: true,
1049
- schema: { type: "string" },
1050
- description: "Sandbox instance name."
1129
+ schema: {
1130
+ type: "integer",
1131
+ minimum: 1024,
1132
+ maximum: 65535
1133
+ }
1051
1134
  }],
1052
1135
  responses: {
1053
- "204": { description: "Sandbox destroyed (best-effort). Container resources are released." },
1054
- "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" }
1055
1140
  }
1056
- } },
1057
- "/v1/sandbox/{id}/session": { post: {
1058
- operationId: "createSession",
1059
- summary: "Create an execution session",
1060
- description: "Sessions isolate working directory and environment variables across commands. The returned session ID is used with the `Session-Id` header on exec and file endpoints.",
1061
- parameters: [{
1062
- name: "id",
1141
+ },
1142
+ delete: {
1143
+ operationId: "deleteTunnel",
1144
+ summary: "Delete the tunnel for a sandbox port",
1145
+ parameters: [sandboxIdParameter$1, {
1146
+ name: "port",
1063
1147
  in: "path",
1064
1148
  required: true,
1065
- schema: { type: "string" },
1066
- description: "Sandbox instance name."
1149
+ schema: {
1150
+ type: "integer",
1151
+ minimum: 1024,
1152
+ maximum: 65535
1153
+ }
1067
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",
1068
1243
  requestBody: {
1069
- required: false,
1244
+ required: true,
1070
1245
  content: { "application/json": { schema: {
1071
1246
  type: "object",
1247
+ required: ["argv"],
1072
1248
  properties: {
1073
- id: {
1074
- type: "string",
1075
- description: "Custom session ID. Auto-generated if omitted."
1076
- },
1077
- cwd: {
1078
- type: "string",
1079
- description: "Initial working directory for the session."
1249
+ argv: {
1250
+ type: "array",
1251
+ items: { type: "string" },
1252
+ minItems: 1
1080
1253
  },
1254
+ cwd: { type: "string" },
1081
1255
  env: {
1082
1256
  type: "object",
1083
- additionalProperties: { type: "string" },
1084
- description: "Environment variables scoped to this session."
1085
- }
1257
+ additionalProperties: { type: "string" }
1258
+ },
1259
+ cols: { type: "integer" },
1260
+ rows: { type: "integer" }
1086
1261
  }
1087
1262
  } } }
1088
1263
  },
1264
+ parameters: [sandboxIdParameter],
1089
1265
  responses: {
1090
1266
  "200": {
1091
- description: "Session created.",
1092
- content: { "application/json": { schema: {
1093
- type: "object",
1094
- required: ["id"],
1095
- properties: { id: {
1096
- type: "string",
1097
- description: "Session ID to pass via `Session-Id` header."
1098
- } }
1099
- } } }
1267
+ description: "Terminal created with a generated ID.",
1268
+ content: { "application/json": { schema: terminalSnapshotSchema } }
1100
1269
  },
1101
- "401": { $ref: "#/components/responses/Unauthorized" },
1102
- "502": {
1103
- description: "Session creation failed.",
1104
- content: { "application/json": {
1105
- schema: { $ref: "#/components/schemas/ErrorResponse" },
1106
- example: {
1107
- error: "session creation failed",
1108
- code: "session_error"
1109
- }
1110
- } }
1111
- }
1270
+ ...terminalErrorResponses
1112
1271
  }
1113
- } },
1114
- "/v1/sandbox/{id}/session/{sid}": { delete: {
1115
- operationId: "deleteSession",
1116
- summary: "Delete an execution session",
1117
- description: "Removes an explicit execution session.",
1118
- parameters: [{
1119
- name: "id",
1120
- in: "path",
1121
- required: true,
1122
- schema: { type: "string" },
1123
- description: "Sandbox instance name."
1124
- }, {
1125
- name: "sid",
1126
- in: "path",
1127
- required: true,
1128
- schema: { type: "string" },
1129
- description: "Session ID to delete."
1130
- }],
1272
+ },
1273
+ get: {
1274
+ operationId: "listTerminals",
1275
+ summary: "List terminals",
1276
+ parameters: [sandboxIdParameter],
1131
1277
  responses: {
1132
1278
  "200": {
1133
- description: "Session deleted.",
1279
+ description: "Terminal snapshots.",
1134
1280
  content: { "application/json": { schema: {
1135
- type: "object",
1136
- required: ["success", "sessionId"],
1137
- properties: {
1138
- success: {
1139
- type: "boolean",
1140
- description: "`true` if the session was deleted."
1141
- },
1142
- sessionId: {
1143
- type: "string",
1144
- description: "ID of the deleted session."
1145
- }
1146
- }
1281
+ type: "array",
1282
+ items: terminalSnapshotSchema
1147
1283
  } } }
1148
1284
  },
1149
- "401": { $ref: "#/components/responses/Unauthorized" },
1150
- "502": {
1151
- description: "Session deletion failed.",
1152
- content: { "application/json": {
1153
- schema: { $ref: "#/components/schemas/ErrorResponse" },
1154
- example: {
1155
- error: "session deletion failed",
1156
- code: "session_error"
1157
- }
1158
- } }
1159
- }
1160
- }
1161
- } },
1162
- "/health": { get: {
1163
- operationId: "healthCheck",
1164
- summary: "Worker health check",
1165
- description: "Simple liveness probe. Not protected by authentication.",
1166
- "x-codeSamples": [{
1167
- lang: "curl",
1168
- label: "cURL",
1169
- source: "curl https://$HOST/health"
1170
- }],
1171
- security: [],
1172
- responses: { "200": {
1173
- description: "Worker is up.",
1174
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
1175
- } }
1176
- } },
1177
- "/v1/openapi.json": { get: {
1178
- operationId: "getOpenApiSchema",
1179
- summary: "OpenAPI schema",
1180
- description: "Returns this OpenAPI 3.1 schema document.",
1181
- "x-codeSamples": [{
1182
- lang: "curl",
1183
- label: "cURL",
1184
- source: "curl https://$HOST/v1/openapi.json \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1185
- }],
1186
- responses: {
1187
- "200": {
1188
- description: "OpenAPI schema document.",
1189
- content: { "application/json": { schema: { type: "object" } } }
1190
- },
1191
- "401": { $ref: "#/components/responses/Unauthorized" }
1192
- }
1193
- } },
1194
- "/v1/pool/stats": { get: {
1195
- operationId: "getPoolStats",
1196
- summary: "Pool statistics",
1197
- description: "Returns current warm pool statistics including warm/assigned counts and configuration.",
1198
- "x-codeSamples": [{
1199
- lang: "curl",
1200
- label: "cURL",
1201
- source: "curl https://$HOST/v1/pool/stats \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1202
- }],
1203
- responses: {
1204
- "200": {
1205
- description: "Pool statistics.",
1206
- content: { "application/json": { schema: { $ref: "#/components/schemas/PoolStats" } } }
1207
- },
1208
- "401": { $ref: "#/components/responses/Unauthorized" }
1285
+ ...terminalErrorResponses
1209
1286
  }
1210
- } },
1211
- "/v1/pool/shutdown-prewarmed": { post: {
1212
- operationId: "shutdownPrewarmed",
1213
- summary: "Shutdown pre-warmed containers",
1214
- description: "Stops all idle (unassigned) warm containers. Does not affect containers assigned to sandbox sessions.",
1215
- "x-codeSamples": [{
1216
- lang: "curl",
1217
- label: "cURL",
1218
- source: "curl -X POST https://$HOST/v1/pool/shutdown-prewarmed \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1219
- }],
1220
- responses: {
1221
- "200": {
1222
- description: "All pre-warmed containers stopped.",
1223
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
1224
- },
1225
- "401": { $ref: "#/components/responses/Unauthorized" }
1226
- }
1227
- } },
1228
- "/v1/pool/prime": { post: {
1229
- operationId: "primePool",
1230
- summary: "Prime the warm pool",
1231
- description: "Pushes the current pool configuration and starts the alarm loop. Called automatically by the cron trigger; can also be called manually after deploy.",
1232
- "x-codeSamples": [{
1233
- lang: "curl",
1234
- label: "cURL",
1235
- source: "curl -X POST https://$HOST/v1/pool/prime \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
1236
- }],
1237
- responses: {
1238
- "200": {
1239
- description: "Pool primed successfully.",
1240
- content: { "application/json": { schema: { $ref: "#/components/schemas/OkResponse" } } }
1241
- },
1242
- "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" }
1243
1326
  }
1244
- } }
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
1245
1365
  }
1246
1366
  };
1247
1367
 
@@ -1774,10 +1894,20 @@ function esc(s) {
1774
1894
  }
1775
1895
 
1776
1896
  //#endregion
1777
- //#region src/bridge/routes.ts
1778
- /** Typed wrapper around the SDK's getSandbox() that returns a BridgeSandbox. */
1779
- function getSandbox$1(ns, containerUUID) {
1780
- 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);
1781
1911
  }
1782
1912
  function hasEndpoint(options) {
1783
1913
  return "endpoint" in options && typeof options.endpoint === "string";
@@ -1821,177 +1951,37 @@ function validateMountOptions(options, binding) {
1821
1951
  if ("credentials" in options && options.credentials !== void 0 && (typeof options.credentials !== "object" || options.credentials === null || typeof options.credentials.accessKeyId !== "string" || typeof options.credentials.secretAccessKey !== "string")) return errorJson("options.credentials must include string accessKeyId and secretAccessKey", "invalid_request", 400);
1822
1952
  return null;
1823
1953
  }
1824
- function resolveMountBucketName(body) {
1825
- if (hasEndpoint(body.options)) {
1826
- if (body.bucket && typeof body.bucket === "string") return body.bucket;
1827
- return errorJson("bucket must be a non-empty string for remote mounts", "invalid_request", 400);
1828
- }
1829
- if (body.binding !== void 0) return body.binding;
1830
- return errorJson("binding must be a non-empty string for R2 binding mounts", "invalid_request", 400);
1831
- }
1832
- function toSDKMountOptions(options) {
1833
- if (hasEndpoint(options)) {
1834
- const remoteOptions = { endpoint: options.endpoint };
1835
- if (options.readOnly !== void 0) remoteOptions.readOnly = options.readOnly;
1836
- if (options.prefix !== void 0) remoteOptions.prefix = options.prefix;
1837
- if (hasCredentials(options)) remoteOptions.credentials = {
1838
- accessKeyId: options.credentials.accessKeyId,
1839
- secretAccessKey: options.credentials.secretAccessKey
1840
- };
1841
- if (options.s3fsOptions !== void 0) remoteOptions.s3fsOptions = options.s3fsOptions;
1842
- if ("credentialProxy" in options && typeof options.credentialProxy === "boolean") remoteOptions.credentialProxy = options.credentialProxy;
1843
- return remoteOptions;
1844
- }
1845
- const r2BindingOptions = {};
1846
- if (options.readOnly !== void 0) r2BindingOptions.readOnly = options.readOnly;
1847
- if (options.prefix !== void 0) r2BindingOptions.prefix = options.prefix;
1848
- if (options.s3fsOptions !== void 0) r2BindingOptions.s3fsOptions = options.s3fsOptions;
1849
- return r2BindingOptions;
1850
- }
1851
- function createBridgeApp(config) {
1852
- const app = new Hono();
1853
- const { sandboxBinding, warmPoolBinding, apiPrefix } = config;
1854
- function getSandboxNs(env$1) {
1855
- return env$1[sandboxBinding];
1856
- }
1857
- function getWarmPoolNs(env$1) {
1858
- return env$1[warmPoolBinding];
1859
- }
1860
- app.use(`${apiPrefix}/sandbox/*`, async (c, next) => {
1861
- const sandboxId = new URL(c.req.url).pathname.split("/")[apiPrefix.split("/").filter(Boolean).length + 2];
1862
- if (sandboxId && !/^[a-z2-7]{1,128}$/.test(sandboxId)) return errorJson("Invalid sandbox ID format", "invalid_request", 400);
1863
- const token = c.env.SANDBOX_API_KEY;
1864
- if (token) {
1865
- const authHeader = c.req.header("Authorization") ?? "";
1866
- if ((authHeader.toLowerCase().startsWith("bearer ") ? authHeader.slice(7) : "") !== token) return errorJson("Unauthorized", "unauthorized", 401);
1867
- }
1868
- return next();
1869
- });
1870
- app.post(`${apiPrefix}/sandbox`, async (c) => {
1871
- const token = c.env.SANDBOX_API_KEY;
1872
- if (token) {
1873
- const authHeader = c.req.header("Authorization") ?? "";
1874
- if ((authHeader.toLowerCase().startsWith("bearer ") ? authHeader.slice(7) : "") !== token) return errorJson("Unauthorized", "unauthorized", 401);
1875
- }
1876
- const bytes = new Uint8Array(16);
1877
- crypto.getRandomValues(bytes);
1878
- const id = base32Encode(bytes);
1879
- return c.json({ id });
1880
- });
1881
- app.use(`${apiPrefix}/sandbox/:id/*`, async (c, next) => {
1882
- const sandboxId = c.req.param("id");
1883
- const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
1884
- const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
1885
- const poolNs = getWarmPoolNs(c.env);
1886
- const poolId = poolNs.idFromName("global-pool");
1887
- const poolStub = poolNs.get(poolId);
1888
- try {
1889
- await poolStub.configure({
1890
- warmTarget,
1891
- refreshInterval
1892
- });
1893
- const containerUUID = await poolStub.getContainer(sandboxId);
1894
- c.set("containerUUID", containerUUID);
1895
- } catch (err) {
1896
- const msg = err instanceof Error ? err.message : String(err);
1897
- if (msg.includes("instance limit reached")) return errorJson(msg, "capacity_exceeded", 503);
1898
- return errorJson(`pool error: ${msg}`, "pool_error", 502);
1899
- }
1900
- return next();
1901
- });
1902
- app.use(`${apiPrefix}/sandbox/:id`, async (c, next) => {
1903
- if (c.req.method !== "DELETE") return next();
1904
- const sandboxId = c.req.param("id");
1905
- const poolNs = getWarmPoolNs(c.env);
1906
- const poolId = poolNs.idFromName("global-pool");
1907
- const poolStub = poolNs.get(poolId);
1908
- try {
1909
- const containerUUID = await poolStub.lookupContainer(sandboxId);
1910
- if (!containerUUID) return new Response(null, { status: 204 });
1911
- c.set("containerUUID", containerUUID);
1912
- } catch (err) {
1913
- return errorJson(`pool error: ${err instanceof Error ? err.message : String(err)}`, "pool_error", 502);
1914
- }
1915
- return next();
1916
- });
1917
- app.post(`${apiPrefix}/sandbox/:id/exec`, async (c) => {
1918
- let body;
1919
- try {
1920
- body = await c.req.json();
1921
- } catch {
1922
- return errorJson("Invalid JSON body", "invalid_request", 400);
1923
- }
1924
- if (!Array.isArray(body.argv) || body.argv.length === 0) return errorJson("argv must be a non-empty array", "invalid_request", 400);
1925
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
1926
- const rawSessionId = c.req.header("Session-Id");
1927
- let executor = sandbox;
1928
- if (rawSessionId) {
1929
- const sessionId = validateSessionId(rawSessionId);
1930
- if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
1931
- executor = await sandbox.getSession(sessionId);
1932
- }
1933
- const command = body.argv.map(shellQuote).join(" ");
1934
- const opts = {};
1935
- if (typeof body.timeout_ms === "number") opts.timeout = body.timeout_ms;
1936
- if (typeof body.cwd === "string") {
1937
- const resolvedCwd = resolveWorkspacePath(body.cwd);
1938
- if (!resolvedCwd) return errorJson("cwd must resolve to a location within /workspace", "invalid_request", 403);
1939
- opts.cwd = resolvedCwd;
1940
- }
1941
- const { readable, writable } = new TransformStream();
1942
- const writer = writable.getWriter();
1943
- const encoder = new TextEncoder();
1944
- let closed = false;
1945
- let lastWrite = Promise.resolve();
1946
- /** Write a single SSE event. Chains on the previous write to respect backpressure. */
1947
- function writeSSE(event, data) {
1948
- if (closed) return;
1949
- const payload = data.split("\n").map((line) => `data: ${line}`).join("\n");
1950
- lastWrite = lastWrite.then(() => writer.write(encoder.encode(`event: ${event}\n${payload}\n\n`)));
1951
- }
1952
- function closeStream() {
1953
- if (closed) return;
1954
- closed = true;
1955
- lastWrite.then(() => writer.close()).catch(() => {});
1956
- }
1957
- (async () => {
1958
- try {
1959
- const proc = await executor.exec(command, opts);
1960
- const decoder = new TextDecoder();
1961
- const pump = async (stream, name) => {
1962
- if (!stream) return;
1963
- const reader = stream.getReader();
1964
- try {
1965
- for (;;) {
1966
- const { value, done } = await reader.read();
1967
- if (done) break;
1968
- if (value && value.byteLength > 0) writeSSE(name, toBase64(decoder.decode(value)));
1969
- }
1970
- } finally {
1971
- reader.releaseLock();
1972
- }
1973
- };
1974
- await Promise.all([pump(proc.stdout, "stdout"), pump(proc.stderr, "stderr")]);
1975
- const exitCode = await proc.exitCode;
1976
- writeSSE("exit", JSON.stringify({ exit_code: exitCode }));
1977
- } catch (err) {
1978
- const msg = err instanceof Error ? err.message : String(err);
1979
- writeSSE("error", JSON.stringify({
1980
- error: `exec failed: ${msg}`,
1981
- code: "exec_transport_error"
1982
- }));
1983
- } finally {
1984
- closeStream();
1985
- }
1986
- })();
1987
- return new Response(readable, {
1988
- status: 200,
1989
- headers: {
1990
- "Content-Type": "text/event-stream",
1991
- "Cache-Control": "no-cache"
1992
- }
1993
- });
1994
- });
1954
+ function resolveMountBucketName(body) {
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) {
1995
1985
  app.get(`${apiPrefix}/sandbox/:id/file/*`, async (c) => {
1996
1986
  const sandboxId = c.req.param("id");
1997
1987
  const fullPath = c.req.path;
@@ -2000,16 +1990,9 @@ function createBridgeApp(config) {
2000
1990
  if (!relativePath) return errorJson("file path must not be empty", "invalid_request", 400);
2001
1991
  const resolvedPath = resolveWorkspacePath(`/${relativePath}`);
2002
1992
  if (!resolvedPath) return errorJson("path must resolve to a location within /workspace", "invalid_request", 403);
2003
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2004
- const rawSessionId = c.req.header("Session-Id");
2005
- let executor = sandbox;
2006
- if (rawSessionId) {
2007
- const sessionId = validateSessionId(rawSessionId);
2008
- if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
2009
- executor = await sandbox.getSession(sessionId);
2010
- }
1993
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2011
1994
  try {
2012
- const stream = await executor.readFileStream(resolvedPath);
1995
+ const stream = await sandbox.readFileStream(resolvedPath);
2013
1996
  return new Response(sseToByteStream(stream), {
2014
1997
  status: 200,
2015
1998
  headers: { "Content-Type": "application/octet-stream" }
@@ -2027,14 +2010,7 @@ function createBridgeApp(config) {
2027
2010
  if (!relativePath) return errorJson("file path must not be empty", "invalid_request", 400);
2028
2011
  const resolvedPath = resolveWorkspacePath(`/${relativePath}`);
2029
2012
  if (!resolvedPath) return errorJson("path must resolve to a location within /workspace", "invalid_request", 403);
2030
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2031
- const rawSessionId = c.req.header("Session-Id");
2032
- let executor = sandbox;
2033
- if (rawSessionId) {
2034
- const sessionId = validateSessionId(rawSessionId);
2035
- if (!sessionId) return errorJson("Invalid session ID format", "invalid_request", 400);
2036
- executor = await sandbox.getSession(sessionId);
2037
- }
2013
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2038
2014
  try {
2039
2015
  const buffer = await c.req.arrayBuffer();
2040
2016
  const MAX_WRITE_BYTES = 32 * 1024 * 1024;
@@ -2043,89 +2019,43 @@ function createBridgeApp(config) {
2043
2019
  let b64 = "";
2044
2020
  const CHUNK = 6144;
2045
2021
  for (let i = 0; i < bytes.length; i += CHUNK) b64 += btoa(String.fromCharCode(...bytes.subarray(i, i + CHUNK)));
2046
- await executor.writeFile(resolvedPath, b64, { encoding: "base64" });
2022
+ await sandbox.writeFile(resolvedPath, b64, { encoding: "base64" });
2047
2023
  return c.json({ ok: true });
2048
2024
  } catch (err) {
2049
2025
  return errorJson(`write failed: ${err instanceof Error ? err.message : String(err)}`, "workspace_archive_write_error", 502);
2050
2026
  }
2051
2027
  });
2052
- app.post(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
2053
- const port = Number(c.req.param("port"));
2054
- if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
2055
- const options = parseTunnelOptions(await c.req.text());
2056
- if (options instanceof Response) return options;
2057
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2058
- try {
2059
- const tunnel = await sandbox.tunnels.get(port, options);
2060
- return c.json(tunnel);
2061
- } catch (err) {
2062
- return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
2063
- }
2064
- });
2065
- app.delete(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
2066
- const port = Number(c.req.param("port"));
2067
- if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
2068
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2069
- try {
2070
- await sandbox.tunnels.destroy(port);
2071
- return c.body(null, 204);
2072
- } catch (err) {
2073
- return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
2074
- }
2075
- });
2076
- app.get(`${apiPrefix}/sandbox/:id/running`, async (c) => {
2077
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2078
- try {
2079
- await sandbox.exec("true");
2080
- return c.json({ running: true });
2081
- } catch {
2082
- return c.json({ running: false });
2083
- }
2084
- });
2085
- app.get(`${apiPrefix}/sandbox/:id/pty`, async (c) => {
2086
- const upgrade = c.req.header("Upgrade");
2087
- if (!upgrade || upgrade.toLowerCase() !== "websocket") return errorJson("WebSocket upgrade required", "invalid_request", 400);
2088
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2089
- const colsParam = c.req.query("cols");
2090
- const rowsParam = c.req.query("rows");
2091
- const shell = c.req.query("shell");
2092
- const cwd = c.req.query("cwd");
2093
- const terminalId = c.req.header("Terminal-Id") ?? c.req.query("terminalId") ?? c.req.query("terminal");
2094
- const cols = colsParam ? Number(colsParam) : 80;
2095
- const rows = rowsParam ? Number(rowsParam) : 24;
2096
- if (Number.isNaN(cols) || Number.isNaN(rows)) return errorJson("cols and rows must be valid numbers", "invalid_request", 400);
2097
- const terminalOptions = {};
2098
- const connectOptions = {
2099
- cols,
2100
- rows
2101
- };
2102
- if (shell) terminalOptions.shell = shell;
2103
- if (cwd) terminalOptions.cwd = cwd;
2104
- if (!terminalId) return errorJson("terminalId query parameter or Terminal-Id header required", "invalid_request", 400);
2105
- const validatedId = validateSessionId(terminalId);
2106
- if (!validatedId) return errorJson("Invalid terminal ID format", "invalid_request", 400);
2107
- terminalOptions.id = validatedId;
2108
- try {
2109
- return await sandbox.terminal(terminalOptions).connect(c.req.raw, connectOptions);
2110
- } catch (err) {
2111
- return errorJson(`terminal failed: ${err instanceof Error ? err.message : String(err)}`, "exec_transport_error", 502);
2112
- }
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) });
2113
2037
  });
2114
2038
  app.post(`${apiPrefix}/sandbox/:id/persist`, async (c) => {
2115
2039
  const root = "/workspace";
2116
2040
  const excludesParam = c.req.query("excludes") ?? "";
2117
2041
  const excludes = excludesParam ? excludesParam.split(",").filter((s) => s.length > 0) : [];
2118
2042
  for (const ex of excludes) if (ex.includes("..")) return errorJson("exclude paths must not contain \"..\"", "invalid_request", 400);
2119
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2120
- const tmpPath = `/tmp/sandbox-persist-${Date.now()}.tar`;
2121
- const excludeArgs = excludes.map((rel) => `--exclude ${shellQuote(`./${rel.replace(/^\.\//, "")}`)}`).join(" ");
2122
- 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"));
2123
2044
  try {
2124
- const result = await sandbox.exec(tarCmd).output({ encoding: "utf8" });
2125
- if (result.exitCode !== 0) return errorJson(`tar failed (exit ${result.exitCode}): ${result.stderr}`, "workspace_archive_read_error", 502);
2126
- const stream = await sandbox.readFileStream(tmpPath);
2127
- sandbox.exec(`rm -f ${shellQuote(tmpPath)}`).catch(() => {});
2128
- 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, {
2129
2059
  status: 200,
2130
2060
  headers: { "Content-Type": "application/octet-stream" }
2131
2061
  });
@@ -2135,7 +2065,7 @@ function createBridgeApp(config) {
2135
2065
  });
2136
2066
  app.post(`${apiPrefix}/sandbox/:id/hydrate`, async (c) => {
2137
2067
  const root = "/workspace";
2138
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2068
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2139
2069
  let tarBytes;
2140
2070
  try {
2141
2071
  const buffer = await c.req.arrayBuffer();
@@ -2146,18 +2076,21 @@ function createBridgeApp(config) {
2146
2076
  if (tarBytes.byteLength === 0) return errorJson("Empty tar payload", "invalid_request", 400);
2147
2077
  const MAX_HYDRATE_BYTES = 32 * 1024 * 1024;
2148
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`;
2149
2080
  try {
2150
- await sandbox.exec(`mkdir -p ${shellQuote(root)}`);
2151
- const tmpPath = `/tmp/sandbox-hydrate-${Date.now()}.tar`;
2152
2081
  let b64 = "";
2153
2082
  const CHUNK = 6144;
2154
2083
  for (let i = 0; i < tarBytes.length; i += CHUNK) b64 += btoa(String.fromCharCode(...tarBytes.subarray(i, i + CHUNK)));
2155
2084
  await sandbox.writeFile(tmpPath, b64, { encoding: "base64" });
2156
- const extractResult = await sandbox.exec(`tar xf ${shellQuote(tmpPath)} -C ${shellQuote(root)} && rm -f ${shellQuote(tmpPath)}`).output({ encoding: "utf8" });
2157
- 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
+ });
2158
2089
  return c.json({ ok: true });
2159
2090
  } catch (err) {
2160
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);
2161
2094
  }
2162
2095
  });
2163
2096
  app.post(`${apiPrefix}/sandbox/:id/mount`, async (c) => {
@@ -2175,7 +2108,7 @@ function createBridgeApp(config) {
2175
2108
  if (optionsError) return optionsError;
2176
2109
  const bucketName = resolveMountBucketName(body);
2177
2110
  if (bucketName instanceof Response) return bucketName;
2178
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2111
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2179
2112
  const sdkOptions = toSDKMountOptions(body.options);
2180
2113
  try {
2181
2114
  await sandbox.mountBucket(bucketName, body.mountPath, sdkOptions);
@@ -2195,55 +2128,29 @@ function createBridgeApp(config) {
2195
2128
  if (!body.mountPath.startsWith("/")) return errorJson("mountPath must be an absolute path (start with /)", "invalid_request", 400);
2196
2129
  const normalizedPath = new URL(body.mountPath, "file:///").pathname;
2197
2130
  if (normalizedPath === "/") return errorJson("mountPath must not resolve to / (filesystem root)", "invalid_request", 400);
2198
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2131
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), c.get("containerUUID"));
2199
2132
  try {
2200
2133
  await sandbox.unmountBucket(normalizedPath);
2201
- const quoted = shellQuote(normalizedPath);
2202
2134
  try {
2203
- await sandbox.exec(`mountpoint -q ${quoted} || rmdir ${quoted}`);
2135
+ await sandbox.cleanupMountDirectory(normalizedPath);
2204
2136
  } catch {}
2205
2137
  return c.json({ ok: true });
2206
2138
  } catch (err) {
2207
2139
  return errorJson(`unmount failed: ${err instanceof Error ? err.message : String(err)}`, "unmount_error", 502);
2208
2140
  }
2209
2141
  });
2210
- app.post(`${apiPrefix}/sandbox/:id/session`, async (c) => {
2211
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2212
- let body = {};
2213
- try {
2214
- body = await c.req.json();
2215
- } catch {}
2216
- try {
2217
- const session = await sandbox.createSession(body);
2218
- return c.json({ id: session.id });
2219
- } catch (err) {
2220
- return errorJson(`session create failed: ${err instanceof Error ? err.message : String(err)}`, "session_error", 502);
2221
- }
2222
- });
2223
- app.delete(`${apiPrefix}/sandbox/:id/session/:sid`, async (c) => {
2224
- const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
2225
- const sid = c.req.param("sid");
2226
- try {
2227
- const result = await sandbox.deleteSession(sid);
2228
- return c.json(result);
2229
- } catch (err) {
2230
- return errorJson(`session delete failed: ${err instanceof Error ? err.message : String(err)}`, "session_error", 502);
2231
- }
2232
- });
2233
2142
  app.delete(`${apiPrefix}/sandbox/:id`, async (c) => {
2234
2143
  const containerUUID = c.get("containerUUID");
2235
- const sandbox = getSandbox$1(getSandboxNs(c.env), containerUUID);
2144
+ const sandbox = getSandbox(getSandboxNs(c.env, sandboxBinding), containerUUID);
2236
2145
  try {
2237
2146
  await sandbox.destroy();
2238
2147
  } catch {}
2239
2148
  try {
2240
- const poolNs = getWarmPoolNs(c.env);
2241
- const poolId = poolNs.idFromName("global-pool");
2242
- await poolNs.get(poolId).reportStopped(containerUUID);
2149
+ await getWarmPoolStub(c.env, warmPoolBinding).reportStopped(containerUUID);
2243
2150
  } catch {}
2244
2151
  return new Response(null, { status: 204 });
2245
2152
  });
2246
- app.get(config.healthPath, (c) => {
2153
+ app.get(healthPath, (c) => {
2247
2154
  const errors = [];
2248
2155
  if (!c.env[sandboxBinding]) errors.push(`Missing required Durable Object binding "${sandboxBinding}". Ensure your wrangler.jsonc has a binding named "${sandboxBinding}".`);
2249
2156
  if (!c.env[warmPoolBinding]) errors.push(`Missing required Durable Object binding "${warmPoolBinding}". Ensure your wrangler.jsonc has a binding named "${warmPoolBinding}".`);
@@ -2264,9 +2171,7 @@ function createBridgeApp(config) {
2264
2171
  app.get(`${apiPrefix}/pool/stats`, async (c) => {
2265
2172
  const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
2266
2173
  const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
2267
- const poolNs = getWarmPoolNs(c.env);
2268
- const poolId = poolNs.idFromName("global-pool");
2269
- const poolStub = poolNs.get(poolId);
2174
+ const poolStub = getWarmPoolStub(c.env, warmPoolBinding);
2270
2175
  try {
2271
2176
  await poolStub.configure({
2272
2177
  warmTarget,
@@ -2279,9 +2184,7 @@ function createBridgeApp(config) {
2279
2184
  app.post(`${apiPrefix}/pool/shutdown-prewarmed`, async (c) => {
2280
2185
  const warmTarget = Number.parseInt(c.env.WARM_POOL_TARGET || "0", 10) || 0;
2281
2186
  const refreshInterval = Number.parseInt(c.env.WARM_POOL_REFRESH_INTERVAL || "10000", 10) || 1e4;
2282
- const poolNs = getWarmPoolNs(c.env);
2283
- const poolId = poolNs.idFromName("global-pool");
2284
- const poolStub = poolNs.get(poolId);
2187
+ const poolStub = getWarmPoolStub(c.env, warmPoolBinding);
2285
2188
  try {
2286
2189
  await poolStub.configure({
2287
2190
  warmTarget,
@@ -2295,6 +2198,399 @@ function createBridgeApp(config) {
2295
2198
  await primePool(c.env, warmPoolBinding);
2296
2199
  return c.json({ ok: true });
2297
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) {
2298
2594
  const openapiAuth = async (c, next) => {
2299
2595
  const token = c.env.SANDBOX_API_KEY;
2300
2596
  if (token) {
@@ -2309,7 +2605,6 @@ function createBridgeApp(config) {
2309
2605
  app.get(`${apiPrefix}/openapi.json`, openapiAuth, (c) => c.json(OPENAPI_SCHEMA));
2310
2606
  app.get(`${apiPrefix}/openapi.html`, openapiAuth, openapiHtmlHandler);
2311
2607
  app.get(`${apiPrefix}/openapi`, openapiAuth, openapiHtmlHandler);
2312
- return app;
2313
2608
  }
2314
2609
 
2315
2610
  //#endregion
@@ -2317,12 +2612,9 @@ function createBridgeApp(config) {
2317
2612
  /**
2318
2613
  * WarmPool — Durable Object that maintains a pool of pre-started sandbox containers.
2319
2614
  *
2320
- * Adapted from https://github.com/mikenomitch/cf-container-warm-pool
2321
- * Inlined and tailored for the @cloudflare/sandbox SDK.
2322
- *
2323
- * The pool keeps N idle containers standing by so new sandbox sessions boot
2324
- * instantly. Once a container is assigned to a sandbox ID it is consumed and
2325
- * 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.
2326
2618
  *
2327
2619
  * Configuration is pushed in via `configure()` on every request (idempotent)
2328
2620
  * so changes to wrangler vars take effect without manual intervention.
@@ -2424,11 +2716,11 @@ var WarmPool = class extends DurableObject {
2424
2716
  async shutdownPrewarmed() {
2425
2717
  await this.init();
2426
2718
  for (const containerUUID of [...this.warmContainers]) try {
2427
- await this.getSandboxStub(containerUUID).stop();
2719
+ await this.getSandboxStub(containerUUID).destroy();
2428
2720
  this.warmContainers.delete(containerUUID);
2429
2721
  } catch (error) {
2430
2722
  console.error({
2431
- message: "Failed to stop container",
2723
+ message: "Failed to destroy container",
2432
2724
  component: "warm-pool",
2433
2725
  containerUUID,
2434
2726
  error
@@ -2608,22 +2900,22 @@ var WarmPool = class extends DurableObject {
2608
2900
  console.info({
2609
2901
  message: "Scaling down pool",
2610
2902
  component: "warm-pool",
2611
- stopping: excess
2903
+ destroying: excess
2612
2904
  });
2613
- const toStop = [...this.warmContainers].slice(0, excess);
2614
- const stopped = [];
2615
- for (const uuid of toStop) try {
2616
- await this.getSandboxStub(uuid).stop();
2617
- 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);
2618
2910
  } catch (error) {
2619
2911
  console.error({
2620
- message: "Failed to stop container",
2912
+ message: "Failed to destroy container",
2621
2913
  component: "warm-pool",
2622
2914
  containerUUID: uuid,
2623
2915
  error
2624
2916
  });
2625
2917
  }
2626
- for (const uuid of stopped) this.warmContainers.delete(uuid);
2918
+ for (const uuid of destroyed) this.warmContainers.delete(uuid);
2627
2919
  await this.persist();
2628
2920
  }
2629
2921
  }