@cloudflare/sandbox 0.1.4 → 0.2.1

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 (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/Dockerfile +37 -11
  3. package/README.md +229 -5
  4. package/container_src/bun.lock +122 -0
  5. package/container_src/handler/exec.ts +4 -2
  6. package/container_src/handler/process.ts +1 -1
  7. package/container_src/index.ts +171 -1
  8. package/container_src/jupyter-server.ts +336 -0
  9. package/container_src/mime-processor.ts +255 -0
  10. package/container_src/package.json +9 -0
  11. package/container_src/startup.sh +52 -0
  12. package/dist/{chunk-YVZ3K26G.js → chunk-CUHYLCMT.js} +9 -21
  13. package/dist/chunk-CUHYLCMT.js.map +1 -0
  14. package/dist/chunk-EGC5IYXA.js +108 -0
  15. package/dist/chunk-EGC5IYXA.js.map +1 -0
  16. package/dist/chunk-FKBV7CZS.js +113 -0
  17. package/dist/chunk-FKBV7CZS.js.map +1 -0
  18. package/dist/{chunk-ZJN2PQOS.js → chunk-IATLC32Y.js} +173 -74
  19. package/dist/chunk-IATLC32Y.js.map +1 -0
  20. package/dist/{chunk-6THNBO4S.js → chunk-S5FFBU4Y.js} +1 -1
  21. package/dist/{chunk-6THNBO4S.js.map → chunk-S5FFBU4Y.js.map} +1 -1
  22. package/dist/chunk-SYMWNYWA.js +185 -0
  23. package/dist/chunk-SYMWNYWA.js.map +1 -0
  24. package/dist/{client-BXYlxy-j.d.ts → client-C7rKCYBD.d.ts} +42 -4
  25. package/dist/client.d.ts +2 -1
  26. package/dist/client.js +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.js +10 -4
  29. package/dist/interpreter-types.d.ts +259 -0
  30. package/dist/interpreter-types.js +9 -0
  31. package/dist/interpreter-types.js.map +1 -0
  32. package/dist/interpreter.d.ts +33 -0
  33. package/dist/interpreter.js +8 -0
  34. package/dist/interpreter.js.map +1 -0
  35. package/dist/jupyter-client.d.ts +4 -0
  36. package/dist/jupyter-client.js +8 -0
  37. package/dist/jupyter-client.js.map +1 -0
  38. package/dist/request-handler.d.ts +2 -1
  39. package/dist/request-handler.js +7 -3
  40. package/dist/sandbox.d.ts +2 -1
  41. package/dist/sandbox.js +7 -3
  42. package/dist/types.d.ts +8 -0
  43. package/dist/types.js +1 -1
  44. package/package.json +1 -1
  45. package/src/client.ts +37 -54
  46. package/src/index.ts +13 -4
  47. package/src/interpreter-types.ts +383 -0
  48. package/src/interpreter.ts +150 -0
  49. package/src/jupyter-client.ts +266 -0
  50. package/src/sandbox.ts +281 -153
  51. package/src/types.ts +15 -0
  52. package/dist/chunk-YVZ3K26G.js.map +0 -1
  53. package/dist/chunk-ZJN2PQOS.js.map +0 -1
@@ -25,6 +25,7 @@ import {
25
25
  handleStartProcessRequest,
26
26
  handleStreamProcessLogsRequest,
27
27
  } from "./handler/process";
28
+ import { type CreateContextRequest, JupyterServer } from "./jupyter-server";
28
29
  import type { ProcessRecord, SessionData } from "./types";
29
30
 
30
31
  // In-memory session storage (in production, you'd want to use a proper database)
@@ -55,8 +56,28 @@ function cleanupOldSessions() {
55
56
  // Run cleanup every 10 minutes
56
57
  setInterval(cleanupOldSessions, 10 * 60 * 1000);
57
58
 
59
+ // Initialize Jupyter server
60
+ const jupyterServer = new JupyterServer();
61
+ let jupyterInitialized = false;
62
+
63
+ // Initialize Jupyter immediately since startup.sh ensures it's ready
64
+ (async () => {
65
+ try {
66
+ await jupyterServer.initialize();
67
+ jupyterInitialized = true;
68
+ console.log("[Container] Jupyter integration initialized successfully");
69
+ } catch (error) {
70
+ console.error("[Container] Failed to initialize Jupyter:", error);
71
+ // Log more details to help debug
72
+ if (error instanceof Error) {
73
+ console.error("[Container] Error details:", error.message);
74
+ console.error("[Container] Stack trace:", error.stack);
75
+ }
76
+ }
77
+ })();
78
+
58
79
  const server = serve({
59
- fetch(req: Request) {
80
+ async fetch(req: Request) {
60
81
  const url = new URL(req.url);
61
82
  const pathname = url.pathname;
62
83
 
@@ -159,6 +180,7 @@ const server = serve({
159
180
  JSON.stringify({
160
181
  message: "pong",
161
182
  timestamp: new Date().toISOString(),
183
+ jupyter: jupyterInitialized ? "ready" : "not ready",
162
184
  }),
163
185
  {
164
186
  headers: {
@@ -280,7 +302,151 @@ const server = serve({
280
302
  }
281
303
  break;
282
304
 
305
+ // Code interpreter endpoints
306
+ case "/api/contexts":
307
+ if (req.method === "POST") {
308
+ if (!jupyterInitialized) {
309
+ return new Response(
310
+ JSON.stringify({
311
+ error: "Jupyter server is not ready. Please try again in a moment."
312
+ }),
313
+ {
314
+ status: 503,
315
+ headers: {
316
+ "Content-Type": "application/json",
317
+ ...corsHeaders,
318
+ },
319
+ }
320
+ );
321
+ }
322
+ try {
323
+ const body = await req.json() as CreateContextRequest;
324
+ const context = await jupyterServer.createContext(body);
325
+ return new Response(
326
+ JSON.stringify({
327
+ id: context.id,
328
+ language: context.language,
329
+ cwd: context.cwd,
330
+ createdAt: context.createdAt,
331
+ lastUsed: context.lastUsed
332
+ }),
333
+ {
334
+ headers: {
335
+ "Content-Type": "application/json",
336
+ ...corsHeaders,
337
+ },
338
+ }
339
+ );
340
+ } catch (error) {
341
+ console.error("[Container] Error creating context:", error);
342
+ return new Response(
343
+ JSON.stringify({
344
+ error: error instanceof Error ? error.message : "Failed to create context"
345
+ }),
346
+ {
347
+ status: 500,
348
+ headers: {
349
+ "Content-Type": "application/json",
350
+ ...corsHeaders,
351
+ },
352
+ }
353
+ );
354
+ }
355
+ } else if (req.method === "GET") {
356
+ if (!jupyterInitialized) {
357
+ return new Response(
358
+ JSON.stringify({ contexts: [] }),
359
+ {
360
+ headers: {
361
+ "Content-Type": "application/json",
362
+ ...corsHeaders,
363
+ },
364
+ }
365
+ );
366
+ }
367
+ const contexts = await jupyterServer.listContexts();
368
+ return new Response(
369
+ JSON.stringify({ contexts }),
370
+ {
371
+ headers: {
372
+ "Content-Type": "application/json",
373
+ ...corsHeaders,
374
+ },
375
+ }
376
+ );
377
+ }
378
+ break;
379
+
380
+ case "/api/execute/code":
381
+ if (req.method === "POST") {
382
+ if (!jupyterInitialized) {
383
+ return new Response(
384
+ JSON.stringify({
385
+ error: "Jupyter server is not ready. Please try again in a moment."
386
+ }),
387
+ {
388
+ status: 503,
389
+ headers: {
390
+ "Content-Type": "application/json",
391
+ ...corsHeaders,
392
+ },
393
+ }
394
+ );
395
+ }
396
+ try {
397
+ const body = await req.json() as { context_id: string; code: string; language?: string };
398
+ return await jupyterServer.executeCode(body.context_id, body.code, body.language);
399
+ } catch (error) {
400
+ console.error("[Container] Error executing code:", error);
401
+ return new Response(
402
+ JSON.stringify({
403
+ error: error instanceof Error ? error.message : "Failed to execute code"
404
+ }),
405
+ {
406
+ status: 500,
407
+ headers: {
408
+ "Content-Type": "application/json",
409
+ ...corsHeaders,
410
+ },
411
+ }
412
+ );
413
+ }
414
+ }
415
+ break;
416
+
283
417
  default:
418
+ // Handle dynamic routes for contexts
419
+ if (pathname.startsWith("/api/contexts/") && pathname.split('/').length === 4) {
420
+ const contextId = pathname.split('/')[3];
421
+ if (req.method === "DELETE") {
422
+ try {
423
+ await jupyterServer.deleteContext(contextId);
424
+ return new Response(
425
+ JSON.stringify({ success: true }),
426
+ {
427
+ headers: {
428
+ "Content-Type": "application/json",
429
+ ...corsHeaders,
430
+ },
431
+ }
432
+ );
433
+ } catch (error) {
434
+ return new Response(
435
+ JSON.stringify({
436
+ error: error instanceof Error ? error.message : "Failed to delete context"
437
+ }),
438
+ {
439
+ status: error instanceof Error && error.message.includes("not found") ? 404 : 500,
440
+ headers: {
441
+ "Content-Type": "application/json",
442
+ ...corsHeaders,
443
+ },
444
+ }
445
+ );
446
+ }
447
+ }
448
+ }
449
+
284
450
  // Handle dynamic routes for individual processes
285
451
  if (pathname.startsWith("/api/process/")) {
286
452
  const segments = pathname.split('/');
@@ -357,5 +523,9 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
357
523
  console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
358
524
  console.log(` DELETE /api/process/kill-all - Kill all processes`);
359
525
  console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
526
+ console.log(` POST /api/contexts - Create a code execution context`);
527
+ console.log(` GET /api/contexts - List all contexts`);
528
+ console.log(` DELETE /api/contexts/{id} - Delete a context`);
529
+ console.log(` POST /api/execute/code - Execute code in a context (streaming)`);
360
530
  console.log(` GET /api/ping - Health check`);
361
531
  console.log(` GET /api/commands - List available commands`);
@@ -0,0 +1,336 @@
1
+ import { type Kernel, KernelManager, ServerConnection } from "@jupyterlab/services";
2
+ import type {
3
+ IDisplayDataMsg,
4
+ IErrorMsg,
5
+ IExecuteResultMsg,
6
+ IIOPubMessage,
7
+ IStreamMsg
8
+ } from "@jupyterlab/services/lib/kernel/messages";
9
+ import {
10
+ isDisplayDataMsg,
11
+ isErrorMsg,
12
+ isExecuteResultMsg,
13
+ isStreamMsg
14
+ } from "@jupyterlab/services/lib/kernel/messages";
15
+ import { v4 as uuidv4 } from "uuid";
16
+ import type { ExecutionResult } from "./mime-processor";
17
+ import { processJupyterMessage } from "./mime-processor";
18
+
19
+ export interface JupyterContext {
20
+ id: string;
21
+ language: string;
22
+ connection: Kernel.IKernelConnection;
23
+ cwd: string;
24
+ createdAt: Date;
25
+ lastUsed: Date;
26
+ }
27
+
28
+ export interface CreateContextRequest {
29
+ language?: string;
30
+ cwd?: string;
31
+ envVars?: Record<string, string>;
32
+ }
33
+
34
+ export interface ExecuteCodeRequest {
35
+ context_id?: string;
36
+ code: string;
37
+ language?: string;
38
+ env_vars?: Record<string, string>;
39
+ }
40
+
41
+ export class JupyterServer {
42
+ private kernelManager: KernelManager;
43
+ private contexts = new Map<string, JupyterContext>();
44
+ private defaultContexts = new Map<string, string>(); // language -> context_id
45
+
46
+ constructor() {
47
+ // Configure connection to local Jupyter server
48
+ const serverSettings = ServerConnection.makeSettings({
49
+ baseUrl: "http://localhost:8888",
50
+ token: "",
51
+ appUrl: "",
52
+ wsUrl: "ws://localhost:8888",
53
+ appendToken: false,
54
+ init: {
55
+ headers: {
56
+ 'Content-Type': 'application/json'
57
+ }
58
+ }
59
+ });
60
+
61
+ this.kernelManager = new KernelManager({ serverSettings });
62
+ }
63
+
64
+ async initialize() {
65
+ await this.kernelManager.ready;
66
+ console.log("[JupyterServer] Kernel manager initialized");
67
+
68
+ // Create default Python context
69
+ const pythonContext = await this.createContext({ language: "python" });
70
+ this.defaultContexts.set("python", pythonContext.id);
71
+ console.log(
72
+ "[JupyterServer] Default Python context created:",
73
+ pythonContext.id
74
+ );
75
+ }
76
+
77
+ async createContext(req: CreateContextRequest): Promise<JupyterContext> {
78
+ const language = req.language || "python";
79
+ const cwd = req.cwd || "/workspace";
80
+
81
+ const kernelModel = await this.kernelManager.startNew({
82
+ name: this.getKernelName(language),
83
+ });
84
+
85
+ const connection = this.kernelManager.connectTo({ model: kernelModel });
86
+
87
+ const context: JupyterContext = {
88
+ id: uuidv4(),
89
+ language,
90
+ connection,
91
+ cwd,
92
+ createdAt: new Date(),
93
+ lastUsed: new Date(),
94
+ };
95
+
96
+ this.contexts.set(context.id, context);
97
+
98
+ // Set working directory
99
+ if (cwd !== "/workspace") {
100
+ await this.changeWorkingDirectory(context, cwd);
101
+ }
102
+
103
+ // Set environment variables if provided
104
+ if (req.envVars) {
105
+ await this.setEnvironmentVariables(context, req.envVars);
106
+ }
107
+
108
+ return context;
109
+ }
110
+
111
+ async executeCode(
112
+ contextId: string | undefined,
113
+ code: string,
114
+ language?: string
115
+ ): Promise<Response> {
116
+ let context: JupyterContext | undefined;
117
+
118
+ if (contextId) {
119
+ context = this.contexts.get(contextId);
120
+ if (!context) {
121
+ return new Response(
122
+ JSON.stringify({ error: `Context ${contextId} not found` }),
123
+ {
124
+ status: 404,
125
+ headers: { "Content-Type": "application/json" },
126
+ }
127
+ );
128
+ }
129
+ } else if (language) {
130
+ // Use default context for the language
131
+ const defaultContextId = this.defaultContexts.get(language);
132
+ if (defaultContextId) {
133
+ context = this.contexts.get(defaultContextId);
134
+ }
135
+
136
+ // Create new default context if needed
137
+ if (!context) {
138
+ context = await this.createContext({ language });
139
+ this.defaultContexts.set(language, context.id);
140
+ }
141
+ } else {
142
+ // Use default Python context
143
+ const pythonContextId = this.defaultContexts.get("python");
144
+ context = pythonContextId
145
+ ? this.contexts.get(pythonContextId)
146
+ : undefined;
147
+ }
148
+
149
+ if (!context) {
150
+ return new Response(JSON.stringify({ error: "No context available" }), {
151
+ status: 400,
152
+ headers: { "Content-Type": "application/json" },
153
+ });
154
+ }
155
+
156
+ // Update last used
157
+ context.lastUsed = new Date();
158
+
159
+ // Execute with streaming
160
+ return this.streamExecution(context.connection, code);
161
+ }
162
+
163
+ private async streamExecution(
164
+ connection: Kernel.IKernelConnection,
165
+ code: string
166
+ ): Promise<Response> {
167
+ const stream = new ReadableStream({
168
+ async start(controller) {
169
+ const future = connection.requestExecute({
170
+ code,
171
+ stop_on_error: false,
172
+ store_history: true,
173
+ silent: false,
174
+ allow_stdin: false,
175
+ });
176
+
177
+ // Handle different message types
178
+ future.onIOPub = (msg: IIOPubMessage) => {
179
+ const result = processJupyterMessage(msg);
180
+ if (result) {
181
+ controller.enqueue(
182
+ new TextEncoder().encode(`${JSON.stringify(result)}\n`)
183
+ );
184
+ }
185
+ };
186
+
187
+ future.onReply = (msg: any) => {
188
+ if (msg.content.status === "ok") {
189
+ controller.enqueue(
190
+ new TextEncoder().encode(
191
+ `${JSON.stringify({
192
+ type: "execution_complete",
193
+ execution_count: msg.content.execution_count,
194
+ })}\n`
195
+ )
196
+ );
197
+ } else if (msg.content.status === "error") {
198
+ controller.enqueue(
199
+ new TextEncoder().encode(
200
+ `${JSON.stringify({
201
+ type: "error",
202
+ ename: msg.content.ename,
203
+ evalue: msg.content.evalue,
204
+ traceback: msg.content.traceback,
205
+ })}\n`
206
+ )
207
+ );
208
+ }
209
+ controller.close();
210
+ };
211
+
212
+ future.onStdin = (msg: any) => {
213
+ // We don't support stdin for now
214
+ console.warn("[JupyterServer] Stdin requested but not supported");
215
+ };
216
+ },
217
+ });
218
+
219
+ return new Response(stream, {
220
+ headers: {
221
+ "Content-Type": "text/event-stream",
222
+ "Cache-Control": "no-cache",
223
+ Connection: "keep-alive",
224
+ },
225
+ });
226
+ }
227
+
228
+ private getKernelName(language: string): string {
229
+ const kernelMap: Record<string, string> = {
230
+ python: "python3",
231
+ javascript: "javascript",
232
+ typescript: "javascript",
233
+ js: "javascript",
234
+ ts: "javascript",
235
+ };
236
+ return kernelMap[language.toLowerCase()] || "python3";
237
+ }
238
+
239
+ private async changeWorkingDirectory(context: JupyterContext, cwd: string) {
240
+ const code =
241
+ context.language === "python"
242
+ ? `import os; os.chdir('${cwd}')`
243
+ : `process.chdir('${cwd}')`;
244
+
245
+ const future = context.connection.requestExecute({
246
+ code,
247
+ silent: true,
248
+ store_history: false,
249
+ });
250
+
251
+ return future.done;
252
+ }
253
+
254
+ private async setEnvironmentVariables(
255
+ context: JupyterContext,
256
+ envVars: Record<string, string>
257
+ ) {
258
+ const commands: string[] = [];
259
+
260
+ for (const [key, value] of Object.entries(envVars)) {
261
+ if (context.language === "python") {
262
+ commands.push(`import os; os.environ['${key}'] = '${value}'`);
263
+ } else if (
264
+ context.language === "javascript" ||
265
+ context.language === "typescript"
266
+ ) {
267
+ commands.push(`process.env['${key}'] = '${value}'`);
268
+ }
269
+ }
270
+
271
+ if (commands.length > 0) {
272
+ const code = commands.join("\n");
273
+ const future = context.connection.requestExecute({
274
+ code,
275
+ silent: true,
276
+ store_history: false,
277
+ });
278
+
279
+ return future.done;
280
+ }
281
+ }
282
+
283
+ async listContexts(): Promise<
284
+ Array<{
285
+ id: string;
286
+ language: string;
287
+ cwd: string;
288
+ createdAt: Date;
289
+ lastUsed: Date;
290
+ }>
291
+ > {
292
+ return Array.from(this.contexts.values()).map((ctx) => ({
293
+ id: ctx.id,
294
+ language: ctx.language,
295
+ cwd: ctx.cwd,
296
+ createdAt: ctx.createdAt,
297
+ lastUsed: ctx.lastUsed,
298
+ }));
299
+ }
300
+
301
+ async deleteContext(contextId: string): Promise<void> {
302
+ const context = this.contexts.get(contextId);
303
+ if (!context) {
304
+ throw new Error(`Context ${contextId} not found`);
305
+ }
306
+
307
+ // Shutdown the kernel
308
+ await context.connection.shutdown();
309
+
310
+ // Remove from maps
311
+ this.contexts.delete(contextId);
312
+
313
+ // Remove from default contexts if it was a default
314
+ for (const [lang, id] of this.defaultContexts.entries()) {
315
+ if (id === contextId) {
316
+ this.defaultContexts.delete(lang);
317
+ break;
318
+ }
319
+ }
320
+ }
321
+
322
+ async shutdown() {
323
+ // Shutdown all kernels
324
+ for (const context of this.contexts.values()) {
325
+ try {
326
+ await context.connection.shutdown();
327
+ } catch (error) {
328
+ console.error("[JupyterServer] Error shutting down kernel:", error);
329
+ }
330
+ }
331
+
332
+ this.contexts.clear();
333
+ this.defaultContexts.clear();
334
+ }
335
+ }
336
+