@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
@@ -0,0 +1,11 @@
1
+ import {
2
+ asyncIterableToSSEStream,
3
+ parseSSEStream,
4
+ responseToAsyncIterable
5
+ } from "./chunk-NNGBXDMY.js";
6
+ export {
7
+ asyncIterableToSSEStream,
8
+ parseSSEStream,
9
+ responseToAsyncIterable
10
+ };
11
+ //# sourceMappingURL=sse-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@cloudflare/sandbox",
3
- "version": "0.0.0-46eb4e6",
3
+ "version": "0.0.0-485cf61",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/sandbox-sdk"
7
7
  },
8
8
  "description": "A sandboxed environment for running commands",
9
9
  "dependencies": {
10
- "@cloudflare/containers": "^0.0.28"
10
+ "@cloudflare/containers": "^0.0.28",
11
+ "@repo/shared": "^0.0.0"
11
12
  },
12
13
  "tags": [
13
14
  "sandbox",
@@ -18,9 +19,14 @@
18
19
  ],
19
20
  "scripts": {
20
21
  "build": "rm -rf dist && tsup src/*.ts --outDir dist --dts --sourcemap --format esm",
21
- "docker:local": "docker build . -t cloudflare/sandbox-test:$npm_package_version",
22
- "docker:publish": "docker buildx build --platform linux/amd64,linux/arm64 -t cloudflare/sandbox:$npm_package_version --push .",
23
- "docker:publish:beta": "docker buildx build --platform linux/amd64,linux/arm64 -t cloudflare/sandbox:$npm_package_version-beta --push ."
22
+ "check": "biome check && npm run typecheck",
23
+ "fix": "biome check --fix && npm run typecheck",
24
+ "typecheck": "tsc --noEmit",
25
+ "docker:local": "cd ../.. && docker build -f packages/sandbox/Dockerfile -t cloudflare/sandbox-test:$npm_package_version .",
26
+ "docker:publish": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile -t cloudflare/sandbox:$npm_package_version --push .",
27
+ "docker:publish:beta": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile -t cloudflare/sandbox:$npm_package_version-beta --push .",
28
+ "test": "vitest run --config vitest.config.ts",
29
+ "test:e2e": "cd ../.. && vitest run --config vitest.e2e.config.ts \"$@\""
24
30
  },
25
31
  "exports": {
26
32
  ".": {
@@ -0,0 +1,297 @@
1
+ import type { ErrorResponse as NewErrorResponse } from '../errors';
2
+ import { createErrorFromResponse, ErrorCode } from '../errors';
3
+ import type {
4
+ HttpClientOptions,
5
+ ResponseHandler
6
+ } from './types';
7
+
8
+ // Container provisioning retry configuration
9
+ const TIMEOUT_MS = 60_000; // 60 seconds total timeout budget
10
+ const MIN_TIME_FOR_RETRY_MS = 10_000; // Need at least 10s remaining to retry (8s Container + 2s delay)
11
+
12
+ /**
13
+ * Abstract base class providing common HTTP functionality for all domain clients
14
+ */
15
+ export abstract class BaseHttpClient {
16
+ protected baseUrl: string;
17
+ protected options: HttpClientOptions;
18
+
19
+ constructor(options: HttpClientOptions = {}) {
20
+ this.options = {
21
+ ...options,
22
+ };
23
+ this.baseUrl = this.options.baseUrl!;
24
+
25
+ }
26
+
27
+ /**
28
+ * Core HTTP request method with automatic retry for container provisioning delays
29
+ */
30
+ protected async doFetch(
31
+ path: string,
32
+ options?: RequestInit
33
+ ): Promise<Response> {
34
+ const startTime = Date.now();
35
+ let attempt = 0;
36
+
37
+ console.log(`[DEBUG] doFetch called for ${options?.method || 'GET'} ${path}`);
38
+
39
+ while (true) {
40
+ const response = await this.executeFetch(path, options);
41
+
42
+ console.log(`[DEBUG] Response status: ${response.status}`);
43
+
44
+ // Only retry container provisioning 503s, not user app 503s
45
+ if (response.status === 503) {
46
+ console.log('[DEBUG] Got 503 response, checking if container provisioning error...');
47
+
48
+ const isContainerProvisioning = await this.isContainerProvisioningError(response);
49
+
50
+ console.log('[DEBUG] isContainerProvisioning result:', isContainerProvisioning);
51
+
52
+ if (isContainerProvisioning) {
53
+ const elapsed = Date.now() - startTime;
54
+ const remaining = TIMEOUT_MS - elapsed;
55
+
56
+ console.log(`[DEBUG] Elapsed: ${elapsed}ms, Remaining: ${remaining}ms`);
57
+
58
+ // Check if we have enough time for another attempt
59
+ // (Need at least 10s: 8s for Container timeout + 2s delay)
60
+ if (remaining > MIN_TIME_FOR_RETRY_MS) {
61
+ // Exponential backoff: 2s, 4s, 8s, 16s (capped at 16s)
62
+ const delay = Math.min(2000 * 2 ** attempt, 16000);
63
+
64
+ console.log(
65
+ `[Sandbox SDK] Container provisioning in progress (attempt ${attempt + 1}), ` +
66
+ `retrying in ${delay}ms (${Math.floor(remaining / 1000)}s remaining)`
67
+ );
68
+
69
+ await new Promise(resolve => setTimeout(resolve, delay));
70
+ attempt++;
71
+ continue;
72
+ } else {
73
+ // Exhausted retries - log error and return response
74
+ // Let existing error handling convert to proper error
75
+ console.error(
76
+ `[Sandbox SDK] Container failed to provision after ${attempt + 1} attempts over 60s.`
77
+ );
78
+ return response;
79
+ }
80
+ } else {
81
+ console.log('[DEBUG] Not a container provisioning error, returning 503 immediately');
82
+ }
83
+ }
84
+
85
+ // Return response (success, user app error, or non-retryable error)
86
+ if (response.status !== 200) {
87
+ console.log(`[DEBUG] Returning response with status ${response.status}`);
88
+ }
89
+ return response;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Make a POST request with JSON body
95
+ */
96
+ protected async post<T>(
97
+ endpoint: string,
98
+ data: Record<string, any>,
99
+ responseHandler?: ResponseHandler<T>
100
+ ): Promise<T> {
101
+ const response = await this.doFetch(endpoint, {
102
+ method: 'POST',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ },
106
+ body: JSON.stringify(data),
107
+ });
108
+
109
+ return this.handleResponse(response, responseHandler);
110
+ }
111
+
112
+ /**
113
+ * Make a GET request
114
+ */
115
+ protected async get<T>(
116
+ endpoint: string,
117
+ responseHandler?: ResponseHandler<T>
118
+ ): Promise<T> {
119
+ const response = await this.doFetch(endpoint, {
120
+ method: 'GET',
121
+ });
122
+
123
+ return this.handleResponse(response, responseHandler);
124
+ }
125
+
126
+ /**
127
+ * Make a DELETE request
128
+ */
129
+ protected async delete<T>(
130
+ endpoint: string,
131
+ responseHandler?: ResponseHandler<T>
132
+ ): Promise<T> {
133
+ const response = await this.doFetch(endpoint, {
134
+ method: 'DELETE',
135
+ });
136
+
137
+ return this.handleResponse(response, responseHandler);
138
+ }
139
+
140
+
141
+ /**
142
+ * Handle HTTP response with error checking and parsing
143
+ */
144
+ protected async handleResponse<T>(
145
+ response: Response,
146
+ customHandler?: ResponseHandler<T>
147
+ ): Promise<T> {
148
+ if (!response.ok) {
149
+ await this.handleErrorResponse(response);
150
+ }
151
+
152
+ if (customHandler) {
153
+ return customHandler(response);
154
+ }
155
+
156
+ try {
157
+ return await response.json();
158
+ } catch (error) {
159
+ // Handle malformed JSON responses gracefully
160
+ const errorResponse: NewErrorResponse = {
161
+ code: ErrorCode.INVALID_JSON_RESPONSE,
162
+ message: `Invalid JSON response: ${error instanceof Error ? error.message : 'Unknown parsing error'}`,
163
+ context: {},
164
+ httpStatus: response.status,
165
+ timestamp: new Date().toISOString()
166
+ };
167
+ throw createErrorFromResponse(errorResponse);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Handle error responses with consistent error throwing
173
+ */
174
+ protected async handleErrorResponse(response: Response): Promise<never> {
175
+ let errorData: NewErrorResponse;
176
+
177
+ try {
178
+ errorData = await response.json();
179
+ } catch {
180
+ // Fallback if response isn't JSON or parsing fails
181
+ errorData = {
182
+ code: ErrorCode.INTERNAL_ERROR,
183
+ message: `HTTP error! status: ${response.status}`,
184
+ context: { statusText: response.statusText },
185
+ httpStatus: response.status,
186
+ timestamp: new Date().toISOString()
187
+ };
188
+ }
189
+
190
+ // Convert ErrorResponse to appropriate Error class
191
+ const error = createErrorFromResponse(errorData);
192
+
193
+ // Call error callback if provided
194
+ this.options.onError?.(errorData.message, undefined);
195
+
196
+ throw error;
197
+ }
198
+
199
+
200
+
201
+ /**
202
+ * Create a streaming response handler for Server-Sent Events
203
+ */
204
+ protected async handleStreamResponse(
205
+ response: Response
206
+ ): Promise<ReadableStream<Uint8Array>> {
207
+ if (!response.ok) {
208
+ await this.handleErrorResponse(response);
209
+ }
210
+
211
+ if (!response.body) {
212
+ throw new Error('No response body for streaming');
213
+ }
214
+
215
+ return response.body;
216
+ }
217
+
218
+ /**
219
+ * Utility method to log successful operations
220
+ */
221
+ protected logSuccess(operation: string, details?: string): void {
222
+ const message = details
223
+ ? `[HTTP Client] ${operation}: ${details}`
224
+ : `[HTTP Client] ${operation} completed successfully`;
225
+ console.log(message);
226
+ }
227
+
228
+ /**
229
+ * Utility method to log errors
230
+ */
231
+ protected logError(operation: string, error: unknown): void {
232
+ console.error(`[HTTP Client] Error in ${operation}:`, error);
233
+ }
234
+
235
+ /**
236
+ * Check if 503 response is from container provisioning (retryable)
237
+ * vs user application (not retryable)
238
+ */
239
+ private async isContainerProvisioningError(response: Response): Promise<boolean> {
240
+ try {
241
+ // Clone response so we don't consume the original body
242
+ const cloned = response.clone();
243
+ const text = await cloned.text();
244
+
245
+ console.log('[DEBUG] 503 response body:', text.substring(0, 200));
246
+
247
+ // Container package returns specific message for provisioning errors
248
+ const isProvisioning = text.includes('There is no Container instance available');
249
+
250
+ console.log('[DEBUG] Is container provisioning error?', isProvisioning);
251
+
252
+ return isProvisioning;
253
+ } catch (error) {
254
+ console.error('[DEBUG] Error checking response body:', error);
255
+ // If we can't read the body, don't retry to be safe
256
+ return false;
257
+ }
258
+ }
259
+
260
+ private async executeFetch(path: string, options?: RequestInit): Promise<Response> {
261
+ const url = this.options.stub
262
+ ? `http://localhost:${this.options.port}${path}`
263
+ : `${this.baseUrl}${path}`;
264
+ const method = options?.method || "GET";
265
+
266
+ console.log(`[HTTP Client] Making ${method} request to ${url}`);
267
+
268
+ try {
269
+ let response: Response;
270
+
271
+ if (this.options.stub) {
272
+ response = await this.options.stub.containerFetch(
273
+ url,
274
+ options || {},
275
+ this.options.port
276
+ );
277
+ } else {
278
+ response = await fetch(url, options);
279
+ }
280
+
281
+ console.log(
282
+ `[HTTP Client] Response: ${response.status} ${response.statusText}`
283
+ );
284
+
285
+ if (!response.ok) {
286
+ console.error(
287
+ `[HTTP Client] Request failed: ${method} ${url} - ${response.status} ${response.statusText}`
288
+ );
289
+ }
290
+
291
+ return response;
292
+ } catch (error) {
293
+ console.error(`[HTTP Client] Request error: ${method} ${url}`, error);
294
+ throw error;
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,118 @@
1
+ import { BaseHttpClient } from './base-client';
2
+ import type { BaseApiResponse, HttpClientOptions, SessionRequest } from './types';
3
+
4
+ /**
5
+ * Request interface for command execution
6
+ */
7
+ export interface ExecuteRequest extends SessionRequest {
8
+ command: string;
9
+ timeoutMs?: number;
10
+ }
11
+
12
+ /**
13
+ * Response interface for command execution
14
+ */
15
+ export interface ExecuteResponse extends BaseApiResponse {
16
+ stdout: string;
17
+ stderr: string;
18
+ exitCode: number;
19
+ command: string;
20
+ }
21
+
22
+ /**
23
+ * Client for command execution operations
24
+ */
25
+ export class CommandClient extends BaseHttpClient {
26
+ constructor(options: HttpClientOptions = {}) {
27
+ super(options);
28
+ }
29
+
30
+ /**
31
+ * Execute a command and return the complete result
32
+ * @param command - The command to execute
33
+ * @param sessionId - The session ID for this command execution
34
+ * @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
35
+ */
36
+ async execute(
37
+ command: string,
38
+ sessionId: string,
39
+ timeoutMs?: number
40
+ ): Promise<ExecuteResponse> {
41
+ try {
42
+ const data: ExecuteRequest = {
43
+ command,
44
+ sessionId,
45
+ ...(timeoutMs !== undefined && { timeoutMs })
46
+ };
47
+
48
+ const response = await this.post<ExecuteResponse>(
49
+ '/api/execute',
50
+ data
51
+ );
52
+
53
+ this.logSuccess(
54
+ 'Command executed',
55
+ `${command}, Success: ${response.success}`
56
+ );
57
+
58
+ // Call the callback if provided
59
+ this.options.onCommandComplete?.(
60
+ response.success,
61
+ response.exitCode,
62
+ response.stdout,
63
+ response.stderr,
64
+ response.command
65
+ );
66
+
67
+ return response;
68
+ } catch (error) {
69
+ this.logError('execute', error);
70
+
71
+ // Call error callback if provided
72
+ this.options.onError?.(
73
+ error instanceof Error ? error.message : String(error),
74
+ command
75
+ );
76
+
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Execute a command and return a stream of events
83
+ * @param command - The command to execute
84
+ * @param sessionId - The session ID for this command execution
85
+ */
86
+ async executeStream(
87
+ command: string,
88
+ sessionId: string
89
+ ): Promise<ReadableStream<Uint8Array>> {
90
+ try {
91
+ const data = { command, sessionId };
92
+
93
+ const response = await this.doFetch('/api/execute/stream', {
94
+ method: 'POST',
95
+ headers: {
96
+ 'Content-Type': 'application/json',
97
+ },
98
+ body: JSON.stringify(data),
99
+ });
100
+
101
+ const stream = await this.handleStreamResponse(response);
102
+
103
+ this.logSuccess('Command stream started', command);
104
+
105
+ return stream;
106
+ } catch (error) {
107
+ this.logError('executeStream', error);
108
+
109
+ // Call error callback if provided
110
+ this.options.onError?.(
111
+ error instanceof Error ? error.message : String(error),
112
+ command
113
+ );
114
+
115
+ throw error;
116
+ }
117
+ }
118
+ }