@cloudflare/sandbox 0.0.0-1ab3f9d → 0.0.0-1be7d53

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);
@@ -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: InterpreterClient;
37
45
  private sandboxName: string | null = null;
46
+ private codeInterpreter: CodeInterpreter;
47
+ private defaultSession: ExecutionSession | null = null;
38
48
 
39
- constructor(ctx: DurableObjectState, env: Env) {
49
+ constructor(ctx: DurableObjectState<{}>, env: Env) {
40
50
  super(ctx, env);
41
- this.client = new HttpClient({
51
+ this.client = new InterpreterClient({
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
 
@@ -121,358 +135,104 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
121
135
  return parseInt(proxyMatch[1]);
122
136
  }
123
137
 
138
+ if (url.port) {
139
+ return parseInt(url.port);
140
+ }
141
+
124
142
  // All other requests go to control plane on port 3000
125
143
  // This includes /api/* endpoints and any other control requests
126
144
  return 3000;
127
145
  }
128
146
 
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
- {
154
- sessionId: options?.sessionId,
155
- cwd: options?.cwd,
156
- env: options?.env,
157
- }
158
- );
159
-
160
- const duration = Date.now() - startTime;
161
- result = this.mapExecuteResponseToExecResult(response, duration, options?.sessionId);
162
- }
163
-
164
- // Call completion callback if provided
165
- if (options?.onComplete) {
166
- options.onComplete(result);
167
- }
168
-
169
- return result;
170
- } catch (error) {
171
- if (options?.onError && error instanceof Error) {
172
- options.onError(error);
173
- }
174
- throw error;
175
- } finally {
176
- if (timeoutId) {
177
- clearTimeout(timeoutId);
178
- }
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}`);
179
158
  }
159
+ return this.defaultSession;
180
160
  }
181
161
 
182
- private async executeWithStreaming(
183
- command: string,
184
- options: ExecOptions,
185
- startTime: number,
186
- timestamp: string
187
- ): Promise<ExecResult> {
188
- let stdout = '';
189
- let stderr = '';
190
162
 
191
- try {
192
- const stream = await this.client.executeCommandStream(command, options.sessionId);
193
- const { parseSSEStream } = await import('./sse-parser');
194
-
195
- for await (const event of parseSSEStream<import('./types').ExecEvent>(stream)) {
196
- // Check for cancellation
197
- if (options.signal?.aborted) {
198
- throw new Error('Operation was aborted');
199
- }
200
-
201
- switch (event.type) {
202
- case 'stdout':
203
- case 'stderr':
204
- if (event.data) {
205
- // Update accumulated output
206
- if (event.type === 'stdout') stdout += event.data;
207
- if (event.type === 'stderr') stderr += event.data;
208
-
209
- // Call user's callback
210
- if (options.onOutput) {
211
- options.onOutput(event.type, event.data);
212
- }
213
- }
214
- break;
215
-
216
- case 'complete': {
217
- // Use result from complete event if available
218
- const duration = Date.now() - startTime;
219
- return event.result || {
220
- success: event.exitCode === 0,
221
- exitCode: event.exitCode || 0,
222
- stdout,
223
- stderr,
224
- command,
225
- duration,
226
- timestamp,
227
- sessionId: options.sessionId
228
- };
229
- }
230
-
231
- case 'error':
232
- throw new Error(event.error || 'Command execution failed');
233
- }
234
- }
235
-
236
- // If we get here without a complete event, something went wrong
237
- throw new Error('Stream ended without completion event');
238
-
239
- } catch (error) {
240
- if (options.signal?.aborted) {
241
- throw new Error('Operation was aborted');
242
- }
243
- throw error;
244
- }
245
- }
246
-
247
- private mapExecuteResponseToExecResult(
248
- response: import('./client').ExecuteResponse,
249
- duration: number,
250
- sessionId?: string
251
- ): ExecResult {
252
- return {
253
- success: response.success,
254
- exitCode: response.exitCode,
255
- stdout: response.stdout,
256
- stderr: response.stderr,
257
- command: response.command,
258
- duration,
259
- timestamp: response.timestamp,
260
- sessionId
261
- };
163
+ async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
164
+ const session = await this.ensureDefaultSession();
165
+ return session.exec(command, options);
262
166
  }
263
167
 
264
-
265
- // Background process management
266
- async startProcess(command: string, options?: ProcessOptions): Promise<Process> {
267
- // Use the new HttpClient method to start the process
268
- try {
269
- const response = await this.client.startProcess(command, {
270
- processId: options?.processId,
271
- sessionId: options?.sessionId,
272
- timeout: options?.timeout,
273
- env: options?.env,
274
- cwd: options?.cwd,
275
- encoding: options?.encoding,
276
- autoCleanup: options?.autoCleanup
277
- });
278
-
279
- const process = response.process;
280
- const processObj: Process = {
281
- id: process.id,
282
- pid: process.pid,
283
- command: process.command,
284
- status: process.status as ProcessStatus,
285
- startTime: new Date(process.startTime),
286
- endTime: undefined,
287
- exitCode: undefined,
288
- sessionId: process.sessionId,
289
-
290
- async kill(): Promise<void> {
291
- throw new Error('Method will be replaced');
292
- },
293
- async getStatus(): Promise<ProcessStatus> {
294
- throw new Error('Method will be replaced');
295
- },
296
- async getLogs(): Promise<{ stdout: string; stderr: string }> {
297
- throw new Error('Method will be replaced');
298
- }
299
- };
300
-
301
- // Bind context properly
302
- processObj.kill = async (signal?: string) => {
303
- await this.killProcess(process.id, signal);
304
- };
305
-
306
- processObj.getStatus = async () => {
307
- const current = await this.getProcess(process.id);
308
- return current?.status || 'error';
309
- };
310
-
311
- processObj.getLogs = async () => {
312
- const logs = await this.getProcessLogs(process.id);
313
- return { stdout: logs.stdout, stderr: logs.stderr };
314
- };
315
-
316
- // Call onStart callback if provided
317
- if (options?.onStart) {
318
- options.onStart(processObj);
319
- }
320
-
321
- return processObj;
322
-
323
- } catch (error) {
324
- if (options?.onError && error instanceof Error) {
325
- options.onError(error);
326
- }
327
-
328
- throw error;
329
- }
168
+ async startProcess(
169
+ command: string,
170
+ options?: ProcessOptions
171
+ ): Promise<Process> {
172
+ const session = await this.ensureDefaultSession();
173
+ return session.startProcess(command, options);
330
174
  }
331
175
 
332
176
  async listProcesses(): Promise<Process[]> {
333
- const response = await this.client.listProcesses();
334
-
335
- return response.processes.map(processData => ({
336
- id: processData.id,
337
- pid: processData.pid,
338
- command: processData.command,
339
- status: processData.status,
340
- startTime: new Date(processData.startTime),
341
- endTime: processData.endTime ? new Date(processData.endTime) : undefined,
342
- exitCode: processData.exitCode,
343
- sessionId: processData.sessionId,
344
-
345
- kill: async (signal?: string) => {
346
- await this.killProcess(processData.id, signal);
347
- },
348
-
349
- getStatus: async () => {
350
- const current = await this.getProcess(processData.id);
351
- return current?.status || 'error';
352
- },
353
-
354
- getLogs: async () => {
355
- const logs = await this.getProcessLogs(processData.id);
356
- return { stdout: logs.stdout, stderr: logs.stderr };
357
- }
358
- }));
177
+ const session = await this.ensureDefaultSession();
178
+ return session.listProcesses();
359
179
  }
360
180
 
361
181
  async getProcess(id: string): Promise<Process | null> {
362
- const response = await this.client.getProcess(id);
363
- if (!response.process) {
364
- return null;
365
- }
366
-
367
- const processData = response.process;
368
- return {
369
- id: processData.id,
370
- pid: processData.pid,
371
- command: processData.command,
372
- status: processData.status,
373
- startTime: new Date(processData.startTime),
374
- endTime: processData.endTime ? new Date(processData.endTime) : undefined,
375
- exitCode: processData.exitCode,
376
- sessionId: processData.sessionId,
377
-
378
- kill: async (signal?: string) => {
379
- await this.killProcess(processData.id, signal);
380
- },
381
-
382
- getStatus: async () => {
383
- const current = await this.getProcess(processData.id);
384
- return current?.status || 'error';
385
- },
386
-
387
- getLogs: async () => {
388
- const logs = await this.getProcessLogs(processData.id);
389
- return { stdout: logs.stdout, stderr: logs.stderr };
390
- }
391
- };
182
+ const session = await this.ensureDefaultSession();
183
+ return session.getProcess(id);
392
184
  }
393
185
 
394
- async killProcess(id: string, _signal?: string): Promise<void> {
395
- try {
396
- // Note: signal parameter is not currently supported by the HttpClient implementation
397
- await this.client.killProcess(id);
398
- } catch (error) {
399
- if (error instanceof Error && error.message.includes('Process not found')) {
400
- throw new ProcessNotFoundError(id);
401
- }
402
- throw new SandboxError(
403
- `Failed to kill process ${id}: ${error instanceof Error ? error.message : 'Unknown error'}`,
404
- 'KILL_PROCESS_FAILED'
405
- );
406
- }
186
+ async killProcess(id: string, signal?: string): Promise<void> {
187
+ const session = await this.ensureDefaultSession();
188
+ return session.killProcess(id, signal);
407
189
  }
408
190
 
409
191
  async killAllProcesses(): Promise<number> {
410
- const response = await this.client.killAllProcesses();
411
- return response.killedCount;
192
+ const session = await this.ensureDefaultSession();
193
+ return session.killAllProcesses();
412
194
  }
413
195
 
414
196
  async cleanupCompletedProcesses(): Promise<number> {
415
- // For now, this would need to be implemented as a container endpoint
416
- // as we no longer maintain local process storage
417
- // We'll return 0 as a placeholder until the container endpoint is added
418
- return 0;
197
+ const session = await this.ensureDefaultSession();
198
+ return session.cleanupCompletedProcesses();
419
199
  }
420
200
 
421
- async getProcessLogs(id: string): Promise<{ stdout: string; stderr: string }> {
422
- try {
423
- const response = await this.client.getProcessLogs(id);
424
- return {
425
- stdout: response.stdout,
426
- stderr: response.stderr
427
- };
428
- } catch (error) {
429
- if (error instanceof Error && error.message.includes('Process not found')) {
430
- throw new ProcessNotFoundError(id);
431
- }
432
- throw error;
433
- }
201
+ async getProcessLogs(
202
+ id: string
203
+ ): Promise<{ stdout: string; stderr: string }> {
204
+ const session = await this.ensureDefaultSession();
205
+ return session.getProcessLogs(id);
434
206
  }
435
207
 
436
-
437
- // Streaming methods - return ReadableStream for RPC compatibility
438
- async execStream(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
439
- // Check for cancellation
440
- if (options?.signal?.aborted) {
441
- throw new Error('Operation was aborted');
442
- }
443
-
444
- // Get the stream from HttpClient (need to add this method)
445
- const stream = await this.client.executeCommandStream(command, options?.sessionId);
446
-
447
- // Return the ReadableStream directly - can be converted to AsyncIterable by consumers
448
- 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);
449
215
  }
450
216
 
451
- async streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {
452
- // Check for cancellation
453
- if (options?.signal?.aborted) {
454
- throw new Error('Operation was aborted');
455
- }
456
-
457
- // Get the stream from HttpClient
458
- const stream = await this.client.streamProcessLogs(processId);
459
-
460
- // Return the ReadableStream directly - can be converted to AsyncIterable by consumers
461
- 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);
462
223
  }
463
224
 
464
225
  async gitCheckout(
465
226
  repoUrl: string,
466
227
  options: { branch?: string; targetDir?: string }
467
228
  ) {
468
- return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
229
+ const session = await this.ensureDefaultSession();
230
+ return session.gitCheckout(repoUrl, options);
469
231
  }
470
232
 
471
- async mkdir(
472
- path: string,
473
- options: { recursive?: boolean } = {}
474
- ) {
475
- 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);
476
236
  }
477
237
 
478
238
  async writeFile(
@@ -480,32 +240,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
480
240
  content: string,
481
241
  options: { encoding?: string } = {}
482
242
  ) {
483
- return this.client.writeFile(path, content, options.encoding);
243
+ const session = await this.ensureDefaultSession();
244
+ return session.writeFile(path, content, options);
484
245
  }
485
246
 
486
247
  async deleteFile(path: string) {
487
- return this.client.deleteFile(path);
248
+ const session = await this.ensureDefaultSession();
249
+ return session.deleteFile(path);
488
250
  }
489
251
 
490
- async renameFile(
491
- oldPath: string,
492
- newPath: string
493
- ) {
494
- 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);
495
255
  }
496
256
 
497
- async moveFile(
498
- sourcePath: string,
499
- destinationPath: string
500
- ) {
501
- 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);
502
260
  }
503
261
 
504
- 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(
505
273
  path: string,
506
- options: { encoding?: string } = {}
274
+ options: {
275
+ recursive?: boolean;
276
+ includeHidden?: boolean;
277
+ } = {}
507
278
  ) {
508
- return this.client.readFile(path, options.encoding);
279
+ const session = await this.ensureDefaultSession();
280
+ return session.listFiles(path, options);
509
281
  }
510
282
 
511
283
  async exposePort(port: number, options: { name?: string; hostname: string }) {
@@ -513,10 +285,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
513
285
 
514
286
  // We need the sandbox name to construct preview URLs
515
287
  if (!this.sandboxName) {
516
- 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
+ );
517
291
  }
518
292
 
519
- const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname);
293
+ const url = this.constructPreviewUrl(
294
+ port,
295
+ this.sandboxName,
296
+ options.hostname
297
+ );
520
298
 
521
299
  return {
522
300
  url,
@@ -527,17 +305,27 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
527
305
 
528
306
  async unexposePort(port: number) {
529
307
  if (!validatePort(port)) {
530
- logSecurityEvent('INVALID_PORT_UNEXPOSE', {
531
- port
532
- }, 'high');
533
- 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
+ );
534
318
  }
535
319
 
536
320
  await this.client.unexposePort(port);
537
321
 
538
- logSecurityEvent('PORT_UNEXPOSED', {
539
- port
540
- }, 'low');
322
+ logSecurityEvent(
323
+ "PORT_UNEXPOSED",
324
+ {
325
+ port,
326
+ },
327
+ "low"
328
+ );
541
329
  }
542
330
 
543
331
  async getExposedPorts(hostname: string) {
@@ -545,10 +333,12 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
545
333
 
546
334
  // We need the sandbox name to construct preview URLs
547
335
  if (!this.sandboxName) {
548
- 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
+ );
549
339
  }
550
340
 
551
- return response.ports.map(port => ({
341
+ return response.ports.map((port) => ({
552
342
  url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
553
343
  port: port.port,
554
344
  name: port.name,
@@ -556,27 +346,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
556
346
  }));
557
347
  }
558
348
 
559
-
560
- private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
349
+ private constructPreviewUrl(
350
+ port: number,
351
+ sandboxId: string,
352
+ hostname: string
353
+ ): string {
561
354
  if (!validatePort(port)) {
562
- logSecurityEvent('INVALID_PORT_REJECTED', {
563
- port,
564
- sandboxId,
565
- hostname
566
- }, 'high');
567
- 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
+ );
568
367
  }
569
368
 
570
369
  let sanitizedSandboxId: string;
571
370
  try {
572
371
  sanitizedSandboxId = sanitizeSandboxId(sandboxId);
573
372
  } catch (error) {
574
- logSecurityEvent('INVALID_SANDBOX_ID_REJECTED', {
575
- sandboxId,
576
- port,
577
- hostname,
578
- error: error instanceof Error ? error.message : 'Unknown error'
579
- }, '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
+ );
580
383
  throw error;
581
384
  }
582
385
 
@@ -584,8 +387,8 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
584
387
 
585
388
  if (isLocalhost) {
586
389
  // Unified subdomain approach for localhost (RFC 6761)
587
- const [host, portStr] = hostname.split(':');
588
- const mainPort = portStr || '80';
390
+ const [host, portStr] = hostname.split(":");
391
+ const mainPort = portStr || "80";
589
392
 
590
393
  // Use URL constructor for safe URL building
591
394
  try {
@@ -596,23 +399,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
596
399
 
597
400
  const finalUrl = baseUrl.toString();
598
401
 
599
- logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {
600
- port,
601
- sandboxId: sanitizedSandboxId,
602
- hostname,
603
- resultUrl: finalUrl,
604
- environment: 'localhost'
605
- }, '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
+ );
606
413
 
607
414
  return finalUrl;
608
415
  } catch (error) {
609
- logSecurityEvent('URL_CONSTRUCTION_FAILED', {
610
- port,
611
- sandboxId: sanitizedSandboxId,
612
- hostname,
613
- error: error instanceof Error ? error.message : 'Unknown error'
614
- }, 'high');
615
- 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
+ );
616
431
  }
617
432
  }
618
433
 
@@ -628,23 +443,314 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
628
443
 
629
444
  const finalUrl = baseUrl.toString();
630
445
 
631
- logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {
632
- port,
633
- sandboxId: sanitizedSandboxId,
634
- hostname,
635
- resultUrl: finalUrl,
636
- environment: 'production'
637
- }, '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
+ );
638
457
 
639
458
  return finalUrl;
640
459
  } catch (error) {
641
- logSecurityEvent('URL_CONSTRUCTION_FAILED', {
642
- port,
643
- sandboxId: sanitizedSandboxId,
644
- hostname,
645
- error: error instanceof Error ? error.message : 'Unknown error'
646
- }, 'high');
647
- 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
+ );
648
475
  }
649
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
+ }
650
756
  }