@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
@@ -1,25 +1,17 @@
1
- import { HttpClient } from "./client.js";
2
- import { isRetryableError, parseErrorResponse } from "./errors.js";
3
- import type {
4
- CodeContext,
5
- CreateContextOptions,
6
- ExecutionError,
7
- OutputMessage,
8
- Result,
9
- } from "./interpreter-types.js";
10
-
11
- // API Response types
12
- interface ContextResponse {
13
- id: string;
14
- language: string;
15
- cwd: string;
16
- createdAt: string; // ISO date string from JSON
17
- lastUsed: string; // ISO date string from JSON
18
- }
19
-
20
- interface ContextListResponse {
21
- contexts: ContextResponse[];
22
- }
1
+ import {
2
+ type CodeContext,
3
+ type ContextCreateResult,
4
+ type ContextListResult,
5
+ type CreateContextOptions,
6
+ type ExecutionError,
7
+ type OutputMessage,
8
+ type Result,
9
+ ResultImpl,
10
+ } from '@repo/shared';
11
+ import type { ErrorResponse } from '../errors';
12
+ import { createErrorFromResponse, ErrorCode, InterpreterNotReadyError } from '../errors';
13
+ import { BaseHttpClient } from './base-client.js';
14
+ import type { HttpClientOptions } from './types.js';
23
15
 
24
16
  // Streaming execution data from the server
25
17
  interface StreamingExecutionData {
@@ -62,10 +54,14 @@ export interface ExecutionCallbacks {
62
54
  onError?: (error: ExecutionError) => void | Promise<void>;
63
55
  }
64
56
 
65
- export class InterpreterClient extends HttpClient {
57
+ export class InterpreterClient extends BaseHttpClient {
66
58
  private readonly maxRetries = 3;
67
59
  private readonly retryDelayMs = 1000;
68
60
 
61
+ constructor(options: HttpClientOptions = {}) {
62
+ super(options);
63
+ }
64
+
69
65
  async createCodeContext(
70
66
  options: CreateContextOptions = {}
71
67
  ): Promise<CodeContext> {
@@ -81,16 +77,21 @@ export class InterpreterClient extends HttpClient {
81
77
  });
82
78
 
83
79
  if (!response.ok) {
84
- throw await parseErrorResponse(response);
80
+ const error = await this.parseErrorResponse(response);
81
+ throw error;
82
+ }
83
+
84
+ const data = (await response.json()) as ContextCreateResult;
85
+ if (!data.success) {
86
+ throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
85
87
  }
86
88
 
87
- const data = (await response.json()) as ContextResponse;
88
89
  return {
89
- id: data.id,
90
+ id: data.contextId,
90
91
  language: data.language,
91
- cwd: data.cwd,
92
- createdAt: new Date(data.createdAt),
93
- lastUsed: new Date(data.lastUsed),
92
+ cwd: data.cwd || '/workspace',
93
+ createdAt: new Date(data.timestamp),
94
+ lastUsed: new Date(data.timestamp),
94
95
  };
95
96
  });
96
97
  }
@@ -99,7 +100,8 @@ export class InterpreterClient extends HttpClient {
99
100
  contextId: string | undefined,
100
101
  code: string,
101
102
  language: string | undefined,
102
- callbacks: ExecutionCallbacks
103
+ callbacks: ExecutionCallbacks,
104
+ timeoutMs?: number
103
105
  ): Promise<void> {
104
106
  return this.executeWithRetry(async () => {
105
107
  const response = await this.doFetch("/api/execute/code", {
@@ -112,11 +114,13 @@ export class InterpreterClient extends HttpClient {
112
114
  context_id: contextId,
113
115
  code,
114
116
  language,
117
+ ...(timeoutMs !== undefined && { timeout_ms: timeoutMs })
115
118
  }),
116
119
  });
117
120
 
118
121
  if (!response.ok) {
119
- throw await parseErrorResponse(response);
122
+ const error = await this.parseErrorResponse(response);
123
+ throw error;
120
124
  }
121
125
 
122
126
  if (!response.body) {
@@ -130,6 +134,111 @@ export class InterpreterClient extends HttpClient {
130
134
  });
131
135
  }
132
136
 
137
+ async listCodeContexts(): Promise<CodeContext[]> {
138
+ return this.executeWithRetry(async () => {
139
+ const response = await this.doFetch("/api/contexts", {
140
+ method: "GET",
141
+ headers: { "Content-Type": "application/json" },
142
+ });
143
+
144
+ if (!response.ok) {
145
+ const error = await this.parseErrorResponse(response);
146
+ throw error;
147
+ }
148
+
149
+ const data = (await response.json()) as ContextListResult;
150
+ if (!data.success) {
151
+ throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
152
+ }
153
+
154
+ return data.contexts.map((ctx) => ({
155
+ id: ctx.id,
156
+ language: ctx.language,
157
+ cwd: ctx.cwd || '/workspace',
158
+ createdAt: new Date(data.timestamp),
159
+ lastUsed: new Date(data.timestamp),
160
+ }));
161
+ });
162
+ }
163
+
164
+ async deleteCodeContext(contextId: string): Promise<void> {
165
+ return this.executeWithRetry(async () => {
166
+ const response = await this.doFetch(`/api/contexts/${contextId}`, {
167
+ method: "DELETE",
168
+ headers: { "Content-Type": "application/json" },
169
+ });
170
+
171
+ if (!response.ok) {
172
+ const error = await this.parseErrorResponse(response);
173
+ throw error;
174
+ }
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Execute an operation with automatic retry for transient errors
180
+ */
181
+ private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
182
+ let lastError: Error | undefined;
183
+
184
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
185
+ try {
186
+ return await operation();
187
+ } catch (error) {
188
+ lastError = error as Error;
189
+
190
+ // Check if it's a retryable error (interpreter not ready)
191
+ if (this.isRetryableError(error)) {
192
+ // Don't retry on the last attempt
193
+ if (attempt < this.maxRetries - 1) {
194
+ // Exponential backoff with jitter
195
+ const delay =
196
+ this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
197
+ await new Promise((resolve) => setTimeout(resolve, delay));
198
+ continue;
199
+ }
200
+ }
201
+
202
+ // Not retryable or last attempt - throw the error
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ throw lastError || new Error("Execution failed after retries");
208
+ }
209
+
210
+ private isRetryableError(error: unknown): boolean {
211
+ if (error instanceof InterpreterNotReadyError) {
212
+ return true;
213
+ }
214
+
215
+ if (error instanceof Error) {
216
+ return (
217
+ error.message.includes("not ready") ||
218
+ error.message.includes("initializing")
219
+ );
220
+ }
221
+
222
+ return false;
223
+ }
224
+
225
+ private async parseErrorResponse(response: Response): Promise<Error> {
226
+ try {
227
+ const errorData = await response.json() as ErrorResponse;
228
+ return createErrorFromResponse(errorData);
229
+ } catch {
230
+ // Fallback if response isn't JSON
231
+ const errorResponse: ErrorResponse = {
232
+ code: ErrorCode.INTERNAL_ERROR,
233
+ message: `HTTP ${response.status}: ${response.statusText}`,
234
+ context: {},
235
+ httpStatus: response.status,
236
+ timestamp: new Date().toISOString()
237
+ };
238
+ return createErrorFromResponse(errorResponse);
239
+ }
240
+ }
241
+
133
242
  private async *readLines(
134
243
  stream: ReadableStream<Uint8Array>
135
244
  ): AsyncGenerator<string> {
@@ -167,8 +276,13 @@ export class InterpreterClient extends HttpClient {
167
276
  ) {
168
277
  if (!line.trim()) return;
169
278
 
279
+ // Skip lines that don't start with "data: " (SSE format)
280
+ if (!line.startsWith('data: ')) return;
281
+
170
282
  try {
171
- const data = JSON.parse(line) as StreamingExecutionData;
283
+ // Strip "data: " prefix and parse JSON
284
+ const jsonData = line.substring(6); // "data: " is 6 characters
285
+ const data = JSON.parse(jsonData) as StreamingExecutionData;
172
286
 
173
287
  switch (data.type) {
174
288
  case "stdout":
@@ -191,34 +305,8 @@ export class InterpreterClient extends HttpClient {
191
305
 
192
306
  case "result":
193
307
  if (callbacks.onResult) {
194
- // Convert raw result to Result interface
195
- const result: Result = {
196
- text: data.text,
197
- html: data.html,
198
- png: data.png,
199
- jpeg: data.jpeg,
200
- svg: data.svg,
201
- latex: data.latex,
202
- markdown: data.markdown,
203
- javascript: data.javascript,
204
- json: data.json,
205
- chart: data.chart,
206
- data: data.data,
207
- formats: () => {
208
- const formats: string[] = [];
209
- if (data.text) formats.push("text");
210
- if (data.html) formats.push("html");
211
- if (data.png) formats.push("png");
212
- if (data.jpeg) formats.push("jpeg");
213
- if (data.svg) formats.push("svg");
214
- if (data.latex) formats.push("latex");
215
- if (data.markdown) formats.push("markdown");
216
- if (data.javascript) formats.push("javascript");
217
- if (data.json) formats.push("json");
218
- if (data.chart) formats.push("chart");
219
- return formats;
220
- },
221
- };
308
+ // Create a ResultImpl instance from the raw data
309
+ const result = new ResultImpl(data);
222
310
  await callbacks.onResult(result);
223
311
  }
224
312
  break;
@@ -227,126 +315,18 @@ export class InterpreterClient extends HttpClient {
227
315
  if (callbacks.onError) {
228
316
  await callbacks.onError({
229
317
  name: data.ename || "Error",
230
- value: data.evalue || data.text || "Unknown error",
318
+ message: data.evalue || "Unknown error",
231
319
  traceback: data.traceback || [],
232
- lineNumber: data.lineNumber,
233
320
  });
234
321
  }
235
322
  break;
236
323
 
237
324
  case "execution_complete":
238
- // Execution completed successfully
325
+ // Signal completion - callbacks can handle cleanup if needed
239
326
  break;
240
327
  }
241
328
  } catch (error) {
242
- console.error(
243
- "[InterpreterClient] Error parsing execution result:",
244
- error
245
- );
246
- }
247
- }
248
-
249
- async listCodeContexts(): Promise<CodeContext[]> {
250
- return this.executeWithRetry(async () => {
251
- const response = await this.doFetch("/api/contexts", {
252
- method: "GET",
253
- headers: { "Content-Type": "application/json" },
254
- });
255
-
256
- if (!response.ok) {
257
- throw await parseErrorResponse(response);
258
- }
259
-
260
- const data = (await response.json()) as ContextListResponse;
261
- return data.contexts.map((ctx) => ({
262
- id: ctx.id,
263
- language: ctx.language,
264
- cwd: ctx.cwd,
265
- createdAt: new Date(ctx.createdAt),
266
- lastUsed: new Date(ctx.lastUsed),
267
- }));
268
- });
269
- }
270
-
271
- async deleteCodeContext(contextId: string): Promise<void> {
272
- return this.executeWithRetry(async () => {
273
- const response = await this.doFetch(`/api/contexts/${contextId}`, {
274
- method: "DELETE",
275
- headers: { "Content-Type": "application/json" },
276
- });
277
-
278
- if (!response.ok) {
279
- throw await parseErrorResponse(response);
280
- }
281
- });
282
- }
283
-
284
- // Override parent doFetch to be public for this class
285
- public async doFetch(path: string, options?: RequestInit): Promise<Response> {
286
- return super.doFetch(path, options);
287
- }
288
-
289
- /**
290
- * Execute an operation with automatic retry for transient errors
291
- */
292
- private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
293
- let lastError: Error | undefined;
294
-
295
- for (let attempt = 0; attempt < this.maxRetries; attempt++) {
296
- try {
297
- return await operation();
298
- } catch (error) {
299
- lastError = error as Error;
300
-
301
- // Check if it's a retryable error (circuit breaker or interpreter not ready)
302
- if (this.isRetryableError(error)) {
303
- // Don't retry on the last attempt
304
- if (attempt < this.maxRetries - 1) {
305
- // Exponential backoff with jitter
306
- const delay =
307
- this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
308
- await new Promise((resolve) => setTimeout(resolve, delay));
309
- continue;
310
- }
311
- }
312
-
313
- // Non-retryable error or last attempt - throw immediately
314
- throw error;
315
- }
329
+ console.error("Failed to parse execution result:", error);
316
330
  }
317
-
318
- // All retries exhausted - throw a clean error without implementation details
319
- if (lastError?.message.includes("Code execution")) {
320
- // If the error already has a clean message about code execution, use it
321
- throw lastError;
322
- }
323
-
324
- // Otherwise, throw a generic but user-friendly error
325
- throw new Error("Unable to execute code at this time");
326
- }
327
-
328
- /**
329
- * Check if an error is retryable
330
- */
331
- private isRetryableError(error: unknown): boolean {
332
- // Use the SDK's built-in retryable check
333
- if (isRetryableError(error)) {
334
- return true;
335
- }
336
-
337
- // Also check for circuit breaker specific errors
338
- if (error instanceof Error) {
339
- // Circuit breaker errors (from the container's response)
340
- if (error.message.includes("Circuit breaker is open")) {
341
- return true;
342
- }
343
-
344
- // Check if error has a status property
345
- if ("status" in error && error.status === "circuit_open") {
346
- return true;
347
- }
348
- }
349
-
350
- return false;
351
331
  }
352
332
  }
@@ -0,0 +1,108 @@
1
+ import type {
2
+ PortCloseResult,
3
+ PortExposeResult,
4
+ PortListResult,
5
+ } from '@repo/shared';
6
+ import { BaseHttpClient } from './base-client';
7
+ import type { HttpClientOptions } from './types';
8
+
9
+ // Re-export for convenience
10
+ export type {
11
+ PortExposeResult,
12
+ PortCloseResult,
13
+ PortListResult,
14
+ };
15
+
16
+ /**
17
+ * Request interface for exposing ports
18
+ */
19
+ export interface ExposePortRequest {
20
+ port: number;
21
+ name?: string;
22
+ }
23
+
24
+ /**
25
+ * Request interface for unexposing ports
26
+ */
27
+ export interface UnexposePortRequest {
28
+ port: number;
29
+ }
30
+
31
+ /**
32
+ * Client for port management and preview URL operations
33
+ */
34
+ export class PortClient extends BaseHttpClient {
35
+ constructor(options: HttpClientOptions = {}) {
36
+ super(options);
37
+ }
38
+
39
+ /**
40
+ * Expose a port and get a preview URL
41
+ * @param port - Port number to expose
42
+ * @param sessionId - The session ID for this operation
43
+ * @param name - Optional name for the port
44
+ */
45
+ async exposePort(
46
+ port: number,
47
+ sessionId: string,
48
+ name?: string
49
+ ): Promise<PortExposeResult> {
50
+ try {
51
+ const data = { port, sessionId, name };
52
+
53
+ const response = await this.post<PortExposeResult>(
54
+ '/api/expose-port',
55
+ data
56
+ );
57
+
58
+ this.logSuccess(
59
+ 'Port exposed',
60
+ `${port} exposed at ${response.url}${name ? ` (${name})` : ''}`
61
+ );
62
+
63
+ return response;
64
+ } catch (error) {
65
+ this.logError('exposePort', error);
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Unexpose a port and remove its preview URL
72
+ * @param port - Port number to unexpose
73
+ * @param sessionId - The session ID for this operation
74
+ */
75
+ async unexposePort(port: number, sessionId: string): Promise<PortCloseResult> {
76
+ try {
77
+ const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
78
+ const response = await this.delete<PortCloseResult>(url);
79
+
80
+ this.logSuccess('Port unexposed', `${port}`);
81
+ return response;
82
+ } catch (error) {
83
+ this.logError('unexposePort', error);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get all currently exposed ports
90
+ * @param sessionId - The session ID for this operation
91
+ */
92
+ async getExposedPorts(sessionId: string): Promise<PortListResult> {
93
+ try {
94
+ const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
95
+ const response = await this.get<PortListResult>(url);
96
+
97
+ this.logSuccess(
98
+ 'Exposed ports retrieved',
99
+ `${response.ports.length} ports exposed`
100
+ );
101
+
102
+ return response;
103
+ } catch (error) {
104
+ this.logError('getExposedPorts', error);
105
+ throw error;
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,180 @@
1
+ import type {
2
+ ProcessCleanupResult,
3
+ ProcessInfoResult,
4
+ ProcessKillResult,
5
+ ProcessListResult,
6
+ ProcessLogsResult,
7
+ ProcessStartResult,
8
+ StartProcessRequest,
9
+ } from '@repo/shared';
10
+ import { BaseHttpClient } from './base-client';
11
+ import type { HttpClientOptions } from './types';
12
+
13
+ // Re-export for convenience
14
+ export type {
15
+ StartProcessRequest,
16
+ ProcessStartResult,
17
+ ProcessListResult,
18
+ ProcessInfoResult,
19
+ ProcessKillResult,
20
+ ProcessLogsResult,
21
+ ProcessCleanupResult,
22
+ };
23
+
24
+
25
+ /**
26
+ * Client for background process management
27
+ */
28
+ export class ProcessClient extends BaseHttpClient {
29
+ constructor(options: HttpClientOptions = {}) {
30
+ super(options);
31
+ }
32
+
33
+ /**
34
+ * Start a background process
35
+ * @param command - Command to execute as a background process
36
+ * @param sessionId - The session ID for this operation
37
+ * @param options - Optional settings (processId)
38
+ */
39
+ async startProcess(
40
+ command: string,
41
+ sessionId: string,
42
+ options?: { processId?: string }
43
+ ): Promise<ProcessStartResult> {
44
+ try {
45
+ const data: StartProcessRequest = {
46
+ command,
47
+ sessionId,
48
+ processId: options?.processId,
49
+ };
50
+
51
+ const response = await this.post<ProcessStartResult>(
52
+ '/api/process/start',
53
+ data
54
+ );
55
+
56
+ this.logSuccess(
57
+ 'Process started',
58
+ `${command} (ID: ${response.processId})`
59
+ );
60
+
61
+ return response;
62
+ } catch (error) {
63
+ this.logError('startProcess', error);
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * List all processes (sandbox-scoped, not session-scoped)
70
+ */
71
+ async listProcesses(): Promise<ProcessListResult> {
72
+ try {
73
+ const url = `/api/process/list`;
74
+ const response = await this.get<ProcessListResult>(url);
75
+
76
+ this.logSuccess('Processes listed', `${response.processes.length} processes`);
77
+ return response;
78
+ } catch (error) {
79
+ this.logError('listProcesses', error);
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get information about a specific process (sandbox-scoped, not session-scoped)
86
+ * @param processId - ID of the process to retrieve
87
+ */
88
+ async getProcess(processId: string): Promise<ProcessInfoResult> {
89
+ try {
90
+ const url = `/api/process/${processId}`;
91
+ const response = await this.get<ProcessInfoResult>(url);
92
+
93
+ this.logSuccess('Process retrieved', `ID: ${processId}`);
94
+ return response;
95
+ } catch (error) {
96
+ this.logError('getProcess', error);
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Kill a specific process (sandbox-scoped, not session-scoped)
103
+ * @param processId - ID of the process to kill
104
+ */
105
+ async killProcess(processId: string): Promise<ProcessKillResult> {
106
+ try {
107
+ const url = `/api/process/${processId}`;
108
+ const response = await this.delete<ProcessKillResult>(url);
109
+
110
+ this.logSuccess('Process killed', `ID: ${processId}`);
111
+ return response;
112
+ } catch (error) {
113
+ this.logError('killProcess', error);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Kill all running processes (sandbox-scoped, not session-scoped)
120
+ */
121
+ async killAllProcesses(): Promise<ProcessCleanupResult> {
122
+ try {
123
+ const url = `/api/process/kill-all`;
124
+ const response = await this.delete<ProcessCleanupResult>(url);
125
+
126
+ this.logSuccess(
127
+ 'All processes killed',
128
+ `${response.cleanedCount} processes terminated`
129
+ );
130
+
131
+ return response;
132
+ } catch (error) {
133
+ this.logError('killAllProcesses', error);
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Get logs from a specific process (sandbox-scoped, not session-scoped)
140
+ * @param processId - ID of the process to get logs from
141
+ */
142
+ async getProcessLogs(processId: string): Promise<ProcessLogsResult> {
143
+ try {
144
+ const url = `/api/process/${processId}/logs`;
145
+ const response = await this.get<ProcessLogsResult>(url);
146
+
147
+ this.logSuccess(
148
+ 'Process logs retrieved',
149
+ `ID: ${processId}, stdout: ${response.stdout.length} chars, stderr: ${response.stderr.length} chars`
150
+ );
151
+
152
+ return response;
153
+ } catch (error) {
154
+ this.logError('getProcessLogs', error);
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Stream logs from a specific process (sandbox-scoped, not session-scoped)
161
+ * @param processId - ID of the process to stream logs from
162
+ */
163
+ async streamProcessLogs(processId: string): Promise<ReadableStream<Uint8Array>> {
164
+ try {
165
+ const url = `/api/process/${processId}/stream`;
166
+ const response = await this.doFetch(url, {
167
+ method: 'GET',
168
+ });
169
+
170
+ const stream = await this.handleStreamResponse(response);
171
+
172
+ this.logSuccess('Process log stream started', `ID: ${processId}`);
173
+
174
+ return stream;
175
+ } catch (error) {
176
+ this.logError('streamProcessLogs', error);
177
+ throw error;
178
+ }
179
+ }
180
+ }