@cloudflare/sandbox 0.0.0-fd5ec7f → 0.0.0-feafd32

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
-
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
162
 
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,39 @@ 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);
260
+ }
261
+
262
+ async readFile(path: string, options: { encoding?: string } = {}) {
263
+ const session = await this.ensureDefaultSession();
264
+ return session.readFile(path, options);
502
265
  }
503
266
 
504
- async readFile(
267
+ async listFiles(
505
268
  path: string,
506
- options: { encoding?: string } = {}
269
+ options: {
270
+ recursive?: boolean;
271
+ includeHidden?: boolean;
272
+ } = {}
507
273
  ) {
508
- return this.client.readFile(path, options.encoding);
274
+ const session = await this.ensureDefaultSession();
275
+ return session.listFiles(path, options);
509
276
  }
510
277
 
511
278
  async exposePort(port: number, options: { name?: string; hostname: string }) {
@@ -513,10 +280,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
513
280
 
514
281
  // We need the sandbox name to construct preview URLs
515
282
  if (!this.sandboxName) {
516
- throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
283
+ throw new Error(
284
+ "Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
285
+ );
517
286
  }
518
287
 
519
- const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname);
288
+ const url = this.constructPreviewUrl(
289
+ port,
290
+ this.sandboxName,
291
+ options.hostname
292
+ );
520
293
 
521
294
  return {
522
295
  url,
@@ -527,17 +300,27 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
527
300
 
528
301
  async unexposePort(port: number) {
529
302
  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.`);
303
+ logSecurityEvent(
304
+ "INVALID_PORT_UNEXPOSE",
305
+ {
306
+ port,
307
+ },
308
+ "high"
309
+ );
310
+ throw new SecurityError(
311
+ `Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
312
+ );
534
313
  }
535
314
 
536
315
  await this.client.unexposePort(port);
537
316
 
538
- logSecurityEvent('PORT_UNEXPOSED', {
539
- port
540
- }, 'low');
317
+ logSecurityEvent(
318
+ "PORT_UNEXPOSED",
319
+ {
320
+ port,
321
+ },
322
+ "low"
323
+ );
541
324
  }
542
325
 
543
326
  async getExposedPorts(hostname: string) {
@@ -545,10 +328,12 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
545
328
 
546
329
  // We need the sandbox name to construct preview URLs
547
330
  if (!this.sandboxName) {
548
- throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
331
+ throw new Error(
332
+ "Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
333
+ );
549
334
  }
550
335
 
551
- return response.ports.map(port => ({
336
+ return response.ports.map((port) => ({
552
337
  url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
553
338
  port: port.port,
554
339
  name: port.name,
@@ -556,27 +341,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
556
341
  }));
557
342
  }
558
343
 
559
-
560
- private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
344
+ private constructPreviewUrl(
345
+ port: number,
346
+ sandboxId: string,
347
+ hostname: string
348
+ ): string {
561
349
  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.`);
350
+ logSecurityEvent(
351
+ "INVALID_PORT_REJECTED",
352
+ {
353
+ port,
354
+ sandboxId,
355
+ hostname,
356
+ },
357
+ "high"
358
+ );
359
+ throw new SecurityError(
360
+ `Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
361
+ );
568
362
  }
569
363
 
570
364
  let sanitizedSandboxId: string;
571
365
  try {
572
366
  sanitizedSandboxId = sanitizeSandboxId(sandboxId);
573
367
  } 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');
368
+ logSecurityEvent(
369
+ "INVALID_SANDBOX_ID_REJECTED",
370
+ {
371
+ sandboxId,
372
+ port,
373
+ hostname,
374
+ error: error instanceof Error ? error.message : "Unknown error",
375
+ },
376
+ "high"
377
+ );
580
378
  throw error;
581
379
  }
582
380
 
@@ -584,8 +382,8 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
584
382
 
585
383
  if (isLocalhost) {
586
384
  // Unified subdomain approach for localhost (RFC 6761)
587
- const [host, portStr] = hostname.split(':');
588
- const mainPort = portStr || '80';
385
+ const [host, portStr] = hostname.split(":");
386
+ const mainPort = portStr || "80";
589
387
 
590
388
  // Use URL constructor for safe URL building
591
389
  try {
@@ -596,23 +394,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
596
394
 
597
395
  const finalUrl = baseUrl.toString();
598
396
 
599
- logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {
600
- port,
601
- sandboxId: sanitizedSandboxId,
602
- hostname,
603
- resultUrl: finalUrl,
604
- environment: 'localhost'
605
- }, 'low');
397
+ logSecurityEvent(
398
+ "PREVIEW_URL_CONSTRUCTED",
399
+ {
400
+ port,
401
+ sandboxId: sanitizedSandboxId,
402
+ hostname,
403
+ resultUrl: finalUrl,
404
+ environment: "localhost",
405
+ },
406
+ "low"
407
+ );
606
408
 
607
409
  return finalUrl;
608
410
  } 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'}`);
411
+ logSecurityEvent(
412
+ "URL_CONSTRUCTION_FAILED",
413
+ {
414
+ port,
415
+ sandboxId: sanitizedSandboxId,
416
+ hostname,
417
+ error: error instanceof Error ? error.message : "Unknown error",
418
+ },
419
+ "high"
420
+ );
421
+ throw new SecurityError(
422
+ `Failed to construct preview URL: ${
423
+ error instanceof Error ? error.message : "Unknown error"
424
+ }`
425
+ );
616
426
  }
617
427
  }
618
428
 
@@ -628,23 +438,310 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
628
438
 
629
439
  const finalUrl = baseUrl.toString();
630
440
 
631
- logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {
632
- port,
633
- sandboxId: sanitizedSandboxId,
634
- hostname,
635
- resultUrl: finalUrl,
636
- environment: 'production'
637
- }, 'low');
441
+ logSecurityEvent(
442
+ "PREVIEW_URL_CONSTRUCTED",
443
+ {
444
+ port,
445
+ sandboxId: sanitizedSandboxId,
446
+ hostname,
447
+ resultUrl: finalUrl,
448
+ environment: "production",
449
+ },
450
+ "low"
451
+ );
638
452
 
639
453
  return finalUrl;
640
454
  } 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'}`);
455
+ logSecurityEvent(
456
+ "URL_CONSTRUCTION_FAILED",
457
+ {
458
+ port,
459
+ sandboxId: sanitizedSandboxId,
460
+ hostname,
461
+ error: error instanceof Error ? error.message : "Unknown error",
462
+ },
463
+ "high"
464
+ );
465
+ throw new SecurityError(
466
+ `Failed to construct preview URL: ${
467
+ error instanceof Error ? error.message : "Unknown error"
468
+ }`
469
+ );
648
470
  }
649
471
  }
472
+
473
+ // Code Interpreter Methods
474
+
475
+ /**
476
+ * Create a new code execution context
477
+ */
478
+ async createCodeContext(
479
+ options?: CreateContextOptions
480
+ ): Promise<CodeContext> {
481
+ return this.codeInterpreter.createCodeContext(options);
482
+ }
483
+
484
+ /**
485
+ * Run code with streaming callbacks
486
+ */
487
+ async runCode(
488
+ code: string,
489
+ options?: RunCodeOptions
490
+ ): Promise<ExecutionResult> {
491
+ const execution = await this.codeInterpreter.runCode(code, options);
492
+ // Convert to plain object for RPC serialization
493
+ return execution.toJSON();
494
+ }
495
+
496
+ /**
497
+ * Run code and return a streaming response
498
+ */
499
+ async runCodeStream(
500
+ code: string,
501
+ options?: RunCodeOptions
502
+ ): Promise<ReadableStream> {
503
+ return this.codeInterpreter.runCodeStream(code, options);
504
+ }
505
+
506
+ /**
507
+ * List all code contexts
508
+ */
509
+ async listCodeContexts(): Promise<CodeContext[]> {
510
+ return this.codeInterpreter.listCodeContexts();
511
+ }
512
+
513
+ /**
514
+ * Delete a code context
515
+ */
516
+ async deleteCodeContext(contextId: string): Promise<void> {
517
+ return this.codeInterpreter.deleteCodeContext(contextId);
518
+ }
519
+
520
+ // ============================================================================
521
+ // Session Management (Simple Isolation)
522
+ // ============================================================================
523
+
524
+ /**
525
+ * Create a new execution session with isolation
526
+ * Returns a session object with exec() method
527
+ */
528
+
529
+ async createSession(options: {
530
+ id?: string;
531
+ env?: Record<string, string>;
532
+ cwd?: string;
533
+ isolation?: boolean;
534
+ }): Promise<ExecutionSession> {
535
+ const sessionId = options.id || `session-${Date.now()}`;
536
+
537
+ await this.client.createSession({
538
+ id: sessionId,
539
+ env: options.env,
540
+ cwd: options.cwd,
541
+ isolation: options.isolation
542
+ });
543
+ // Return comprehensive ExecutionSession object that implements all ISandbox methods
544
+ return {
545
+ id: sessionId,
546
+
547
+ // Command execution - clean method names
548
+ exec: async (command: string, options?: ExecOptions) => {
549
+ const result = await this.client.exec(sessionId, command);
550
+ return {
551
+ ...result,
552
+ command,
553
+ duration: 0,
554
+ timestamp: new Date().toISOString()
555
+ };
556
+ },
557
+
558
+ execStream: async (command: string, options?: StreamOptions) => {
559
+ return await this.client.execStream(sessionId, command);
560
+ },
561
+
562
+ // Process management - route to session-aware methods
563
+ startProcess: async (command: string, options?: ProcessOptions) => {
564
+ // Use session-specific process management
565
+ const response = await this.client.startProcess(command, sessionId, {
566
+ processId: options?.processId,
567
+ timeout: options?.timeout,
568
+ env: options?.env,
569
+ cwd: options?.cwd,
570
+ encoding: options?.encoding,
571
+ autoCleanup: options?.autoCleanup,
572
+ });
573
+
574
+ // Convert response to Process object with bound methods
575
+ const process = response.process;
576
+ return {
577
+ id: process.id,
578
+ pid: process.pid,
579
+ command: process.command,
580
+ status: process.status as ProcessStatus,
581
+ startTime: new Date(process.startTime),
582
+ endTime: process.endTime ? new Date(process.endTime) : undefined,
583
+ exitCode: process.exitCode ?? undefined,
584
+ kill: async (signal?: string) => {
585
+ await this.client.killProcess(process.id);
586
+ },
587
+ getStatus: async () => {
588
+ const resp = await this.client.getProcess(process.id);
589
+ return resp.process?.status as ProcessStatus || "error";
590
+ },
591
+ getLogs: async () => {
592
+ return await this.client.getProcessLogs(process.id);
593
+ },
594
+ };
595
+ },
596
+
597
+ listProcesses: async () => {
598
+ // Get processes for this specific session
599
+ const response = await this.client.listProcesses(sessionId);
600
+
601
+ // Convert to Process objects with bound methods
602
+ return response.processes.map(p => ({
603
+ id: p.id,
604
+ pid: p.pid,
605
+ command: p.command,
606
+ status: p.status as ProcessStatus,
607
+ startTime: new Date(p.startTime),
608
+ endTime: p.endTime ? new Date(p.endTime) : undefined,
609
+ exitCode: p.exitCode ?? undefined,
610
+ kill: async (signal?: string) => {
611
+ await this.client.killProcess(p.id);
612
+ },
613
+ getStatus: async () => {
614
+ const processResp = await this.client.getProcess(p.id);
615
+ return processResp.process?.status as ProcessStatus || "error";
616
+ },
617
+ getLogs: async () => {
618
+ return this.client.getProcessLogs(p.id);
619
+ },
620
+ }));
621
+ },
622
+
623
+ getProcess: async (id: string) => {
624
+ const response = await this.client.getProcess(id);
625
+ if (!response.process) return null;
626
+
627
+ const p = response.process;
628
+ return {
629
+ id: p.id,
630
+ pid: p.pid,
631
+ command: p.command,
632
+ status: p.status as ProcessStatus,
633
+ startTime: new Date(p.startTime),
634
+ endTime: p.endTime ? new Date(p.endTime) : undefined,
635
+ exitCode: p.exitCode ?? undefined,
636
+ kill: async (signal?: string) => {
637
+ await this.client.killProcess(p.id);
638
+ },
639
+ getStatus: async () => {
640
+ const processResp = await this.client.getProcess(p.id);
641
+ return processResp.process?.status as ProcessStatus || "error";
642
+ },
643
+ getLogs: async () => {
644
+ return this.client.getProcessLogs(p.id);
645
+ },
646
+ };
647
+ },
648
+
649
+ killProcess: async (id: string, signal?: string) => {
650
+ await this.client.killProcess(id);
651
+ },
652
+
653
+ killAllProcesses: async () => {
654
+ // Kill all processes for this specific session
655
+ const response = await this.client.killAllProcesses(sessionId);
656
+ return response.killedCount;
657
+ },
658
+
659
+ streamProcessLogs: async (processId: string, options?: { signal?: AbortSignal }) => {
660
+ return await this.client.streamProcessLogs(processId, options);
661
+ },
662
+
663
+ getProcessLogs: async (id: string) => {
664
+ return await this.client.getProcessLogs(id);
665
+ },
666
+
667
+ cleanupCompletedProcesses: async () => {
668
+ // This would need a new endpoint to cleanup processes for a specific session
669
+ // For now, return 0 as no cleanup is performed
670
+ return 0;
671
+ },
672
+
673
+ // File operations - clean method names (no "InSession" suffix)
674
+ writeFile: async (path: string, content: string, options?: { encoding?: string }) => {
675
+ return await this.client.writeFile(path, content, options?.encoding, sessionId);
676
+ },
677
+
678
+ readFile: async (path: string, options?: { encoding?: string }) => {
679
+ return await this.client.readFile(path, options?.encoding, sessionId);
680
+ },
681
+
682
+ mkdir: async (path: string, options?: { recursive?: boolean }) => {
683
+ return await this.client.mkdir(path, options?.recursive, sessionId);
684
+ },
685
+
686
+ deleteFile: async (path: string) => {
687
+ return await this.client.deleteFile(path, sessionId);
688
+ },
689
+
690
+ renameFile: async (oldPath: string, newPath: string) => {
691
+ return await this.client.renameFile(oldPath, newPath, sessionId);
692
+ },
693
+
694
+ moveFile: async (sourcePath: string, destinationPath: string) => {
695
+ return await this.client.moveFile(sourcePath, destinationPath, sessionId);
696
+ },
697
+
698
+ listFiles: async (path: string, options?: { recursive?: boolean; includeHidden?: boolean }) => {
699
+ return await this.client.listFiles(path, sessionId, options);
700
+ },
701
+
702
+ gitCheckout: async (repoUrl: string, options?: { branch?: string; targetDir?: string }) => {
703
+ return await this.client.gitCheckout(repoUrl, sessionId, options?.branch, options?.targetDir);
704
+ },
705
+
706
+ // Port management
707
+ exposePort: async (port: number, options: { name?: string; hostname: string }) => {
708
+ return await this.exposePort(port, options);
709
+ },
710
+
711
+ unexposePort: async (port: number) => {
712
+ return await this.unexposePort(port);
713
+ },
714
+
715
+ getExposedPorts: async (hostname: string) => {
716
+ return await this.getExposedPorts(hostname);
717
+ },
718
+
719
+ // Environment management
720
+ setEnvVars: async (envVars: Record<string, string>) => {
721
+ // TODO: Implement session-specific environment updates
722
+ console.log(`[Session ${sessionId}] Environment variables update not yet implemented`);
723
+ },
724
+
725
+ // Code Interpreter API
726
+ createCodeContext: async (options?: any) => {
727
+ return await this.createCodeContext(options);
728
+ },
729
+
730
+ runCode: async (code: string, options?: any) => {
731
+ return await this.runCode(code, options);
732
+ },
733
+
734
+ runCodeStream: async (code: string, options?: any) => {
735
+ return await this.runCodeStream(code, options);
736
+ },
737
+
738
+ listCodeContexts: async () => {
739
+ return await this.listCodeContexts();
740
+ },
741
+
742
+ deleteCodeContext: async (contextId: string) => {
743
+ return await this.deleteCodeContext(contextId);
744
+ }
745
+ };
746
+ }
650
747
  }