@cloudflare/sandbox 0.0.0-0608f1e → 0.0.0-0dad837
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/CHANGELOG.md +64 -0
- package/Dockerfile +44 -18
- package/README.md +784 -0
- package/container_src/bun.lock +122 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/handler/exec.ts +17 -14
- package/container_src/handler/file.ts +222 -2
- package/container_src/handler/process.ts +1 -1
- package/container_src/index.ts +312 -10
- package/container_src/jupyter-server.ts +579 -0
- package/container_src/jupyter-service.ts +461 -0
- package/container_src/jupyter_config.py +48 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/startup.sh +84 -0
- package/container_src/types.ts +17 -3
- package/package.json +5 -4
- package/src/client.ts +108 -122
- package/src/errors.ts +218 -0
- package/src/index.ts +51 -15
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +349 -0
- package/src/sandbox.ts +292 -149
- package/src/types.ts +125 -0
- package/tsconfig.json +1 -1
package/src/sandbox.ts
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
import { Container, getContainer } from "@cloudflare/containers";
|
|
2
|
-
import {
|
|
2
|
+
import { CodeInterpreter } from "./interpreter";
|
|
3
|
+
import type {
|
|
4
|
+
CodeContext,
|
|
5
|
+
CreateContextOptions,
|
|
6
|
+
ExecutionResult,
|
|
7
|
+
RunCodeOptions,
|
|
8
|
+
} from "./interpreter-types";
|
|
9
|
+
import { JupyterClient } from "./jupyter-client";
|
|
3
10
|
import { isLocalhostPattern } from "./request-handler";
|
|
4
11
|
import {
|
|
5
12
|
logSecurityEvent,
|
|
6
13
|
SecurityError,
|
|
7
14
|
sanitizeSandboxId,
|
|
8
|
-
validatePort
|
|
15
|
+
validatePort,
|
|
9
16
|
} from "./security";
|
|
17
|
+
import { parseSSEStream } from "./sse-parser";
|
|
10
18
|
import type {
|
|
19
|
+
ExecEvent,
|
|
11
20
|
ExecOptions,
|
|
12
21
|
ExecResult,
|
|
22
|
+
ExecuteResponse,
|
|
13
23
|
ISandbox,
|
|
14
24
|
Process,
|
|
15
25
|
ProcessOptions,
|
|
16
26
|
ProcessStatus,
|
|
17
|
-
StreamOptions
|
|
18
|
-
} from "./types";
|
|
19
|
-
import {
|
|
20
|
-
ProcessNotFoundError,
|
|
21
|
-
SandboxError
|
|
27
|
+
StreamOptions,
|
|
22
28
|
} from "./types";
|
|
29
|
+
import { ProcessNotFoundError, SandboxError } from "./types";
|
|
23
30
|
|
|
24
31
|
export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
25
32
|
const stub = getContainer(ns, id);
|
|
@@ -31,22 +38,22 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
defaultPort = 3000; // Default port for the container's Bun server
|
|
42
|
+
sleepAfter = "20m"; // Keep container warm for 20 minutes to avoid cold starts
|
|
43
|
+
client: JupyterClient;
|
|
36
44
|
private sandboxName: string | null = null;
|
|
45
|
+
private codeInterpreter: CodeInterpreter;
|
|
37
46
|
|
|
38
47
|
constructor(ctx: DurableObjectState, env: Env) {
|
|
39
48
|
super(ctx, env);
|
|
40
|
-
this.client = new
|
|
49
|
+
this.client = new JupyterClient({
|
|
41
50
|
onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {
|
|
42
51
|
console.log(
|
|
43
52
|
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
44
53
|
);
|
|
45
54
|
},
|
|
46
55
|
onCommandStart: (command) => {
|
|
47
|
-
console.log(
|
|
48
|
-
`[Container] Command started: ${command}`
|
|
49
|
-
);
|
|
56
|
+
console.log(`[Container] Command started: ${command}`);
|
|
50
57
|
},
|
|
51
58
|
onError: (error, _command) => {
|
|
52
59
|
console.error(`[Container] Command error: ${error}`);
|
|
@@ -58,9 +65,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
58
65
|
stub: this,
|
|
59
66
|
});
|
|
60
67
|
|
|
68
|
+
// Initialize code interpreter
|
|
69
|
+
this.codeInterpreter = new CodeInterpreter(this);
|
|
70
|
+
|
|
61
71
|
// Load the sandbox name from storage on initialization
|
|
62
72
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
63
|
-
this.sandboxName =
|
|
73
|
+
this.sandboxName =
|
|
74
|
+
(await this.ctx.storage.get<string>("sandboxName")) || null;
|
|
64
75
|
});
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -68,7 +79,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
68
79
|
async setSandboxName(name: string): Promise<void> {
|
|
69
80
|
if (!this.sandboxName) {
|
|
70
81
|
this.sandboxName = name;
|
|
71
|
-
await this.ctx.storage.put(
|
|
82
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
72
83
|
console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
|
|
73
84
|
}
|
|
74
85
|
}
|
|
@@ -99,10 +110,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
99
110
|
const url = new URL(request.url);
|
|
100
111
|
|
|
101
112
|
// Capture and store the sandbox name from the header if present
|
|
102
|
-
if (!this.sandboxName && request.headers.has(
|
|
103
|
-
const name = request.headers.get(
|
|
113
|
+
if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
|
|
114
|
+
const name = request.headers.get("X-Sandbox-Name")!;
|
|
104
115
|
this.sandboxName = name;
|
|
105
|
-
await this.ctx.storage.put(
|
|
116
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
106
117
|
console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
|
|
107
118
|
}
|
|
108
119
|
|
|
@@ -137,23 +148,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
137
148
|
try {
|
|
138
149
|
// Handle cancellation
|
|
139
150
|
if (options?.signal?.aborted) {
|
|
140
|
-
throw new Error(
|
|
151
|
+
throw new Error("Operation was aborted");
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
let result: ExecResult;
|
|
144
155
|
|
|
145
156
|
if (options?.stream && options?.onOutput) {
|
|
146
157
|
// Streaming with callbacks - we need to collect the final result
|
|
147
|
-
result = await this.executeWithStreaming(
|
|
148
|
-
} else {
|
|
149
|
-
// Regular execution
|
|
150
|
-
const response = await this.client.execute(
|
|
158
|
+
result = await this.executeWithStreaming(
|
|
151
159
|
command,
|
|
152
|
-
options
|
|
160
|
+
options,
|
|
161
|
+
startTime,
|
|
162
|
+
timestamp
|
|
153
163
|
);
|
|
164
|
+
} else {
|
|
165
|
+
// Regular execution
|
|
166
|
+
const response = await this.client.execute(command, {
|
|
167
|
+
sessionId: options?.sessionId,
|
|
168
|
+
cwd: options?.cwd,
|
|
169
|
+
env: options?.env,
|
|
170
|
+
});
|
|
154
171
|
|
|
155
172
|
const duration = Date.now() - startTime;
|
|
156
|
-
result = this.mapExecuteResponseToExecResult(
|
|
173
|
+
result = this.mapExecuteResponseToExecResult(
|
|
174
|
+
response,
|
|
175
|
+
duration,
|
|
176
|
+
options?.sessionId
|
|
177
|
+
);
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
// Call completion callback if provided
|
|
@@ -180,26 +201,28 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
180
201
|
startTime: number,
|
|
181
202
|
timestamp: string
|
|
182
203
|
): Promise<ExecResult> {
|
|
183
|
-
let stdout =
|
|
184
|
-
let stderr =
|
|
204
|
+
let stdout = "";
|
|
205
|
+
let stderr = "";
|
|
185
206
|
|
|
186
207
|
try {
|
|
187
|
-
const stream = await this.client.executeCommandStream(
|
|
188
|
-
|
|
208
|
+
const stream = await this.client.executeCommandStream(
|
|
209
|
+
command,
|
|
210
|
+
options.sessionId
|
|
211
|
+
);
|
|
189
212
|
|
|
190
|
-
for await (const event of parseSSEStream<
|
|
213
|
+
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
191
214
|
// Check for cancellation
|
|
192
215
|
if (options.signal?.aborted) {
|
|
193
|
-
throw new Error(
|
|
216
|
+
throw new Error("Operation was aborted");
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
switch (event.type) {
|
|
197
|
-
case
|
|
198
|
-
case
|
|
220
|
+
case "stdout":
|
|
221
|
+
case "stderr":
|
|
199
222
|
if (event.data) {
|
|
200
223
|
// Update accumulated output
|
|
201
|
-
if (event.type ===
|
|
202
|
-
if (event.type ===
|
|
224
|
+
if (event.type === "stdout") stdout += event.data;
|
|
225
|
+
if (event.type === "stderr") stderr += event.data;
|
|
203
226
|
|
|
204
227
|
// Call user's callback
|
|
205
228
|
if (options.onOutput) {
|
|
@@ -208,39 +231,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
208
231
|
}
|
|
209
232
|
break;
|
|
210
233
|
|
|
211
|
-
case
|
|
234
|
+
case "complete": {
|
|
212
235
|
// Use result from complete event if available
|
|
213
236
|
const duration = Date.now() - startTime;
|
|
214
|
-
return
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
237
|
+
return (
|
|
238
|
+
event.result || {
|
|
239
|
+
success: event.exitCode === 0,
|
|
240
|
+
exitCode: event.exitCode || 0,
|
|
241
|
+
stdout,
|
|
242
|
+
stderr,
|
|
243
|
+
command,
|
|
244
|
+
duration,
|
|
245
|
+
timestamp,
|
|
246
|
+
sessionId: options.sessionId,
|
|
247
|
+
}
|
|
248
|
+
);
|
|
224
249
|
}
|
|
225
250
|
|
|
226
|
-
case
|
|
227
|
-
throw new Error(event.error ||
|
|
251
|
+
case "error":
|
|
252
|
+
throw new Error(event.error || "Command execution failed");
|
|
228
253
|
}
|
|
229
254
|
}
|
|
230
255
|
|
|
231
256
|
// If we get here without a complete event, something went wrong
|
|
232
|
-
throw new Error(
|
|
233
|
-
|
|
257
|
+
throw new Error("Stream ended without completion event");
|
|
234
258
|
} catch (error) {
|
|
235
259
|
if (options.signal?.aborted) {
|
|
236
|
-
throw new Error(
|
|
260
|
+
throw new Error("Operation was aborted");
|
|
237
261
|
}
|
|
238
262
|
throw error;
|
|
239
263
|
}
|
|
240
264
|
}
|
|
241
265
|
|
|
242
266
|
private mapExecuteResponseToExecResult(
|
|
243
|
-
response:
|
|
267
|
+
response: ExecuteResponse,
|
|
244
268
|
duration: number,
|
|
245
269
|
sessionId?: string
|
|
246
270
|
): ExecResult {
|
|
@@ -252,13 +276,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
252
276
|
command: response.command,
|
|
253
277
|
duration,
|
|
254
278
|
timestamp: response.timestamp,
|
|
255
|
-
sessionId
|
|
279
|
+
sessionId,
|
|
256
280
|
};
|
|
257
281
|
}
|
|
258
282
|
|
|
259
|
-
|
|
260
283
|
// Background process management
|
|
261
|
-
async startProcess(
|
|
284
|
+
async startProcess(
|
|
285
|
+
command: string,
|
|
286
|
+
options?: ProcessOptions
|
|
287
|
+
): Promise<Process> {
|
|
262
288
|
// Use the new HttpClient method to start the process
|
|
263
289
|
try {
|
|
264
290
|
const response = await this.client.startProcess(command, {
|
|
@@ -268,7 +294,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
268
294
|
env: options?.env,
|
|
269
295
|
cwd: options?.cwd,
|
|
270
296
|
encoding: options?.encoding,
|
|
271
|
-
autoCleanup: options?.autoCleanup
|
|
297
|
+
autoCleanup: options?.autoCleanup,
|
|
272
298
|
});
|
|
273
299
|
|
|
274
300
|
const process = response.process;
|
|
@@ -283,14 +309,14 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
283
309
|
sessionId: process.sessionId,
|
|
284
310
|
|
|
285
311
|
async kill(): Promise<void> {
|
|
286
|
-
throw new Error(
|
|
312
|
+
throw new Error("Method will be replaced");
|
|
287
313
|
},
|
|
288
314
|
async getStatus(): Promise<ProcessStatus> {
|
|
289
|
-
throw new Error(
|
|
315
|
+
throw new Error("Method will be replaced");
|
|
290
316
|
},
|
|
291
317
|
async getLogs(): Promise<{ stdout: string; stderr: string }> {
|
|
292
|
-
throw new Error(
|
|
293
|
-
}
|
|
318
|
+
throw new Error("Method will be replaced");
|
|
319
|
+
},
|
|
294
320
|
};
|
|
295
321
|
|
|
296
322
|
// Bind context properly
|
|
@@ -300,7 +326,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
300
326
|
|
|
301
327
|
processObj.getStatus = async () => {
|
|
302
328
|
const current = await this.getProcess(process.id);
|
|
303
|
-
return current?.status ||
|
|
329
|
+
return current?.status || "error";
|
|
304
330
|
};
|
|
305
331
|
|
|
306
332
|
processObj.getLogs = async () => {
|
|
@@ -314,7 +340,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
314
340
|
}
|
|
315
341
|
|
|
316
342
|
return processObj;
|
|
317
|
-
|
|
318
343
|
} catch (error) {
|
|
319
344
|
if (options?.onError && error instanceof Error) {
|
|
320
345
|
options.onError(error);
|
|
@@ -327,7 +352,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
327
352
|
async listProcesses(): Promise<Process[]> {
|
|
328
353
|
const response = await this.client.listProcesses();
|
|
329
354
|
|
|
330
|
-
return response.processes.map(processData => ({
|
|
355
|
+
return response.processes.map((processData) => ({
|
|
331
356
|
id: processData.id,
|
|
332
357
|
pid: processData.pid,
|
|
333
358
|
command: processData.command,
|
|
@@ -343,13 +368,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
343
368
|
|
|
344
369
|
getStatus: async () => {
|
|
345
370
|
const current = await this.getProcess(processData.id);
|
|
346
|
-
return current?.status ||
|
|
371
|
+
return current?.status || "error";
|
|
347
372
|
},
|
|
348
373
|
|
|
349
374
|
getLogs: async () => {
|
|
350
375
|
const logs = await this.getProcessLogs(processData.id);
|
|
351
376
|
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
352
|
-
}
|
|
377
|
+
},
|
|
353
378
|
}));
|
|
354
379
|
}
|
|
355
380
|
|
|
@@ -376,13 +401,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
376
401
|
|
|
377
402
|
getStatus: async () => {
|
|
378
403
|
const current = await this.getProcess(processData.id);
|
|
379
|
-
return current?.status ||
|
|
404
|
+
return current?.status || "error";
|
|
380
405
|
},
|
|
381
406
|
|
|
382
407
|
getLogs: async () => {
|
|
383
408
|
const logs = await this.getProcessLogs(processData.id);
|
|
384
409
|
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
385
|
-
}
|
|
410
|
+
},
|
|
386
411
|
};
|
|
387
412
|
}
|
|
388
413
|
|
|
@@ -391,12 +416,17 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
391
416
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
392
417
|
await this.client.killProcess(id);
|
|
393
418
|
} catch (error) {
|
|
394
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
error instanceof Error &&
|
|
421
|
+
error.message.includes("Process not found")
|
|
422
|
+
) {
|
|
395
423
|
throw new ProcessNotFoundError(id);
|
|
396
424
|
}
|
|
397
425
|
throw new SandboxError(
|
|
398
|
-
`Failed to kill process ${id}: ${
|
|
399
|
-
|
|
426
|
+
`Failed to kill process ${id}: ${
|
|
427
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
428
|
+
}`,
|
|
429
|
+
"KILL_PROCESS_FAILED"
|
|
400
430
|
);
|
|
401
431
|
}
|
|
402
432
|
}
|
|
@@ -413,40 +443,53 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
413
443
|
return 0;
|
|
414
444
|
}
|
|
415
445
|
|
|
416
|
-
async getProcessLogs(
|
|
446
|
+
async getProcessLogs(
|
|
447
|
+
id: string
|
|
448
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
417
449
|
try {
|
|
418
450
|
const response = await this.client.getProcessLogs(id);
|
|
419
451
|
return {
|
|
420
452
|
stdout: response.stdout,
|
|
421
|
-
stderr: response.stderr
|
|
453
|
+
stderr: response.stderr,
|
|
422
454
|
};
|
|
423
455
|
} catch (error) {
|
|
424
|
-
if (
|
|
456
|
+
if (
|
|
457
|
+
error instanceof Error &&
|
|
458
|
+
error.message.includes("Process not found")
|
|
459
|
+
) {
|
|
425
460
|
throw new ProcessNotFoundError(id);
|
|
426
461
|
}
|
|
427
462
|
throw error;
|
|
428
463
|
}
|
|
429
464
|
}
|
|
430
465
|
|
|
431
|
-
|
|
432
466
|
// Streaming methods - return ReadableStream for RPC compatibility
|
|
433
|
-
async execStream(
|
|
467
|
+
async execStream(
|
|
468
|
+
command: string,
|
|
469
|
+
options?: StreamOptions
|
|
470
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
434
471
|
// Check for cancellation
|
|
435
472
|
if (options?.signal?.aborted) {
|
|
436
|
-
throw new Error(
|
|
473
|
+
throw new Error("Operation was aborted");
|
|
437
474
|
}
|
|
438
475
|
|
|
439
476
|
// Get the stream from HttpClient (need to add this method)
|
|
440
|
-
const stream = await this.client.executeCommandStream(
|
|
477
|
+
const stream = await this.client.executeCommandStream(
|
|
478
|
+
command,
|
|
479
|
+
options?.sessionId
|
|
480
|
+
);
|
|
441
481
|
|
|
442
482
|
// Return the ReadableStream directly - can be converted to AsyncIterable by consumers
|
|
443
483
|
return stream;
|
|
444
484
|
}
|
|
445
485
|
|
|
446
|
-
async streamProcessLogs(
|
|
486
|
+
async streamProcessLogs(
|
|
487
|
+
processId: string,
|
|
488
|
+
options?: { signal?: AbortSignal }
|
|
489
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
447
490
|
// Check for cancellation
|
|
448
491
|
if (options?.signal?.aborted) {
|
|
449
|
-
throw new Error(
|
|
492
|
+
throw new Error("Operation was aborted");
|
|
450
493
|
}
|
|
451
494
|
|
|
452
495
|
// Get the stream from HttpClient
|
|
@@ -463,10 +506,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
463
506
|
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
464
507
|
}
|
|
465
508
|
|
|
466
|
-
async mkdir(
|
|
467
|
-
path: string,
|
|
468
|
-
options: { recursive?: boolean } = {}
|
|
469
|
-
) {
|
|
509
|
+
async mkdir(path: string, options: { recursive?: boolean } = {}) {
|
|
470
510
|
return this.client.mkdir(path, options.recursive);
|
|
471
511
|
}
|
|
472
512
|
|
|
@@ -482,25 +522,26 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
482
522
|
return this.client.deleteFile(path);
|
|
483
523
|
}
|
|
484
524
|
|
|
485
|
-
async renameFile(
|
|
486
|
-
oldPath: string,
|
|
487
|
-
newPath: string
|
|
488
|
-
) {
|
|
525
|
+
async renameFile(oldPath: string, newPath: string) {
|
|
489
526
|
return this.client.renameFile(oldPath, newPath);
|
|
490
527
|
}
|
|
491
528
|
|
|
492
|
-
async moveFile(
|
|
493
|
-
sourcePath: string,
|
|
494
|
-
destinationPath: string
|
|
495
|
-
) {
|
|
529
|
+
async moveFile(sourcePath: string, destinationPath: string) {
|
|
496
530
|
return this.client.moveFile(sourcePath, destinationPath);
|
|
497
531
|
}
|
|
498
532
|
|
|
499
|
-
async readFile(
|
|
533
|
+
async readFile(path: string, options: { encoding?: string } = {}) {
|
|
534
|
+
return this.client.readFile(path, options.encoding);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async listFiles(
|
|
500
538
|
path: string,
|
|
501
|
-
options: {
|
|
539
|
+
options: {
|
|
540
|
+
recursive?: boolean;
|
|
541
|
+
includeHidden?: boolean;
|
|
542
|
+
} = {}
|
|
502
543
|
) {
|
|
503
|
-
return this.client.
|
|
544
|
+
return this.client.listFiles(path, options);
|
|
504
545
|
}
|
|
505
546
|
|
|
506
547
|
async exposePort(port: number, options: { name?: string; hostname: string }) {
|
|
@@ -508,10 +549,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
508
549
|
|
|
509
550
|
// We need the sandbox name to construct preview URLs
|
|
510
551
|
if (!this.sandboxName) {
|
|
511
|
-
throw new Error(
|
|
552
|
+
throw new Error(
|
|
553
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
554
|
+
);
|
|
512
555
|
}
|
|
513
556
|
|
|
514
|
-
const url = this.constructPreviewUrl(
|
|
557
|
+
const url = this.constructPreviewUrl(
|
|
558
|
+
port,
|
|
559
|
+
this.sandboxName,
|
|
560
|
+
options.hostname
|
|
561
|
+
);
|
|
515
562
|
|
|
516
563
|
return {
|
|
517
564
|
url,
|
|
@@ -522,17 +569,27 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
522
569
|
|
|
523
570
|
async unexposePort(port: number) {
|
|
524
571
|
if (!validatePort(port)) {
|
|
525
|
-
logSecurityEvent(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
572
|
+
logSecurityEvent(
|
|
573
|
+
"INVALID_PORT_UNEXPOSE",
|
|
574
|
+
{
|
|
575
|
+
port,
|
|
576
|
+
},
|
|
577
|
+
"high"
|
|
578
|
+
);
|
|
579
|
+
throw new SecurityError(
|
|
580
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
581
|
+
);
|
|
529
582
|
}
|
|
530
583
|
|
|
531
584
|
await this.client.unexposePort(port);
|
|
532
585
|
|
|
533
|
-
logSecurityEvent(
|
|
534
|
-
|
|
535
|
-
|
|
586
|
+
logSecurityEvent(
|
|
587
|
+
"PORT_UNEXPOSED",
|
|
588
|
+
{
|
|
589
|
+
port,
|
|
590
|
+
},
|
|
591
|
+
"low"
|
|
592
|
+
);
|
|
536
593
|
}
|
|
537
594
|
|
|
538
595
|
async getExposedPorts(hostname: string) {
|
|
@@ -540,10 +597,12 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
540
597
|
|
|
541
598
|
// We need the sandbox name to construct preview URLs
|
|
542
599
|
if (!this.sandboxName) {
|
|
543
|
-
throw new Error(
|
|
600
|
+
throw new Error(
|
|
601
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
602
|
+
);
|
|
544
603
|
}
|
|
545
604
|
|
|
546
|
-
return response.ports.map(port => ({
|
|
605
|
+
return response.ports.map((port) => ({
|
|
547
606
|
url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
|
|
548
607
|
port: port.port,
|
|
549
608
|
name: port.name,
|
|
@@ -551,27 +610,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
551
610
|
}));
|
|
552
611
|
}
|
|
553
612
|
|
|
554
|
-
|
|
555
|
-
|
|
613
|
+
private constructPreviewUrl(
|
|
614
|
+
port: number,
|
|
615
|
+
sandboxId: string,
|
|
616
|
+
hostname: string
|
|
617
|
+
): string {
|
|
556
618
|
if (!validatePort(port)) {
|
|
557
|
-
logSecurityEvent(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
619
|
+
logSecurityEvent(
|
|
620
|
+
"INVALID_PORT_REJECTED",
|
|
621
|
+
{
|
|
622
|
+
port,
|
|
623
|
+
sandboxId,
|
|
624
|
+
hostname,
|
|
625
|
+
},
|
|
626
|
+
"high"
|
|
627
|
+
);
|
|
628
|
+
throw new SecurityError(
|
|
629
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
630
|
+
);
|
|
563
631
|
}
|
|
564
632
|
|
|
565
633
|
let sanitizedSandboxId: string;
|
|
566
634
|
try {
|
|
567
635
|
sanitizedSandboxId = sanitizeSandboxId(sandboxId);
|
|
568
636
|
} catch (error) {
|
|
569
|
-
logSecurityEvent(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
637
|
+
logSecurityEvent(
|
|
638
|
+
"INVALID_SANDBOX_ID_REJECTED",
|
|
639
|
+
{
|
|
640
|
+
sandboxId,
|
|
641
|
+
port,
|
|
642
|
+
hostname,
|
|
643
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
644
|
+
},
|
|
645
|
+
"high"
|
|
646
|
+
);
|
|
575
647
|
throw error;
|
|
576
648
|
}
|
|
577
649
|
|
|
@@ -579,8 +651,8 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
579
651
|
|
|
580
652
|
if (isLocalhost) {
|
|
581
653
|
// Unified subdomain approach for localhost (RFC 6761)
|
|
582
|
-
const [host, portStr] = hostname.split(
|
|
583
|
-
const mainPort = portStr ||
|
|
654
|
+
const [host, portStr] = hostname.split(":");
|
|
655
|
+
const mainPort = portStr || "80";
|
|
584
656
|
|
|
585
657
|
// Use URL constructor for safe URL building
|
|
586
658
|
try {
|
|
@@ -591,23 +663,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
591
663
|
|
|
592
664
|
const finalUrl = baseUrl.toString();
|
|
593
665
|
|
|
594
|
-
logSecurityEvent(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
666
|
+
logSecurityEvent(
|
|
667
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
668
|
+
{
|
|
669
|
+
port,
|
|
670
|
+
sandboxId: sanitizedSandboxId,
|
|
671
|
+
hostname,
|
|
672
|
+
resultUrl: finalUrl,
|
|
673
|
+
environment: "localhost",
|
|
674
|
+
},
|
|
675
|
+
"low"
|
|
676
|
+
);
|
|
601
677
|
|
|
602
678
|
return finalUrl;
|
|
603
679
|
} catch (error) {
|
|
604
|
-
logSecurityEvent(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
680
|
+
logSecurityEvent(
|
|
681
|
+
"URL_CONSTRUCTION_FAILED",
|
|
682
|
+
{
|
|
683
|
+
port,
|
|
684
|
+
sandboxId: sanitizedSandboxId,
|
|
685
|
+
hostname,
|
|
686
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
687
|
+
},
|
|
688
|
+
"high"
|
|
689
|
+
);
|
|
690
|
+
throw new SecurityError(
|
|
691
|
+
`Failed to construct preview URL: ${
|
|
692
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
693
|
+
}`
|
|
694
|
+
);
|
|
611
695
|
}
|
|
612
696
|
}
|
|
613
697
|
|
|
@@ -623,23 +707,82 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
623
707
|
|
|
624
708
|
const finalUrl = baseUrl.toString();
|
|
625
709
|
|
|
626
|
-
logSecurityEvent(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
710
|
+
logSecurityEvent(
|
|
711
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
712
|
+
{
|
|
713
|
+
port,
|
|
714
|
+
sandboxId: sanitizedSandboxId,
|
|
715
|
+
hostname,
|
|
716
|
+
resultUrl: finalUrl,
|
|
717
|
+
environment: "production",
|
|
718
|
+
},
|
|
719
|
+
"low"
|
|
720
|
+
);
|
|
633
721
|
|
|
634
722
|
return finalUrl;
|
|
635
723
|
} catch (error) {
|
|
636
|
-
logSecurityEvent(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
724
|
+
logSecurityEvent(
|
|
725
|
+
"URL_CONSTRUCTION_FAILED",
|
|
726
|
+
{
|
|
727
|
+
port,
|
|
728
|
+
sandboxId: sanitizedSandboxId,
|
|
729
|
+
hostname,
|
|
730
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
731
|
+
},
|
|
732
|
+
"high"
|
|
733
|
+
);
|
|
734
|
+
throw new SecurityError(
|
|
735
|
+
`Failed to construct preview URL: ${
|
|
736
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
737
|
+
}`
|
|
738
|
+
);
|
|
643
739
|
}
|
|
644
740
|
}
|
|
741
|
+
|
|
742
|
+
// Code Interpreter Methods
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Create a new code execution context
|
|
746
|
+
*/
|
|
747
|
+
async createCodeContext(
|
|
748
|
+
options?: CreateContextOptions
|
|
749
|
+
): Promise<CodeContext> {
|
|
750
|
+
return this.codeInterpreter.createCodeContext(options);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Run code with streaming callbacks
|
|
755
|
+
*/
|
|
756
|
+
async runCode(
|
|
757
|
+
code: string,
|
|
758
|
+
options?: RunCodeOptions
|
|
759
|
+
): Promise<ExecutionResult> {
|
|
760
|
+
const execution = await this.codeInterpreter.runCode(code, options);
|
|
761
|
+
// Convert to plain object for RPC serialization
|
|
762
|
+
return execution.toJSON();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Run code and return a streaming response
|
|
767
|
+
*/
|
|
768
|
+
async runCodeStream(
|
|
769
|
+
code: string,
|
|
770
|
+
options?: RunCodeOptions
|
|
771
|
+
): Promise<ReadableStream> {
|
|
772
|
+
return this.codeInterpreter.runCodeStream(code, options);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* List all code contexts
|
|
777
|
+
*/
|
|
778
|
+
async listCodeContexts(): Promise<CodeContext[]> {
|
|
779
|
+
return this.codeInterpreter.listCodeContexts();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Delete a code context
|
|
784
|
+
*/
|
|
785
|
+
async deleteCodeContext(contextId: string): Promise<void> {
|
|
786
|
+
return this.codeInterpreter.deleteCodeContext(contextId);
|
|
787
|
+
}
|
|
645
788
|
}
|