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