@cloudflare/sandbox 0.0.0-6a2c669 → 0.0.0-6c54d07

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