@cloudflare/sandbox 0.0.0-e1fa354 → 0.0.0-e489cbb

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 (94) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/Dockerfile +107 -38
  3. package/README.md +89 -771
  4. package/dist/chunk-53JFOF7F.js +2352 -0
  5. package/dist/chunk-53JFOF7F.js.map +1 -0
  6. package/dist/chunk-BFVUNTP4.js +104 -0
  7. package/dist/chunk-BFVUNTP4.js.map +1 -0
  8. package/dist/chunk-EKSWCBCA.js +86 -0
  9. package/dist/chunk-EKSWCBCA.js.map +1 -0
  10. package/dist/chunk-JXZMAU2C.js +559 -0
  11. package/dist/chunk-JXZMAU2C.js.map +1 -0
  12. package/dist/chunk-Z532A7QC.js +78 -0
  13. package/dist/chunk-Z532A7QC.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 +66 -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-D9K2ypln.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 +31 -0
  31. package/dist/security.js +13 -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 +13 -5
  37. package/src/clients/base-client.ts +280 -0
  38. package/src/clients/command-client.ts +115 -0
  39. package/src/clients/file-client.ts +269 -0
  40. package/src/clients/git-client.ts +92 -0
  41. package/src/clients/index.ts +63 -0
  42. package/src/{jupyter-client.ts → clients/interpreter-client.ts} +148 -168
  43. package/src/clients/port-client.ts +105 -0
  44. package/src/clients/process-client.ts +177 -0
  45. package/src/clients/sandbox-client.ts +41 -0
  46. package/src/clients/types.ts +84 -0
  47. package/src/clients/utility-client.ts +94 -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 +164 -0
  52. package/src/index.ts +82 -53
  53. package/src/interpreter.ts +22 -13
  54. package/src/request-handler.ts +69 -43
  55. package/src/sandbox.ts +697 -527
  56. package/src/security.ts +14 -23
  57. package/src/sse-parser.ts +4 -8
  58. package/startup.sh +3 -0
  59. package/tests/base-client.test.ts +328 -0
  60. package/tests/command-client.test.ts +407 -0
  61. package/tests/file-client.test.ts +643 -0
  62. package/tests/file-stream.test.ts +306 -0
  63. package/tests/git-client.test.ts +328 -0
  64. package/tests/port-client.test.ts +301 -0
  65. package/tests/process-client.test.ts +658 -0
  66. package/tests/sandbox.test.ts +465 -0
  67. package/tests/sse-parser.test.ts +290 -0
  68. package/tests/utility-client.test.ts +266 -0
  69. package/tests/wrangler.jsonc +35 -0
  70. package/tsconfig.json +9 -1
  71. package/vitest.config.ts +31 -0
  72. package/container_src/bun.lock +0 -122
  73. package/container_src/circuit-breaker.ts +0 -121
  74. package/container_src/control-process.ts +0 -784
  75. package/container_src/handler/exec.ts +0 -185
  76. package/container_src/handler/file.ts +0 -406
  77. package/container_src/handler/git.ts +0 -130
  78. package/container_src/handler/ports.ts +0 -314
  79. package/container_src/handler/process.ts +0 -568
  80. package/container_src/handler/session.ts +0 -92
  81. package/container_src/index.ts +0 -601
  82. package/container_src/isolation.ts +0 -1038
  83. package/container_src/jupyter-server.ts +0 -579
  84. package/container_src/jupyter-service.ts +0 -461
  85. package/container_src/jupyter_config.py +0 -48
  86. package/container_src/mime-processor.ts +0 -255
  87. package/container_src/package.json +0 -18
  88. package/container_src/shell-escape.ts +0 -42
  89. package/container_src/startup.sh +0 -84
  90. package/container_src/types.ts +0 -131
  91. package/src/client.ts +0 -1009
  92. package/src/errors.ts +0 -218
  93. package/src/interpreter-types.ts +0 -383
  94. package/src/types.ts +0 -502
@@ -1,255 +0,0 @@
1
- export interface ExecutionResult {
2
- type: 'result' | 'stdout' | 'stderr' | 'error' | 'execution_complete';
3
- text?: string;
4
- html?: string;
5
- png?: string; // base64
6
- jpeg?: string; // base64
7
- svg?: string;
8
- latex?: string;
9
- markdown?: string;
10
- javascript?: string;
11
- json?: any;
12
- chart?: ChartData;
13
- data?: any;
14
- metadata?: any;
15
- execution_count?: number;
16
- ename?: string;
17
- evalue?: string;
18
- traceback?: string[];
19
- timestamp: number;
20
- }
21
-
22
- export interface ChartData {
23
- type: 'line' | 'bar' | 'scatter' | 'pie' | 'histogram' | 'heatmap' | 'unknown';
24
- title?: string;
25
- data: any;
26
- layout?: any;
27
- config?: any;
28
- library?: 'matplotlib' | 'plotly' | 'altair' | 'seaborn' | 'unknown';
29
- }
30
-
31
- export function processJupyterMessage(msg: any): ExecutionResult | null {
32
- const msgType = msg.header?.msg_type || msg.msg_type;
33
-
34
- switch (msgType) {
35
- case 'execute_result':
36
- case 'display_data':
37
- return processDisplayData(msg.content.data, msg.content.metadata);
38
-
39
- case 'stream':
40
- return {
41
- type: msg.content.name === 'stdout' ? 'stdout' : 'stderr',
42
- text: msg.content.text,
43
- timestamp: Date.now()
44
- };
45
-
46
- case 'error':
47
- return {
48
- type: 'error',
49
- ename: msg.content.ename,
50
- evalue: msg.content.evalue,
51
- traceback: msg.content.traceback,
52
- timestamp: Date.now()
53
- };
54
-
55
- default:
56
- return null;
57
- }
58
- }
59
-
60
- function processDisplayData(data: any, metadata?: any): ExecutionResult {
61
- const result: ExecutionResult = {
62
- type: 'result',
63
- timestamp: Date.now(),
64
- metadata
65
- };
66
-
67
- // Process different MIME types in order of preference
68
-
69
- // Interactive/Rich formats
70
- if (data['application/vnd.plotly.v1+json']) {
71
- result.chart = extractPlotlyChart(data['application/vnd.plotly.v1+json']);
72
- result.json = data['application/vnd.plotly.v1+json'];
73
- }
74
-
75
- if (data['application/vnd.vega.v5+json']) {
76
- result.chart = extractVegaChart(data['application/vnd.vega.v5+json'], 'vega');
77
- result.json = data['application/vnd.vega.v5+json'];
78
- }
79
-
80
- if (data['application/vnd.vegalite.v4+json'] || data['application/vnd.vegalite.v5+json']) {
81
- const vegaData = data['application/vnd.vegalite.v4+json'] || data['application/vnd.vegalite.v5+json'];
82
- result.chart = extractVegaChart(vegaData, 'vega-lite');
83
- result.json = vegaData;
84
- }
85
-
86
- // HTML content (tables, formatted output)
87
- if (data['text/html']) {
88
- result.html = data['text/html'];
89
-
90
- // Check if it's a pandas DataFrame
91
- if (isPandasDataFrame(data['text/html'])) {
92
- result.data = { type: 'dataframe', html: data['text/html'] };
93
- }
94
- }
95
-
96
- // Images
97
- if (data['image/png']) {
98
- result.png = data['image/png'];
99
-
100
- // Try to detect if it's a chart
101
- if (isLikelyChart(data, metadata)) {
102
- result.chart = {
103
- type: 'unknown',
104
- library: 'matplotlib',
105
- data: { image: data['image/png'] }
106
- };
107
- }
108
- }
109
-
110
- if (data['image/jpeg']) {
111
- result.jpeg = data['image/jpeg'];
112
- }
113
-
114
- if (data['image/svg+xml']) {
115
- result.svg = data['image/svg+xml'];
116
- }
117
-
118
- // Mathematical content
119
- if (data['text/latex']) {
120
- result.latex = data['text/latex'];
121
- }
122
-
123
- // Code
124
- if (data['application/javascript']) {
125
- result.javascript = data['application/javascript'];
126
- }
127
-
128
- // Structured data
129
- if (data['application/json']) {
130
- result.json = data['application/json'];
131
- }
132
-
133
- // Markdown
134
- if (data['text/markdown']) {
135
- result.markdown = data['text/markdown'];
136
- }
137
-
138
- // Plain text (fallback)
139
- if (data['text/plain']) {
140
- result.text = data['text/plain'];
141
- }
142
-
143
- return result;
144
- }
145
-
146
- function extractPlotlyChart(plotlyData: any): ChartData {
147
- const data = plotlyData.data || plotlyData;
148
- const layout = plotlyData.layout || {};
149
-
150
- // Try to detect chart type from traces
151
- let chartType: ChartData['type'] = 'unknown';
152
- if (data && data.length > 0) {
153
- const firstTrace = data[0];
154
- if (firstTrace.type === 'scatter') {
155
- chartType = firstTrace.mode?.includes('lines') ? 'line' : 'scatter';
156
- } else if (firstTrace.type === 'bar') {
157
- chartType = 'bar';
158
- } else if (firstTrace.type === 'pie') {
159
- chartType = 'pie';
160
- } else if (firstTrace.type === 'histogram') {
161
- chartType = 'histogram';
162
- } else if (firstTrace.type === 'heatmap') {
163
- chartType = 'heatmap';
164
- }
165
- }
166
-
167
- return {
168
- type: chartType,
169
- title: layout.title?.text || layout.title,
170
- data: data,
171
- layout: layout,
172
- config: plotlyData.config,
173
- library: 'plotly'
174
- };
175
- }
176
-
177
- function extractVegaChart(vegaData: any, format: 'vega' | 'vega-lite'): ChartData {
178
- // Try to detect chart type from mark or encoding
179
- let chartType: ChartData['type'] = 'unknown';
180
-
181
- if (format === 'vega-lite' && vegaData.mark) {
182
- const mark = typeof vegaData.mark === 'string' ? vegaData.mark : vegaData.mark.type;
183
- switch (mark) {
184
- case 'line':
185
- chartType = 'line';
186
- break;
187
- case 'bar':
188
- chartType = 'bar';
189
- break;
190
- case 'point':
191
- case 'circle':
192
- chartType = 'scatter';
193
- break;
194
- case 'arc':
195
- chartType = 'pie';
196
- break;
197
- case 'rect':
198
- if (vegaData.encoding?.color) {
199
- chartType = 'heatmap';
200
- }
201
- break;
202
- }
203
- }
204
-
205
- return {
206
- type: chartType,
207
- title: vegaData.title,
208
- data: vegaData,
209
- library: 'altair' // Altair outputs Vega-Lite
210
- };
211
- }
212
-
213
- function isPandasDataFrame(html: string): boolean {
214
- // Simple heuristic to detect pandas DataFrame HTML
215
- return html.includes('dataframe') ||
216
- (html.includes('<table') && html.includes('<thead') && html.includes('<tbody'));
217
- }
218
-
219
- function isLikelyChart(data: any, metadata?: any): boolean {
220
- // Check metadata for hints
221
- if (metadata?.needs?.includes('matplotlib')) {
222
- return true;
223
- }
224
-
225
- // Check if other chart formats are present
226
- if (data['application/vnd.plotly.v1+json'] ||
227
- data['application/vnd.vega.v5+json'] ||
228
- data['application/vnd.vegalite.v4+json']) {
229
- return true;
230
- }
231
-
232
- // If only image output without text, likely a chart
233
- if ((data['image/png'] || data['image/svg+xml']) && !data['text/plain']) {
234
- return true;
235
- }
236
-
237
- return false;
238
- }
239
-
240
- export function extractFormats(result: ExecutionResult): string[] {
241
- const formats: string[] = [];
242
-
243
- if (result.text) formats.push('text');
244
- if (result.html) formats.push('html');
245
- if (result.png) formats.push('png');
246
- if (result.jpeg) formats.push('jpeg');
247
- if (result.svg) formats.push('svg');
248
- if (result.latex) formats.push('latex');
249
- if (result.markdown) formats.push('markdown');
250
- if (result.javascript) formats.push('javascript');
251
- if (result.json) formats.push('json');
252
- if (result.chart) formats.push('chart');
253
-
254
- return formats;
255
- }
@@ -1,18 +0,0 @@
1
- {
2
- "name": "sandbox-server",
3
- "version": "1.0.0",
4
- "description": "A server for the sandbox package",
5
- "main": "index.ts",
6
- "scripts": {
7
- "start": "bun run index.ts"
8
- },
9
- "dependencies": {
10
- "@jupyterlab/services": "^7.0.0",
11
- "ws": "^8.16.0",
12
- "uuid": "^9.0.1"
13
- },
14
- "devDependencies": {
15
- "@types/ws": "^8.5.10",
16
- "@types/uuid": "^9.0.7"
17
- }
18
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * Secure shell command utilities to prevent injection attacks
3
- */
4
-
5
- /**
6
- * Escapes a string for safe use in shell commands.
7
- * This follows POSIX shell escaping rules to prevent command injection.
8
- *
9
- * @param str - The string to escape
10
- * @returns The escaped string safe for shell use
11
- */
12
- export function escapeShellArg(str: string): string {
13
- // If string is empty, return empty quotes
14
- if (str === '') {
15
- return "''";
16
- }
17
-
18
- // Check if string contains any characters that need escaping
19
- // Safe characters: alphanumeric, dash, underscore, dot, slash
20
- if (/^[a-zA-Z0-9._\-/]+$/.test(str)) {
21
- return str;
22
- }
23
-
24
- // For strings with special characters, use single quotes and escape single quotes
25
- // Single quotes preserve all characters literally except the single quote itself
26
- // To include a single quote, we end the quoted string, add an escaped quote, and start a new quoted string
27
- return `'${str.replace(/'/g, "'\\''")}'`;
28
- }
29
-
30
- /**
31
- * Escapes a file path for safe use in shell commands.
32
- *
33
- * @param path - The file path to escape
34
- * @returns The escaped path safe for shell use
35
- */
36
- export function escapeShellPath(path: string): string {
37
- // Normalize path to prevent issues with multiple slashes
38
- const normalizedPath = path.replace(/\/+/g, '/');
39
-
40
- // Apply standard shell escaping
41
- return escapeShellArg(normalizedPath);
42
- }
@@ -1,84 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Function to check if Jupyter is ready
4
- check_jupyter_ready() {
5
- # Check if API is responsive and kernelspecs are available
6
- curl -s http://localhost:8888/api/kernelspecs > /dev/null 2>&1
7
- }
8
-
9
- # Function to notify Bun server that Jupyter is ready
10
- notify_jupyter_ready() {
11
- # Create a marker file that the Bun server can check
12
- touch /tmp/jupyter-ready
13
- echo "[Startup] Jupyter is ready, notified Bun server"
14
- }
15
-
16
- # Start Jupyter server in background
17
- echo "[Startup] Starting Jupyter server..."
18
- jupyter server \
19
- --config=/container-server/jupyter_config.py \
20
- > /tmp/jupyter.log 2>&1 &
21
-
22
- JUPYTER_PID=$!
23
-
24
- # Start Bun server immediately (parallel startup)
25
- echo "[Startup] Starting Bun server..."
26
- bun index.ts &
27
- BUN_PID=$!
28
-
29
- # Monitor Jupyter readiness in background
30
- (
31
- echo "[Startup] Monitoring Jupyter readiness in background..."
32
- MAX_ATTEMPTS=60
33
- ATTEMPT=0
34
-
35
- # Track start time for reporting
36
- START_TIME=$(date +%s.%N)
37
-
38
- while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
39
- if check_jupyter_ready; then
40
- notify_jupyter_ready
41
- END_TIME=$(date +%s.%N)
42
- ELAPSED=$(awk "BEGIN {printf \"%.2f\", $END_TIME - $START_TIME}")
43
- echo "[Startup] Jupyter server is ready after $ELAPSED seconds ($ATTEMPT attempts)"
44
- break
45
- fi
46
-
47
- # Check if Jupyter process is still running
48
- if ! kill -0 $JUPYTER_PID 2>/dev/null; then
49
- echo "[Startup] WARNING: Jupyter process died. Check /tmp/jupyter.log for details"
50
- cat /tmp/jupyter.log
51
- # Don't exit - let Bun server continue running in degraded mode
52
- break
53
- fi
54
-
55
- ATTEMPT=$((ATTEMPT + 1))
56
-
57
- # Start with faster checks
58
- if [ $ATTEMPT -eq 1 ]; then
59
- DELAY=0.5 # Start at 0.5s
60
- else
61
- # Exponential backoff with 1.3x multiplier (less aggressive than 1.5x)
62
- DELAY=$(awk "BEGIN {printf \"%.2f\", $DELAY * 1.3}")
63
- # Cap at 2s max (instead of 5s)
64
- if [ $(awk "BEGIN {print ($DELAY > 2)}") -eq 1 ]; then
65
- DELAY=2
66
- fi
67
- fi
68
-
69
- # Log with current delay for transparency
70
- echo "[Startup] Jupyter not ready yet (attempt $ATTEMPT/$MAX_ATTEMPTS, next check in ${DELAY}s)"
71
-
72
- sleep $DELAY
73
- done
74
-
75
- if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
76
- echo "[Startup] WARNING: Jupyter failed to become ready within attempts"
77
- echo "[Startup] Jupyter logs:"
78
- cat /tmp/jupyter.log
79
- # Don't exit - let Bun server continue in degraded mode
80
- fi
81
- ) &
82
-
83
- # Wait for Bun server (main process)
84
- wait $BUN_PID
@@ -1,131 +0,0 @@
1
- import type { ChildProcess } from "node:child_process";
2
-
3
- // Process management types
4
- export type ProcessStatus =
5
- | 'starting'
6
- | 'running'
7
- | 'completed'
8
- | 'failed'
9
- | 'killed'
10
- | 'error';
11
-
12
- export interface ProcessRecord {
13
- id: string;
14
- pid?: number;
15
- command: string;
16
- status: ProcessStatus;
17
- startTime: Date;
18
- endTime?: Date;
19
- exitCode?: number;
20
- stdoutFile?: string; // Path to temp file containing stdout
21
- stderrFile?: string; // Path to temp file containing stderr
22
- stdout: string;
23
- stderr: string;
24
- outputListeners: Set<(stream: 'stdout' | 'stderr', data: string) => void>;
25
- statusListeners: Set<(status: ProcessStatus) => void>;
26
- monitoringInterval?: NodeJS.Timeout; // For polling temp files when streaming
27
- }
28
-
29
- export interface StartProcessRequest {
30
- command: string;
31
- sessionId: string;
32
- options?: {
33
- processId?: string;
34
- timeout?: number;
35
- env?: Record<string, string>;
36
- cwd?: string;
37
- encoding?: string;
38
- autoCleanup?: boolean;
39
- };
40
- }
41
-
42
- export interface ExecuteOptions {
43
- background?: boolean;
44
- cwd?: string | URL;
45
- env?: Record<string, string>;
46
- }
47
-
48
- export interface ExecuteRequest extends ExecuteOptions {
49
- command: string;
50
- sessionId: string;
51
- }
52
-
53
- export interface GitCheckoutRequest {
54
- repoUrl: string;
55
- branch?: string;
56
- targetDir?: string;
57
- sessionId: string;
58
- }
59
-
60
- export interface MkdirRequest {
61
- path: string;
62
- recursive?: boolean;
63
- sessionId: string;
64
- }
65
-
66
- export interface WriteFileRequest {
67
- path: string;
68
- content: string;
69
- encoding?: string;
70
- sessionId: string;
71
- }
72
-
73
- export interface ReadFileRequest {
74
- path: string;
75
- encoding?: string;
76
- sessionId: string;
77
- }
78
-
79
- export interface DeleteFileRequest {
80
- path: string;
81
- sessionId: string;
82
- }
83
-
84
- export interface RenameFileRequest {
85
- oldPath: string;
86
- newPath: string;
87
- sessionId: string;
88
- }
89
-
90
- export interface MoveFileRequest {
91
- sourcePath: string;
92
- destinationPath: string;
93
- sessionId: string;
94
- }
95
-
96
- export interface ListFilesRequest {
97
- path: string;
98
- options?: {
99
- recursive?: boolean;
100
- includeHidden?: boolean;
101
- };
102
- sessionId: string;
103
- }
104
-
105
- export interface ExposePortRequest {
106
- port: number;
107
- name?: string;
108
- }
109
-
110
- export interface UnexposePortRequest {
111
- port: number;
112
- }
113
-
114
- export interface SessionData {
115
- id: string;
116
- activeProcess: ChildProcess | null;
117
- createdAt: Date;
118
- }
119
-
120
- // Session management API types
121
- export interface CreateSessionRequest {
122
- id: string;
123
- env?: Record<string, string>;
124
- cwd?: string;
125
- isolation?: boolean;
126
- }
127
-
128
- export interface SessionExecRequest {
129
- id: string;
130
- command: string;
131
- }