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