@cloudflare/sandbox 0.0.0-c39674b → 0.0.0-c484232
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 +130 -15
- 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 +6 -6
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +51 -20
- 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 +34 -21
- package/src/sandbox.ts +417 -146
- 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 +373 -185
- 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 +292 -0
- package/tests/sandbox.test.ts +336 -62
- 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-JXZMAU2C.js +0 -559
- package/dist/chunk-JXZMAU2C.js.map +0 -1
- package/dist/chunk-UZQBJBJF.js +0 -7
- package/dist/chunk-UZQBJBJF.js.map +0 -1
- package/dist/chunk-YEZBBFK7.js +0 -2420
- package/dist/chunk-YEZBBFK7.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-DMlNr93l.d.ts +0 -596
- 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,7 +314,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
238
314
|
await this.ctx.storage.put('sandboxName', name);
|
|
239
315
|
}
|
|
240
316
|
|
|
241
|
-
//
|
|
317
|
+
// Detect WebSocket upgrade request (RFC 6455 compliant)
|
|
318
|
+
const upgradeHeader = request.headers.get('Upgrade');
|
|
319
|
+
const connectionHeader = request.headers.get('Connection');
|
|
320
|
+
const isWebSocket =
|
|
321
|
+
upgradeHeader?.toLowerCase() === 'websocket' &&
|
|
322
|
+
connectionHeader?.toLowerCase().includes('upgrade');
|
|
323
|
+
|
|
324
|
+
if (isWebSocket) {
|
|
325
|
+
// WebSocket path: Let parent Container class handle WebSocket proxying
|
|
326
|
+
// This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
|
|
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
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Non-WebSocket: Use existing port determination and HTTP routing logic
|
|
242
344
|
const port = this.determinePort(url);
|
|
243
345
|
|
|
244
346
|
// Route to the appropriate port
|
|
@@ -246,6 +348,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
246
348
|
});
|
|
247
349
|
}
|
|
248
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
|
+
|
|
249
356
|
private determinePort(url: URL): number {
|
|
250
357
|
// Extract port from proxy requests (e.g., /proxy/8080/*)
|
|
251
358
|
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
@@ -275,7 +382,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
275
382
|
await this.client.utils.createSession({
|
|
276
383
|
id: sessionId,
|
|
277
384
|
env: this.envVars || {},
|
|
278
|
-
cwd: '/workspace'
|
|
385
|
+
cwd: '/workspace'
|
|
279
386
|
});
|
|
280
387
|
|
|
281
388
|
this.defaultSession = sessionId;
|
|
@@ -284,8 +391,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
284
391
|
this.logger.debug('Default session initialized', { sessionId });
|
|
285
392
|
} catch (error: any) {
|
|
286
393
|
// If session already exists (e.g., after hot reload), reuse it
|
|
287
|
-
if (error?.message?.includes('already exists')
|
|
288
|
-
this.logger.debug('Reusing existing session after reload', {
|
|
394
|
+
if (error?.message?.includes('already exists')) {
|
|
395
|
+
this.logger.debug('Reusing existing session after reload', {
|
|
396
|
+
sessionId
|
|
397
|
+
});
|
|
289
398
|
this.defaultSession = sessionId;
|
|
290
399
|
// Persist to storage in case it wasn't saved before
|
|
291
400
|
await this.ctx.storage.put('defaultSession', sessionId);
|
|
@@ -317,7 +426,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
317
426
|
const startTime = Date.now();
|
|
318
427
|
const timestamp = new Date().toISOString();
|
|
319
428
|
|
|
320
|
-
// Handle timeout
|
|
321
429
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
322
430
|
|
|
323
431
|
try {
|
|
@@ -330,13 +438,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
330
438
|
|
|
331
439
|
if (options?.stream && options?.onOutput) {
|
|
332
440
|
// Streaming with callbacks - we need to collect the final result
|
|
333
|
-
result = await this.executeWithStreaming(
|
|
441
|
+
result = await this.executeWithStreaming(
|
|
442
|
+
command,
|
|
443
|
+
sessionId,
|
|
444
|
+
options,
|
|
445
|
+
startTime,
|
|
446
|
+
timestamp
|
|
447
|
+
);
|
|
334
448
|
} else {
|
|
335
449
|
// Regular execution with session
|
|
336
450
|
const response = await this.client.commands.execute(command, sessionId);
|
|
337
451
|
|
|
338
452
|
const duration = Date.now() - startTime;
|
|
339
|
-
result = this.mapExecuteResponseToExecResult(
|
|
453
|
+
result = this.mapExecuteResponseToExecResult(
|
|
454
|
+
response,
|
|
455
|
+
duration,
|
|
456
|
+
sessionId
|
|
457
|
+
);
|
|
340
458
|
}
|
|
341
459
|
|
|
342
460
|
// Call completion callback if provided
|
|
@@ -368,7 +486,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
368
486
|
let stderr = '';
|
|
369
487
|
|
|
370
488
|
try {
|
|
371
|
-
const stream = await this.client.commands.executeStream(
|
|
489
|
+
const stream = await this.client.commands.executeStream(
|
|
490
|
+
command,
|
|
491
|
+
sessionId
|
|
492
|
+
);
|
|
372
493
|
|
|
373
494
|
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
374
495
|
// Check for cancellation
|
|
@@ -413,7 +534,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
413
534
|
|
|
414
535
|
// If we get here without a complete event, something went wrong
|
|
415
536
|
throw new Error('Stream ended without completion event');
|
|
416
|
-
|
|
417
537
|
} catch (error) {
|
|
418
538
|
if (options.signal?.aborted) {
|
|
419
539
|
throw new Error('Operation was aborted');
|
|
@@ -461,8 +581,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
461
581
|
pid: data.pid,
|
|
462
582
|
command: data.command,
|
|
463
583
|
status: data.status,
|
|
464
|
-
startTime:
|
|
465
|
-
|
|
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,
|
|
466
593
|
exitCode: data.exitCode,
|
|
467
594
|
sessionId,
|
|
468
595
|
|
|
@@ -482,25 +609,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
482
609
|
};
|
|
483
610
|
}
|
|
484
611
|
|
|
485
|
-
|
|
486
612
|
// Background process management
|
|
487
|
-
async startProcess(
|
|
613
|
+
async startProcess(
|
|
614
|
+
command: string,
|
|
615
|
+
options?: ProcessOptions,
|
|
616
|
+
sessionId?: string
|
|
617
|
+
): Promise<Process> {
|
|
488
618
|
// Use the new HttpClient method to start the process
|
|
489
619
|
try {
|
|
490
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
491
|
-
const response = await this.client.processes.startProcess(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
);
|
|
504
641
|
|
|
505
642
|
// Call onStart callback if provided
|
|
506
643
|
if (options?.onStart) {
|
|
@@ -508,7 +645,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
508
645
|
}
|
|
509
646
|
|
|
510
647
|
return processObj;
|
|
511
|
-
|
|
512
648
|
} catch (error) {
|
|
513
649
|
if (options?.onError && error instanceof Error) {
|
|
514
650
|
options.onError(error);
|
|
@@ -519,42 +655,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
519
655
|
}
|
|
520
656
|
|
|
521
657
|
async listProcesses(sessionId?: string): Promise<Process[]> {
|
|
522
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
658
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
523
659
|
const response = await this.client.processes.listProcesses();
|
|
524
660
|
|
|
525
|
-
return response.processes.map(processData =>
|
|
526
|
-
this.createProcessFromDTO(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
+
)
|
|
535
674
|
);
|
|
536
675
|
}
|
|
537
676
|
|
|
538
677
|
async getProcess(id: string, sessionId?: string): Promise<Process | null> {
|
|
539
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
678
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
540
679
|
const response = await this.client.processes.getProcess(id);
|
|
541
680
|
if (!response.process) {
|
|
542
681
|
return null;
|
|
543
682
|
}
|
|
544
683
|
|
|
545
684
|
const processData = response.process;
|
|
546
|
-
return this.createProcessFromDTO(
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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> {
|
|
558
704
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
559
705
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
560
706
|
await this.client.processes.killProcess(id);
|
|
@@ -572,7 +718,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
572
718
|
return 0;
|
|
573
719
|
}
|
|
574
720
|
|
|
575
|
-
async getProcessLogs(
|
|
721
|
+
async getProcessLogs(
|
|
722
|
+
id: string,
|
|
723
|
+
sessionId?: string
|
|
724
|
+
): Promise<{ stdout: string; stderr: string; processId: string }> {
|
|
576
725
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
577
726
|
const response = await this.client.processes.getProcessLogs(id);
|
|
578
727
|
return {
|
|
@@ -582,9 +731,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
582
731
|
};
|
|
583
732
|
}
|
|
584
733
|
|
|
585
|
-
|
|
586
734
|
// Streaming methods - return ReadableStream for RPC compatibility
|
|
587
|
-
async execStream(
|
|
735
|
+
async execStream(
|
|
736
|
+
command: string,
|
|
737
|
+
options?: StreamOptions
|
|
738
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
588
739
|
// Check for cancellation
|
|
589
740
|
if (options?.signal?.aborted) {
|
|
590
741
|
throw new Error('Operation was aborted');
|
|
@@ -598,7 +749,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
598
749
|
/**
|
|
599
750
|
* Internal session-aware execStream implementation
|
|
600
751
|
*/
|
|
601
|
-
private async execStreamWithSession(
|
|
752
|
+
private async execStreamWithSession(
|
|
753
|
+
command: string,
|
|
754
|
+
sessionId: string,
|
|
755
|
+
options?: StreamOptions
|
|
756
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
602
757
|
// Check for cancellation
|
|
603
758
|
if (options?.signal?.aborted) {
|
|
604
759
|
throw new Error('Operation was aborted');
|
|
@@ -607,7 +762,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
607
762
|
return this.client.commands.executeStream(command, sessionId);
|
|
608
763
|
}
|
|
609
764
|
|
|
610
|
-
|
|
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>> {
|
|
611
772
|
// Check for cancellation
|
|
612
773
|
if (options?.signal?.aborted) {
|
|
613
774
|
throw new Error('Operation was aborted');
|
|
@@ -620,7 +781,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
620
781
|
repoUrl: string,
|
|
621
782
|
options: { branch?: string; targetDir?: string; sessionId?: string }
|
|
622
783
|
) {
|
|
623
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
784
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
624
785
|
return this.client.git.checkout(repoUrl, session, {
|
|
625
786
|
branch: options.branch,
|
|
626
787
|
targetDir: options.targetDir
|
|
@@ -631,8 +792,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
631
792
|
path: string,
|
|
632
793
|
options: { recursive?: boolean; sessionId?: string } = {}
|
|
633
794
|
) {
|
|
634
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
635
|
-
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
|
+
});
|
|
636
799
|
}
|
|
637
800
|
|
|
638
801
|
async writeFile(
|
|
@@ -640,21 +803,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
640
803
|
content: string,
|
|
641
804
|
options: { encoding?: string; sessionId?: string } = {}
|
|
642
805
|
) {
|
|
643
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
644
|
-
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
|
+
});
|
|
645
810
|
}
|
|
646
811
|
|
|
647
812
|
async deleteFile(path: string, sessionId?: string) {
|
|
648
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
813
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
649
814
|
return this.client.files.deleteFile(path, session);
|
|
650
815
|
}
|
|
651
816
|
|
|
652
|
-
async renameFile(
|
|
653
|
-
|
|
654
|
-
newPath: string,
|
|
655
|
-
sessionId?: string
|
|
656
|
-
) {
|
|
657
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
817
|
+
async renameFile(oldPath: string, newPath: string, sessionId?: string) {
|
|
818
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
658
819
|
return this.client.files.renameFile(oldPath, newPath, session);
|
|
659
820
|
}
|
|
660
821
|
|
|
@@ -663,7 +824,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
663
824
|
destinationPath: string,
|
|
664
825
|
sessionId?: string
|
|
665
826
|
) {
|
|
666
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
827
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
667
828
|
return this.client.files.moveFile(sourcePath, destinationPath, session);
|
|
668
829
|
}
|
|
669
830
|
|
|
@@ -671,8 +832,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
671
832
|
path: string,
|
|
672
833
|
options: { encoding?: string; sessionId?: string } = {}
|
|
673
834
|
) {
|
|
674
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
675
|
-
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
|
+
});
|
|
676
839
|
}
|
|
677
840
|
|
|
678
841
|
/**
|
|
@@ -685,7 +848,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
685
848
|
path: string,
|
|
686
849
|
options: { sessionId?: string } = {}
|
|
687
850
|
): Promise<ReadableStream<Uint8Array>> {
|
|
688
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
851
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
689
852
|
return this.client.files.readFileStream(path, session);
|
|
690
853
|
}
|
|
691
854
|
|
|
@@ -697,6 +860,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
697
860
|
return this.client.files.listFiles(path, session, options);
|
|
698
861
|
}
|
|
699
862
|
|
|
863
|
+
async exists(path: string, sessionId?: string) {
|
|
864
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
865
|
+
return this.client.files.exists(path, session);
|
|
866
|
+
}
|
|
867
|
+
|
|
700
868
|
async exposePort(port: number, options: { name?: string; hostname: string }) {
|
|
701
869
|
// Check if hostname is workers.dev domain (doesn't support wildcard subdomains)
|
|
702
870
|
if (options.hostname.endsWith('.workers.dev')) {
|
|
@@ -715,7 +883,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
715
883
|
|
|
716
884
|
// We need the sandbox name to construct preview URLs
|
|
717
885
|
if (!this.sandboxName) {
|
|
718
|
-
throw new Error(
|
|
886
|
+
throw new Error(
|
|
887
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
888
|
+
);
|
|
719
889
|
}
|
|
720
890
|
|
|
721
891
|
// Generate and store token for this port
|
|
@@ -723,18 +893,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
723
893
|
this.portTokens.set(port, token);
|
|
724
894
|
await this.persistPortTokens();
|
|
725
895
|
|
|
726
|
-
const url = this.constructPreviewUrl(
|
|
896
|
+
const url = this.constructPreviewUrl(
|
|
897
|
+
port,
|
|
898
|
+
this.sandboxName,
|
|
899
|
+
options.hostname,
|
|
900
|
+
token
|
|
901
|
+
);
|
|
727
902
|
|
|
728
903
|
return {
|
|
729
904
|
url,
|
|
730
905
|
port,
|
|
731
|
-
name: options?.name
|
|
906
|
+
name: options?.name
|
|
732
907
|
};
|
|
733
908
|
}
|
|
734
909
|
|
|
735
910
|
async unexposePort(port: number) {
|
|
736
911
|
if (!validatePort(port)) {
|
|
737
|
-
throw new SecurityError(
|
|
912
|
+
throw new SecurityError(
|
|
913
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
914
|
+
);
|
|
738
915
|
}
|
|
739
916
|
|
|
740
917
|
const sessionId = await this.ensureDefaultSession();
|
|
@@ -753,32 +930,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
753
930
|
|
|
754
931
|
// We need the sandbox name to construct preview URLs
|
|
755
932
|
if (!this.sandboxName) {
|
|
756
|
-
throw new Error(
|
|
933
|
+
throw new Error(
|
|
934
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
935
|
+
);
|
|
757
936
|
}
|
|
758
937
|
|
|
759
|
-
return response.ports.map(port => {
|
|
938
|
+
return response.ports.map((port) => {
|
|
760
939
|
// Get token for this port - must exist for all exposed ports
|
|
761
940
|
const token = this.portTokens.get(port.port);
|
|
762
941
|
if (!token) {
|
|
763
|
-
throw new Error(
|
|
942
|
+
throw new Error(
|
|
943
|
+
`Port ${port.port} is exposed but has no token. This should not happen.`
|
|
944
|
+
);
|
|
764
945
|
}
|
|
765
946
|
|
|
766
947
|
return {
|
|
767
|
-
url: this.constructPreviewUrl(
|
|
948
|
+
url: this.constructPreviewUrl(
|
|
949
|
+
port.port,
|
|
950
|
+
this.sandboxName!,
|
|
951
|
+
hostname,
|
|
952
|
+
token
|
|
953
|
+
),
|
|
768
954
|
port: port.port,
|
|
769
|
-
status: port.status
|
|
955
|
+
status: port.status
|
|
770
956
|
};
|
|
771
957
|
});
|
|
772
958
|
}
|
|
773
959
|
|
|
774
|
-
|
|
775
960
|
async isPortExposed(port: number): Promise<boolean> {
|
|
776
961
|
try {
|
|
777
962
|
const sessionId = await this.ensureDefaultSession();
|
|
778
963
|
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
779
|
-
return response.ports.some(exposedPort => exposedPort.port === port);
|
|
964
|
+
return response.ports.some((exposedPort) => exposedPort.port === port);
|
|
780
965
|
} catch (error) {
|
|
781
|
-
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
|
+
);
|
|
782
971
|
return false;
|
|
783
972
|
}
|
|
784
973
|
}
|
|
@@ -794,7 +983,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
794
983
|
const storedToken = this.portTokens.get(port);
|
|
795
984
|
if (!storedToken) {
|
|
796
985
|
// This should not happen - all exposed ports must have tokens
|
|
797
|
-
this.logger.error(
|
|
986
|
+
this.logger.error(
|
|
987
|
+
'Port is exposed but has no token - bug detected',
|
|
988
|
+
undefined,
|
|
989
|
+
{ port }
|
|
990
|
+
);
|
|
798
991
|
return false;
|
|
799
992
|
}
|
|
800
993
|
|
|
@@ -810,7 +1003,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
810
1003
|
|
|
811
1004
|
// Convert to base64url format (URL-safe, no padding, lowercase)
|
|
812
1005
|
const base64 = btoa(String.fromCharCode(...array));
|
|
813
|
-
return base64
|
|
1006
|
+
return base64
|
|
1007
|
+
.replace(/\+/g, '-')
|
|
1008
|
+
.replace(/\//g, '_')
|
|
1009
|
+
.replace(/=/g, '')
|
|
1010
|
+
.toLowerCase();
|
|
814
1011
|
}
|
|
815
1012
|
|
|
816
1013
|
private async persistPortTokens(): Promise<void> {
|
|
@@ -822,9 +1019,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
822
1019
|
await this.ctx.storage.put('portTokens', tokensObj);
|
|
823
1020
|
}
|
|
824
1021
|
|
|
825
|
-
private constructPreviewUrl(
|
|
1022
|
+
private constructPreviewUrl(
|
|
1023
|
+
port: number,
|
|
1024
|
+
sandboxId: string,
|
|
1025
|
+
hostname: string,
|
|
1026
|
+
token: string
|
|
1027
|
+
): string {
|
|
826
1028
|
if (!validatePort(port)) {
|
|
827
|
-
throw new SecurityError(
|
|
1029
|
+
throw new SecurityError(
|
|
1030
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
1031
|
+
);
|
|
828
1032
|
}
|
|
829
1033
|
|
|
830
1034
|
// Validate sandbox ID (will throw SecurityError if invalid)
|
|
@@ -846,14 +1050,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
846
1050
|
|
|
847
1051
|
return baseUrl.toString();
|
|
848
1052
|
} catch (error) {
|
|
849
|
-
throw new SecurityError(
|
|
1053
|
+
throw new SecurityError(
|
|
1054
|
+
`Failed to construct preview URL: ${
|
|
1055
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1056
|
+
}`
|
|
1057
|
+
);
|
|
850
1058
|
}
|
|
851
1059
|
}
|
|
852
1060
|
|
|
853
1061
|
// Production subdomain logic - enforce HTTPS
|
|
854
1062
|
try {
|
|
855
1063
|
// Always use HTTPS for production (non-localhost)
|
|
856
|
-
const protocol =
|
|
1064
|
+
const protocol = 'https';
|
|
857
1065
|
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
858
1066
|
|
|
859
1067
|
// Construct subdomain safely with mandatory token
|
|
@@ -862,7 +1070,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
862
1070
|
|
|
863
1071
|
return baseUrl.toString();
|
|
864
1072
|
} catch (error) {
|
|
865
|
-
throw new SecurityError(
|
|
1073
|
+
throw new SecurityError(
|
|
1074
|
+
`Failed to construct preview URL: ${
|
|
1075
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1076
|
+
}`
|
|
1077
|
+
);
|
|
866
1078
|
}
|
|
867
1079
|
}
|
|
868
1080
|
|
|
@@ -881,7 +1093,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
881
1093
|
await this.client.utils.createSession({
|
|
882
1094
|
id: sessionId,
|
|
883
1095
|
env: options?.env,
|
|
884
|
-
cwd: options?.cwd
|
|
1096
|
+
cwd: options?.cwd
|
|
885
1097
|
});
|
|
886
1098
|
|
|
887
1099
|
// Return wrapper that binds sessionId to all operations
|
|
@@ -903,6 +1115,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
903
1115
|
return this.getSessionWrapper(sessionId);
|
|
904
1116
|
}
|
|
905
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
|
+
|
|
906
1146
|
/**
|
|
907
1147
|
* Internal helper to create ExecutionSession wrapper for a given sessionId
|
|
908
1148
|
* Used by both createSession and getSession
|
|
@@ -912,31 +1152,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
912
1152
|
id: sessionId,
|
|
913
1153
|
|
|
914
1154
|
// Command execution - delegate to internal session-aware methods
|
|
915
|
-
exec: (command, options) =>
|
|
916
|
-
|
|
1155
|
+
exec: (command, options) =>
|
|
1156
|
+
this.execWithSession(command, sessionId, options),
|
|
1157
|
+
execStream: (command, options) =>
|
|
1158
|
+
this.execStreamWithSession(command, sessionId, options),
|
|
917
1159
|
|
|
918
1160
|
// Process management
|
|
919
|
-
startProcess: (command, options) =>
|
|
1161
|
+
startProcess: (command, options) =>
|
|
1162
|
+
this.startProcess(command, options, sessionId),
|
|
920
1163
|
listProcesses: () => this.listProcesses(sessionId),
|
|
921
1164
|
getProcess: (id) => this.getProcess(id, sessionId),
|
|
922
1165
|
killProcess: (id, signal) => this.killProcess(id, signal),
|
|
923
1166
|
killAllProcesses: () => this.killAllProcesses(),
|
|
924
1167
|
cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
|
|
925
1168
|
getProcessLogs: (id) => this.getProcessLogs(id),
|
|
926
|
-
streamProcessLogs: (processId, options) =>
|
|
1169
|
+
streamProcessLogs: (processId, options) =>
|
|
1170
|
+
this.streamProcessLogs(processId, options),
|
|
927
1171
|
|
|
928
1172
|
// File operations - pass sessionId via options or parameter
|
|
929
|
-
writeFile: (path, content, options) =>
|
|
930
|
-
|
|
1173
|
+
writeFile: (path, content, options) =>
|
|
1174
|
+
this.writeFile(path, content, { ...options, sessionId }),
|
|
1175
|
+
readFile: (path, options) =>
|
|
1176
|
+
this.readFile(path, { ...options, sessionId }),
|
|
931
1177
|
readFileStream: (path) => this.readFileStream(path, { sessionId }),
|
|
932
1178
|
mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
|
|
933
1179
|
deleteFile: (path) => this.deleteFile(path, sessionId),
|
|
934
|
-
renameFile: (oldPath, newPath) =>
|
|
935
|
-
|
|
936
|
-
|
|
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),
|
|
1186
|
+
exists: (path) => this.exists(path, sessionId),
|
|
937
1187
|
|
|
938
1188
|
// Git operations
|
|
939
|
-
gitCheckout: (repoUrl, options) =>
|
|
1189
|
+
gitCheckout: (repoUrl, options) =>
|
|
1190
|
+
this.gitCheckout(repoUrl, { ...options, sessionId }),
|
|
940
1191
|
|
|
941
1192
|
// Environment management - needs special handling
|
|
942
1193
|
setEnvVars: async (envVars: Record<string, string>) => {
|
|
@@ -946,27 +1197,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
946
1197
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
947
1198
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
948
1199
|
|
|
949
|
-
const result = await this.client.commands.execute(
|
|
1200
|
+
const result = await this.client.commands.execute(
|
|
1201
|
+
exportCommand,
|
|
1202
|
+
sessionId
|
|
1203
|
+
);
|
|
950
1204
|
|
|
951
1205
|
if (result.exitCode !== 0) {
|
|
952
|
-
throw new Error(
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
1208
|
+
);
|
|
953
1209
|
}
|
|
954
1210
|
}
|
|
955
1211
|
} catch (error) {
|
|
956
|
-
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
|
+
);
|
|
957
1217
|
throw error;
|
|
958
1218
|
}
|
|
959
1219
|
},
|
|
960
1220
|
|
|
961
1221
|
// Code interpreter methods - delegate to sandbox's code interpreter
|
|
962
|
-
createCodeContext: (options) =>
|
|
1222
|
+
createCodeContext: (options) =>
|
|
1223
|
+
this.codeInterpreter.createCodeContext(options),
|
|
963
1224
|
runCode: async (code, options) => {
|
|
964
1225
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
965
1226
|
return execution.toJSON();
|
|
966
1227
|
},
|
|
967
|
-
runCodeStream: (code, options) =>
|
|
1228
|
+
runCodeStream: (code, options) =>
|
|
1229
|
+
this.codeInterpreter.runCodeStream(code, options),
|
|
968
1230
|
listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
|
|
969
|
-
deleteCodeContext: (contextId) =>
|
|
1231
|
+
deleteCodeContext: (contextId) =>
|
|
1232
|
+
this.codeInterpreter.deleteCodeContext(contextId)
|
|
970
1233
|
};
|
|
971
1234
|
}
|
|
972
1235
|
|
|
@@ -974,16 +1237,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
974
1237
|
// Code interpreter methods - delegate to CodeInterpreter wrapper
|
|
975
1238
|
// ============================================================================
|
|
976
1239
|
|
|
977
|
-
async createCodeContext(
|
|
1240
|
+
async createCodeContext(
|
|
1241
|
+
options?: CreateContextOptions
|
|
1242
|
+
): Promise<CodeContext> {
|
|
978
1243
|
return this.codeInterpreter.createCodeContext(options);
|
|
979
1244
|
}
|
|
980
1245
|
|
|
981
|
-
async runCode(
|
|
1246
|
+
async runCode(
|
|
1247
|
+
code: string,
|
|
1248
|
+
options?: RunCodeOptions
|
|
1249
|
+
): Promise<ExecutionResult> {
|
|
982
1250
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
983
1251
|
return execution.toJSON();
|
|
984
1252
|
}
|
|
985
1253
|
|
|
986
|
-
async runCodeStream(
|
|
1254
|
+
async runCodeStream(
|
|
1255
|
+
code: string,
|
|
1256
|
+
options?: RunCodeOptions
|
|
1257
|
+
): Promise<ReadableStream> {
|
|
987
1258
|
return this.codeInterpreter.runCodeStream(code, options);
|
|
988
1259
|
}
|
|
989
1260
|
|