@cloudflare/sandbox 0.0.0-598205f → 0.0.0-603d05f

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