@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/src/sandbox.ts CHANGED
@@ -1,25 +1,32 @@
1
1
  import { Container, getContainer } from "@cloudflare/containers";
2
- import { HttpClient } from "./client";
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
- sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
35
- client: HttpClient;
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 HttpClient({
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 = await this.ctx.storage.get<string>('sandboxName') || null;
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('sandboxName', name);
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('X-Sandbox-Name')) {
103
- const name = request.headers.get('X-Sandbox-Name')!;
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('sandboxName', name);
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('Operation was aborted');
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(command, options, startTime, timestamp);
148
- } else {
149
- // Regular execution
150
- const response = await this.client.execute(
158
+ result = await this.executeWithStreaming(
151
159
  command,
152
- options?.sessionId
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(response, duration, options?.sessionId);
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(command, options.sessionId);
188
- const { parseSSEStream } = await import('./sse-parser');
208
+ const stream = await this.client.executeCommandStream(
209
+ command,
210
+ options.sessionId
211
+ );
189
212
 
190
- for await (const event of parseSSEStream<import('./types').ExecEvent>(stream)) {
213
+ for await (const event of parseSSEStream<ExecEvent>(stream)) {
191
214
  // Check for cancellation
192
215
  if (options.signal?.aborted) {
193
- throw new Error('Operation was aborted');
216
+ throw new Error("Operation was aborted");
194
217
  }
195
218
 
196
219
  switch (event.type) {
197
- case 'stdout':
198
- case 'stderr':
220
+ case "stdout":
221
+ case "stderr":
199
222
  if (event.data) {
200
223
  // Update accumulated output
201
- if (event.type === 'stdout') stdout += event.data;
202
- if (event.type === 'stderr') stderr += event.data;
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 'complete': {
234
+ case "complete": {
212
235
  // Use result from complete event if available
213
236
  const duration = Date.now() - startTime;
214
- return event.result || {
215
- success: event.exitCode === 0,
216
- exitCode: event.exitCode || 0,
217
- stdout,
218
- stderr,
219
- command,
220
- duration,
221
- timestamp,
222
- sessionId: options.sessionId
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 'error':
227
- throw new Error(event.error || 'Command execution failed');
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('Stream ended without completion event');
233
-
257
+ throw new Error("Stream ended without completion event");
234
258
  } catch (error) {
235
259
  if (options.signal?.aborted) {
236
- throw new Error('Operation was aborted');
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: import('./client').ExecuteResponse,
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(command: string, options?: ProcessOptions): Promise<Process> {
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('Method will be replaced');
312
+ throw new Error("Method will be replaced");
287
313
  },
288
314
  async getStatus(): Promise<ProcessStatus> {
289
- throw new Error('Method will be replaced');
315
+ throw new Error("Method will be replaced");
290
316
  },
291
317
  async getLogs(): Promise<{ stdout: string; stderr: string }> {
292
- throw new Error('Method will be replaced');
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 || 'error';
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 || 'error';
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 || 'error';
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 (error instanceof Error && error.message.includes('Process not found')) {
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}: ${error instanceof Error ? error.message : 'Unknown error'}`,
399
- 'KILL_PROCESS_FAILED'
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(id: string): Promise<{ stdout: string; stderr: string }> {
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 (error instanceof Error && error.message.includes('Process not found')) {
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(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
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('Operation was aborted');
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(command, options?.sessionId);
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(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {
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('Operation was aborted');
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: { encoding?: string } = {}
539
+ options: {
540
+ recursive?: boolean;
541
+ includeHidden?: boolean;
542
+ } = {}
502
543
  ) {
503
- return this.client.readFile(path, options.encoding);
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('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
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(port, this.sandboxName, options.hostname);
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('INVALID_PORT_UNEXPOSE', {
526
- port
527
- }, 'high');
528
- throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
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('PORT_UNEXPOSED', {
534
- port
535
- }, 'low');
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('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
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
- private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
613
+ private constructPreviewUrl(
614
+ port: number,
615
+ sandboxId: string,
616
+ hostname: string
617
+ ): string {
556
618
  if (!validatePort(port)) {
557
- logSecurityEvent('INVALID_PORT_REJECTED', {
558
- port,
559
- sandboxId,
560
- hostname
561
- }, 'high');
562
- throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
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('INVALID_SANDBOX_ID_REJECTED', {
570
- sandboxId,
571
- port,
572
- hostname,
573
- error: error instanceof Error ? error.message : 'Unknown error'
574
- }, 'high');
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 || '80';
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('PREVIEW_URL_CONSTRUCTED', {
595
- port,
596
- sandboxId: sanitizedSandboxId,
597
- hostname,
598
- resultUrl: finalUrl,
599
- environment: 'localhost'
600
- }, 'low');
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('URL_CONSTRUCTION_FAILED', {
605
- port,
606
- sandboxId: sanitizedSandboxId,
607
- hostname,
608
- error: error instanceof Error ? error.message : 'Unknown error'
609
- }, 'high');
610
- throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
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('PREVIEW_URL_CONSTRUCTED', {
627
- port,
628
- sandboxId: sanitizedSandboxId,
629
- hostname,
630
- resultUrl: finalUrl,
631
- environment: 'production'
632
- }, 'low');
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('URL_CONSTRUCTION_FAILED', {
637
- port,
638
- sandboxId: sanitizedSandboxId,
639
- hostname,
640
- error: error instanceof Error ? error.message : 'Unknown error'
641
- }, 'high');
642
- throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
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
  }