@cloudflare/sandbox 0.0.0-46eb4e6 → 0.0.0-485cf61

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +0 -6
  2. package/Dockerfile +82 -18
  3. package/README.md +89 -824
  4. package/dist/chunk-3NEP4CNV.js +99 -0
  5. package/dist/chunk-3NEP4CNV.js.map +1 -0
  6. package/dist/chunk-6IYG2RIN.js +117 -0
  7. package/dist/chunk-6IYG2RIN.js.map +1 -0
  8. package/dist/chunk-HB44YO2A.js +2331 -0
  9. package/dist/chunk-HB44YO2A.js.map +1 -0
  10. package/dist/chunk-KPVMMMIP.js +105 -0
  11. package/dist/chunk-KPVMMMIP.js.map +1 -0
  12. package/dist/chunk-NNGBXDMY.js +89 -0
  13. package/dist/chunk-NNGBXDMY.js.map +1 -0
  14. package/dist/file-stream.d.ts +43 -0
  15. package/dist/file-stream.js +9 -0
  16. package/dist/file-stream.js.map +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +55 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/interpreter.d.ts +33 -0
  21. package/dist/interpreter.js +8 -0
  22. package/dist/interpreter.js.map +1 -0
  23. package/dist/request-handler.d.ts +18 -0
  24. package/dist/request-handler.js +12 -0
  25. package/dist/request-handler.js.map +1 -0
  26. package/dist/sandbox-CtlKjZwf.d.ts +583 -0
  27. package/dist/sandbox.d.ts +4 -0
  28. package/dist/sandbox.js +12 -0
  29. package/dist/sandbox.js.map +1 -0
  30. package/dist/security.d.ts +35 -0
  31. package/dist/security.js +15 -0
  32. package/dist/security.js.map +1 -0
  33. package/dist/sse-parser.d.ts +28 -0
  34. package/dist/sse-parser.js +11 -0
  35. package/dist/sse-parser.js.map +1 -0
  36. package/package.json +11 -5
  37. package/src/clients/base-client.ts +297 -0
  38. package/src/clients/command-client.ts +118 -0
  39. package/src/clients/file-client.ts +272 -0
  40. package/src/clients/git-client.ts +95 -0
  41. package/src/clients/index.ts +63 -0
  42. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +151 -171
  43. package/src/clients/port-client.ts +108 -0
  44. package/src/clients/process-client.ts +180 -0
  45. package/src/clients/sandbox-client.ts +41 -0
  46. package/src/clients/types.ts +81 -0
  47. package/src/clients/utility-client.ts +97 -0
  48. package/src/errors/adapter.ts +180 -0
  49. package/src/errors/classes.ts +469 -0
  50. package/src/errors/index.ts +105 -0
  51. package/src/file-stream.ts +119 -117
  52. package/src/index.ts +81 -69
  53. package/src/interpreter.ts +17 -8
  54. package/src/request-handler.ts +61 -7
  55. package/src/sandbox.ts +698 -495
  56. package/src/security.ts +20 -0
  57. package/startup.sh +7 -0
  58. package/tests/base-client.test.ts +328 -0
  59. package/tests/command-client.test.ts +407 -0
  60. package/tests/file-client.test.ts +643 -0
  61. package/tests/file-stream.test.ts +306 -0
  62. package/tests/git-client.test.ts +328 -0
  63. package/tests/port-client.test.ts +301 -0
  64. package/tests/process-client.test.ts +658 -0
  65. package/tests/sandbox.test.ts +465 -0
  66. package/tests/sse-parser.test.ts +291 -0
  67. package/tests/utility-client.test.ts +266 -0
  68. package/tests/wrangler.jsonc +35 -0
  69. package/tsconfig.json +9 -1
  70. package/vitest.config.ts +31 -0
  71. package/container_src/bun.lock +0 -76
  72. package/container_src/circuit-breaker.ts +0 -121
  73. package/container_src/control-process.ts +0 -784
  74. package/container_src/handler/exec.ts +0 -185
  75. package/container_src/handler/file.ts +0 -457
  76. package/container_src/handler/git.ts +0 -130
  77. package/container_src/handler/ports.ts +0 -314
  78. package/container_src/handler/process.ts +0 -568
  79. package/container_src/handler/session.ts +0 -92
  80. package/container_src/index.ts +0 -600
  81. package/container_src/interpreter-service.ts +0 -276
  82. package/container_src/isolation.ts +0 -1213
  83. package/container_src/mime-processor.ts +0 -255
  84. package/container_src/package.json +0 -18
  85. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  86. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  87. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  88. package/container_src/runtime/process-pool.ts +0 -464
  89. package/container_src/shell-escape.ts +0 -42
  90. package/container_src/startup.sh +0 -11
  91. package/container_src/types.ts +0 -131
  92. package/src/client.ts +0 -1048
  93. package/src/errors.ts +0 -219
  94. package/src/interpreter-types.ts +0 -390
  95. package/src/types.ts +0 -571
package/src/sandbox.ts CHANGED
@@ -1,33 +1,33 @@
1
+ import type { DurableObject } from 'cloudflare:workers';
1
2
  import { Container, getContainer } from "@cloudflare/containers";
2
- import { CodeInterpreter } from "./interpreter";
3
- import { InterpreterClient } from "./interpreter-client";
4
3
  import type {
5
4
  CodeContext,
6
5
  CreateContextOptions,
6
+ ExecEvent,
7
+ ExecOptions,
8
+ ExecResult,
7
9
  ExecutionResult,
10
+ ExecutionSession,
11
+ ISandbox,
12
+ Process,
13
+ ProcessOptions,
14
+ ProcessStatus,
8
15
  RunCodeOptions,
9
- } from "./interpreter-types";
16
+ SessionOptions,
17
+ StreamOptions
18
+ } from "@repo/shared";
19
+ import { type ExecuteResponse, SandboxClient } from "./clients";
20
+ import type { ErrorResponse } from './errors';
21
+ import { CustomDomainRequiredError, ErrorCode } from './errors';
22
+ import { CodeInterpreter } from "./interpreter";
10
23
  import { isLocalhostPattern } from "./request-handler";
11
24
  import {
12
25
  logSecurityEvent,
13
26
  SecurityError,
14
27
  sanitizeSandboxId,
15
- validatePort,
28
+ validatePort
16
29
  } from "./security";
17
30
  import { parseSSEStream } from "./sse-parser";
18
- import type {
19
- ExecEvent,
20
- ExecOptions,
21
- ExecResult,
22
- ExecuteResponse,
23
- ExecutionSession,
24
- ISandbox,
25
- Process,
26
- ProcessOptions,
27
- ProcessStatus,
28
- StreamOptions,
29
- } from "./types";
30
- import { ProcessNotFoundError, SandboxError } from "./types";
31
31
 
32
32
  export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
33
33
  const stub = getContainer(ns, id);
@@ -40,40 +40,44 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
40
40
 
41
41
  export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
42
42
  defaultPort = 3000; // Default port for the container's Bun server
43
- sleepAfter = "20m"; // Keep container warm for 20 minutes to avoid cold starts
44
- client: InterpreterClient;
45
- private sandboxName: string | null = null;
43
+ sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
44
+
45
+ client: SandboxClient;
46
46
  private codeInterpreter: CodeInterpreter;
47
- private defaultSession: ExecutionSession | null = null;
47
+ private sandboxName: string | null = null;
48
+ private portTokens: Map<number, string> = new Map();
49
+ private defaultSession: string | null = null;
50
+ envVars: Record<string, string> = {};
48
51
 
49
- constructor(ctx: DurableObjectState<{}>, env: Env) {
52
+ constructor(ctx: DurableObject['ctx'], env: Env) {
50
53
  super(ctx, env);
51
- this.client = new InterpreterClient({
54
+ this.client = new SandboxClient({
52
55
  onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {
53
56
  console.log(
54
57
  `[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
55
58
  );
56
59
  },
57
- onCommandStart: (command) => {
58
- console.log(`[Container] Command started: ${command}`);
59
- },
60
60
  onError: (error, _command) => {
61
61
  console.error(`[Container] Command error: ${error}`);
62
62
  },
63
- onOutput: (stream, data, _command) => {
64
- console.log(`[Container] [${stream}] ${data}`);
65
- },
66
63
  port: 3000, // Control plane port
67
64
  stub: this,
68
65
  });
69
66
 
70
- // Initialize code interpreter
67
+ // Initialize code interpreter - pass 'this' after client is ready
68
+ // The CodeInterpreter extracts client.interpreter from the sandbox
71
69
  this.codeInterpreter = new CodeInterpreter(this);
72
70
 
73
- // Load the sandbox name from storage on initialization
71
+ // Load the sandbox name and port tokens from storage on initialization
74
72
  this.ctx.blockConcurrencyWhile(async () => {
75
- this.sandboxName =
76
- (await this.ctx.storage.get<string>("sandboxName")) || null;
73
+ this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
74
+ const storedTokens = await this.ctx.storage.get<Record<string, string>>('portTokens') || {};
75
+
76
+ // Convert stored tokens back to Map
77
+ this.portTokens = new Map();
78
+ for (const [portStr, token] of Object.entries(storedTokens)) {
79
+ this.portTokens.set(parseInt(portStr, 10), token);
80
+ }
77
81
  });
78
82
  }
79
83
 
@@ -81,22 +85,43 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
81
85
  async setSandboxName(name: string): Promise<void> {
82
86
  if (!this.sandboxName) {
83
87
  this.sandboxName = name;
84
- await this.ctx.storage.put("sandboxName", name);
88
+ await this.ctx.storage.put('sandboxName', name);
85
89
  console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
86
90
  }
87
91
  }
88
92
 
89
93
  // RPC method to set environment variables
90
94
  async setEnvVars(envVars: Record<string, string>): Promise<void> {
95
+ // Update local state for new sessions
91
96
  this.envVars = { ...this.envVars, ...envVars };
92
- console.log(`[Sandbox] Updated environment variables`);
93
-
94
- // If we have a default session, update its environment too
97
+
98
+ // If default session already exists, update it directly
95
99
  if (this.defaultSession) {
96
- await this.defaultSession.setEnvVars(envVars);
100
+ // Set environment variables by executing export commands in the existing session
101
+ for (const [key, value] of Object.entries(envVars)) {
102
+ const escapedValue = value.replace(/'/g, "'\\''");
103
+ const exportCommand = `export ${key}='${escapedValue}'`;
104
+
105
+ const result = await this.client.commands.execute(exportCommand, this.defaultSession);
106
+
107
+ if (result.exitCode !== 0) {
108
+ throw new Error(`Failed to set ${key}: ${result.stderr || 'Unknown error'}`);
109
+ }
110
+ }
111
+ console.log(`[Sandbox] Updated environment variables in existing session`);
112
+ } else {
113
+ console.log(`[Sandbox] Updated environment variables (will be set when session is created)`);
97
114
  }
98
115
  }
99
116
 
117
+ /**
118
+ * Cleanup and destroy the sandbox container
119
+ */
120
+ override async destroy(): Promise<void> {
121
+ console.log(`[Sandbox] Cleanup requested, destroying container`);
122
+ await super.destroy();
123
+ }
124
+
100
125
  override onStart() {
101
126
  console.log("Sandbox successfully started");
102
127
  }
@@ -114,10 +139,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
114
139
  const url = new URL(request.url);
115
140
 
116
141
  // Capture and store the sandbox name from the header if present
117
- if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
118
- const name = request.headers.get("X-Sandbox-Name")!;
142
+ if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {
143
+ const name = request.headers.get('X-Sandbox-Name')!;
119
144
  this.sandboxName = name;
120
- await this.ctx.storage.put("sandboxName", name);
145
+ await this.ctx.storage.put('sandboxName', name);
121
146
  console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
122
147
  }
123
148
 
@@ -132,11 +157,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
132
157
  // Extract port from proxy requests (e.g., /proxy/8080/*)
133
158
  const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
134
159
  if (proxyMatch) {
135
- return parseInt(proxyMatch[1]);
136
- }
137
-
138
- if (url.port) {
139
- return parseInt(url.port);
160
+ return parseInt(proxyMatch[1], 10);
140
161
  }
141
162
 
142
163
  // All other requests go to control plane on port 3000
@@ -144,157 +165,471 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
144
165
  return 3000;
145
166
  }
146
167
 
147
- // Helper to ensure default session is initialized
148
- private async ensureDefaultSession(): Promise<ExecutionSession> {
168
+ /**
169
+ * Ensure default session exists - lazy initialization
170
+ * This is called automatically by all public methods that need a session
171
+ */
172
+ private async ensureDefaultSession(): Promise<string> {
149
173
  if (!this.defaultSession) {
150
174
  const sessionId = `sandbox-${this.sandboxName || 'default'}`;
151
- this.defaultSession = await this.createSession({
175
+
176
+ // Create session in container
177
+ await this.client.utils.createSession({
152
178
  id: sessionId,
153
179
  env: this.envVars || {},
154
180
  cwd: '/workspace',
155
- isolation: true
156
181
  });
182
+
183
+ this.defaultSession = sessionId;
157
184
  console.log(`[Sandbox] Default session initialized: ${sessionId}`);
158
185
  }
159
186
  return this.defaultSession;
160
187
  }
161
188
 
162
-
189
+ // Enhanced exec method - always returns ExecResult with optional streaming
190
+ // This replaces the old exec method to match ISandbox interface
163
191
  async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
164
192
  const session = await this.ensureDefaultSession();
165
- return session.exec(command, options);
193
+ return this.execWithSession(command, session, options);
166
194
  }
167
195
 
168
- async startProcess(
196
+ /**
197
+ * Internal session-aware exec implementation
198
+ * Used by both public exec() and session wrappers
199
+ */
200
+ private async execWithSession(
169
201
  command: string,
170
- options?: ProcessOptions
171
- ): Promise<Process> {
172
- const session = await this.ensureDefaultSession();
173
- return session.startProcess(command, options);
202
+ sessionId: string,
203
+ options?: ExecOptions
204
+ ): Promise<ExecResult> {
205
+ const startTime = Date.now();
206
+ const timestamp = new Date().toISOString();
207
+
208
+ // Handle timeout
209
+ let timeoutId: NodeJS.Timeout | undefined;
210
+
211
+ try {
212
+ // Handle cancellation
213
+ if (options?.signal?.aborted) {
214
+ throw new Error('Operation was aborted');
215
+ }
216
+
217
+ let result: ExecResult;
218
+
219
+ if (options?.stream && options?.onOutput) {
220
+ // Streaming with callbacks - we need to collect the final result
221
+ result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp);
222
+ } else {
223
+ // Regular execution with session
224
+ const response = await this.client.commands.execute(command, sessionId);
225
+
226
+ const duration = Date.now() - startTime;
227
+ result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
228
+ }
229
+
230
+ // Call completion callback if provided
231
+ if (options?.onComplete) {
232
+ options.onComplete(result);
233
+ }
234
+
235
+ return result;
236
+ } catch (error) {
237
+ if (options?.onError && error instanceof Error) {
238
+ options.onError(error);
239
+ }
240
+ throw error;
241
+ } finally {
242
+ if (timeoutId) {
243
+ clearTimeout(timeoutId);
244
+ }
245
+ }
174
246
  }
175
247
 
176
- async listProcesses(): Promise<Process[]> {
177
- const session = await this.ensureDefaultSession();
178
- return session.listProcesses();
248
+ private async executeWithStreaming(
249
+ command: string,
250
+ sessionId: string,
251
+ options: ExecOptions,
252
+ startTime: number,
253
+ timestamp: string
254
+ ): Promise<ExecResult> {
255
+ let stdout = '';
256
+ let stderr = '';
257
+
258
+ try {
259
+ const stream = await this.client.commands.executeStream(command, sessionId);
260
+
261
+ for await (const event of parseSSEStream<ExecEvent>(stream)) {
262
+ // Check for cancellation
263
+ if (options.signal?.aborted) {
264
+ throw new Error('Operation was aborted');
265
+ }
266
+
267
+ switch (event.type) {
268
+ case 'stdout':
269
+ case 'stderr':
270
+ if (event.data) {
271
+ // Update accumulated output
272
+ if (event.type === 'stdout') stdout += event.data;
273
+ if (event.type === 'stderr') stderr += event.data;
274
+
275
+ // Call user's callback
276
+ if (options.onOutput) {
277
+ options.onOutput(event.type, event.data);
278
+ }
279
+ }
280
+ break;
281
+
282
+ case 'complete': {
283
+ // Use result from complete event if available
284
+ const duration = Date.now() - startTime;
285
+ return {
286
+ success: (event.exitCode ?? 0) === 0,
287
+ exitCode: event.exitCode ?? 0,
288
+ stdout,
289
+ stderr,
290
+ command,
291
+ duration,
292
+ timestamp,
293
+ sessionId
294
+ };
295
+ }
296
+
297
+ case 'error':
298
+ throw new Error(event.data || 'Command execution failed');
299
+ }
300
+ }
301
+
302
+ // If we get here without a complete event, something went wrong
303
+ throw new Error('Stream ended without completion event');
304
+
305
+ } catch (error) {
306
+ if (options.signal?.aborted) {
307
+ throw new Error('Operation was aborted');
308
+ }
309
+ throw error;
310
+ }
179
311
  }
180
312
 
181
- async getProcess(id: string): Promise<Process | null> {
182
- const session = await this.ensureDefaultSession();
183
- return session.getProcess(id);
313
+ private mapExecuteResponseToExecResult(
314
+ response: ExecuteResponse,
315
+ duration: number,
316
+ sessionId?: string
317
+ ): ExecResult {
318
+ return {
319
+ success: response.success,
320
+ exitCode: response.exitCode,
321
+ stdout: response.stdout,
322
+ stderr: response.stderr,
323
+ command: response.command,
324
+ duration,
325
+ timestamp: response.timestamp,
326
+ sessionId
327
+ };
184
328
  }
185
329
 
186
- async killProcess(id: string, signal?: string): Promise<void> {
187
- const session = await this.ensureDefaultSession();
188
- return session.killProcess(id, signal);
330
+ /**
331
+ * Create a Process domain object from HTTP client DTO
332
+ * Centralizes process object creation with bound methods
333
+ * This eliminates duplication across startProcess, listProcesses, getProcess, and session wrappers
334
+ */
335
+ private createProcessFromDTO(
336
+ data: {
337
+ id: string;
338
+ pid?: number;
339
+ command: string;
340
+ status: ProcessStatus;
341
+ startTime: string | Date;
342
+ endTime?: string | Date;
343
+ exitCode?: number;
344
+ },
345
+ sessionId: string
346
+ ): Process {
347
+ return {
348
+ id: data.id,
349
+ pid: data.pid,
350
+ command: data.command,
351
+ status: data.status,
352
+ startTime: typeof data.startTime === 'string' ? new Date(data.startTime) : data.startTime,
353
+ endTime: data.endTime ? (typeof data.endTime === 'string' ? new Date(data.endTime) : data.endTime) : undefined,
354
+ exitCode: data.exitCode,
355
+ sessionId,
356
+
357
+ kill: async (signal?: string) => {
358
+ await this.killProcess(data.id, signal);
359
+ },
360
+
361
+ getStatus: async () => {
362
+ const current = await this.getProcess(data.id);
363
+ return current?.status || 'error';
364
+ },
365
+
366
+ getLogs: async () => {
367
+ const logs = await this.getProcessLogs(data.id);
368
+ return { stdout: logs.stdout, stderr: logs.stderr };
369
+ }
370
+ };
189
371
  }
190
372
 
191
- async killAllProcesses(): Promise<number> {
192
- const session = await this.ensureDefaultSession();
193
- return session.killAllProcesses();
373
+
374
+ // Background process management
375
+ async startProcess(command: string, options?: ProcessOptions, sessionId?: string): Promise<Process> {
376
+ // Use the new HttpClient method to start the process
377
+ try {
378
+ const session = sessionId ?? await this.ensureDefaultSession();
379
+ const response = await this.client.processes.startProcess(command, session, {
380
+ processId: options?.processId
381
+ });
382
+
383
+ const processObj = this.createProcessFromDTO({
384
+ id: response.processId,
385
+ pid: response.pid,
386
+ command: response.command,
387
+ status: 'running' as ProcessStatus,
388
+ startTime: new Date(),
389
+ endTime: undefined,
390
+ exitCode: undefined
391
+ }, session);
392
+
393
+ // Call onStart callback if provided
394
+ if (options?.onStart) {
395
+ options.onStart(processObj);
396
+ }
397
+
398
+ return processObj;
399
+
400
+ } catch (error) {
401
+ if (options?.onError && error instanceof Error) {
402
+ options.onError(error);
403
+ }
404
+
405
+ throw error;
406
+ }
194
407
  }
195
408
 
196
- async cleanupCompletedProcesses(): Promise<number> {
197
- const session = await this.ensureDefaultSession();
198
- return session.cleanupCompletedProcesses();
409
+ async listProcesses(sessionId?: string): Promise<Process[]> {
410
+ const session = sessionId ?? await this.ensureDefaultSession();
411
+ const response = await this.client.processes.listProcesses();
412
+
413
+ return response.processes.map(processData =>
414
+ this.createProcessFromDTO({
415
+ id: processData.id,
416
+ pid: processData.pid,
417
+ command: processData.command,
418
+ status: processData.status,
419
+ startTime: processData.startTime,
420
+ endTime: processData.endTime,
421
+ exitCode: processData.exitCode
422
+ }, session)
423
+ );
199
424
  }
200
425
 
201
- async getProcessLogs(
202
- id: string
203
- ): Promise<{ stdout: string; stderr: string }> {
204
- const session = await this.ensureDefaultSession();
205
- return session.getProcessLogs(id);
426
+ async getProcess(id: string, sessionId?: string): Promise<Process | null> {
427
+ const session = sessionId ?? await this.ensureDefaultSession();
428
+ const response = await this.client.processes.getProcess(id);
429
+ if (!response.process) {
430
+ return null;
431
+ }
432
+
433
+ const processData = response.process;
434
+ return this.createProcessFromDTO({
435
+ id: processData.id,
436
+ pid: processData.pid,
437
+ command: processData.command,
438
+ status: processData.status,
439
+ startTime: processData.startTime,
440
+ endTime: processData.endTime,
441
+ exitCode: processData.exitCode
442
+ }, session);
206
443
  }
207
444
 
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
+ async killProcess(id: string, signal?: string, sessionId?: string): Promise<void> {
446
+ // Note: signal parameter is not currently supported by the HttpClient implementation
447
+ // The HTTP client already throws properly typed errors, so we just let them propagate
448
+ await this.client.processes.killProcess(id);
215
449
  }
216
450
 
217
- async streamProcessLogs(
218
- processId: string,
219
- options?: { signal?: AbortSignal }
220
- ): Promise<ReadableStream<Uint8Array>> {
451
+ async killAllProcesses(sessionId?: string): Promise<number> {
452
+ const response = await this.client.processes.killAllProcesses();
453
+ return response.cleanedCount;
454
+ }
455
+
456
+ async cleanupCompletedProcesses(sessionId?: string): Promise<number> {
457
+ // For now, this would need to be implemented as a container endpoint
458
+ // as we no longer maintain local process storage
459
+ // We'll return 0 as a placeholder until the container endpoint is added
460
+ return 0;
461
+ }
462
+
463
+ async getProcessLogs(id: string, sessionId?: string): Promise<{ stdout: string; stderr: string; processId: string }> {
464
+ // The HTTP client already throws properly typed errors, so we just let them propagate
465
+ const response = await this.client.processes.getProcessLogs(id);
466
+ return {
467
+ stdout: response.stdout,
468
+ stderr: response.stderr,
469
+ processId: response.processId
470
+ };
471
+ }
472
+
473
+
474
+ // Streaming methods - return ReadableStream for RPC compatibility
475
+ async execStream(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
476
+ // Check for cancellation
477
+ if (options?.signal?.aborted) {
478
+ throw new Error('Operation was aborted');
479
+ }
480
+
221
481
  const session = await this.ensureDefaultSession();
222
- return session.streamProcessLogs(processId, options);
482
+ // Get the stream from CommandClient
483
+ return this.client.commands.executeStream(command, session);
484
+ }
485
+
486
+ /**
487
+ * Internal session-aware execStream implementation
488
+ */
489
+ private async execStreamWithSession(command: string, sessionId: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
490
+ // Check for cancellation
491
+ if (options?.signal?.aborted) {
492
+ throw new Error('Operation was aborted');
493
+ }
494
+
495
+ return this.client.commands.executeStream(command, sessionId);
496
+ }
497
+
498
+ async streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {
499
+ // Check for cancellation
500
+ if (options?.signal?.aborted) {
501
+ throw new Error('Operation was aborted');
502
+ }
503
+
504
+ return this.client.processes.streamProcessLogs(processId);
505
+ }
506
+
507
+ /**
508
+ * Internal session-aware streamProcessLogs implementation
509
+ */
510
+ private async streamProcessLogsWithSession(processId: string, sessionId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {
511
+ // Check for cancellation
512
+ if (options?.signal?.aborted) {
513
+ throw new Error('Operation was aborted');
514
+ }
515
+
516
+ return this.client.processes.streamProcessLogs(processId);
223
517
  }
224
518
 
225
519
  async gitCheckout(
226
520
  repoUrl: string,
227
- options: { branch?: string; targetDir?: string }
521
+ options: { branch?: string; targetDir?: string; sessionId?: string }
228
522
  ) {
229
- const session = await this.ensureDefaultSession();
230
- return session.gitCheckout(repoUrl, options);
523
+ const session = options.sessionId ?? await this.ensureDefaultSession();
524
+ return this.client.git.checkout(repoUrl, session, {
525
+ branch: options.branch,
526
+ targetDir: options.targetDir
527
+ });
231
528
  }
232
529
 
233
- async mkdir(path: string, options: { recursive?: boolean } = {}) {
234
- const session = await this.ensureDefaultSession();
235
- return session.mkdir(path, options);
530
+ async mkdir(
531
+ path: string,
532
+ options: { recursive?: boolean; sessionId?: string } = {}
533
+ ) {
534
+ const session = options.sessionId ?? await this.ensureDefaultSession();
535
+ return this.client.files.mkdir(path, session, { recursive: options.recursive });
236
536
  }
237
537
 
238
538
  async writeFile(
239
539
  path: string,
240
540
  content: string,
241
- options: { encoding?: string } = {}
541
+ options: { encoding?: string; sessionId?: string } = {}
242
542
  ) {
243
- const session = await this.ensureDefaultSession();
244
- return session.writeFile(path, content, options);
543
+ const session = options.sessionId ?? await this.ensureDefaultSession();
544
+ return this.client.files.writeFile(path, content, session, { encoding: options.encoding });
245
545
  }
246
546
 
247
- async deleteFile(path: string) {
248
- const session = await this.ensureDefaultSession();
249
- return session.deleteFile(path);
547
+ async deleteFile(path: string, sessionId?: string) {
548
+ const session = sessionId ?? await this.ensureDefaultSession();
549
+ return this.client.files.deleteFile(path, session);
250
550
  }
251
551
 
252
- async renameFile(oldPath: string, newPath: string) {
253
- const session = await this.ensureDefaultSession();
254
- return session.renameFile(oldPath, newPath);
552
+ async renameFile(
553
+ oldPath: string,
554
+ newPath: string,
555
+ sessionId?: string
556
+ ) {
557
+ const session = sessionId ?? await this.ensureDefaultSession();
558
+ return this.client.files.renameFile(oldPath, newPath, session);
255
559
  }
256
560
 
257
- async moveFile(sourcePath: string, destinationPath: string) {
258
- const session = await this.ensureDefaultSession();
259
- return session.moveFile(sourcePath, destinationPath);
561
+ async moveFile(
562
+ sourcePath: string,
563
+ destinationPath: string,
564
+ sessionId?: string
565
+ ) {
566
+ const session = sessionId ?? await this.ensureDefaultSession();
567
+ return this.client.files.moveFile(sourcePath, destinationPath, session);
260
568
  }
261
569
 
262
- async readFile(path: string, options: { encoding?: string } = {}) {
263
- const session = await this.ensureDefaultSession();
264
- return session.readFile(path, options);
570
+ async readFile(
571
+ path: string,
572
+ options: { encoding?: string; sessionId?: string } = {}
573
+ ) {
574
+ const session = options.sessionId ?? await this.ensureDefaultSession();
575
+ return this.client.files.readFile(path, session, { encoding: options.encoding });
265
576
  }
266
577
 
267
- async readFileStream(path: string): Promise<ReadableStream<Uint8Array>> {
268
- const session = await this.ensureDefaultSession();
269
- return session.readFileStream(path);
578
+ /**
579
+ * Stream a file from the sandbox using Server-Sent Events
580
+ * Returns a ReadableStream that can be consumed with streamFile() or collectFile() utilities
581
+ * @param path - Path to the file to stream
582
+ * @param options - Optional session ID
583
+ */
584
+ async readFileStream(
585
+ path: string,
586
+ options: { sessionId?: string } = {}
587
+ ): Promise<ReadableStream<Uint8Array>> {
588
+ const session = options.sessionId ?? await this.ensureDefaultSession();
589
+ return this.client.files.readFileStream(path, session);
270
590
  }
271
591
 
272
592
  async listFiles(
273
593
  path: string,
274
- options: {
275
- recursive?: boolean;
276
- includeHidden?: boolean;
277
- } = {}
594
+ options?: { recursive?: boolean; includeHidden?: boolean }
278
595
  ) {
279
596
  const session = await this.ensureDefaultSession();
280
- return session.listFiles(path, options);
597
+ return this.client.files.listFiles(path, session, options);
281
598
  }
282
599
 
283
600
  async exposePort(port: number, options: { name?: string; hostname: string }) {
284
- await this.client.exposePort(port, options?.name);
601
+ // Check if hostname is workers.dev domain (doesn't support wildcard subdomains)
602
+ if (options.hostname.endsWith('.workers.dev')) {
603
+ const errorResponse: ErrorResponse = {
604
+ code: ErrorCode.CUSTOM_DOMAIN_REQUIRED,
605
+ message: `Port exposure requires a custom domain. .workers.dev domains do not support wildcard subdomains required for port proxying.`,
606
+ context: { originalError: options.hostname },
607
+ httpStatus: 400,
608
+ timestamp: new Date().toISOString()
609
+ };
610
+ throw new CustomDomainRequiredError(errorResponse);
611
+ }
612
+
613
+ const sessionId = await this.ensureDefaultSession();
614
+ await this.client.ports.exposePort(port, sessionId, options?.name);
285
615
 
286
616
  // We need the sandbox name to construct preview URLs
287
617
  if (!this.sandboxName) {
288
- throw new Error(
289
- "Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
290
- );
618
+ throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
291
619
  }
292
620
 
293
- const url = this.constructPreviewUrl(
621
+ // Generate and store token for this port
622
+ const token = this.generatePortToken();
623
+ this.portTokens.set(port, token);
624
+ await this.persistPortTokens();
625
+
626
+ const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname, token);
627
+
628
+ logSecurityEvent('PORT_TOKEN_GENERATED', {
294
629
  port,
295
- this.sandboxName,
296
- options.hostname
297
- );
630
+ sandboxId: this.sandboxName,
631
+ tokenLength: token.length
632
+ }, 'low');
298
633
 
299
634
  return {
300
635
  url,
@@ -305,81 +640,121 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
305
640
 
306
641
  async unexposePort(port: number) {
307
642
  if (!validatePort(port)) {
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
- );
643
+ logSecurityEvent('INVALID_PORT_UNEXPOSE', {
644
+ port
645
+ }, 'high');
646
+ throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
318
647
  }
319
648
 
320
- await this.client.unexposePort(port);
649
+ const sessionId = await this.ensureDefaultSession();
650
+ await this.client.ports.unexposePort(port, sessionId);
321
651
 
322
- logSecurityEvent(
323
- "PORT_UNEXPOSED",
324
- {
325
- port,
326
- },
327
- "low"
328
- );
652
+ // Clean up token for this port
653
+ if (this.portTokens.has(port)) {
654
+ this.portTokens.delete(port);
655
+ await this.persistPortTokens();
656
+ }
657
+
658
+ logSecurityEvent('PORT_UNEXPOSED', {
659
+ port
660
+ }, 'low');
329
661
  }
330
662
 
331
663
  async getExposedPorts(hostname: string) {
332
- const response = await this.client.getExposedPorts();
664
+ const sessionId = await this.ensureDefaultSession();
665
+ const response = await this.client.ports.getExposedPorts(sessionId);
333
666
 
334
667
  // We need the sandbox name to construct preview URLs
335
668
  if (!this.sandboxName) {
336
- throw new Error(
337
- "Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
338
- );
669
+ throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
339
670
  }
340
671
 
341
- return response.ports.map((port) => ({
342
- url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
343
- port: port.port,
344
- name: port.name,
345
- exposedAt: port.exposedAt,
346
- }));
672
+ return response.ports.map(port => {
673
+ // Get token for this port - must exist for all exposed ports
674
+ const token = this.portTokens.get(port.port);
675
+ if (!token) {
676
+ throw new Error(`Port ${port.port} is exposed but has no token. This should not happen.`);
677
+ }
678
+
679
+ return {
680
+ url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname, token),
681
+ port: port.port,
682
+ status: port.status,
683
+ };
684
+ });
685
+ }
686
+
687
+
688
+ async isPortExposed(port: number): Promise<boolean> {
689
+ try {
690
+ const sessionId = await this.ensureDefaultSession();
691
+ const response = await this.client.ports.getExposedPorts(sessionId);
692
+ return response.ports.some(exposedPort => exposedPort.port === port);
693
+ } catch (error) {
694
+ console.error(`[Sandbox] Error checking if port ${port} is exposed:`, error);
695
+ return false;
696
+ }
347
697
  }
348
698
 
349
- private constructPreviewUrl(
350
- port: number,
351
- sandboxId: string,
352
- hostname: string
353
- ): string {
699
+ async validatePortToken(port: number, token: string): Promise<boolean> {
700
+ // First check if port is exposed
701
+ const isExposed = await this.isPortExposed(port);
702
+ if (!isExposed) {
703
+ return false;
704
+ }
705
+
706
+ // Get stored token for this port - must exist for all exposed ports
707
+ const storedToken = this.portTokens.get(port);
708
+ if (!storedToken) {
709
+ // This should not happen - all exposed ports must have tokens
710
+ console.error(`Port ${port} is exposed but has no token. This indicates a bug.`);
711
+ return false;
712
+ }
713
+
714
+ // Constant-time comparison to prevent timing attacks
715
+ return storedToken === token;
716
+ }
717
+
718
+ private generatePortToken(): string {
719
+ // Generate cryptographically secure 16-character token using Web Crypto API
720
+ // Available in Cloudflare Workers runtime
721
+ const array = new Uint8Array(12); // 12 bytes = 16 base64url chars (after padding removal)
722
+ crypto.getRandomValues(array);
723
+
724
+ // Convert to base64url format (URL-safe, no padding, lowercase)
725
+ const base64 = btoa(String.fromCharCode(...array));
726
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '').toLowerCase();
727
+ }
728
+
729
+ private async persistPortTokens(): Promise<void> {
730
+ // Convert Map to plain object for storage
731
+ const tokensObj: Record<string, string> = {};
732
+ for (const [port, token] of this.portTokens.entries()) {
733
+ tokensObj[port.toString()] = token;
734
+ }
735
+ await this.ctx.storage.put('portTokens', tokensObj);
736
+ }
737
+
738
+ private constructPreviewUrl(port: number, sandboxId: string, hostname: string, token: string): string {
354
739
  if (!validatePort(port)) {
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
- );
740
+ logSecurityEvent('INVALID_PORT_REJECTED', {
741
+ port,
742
+ sandboxId,
743
+ hostname
744
+ }, 'high');
745
+ throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
367
746
  }
368
747
 
369
748
  let sanitizedSandboxId: string;
370
749
  try {
371
750
  sanitizedSandboxId = sanitizeSandboxId(sandboxId);
372
751
  } catch (error) {
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
- );
752
+ logSecurityEvent('INVALID_SANDBOX_ID_REJECTED', {
753
+ sandboxId,
754
+ port,
755
+ hostname,
756
+ error: error instanceof Error ? error.message : 'Unknown error'
757
+ }, 'high');
383
758
  throw error;
384
759
  }
385
760
 
@@ -387,47 +762,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
387
762
 
388
763
  if (isLocalhost) {
389
764
  // Unified subdomain approach for localhost (RFC 6761)
390
- const [host, portStr] = hostname.split(":");
391
- const mainPort = portStr || "80";
765
+ const [host, portStr] = hostname.split(':');
766
+ const mainPort = portStr || '80';
392
767
 
393
768
  // Use URL constructor for safe URL building
394
769
  try {
395
770
  const baseUrl = new URL(`http://${host}:${mainPort}`);
396
- // Construct subdomain safely
397
- const subdomainHost = `${port}-${sanitizedSandboxId}.${host}`;
771
+ // Construct subdomain safely with mandatory token
772
+ const subdomainHost = `${port}-${sanitizedSandboxId}-${token}.${host}`;
398
773
  baseUrl.hostname = subdomainHost;
399
774
 
400
775
  const finalUrl = baseUrl.toString();
401
776
 
402
- logSecurityEvent(
403
- "PREVIEW_URL_CONSTRUCTED",
404
- {
405
- port,
406
- sandboxId: sanitizedSandboxId,
407
- hostname,
408
- resultUrl: finalUrl,
409
- environment: "localhost",
410
- },
411
- "low"
412
- );
777
+ logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {
778
+ port,
779
+ sandboxId: sanitizedSandboxId,
780
+ hostname,
781
+ resultUrl: finalUrl,
782
+ environment: 'localhost'
783
+ }, 'low');
413
784
 
414
785
  return finalUrl;
415
786
  } catch (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
- );
787
+ logSecurityEvent('URL_CONSTRUCTION_FAILED', {
788
+ port,
789
+ sandboxId: sanitizedSandboxId,
790
+ hostname,
791
+ error: error instanceof Error ? error.message : 'Unknown error'
792
+ }, 'high');
793
+ throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
431
794
  }
432
795
  }
433
796
 
@@ -437,320 +800,160 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
437
800
  const protocol = "https";
438
801
  const baseUrl = new URL(`${protocol}://${hostname}`);
439
802
 
440
- // Construct subdomain safely
441
- const subdomainHost = `${port}-${sanitizedSandboxId}.${hostname}`;
803
+ // Construct subdomain safely with mandatory token
804
+ const subdomainHost = `${port}-${sanitizedSandboxId}-${token}.${hostname}`;
442
805
  baseUrl.hostname = subdomainHost;
443
806
 
444
807
  const finalUrl = baseUrl.toString();
445
808
 
446
- logSecurityEvent(
447
- "PREVIEW_URL_CONSTRUCTED",
448
- {
449
- port,
450
- sandboxId: sanitizedSandboxId,
451
- hostname,
452
- resultUrl: finalUrl,
453
- environment: "production",
454
- },
455
- "low"
456
- );
809
+ logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {
810
+ port,
811
+ sandboxId: sanitizedSandboxId,
812
+ hostname,
813
+ resultUrl: finalUrl,
814
+ environment: 'production'
815
+ }, 'low');
457
816
 
458
817
  return finalUrl;
459
818
  } catch (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
- );
819
+ logSecurityEvent('URL_CONSTRUCTION_FAILED', {
820
+ port,
821
+ sandboxId: sanitizedSandboxId,
822
+ hostname,
823
+ error: error instanceof Error ? error.message : 'Unknown error'
824
+ }, 'high');
825
+ throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
475
826
  }
476
827
  }
477
828
 
478
- // Code Interpreter Methods
829
+ // ============================================================================
830
+ // Session Management - Advanced Use Cases
831
+ // ============================================================================
479
832
 
480
833
  /**
481
- * Create a new code execution context
834
+ * Create isolated execution session for advanced use cases
835
+ * Returns ExecutionSession with full sandbox API bound to specific session
482
836
  */
483
- async createCodeContext(
484
- options?: CreateContextOptions
485
- ): Promise<CodeContext> {
486
- return this.codeInterpreter.createCodeContext(options);
487
- }
837
+ async createSession(options?: SessionOptions): Promise<ExecutionSession> {
838
+ const sessionId = options?.id || `session-${Date.now()}`;
488
839
 
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
- }
840
+ // Create session in container
841
+ await this.client.utils.createSession({
842
+ id: sessionId,
843
+ env: options?.env,
844
+ cwd: options?.cwd,
845
+ });
500
846
 
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);
847
+ // Return wrapper that binds sessionId to all operations
848
+ return this.getSessionWrapper(sessionId);
509
849
  }
510
850
 
511
851
  /**
512
- * List all code contexts
852
+ * Get an existing session by ID
853
+ * Returns ExecutionSession wrapper bound to the specified session
854
+ *
855
+ * This is useful for retrieving sessions across different requests/contexts
856
+ * without storing the ExecutionSession object (which has RPC lifecycle limitations)
857
+ *
858
+ * @param sessionId - The ID of an existing session
859
+ * @returns ExecutionSession wrapper bound to the session
513
860
  */
514
- async listCodeContexts(): Promise<CodeContext[]> {
515
- return this.codeInterpreter.listCodeContexts();
861
+ async getSession(sessionId: string): Promise<ExecutionSession> {
862
+ // No need to verify session exists in container - operations will fail naturally if it doesn't
863
+ return this.getSessionWrapper(sessionId);
516
864
  }
517
865
 
518
866
  /**
519
- * Delete a code context
867
+ * Internal helper to create ExecutionSession wrapper for a given sessionId
868
+ * Used by both createSession and getSession
520
869
  */
521
- async deleteCodeContext(contextId: string): Promise<void> {
522
- return this.codeInterpreter.deleteCodeContext(contextId);
870
+ private getSessionWrapper(sessionId: string): ExecutionSession {
871
+ return {
872
+ id: sessionId,
873
+
874
+ // Command execution - delegate to internal session-aware methods
875
+ exec: (command, options) => this.execWithSession(command, sessionId, options),
876
+ execStream: (command, options) => this.execStreamWithSession(command, sessionId, options),
877
+
878
+ // Process management
879
+ startProcess: (command, options) => this.startProcess(command, options, sessionId),
880
+ listProcesses: () => this.listProcesses(sessionId),
881
+ getProcess: (id) => this.getProcess(id, sessionId),
882
+ killProcess: (id, signal) => this.killProcess(id, signal),
883
+ killAllProcesses: () => this.killAllProcesses(),
884
+ cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
885
+ getProcessLogs: (id) => this.getProcessLogs(id),
886
+ streamProcessLogs: (processId, options) => this.streamProcessLogs(processId, options),
887
+
888
+ // File operations - pass sessionId via options or parameter
889
+ writeFile: (path, content, options) => this.writeFile(path, content, { ...options, sessionId }),
890
+ readFile: (path, options) => this.readFile(path, { ...options, sessionId }),
891
+ readFileStream: (path) => this.readFileStream(path, { sessionId }),
892
+ mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
893
+ deleteFile: (path) => this.deleteFile(path, sessionId),
894
+ renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId),
895
+ moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId),
896
+ listFiles: (path, options) => this.client.files.listFiles(path, sessionId, options),
897
+
898
+ // Git operations
899
+ gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, { ...options, sessionId }),
900
+
901
+ // Environment management - needs special handling
902
+ setEnvVars: async (envVars: Record<string, string>) => {
903
+ try {
904
+ // Set environment variables by executing export commands
905
+ for (const [key, value] of Object.entries(envVars)) {
906
+ const escapedValue = value.replace(/'/g, "'\\''");
907
+ const exportCommand = `export ${key}='${escapedValue}'`;
908
+
909
+ const result = await this.client.commands.execute(exportCommand, sessionId);
910
+
911
+ if (result.exitCode !== 0) {
912
+ throw new Error(`Failed to set ${key}: ${result.stderr || 'Unknown error'}`);
913
+ }
914
+ }
915
+
916
+ console.log(`[Session ${sessionId}] Environment variables updated successfully`);
917
+ } catch (error) {
918
+ console.error(`[Session ${sessionId}] Failed to set environment variables:`, error);
919
+ throw error;
920
+ }
921
+ },
922
+
923
+ // Code interpreter methods - delegate to sandbox's code interpreter
924
+ createCodeContext: (options) => this.codeInterpreter.createCodeContext(options),
925
+ runCode: async (code, options) => {
926
+ const execution = await this.codeInterpreter.runCode(code, options);
927
+ return execution.toJSON();
928
+ },
929
+ runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options),
930
+ listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
931
+ deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId),
932
+ };
523
933
  }
524
934
 
525
935
  // ============================================================================
526
- // Session Management (Simple Isolation)
936
+ // Code interpreter methods - delegate to CodeInterpreter wrapper
527
937
  // ============================================================================
528
938
 
529
- /**
530
- * Create a new execution session with isolation
531
- * Returns a session object with exec() method
532
- */
939
+ async createCodeContext(options?: CreateContextOptions): Promise<CodeContext> {
940
+ return this.codeInterpreter.createCodeContext(options);
941
+ }
533
942
 
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
- },
943
+ async runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult> {
944
+ const execution = await this.codeInterpreter.runCode(code, options);
945
+ return execution.toJSON();
946
+ }
686
947
 
687
- readFileStream: async (path: string) => {
688
- return await this.client.readFileStream(path, sessionId);
689
- },
948
+ async runCodeStream(code: string, options?: RunCodeOptions): Promise<ReadableStream> {
949
+ return this.codeInterpreter.runCodeStream(code, options);
950
+ }
690
951
 
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
- };
952
+ async listCodeContexts(): Promise<CodeContext[]> {
953
+ return this.codeInterpreter.listCodeContexts();
954
+ }
955
+
956
+ async deleteCodeContext(contextId: string): Promise<void> {
957
+ return this.codeInterpreter.deleteCodeContext(contextId);
755
958
  }
756
959
  }