@cloudflare/sandbox 0.0.0-f5fcd52 → 0.0.0-fb3c9c2

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 (40) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/Dockerfile +47 -36
  3. package/README.md +805 -24
  4. package/container_src/bun.lock +122 -0
  5. package/container_src/circuit-breaker.ts +121 -0
  6. package/container_src/control-process.ts +784 -0
  7. package/container_src/handler/exec.ts +185 -0
  8. package/container_src/handler/file.ts +406 -0
  9. package/container_src/handler/git.ts +130 -0
  10. package/container_src/handler/ports.ts +314 -0
  11. package/container_src/handler/process.ts +568 -0
  12. package/container_src/handler/session.ts +92 -0
  13. package/container_src/index.ts +411 -3006
  14. package/container_src/isolation.ts +1038 -0
  15. package/container_src/jupyter-server.ts +579 -0
  16. package/container_src/jupyter-service.ts +461 -0
  17. package/container_src/jupyter_config.py +48 -0
  18. package/container_src/mime-processor.ts +255 -0
  19. package/container_src/package.json +9 -0
  20. package/container_src/shell-escape.ts +42 -0
  21. package/container_src/startup.sh +84 -0
  22. package/container_src/types.ts +131 -0
  23. package/package.json +5 -9
  24. package/src/client.ts +333 -1413
  25. package/src/errors.ts +218 -0
  26. package/src/index.ts +60 -10
  27. package/src/interpreter-types.ts +383 -0
  28. package/src/interpreter.ts +150 -0
  29. package/src/jupyter-client.ts +349 -0
  30. package/src/request-handler.ts +69 -20
  31. package/src/sandbox.ts +594 -105
  32. package/src/security.ts +113 -0
  33. package/src/sse-parser.ts +147 -0
  34. package/src/types.ts +502 -0
  35. package/tsconfig.json +1 -1
  36. package/tests/client.example.ts +0 -308
  37. package/tests/connection-test.ts +0 -81
  38. package/tests/simple-test.ts +0 -81
  39. package/tests/test1.ts +0 -281
  40. package/tests/test2.ts +0 -929
package/src/sandbox.ts CHANGED
@@ -1,6 +1,33 @@
1
1
  import { Container, getContainer } from "@cloudflare/containers";
2
- import { HttpClient } from "./client";
2
+ import { CodeInterpreter } from "./interpreter";
3
+ import type {
4
+ CodeContext,
5
+ CreateContextOptions,
6
+ ExecutionResult,
7
+ RunCodeOptions,
8
+ } from "./interpreter-types";
9
+ import { JupyterClient } from "./jupyter-client";
3
10
  import { isLocalhostPattern } from "./request-handler";
11
+ import {
12
+ logSecurityEvent,
13
+ SecurityError,
14
+ sanitizeSandboxId,
15
+ validatePort,
16
+ } from "./security";
17
+ 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";
4
31
 
5
32
  export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
6
33
  const stub = getContainer(ns, id);
@@ -11,26 +38,26 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
11
38
  return stub;
12
39
  }
13
40
 
14
- export class Sandbox<Env = unknown> extends Container<Env> {
15
- sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
16
- client: HttpClient;
17
- private workerHostname: string | null = null;
41
+ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
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: JupyterClient;
18
45
  private sandboxName: string | null = null;
46
+ private codeInterpreter: CodeInterpreter;
47
+ private defaultSession: ExecutionSession | null = null;
19
48
 
20
49
  constructor(ctx: DurableObjectState, env: Env) {
21
50
  super(ctx, env);
22
- this.client = new HttpClient({
23
- onCommandComplete: (success, exitCode, _stdout, _stderr, command, _args) => {
51
+ this.client = new JupyterClient({
52
+ onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {
24
53
  console.log(
25
54
  `[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
26
55
  );
27
56
  },
28
- onCommandStart: (command, args) => {
29
- console.log(
30
- `[Container] Command started: ${command} ${args.join(" ")}`
31
- );
57
+ onCommandStart: (command) => {
58
+ console.log(`[Container] Command started: ${command}`);
32
59
  },
33
- onError: (error, _command, _args) => {
60
+ onError: (error, _command) => {
34
61
  console.error(`[Container] Command error: ${error}`);
35
62
  },
36
63
  onOutput: (stream, data, _command) => {
@@ -40,9 +67,13 @@ export class Sandbox<Env = unknown> extends Container<Env> {
40
67
  stub: this,
41
68
  });
42
69
 
70
+ // Initialize code interpreter
71
+ this.codeInterpreter = new CodeInterpreter(this);
72
+
43
73
  // Load the sandbox name from storage on initialization
44
74
  this.ctx.blockConcurrencyWhile(async () => {
45
- this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
75
+ this.sandboxName =
76
+ (await this.ctx.storage.get<string>("sandboxName")) || null;
46
77
  });
47
78
  }
48
79
 
@@ -50,7 +81,7 @@ export class Sandbox<Env = unknown> extends Container<Env> {
50
81
  async setSandboxName(name: string): Promise<void> {
51
82
  if (!this.sandboxName) {
52
83
  this.sandboxName = name;
53
- await this.ctx.storage.put('sandboxName', name);
84
+ await this.ctx.storage.put("sandboxName", name);
54
85
  console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
55
86
  }
56
87
  }
@@ -59,6 +90,11 @@ export class Sandbox<Env = unknown> extends Container<Env> {
59
90
  async setEnvVars(envVars: Record<string, string>): Promise<void> {
60
91
  this.envVars = { ...this.envVars, ...envVars };
61
92
  console.log(`[Sandbox] Updated environment variables`);
93
+
94
+ // If we have a default session, update its environment too
95
+ if (this.defaultSession) {
96
+ await this.defaultSession.setEnvVars(envVars);
97
+ }
62
98
  }
63
99
 
64
100
  override onStart() {
@@ -67,30 +103,21 @@ export class Sandbox<Env = unknown> extends Container<Env> {
67
103
 
68
104
  override onStop() {
69
105
  console.log("Sandbox successfully shut down");
70
- if (this.client) {
71
- this.client.clearSession();
72
- }
73
106
  }
74
107
 
75
108
  override onError(error: unknown) {
76
109
  console.log("Sandbox error:", error);
77
110
  }
78
111
 
79
- // Override fetch to capture the hostname and route to appropriate ports
112
+ // Override fetch to route internal container requests to appropriate ports
80
113
  override async fetch(request: Request): Promise<Response> {
81
114
  const url = new URL(request.url);
82
115
 
83
- // Capture the hostname from the first request
84
- if (!this.workerHostname) {
85
- this.workerHostname = url.hostname;
86
- console.log(`[Sandbox] Captured hostname: ${this.workerHostname}`);
87
- }
88
-
89
116
  // Capture and store the sandbox name from the header if present
90
- if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {
91
- const name = request.headers.get('X-Sandbox-Name')!;
117
+ if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
118
+ const name = request.headers.get("X-Sandbox-Name")!;
92
119
  this.sandboxName = name;
93
- await this.ctx.storage.put('sandboxName', name);
120
+ await this.ctx.storage.put("sandboxName", name);
94
121
  console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
95
122
  }
96
123
 
@@ -108,102 +135,161 @@ export class Sandbox<Env = unknown> extends Container<Env> {
108
135
  return parseInt(proxyMatch[1]);
109
136
  }
110
137
 
138
+ if (url.port) {
139
+ return parseInt(url.port);
140
+ }
141
+
111
142
  // All other requests go to control plane on port 3000
112
143
  // This includes /api/* endpoints and any other control requests
113
144
  return 3000;
114
145
  }
115
146
 
116
- async exec(command: string, args: string[], options?: { stream?: boolean; background?: boolean }) {
117
- if (options?.stream) {
118
- return this.client.executeStream(command, args, undefined, options?.background);
147
+ // Helper to ensure default session is initialized
148
+ private async ensureDefaultSession(): Promise<ExecutionSession> {
149
+ if (!this.defaultSession) {
150
+ const sessionId = `sandbox-${this.sandboxName || 'default'}`;
151
+ this.defaultSession = await this.createSession({
152
+ id: sessionId,
153
+ env: this.envVars || {},
154
+ cwd: '/workspace',
155
+ isolation: true
156
+ });
157
+ console.log(`[Sandbox] Default session initialized: ${sessionId}`);
119
158
  }
120
- return this.client.execute(command, args, undefined, options?.background);
159
+ return this.defaultSession;
160
+ }
161
+
162
+
163
+ async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
164
+ const session = await this.ensureDefaultSession();
165
+ return session.exec(command, options);
166
+ }
167
+
168
+ async startProcess(
169
+ command: string,
170
+ options?: ProcessOptions
171
+ ): Promise<Process> {
172
+ const session = await this.ensureDefaultSession();
173
+ return session.startProcess(command, options);
174
+ }
175
+
176
+ async listProcesses(): Promise<Process[]> {
177
+ const session = await this.ensureDefaultSession();
178
+ return session.listProcesses();
179
+ }
180
+
181
+ async getProcess(id: string): Promise<Process | null> {
182
+ const session = await this.ensureDefaultSession();
183
+ return session.getProcess(id);
184
+ }
185
+
186
+ async killProcess(id: string, signal?: string): Promise<void> {
187
+ const session = await this.ensureDefaultSession();
188
+ return session.killProcess(id, signal);
189
+ }
190
+
191
+ async killAllProcesses(): Promise<number> {
192
+ const session = await this.ensureDefaultSession();
193
+ return session.killAllProcesses();
194
+ }
195
+
196
+ async cleanupCompletedProcesses(): Promise<number> {
197
+ const session = await this.ensureDefaultSession();
198
+ return session.cleanupCompletedProcesses();
199
+ }
200
+
201
+ async getProcessLogs(
202
+ id: string
203
+ ): Promise<{ stdout: string; stderr: string }> {
204
+ const session = await this.ensureDefaultSession();
205
+ return session.getProcessLogs(id);
206
+ }
207
+
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);
215
+ }
216
+
217
+ async streamProcessLogs(
218
+ processId: string,
219
+ options?: { signal?: AbortSignal }
220
+ ): Promise<ReadableStream<Uint8Array>> {
221
+ const session = await this.ensureDefaultSession();
222
+ return session.streamProcessLogs(processId, options);
121
223
  }
122
224
 
123
225
  async gitCheckout(
124
226
  repoUrl: string,
125
- options: { branch?: string; targetDir?: string; stream?: boolean }
227
+ options: { branch?: string; targetDir?: string }
126
228
  ) {
127
- if (options?.stream) {
128
- return this.client.gitCheckoutStream(
129
- repoUrl,
130
- options.branch,
131
- options.targetDir
132
- );
133
- }
134
- return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
229
+ const session = await this.ensureDefaultSession();
230
+ return session.gitCheckout(repoUrl, options);
135
231
  }
136
232
 
137
- async mkdir(
138
- path: string,
139
- options: { recursive?: boolean; stream?: boolean } = {}
140
- ) {
141
- if (options?.stream) {
142
- return this.client.mkdirStream(path, options.recursive);
143
- }
144
- return this.client.mkdir(path, options.recursive);
233
+ async mkdir(path: string, options: { recursive?: boolean } = {}) {
234
+ const session = await this.ensureDefaultSession();
235
+ return session.mkdir(path, options);
145
236
  }
146
237
 
147
238
  async writeFile(
148
239
  path: string,
149
240
  content: string,
150
- options: { encoding?: string; stream?: boolean } = {}
241
+ options: { encoding?: string } = {}
151
242
  ) {
152
- if (options?.stream) {
153
- return this.client.writeFileStream(path, content, options.encoding);
154
- }
155
- return this.client.writeFile(path, content, options.encoding);
243
+ const session = await this.ensureDefaultSession();
244
+ return session.writeFile(path, content, options);
156
245
  }
157
246
 
158
- async deleteFile(path: string, options: { stream?: boolean } = {}) {
159
- if (options?.stream) {
160
- return this.client.deleteFileStream(path);
161
- }
162
- return this.client.deleteFile(path);
247
+ async deleteFile(path: string) {
248
+ const session = await this.ensureDefaultSession();
249
+ return session.deleteFile(path);
163
250
  }
164
251
 
165
- async renameFile(
166
- oldPath: string,
167
- newPath: string,
168
- options: { stream?: boolean } = {}
169
- ) {
170
- if (options?.stream) {
171
- return this.client.renameFileStream(oldPath, newPath);
172
- }
173
- return this.client.renameFile(oldPath, newPath);
252
+ async renameFile(oldPath: string, newPath: string) {
253
+ const session = await this.ensureDefaultSession();
254
+ return session.renameFile(oldPath, newPath);
174
255
  }
175
256
 
176
- async moveFile(
177
- sourcePath: string,
178
- destinationPath: string,
179
- options: { stream?: boolean } = {}
180
- ) {
181
- if (options?.stream) {
182
- return this.client.moveFileStream(sourcePath, destinationPath);
183
- }
184
- return this.client.moveFile(sourcePath, destinationPath);
257
+ async moveFile(sourcePath: string, destinationPath: string) {
258
+ const session = await this.ensureDefaultSession();
259
+ return session.moveFile(sourcePath, destinationPath);
260
+ }
261
+
262
+ async readFile(path: string, options: { encoding?: string } = {}) {
263
+ const session = await this.ensureDefaultSession();
264
+ return session.readFile(path, options);
185
265
  }
186
266
 
187
- async readFile(
267
+ async listFiles(
188
268
  path: string,
189
- options: { encoding?: string; stream?: boolean } = {}
269
+ options: {
270
+ recursive?: boolean;
271
+ includeHidden?: boolean;
272
+ } = {}
190
273
  ) {
191
- if (options?.stream) {
192
- return this.client.readFileStream(path, options.encoding);
193
- }
194
- return this.client.readFile(path, options.encoding);
274
+ const session = await this.ensureDefaultSession();
275
+ return session.listFiles(path, options);
195
276
  }
196
277
 
197
- async exposePort(port: number, options?: { name?: string }) {
278
+ async exposePort(port: number, options: { name?: string; hostname: string }) {
198
279
  await this.client.exposePort(port, options?.name);
199
280
 
200
281
  // We need the sandbox name to construct preview URLs
201
282
  if (!this.sandboxName) {
202
- throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
283
+ throw new Error(
284
+ "Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
285
+ );
203
286
  }
204
287
 
205
- const hostname = this.getHostname();
206
- const url = this.constructPreviewUrl(port, this.sandboxName, hostname);
288
+ const url = this.constructPreviewUrl(
289
+ port,
290
+ this.sandboxName,
291
+ options.hostname
292
+ );
207
293
 
208
294
  return {
209
295
  url,
@@ -213,20 +299,41 @@ export class Sandbox<Env = unknown> extends Container<Env> {
213
299
  }
214
300
 
215
301
  async unexposePort(port: number) {
302
+ if (!validatePort(port)) {
303
+ logSecurityEvent(
304
+ "INVALID_PORT_UNEXPOSE",
305
+ {
306
+ port,
307
+ },
308
+ "high"
309
+ );
310
+ throw new SecurityError(
311
+ `Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
312
+ );
313
+ }
314
+
216
315
  await this.client.unexposePort(port);
316
+
317
+ logSecurityEvent(
318
+ "PORT_UNEXPOSED",
319
+ {
320
+ port,
321
+ },
322
+ "low"
323
+ );
217
324
  }
218
325
 
219
- async getExposedPorts() {
326
+ async getExposedPorts(hostname: string) {
220
327
  const response = await this.client.getExposedPorts();
221
328
 
222
329
  // We need the sandbox name to construct preview URLs
223
330
  if (!this.sandboxName) {
224
- throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
331
+ throw new Error(
332
+ "Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
333
+ );
225
334
  }
226
335
 
227
- const hostname = this.getHostname();
228
-
229
- return response.ports.map(port => ({
336
+ return response.ports.map((port) => ({
230
337
  url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
231
338
  port: port.port,
232
339
  name: port.name,
@@ -234,25 +341,407 @@ export class Sandbox<Env = unknown> extends Container<Env> {
234
341
  }));
235
342
  }
236
343
 
237
- private getHostname(): string {
238
- // Use the captured hostname or fall back to localhost for development
239
- return this.workerHostname || "localhost:8787";
240
- }
344
+ private constructPreviewUrl(
345
+ port: number,
346
+ sandboxId: string,
347
+ hostname: string
348
+ ): string {
349
+ if (!validatePort(port)) {
350
+ logSecurityEvent(
351
+ "INVALID_PORT_REJECTED",
352
+ {
353
+ port,
354
+ sandboxId,
355
+ hostname,
356
+ },
357
+ "high"
358
+ );
359
+ throw new SecurityError(
360
+ `Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
361
+ );
362
+ }
363
+
364
+ let sanitizedSandboxId: string;
365
+ try {
366
+ sanitizedSandboxId = sanitizeSandboxId(sandboxId);
367
+ } catch (error) {
368
+ logSecurityEvent(
369
+ "INVALID_SANDBOX_ID_REJECTED",
370
+ {
371
+ sandboxId,
372
+ port,
373
+ hostname,
374
+ error: error instanceof Error ? error.message : "Unknown error",
375
+ },
376
+ "high"
377
+ );
378
+ throw error;
379
+ }
241
380
 
242
- private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
243
- // Check if this is a localhost pattern
244
381
  const isLocalhost = isLocalhostPattern(hostname);
245
382
 
246
383
  if (isLocalhost) {
247
- // For local development, we need to use a different approach
248
- // Since subdomains don't work with localhost, we'll use the base URL
249
- // with a note that the user needs to handle routing differently
250
- return `http://${hostname}/preview/${port}/${sandboxId}`;
384
+ // Unified subdomain approach for localhost (RFC 6761)
385
+ const [host, portStr] = hostname.split(":");
386
+ const mainPort = portStr || "80";
387
+
388
+ // Use URL constructor for safe URL building
389
+ try {
390
+ const baseUrl = new URL(`http://${host}:${mainPort}`);
391
+ // Construct subdomain safely
392
+ const subdomainHost = `${port}-${sanitizedSandboxId}.${host}`;
393
+ baseUrl.hostname = subdomainHost;
394
+
395
+ const finalUrl = baseUrl.toString();
396
+
397
+ logSecurityEvent(
398
+ "PREVIEW_URL_CONSTRUCTED",
399
+ {
400
+ port,
401
+ sandboxId: sanitizedSandboxId,
402
+ hostname,
403
+ resultUrl: finalUrl,
404
+ environment: "localhost",
405
+ },
406
+ "low"
407
+ );
408
+
409
+ return finalUrl;
410
+ } catch (error) {
411
+ logSecurityEvent(
412
+ "URL_CONSTRUCTION_FAILED",
413
+ {
414
+ port,
415
+ sandboxId: sanitizedSandboxId,
416
+ hostname,
417
+ error: error instanceof Error ? error.message : "Unknown error",
418
+ },
419
+ "high"
420
+ );
421
+ throw new SecurityError(
422
+ `Failed to construct preview URL: ${
423
+ error instanceof Error ? error.message : "Unknown error"
424
+ }`
425
+ );
426
+ }
427
+ }
428
+
429
+ // Production subdomain logic - enforce HTTPS
430
+ try {
431
+ // Always use HTTPS for production (non-localhost)
432
+ const protocol = "https";
433
+ const baseUrl = new URL(`${protocol}://${hostname}`);
434
+
435
+ // Construct subdomain safely
436
+ const subdomainHost = `${port}-${sanitizedSandboxId}.${hostname}`;
437
+ baseUrl.hostname = subdomainHost;
438
+
439
+ const finalUrl = baseUrl.toString();
440
+
441
+ logSecurityEvent(
442
+ "PREVIEW_URL_CONSTRUCTED",
443
+ {
444
+ port,
445
+ sandboxId: sanitizedSandboxId,
446
+ hostname,
447
+ resultUrl: finalUrl,
448
+ environment: "production",
449
+ },
450
+ "low"
451
+ );
452
+
453
+ return finalUrl;
454
+ } catch (error) {
455
+ logSecurityEvent(
456
+ "URL_CONSTRUCTION_FAILED",
457
+ {
458
+ port,
459
+ sandboxId: sanitizedSandboxId,
460
+ hostname,
461
+ error: error instanceof Error ? error.message : "Unknown error",
462
+ },
463
+ "high"
464
+ );
465
+ throw new SecurityError(
466
+ `Failed to construct preview URL: ${
467
+ error instanceof Error ? error.message : "Unknown error"
468
+ }`
469
+ );
251
470
  }
471
+ }
472
+
473
+ // Code Interpreter Methods
474
+
475
+ /**
476
+ * Create a new code execution context
477
+ */
478
+ async createCodeContext(
479
+ options?: CreateContextOptions
480
+ ): Promise<CodeContext> {
481
+ return this.codeInterpreter.createCodeContext(options);
482
+ }
483
+
484
+ /**
485
+ * Run code with streaming callbacks
486
+ */
487
+ async runCode(
488
+ code: string,
489
+ options?: RunCodeOptions
490
+ ): Promise<ExecutionResult> {
491
+ const execution = await this.codeInterpreter.runCode(code, options);
492
+ // Convert to plain object for RPC serialization
493
+ return execution.toJSON();
494
+ }
252
495
 
253
- // For all other domains (workers.dev, custom domains, etc.)
254
- // Use subdomain-based routing pattern
255
- const protocol = hostname.includes(":") ? "http" : "https";
256
- return `${protocol}://${port}-${sandboxId}.${hostname}`;
496
+ /**
497
+ * Run code and return a streaming response
498
+ */
499
+ async runCodeStream(
500
+ code: string,
501
+ options?: RunCodeOptions
502
+ ): Promise<ReadableStream> {
503
+ return this.codeInterpreter.runCodeStream(code, options);
504
+ }
505
+
506
+ /**
507
+ * List all code contexts
508
+ */
509
+ async listCodeContexts(): Promise<CodeContext[]> {
510
+ return this.codeInterpreter.listCodeContexts();
511
+ }
512
+
513
+ /**
514
+ * Delete a code context
515
+ */
516
+ async deleteCodeContext(contextId: string): Promise<void> {
517
+ return this.codeInterpreter.deleteCodeContext(contextId);
518
+ }
519
+
520
+ // ============================================================================
521
+ // Session Management (Simple Isolation)
522
+ // ============================================================================
523
+
524
+ /**
525
+ * Create a new execution session with isolation
526
+ * Returns a session object with exec() method
527
+ */
528
+
529
+ async createSession(options: {
530
+ id?: string;
531
+ env?: Record<string, string>;
532
+ cwd?: string;
533
+ isolation?: boolean;
534
+ }): Promise<ExecutionSession> {
535
+ const sessionId = options.id || `session-${Date.now()}`;
536
+
537
+ await this.client.createSession({
538
+ id: sessionId,
539
+ env: options.env,
540
+ cwd: options.cwd,
541
+ isolation: options.isolation
542
+ });
543
+ // Return comprehensive ExecutionSession object that implements all ISandbox methods
544
+ return {
545
+ id: sessionId,
546
+
547
+ // Command execution - clean method names
548
+ exec: async (command: string, options?: ExecOptions) => {
549
+ const result = await this.client.exec(sessionId, command);
550
+ return {
551
+ ...result,
552
+ command,
553
+ duration: 0,
554
+ timestamp: new Date().toISOString()
555
+ };
556
+ },
557
+
558
+ execStream: async (command: string, options?: StreamOptions) => {
559
+ return await this.client.execStream(sessionId, command);
560
+ },
561
+
562
+ // Process management - route to session-aware methods
563
+ startProcess: async (command: string, options?: ProcessOptions) => {
564
+ // Use session-specific process management
565
+ const response = await this.client.startProcess(command, sessionId, {
566
+ processId: options?.processId,
567
+ timeout: options?.timeout,
568
+ env: options?.env,
569
+ cwd: options?.cwd,
570
+ encoding: options?.encoding,
571
+ autoCleanup: options?.autoCleanup,
572
+ });
573
+
574
+ // Convert response to Process object with bound methods
575
+ const process = response.process;
576
+ return {
577
+ id: process.id,
578
+ pid: process.pid,
579
+ command: process.command,
580
+ status: process.status as ProcessStatus,
581
+ startTime: new Date(process.startTime),
582
+ endTime: process.endTime ? new Date(process.endTime) : undefined,
583
+ exitCode: process.exitCode ?? undefined,
584
+ kill: async (signal?: string) => {
585
+ await this.client.killProcess(process.id);
586
+ },
587
+ getStatus: async () => {
588
+ const resp = await this.client.getProcess(process.id);
589
+ return resp.process?.status as ProcessStatus || "error";
590
+ },
591
+ getLogs: async () => {
592
+ return await this.client.getProcessLogs(process.id);
593
+ },
594
+ };
595
+ },
596
+
597
+ listProcesses: async () => {
598
+ // Get processes for this specific session
599
+ const response = await this.client.listProcesses(sessionId);
600
+
601
+ // Convert to Process objects with bound methods
602
+ return response.processes.map(p => ({
603
+ id: p.id,
604
+ pid: p.pid,
605
+ command: p.command,
606
+ status: p.status as ProcessStatus,
607
+ startTime: new Date(p.startTime),
608
+ endTime: p.endTime ? new Date(p.endTime) : undefined,
609
+ exitCode: p.exitCode ?? undefined,
610
+ kill: async (signal?: string) => {
611
+ await this.client.killProcess(p.id);
612
+ },
613
+ getStatus: async () => {
614
+ const processResp = await this.client.getProcess(p.id);
615
+ return processResp.process?.status as ProcessStatus || "error";
616
+ },
617
+ getLogs: async () => {
618
+ return this.client.getProcessLogs(p.id);
619
+ },
620
+ }));
621
+ },
622
+
623
+ getProcess: async (id: string) => {
624
+ const response = await this.client.getProcess(id);
625
+ if (!response.process) return null;
626
+
627
+ const p = response.process;
628
+ return {
629
+ id: p.id,
630
+ pid: p.pid,
631
+ command: p.command,
632
+ status: p.status as ProcessStatus,
633
+ startTime: new Date(p.startTime),
634
+ endTime: p.endTime ? new Date(p.endTime) : undefined,
635
+ exitCode: p.exitCode ?? undefined,
636
+ kill: async (signal?: string) => {
637
+ await this.client.killProcess(p.id);
638
+ },
639
+ getStatus: async () => {
640
+ const processResp = await this.client.getProcess(p.id);
641
+ return processResp.process?.status as ProcessStatus || "error";
642
+ },
643
+ getLogs: async () => {
644
+ return this.client.getProcessLogs(p.id);
645
+ },
646
+ };
647
+ },
648
+
649
+ killProcess: async (id: string, signal?: string) => {
650
+ await this.client.killProcess(id);
651
+ },
652
+
653
+ killAllProcesses: async () => {
654
+ // Kill all processes for this specific session
655
+ const response = await this.client.killAllProcesses(sessionId);
656
+ return response.killedCount;
657
+ },
658
+
659
+ streamProcessLogs: async (processId: string, options?: { signal?: AbortSignal }) => {
660
+ return await this.client.streamProcessLogs(processId, options);
661
+ },
662
+
663
+ getProcessLogs: async (id: string) => {
664
+ return await this.client.getProcessLogs(id);
665
+ },
666
+
667
+ cleanupCompletedProcesses: async () => {
668
+ // This would need a new endpoint to cleanup processes for a specific session
669
+ // For now, return 0 as no cleanup is performed
670
+ return 0;
671
+ },
672
+
673
+ // File operations - clean method names (no "InSession" suffix)
674
+ writeFile: async (path: string, content: string, options?: { encoding?: string }) => {
675
+ return await this.client.writeFile(path, content, options?.encoding, sessionId);
676
+ },
677
+
678
+ readFile: async (path: string, options?: { encoding?: string }) => {
679
+ return await this.client.readFile(path, options?.encoding, sessionId);
680
+ },
681
+
682
+ mkdir: async (path: string, options?: { recursive?: boolean }) => {
683
+ return await this.client.mkdir(path, options?.recursive, sessionId);
684
+ },
685
+
686
+ deleteFile: async (path: string) => {
687
+ return await this.client.deleteFile(path, sessionId);
688
+ },
689
+
690
+ renameFile: async (oldPath: string, newPath: string) => {
691
+ return await this.client.renameFile(oldPath, newPath, sessionId);
692
+ },
693
+
694
+ moveFile: async (sourcePath: string, destinationPath: string) => {
695
+ return await this.client.moveFile(sourcePath, destinationPath, sessionId);
696
+ },
697
+
698
+ listFiles: async (path: string, options?: { recursive?: boolean; includeHidden?: boolean }) => {
699
+ return await this.client.listFiles(path, sessionId, options);
700
+ },
701
+
702
+ gitCheckout: async (repoUrl: string, options?: { branch?: string; targetDir?: string }) => {
703
+ return await this.client.gitCheckout(repoUrl, sessionId, options?.branch, options?.targetDir);
704
+ },
705
+
706
+ // Port management
707
+ exposePort: async (port: number, options: { name?: string; hostname: string }) => {
708
+ return await this.exposePort(port, options);
709
+ },
710
+
711
+ unexposePort: async (port: number) => {
712
+ return await this.unexposePort(port);
713
+ },
714
+
715
+ getExposedPorts: async (hostname: string) => {
716
+ return await this.getExposedPorts(hostname);
717
+ },
718
+
719
+ // Environment management
720
+ setEnvVars: async (envVars: Record<string, string>) => {
721
+ // TODO: Implement session-specific environment updates
722
+ console.log(`[Session ${sessionId}] Environment variables update not yet implemented`);
723
+ },
724
+
725
+ // Code Interpreter API
726
+ createCodeContext: async (options?: any) => {
727
+ return await this.createCodeContext(options);
728
+ },
729
+
730
+ runCode: async (code: string, options?: any) => {
731
+ return await this.runCode(code, options);
732
+ },
733
+
734
+ runCodeStream: async (code: string, options?: any) => {
735
+ return await this.runCodeStream(code, options);
736
+ },
737
+
738
+ listCodeContexts: async () => {
739
+ return await this.listCodeContexts();
740
+ },
741
+
742
+ deleteCodeContext: async (contextId: string) => {
743
+ return await this.deleteCodeContext(contextId);
744
+ }
745
+ };
257
746
  }
258
747
  }