@cloudflare/sandbox 0.0.0-af03394 → 0.0.0-af082ab

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