@cloudflare/sandbox 0.0.0-153e416 → 0.0.0-1558ec9
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 +98 -16
- package/Dockerfile +79 -32
- package/LICENSE +176 -0
- package/README.md +10 -3
- package/dist/index.d.ts +1953 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3278 -65
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +31 -26
- package/src/clients/git-client.ts +9 -3
- package/src/clients/index.ts +16 -16
- package/src/clients/interpreter-client.ts +51 -47
- package/src/clients/port-client.ts +10 -10
- package/src/clients/process-client.ts +11 -8
- package/src/clients/sandbox-client.ts +2 -4
- package/src/clients/types.ts +6 -2
- package/src/clients/utility-client.ts +43 -6
- package/src/errors/adapter.ts +90 -32
- package/src/errors/classes.ts +189 -64
- package/src/errors/index.ts +9 -5
- package/src/file-stream.ts +11 -6
- package/src/index.ts +28 -17
- package/src/interpreter.ts +50 -41
- package/src/request-handler.ts +24 -21
- package/src/sandbox.ts +403 -148
- package/src/security.ts +21 -6
- package/src/sse-parser.ts +4 -3
- package/src/version.ts +1 -1
- package/startup.sh +1 -1
- package/tests/base-client.test.ts +116 -80
- package/tests/command-client.test.ts +149 -112
- package/tests/file-client.test.ts +309 -197
- package/tests/file-stream.test.ts +24 -20
- package/tests/get-sandbox.test.ts +45 -6
- package/tests/git-client.test.ts +260 -101
- package/tests/port-client.test.ts +100 -108
- package/tests/process-client.test.ts +204 -179
- package/tests/request-handler.test.ts +117 -65
- package/tests/sandbox.test.ts +253 -68
- package/tests/sse-parser.test.ts +17 -16
- package/tests/utility-client.test.ts +79 -72
- package/tsdown.config.ts +12 -0
- package/vitest.config.ts +6 -6
- package/dist/chunk-BFVUNTP4.js +0 -104
- package/dist/chunk-BFVUNTP4.js.map +0 -1
- package/dist/chunk-EKSWCBCA.js +0 -86
- package/dist/chunk-EKSWCBCA.js.map +0 -1
- package/dist/chunk-FE4PJSRB.js +0 -7
- package/dist/chunk-FE4PJSRB.js.map +0 -1
- package/dist/chunk-JXZMAU2C.js +0 -559
- package/dist/chunk-JXZMAU2C.js.map +0 -1
- package/dist/chunk-SVWLTRHD.js +0 -2456
- package/dist/chunk-SVWLTRHD.js.map +0 -1
- package/dist/chunk-Z532A7QC.js +0 -78
- package/dist/chunk-Z532A7QC.js.map +0 -1
- package/dist/file-stream.d.ts +0 -43
- package/dist/file-stream.js +0 -9
- package/dist/file-stream.js.map +0 -1
- package/dist/interpreter.d.ts +0 -33
- package/dist/interpreter.js +0 -8
- package/dist/interpreter.js.map +0 -1
- package/dist/request-handler.d.ts +0 -18
- package/dist/request-handler.js +0 -13
- package/dist/request-handler.js.map +0 -1
- package/dist/sandbox-DWQVgVTY.d.ts +0 -603
- package/dist/sandbox.d.ts +0 -4
- package/dist/sandbox.js +0 -13
- package/dist/sandbox.js.map +0 -1
- package/dist/security.d.ts +0 -31
- package/dist/security.js +0 -13
- package/dist/security.js.map +0 -1
- package/dist/sse-parser.d.ts +0 -28
- package/dist/sse-parser.js +0 -11
- package/dist/sse-parser.js.map +0 -1
- package/dist/version.d.ts +0 -8
- package/dist/version.js +0 -7
- package/dist/version.js.map +0 -1
package/src/sandbox.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DurableObject } from 'cloudflare:workers';
|
|
2
|
-
import { Container, getContainer } from
|
|
2
|
+
import { Container, getContainer, switchPort } from '@cloudflare/containers';
|
|
3
3
|
import type {
|
|
4
4
|
CodeContext,
|
|
5
5
|
CreateContextOptions,
|
|
@@ -16,27 +16,28 @@ import type {
|
|
|
16
16
|
SandboxOptions,
|
|
17
17
|
SessionOptions,
|
|
18
18
|
StreamOptions
|
|
19
|
-
} from
|
|
20
|
-
import {
|
|
21
|
-
|
|
19
|
+
} from '@repo/shared';
|
|
20
|
+
import {
|
|
21
|
+
createLogger,
|
|
22
|
+
runWithLogger,
|
|
23
|
+
type SessionDeleteResult,
|
|
24
|
+
TraceContext
|
|
25
|
+
} from '@repo/shared';
|
|
26
|
+
import { type ExecuteResponse, SandboxClient } from './clients';
|
|
22
27
|
import type { ErrorResponse } from './errors';
|
|
23
28
|
import { CustomDomainRequiredError, ErrorCode } from './errors';
|
|
24
|
-
import { CodeInterpreter } from
|
|
25
|
-
import { isLocalhostPattern } from
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
validatePort
|
|
30
|
-
} from "./security";
|
|
31
|
-
import { parseSSEStream } from "./sse-parser";
|
|
32
|
-
import { SDK_VERSION } from "./version";
|
|
29
|
+
import { CodeInterpreter } from './interpreter';
|
|
30
|
+
import { isLocalhostPattern } from './request-handler';
|
|
31
|
+
import { SecurityError, sanitizeSandboxId, validatePort } from './security';
|
|
32
|
+
import { parseSSEStream } from './sse-parser';
|
|
33
|
+
import { SDK_VERSION } from './version';
|
|
33
34
|
|
|
34
35
|
export function getSandbox(
|
|
35
36
|
ns: DurableObjectNamespace<Sandbox>,
|
|
36
37
|
id: string,
|
|
37
38
|
options?: SandboxOptions
|
|
38
|
-
) {
|
|
39
|
-
const stub = getContainer(ns, id);
|
|
39
|
+
): Sandbox {
|
|
40
|
+
const stub = getContainer(ns, id) as unknown as Sandbox;
|
|
40
41
|
|
|
41
42
|
// Store the name on first access
|
|
42
43
|
stub.setSandboxName?.(id);
|
|
@@ -49,12 +50,33 @@ export function getSandbox(
|
|
|
49
50
|
stub.setSleepAfter(options.sleepAfter);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
if (options?.keepAlive !== undefined) {
|
|
54
|
+
stub.setKeepAlive(options.keepAlive);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Object.assign(stub, {
|
|
58
|
+
wsConnect: connect(stub)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function connect(stub: {
|
|
63
|
+
fetch: (request: Request) => Promise<Response>;
|
|
64
|
+
}) {
|
|
65
|
+
return async (request: Request, port: number) => {
|
|
66
|
+
// Validate port before routing
|
|
67
|
+
if (!validatePort(port)) {
|
|
68
|
+
throw new SecurityError(
|
|
69
|
+
`Invalid or restricted port: ${port}. Ports must be in range 1024-65535 and not reserved.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const portSwitchedRequest = switchPort(request, port);
|
|
73
|
+
return await stub.fetch(portSwitchedRequest);
|
|
74
|
+
};
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
56
78
|
defaultPort = 3000; // Default port for the container's Bun server
|
|
57
|
-
sleepAfter: string | number =
|
|
79
|
+
sleepAfter: string | number = '10m'; // Sleep the sandbox if no requests are made in this timeframe
|
|
58
80
|
|
|
59
81
|
client: SandboxClient;
|
|
60
82
|
private codeInterpreter: CodeInterpreter;
|
|
@@ -64,14 +86,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
64
86
|
private defaultSession: string | null = null;
|
|
65
87
|
envVars: Record<string, string> = {};
|
|
66
88
|
private logger: ReturnType<typeof createLogger>;
|
|
89
|
+
private keepAliveEnabled: boolean = false;
|
|
67
90
|
|
|
68
|
-
constructor(ctx:
|
|
91
|
+
constructor(ctx: DurableObjectState<{}>, env: Env) {
|
|
69
92
|
super(ctx, env);
|
|
70
93
|
|
|
71
94
|
const envObj = env as any;
|
|
72
95
|
// Set sandbox environment variables from env object
|
|
73
96
|
const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const;
|
|
74
|
-
sandboxEnvKeys.forEach(key => {
|
|
97
|
+
sandboxEnvKeys.forEach((key) => {
|
|
75
98
|
if (envObj?.[key]) {
|
|
76
99
|
this.envVars[key] = envObj[key];
|
|
77
100
|
}
|
|
@@ -85,7 +108,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
85
108
|
this.client = new SandboxClient({
|
|
86
109
|
logger: this.logger,
|
|
87
110
|
port: 3000, // Control plane port
|
|
88
|
-
stub: this
|
|
111
|
+
stub: this
|
|
89
112
|
});
|
|
90
113
|
|
|
91
114
|
// Initialize code interpreter - pass 'this' after client is ready
|
|
@@ -94,9 +117,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
94
117
|
|
|
95
118
|
// Load the sandbox name, port tokens, and default session from storage on initialization
|
|
96
119
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
97
|
-
this.sandboxName =
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
this.sandboxName =
|
|
121
|
+
(await this.ctx.storage.get<string>('sandboxName')) || null;
|
|
122
|
+
this.defaultSession =
|
|
123
|
+
(await this.ctx.storage.get<string>('defaultSession')) || null;
|
|
124
|
+
const storedTokens =
|
|
125
|
+
(await this.ctx.storage.get<Record<string, string>>('portTokens')) ||
|
|
126
|
+
{};
|
|
100
127
|
|
|
101
128
|
// Convert stored tokens back to Map
|
|
102
129
|
this.portTokens = new Map();
|
|
@@ -120,8 +147,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
120
147
|
this.baseUrl = baseUrl;
|
|
121
148
|
await this.ctx.storage.put('baseUrl', baseUrl);
|
|
122
149
|
} else {
|
|
123
|
-
if(this.baseUrl !== baseUrl) {
|
|
124
|
-
throw new Error(
|
|
150
|
+
if (this.baseUrl !== baseUrl) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
'Base URL already set and different from one previously provided'
|
|
153
|
+
);
|
|
125
154
|
}
|
|
126
155
|
}
|
|
127
156
|
}
|
|
@@ -131,6 +160,20 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
131
160
|
this.sleepAfter = sleepAfter;
|
|
132
161
|
}
|
|
133
162
|
|
|
163
|
+
// RPC method to enable keepAlive mode
|
|
164
|
+
async setKeepAlive(keepAlive: boolean): Promise<void> {
|
|
165
|
+
this.keepAliveEnabled = keepAlive;
|
|
166
|
+
if (keepAlive) {
|
|
167
|
+
this.logger.info(
|
|
168
|
+
'KeepAlive mode enabled - container will stay alive until explicitly destroyed'
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
this.logger.info(
|
|
172
|
+
'KeepAlive mode disabled - container will timeout normally'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
134
177
|
// RPC method to set environment variables
|
|
135
178
|
async setEnvVars(envVars: Record<string, string>): Promise<void> {
|
|
136
179
|
// Update local state for new sessions
|
|
@@ -143,10 +186,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
143
186
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
144
187
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
145
188
|
|
|
146
|
-
const result = await this.client.commands.execute(
|
|
189
|
+
const result = await this.client.commands.execute(
|
|
190
|
+
exportCommand,
|
|
191
|
+
this.defaultSession
|
|
192
|
+
);
|
|
147
193
|
|
|
148
194
|
if (result.exitCode !== 0) {
|
|
149
|
-
throw new Error(
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
197
|
+
);
|
|
150
198
|
}
|
|
151
199
|
}
|
|
152
200
|
}
|
|
@@ -164,8 +212,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
164
212
|
this.logger.debug('Sandbox started');
|
|
165
213
|
|
|
166
214
|
// Check version compatibility asynchronously (don't block startup)
|
|
167
|
-
this.checkVersionCompatibility().catch(error => {
|
|
168
|
-
this.logger.error(
|
|
215
|
+
this.checkVersionCompatibility().catch((error) => {
|
|
216
|
+
this.logger.error(
|
|
217
|
+
'Version compatibility check failed',
|
|
218
|
+
error instanceof Error ? error : new Error(String(error))
|
|
219
|
+
);
|
|
169
220
|
});
|
|
170
221
|
}
|
|
171
222
|
|
|
@@ -185,8 +236,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
185
236
|
if (containerVersion === 'unknown') {
|
|
186
237
|
this.logger.warn(
|
|
187
238
|
'Container version check: Container version could not be determined. ' +
|
|
188
|
-
|
|
189
|
-
|
|
239
|
+
'This may indicate an outdated container image. ' +
|
|
240
|
+
'Please update your container to match SDK version ' +
|
|
241
|
+
sdkVersion
|
|
190
242
|
);
|
|
191
243
|
return;
|
|
192
244
|
}
|
|
@@ -202,7 +254,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
202
254
|
// so we always use warning level as requested by the user
|
|
203
255
|
this.logger.warn(message);
|
|
204
256
|
} else {
|
|
205
|
-
this.logger.debug('Version check passed', {
|
|
257
|
+
this.logger.debug('Version check passed', {
|
|
258
|
+
sdkVersion,
|
|
259
|
+
containerVersion
|
|
260
|
+
});
|
|
206
261
|
}
|
|
207
262
|
} catch (error) {
|
|
208
263
|
// Don't fail the sandbox initialization if version check fails
|
|
@@ -217,13 +272,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
217
272
|
}
|
|
218
273
|
|
|
219
274
|
override onError(error: unknown) {
|
|
220
|
-
this.logger.error(
|
|
275
|
+
this.logger.error(
|
|
276
|
+
'Sandbox error',
|
|
277
|
+
error instanceof Error ? error : new Error(String(error))
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Override onActivityExpired to prevent automatic shutdown when keepAlive is enabled
|
|
283
|
+
* When keepAlive is disabled, calls parent implementation which stops the container
|
|
284
|
+
*/
|
|
285
|
+
override async onActivityExpired(): Promise<void> {
|
|
286
|
+
if (this.keepAliveEnabled) {
|
|
287
|
+
this.logger.debug(
|
|
288
|
+
'Activity expired but keepAlive is enabled - container will stay alive'
|
|
289
|
+
);
|
|
290
|
+
// Do nothing - don't call stop(), container stays alive
|
|
291
|
+
} else {
|
|
292
|
+
// Default behavior: stop the container
|
|
293
|
+
this.logger.debug('Activity expired - stopping container');
|
|
294
|
+
await super.onActivityExpired();
|
|
295
|
+
}
|
|
221
296
|
}
|
|
222
297
|
|
|
223
298
|
// Override fetch to route internal container requests to appropriate ports
|
|
224
299
|
override async fetch(request: Request): Promise<Response> {
|
|
225
300
|
// Extract or generate trace ID from request
|
|
226
|
-
const traceId =
|
|
301
|
+
const traceId =
|
|
302
|
+
TraceContext.fromHeaders(request.headers) || TraceContext.generate();
|
|
227
303
|
|
|
228
304
|
// Create request-specific logger with trace ID
|
|
229
305
|
const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
|
|
@@ -238,14 +314,30 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
238
314
|
await this.ctx.storage.put('sandboxName', name);
|
|
239
315
|
}
|
|
240
316
|
|
|
241
|
-
// Detect WebSocket upgrade request
|
|
317
|
+
// Detect WebSocket upgrade request (RFC 6455 compliant)
|
|
242
318
|
const upgradeHeader = request.headers.get('Upgrade');
|
|
243
|
-
const
|
|
319
|
+
const connectionHeader = request.headers.get('Connection');
|
|
320
|
+
const isWebSocket =
|
|
321
|
+
upgradeHeader?.toLowerCase() === 'websocket' &&
|
|
322
|
+
connectionHeader?.toLowerCase().includes('upgrade');
|
|
244
323
|
|
|
245
324
|
if (isWebSocket) {
|
|
246
325
|
// WebSocket path: Let parent Container class handle WebSocket proxying
|
|
247
326
|
// This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
|
|
248
|
-
|
|
327
|
+
try {
|
|
328
|
+
requestLogger.debug('WebSocket upgrade requested', {
|
|
329
|
+
path: url.pathname,
|
|
330
|
+
port: this.determinePort(url)
|
|
331
|
+
});
|
|
332
|
+
return await super.fetch(request);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
requestLogger.error(
|
|
335
|
+
'WebSocket connection failed',
|
|
336
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
337
|
+
{ path: url.pathname }
|
|
338
|
+
);
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
249
341
|
}
|
|
250
342
|
|
|
251
343
|
// Non-WebSocket: Use existing port determination and HTTP routing logic
|
|
@@ -256,6 +348,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
256
348
|
});
|
|
257
349
|
}
|
|
258
350
|
|
|
351
|
+
wsConnect(request: Request, port: number): Promise<Response> {
|
|
352
|
+
// Dummy implementation that will be overridden by the stub
|
|
353
|
+
throw new Error('Not implemented here to avoid RPC serialization issues');
|
|
354
|
+
}
|
|
355
|
+
|
|
259
356
|
private determinePort(url: URL): number {
|
|
260
357
|
// Extract port from proxy requests (e.g., /proxy/8080/*)
|
|
261
358
|
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
@@ -285,7 +382,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
285
382
|
await this.client.utils.createSession({
|
|
286
383
|
id: sessionId,
|
|
287
384
|
env: this.envVars || {},
|
|
288
|
-
cwd: '/workspace'
|
|
385
|
+
cwd: '/workspace'
|
|
289
386
|
});
|
|
290
387
|
|
|
291
388
|
this.defaultSession = sessionId;
|
|
@@ -295,7 +392,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
295
392
|
} catch (error: any) {
|
|
296
393
|
// If session already exists (e.g., after hot reload), reuse it
|
|
297
394
|
if (error?.message?.includes('already exists')) {
|
|
298
|
-
this.logger.debug('Reusing existing session after reload', {
|
|
395
|
+
this.logger.debug('Reusing existing session after reload', {
|
|
396
|
+
sessionId
|
|
397
|
+
});
|
|
299
398
|
this.defaultSession = sessionId;
|
|
300
399
|
// Persist to storage in case it wasn't saved before
|
|
301
400
|
await this.ctx.storage.put('defaultSession', sessionId);
|
|
@@ -327,7 +426,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
327
426
|
const startTime = Date.now();
|
|
328
427
|
const timestamp = new Date().toISOString();
|
|
329
428
|
|
|
330
|
-
// Handle timeout
|
|
331
429
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
332
430
|
|
|
333
431
|
try {
|
|
@@ -340,13 +438,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
340
438
|
|
|
341
439
|
if (options?.stream && options?.onOutput) {
|
|
342
440
|
// Streaming with callbacks - we need to collect the final result
|
|
343
|
-
result = await this.executeWithStreaming(
|
|
441
|
+
result = await this.executeWithStreaming(
|
|
442
|
+
command,
|
|
443
|
+
sessionId,
|
|
444
|
+
options,
|
|
445
|
+
startTime,
|
|
446
|
+
timestamp
|
|
447
|
+
);
|
|
344
448
|
} else {
|
|
345
449
|
// Regular execution with session
|
|
346
450
|
const response = await this.client.commands.execute(command, sessionId);
|
|
347
451
|
|
|
348
452
|
const duration = Date.now() - startTime;
|
|
349
|
-
result = this.mapExecuteResponseToExecResult(
|
|
453
|
+
result = this.mapExecuteResponseToExecResult(
|
|
454
|
+
response,
|
|
455
|
+
duration,
|
|
456
|
+
sessionId
|
|
457
|
+
);
|
|
350
458
|
}
|
|
351
459
|
|
|
352
460
|
// Call completion callback if provided
|
|
@@ -378,7 +486,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
378
486
|
let stderr = '';
|
|
379
487
|
|
|
380
488
|
try {
|
|
381
|
-
const stream = await this.client.commands.executeStream(
|
|
489
|
+
const stream = await this.client.commands.executeStream(
|
|
490
|
+
command,
|
|
491
|
+
sessionId
|
|
492
|
+
);
|
|
382
493
|
|
|
383
494
|
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
384
495
|
// Check for cancellation
|
|
@@ -423,7 +534,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
423
534
|
|
|
424
535
|
// If we get here without a complete event, something went wrong
|
|
425
536
|
throw new Error('Stream ended without completion event');
|
|
426
|
-
|
|
427
537
|
} catch (error) {
|
|
428
538
|
if (options.signal?.aborted) {
|
|
429
539
|
throw new Error('Operation was aborted');
|
|
@@ -471,8 +581,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
471
581
|
pid: data.pid,
|
|
472
582
|
command: data.command,
|
|
473
583
|
status: data.status,
|
|
474
|
-
startTime:
|
|
475
|
-
|
|
584
|
+
startTime:
|
|
585
|
+
typeof data.startTime === 'string'
|
|
586
|
+
? new Date(data.startTime)
|
|
587
|
+
: data.startTime,
|
|
588
|
+
endTime: data.endTime
|
|
589
|
+
? typeof data.endTime === 'string'
|
|
590
|
+
? new Date(data.endTime)
|
|
591
|
+
: data.endTime
|
|
592
|
+
: undefined,
|
|
476
593
|
exitCode: data.exitCode,
|
|
477
594
|
sessionId,
|
|
478
595
|
|
|
@@ -492,25 +609,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
492
609
|
};
|
|
493
610
|
}
|
|
494
611
|
|
|
495
|
-
|
|
496
612
|
// Background process management
|
|
497
|
-
async startProcess(
|
|
613
|
+
async startProcess(
|
|
614
|
+
command: string,
|
|
615
|
+
options?: ProcessOptions,
|
|
616
|
+
sessionId?: string
|
|
617
|
+
): Promise<Process> {
|
|
498
618
|
// Use the new HttpClient method to start the process
|
|
499
619
|
try {
|
|
500
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
501
|
-
const response = await this.client.processes.startProcess(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
620
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
621
|
+
const response = await this.client.processes.startProcess(
|
|
622
|
+
command,
|
|
623
|
+
session,
|
|
624
|
+
{
|
|
625
|
+
processId: options?.processId
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
const processObj = this.createProcessFromDTO(
|
|
630
|
+
{
|
|
631
|
+
id: response.processId,
|
|
632
|
+
pid: response.pid,
|
|
633
|
+
command: response.command,
|
|
634
|
+
status: 'running' as ProcessStatus,
|
|
635
|
+
startTime: new Date(),
|
|
636
|
+
endTime: undefined,
|
|
637
|
+
exitCode: undefined
|
|
638
|
+
},
|
|
639
|
+
session
|
|
640
|
+
);
|
|
514
641
|
|
|
515
642
|
// Call onStart callback if provided
|
|
516
643
|
if (options?.onStart) {
|
|
@@ -518,7 +645,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
518
645
|
}
|
|
519
646
|
|
|
520
647
|
return processObj;
|
|
521
|
-
|
|
522
648
|
} catch (error) {
|
|
523
649
|
if (options?.onError && error instanceof Error) {
|
|
524
650
|
options.onError(error);
|
|
@@ -529,42 +655,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
529
655
|
}
|
|
530
656
|
|
|
531
657
|
async listProcesses(sessionId?: string): Promise<Process[]> {
|
|
532
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
658
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
533
659
|
const response = await this.client.processes.listProcesses();
|
|
534
660
|
|
|
535
|
-
return response.processes.map(processData =>
|
|
536
|
-
this.createProcessFromDTO(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
661
|
+
return response.processes.map((processData) =>
|
|
662
|
+
this.createProcessFromDTO(
|
|
663
|
+
{
|
|
664
|
+
id: processData.id,
|
|
665
|
+
pid: processData.pid,
|
|
666
|
+
command: processData.command,
|
|
667
|
+
status: processData.status,
|
|
668
|
+
startTime: processData.startTime,
|
|
669
|
+
endTime: processData.endTime,
|
|
670
|
+
exitCode: processData.exitCode
|
|
671
|
+
},
|
|
672
|
+
session
|
|
673
|
+
)
|
|
545
674
|
);
|
|
546
675
|
}
|
|
547
676
|
|
|
548
677
|
async getProcess(id: string, sessionId?: string): Promise<Process | null> {
|
|
549
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
678
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
550
679
|
const response = await this.client.processes.getProcess(id);
|
|
551
680
|
if (!response.process) {
|
|
552
681
|
return null;
|
|
553
682
|
}
|
|
554
683
|
|
|
555
684
|
const processData = response.process;
|
|
556
|
-
return this.createProcessFromDTO(
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
685
|
+
return this.createProcessFromDTO(
|
|
686
|
+
{
|
|
687
|
+
id: processData.id,
|
|
688
|
+
pid: processData.pid,
|
|
689
|
+
command: processData.command,
|
|
690
|
+
status: processData.status,
|
|
691
|
+
startTime: processData.startTime,
|
|
692
|
+
endTime: processData.endTime,
|
|
693
|
+
exitCode: processData.exitCode
|
|
694
|
+
},
|
|
695
|
+
session
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async killProcess(
|
|
700
|
+
id: string,
|
|
701
|
+
signal?: string,
|
|
702
|
+
sessionId?: string
|
|
703
|
+
): Promise<void> {
|
|
568
704
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
569
705
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
570
706
|
await this.client.processes.killProcess(id);
|
|
@@ -582,7 +718,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
582
718
|
return 0;
|
|
583
719
|
}
|
|
584
720
|
|
|
585
|
-
async getProcessLogs(
|
|
721
|
+
async getProcessLogs(
|
|
722
|
+
id: string,
|
|
723
|
+
sessionId?: string
|
|
724
|
+
): Promise<{ stdout: string; stderr: string; processId: string }> {
|
|
586
725
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
587
726
|
const response = await this.client.processes.getProcessLogs(id);
|
|
588
727
|
return {
|
|
@@ -592,9 +731,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
592
731
|
};
|
|
593
732
|
}
|
|
594
733
|
|
|
595
|
-
|
|
596
734
|
// Streaming methods - return ReadableStream for RPC compatibility
|
|
597
|
-
async execStream(
|
|
735
|
+
async execStream(
|
|
736
|
+
command: string,
|
|
737
|
+
options?: StreamOptions
|
|
738
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
598
739
|
// Check for cancellation
|
|
599
740
|
if (options?.signal?.aborted) {
|
|
600
741
|
throw new Error('Operation was aborted');
|
|
@@ -608,7 +749,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
608
749
|
/**
|
|
609
750
|
* Internal session-aware execStream implementation
|
|
610
751
|
*/
|
|
611
|
-
private async execStreamWithSession(
|
|
752
|
+
private async execStreamWithSession(
|
|
753
|
+
command: string,
|
|
754
|
+
sessionId: string,
|
|
755
|
+
options?: StreamOptions
|
|
756
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
612
757
|
// Check for cancellation
|
|
613
758
|
if (options?.signal?.aborted) {
|
|
614
759
|
throw new Error('Operation was aborted');
|
|
@@ -617,7 +762,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
617
762
|
return this.client.commands.executeStream(command, sessionId);
|
|
618
763
|
}
|
|
619
764
|
|
|
620
|
-
|
|
765
|
+
/**
|
|
766
|
+
* Stream logs from a background process as a ReadableStream.
|
|
767
|
+
*/
|
|
768
|
+
async streamProcessLogs(
|
|
769
|
+
processId: string,
|
|
770
|
+
options?: { signal?: AbortSignal }
|
|
771
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
621
772
|
// Check for cancellation
|
|
622
773
|
if (options?.signal?.aborted) {
|
|
623
774
|
throw new Error('Operation was aborted');
|
|
@@ -630,7 +781,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
630
781
|
repoUrl: string,
|
|
631
782
|
options: { branch?: string; targetDir?: string; sessionId?: string }
|
|
632
783
|
) {
|
|
633
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
784
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
634
785
|
return this.client.git.checkout(repoUrl, session, {
|
|
635
786
|
branch: options.branch,
|
|
636
787
|
targetDir: options.targetDir
|
|
@@ -641,8 +792,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
641
792
|
path: string,
|
|
642
793
|
options: { recursive?: boolean; sessionId?: string } = {}
|
|
643
794
|
) {
|
|
644
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
645
|
-
return this.client.files.mkdir(path, session, {
|
|
795
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
796
|
+
return this.client.files.mkdir(path, session, {
|
|
797
|
+
recursive: options.recursive
|
|
798
|
+
});
|
|
646
799
|
}
|
|
647
800
|
|
|
648
801
|
async writeFile(
|
|
@@ -650,21 +803,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
650
803
|
content: string,
|
|
651
804
|
options: { encoding?: string; sessionId?: string } = {}
|
|
652
805
|
) {
|
|
653
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
654
|
-
return this.client.files.writeFile(path, content, session, {
|
|
806
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
807
|
+
return this.client.files.writeFile(path, content, session, {
|
|
808
|
+
encoding: options.encoding
|
|
809
|
+
});
|
|
655
810
|
}
|
|
656
811
|
|
|
657
812
|
async deleteFile(path: string, sessionId?: string) {
|
|
658
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
813
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
659
814
|
return this.client.files.deleteFile(path, session);
|
|
660
815
|
}
|
|
661
816
|
|
|
662
|
-
async renameFile(
|
|
663
|
-
|
|
664
|
-
newPath: string,
|
|
665
|
-
sessionId?: string
|
|
666
|
-
) {
|
|
667
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
817
|
+
async renameFile(oldPath: string, newPath: string, sessionId?: string) {
|
|
818
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
668
819
|
return this.client.files.renameFile(oldPath, newPath, session);
|
|
669
820
|
}
|
|
670
821
|
|
|
@@ -673,7 +824,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
673
824
|
destinationPath: string,
|
|
674
825
|
sessionId?: string
|
|
675
826
|
) {
|
|
676
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
827
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
677
828
|
return this.client.files.moveFile(sourcePath, destinationPath, session);
|
|
678
829
|
}
|
|
679
830
|
|
|
@@ -681,8 +832,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
681
832
|
path: string,
|
|
682
833
|
options: { encoding?: string; sessionId?: string } = {}
|
|
683
834
|
) {
|
|
684
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
685
|
-
return this.client.files.readFile(path, session, {
|
|
835
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
836
|
+
return this.client.files.readFile(path, session, {
|
|
837
|
+
encoding: options.encoding
|
|
838
|
+
});
|
|
686
839
|
}
|
|
687
840
|
|
|
688
841
|
/**
|
|
@@ -695,7 +848,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
695
848
|
path: string,
|
|
696
849
|
options: { sessionId?: string } = {}
|
|
697
850
|
): Promise<ReadableStream<Uint8Array>> {
|
|
698
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
851
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
699
852
|
return this.client.files.readFileStream(path, session);
|
|
700
853
|
}
|
|
701
854
|
|
|
@@ -708,7 +861,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
708
861
|
}
|
|
709
862
|
|
|
710
863
|
async exists(path: string, sessionId?: string) {
|
|
711
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
864
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
712
865
|
return this.client.files.exists(path, session);
|
|
713
866
|
}
|
|
714
867
|
|
|
@@ -730,7 +883,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
730
883
|
|
|
731
884
|
// We need the sandbox name to construct preview URLs
|
|
732
885
|
if (!this.sandboxName) {
|
|
733
|
-
throw new Error(
|
|
886
|
+
throw new Error(
|
|
887
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
888
|
+
);
|
|
734
889
|
}
|
|
735
890
|
|
|
736
891
|
// Generate and store token for this port
|
|
@@ -738,18 +893,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
738
893
|
this.portTokens.set(port, token);
|
|
739
894
|
await this.persistPortTokens();
|
|
740
895
|
|
|
741
|
-
const url = this.constructPreviewUrl(
|
|
896
|
+
const url = this.constructPreviewUrl(
|
|
897
|
+
port,
|
|
898
|
+
this.sandboxName,
|
|
899
|
+
options.hostname,
|
|
900
|
+
token
|
|
901
|
+
);
|
|
742
902
|
|
|
743
903
|
return {
|
|
744
904
|
url,
|
|
745
905
|
port,
|
|
746
|
-
name: options?.name
|
|
906
|
+
name: options?.name
|
|
747
907
|
};
|
|
748
908
|
}
|
|
749
909
|
|
|
750
910
|
async unexposePort(port: number) {
|
|
751
911
|
if (!validatePort(port)) {
|
|
752
|
-
throw new SecurityError(
|
|
912
|
+
throw new SecurityError(
|
|
913
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
914
|
+
);
|
|
753
915
|
}
|
|
754
916
|
|
|
755
917
|
const sessionId = await this.ensureDefaultSession();
|
|
@@ -768,32 +930,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
768
930
|
|
|
769
931
|
// We need the sandbox name to construct preview URLs
|
|
770
932
|
if (!this.sandboxName) {
|
|
771
|
-
throw new Error(
|
|
933
|
+
throw new Error(
|
|
934
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
935
|
+
);
|
|
772
936
|
}
|
|
773
937
|
|
|
774
|
-
return response.ports.map(port => {
|
|
938
|
+
return response.ports.map((port) => {
|
|
775
939
|
// Get token for this port - must exist for all exposed ports
|
|
776
940
|
const token = this.portTokens.get(port.port);
|
|
777
941
|
if (!token) {
|
|
778
|
-
throw new Error(
|
|
942
|
+
throw new Error(
|
|
943
|
+
`Port ${port.port} is exposed but has no token. This should not happen.`
|
|
944
|
+
);
|
|
779
945
|
}
|
|
780
946
|
|
|
781
947
|
return {
|
|
782
|
-
url: this.constructPreviewUrl(
|
|
948
|
+
url: this.constructPreviewUrl(
|
|
949
|
+
port.port,
|
|
950
|
+
this.sandboxName!,
|
|
951
|
+
hostname,
|
|
952
|
+
token
|
|
953
|
+
),
|
|
783
954
|
port: port.port,
|
|
784
|
-
status: port.status
|
|
955
|
+
status: port.status
|
|
785
956
|
};
|
|
786
957
|
});
|
|
787
958
|
}
|
|
788
959
|
|
|
789
|
-
|
|
790
960
|
async isPortExposed(port: number): Promise<boolean> {
|
|
791
961
|
try {
|
|
792
962
|
const sessionId = await this.ensureDefaultSession();
|
|
793
963
|
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
794
|
-
return response.ports.some(exposedPort => exposedPort.port === port);
|
|
964
|
+
return response.ports.some((exposedPort) => exposedPort.port === port);
|
|
795
965
|
} catch (error) {
|
|
796
|
-
this.logger.error(
|
|
966
|
+
this.logger.error(
|
|
967
|
+
'Error checking if port is exposed',
|
|
968
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
969
|
+
{ port }
|
|
970
|
+
);
|
|
797
971
|
return false;
|
|
798
972
|
}
|
|
799
973
|
}
|
|
@@ -809,7 +983,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
809
983
|
const storedToken = this.portTokens.get(port);
|
|
810
984
|
if (!storedToken) {
|
|
811
985
|
// This should not happen - all exposed ports must have tokens
|
|
812
|
-
this.logger.error(
|
|
986
|
+
this.logger.error(
|
|
987
|
+
'Port is exposed but has no token - bug detected',
|
|
988
|
+
undefined,
|
|
989
|
+
{ port }
|
|
990
|
+
);
|
|
813
991
|
return false;
|
|
814
992
|
}
|
|
815
993
|
|
|
@@ -825,7 +1003,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
825
1003
|
|
|
826
1004
|
// Convert to base64url format (URL-safe, no padding, lowercase)
|
|
827
1005
|
const base64 = btoa(String.fromCharCode(...array));
|
|
828
|
-
return base64
|
|
1006
|
+
return base64
|
|
1007
|
+
.replace(/\+/g, '-')
|
|
1008
|
+
.replace(/\//g, '_')
|
|
1009
|
+
.replace(/=/g, '')
|
|
1010
|
+
.toLowerCase();
|
|
829
1011
|
}
|
|
830
1012
|
|
|
831
1013
|
private async persistPortTokens(): Promise<void> {
|
|
@@ -837,9 +1019,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
837
1019
|
await this.ctx.storage.put('portTokens', tokensObj);
|
|
838
1020
|
}
|
|
839
1021
|
|
|
840
|
-
private constructPreviewUrl(
|
|
1022
|
+
private constructPreviewUrl(
|
|
1023
|
+
port: number,
|
|
1024
|
+
sandboxId: string,
|
|
1025
|
+
hostname: string,
|
|
1026
|
+
token: string
|
|
1027
|
+
): string {
|
|
841
1028
|
if (!validatePort(port)) {
|
|
842
|
-
throw new SecurityError(
|
|
1029
|
+
throw new SecurityError(
|
|
1030
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
1031
|
+
);
|
|
843
1032
|
}
|
|
844
1033
|
|
|
845
1034
|
// Validate sandbox ID (will throw SecurityError if invalid)
|
|
@@ -861,14 +1050,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
861
1050
|
|
|
862
1051
|
return baseUrl.toString();
|
|
863
1052
|
} catch (error) {
|
|
864
|
-
throw new SecurityError(
|
|
1053
|
+
throw new SecurityError(
|
|
1054
|
+
`Failed to construct preview URL: ${
|
|
1055
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1056
|
+
}`
|
|
1057
|
+
);
|
|
865
1058
|
}
|
|
866
1059
|
}
|
|
867
1060
|
|
|
868
1061
|
// Production subdomain logic - enforce HTTPS
|
|
869
1062
|
try {
|
|
870
1063
|
// Always use HTTPS for production (non-localhost)
|
|
871
|
-
const protocol =
|
|
1064
|
+
const protocol = 'https';
|
|
872
1065
|
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
873
1066
|
|
|
874
1067
|
// Construct subdomain safely with mandatory token
|
|
@@ -877,7 +1070,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
877
1070
|
|
|
878
1071
|
return baseUrl.toString();
|
|
879
1072
|
} catch (error) {
|
|
880
|
-
throw new SecurityError(
|
|
1073
|
+
throw new SecurityError(
|
|
1074
|
+
`Failed to construct preview URL: ${
|
|
1075
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1076
|
+
}`
|
|
1077
|
+
);
|
|
881
1078
|
}
|
|
882
1079
|
}
|
|
883
1080
|
|
|
@@ -896,7 +1093,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
896
1093
|
await this.client.utils.createSession({
|
|
897
1094
|
id: sessionId,
|
|
898
1095
|
env: options?.env,
|
|
899
|
-
cwd: options?.cwd
|
|
1096
|
+
cwd: options?.cwd
|
|
900
1097
|
});
|
|
901
1098
|
|
|
902
1099
|
// Return wrapper that binds sessionId to all operations
|
|
@@ -918,6 +1115,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
918
1115
|
return this.getSessionWrapper(sessionId);
|
|
919
1116
|
}
|
|
920
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* Delete an execution session
|
|
1120
|
+
* Cleans up session resources and removes it from the container
|
|
1121
|
+
* Note: Cannot delete the default session. To reset the default session,
|
|
1122
|
+
* use sandbox.destroy() to terminate the entire sandbox.
|
|
1123
|
+
*
|
|
1124
|
+
* @param sessionId - The ID of the session to delete
|
|
1125
|
+
* @returns Result with success status, sessionId, and timestamp
|
|
1126
|
+
* @throws Error if attempting to delete the default session
|
|
1127
|
+
*/
|
|
1128
|
+
async deleteSession(sessionId: string): Promise<SessionDeleteResult> {
|
|
1129
|
+
// Prevent deletion of default session
|
|
1130
|
+
if (this.defaultSession && sessionId === this.defaultSession) {
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
`Cannot delete default session '${sessionId}'. Use sandbox.destroy() to terminate the sandbox.`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const response = await this.client.utils.deleteSession(sessionId);
|
|
1137
|
+
|
|
1138
|
+
// Map HTTP response to result type
|
|
1139
|
+
return {
|
|
1140
|
+
success: response.success,
|
|
1141
|
+
sessionId: response.sessionId,
|
|
1142
|
+
timestamp: response.timestamp
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
921
1146
|
/**
|
|
922
1147
|
* Internal helper to create ExecutionSession wrapper for a given sessionId
|
|
923
1148
|
* Used by both createSession and getSession
|
|
@@ -927,32 +1152,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
927
1152
|
id: sessionId,
|
|
928
1153
|
|
|
929
1154
|
// Command execution - delegate to internal session-aware methods
|
|
930
|
-
exec: (command, options) =>
|
|
931
|
-
|
|
1155
|
+
exec: (command, options) =>
|
|
1156
|
+
this.execWithSession(command, sessionId, options),
|
|
1157
|
+
execStream: (command, options) =>
|
|
1158
|
+
this.execStreamWithSession(command, sessionId, options),
|
|
932
1159
|
|
|
933
1160
|
// Process management
|
|
934
|
-
startProcess: (command, options) =>
|
|
1161
|
+
startProcess: (command, options) =>
|
|
1162
|
+
this.startProcess(command, options, sessionId),
|
|
935
1163
|
listProcesses: () => this.listProcesses(sessionId),
|
|
936
1164
|
getProcess: (id) => this.getProcess(id, sessionId),
|
|
937
1165
|
killProcess: (id, signal) => this.killProcess(id, signal),
|
|
938
1166
|
killAllProcesses: () => this.killAllProcesses(),
|
|
939
1167
|
cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
|
|
940
1168
|
getProcessLogs: (id) => this.getProcessLogs(id),
|
|
941
|
-
streamProcessLogs: (processId, options) =>
|
|
1169
|
+
streamProcessLogs: (processId, options) =>
|
|
1170
|
+
this.streamProcessLogs(processId, options),
|
|
942
1171
|
|
|
943
1172
|
// File operations - pass sessionId via options or parameter
|
|
944
|
-
writeFile: (path, content, options) =>
|
|
945
|
-
|
|
1173
|
+
writeFile: (path, content, options) =>
|
|
1174
|
+
this.writeFile(path, content, { ...options, sessionId }),
|
|
1175
|
+
readFile: (path, options) =>
|
|
1176
|
+
this.readFile(path, { ...options, sessionId }),
|
|
946
1177
|
readFileStream: (path) => this.readFileStream(path, { sessionId }),
|
|
947
1178
|
mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
|
|
948
1179
|
deleteFile: (path) => this.deleteFile(path, sessionId),
|
|
949
|
-
renameFile: (oldPath, newPath) =>
|
|
950
|
-
|
|
951
|
-
|
|
1180
|
+
renameFile: (oldPath, newPath) =>
|
|
1181
|
+
this.renameFile(oldPath, newPath, sessionId),
|
|
1182
|
+
moveFile: (sourcePath, destPath) =>
|
|
1183
|
+
this.moveFile(sourcePath, destPath, sessionId),
|
|
1184
|
+
listFiles: (path, options) =>
|
|
1185
|
+
this.client.files.listFiles(path, sessionId, options),
|
|
952
1186
|
exists: (path) => this.exists(path, sessionId),
|
|
953
1187
|
|
|
954
1188
|
// Git operations
|
|
955
|
-
gitCheckout: (repoUrl, options) =>
|
|
1189
|
+
gitCheckout: (repoUrl, options) =>
|
|
1190
|
+
this.gitCheckout(repoUrl, { ...options, sessionId }),
|
|
956
1191
|
|
|
957
1192
|
// Environment management - needs special handling
|
|
958
1193
|
setEnvVars: async (envVars: Record<string, string>) => {
|
|
@@ -962,27 +1197,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
962
1197
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
963
1198
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
964
1199
|
|
|
965
|
-
const result = await this.client.commands.execute(
|
|
1200
|
+
const result = await this.client.commands.execute(
|
|
1201
|
+
exportCommand,
|
|
1202
|
+
sessionId
|
|
1203
|
+
);
|
|
966
1204
|
|
|
967
1205
|
if (result.exitCode !== 0) {
|
|
968
|
-
throw new Error(
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
1208
|
+
);
|
|
969
1209
|
}
|
|
970
1210
|
}
|
|
971
1211
|
} catch (error) {
|
|
972
|
-
this.logger.error(
|
|
1212
|
+
this.logger.error(
|
|
1213
|
+
'Failed to set environment variables',
|
|
1214
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1215
|
+
{ sessionId }
|
|
1216
|
+
);
|
|
973
1217
|
throw error;
|
|
974
1218
|
}
|
|
975
1219
|
},
|
|
976
1220
|
|
|
977
1221
|
// Code interpreter methods - delegate to sandbox's code interpreter
|
|
978
|
-
createCodeContext: (options) =>
|
|
1222
|
+
createCodeContext: (options) =>
|
|
1223
|
+
this.codeInterpreter.createCodeContext(options),
|
|
979
1224
|
runCode: async (code, options) => {
|
|
980
1225
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
981
1226
|
return execution.toJSON();
|
|
982
1227
|
},
|
|
983
|
-
runCodeStream: (code, options) =>
|
|
1228
|
+
runCodeStream: (code, options) =>
|
|
1229
|
+
this.codeInterpreter.runCodeStream(code, options),
|
|
984
1230
|
listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
|
|
985
|
-
deleteCodeContext: (contextId) =>
|
|
1231
|
+
deleteCodeContext: (contextId) =>
|
|
1232
|
+
this.codeInterpreter.deleteCodeContext(contextId)
|
|
986
1233
|
};
|
|
987
1234
|
}
|
|
988
1235
|
|
|
@@ -990,16 +1237,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
990
1237
|
// Code interpreter methods - delegate to CodeInterpreter wrapper
|
|
991
1238
|
// ============================================================================
|
|
992
1239
|
|
|
993
|
-
async createCodeContext(
|
|
1240
|
+
async createCodeContext(
|
|
1241
|
+
options?: CreateContextOptions
|
|
1242
|
+
): Promise<CodeContext> {
|
|
994
1243
|
return this.codeInterpreter.createCodeContext(options);
|
|
995
1244
|
}
|
|
996
1245
|
|
|
997
|
-
async runCode(
|
|
1246
|
+
async runCode(
|
|
1247
|
+
code: string,
|
|
1248
|
+
options?: RunCodeOptions
|
|
1249
|
+
): Promise<ExecutionResult> {
|
|
998
1250
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
999
1251
|
return execution.toJSON();
|
|
1000
1252
|
}
|
|
1001
1253
|
|
|
1002
|
-
async runCodeStream(
|
|
1254
|
+
async runCodeStream(
|
|
1255
|
+
code: string,
|
|
1256
|
+
options?: RunCodeOptions
|
|
1257
|
+
): Promise<ReadableStream> {
|
|
1003
1258
|
return this.codeInterpreter.runCodeStream(code, options);
|
|
1004
1259
|
}
|
|
1005
1260
|
|