@cloudflare/sandbox 0.0.0-feafd32 → 0.0.0-ff2fa91

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 (74) hide show
  1. package/CHANGELOG.md +130 -15
  2. package/Dockerfile +156 -68
  3. package/README.md +92 -769
  4. package/dist/index.d.ts +1889 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +3146 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +16 -8
  9. package/src/clients/base-client.ts +295 -0
  10. package/src/clients/command-client.ts +115 -0
  11. package/src/clients/file-client.ts +300 -0
  12. package/src/clients/git-client.ts +91 -0
  13. package/src/clients/index.ts +60 -0
  14. package/src/clients/interpreter-client.ts +333 -0
  15. package/src/clients/port-client.ts +105 -0
  16. package/src/clients/process-client.ts +180 -0
  17. package/src/clients/sandbox-client.ts +39 -0
  18. package/src/clients/types.ts +88 -0
  19. package/src/clients/utility-client.ts +123 -0
  20. package/src/errors/adapter.ts +238 -0
  21. package/src/errors/classes.ts +594 -0
  22. package/src/errors/index.ts +109 -0
  23. package/src/file-stream.ts +169 -0
  24. package/src/index.ts +88 -63
  25. package/src/interpreter.ts +58 -40
  26. package/src/request-handler.ts +94 -55
  27. package/src/sandbox.ts +978 -490
  28. package/src/security.ts +34 -28
  29. package/src/sse-parser.ts +8 -11
  30. package/src/version.ts +6 -0
  31. package/startup.sh +3 -0
  32. package/tests/base-client.test.ts +364 -0
  33. package/tests/command-client.test.ts +444 -0
  34. package/tests/file-client.test.ts +831 -0
  35. package/tests/file-stream.test.ts +310 -0
  36. package/tests/get-sandbox.test.ts +149 -0
  37. package/tests/git-client.test.ts +415 -0
  38. package/tests/port-client.test.ts +293 -0
  39. package/tests/process-client.test.ts +683 -0
  40. package/tests/request-handler.test.ts +292 -0
  41. package/tests/sandbox.test.ts +706 -0
  42. package/tests/sse-parser.test.ts +291 -0
  43. package/tests/utility-client.test.ts +339 -0
  44. package/tests/version.test.ts +16 -0
  45. package/tests/wrangler.jsonc +35 -0
  46. package/tsconfig.json +9 -1
  47. package/tsdown.config.ts +12 -0
  48. package/vitest.config.ts +31 -0
  49. package/container_src/bun.lock +0 -76
  50. package/container_src/circuit-breaker.ts +0 -121
  51. package/container_src/control-process.ts +0 -784
  52. package/container_src/handler/exec.ts +0 -185
  53. package/container_src/handler/file.ts +0 -406
  54. package/container_src/handler/git.ts +0 -130
  55. package/container_src/handler/ports.ts +0 -314
  56. package/container_src/handler/process.ts +0 -568
  57. package/container_src/handler/session.ts +0 -92
  58. package/container_src/index.ts +0 -592
  59. package/container_src/interpreter-service.ts +0 -276
  60. package/container_src/isolation.ts +0 -1049
  61. package/container_src/mime-processor.ts +0 -255
  62. package/container_src/package.json +0 -18
  63. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  64. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  65. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  66. package/container_src/runtime/process-pool.ts +0 -464
  67. package/container_src/shell-escape.ts +0 -42
  68. package/container_src/startup.sh +0 -11
  69. package/container_src/types.ts +0 -131
  70. package/src/client.ts +0 -1009
  71. package/src/errors.ts +0 -219
  72. package/src/interpreter-client.ts +0 -352
  73. package/src/interpreter-types.ts +0 -390
  74. package/src/types.ts +0 -502
@@ -0,0 +1,91 @@
1
+ import type { GitCheckoutResult } from '@repo/shared';
2
+ import { BaseHttpClient } from './base-client';
3
+ import type { HttpClientOptions, SessionRequest } from './types';
4
+
5
+ // Re-export for convenience
6
+ export type { GitCheckoutResult };
7
+
8
+ /**
9
+ * Request interface for Git checkout operations
10
+ */
11
+ export interface GitCheckoutRequest extends SessionRequest {
12
+ repoUrl: string;
13
+ branch?: string;
14
+ targetDir?: string;
15
+ }
16
+
17
+ /**
18
+ * Client for Git repository operations
19
+ */
20
+ export class GitClient extends BaseHttpClient {
21
+ /**
22
+ * Clone a Git repository
23
+ * @param repoUrl - URL of the Git repository to clone
24
+ * @param sessionId - The session ID for this operation
25
+ * @param options - Optional settings (branch, targetDir)
26
+ */
27
+ async checkout(
28
+ repoUrl: string,
29
+ sessionId: string,
30
+ options?: {
31
+ branch?: string;
32
+ targetDir?: string;
33
+ }
34
+ ): Promise<GitCheckoutResult> {
35
+ try {
36
+ // Determine target directory - use provided path or generate from repo name
37
+ let targetDir = options?.targetDir;
38
+ if (!targetDir) {
39
+ const repoName = this.extractRepoName(repoUrl);
40
+ // Ensure absolute path in /workspace
41
+ targetDir = `/workspace/${repoName}`;
42
+ }
43
+
44
+ const data: GitCheckoutRequest = {
45
+ repoUrl,
46
+ sessionId,
47
+ targetDir
48
+ };
49
+
50
+ // Only include branch if explicitly specified
51
+ // This allows Git to use the repository's default branch
52
+ if (options?.branch) {
53
+ data.branch = options.branch;
54
+ }
55
+
56
+ const response = await this.post<GitCheckoutResult>(
57
+ '/api/git/checkout',
58
+ data
59
+ );
60
+
61
+ this.logSuccess(
62
+ 'Repository cloned',
63
+ `${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`
64
+ );
65
+
66
+ return response;
67
+ } catch (error) {
68
+ this.logError('checkout', error);
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Extract repository name from URL for default directory name
75
+ */
76
+ private extractRepoName(repoUrl: string): string {
77
+ try {
78
+ const url = new URL(repoUrl);
79
+ const pathParts = url.pathname.split('/');
80
+ const repoName = pathParts[pathParts.length - 1];
81
+
82
+ // Remove .git extension if present
83
+ return repoName.replace(/\.git$/, '');
84
+ } catch {
85
+ // Fallback for invalid URLs
86
+ const parts = repoUrl.split('/');
87
+ const repoName = parts[parts.length - 1];
88
+ return repoName.replace(/\.git$/, '') || 'repo';
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,60 @@
1
+ // Main client exports
2
+
3
+ // Command client types
4
+ export type { ExecuteRequest, ExecuteResponse } from './command-client';
5
+
6
+ // Domain-specific clients
7
+ export { CommandClient } from './command-client';
8
+ // File client types
9
+ export type {
10
+ FileOperationRequest,
11
+ MkdirRequest,
12
+ ReadFileRequest,
13
+ WriteFileRequest
14
+ } from './file-client';
15
+ export { FileClient } from './file-client';
16
+ // Git client types
17
+ export type { GitCheckoutRequest, GitCheckoutResult } from './git-client';
18
+ export { GitClient } from './git-client';
19
+ export {
20
+ type ExecutionCallbacks,
21
+ InterpreterClient
22
+ } from './interpreter-client';
23
+ // Port client types
24
+ export type {
25
+ ExposePortRequest,
26
+ PortCloseResult,
27
+ PortExposeResult,
28
+ PortListResult,
29
+ UnexposePortRequest
30
+ } from './port-client';
31
+ export { PortClient } from './port-client';
32
+ // Process client types
33
+ export type {
34
+ ProcessCleanupResult,
35
+ ProcessInfoResult,
36
+ ProcessKillResult,
37
+ ProcessListResult,
38
+ ProcessLogsResult,
39
+ ProcessStartResult,
40
+ StartProcessRequest
41
+ } from './process-client';
42
+ export { ProcessClient } from './process-client';
43
+ export { SandboxClient } from './sandbox-client';
44
+ // Types and interfaces
45
+ export type {
46
+ BaseApiResponse,
47
+ ContainerStub,
48
+ ErrorResponse,
49
+ HttpClientOptions,
50
+ RequestConfig,
51
+ ResponseHandler,
52
+ SessionRequest
53
+ } from './types';
54
+ // Utility client types
55
+ export type {
56
+ CommandsResponse,
57
+ PingResponse,
58
+ VersionResponse
59
+ } from './utility-client';
60
+ export { UtilityClient } from './utility-client';
@@ -0,0 +1,333 @@
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 {
13
+ createErrorFromResponse,
14
+ ErrorCode,
15
+ InterpreterNotReadyError
16
+ } from '../errors';
17
+ import { BaseHttpClient } from './base-client.js';
18
+ import type { HttpClientOptions } from './types.js';
19
+
20
+ // Streaming execution data from the server
21
+ interface StreamingExecutionData {
22
+ type: 'result' | 'stdout' | 'stderr' | 'error' | 'execution_complete';
23
+ text?: string;
24
+ html?: string;
25
+ png?: string; // base64
26
+ jpeg?: string; // base64
27
+ svg?: string;
28
+ latex?: string;
29
+ markdown?: string;
30
+ javascript?: string;
31
+ json?: unknown;
32
+ chart?: {
33
+ type:
34
+ | 'line'
35
+ | 'bar'
36
+ | 'scatter'
37
+ | 'pie'
38
+ | 'histogram'
39
+ | 'heatmap'
40
+ | 'unknown';
41
+ data: unknown;
42
+ options?: unknown;
43
+ };
44
+ data?: unknown;
45
+ metadata?: Record<string, unknown>;
46
+ execution_count?: number;
47
+ ename?: string;
48
+ evalue?: string;
49
+ traceback?: string[];
50
+ lineNumber?: number;
51
+ timestamp?: number;
52
+ }
53
+
54
+ export interface ExecutionCallbacks {
55
+ onStdout?: (output: OutputMessage) => void | Promise<void>;
56
+ onStderr?: (output: OutputMessage) => void | Promise<void>;
57
+ onResult?: (result: Result) => void | Promise<void>;
58
+ onError?: (error: ExecutionError) => void | Promise<void>;
59
+ }
60
+
61
+ export class InterpreterClient extends BaseHttpClient {
62
+ private readonly maxRetries = 3;
63
+ private readonly retryDelayMs = 1000;
64
+
65
+ async createCodeContext(
66
+ options: CreateContextOptions = {}
67
+ ): Promise<CodeContext> {
68
+ return this.executeWithRetry(async () => {
69
+ const response = await this.doFetch('/api/contexts', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({
73
+ language: options.language || 'python',
74
+ cwd: options.cwd || '/workspace',
75
+ env_vars: options.envVars
76
+ })
77
+ });
78
+
79
+ if (!response.ok) {
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)}`);
87
+ }
88
+
89
+ return {
90
+ id: data.contextId,
91
+ language: data.language,
92
+ cwd: data.cwd || '/workspace',
93
+ createdAt: new Date(data.timestamp),
94
+ lastUsed: new Date(data.timestamp)
95
+ };
96
+ });
97
+ }
98
+
99
+ async runCodeStream(
100
+ contextId: string | undefined,
101
+ code: string,
102
+ language: string | undefined,
103
+ callbacks: ExecutionCallbacks,
104
+ timeoutMs?: number
105
+ ): Promise<void> {
106
+ return this.executeWithRetry(async () => {
107
+ const response = await this.doFetch('/api/execute/code', {
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': 'application/json',
111
+ Accept: 'text/event-stream'
112
+ },
113
+ body: JSON.stringify({
114
+ context_id: contextId,
115
+ code,
116
+ language,
117
+ ...(timeoutMs !== undefined && { timeout_ms: timeoutMs })
118
+ })
119
+ });
120
+
121
+ if (!response.ok) {
122
+ const error = await this.parseErrorResponse(response);
123
+ throw error;
124
+ }
125
+
126
+ if (!response.body) {
127
+ throw new Error('No response body for streaming execution');
128
+ }
129
+
130
+ // Process streaming response
131
+ for await (const chunk of this.readLines(response.body)) {
132
+ await this.parseExecutionResult(chunk, callbacks);
133
+ }
134
+ });
135
+ }
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
+ this.logError('executeWithRetry', error);
189
+ lastError = error as Error;
190
+
191
+ // Check if it's a retryable error (interpreter not ready)
192
+ if (this.isRetryableError(error)) {
193
+ // Don't retry on the last attempt
194
+ if (attempt < this.maxRetries - 1) {
195
+ // Exponential backoff with jitter
196
+ const delay =
197
+ this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
198
+ await new Promise((resolve) => setTimeout(resolve, delay));
199
+ continue;
200
+ }
201
+ }
202
+
203
+ // Not retryable or last attempt - throw the error
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ throw lastError || new Error('Execution failed after retries');
209
+ }
210
+
211
+ private isRetryableError(error: unknown): boolean {
212
+ if (error instanceof InterpreterNotReadyError) {
213
+ return true;
214
+ }
215
+
216
+ if (error instanceof Error) {
217
+ return (
218
+ error.message.includes('not ready') ||
219
+ error.message.includes('initializing')
220
+ );
221
+ }
222
+
223
+ return false;
224
+ }
225
+
226
+ private async parseErrorResponse(response: Response): Promise<Error> {
227
+ try {
228
+ const errorData = (await response.json()) as ErrorResponse;
229
+ return createErrorFromResponse(errorData);
230
+ } catch {
231
+ // Fallback if response isn't JSON
232
+ const errorResponse: ErrorResponse = {
233
+ code: ErrorCode.INTERNAL_ERROR,
234
+ message: `HTTP ${response.status}: ${response.statusText}`,
235
+ context: {},
236
+ httpStatus: response.status,
237
+ timestamp: new Date().toISOString()
238
+ };
239
+ return createErrorFromResponse(errorResponse);
240
+ }
241
+ }
242
+
243
+ private async *readLines(
244
+ stream: ReadableStream<Uint8Array>
245
+ ): AsyncGenerator<string> {
246
+ const reader = stream.getReader();
247
+ let buffer = '';
248
+
249
+ try {
250
+ while (true) {
251
+ const { done, value } = await reader.read();
252
+ if (value) {
253
+ buffer += new TextDecoder().decode(value);
254
+ }
255
+ if (done) break;
256
+
257
+ let newlineIdx = buffer.indexOf('\n');
258
+ while (newlineIdx !== -1) {
259
+ yield buffer.slice(0, newlineIdx);
260
+ buffer = buffer.slice(newlineIdx + 1);
261
+ newlineIdx = buffer.indexOf('\n');
262
+ }
263
+ }
264
+
265
+ // Yield any remaining data
266
+ if (buffer.length > 0) {
267
+ yield buffer;
268
+ }
269
+ } finally {
270
+ reader.releaseLock();
271
+ }
272
+ }
273
+
274
+ private async parseExecutionResult(
275
+ line: string,
276
+ callbacks: ExecutionCallbacks
277
+ ) {
278
+ if (!line.trim()) return;
279
+
280
+ // Skip lines that don't start with "data: " (SSE format)
281
+ if (!line.startsWith('data: ')) return;
282
+
283
+ try {
284
+ // Strip "data: " prefix and parse JSON
285
+ const jsonData = line.substring(6); // "data: " is 6 characters
286
+ const data = JSON.parse(jsonData) as StreamingExecutionData;
287
+
288
+ switch (data.type) {
289
+ case 'stdout':
290
+ if (callbacks.onStdout && data.text) {
291
+ await callbacks.onStdout({
292
+ text: data.text,
293
+ timestamp: data.timestamp || Date.now()
294
+ });
295
+ }
296
+ break;
297
+
298
+ case 'stderr':
299
+ if (callbacks.onStderr && data.text) {
300
+ await callbacks.onStderr({
301
+ text: data.text,
302
+ timestamp: data.timestamp || Date.now()
303
+ });
304
+ }
305
+ break;
306
+
307
+ case 'result':
308
+ if (callbacks.onResult) {
309
+ // Create a ResultImpl instance from the raw data
310
+ const result = new ResultImpl(data);
311
+ await callbacks.onResult(result);
312
+ }
313
+ break;
314
+
315
+ case 'error':
316
+ if (callbacks.onError) {
317
+ await callbacks.onError({
318
+ name: data.ename || 'Error',
319
+ message: data.evalue || 'Unknown error',
320
+ traceback: data.traceback || []
321
+ });
322
+ }
323
+ break;
324
+
325
+ case 'execution_complete':
326
+ // Signal completion - callbacks can handle cleanup if needed
327
+ break;
328
+ }
329
+ } catch (error) {
330
+ this.logError('parseExecutionResult', error);
331
+ }
332
+ }
333
+ }
@@ -0,0 +1,105 @@
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 { PortExposeResult, PortCloseResult, PortListResult };
11
+
12
+ /**
13
+ * Request interface for exposing ports
14
+ */
15
+ export interface ExposePortRequest {
16
+ port: number;
17
+ name?: string;
18
+ }
19
+
20
+ /**
21
+ * Request interface for unexposing ports
22
+ */
23
+ export interface UnexposePortRequest {
24
+ port: number;
25
+ }
26
+
27
+ /**
28
+ * Client for port management and preview URL operations
29
+ */
30
+ export class PortClient extends BaseHttpClient {
31
+ /**
32
+ * Expose a port and get a preview URL
33
+ * @param port - Port number to expose
34
+ * @param sessionId - The session ID for this operation
35
+ * @param name - Optional name for the port
36
+ */
37
+ async exposePort(
38
+ port: number,
39
+ sessionId: string,
40
+ name?: string
41
+ ): Promise<PortExposeResult> {
42
+ try {
43
+ const data = { port, sessionId, name };
44
+
45
+ const response = await this.post<PortExposeResult>(
46
+ '/api/expose-port',
47
+ data
48
+ );
49
+
50
+ this.logSuccess(
51
+ 'Port exposed',
52
+ `${port} exposed at ${response.url}${name ? ` (${name})` : ''}`
53
+ );
54
+
55
+ return response;
56
+ } catch (error) {
57
+ this.logError('exposePort', error);
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Unexpose a port and remove its preview URL
64
+ * @param port - Port number to unexpose
65
+ * @param sessionId - The session ID for this operation
66
+ */
67
+ async unexposePort(
68
+ port: number,
69
+ sessionId: string
70
+ ): Promise<PortCloseResult> {
71
+ try {
72
+ const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(
73
+ sessionId
74
+ )}`;
75
+ const response = await this.delete<PortCloseResult>(url);
76
+
77
+ this.logSuccess('Port unexposed', `${port}`);
78
+ return response;
79
+ } catch (error) {
80
+ this.logError('unexposePort', error);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get all currently exposed ports
87
+ * @param sessionId - The session ID for this operation
88
+ */
89
+ async getExposedPorts(sessionId: string): Promise<PortListResult> {
90
+ try {
91
+ const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
92
+ const response = await this.get<PortListResult>(url);
93
+
94
+ this.logSuccess(
95
+ 'Exposed ports retrieved',
96
+ `${response.ports.length} ports exposed`
97
+ );
98
+
99
+ return response;
100
+ } catch (error) {
101
+ this.logError('getExposedPorts', error);
102
+ throw error;
103
+ }
104
+ }
105
+ }