@cloudflare/sandbox 0.0.0-d55b0f4 → 0.0.0-d670ba2

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,354 +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
- 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
- }
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}`);
175
158
  }
159
+ return this.defaultSession;
176
160
  }
177
161
 
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
-
235
- } catch (error) {
236
- if (options.signal?.aborted) {
237
- throw new Error('Operation was aborted');
238
- }
239
- throw error;
240
- }
241
- }
242
162
 
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
- };
163
+ async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
164
+ const session = await this.ensureDefaultSession();
165
+ return session.exec(command, options);
258
166
  }
259
167
 
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
- }
168
+ async startProcess(
169
+ command: string,
170
+ options?: ProcessOptions
171
+ ): Promise<Process> {
172
+ const session = await this.ensureDefaultSession();
173
+ return session.startProcess(command, options);
326
174
  }
327
175
 
328
176
  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
- }));
177
+ const session = await this.ensureDefaultSession();
178
+ return session.listProcesses();
355
179
  }
356
180
 
357
181
  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
- };
182
+ const session = await this.ensureDefaultSession();
183
+ return session.getProcess(id);
388
184
  }
389
185
 
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
- }
186
+ async killProcess(id: string, signal?: string): Promise<void> {
187
+ const session = await this.ensureDefaultSession();
188
+ return session.killProcess(id, signal);
403
189
  }
404
190
 
405
191
  async killAllProcesses(): Promise<number> {
406
- const response = await this.client.killAllProcesses();
407
- return response.killedCount;
192
+ const session = await this.ensureDefaultSession();
193
+ return session.killAllProcesses();
408
194
  }
409
195
 
410
196
  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;
197
+ const session = await this.ensureDefaultSession();
198
+ return session.cleanupCompletedProcesses();
415
199
  }
416
200
 
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
- }
201
+ async getProcessLogs(
202
+ id: string
203
+ ): Promise<{ stdout: string; stderr: string }> {
204
+ const session = await this.ensureDefaultSession();
205
+ return session.getProcessLogs(id);
430
206
  }
431
207
 
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;
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);
445
215
  }
446
216
 
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;
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);
458
223
  }
459
224
 
460
225
  async gitCheckout(
461
226
  repoUrl: string,
462
227
  options: { branch?: string; targetDir?: string }
463
228
  ) {
464
- return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
229
+ const session = await this.ensureDefaultSession();
230
+ return session.gitCheckout(repoUrl, options);
465
231
  }
466
232
 
467
- async mkdir(
468
- path: string,
469
- options: { recursive?: boolean } = {}
470
- ) {
471
- 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);
472
236
  }
473
237
 
474
238
  async writeFile(
@@ -476,32 +240,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
476
240
  content: string,
477
241
  options: { encoding?: string } = {}
478
242
  ) {
479
- return this.client.writeFile(path, content, options.encoding);
243
+ const session = await this.ensureDefaultSession();
244
+ return session.writeFile(path, content, options);
480
245
  }
481
246
 
482
247
  async deleteFile(path: string) {
483
- return this.client.deleteFile(path);
248
+ const session = await this.ensureDefaultSession();
249
+ return session.deleteFile(path);
484
250
  }
485
251
 
486
- async renameFile(
487
- oldPath: string,
488
- newPath: string
489
- ) {
490
- 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);
491
255
  }
492
256
 
493
- async moveFile(
494
- sourcePath: string,
495
- destinationPath: string
496
- ) {
497
- 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);
498
260
  }
499
261
 
500
- 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(
501
273
  path: string,
502
- options: { encoding?: string } = {}
274
+ options: {
275
+ recursive?: boolean;
276
+ includeHidden?: boolean;
277
+ } = {}
503
278
  ) {
504
- return this.client.readFile(path, options.encoding);
279
+ const session = await this.ensureDefaultSession();
280
+ return session.listFiles(path, options);
505
281
  }
506
282
 
507
283
  async exposePort(port: number, options: { name?: string; hostname: string }) {
@@ -509,10 +285,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
509
285
 
510
286
  // We need the sandbox name to construct preview URLs
511
287
  if (!this.sandboxName) {
512
- 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
+ );
513
291
  }
514
292
 
515
- const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname);
293
+ const url = this.constructPreviewUrl(
294
+ port,
295
+ this.sandboxName,
296
+ options.hostname
297
+ );
516
298
 
517
299
  return {
518
300
  url,
@@ -523,17 +305,27 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
523
305
 
524
306
  async unexposePort(port: number) {
525
307
  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.`);
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
+ );
530
318
  }
531
319
 
532
320
  await this.client.unexposePort(port);
533
321
 
534
- logSecurityEvent('PORT_UNEXPOSED', {
535
- port
536
- }, 'low');
322
+ logSecurityEvent(
323
+ "PORT_UNEXPOSED",
324
+ {
325
+ port,
326
+ },
327
+ "low"
328
+ );
537
329
  }
538
330
 
539
331
  async getExposedPorts(hostname: string) {
@@ -541,10 +333,12 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
541
333
 
542
334
  // We need the sandbox name to construct preview URLs
543
335
  if (!this.sandboxName) {
544
- 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
+ );
545
339
  }
546
340
 
547
- return response.ports.map(port => ({
341
+ return response.ports.map((port) => ({
548
342
  url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
549
343
  port: port.port,
550
344
  name: port.name,
@@ -552,27 +346,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
552
346
  }));
553
347
  }
554
348
 
555
-
556
- private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
349
+ private constructPreviewUrl(
350
+ port: number,
351
+ sandboxId: string,
352
+ hostname: string
353
+ ): string {
557
354
  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.`);
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
+ );
564
367
  }
565
368
 
566
369
  let sanitizedSandboxId: string;
567
370
  try {
568
371
  sanitizedSandboxId = sanitizeSandboxId(sandboxId);
569
372
  } 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');
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
+ );
576
383
  throw error;
577
384
  }
578
385
 
@@ -580,8 +387,8 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
580
387
 
581
388
  if (isLocalhost) {
582
389
  // Unified subdomain approach for localhost (RFC 6761)
583
- const [host, portStr] = hostname.split(':');
584
- const mainPort = portStr || '80';
390
+ const [host, portStr] = hostname.split(":");
391
+ const mainPort = portStr || "80";
585
392
 
586
393
  // Use URL constructor for safe URL building
587
394
  try {
@@ -592,23 +399,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
592
399
 
593
400
  const finalUrl = baseUrl.toString();
594
401
 
595
- logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {
596
- port,
597
- sandboxId: sanitizedSandboxId,
598
- hostname,
599
- resultUrl: finalUrl,
600
- environment: 'localhost'
601
- }, '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
+ );
602
413
 
603
414
  return finalUrl;
604
415
  } 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'}`);
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
+ );
612
431
  }
613
432
  }
614
433
 
@@ -624,23 +443,314 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
624
443
 
625
444
  const finalUrl = baseUrl.toString();
626
445
 
627
- logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {
628
- port,
629
- sandboxId: sanitizedSandboxId,
630
- hostname,
631
- resultUrl: finalUrl,
632
- environment: 'production'
633
- }, '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
+ );
634
457
 
635
458
  return finalUrl;
636
459
  } 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'}`);
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
+ );
644
475
  }
645
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
+ }
646
756
  }