@cloudflare/sandbox 0.0.0-d55b0f4 → 0.0.0-d81d2a5

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