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