@cloudflare/sandbox 0.0.0-f5fcd52 → 0.0.0-fb3c9c2
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.
- package/CHANGELOG.md +137 -0
- package/Dockerfile +47 -36
- package/README.md +805 -24
- package/container_src/bun.lock +122 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/control-process.ts +784 -0
- package/container_src/handler/exec.ts +185 -0
- package/container_src/handler/file.ts +406 -0
- package/container_src/handler/git.ts +130 -0
- package/container_src/handler/ports.ts +314 -0
- package/container_src/handler/process.ts +568 -0
- package/container_src/handler/session.ts +92 -0
- package/container_src/index.ts +411 -3006
- package/container_src/isolation.ts +1038 -0
- package/container_src/jupyter-server.ts +579 -0
- package/container_src/jupyter-service.ts +461 -0
- package/container_src/jupyter_config.py +48 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/shell-escape.ts +42 -0
- package/container_src/startup.sh +84 -0
- package/container_src/types.ts +131 -0
- package/package.json +5 -9
- package/src/client.ts +333 -1413
- package/src/errors.ts +218 -0
- package/src/index.ts +60 -10
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +349 -0
- package/src/request-handler.ts +69 -20
- package/src/sandbox.ts +594 -105
- package/src/security.ts +113 -0
- package/src/sse-parser.ts +147 -0
- package/src/types.ts +502 -0
- package/tsconfig.json +1 -1
- package/tests/client.example.ts +0 -308
- package/tests/connection-test.ts +0 -81
- package/tests/simple-test.ts +0 -81
- package/tests/test1.ts +0 -281
- package/tests/test2.ts +0 -929
package/src/sandbox.ts
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { Container, getContainer } from "@cloudflare/containers";
|
|
2
|
-
import {
|
|
2
|
+
import { CodeInterpreter } from "./interpreter";
|
|
3
|
+
import type {
|
|
4
|
+
CodeContext,
|
|
5
|
+
CreateContextOptions,
|
|
6
|
+
ExecutionResult,
|
|
7
|
+
RunCodeOptions,
|
|
8
|
+
} from "./interpreter-types";
|
|
9
|
+
import { JupyterClient } from "./jupyter-client";
|
|
3
10
|
import { isLocalhostPattern } from "./request-handler";
|
|
11
|
+
import {
|
|
12
|
+
logSecurityEvent,
|
|
13
|
+
SecurityError,
|
|
14
|
+
sanitizeSandboxId,
|
|
15
|
+
validatePort,
|
|
16
|
+
} from "./security";
|
|
17
|
+
import { parseSSEStream } from "./sse-parser";
|
|
18
|
+
import type {
|
|
19
|
+
ExecEvent,
|
|
20
|
+
ExecOptions,
|
|
21
|
+
ExecResult,
|
|
22
|
+
ExecuteResponse,
|
|
23
|
+
ExecutionSession,
|
|
24
|
+
ISandbox,
|
|
25
|
+
Process,
|
|
26
|
+
ProcessOptions,
|
|
27
|
+
ProcessStatus,
|
|
28
|
+
StreamOptions,
|
|
29
|
+
} from "./types";
|
|
30
|
+
import { ProcessNotFoundError, SandboxError } from "./types";
|
|
4
31
|
|
|
5
32
|
export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
6
33
|
const stub = getContainer(ns, id);
|
|
@@ -11,26 +38,26 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
|
11
38
|
return stub;
|
|
12
39
|
}
|
|
13
40
|
|
|
14
|
-
export class Sandbox<Env = unknown> extends Container<Env> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
41
|
+
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
42
|
+
defaultPort = 3000; // Default port for the container's Bun server
|
|
43
|
+
sleepAfter = "20m"; // Keep container warm for 20 minutes to avoid cold starts
|
|
44
|
+
client: JupyterClient;
|
|
18
45
|
private sandboxName: string | null = null;
|
|
46
|
+
private codeInterpreter: CodeInterpreter;
|
|
47
|
+
private defaultSession: ExecutionSession | null = null;
|
|
19
48
|
|
|
20
49
|
constructor(ctx: DurableObjectState, env: Env) {
|
|
21
50
|
super(ctx, env);
|
|
22
|
-
this.client = new
|
|
23
|
-
onCommandComplete: (success, exitCode, _stdout, _stderr, command
|
|
51
|
+
this.client = new JupyterClient({
|
|
52
|
+
onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {
|
|
24
53
|
console.log(
|
|
25
54
|
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
26
55
|
);
|
|
27
56
|
},
|
|
28
|
-
onCommandStart: (command
|
|
29
|
-
console.log(
|
|
30
|
-
`[Container] Command started: ${command} ${args.join(" ")}`
|
|
31
|
-
);
|
|
57
|
+
onCommandStart: (command) => {
|
|
58
|
+
console.log(`[Container] Command started: ${command}`);
|
|
32
59
|
},
|
|
33
|
-
onError: (error, _command
|
|
60
|
+
onError: (error, _command) => {
|
|
34
61
|
console.error(`[Container] Command error: ${error}`);
|
|
35
62
|
},
|
|
36
63
|
onOutput: (stream, data, _command) => {
|
|
@@ -40,9 +67,13 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
40
67
|
stub: this,
|
|
41
68
|
});
|
|
42
69
|
|
|
70
|
+
// Initialize code interpreter
|
|
71
|
+
this.codeInterpreter = new CodeInterpreter(this);
|
|
72
|
+
|
|
43
73
|
// Load the sandbox name from storage on initialization
|
|
44
74
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
45
|
-
this.sandboxName =
|
|
75
|
+
this.sandboxName =
|
|
76
|
+
(await this.ctx.storage.get<string>("sandboxName")) || null;
|
|
46
77
|
});
|
|
47
78
|
}
|
|
48
79
|
|
|
@@ -50,7 +81,7 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
50
81
|
async setSandboxName(name: string): Promise<void> {
|
|
51
82
|
if (!this.sandboxName) {
|
|
52
83
|
this.sandboxName = name;
|
|
53
|
-
await this.ctx.storage.put(
|
|
84
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
54
85
|
console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
|
|
55
86
|
}
|
|
56
87
|
}
|
|
@@ -59,6 +90,11 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
59
90
|
async setEnvVars(envVars: Record<string, string>): Promise<void> {
|
|
60
91
|
this.envVars = { ...this.envVars, ...envVars };
|
|
61
92
|
console.log(`[Sandbox] Updated environment variables`);
|
|
93
|
+
|
|
94
|
+
// If we have a default session, update its environment too
|
|
95
|
+
if (this.defaultSession) {
|
|
96
|
+
await this.defaultSession.setEnvVars(envVars);
|
|
97
|
+
}
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
override onStart() {
|
|
@@ -67,30 +103,21 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
67
103
|
|
|
68
104
|
override onStop() {
|
|
69
105
|
console.log("Sandbox successfully shut down");
|
|
70
|
-
if (this.client) {
|
|
71
|
-
this.client.clearSession();
|
|
72
|
-
}
|
|
73
106
|
}
|
|
74
107
|
|
|
75
108
|
override onError(error: unknown) {
|
|
76
109
|
console.log("Sandbox error:", error);
|
|
77
110
|
}
|
|
78
111
|
|
|
79
|
-
// Override fetch to
|
|
112
|
+
// Override fetch to route internal container requests to appropriate ports
|
|
80
113
|
override async fetch(request: Request): Promise<Response> {
|
|
81
114
|
const url = new URL(request.url);
|
|
82
115
|
|
|
83
|
-
// Capture the hostname from the first request
|
|
84
|
-
if (!this.workerHostname) {
|
|
85
|
-
this.workerHostname = url.hostname;
|
|
86
|
-
console.log(`[Sandbox] Captured hostname: ${this.workerHostname}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
116
|
// Capture and store the sandbox name from the header if present
|
|
90
|
-
if (!this.sandboxName && request.headers.has(
|
|
91
|
-
const name = request.headers.get(
|
|
117
|
+
if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
|
|
118
|
+
const name = request.headers.get("X-Sandbox-Name")!;
|
|
92
119
|
this.sandboxName = name;
|
|
93
|
-
await this.ctx.storage.put(
|
|
120
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
94
121
|
console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
|
|
95
122
|
}
|
|
96
123
|
|
|
@@ -108,102 +135,161 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
108
135
|
return parseInt(proxyMatch[1]);
|
|
109
136
|
}
|
|
110
137
|
|
|
138
|
+
if (url.port) {
|
|
139
|
+
return parseInt(url.port);
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
// All other requests go to control plane on port 3000
|
|
112
143
|
// This includes /api/* endpoints and any other control requests
|
|
113
144
|
return 3000;
|
|
114
145
|
}
|
|
115
146
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
147
|
+
// Helper to ensure default session is initialized
|
|
148
|
+
private async ensureDefaultSession(): Promise<ExecutionSession> {
|
|
149
|
+
if (!this.defaultSession) {
|
|
150
|
+
const sessionId = `sandbox-${this.sandboxName || 'default'}`;
|
|
151
|
+
this.defaultSession = await this.createSession({
|
|
152
|
+
id: sessionId,
|
|
153
|
+
env: this.envVars || {},
|
|
154
|
+
cwd: '/workspace',
|
|
155
|
+
isolation: true
|
|
156
|
+
});
|
|
157
|
+
console.log(`[Sandbox] Default session initialized: ${sessionId}`);
|
|
119
158
|
}
|
|
120
|
-
return this.
|
|
159
|
+
return this.defaultSession;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
|
|
164
|
+
const session = await this.ensureDefaultSession();
|
|
165
|
+
return session.exec(command, options);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async startProcess(
|
|
169
|
+
command: string,
|
|
170
|
+
options?: ProcessOptions
|
|
171
|
+
): Promise<Process> {
|
|
172
|
+
const session = await this.ensureDefaultSession();
|
|
173
|
+
return session.startProcess(command, options);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async listProcesses(): Promise<Process[]> {
|
|
177
|
+
const session = await this.ensureDefaultSession();
|
|
178
|
+
return session.listProcesses();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async getProcess(id: string): Promise<Process | null> {
|
|
182
|
+
const session = await this.ensureDefaultSession();
|
|
183
|
+
return session.getProcess(id);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async killProcess(id: string, signal?: string): Promise<void> {
|
|
187
|
+
const session = await this.ensureDefaultSession();
|
|
188
|
+
return session.killProcess(id, signal);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async killAllProcesses(): Promise<number> {
|
|
192
|
+
const session = await this.ensureDefaultSession();
|
|
193
|
+
return session.killAllProcesses();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async cleanupCompletedProcesses(): Promise<number> {
|
|
197
|
+
const session = await this.ensureDefaultSession();
|
|
198
|
+
return session.cleanupCompletedProcesses();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async getProcessLogs(
|
|
202
|
+
id: string
|
|
203
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
204
|
+
const session = await this.ensureDefaultSession();
|
|
205
|
+
return session.getProcessLogs(id);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Streaming methods - delegates to default session
|
|
209
|
+
async execStream(
|
|
210
|
+
command: string,
|
|
211
|
+
options?: StreamOptions
|
|
212
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
213
|
+
const session = await this.ensureDefaultSession();
|
|
214
|
+
return session.execStream(command, options);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async streamProcessLogs(
|
|
218
|
+
processId: string,
|
|
219
|
+
options?: { signal?: AbortSignal }
|
|
220
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
221
|
+
const session = await this.ensureDefaultSession();
|
|
222
|
+
return session.streamProcessLogs(processId, options);
|
|
121
223
|
}
|
|
122
224
|
|
|
123
225
|
async gitCheckout(
|
|
124
226
|
repoUrl: string,
|
|
125
|
-
options: { branch?: string; targetDir?: string
|
|
227
|
+
options: { branch?: string; targetDir?: string }
|
|
126
228
|
) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
repoUrl,
|
|
130
|
-
options.branch,
|
|
131
|
-
options.targetDir
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
229
|
+
const session = await this.ensureDefaultSession();
|
|
230
|
+
return session.gitCheckout(repoUrl, options);
|
|
135
231
|
}
|
|
136
232
|
|
|
137
|
-
async mkdir(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
) {
|
|
141
|
-
if (options?.stream) {
|
|
142
|
-
return this.client.mkdirStream(path, options.recursive);
|
|
143
|
-
}
|
|
144
|
-
return this.client.mkdir(path, options.recursive);
|
|
233
|
+
async mkdir(path: string, options: { recursive?: boolean } = {}) {
|
|
234
|
+
const session = await this.ensureDefaultSession();
|
|
235
|
+
return session.mkdir(path, options);
|
|
145
236
|
}
|
|
146
237
|
|
|
147
238
|
async writeFile(
|
|
148
239
|
path: string,
|
|
149
240
|
content: string,
|
|
150
|
-
options: { encoding?: string
|
|
241
|
+
options: { encoding?: string } = {}
|
|
151
242
|
) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
return this.client.writeFile(path, content, options.encoding);
|
|
243
|
+
const session = await this.ensureDefaultSession();
|
|
244
|
+
return session.writeFile(path, content, options);
|
|
156
245
|
}
|
|
157
246
|
|
|
158
|
-
async deleteFile(path: string
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
return this.client.deleteFile(path);
|
|
247
|
+
async deleteFile(path: string) {
|
|
248
|
+
const session = await this.ensureDefaultSession();
|
|
249
|
+
return session.deleteFile(path);
|
|
163
250
|
}
|
|
164
251
|
|
|
165
|
-
async renameFile(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
options: { stream?: boolean } = {}
|
|
169
|
-
) {
|
|
170
|
-
if (options?.stream) {
|
|
171
|
-
return this.client.renameFileStream(oldPath, newPath);
|
|
172
|
-
}
|
|
173
|
-
return this.client.renameFile(oldPath, newPath);
|
|
252
|
+
async renameFile(oldPath: string, newPath: string) {
|
|
253
|
+
const session = await this.ensureDefaultSession();
|
|
254
|
+
return session.renameFile(oldPath, newPath);
|
|
174
255
|
}
|
|
175
256
|
|
|
176
|
-
async moveFile(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return this.client.moveFile(sourcePath, destinationPath);
|
|
257
|
+
async moveFile(sourcePath: string, destinationPath: string) {
|
|
258
|
+
const session = await this.ensureDefaultSession();
|
|
259
|
+
return session.moveFile(sourcePath, destinationPath);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async readFile(path: string, options: { encoding?: string } = {}) {
|
|
263
|
+
const session = await this.ensureDefaultSession();
|
|
264
|
+
return session.readFile(path, options);
|
|
185
265
|
}
|
|
186
266
|
|
|
187
|
-
async
|
|
267
|
+
async listFiles(
|
|
188
268
|
path: string,
|
|
189
|
-
options: {
|
|
269
|
+
options: {
|
|
270
|
+
recursive?: boolean;
|
|
271
|
+
includeHidden?: boolean;
|
|
272
|
+
} = {}
|
|
190
273
|
) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
return this.client.readFile(path, options.encoding);
|
|
274
|
+
const session = await this.ensureDefaultSession();
|
|
275
|
+
return session.listFiles(path, options);
|
|
195
276
|
}
|
|
196
277
|
|
|
197
|
-
async exposePort(port: number, options
|
|
278
|
+
async exposePort(port: number, options: { name?: string; hostname: string }) {
|
|
198
279
|
await this.client.exposePort(port, options?.name);
|
|
199
280
|
|
|
200
281
|
// We need the sandbox name to construct preview URLs
|
|
201
282
|
if (!this.sandboxName) {
|
|
202
|
-
throw new Error(
|
|
283
|
+
throw new Error(
|
|
284
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
285
|
+
);
|
|
203
286
|
}
|
|
204
287
|
|
|
205
|
-
const
|
|
206
|
-
|
|
288
|
+
const url = this.constructPreviewUrl(
|
|
289
|
+
port,
|
|
290
|
+
this.sandboxName,
|
|
291
|
+
options.hostname
|
|
292
|
+
);
|
|
207
293
|
|
|
208
294
|
return {
|
|
209
295
|
url,
|
|
@@ -213,20 +299,41 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
213
299
|
}
|
|
214
300
|
|
|
215
301
|
async unexposePort(port: number) {
|
|
302
|
+
if (!validatePort(port)) {
|
|
303
|
+
logSecurityEvent(
|
|
304
|
+
"INVALID_PORT_UNEXPOSE",
|
|
305
|
+
{
|
|
306
|
+
port,
|
|
307
|
+
},
|
|
308
|
+
"high"
|
|
309
|
+
);
|
|
310
|
+
throw new SecurityError(
|
|
311
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
216
315
|
await this.client.unexposePort(port);
|
|
316
|
+
|
|
317
|
+
logSecurityEvent(
|
|
318
|
+
"PORT_UNEXPOSED",
|
|
319
|
+
{
|
|
320
|
+
port,
|
|
321
|
+
},
|
|
322
|
+
"low"
|
|
323
|
+
);
|
|
217
324
|
}
|
|
218
325
|
|
|
219
|
-
async getExposedPorts() {
|
|
326
|
+
async getExposedPorts(hostname: string) {
|
|
220
327
|
const response = await this.client.getExposedPorts();
|
|
221
328
|
|
|
222
329
|
// We need the sandbox name to construct preview URLs
|
|
223
330
|
if (!this.sandboxName) {
|
|
224
|
-
throw new Error(
|
|
331
|
+
throw new Error(
|
|
332
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
333
|
+
);
|
|
225
334
|
}
|
|
226
335
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return response.ports.map(port => ({
|
|
336
|
+
return response.ports.map((port) => ({
|
|
230
337
|
url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
|
|
231
338
|
port: port.port,
|
|
232
339
|
name: port.name,
|
|
@@ -234,25 +341,407 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
234
341
|
}));
|
|
235
342
|
}
|
|
236
343
|
|
|
237
|
-
private
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
344
|
+
private constructPreviewUrl(
|
|
345
|
+
port: number,
|
|
346
|
+
sandboxId: string,
|
|
347
|
+
hostname: string
|
|
348
|
+
): string {
|
|
349
|
+
if (!validatePort(port)) {
|
|
350
|
+
logSecurityEvent(
|
|
351
|
+
"INVALID_PORT_REJECTED",
|
|
352
|
+
{
|
|
353
|
+
port,
|
|
354
|
+
sandboxId,
|
|
355
|
+
hostname,
|
|
356
|
+
},
|
|
357
|
+
"high"
|
|
358
|
+
);
|
|
359
|
+
throw new SecurityError(
|
|
360
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let sanitizedSandboxId: string;
|
|
365
|
+
try {
|
|
366
|
+
sanitizedSandboxId = sanitizeSandboxId(sandboxId);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
logSecurityEvent(
|
|
369
|
+
"INVALID_SANDBOX_ID_REJECTED",
|
|
370
|
+
{
|
|
371
|
+
sandboxId,
|
|
372
|
+
port,
|
|
373
|
+
hostname,
|
|
374
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
375
|
+
},
|
|
376
|
+
"high"
|
|
377
|
+
);
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
241
380
|
|
|
242
|
-
private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
|
|
243
|
-
// Check if this is a localhost pattern
|
|
244
381
|
const isLocalhost = isLocalhostPattern(hostname);
|
|
245
382
|
|
|
246
383
|
if (isLocalhost) {
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
384
|
+
// Unified subdomain approach for localhost (RFC 6761)
|
|
385
|
+
const [host, portStr] = hostname.split(":");
|
|
386
|
+
const mainPort = portStr || "80";
|
|
387
|
+
|
|
388
|
+
// Use URL constructor for safe URL building
|
|
389
|
+
try {
|
|
390
|
+
const baseUrl = new URL(`http://${host}:${mainPort}`);
|
|
391
|
+
// Construct subdomain safely
|
|
392
|
+
const subdomainHost = `${port}-${sanitizedSandboxId}.${host}`;
|
|
393
|
+
baseUrl.hostname = subdomainHost;
|
|
394
|
+
|
|
395
|
+
const finalUrl = baseUrl.toString();
|
|
396
|
+
|
|
397
|
+
logSecurityEvent(
|
|
398
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
399
|
+
{
|
|
400
|
+
port,
|
|
401
|
+
sandboxId: sanitizedSandboxId,
|
|
402
|
+
hostname,
|
|
403
|
+
resultUrl: finalUrl,
|
|
404
|
+
environment: "localhost",
|
|
405
|
+
},
|
|
406
|
+
"low"
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
return finalUrl;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
logSecurityEvent(
|
|
412
|
+
"URL_CONSTRUCTION_FAILED",
|
|
413
|
+
{
|
|
414
|
+
port,
|
|
415
|
+
sandboxId: sanitizedSandboxId,
|
|
416
|
+
hostname,
|
|
417
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
418
|
+
},
|
|
419
|
+
"high"
|
|
420
|
+
);
|
|
421
|
+
throw new SecurityError(
|
|
422
|
+
`Failed to construct preview URL: ${
|
|
423
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
424
|
+
}`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Production subdomain logic - enforce HTTPS
|
|
430
|
+
try {
|
|
431
|
+
// Always use HTTPS for production (non-localhost)
|
|
432
|
+
const protocol = "https";
|
|
433
|
+
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
434
|
+
|
|
435
|
+
// Construct subdomain safely
|
|
436
|
+
const subdomainHost = `${port}-${sanitizedSandboxId}.${hostname}`;
|
|
437
|
+
baseUrl.hostname = subdomainHost;
|
|
438
|
+
|
|
439
|
+
const finalUrl = baseUrl.toString();
|
|
440
|
+
|
|
441
|
+
logSecurityEvent(
|
|
442
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
443
|
+
{
|
|
444
|
+
port,
|
|
445
|
+
sandboxId: sanitizedSandboxId,
|
|
446
|
+
hostname,
|
|
447
|
+
resultUrl: finalUrl,
|
|
448
|
+
environment: "production",
|
|
449
|
+
},
|
|
450
|
+
"low"
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
return finalUrl;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
logSecurityEvent(
|
|
456
|
+
"URL_CONSTRUCTION_FAILED",
|
|
457
|
+
{
|
|
458
|
+
port,
|
|
459
|
+
sandboxId: sanitizedSandboxId,
|
|
460
|
+
hostname,
|
|
461
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
462
|
+
},
|
|
463
|
+
"high"
|
|
464
|
+
);
|
|
465
|
+
throw new SecurityError(
|
|
466
|
+
`Failed to construct preview URL: ${
|
|
467
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
468
|
+
}`
|
|
469
|
+
);
|
|
251
470
|
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Code Interpreter Methods
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Create a new code execution context
|
|
477
|
+
*/
|
|
478
|
+
async createCodeContext(
|
|
479
|
+
options?: CreateContextOptions
|
|
480
|
+
): Promise<CodeContext> {
|
|
481
|
+
return this.codeInterpreter.createCodeContext(options);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Run code with streaming callbacks
|
|
486
|
+
*/
|
|
487
|
+
async runCode(
|
|
488
|
+
code: string,
|
|
489
|
+
options?: RunCodeOptions
|
|
490
|
+
): Promise<ExecutionResult> {
|
|
491
|
+
const execution = await this.codeInterpreter.runCode(code, options);
|
|
492
|
+
// Convert to plain object for RPC serialization
|
|
493
|
+
return execution.toJSON();
|
|
494
|
+
}
|
|
252
495
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
496
|
+
/**
|
|
497
|
+
* Run code and return a streaming response
|
|
498
|
+
*/
|
|
499
|
+
async runCodeStream(
|
|
500
|
+
code: string,
|
|
501
|
+
options?: RunCodeOptions
|
|
502
|
+
): Promise<ReadableStream> {
|
|
503
|
+
return this.codeInterpreter.runCodeStream(code, options);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* List all code contexts
|
|
508
|
+
*/
|
|
509
|
+
async listCodeContexts(): Promise<CodeContext[]> {
|
|
510
|
+
return this.codeInterpreter.listCodeContexts();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Delete a code context
|
|
515
|
+
*/
|
|
516
|
+
async deleteCodeContext(contextId: string): Promise<void> {
|
|
517
|
+
return this.codeInterpreter.deleteCodeContext(contextId);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ============================================================================
|
|
521
|
+
// Session Management (Simple Isolation)
|
|
522
|
+
// ============================================================================
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Create a new execution session with isolation
|
|
526
|
+
* Returns a session object with exec() method
|
|
527
|
+
*/
|
|
528
|
+
|
|
529
|
+
async createSession(options: {
|
|
530
|
+
id?: string;
|
|
531
|
+
env?: Record<string, string>;
|
|
532
|
+
cwd?: string;
|
|
533
|
+
isolation?: boolean;
|
|
534
|
+
}): Promise<ExecutionSession> {
|
|
535
|
+
const sessionId = options.id || `session-${Date.now()}`;
|
|
536
|
+
|
|
537
|
+
await this.client.createSession({
|
|
538
|
+
id: sessionId,
|
|
539
|
+
env: options.env,
|
|
540
|
+
cwd: options.cwd,
|
|
541
|
+
isolation: options.isolation
|
|
542
|
+
});
|
|
543
|
+
// Return comprehensive ExecutionSession object that implements all ISandbox methods
|
|
544
|
+
return {
|
|
545
|
+
id: sessionId,
|
|
546
|
+
|
|
547
|
+
// Command execution - clean method names
|
|
548
|
+
exec: async (command: string, options?: ExecOptions) => {
|
|
549
|
+
const result = await this.client.exec(sessionId, command);
|
|
550
|
+
return {
|
|
551
|
+
...result,
|
|
552
|
+
command,
|
|
553
|
+
duration: 0,
|
|
554
|
+
timestamp: new Date().toISOString()
|
|
555
|
+
};
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
execStream: async (command: string, options?: StreamOptions) => {
|
|
559
|
+
return await this.client.execStream(sessionId, command);
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
// Process management - route to session-aware methods
|
|
563
|
+
startProcess: async (command: string, options?: ProcessOptions) => {
|
|
564
|
+
// Use session-specific process management
|
|
565
|
+
const response = await this.client.startProcess(command, sessionId, {
|
|
566
|
+
processId: options?.processId,
|
|
567
|
+
timeout: options?.timeout,
|
|
568
|
+
env: options?.env,
|
|
569
|
+
cwd: options?.cwd,
|
|
570
|
+
encoding: options?.encoding,
|
|
571
|
+
autoCleanup: options?.autoCleanup,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Convert response to Process object with bound methods
|
|
575
|
+
const process = response.process;
|
|
576
|
+
return {
|
|
577
|
+
id: process.id,
|
|
578
|
+
pid: process.pid,
|
|
579
|
+
command: process.command,
|
|
580
|
+
status: process.status as ProcessStatus,
|
|
581
|
+
startTime: new Date(process.startTime),
|
|
582
|
+
endTime: process.endTime ? new Date(process.endTime) : undefined,
|
|
583
|
+
exitCode: process.exitCode ?? undefined,
|
|
584
|
+
kill: async (signal?: string) => {
|
|
585
|
+
await this.client.killProcess(process.id);
|
|
586
|
+
},
|
|
587
|
+
getStatus: async () => {
|
|
588
|
+
const resp = await this.client.getProcess(process.id);
|
|
589
|
+
return resp.process?.status as ProcessStatus || "error";
|
|
590
|
+
},
|
|
591
|
+
getLogs: async () => {
|
|
592
|
+
return await this.client.getProcessLogs(process.id);
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
listProcesses: async () => {
|
|
598
|
+
// Get processes for this specific session
|
|
599
|
+
const response = await this.client.listProcesses(sessionId);
|
|
600
|
+
|
|
601
|
+
// Convert to Process objects with bound methods
|
|
602
|
+
return response.processes.map(p => ({
|
|
603
|
+
id: p.id,
|
|
604
|
+
pid: p.pid,
|
|
605
|
+
command: p.command,
|
|
606
|
+
status: p.status as ProcessStatus,
|
|
607
|
+
startTime: new Date(p.startTime),
|
|
608
|
+
endTime: p.endTime ? new Date(p.endTime) : undefined,
|
|
609
|
+
exitCode: p.exitCode ?? undefined,
|
|
610
|
+
kill: async (signal?: string) => {
|
|
611
|
+
await this.client.killProcess(p.id);
|
|
612
|
+
},
|
|
613
|
+
getStatus: async () => {
|
|
614
|
+
const processResp = await this.client.getProcess(p.id);
|
|
615
|
+
return processResp.process?.status as ProcessStatus || "error";
|
|
616
|
+
},
|
|
617
|
+
getLogs: async () => {
|
|
618
|
+
return this.client.getProcessLogs(p.id);
|
|
619
|
+
},
|
|
620
|
+
}));
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
getProcess: async (id: string) => {
|
|
624
|
+
const response = await this.client.getProcess(id);
|
|
625
|
+
if (!response.process) return null;
|
|
626
|
+
|
|
627
|
+
const p = response.process;
|
|
628
|
+
return {
|
|
629
|
+
id: p.id,
|
|
630
|
+
pid: p.pid,
|
|
631
|
+
command: p.command,
|
|
632
|
+
status: p.status as ProcessStatus,
|
|
633
|
+
startTime: new Date(p.startTime),
|
|
634
|
+
endTime: p.endTime ? new Date(p.endTime) : undefined,
|
|
635
|
+
exitCode: p.exitCode ?? undefined,
|
|
636
|
+
kill: async (signal?: string) => {
|
|
637
|
+
await this.client.killProcess(p.id);
|
|
638
|
+
},
|
|
639
|
+
getStatus: async () => {
|
|
640
|
+
const processResp = await this.client.getProcess(p.id);
|
|
641
|
+
return processResp.process?.status as ProcessStatus || "error";
|
|
642
|
+
},
|
|
643
|
+
getLogs: async () => {
|
|
644
|
+
return this.client.getProcessLogs(p.id);
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
killProcess: async (id: string, signal?: string) => {
|
|
650
|
+
await this.client.killProcess(id);
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
killAllProcesses: async () => {
|
|
654
|
+
// Kill all processes for this specific session
|
|
655
|
+
const response = await this.client.killAllProcesses(sessionId);
|
|
656
|
+
return response.killedCount;
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
streamProcessLogs: async (processId: string, options?: { signal?: AbortSignal }) => {
|
|
660
|
+
return await this.client.streamProcessLogs(processId, options);
|
|
661
|
+
},
|
|
662
|
+
|
|
663
|
+
getProcessLogs: async (id: string) => {
|
|
664
|
+
return await this.client.getProcessLogs(id);
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
cleanupCompletedProcesses: async () => {
|
|
668
|
+
// This would need a new endpoint to cleanup processes for a specific session
|
|
669
|
+
// For now, return 0 as no cleanup is performed
|
|
670
|
+
return 0;
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
// File operations - clean method names (no "InSession" suffix)
|
|
674
|
+
writeFile: async (path: string, content: string, options?: { encoding?: string }) => {
|
|
675
|
+
return await this.client.writeFile(path, content, options?.encoding, sessionId);
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
readFile: async (path: string, options?: { encoding?: string }) => {
|
|
679
|
+
return await this.client.readFile(path, options?.encoding, sessionId);
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
mkdir: async (path: string, options?: { recursive?: boolean }) => {
|
|
683
|
+
return await this.client.mkdir(path, options?.recursive, sessionId);
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
deleteFile: async (path: string) => {
|
|
687
|
+
return await this.client.deleteFile(path, sessionId);
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
renameFile: async (oldPath: string, newPath: string) => {
|
|
691
|
+
return await this.client.renameFile(oldPath, newPath, sessionId);
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
moveFile: async (sourcePath: string, destinationPath: string) => {
|
|
695
|
+
return await this.client.moveFile(sourcePath, destinationPath, sessionId);
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
listFiles: async (path: string, options?: { recursive?: boolean; includeHidden?: boolean }) => {
|
|
699
|
+
return await this.client.listFiles(path, sessionId, options);
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
gitCheckout: async (repoUrl: string, options?: { branch?: string; targetDir?: string }) => {
|
|
703
|
+
return await this.client.gitCheckout(repoUrl, sessionId, options?.branch, options?.targetDir);
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
// Port management
|
|
707
|
+
exposePort: async (port: number, options: { name?: string; hostname: string }) => {
|
|
708
|
+
return await this.exposePort(port, options);
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
unexposePort: async (port: number) => {
|
|
712
|
+
return await this.unexposePort(port);
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
getExposedPorts: async (hostname: string) => {
|
|
716
|
+
return await this.getExposedPorts(hostname);
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
// Environment management
|
|
720
|
+
setEnvVars: async (envVars: Record<string, string>) => {
|
|
721
|
+
// TODO: Implement session-specific environment updates
|
|
722
|
+
console.log(`[Session ${sessionId}] Environment variables update not yet implemented`);
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
// Code Interpreter API
|
|
726
|
+
createCodeContext: async (options?: any) => {
|
|
727
|
+
return await this.createCodeContext(options);
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
runCode: async (code: string, options?: any) => {
|
|
731
|
+
return await this.runCode(code, options);
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
runCodeStream: async (code: string, options?: any) => {
|
|
735
|
+
return await this.runCodeStream(code, options);
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
listCodeContexts: async () => {
|
|
739
|
+
return await this.listCodeContexts();
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
deleteCodeContext: async (contextId: string) => {
|
|
743
|
+
return await this.deleteCodeContext(contextId);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
257
746
|
}
|
|
258
747
|
}
|