@cloudflare/sandbox 0.0.0-02ee8fe → 0.0.0-04a60d8
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 +70 -16
- package/Dockerfile +79 -32
- package/README.md +9 -2
- package/dist/index.d.ts +1953 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3278 -65
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +31 -26
- package/src/clients/git-client.ts +9 -3
- package/src/clients/index.ts +16 -16
- package/src/clients/interpreter-client.ts +51 -47
- package/src/clients/port-client.ts +10 -10
- package/src/clients/process-client.ts +11 -8
- package/src/clients/sandbox-client.ts +2 -4
- package/src/clients/types.ts +6 -2
- package/src/clients/utility-client.ts +43 -6
- package/src/errors/adapter.ts +90 -32
- package/src/errors/classes.ts +189 -64
- package/src/errors/index.ts +9 -5
- package/src/file-stream.ts +11 -6
- package/src/index.ts +28 -17
- package/src/interpreter.ts +50 -41
- package/src/request-handler.ts +24 -21
- package/src/sandbox.ts +372 -149
- package/src/security.ts +21 -6
- package/src/sse-parser.ts +4 -3
- package/src/version.ts +1 -1
- package/startup.sh +1 -1
- package/tests/base-client.test.ts +116 -80
- package/tests/command-client.test.ts +149 -112
- package/tests/file-client.test.ts +309 -197
- package/tests/file-stream.test.ts +24 -20
- package/tests/get-sandbox.test.ts +10 -10
- package/tests/git-client.test.ts +260 -101
- package/tests/port-client.test.ts +100 -108
- package/tests/process-client.test.ts +204 -179
- package/tests/request-handler.test.ts +117 -65
- package/tests/sandbox.test.ts +252 -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,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);
|
|
@@ -53,12 +54,29 @@ export function getSandbox(
|
|
|
53
54
|
stub.setKeepAlive(options.keepAlive);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
return stub
|
|
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
|
+
};
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
60
78
|
defaultPort = 3000; // Default port for the container's Bun server
|
|
61
|
-
sleepAfter: string | number =
|
|
79
|
+
sleepAfter: string | number = '10m'; // Sleep the sandbox if no requests are made in this timeframe
|
|
62
80
|
|
|
63
81
|
client: SandboxClient;
|
|
64
82
|
private codeInterpreter: CodeInterpreter;
|
|
@@ -70,13 +88,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
70
88
|
private logger: ReturnType<typeof createLogger>;
|
|
71
89
|
private keepAliveEnabled: boolean = false;
|
|
72
90
|
|
|
73
|
-
constructor(ctx:
|
|
91
|
+
constructor(ctx: DurableObjectState<{}>, env: Env) {
|
|
74
92
|
super(ctx, env);
|
|
75
93
|
|
|
76
94
|
const envObj = env as any;
|
|
77
95
|
// Set sandbox environment variables from env object
|
|
78
96
|
const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const;
|
|
79
|
-
sandboxEnvKeys.forEach(key => {
|
|
97
|
+
sandboxEnvKeys.forEach((key) => {
|
|
80
98
|
if (envObj?.[key]) {
|
|
81
99
|
this.envVars[key] = envObj[key];
|
|
82
100
|
}
|
|
@@ -90,7 +108,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
90
108
|
this.client = new SandboxClient({
|
|
91
109
|
logger: this.logger,
|
|
92
110
|
port: 3000, // Control plane port
|
|
93
|
-
stub: this
|
|
111
|
+
stub: this
|
|
94
112
|
});
|
|
95
113
|
|
|
96
114
|
// Initialize code interpreter - pass 'this' after client is ready
|
|
@@ -99,9 +117,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
99
117
|
|
|
100
118
|
// Load the sandbox name, port tokens, and default session from storage on initialization
|
|
101
119
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
102
|
-
this.sandboxName =
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
{};
|
|
105
127
|
|
|
106
128
|
// Convert stored tokens back to Map
|
|
107
129
|
this.portTokens = new Map();
|
|
@@ -125,8 +147,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
125
147
|
this.baseUrl = baseUrl;
|
|
126
148
|
await this.ctx.storage.put('baseUrl', baseUrl);
|
|
127
149
|
} else {
|
|
128
|
-
if(this.baseUrl !== baseUrl) {
|
|
129
|
-
throw new Error(
|
|
150
|
+
if (this.baseUrl !== baseUrl) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
'Base URL already set and different from one previously provided'
|
|
153
|
+
);
|
|
130
154
|
}
|
|
131
155
|
}
|
|
132
156
|
}
|
|
@@ -140,9 +164,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
140
164
|
async setKeepAlive(keepAlive: boolean): Promise<void> {
|
|
141
165
|
this.keepAliveEnabled = keepAlive;
|
|
142
166
|
if (keepAlive) {
|
|
143
|
-
this.logger.info(
|
|
167
|
+
this.logger.info(
|
|
168
|
+
'KeepAlive mode enabled - container will stay alive until explicitly destroyed'
|
|
169
|
+
);
|
|
144
170
|
} else {
|
|
145
|
-
this.logger.info(
|
|
171
|
+
this.logger.info(
|
|
172
|
+
'KeepAlive mode disabled - container will timeout normally'
|
|
173
|
+
);
|
|
146
174
|
}
|
|
147
175
|
}
|
|
148
176
|
|
|
@@ -158,10 +186,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
158
186
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
159
187
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
160
188
|
|
|
161
|
-
const result = await this.client.commands.execute(
|
|
189
|
+
const result = await this.client.commands.execute(
|
|
190
|
+
exportCommand,
|
|
191
|
+
this.defaultSession
|
|
192
|
+
);
|
|
162
193
|
|
|
163
194
|
if (result.exitCode !== 0) {
|
|
164
|
-
throw new Error(
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
197
|
+
);
|
|
165
198
|
}
|
|
166
199
|
}
|
|
167
200
|
}
|
|
@@ -179,8 +212,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
179
212
|
this.logger.debug('Sandbox started');
|
|
180
213
|
|
|
181
214
|
// Check version compatibility asynchronously (don't block startup)
|
|
182
|
-
this.checkVersionCompatibility().catch(error => {
|
|
183
|
-
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
|
+
);
|
|
184
220
|
});
|
|
185
221
|
}
|
|
186
222
|
|
|
@@ -200,8 +236,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
200
236
|
if (containerVersion === 'unknown') {
|
|
201
237
|
this.logger.warn(
|
|
202
238
|
'Container version check: Container version could not be determined. ' +
|
|
203
|
-
|
|
204
|
-
|
|
239
|
+
'This may indicate an outdated container image. ' +
|
|
240
|
+
'Please update your container to match SDK version ' +
|
|
241
|
+
sdkVersion
|
|
205
242
|
);
|
|
206
243
|
return;
|
|
207
244
|
}
|
|
@@ -217,7 +254,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
217
254
|
// so we always use warning level as requested by the user
|
|
218
255
|
this.logger.warn(message);
|
|
219
256
|
} else {
|
|
220
|
-
this.logger.debug('Version check passed', {
|
|
257
|
+
this.logger.debug('Version check passed', {
|
|
258
|
+
sdkVersion,
|
|
259
|
+
containerVersion
|
|
260
|
+
});
|
|
221
261
|
}
|
|
222
262
|
} catch (error) {
|
|
223
263
|
// Don't fail the sandbox initialization if version check fails
|
|
@@ -232,7 +272,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
232
272
|
}
|
|
233
273
|
|
|
234
274
|
override onError(error: unknown) {
|
|
235
|
-
this.logger.error(
|
|
275
|
+
this.logger.error(
|
|
276
|
+
'Sandbox error',
|
|
277
|
+
error instanceof Error ? error : new Error(String(error))
|
|
278
|
+
);
|
|
236
279
|
}
|
|
237
280
|
|
|
238
281
|
/**
|
|
@@ -241,7 +284,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
241
284
|
*/
|
|
242
285
|
override async onActivityExpired(): Promise<void> {
|
|
243
286
|
if (this.keepAliveEnabled) {
|
|
244
|
-
this.logger.debug(
|
|
287
|
+
this.logger.debug(
|
|
288
|
+
'Activity expired but keepAlive is enabled - container will stay alive'
|
|
289
|
+
);
|
|
245
290
|
// Do nothing - don't call stop(), container stays alive
|
|
246
291
|
} else {
|
|
247
292
|
// Default behavior: stop the container
|
|
@@ -250,11 +295,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
250
295
|
}
|
|
251
296
|
}
|
|
252
297
|
|
|
253
|
-
|
|
254
298
|
// Override fetch to route internal container requests to appropriate ports
|
|
255
299
|
override async fetch(request: Request): Promise<Response> {
|
|
256
300
|
// Extract or generate trace ID from request
|
|
257
|
-
const traceId =
|
|
301
|
+
const traceId =
|
|
302
|
+
TraceContext.fromHeaders(request.headers) || TraceContext.generate();
|
|
258
303
|
|
|
259
304
|
// Create request-specific logger with trace ID
|
|
260
305
|
const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
|
|
@@ -269,14 +314,30 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
269
314
|
await this.ctx.storage.put('sandboxName', name);
|
|
270
315
|
}
|
|
271
316
|
|
|
272
|
-
// Detect WebSocket upgrade request
|
|
317
|
+
// Detect WebSocket upgrade request (RFC 6455 compliant)
|
|
273
318
|
const upgradeHeader = request.headers.get('Upgrade');
|
|
274
|
-
const
|
|
319
|
+
const connectionHeader = request.headers.get('Connection');
|
|
320
|
+
const isWebSocket =
|
|
321
|
+
upgradeHeader?.toLowerCase() === 'websocket' &&
|
|
322
|
+
connectionHeader?.toLowerCase().includes('upgrade');
|
|
275
323
|
|
|
276
324
|
if (isWebSocket) {
|
|
277
325
|
// WebSocket path: Let parent Container class handle WebSocket proxying
|
|
278
326
|
// This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
|
|
279
|
-
|
|
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
|
+
}
|
|
280
341
|
}
|
|
281
342
|
|
|
282
343
|
// Non-WebSocket: Use existing port determination and HTTP routing logic
|
|
@@ -287,6 +348,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
287
348
|
});
|
|
288
349
|
}
|
|
289
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
|
+
|
|
290
356
|
private determinePort(url: URL): number {
|
|
291
357
|
// Extract port from proxy requests (e.g., /proxy/8080/*)
|
|
292
358
|
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
@@ -316,7 +382,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
316
382
|
await this.client.utils.createSession({
|
|
317
383
|
id: sessionId,
|
|
318
384
|
env: this.envVars || {},
|
|
319
|
-
cwd: '/workspace'
|
|
385
|
+
cwd: '/workspace'
|
|
320
386
|
});
|
|
321
387
|
|
|
322
388
|
this.defaultSession = sessionId;
|
|
@@ -326,7 +392,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
326
392
|
} catch (error: any) {
|
|
327
393
|
// If session already exists (e.g., after hot reload), reuse it
|
|
328
394
|
if (error?.message?.includes('already exists')) {
|
|
329
|
-
this.logger.debug('Reusing existing session after reload', {
|
|
395
|
+
this.logger.debug('Reusing existing session after reload', {
|
|
396
|
+
sessionId
|
|
397
|
+
});
|
|
330
398
|
this.defaultSession = sessionId;
|
|
331
399
|
// Persist to storage in case it wasn't saved before
|
|
332
400
|
await this.ctx.storage.put('defaultSession', sessionId);
|
|
@@ -370,13 +438,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
370
438
|
|
|
371
439
|
if (options?.stream && options?.onOutput) {
|
|
372
440
|
// Streaming with callbacks - we need to collect the final result
|
|
373
|
-
result = await this.executeWithStreaming(
|
|
441
|
+
result = await this.executeWithStreaming(
|
|
442
|
+
command,
|
|
443
|
+
sessionId,
|
|
444
|
+
options,
|
|
445
|
+
startTime,
|
|
446
|
+
timestamp
|
|
447
|
+
);
|
|
374
448
|
} else {
|
|
375
449
|
// Regular execution with session
|
|
376
450
|
const response = await this.client.commands.execute(command, sessionId);
|
|
377
451
|
|
|
378
452
|
const duration = Date.now() - startTime;
|
|
379
|
-
result = this.mapExecuteResponseToExecResult(
|
|
453
|
+
result = this.mapExecuteResponseToExecResult(
|
|
454
|
+
response,
|
|
455
|
+
duration,
|
|
456
|
+
sessionId
|
|
457
|
+
);
|
|
380
458
|
}
|
|
381
459
|
|
|
382
460
|
// Call completion callback if provided
|
|
@@ -408,7 +486,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
408
486
|
let stderr = '';
|
|
409
487
|
|
|
410
488
|
try {
|
|
411
|
-
const stream = await this.client.commands.executeStream(
|
|
489
|
+
const stream = await this.client.commands.executeStream(
|
|
490
|
+
command,
|
|
491
|
+
sessionId
|
|
492
|
+
);
|
|
412
493
|
|
|
413
494
|
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
414
495
|
// Check for cancellation
|
|
@@ -453,7 +534,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
453
534
|
|
|
454
535
|
// If we get here without a complete event, something went wrong
|
|
455
536
|
throw new Error('Stream ended without completion event');
|
|
456
|
-
|
|
457
537
|
} catch (error) {
|
|
458
538
|
if (options.signal?.aborted) {
|
|
459
539
|
throw new Error('Operation was aborted');
|
|
@@ -501,8 +581,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
501
581
|
pid: data.pid,
|
|
502
582
|
command: data.command,
|
|
503
583
|
status: data.status,
|
|
504
|
-
startTime:
|
|
505
|
-
|
|
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,
|
|
506
593
|
exitCode: data.exitCode,
|
|
507
594
|
sessionId,
|
|
508
595
|
|
|
@@ -522,25 +609,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
522
609
|
};
|
|
523
610
|
}
|
|
524
611
|
|
|
525
|
-
|
|
526
612
|
// Background process management
|
|
527
|
-
async startProcess(
|
|
613
|
+
async startProcess(
|
|
614
|
+
command: string,
|
|
615
|
+
options?: ProcessOptions,
|
|
616
|
+
sessionId?: string
|
|
617
|
+
): Promise<Process> {
|
|
528
618
|
// Use the new HttpClient method to start the process
|
|
529
619
|
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
|
-
|
|
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
|
+
);
|
|
544
641
|
|
|
545
642
|
// Call onStart callback if provided
|
|
546
643
|
if (options?.onStart) {
|
|
@@ -548,7 +645,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
548
645
|
}
|
|
549
646
|
|
|
550
647
|
return processObj;
|
|
551
|
-
|
|
552
648
|
} catch (error) {
|
|
553
649
|
if (options?.onError && error instanceof Error) {
|
|
554
650
|
options.onError(error);
|
|
@@ -559,42 +655,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
559
655
|
}
|
|
560
656
|
|
|
561
657
|
async listProcesses(sessionId?: string): Promise<Process[]> {
|
|
562
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
658
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
563
659
|
const response = await this.client.processes.listProcesses();
|
|
564
660
|
|
|
565
|
-
return response.processes.map(processData =>
|
|
566
|
-
this.createProcessFromDTO(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
+
)
|
|
575
674
|
);
|
|
576
675
|
}
|
|
577
676
|
|
|
578
677
|
async getProcess(id: string, sessionId?: string): Promise<Process | null> {
|
|
579
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
678
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
580
679
|
const response = await this.client.processes.getProcess(id);
|
|
581
680
|
if (!response.process) {
|
|
582
681
|
return null;
|
|
583
682
|
}
|
|
584
683
|
|
|
585
684
|
const processData = response.process;
|
|
586
|
-
return this.createProcessFromDTO(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
+
);
|
|
595
697
|
}
|
|
596
698
|
|
|
597
|
-
async killProcess(
|
|
699
|
+
async killProcess(
|
|
700
|
+
id: string,
|
|
701
|
+
signal?: string,
|
|
702
|
+
sessionId?: string
|
|
703
|
+
): Promise<void> {
|
|
598
704
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
599
705
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
600
706
|
await this.client.processes.killProcess(id);
|
|
@@ -612,7 +718,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
612
718
|
return 0;
|
|
613
719
|
}
|
|
614
720
|
|
|
615
|
-
async getProcessLogs(
|
|
721
|
+
async getProcessLogs(
|
|
722
|
+
id: string,
|
|
723
|
+
sessionId?: string
|
|
724
|
+
): Promise<{ stdout: string; stderr: string; processId: string }> {
|
|
616
725
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
617
726
|
const response = await this.client.processes.getProcessLogs(id);
|
|
618
727
|
return {
|
|
@@ -622,8 +731,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
622
731
|
};
|
|
623
732
|
}
|
|
624
733
|
|
|
625
|
-
// Streaming methods - return ReadableStream for RPC compatibility
|
|
626
|
-
async execStream(
|
|
734
|
+
// Streaming methods - return ReadableStream for RPC compatibility
|
|
735
|
+
async execStream(
|
|
736
|
+
command: string,
|
|
737
|
+
options?: StreamOptions
|
|
738
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
627
739
|
// Check for cancellation
|
|
628
740
|
if (options?.signal?.aborted) {
|
|
629
741
|
throw new Error('Operation was aborted');
|
|
@@ -637,7 +749,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
637
749
|
/**
|
|
638
750
|
* Internal session-aware execStream implementation
|
|
639
751
|
*/
|
|
640
|
-
private async execStreamWithSession(
|
|
752
|
+
private async execStreamWithSession(
|
|
753
|
+
command: string,
|
|
754
|
+
sessionId: string,
|
|
755
|
+
options?: StreamOptions
|
|
756
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
641
757
|
// Check for cancellation
|
|
642
758
|
if (options?.signal?.aborted) {
|
|
643
759
|
throw new Error('Operation was aborted');
|
|
@@ -649,7 +765,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
649
765
|
/**
|
|
650
766
|
* Stream logs from a background process as a ReadableStream.
|
|
651
767
|
*/
|
|
652
|
-
async streamProcessLogs(
|
|
768
|
+
async streamProcessLogs(
|
|
769
|
+
processId: string,
|
|
770
|
+
options?: { signal?: AbortSignal }
|
|
771
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
653
772
|
// Check for cancellation
|
|
654
773
|
if (options?.signal?.aborted) {
|
|
655
774
|
throw new Error('Operation was aborted');
|
|
@@ -662,7 +781,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
662
781
|
repoUrl: string,
|
|
663
782
|
options: { branch?: string; targetDir?: string; sessionId?: string }
|
|
664
783
|
) {
|
|
665
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
784
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
666
785
|
return this.client.git.checkout(repoUrl, session, {
|
|
667
786
|
branch: options.branch,
|
|
668
787
|
targetDir: options.targetDir
|
|
@@ -673,8 +792,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
673
792
|
path: string,
|
|
674
793
|
options: { recursive?: boolean; sessionId?: string } = {}
|
|
675
794
|
) {
|
|
676
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
677
|
-
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
|
+
});
|
|
678
799
|
}
|
|
679
800
|
|
|
680
801
|
async writeFile(
|
|
@@ -682,21 +803,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
682
803
|
content: string,
|
|
683
804
|
options: { encoding?: string; sessionId?: string } = {}
|
|
684
805
|
) {
|
|
685
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
686
|
-
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
|
+
});
|
|
687
810
|
}
|
|
688
811
|
|
|
689
812
|
async deleteFile(path: string, sessionId?: string) {
|
|
690
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
813
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
691
814
|
return this.client.files.deleteFile(path, session);
|
|
692
815
|
}
|
|
693
816
|
|
|
694
|
-
async renameFile(
|
|
695
|
-
|
|
696
|
-
newPath: string,
|
|
697
|
-
sessionId?: string
|
|
698
|
-
) {
|
|
699
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
817
|
+
async renameFile(oldPath: string, newPath: string, sessionId?: string) {
|
|
818
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
700
819
|
return this.client.files.renameFile(oldPath, newPath, session);
|
|
701
820
|
}
|
|
702
821
|
|
|
@@ -705,7 +824,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
705
824
|
destinationPath: string,
|
|
706
825
|
sessionId?: string
|
|
707
826
|
) {
|
|
708
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
827
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
709
828
|
return this.client.files.moveFile(sourcePath, destinationPath, session);
|
|
710
829
|
}
|
|
711
830
|
|
|
@@ -713,8 +832,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
713
832
|
path: string,
|
|
714
833
|
options: { encoding?: string; sessionId?: string } = {}
|
|
715
834
|
) {
|
|
716
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
717
|
-
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
|
+
});
|
|
718
839
|
}
|
|
719
840
|
|
|
720
841
|
/**
|
|
@@ -727,7 +848,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
727
848
|
path: string,
|
|
728
849
|
options: { sessionId?: string } = {}
|
|
729
850
|
): Promise<ReadableStream<Uint8Array>> {
|
|
730
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
851
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
731
852
|
return this.client.files.readFileStream(path, session);
|
|
732
853
|
}
|
|
733
854
|
|
|
@@ -740,7 +861,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
740
861
|
}
|
|
741
862
|
|
|
742
863
|
async exists(path: string, sessionId?: string) {
|
|
743
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
864
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
744
865
|
return this.client.files.exists(path, session);
|
|
745
866
|
}
|
|
746
867
|
|
|
@@ -762,7 +883,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
762
883
|
|
|
763
884
|
// We need the sandbox name to construct preview URLs
|
|
764
885
|
if (!this.sandboxName) {
|
|
765
|
-
throw new Error(
|
|
886
|
+
throw new Error(
|
|
887
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
888
|
+
);
|
|
766
889
|
}
|
|
767
890
|
|
|
768
891
|
// Generate and store token for this port
|
|
@@ -770,18 +893,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
770
893
|
this.portTokens.set(port, token);
|
|
771
894
|
await this.persistPortTokens();
|
|
772
895
|
|
|
773
|
-
const url = this.constructPreviewUrl(
|
|
896
|
+
const url = this.constructPreviewUrl(
|
|
897
|
+
port,
|
|
898
|
+
this.sandboxName,
|
|
899
|
+
options.hostname,
|
|
900
|
+
token
|
|
901
|
+
);
|
|
774
902
|
|
|
775
903
|
return {
|
|
776
904
|
url,
|
|
777
905
|
port,
|
|
778
|
-
name: options?.name
|
|
906
|
+
name: options?.name
|
|
779
907
|
};
|
|
780
908
|
}
|
|
781
909
|
|
|
782
910
|
async unexposePort(port: number) {
|
|
783
911
|
if (!validatePort(port)) {
|
|
784
|
-
throw new SecurityError(
|
|
912
|
+
throw new SecurityError(
|
|
913
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
914
|
+
);
|
|
785
915
|
}
|
|
786
916
|
|
|
787
917
|
const sessionId = await this.ensureDefaultSession();
|
|
@@ -800,32 +930,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
800
930
|
|
|
801
931
|
// We need the sandbox name to construct preview URLs
|
|
802
932
|
if (!this.sandboxName) {
|
|
803
|
-
throw new Error(
|
|
933
|
+
throw new Error(
|
|
934
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
935
|
+
);
|
|
804
936
|
}
|
|
805
937
|
|
|
806
|
-
return response.ports.map(port => {
|
|
938
|
+
return response.ports.map((port) => {
|
|
807
939
|
// Get token for this port - must exist for all exposed ports
|
|
808
940
|
const token = this.portTokens.get(port.port);
|
|
809
941
|
if (!token) {
|
|
810
|
-
throw new Error(
|
|
942
|
+
throw new Error(
|
|
943
|
+
`Port ${port.port} is exposed but has no token. This should not happen.`
|
|
944
|
+
);
|
|
811
945
|
}
|
|
812
946
|
|
|
813
947
|
return {
|
|
814
|
-
url: this.constructPreviewUrl(
|
|
948
|
+
url: this.constructPreviewUrl(
|
|
949
|
+
port.port,
|
|
950
|
+
this.sandboxName!,
|
|
951
|
+
hostname,
|
|
952
|
+
token
|
|
953
|
+
),
|
|
815
954
|
port: port.port,
|
|
816
|
-
status: port.status
|
|
955
|
+
status: port.status
|
|
817
956
|
};
|
|
818
957
|
});
|
|
819
958
|
}
|
|
820
959
|
|
|
821
|
-
|
|
822
960
|
async isPortExposed(port: number): Promise<boolean> {
|
|
823
961
|
try {
|
|
824
962
|
const sessionId = await this.ensureDefaultSession();
|
|
825
963
|
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
826
|
-
return response.ports.some(exposedPort => exposedPort.port === port);
|
|
964
|
+
return response.ports.some((exposedPort) => exposedPort.port === port);
|
|
827
965
|
} catch (error) {
|
|
828
|
-
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
|
+
);
|
|
829
971
|
return false;
|
|
830
972
|
}
|
|
831
973
|
}
|
|
@@ -841,7 +983,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
841
983
|
const storedToken = this.portTokens.get(port);
|
|
842
984
|
if (!storedToken) {
|
|
843
985
|
// This should not happen - all exposed ports must have tokens
|
|
844
|
-
this.logger.error(
|
|
986
|
+
this.logger.error(
|
|
987
|
+
'Port is exposed but has no token - bug detected',
|
|
988
|
+
undefined,
|
|
989
|
+
{ port }
|
|
990
|
+
);
|
|
845
991
|
return false;
|
|
846
992
|
}
|
|
847
993
|
|
|
@@ -857,7 +1003,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
857
1003
|
|
|
858
1004
|
// Convert to base64url format (URL-safe, no padding, lowercase)
|
|
859
1005
|
const base64 = btoa(String.fromCharCode(...array));
|
|
860
|
-
return base64
|
|
1006
|
+
return base64
|
|
1007
|
+
.replace(/\+/g, '-')
|
|
1008
|
+
.replace(/\//g, '_')
|
|
1009
|
+
.replace(/=/g, '')
|
|
1010
|
+
.toLowerCase();
|
|
861
1011
|
}
|
|
862
1012
|
|
|
863
1013
|
private async persistPortTokens(): Promise<void> {
|
|
@@ -869,9 +1019,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
869
1019
|
await this.ctx.storage.put('portTokens', tokensObj);
|
|
870
1020
|
}
|
|
871
1021
|
|
|
872
|
-
private constructPreviewUrl(
|
|
1022
|
+
private constructPreviewUrl(
|
|
1023
|
+
port: number,
|
|
1024
|
+
sandboxId: string,
|
|
1025
|
+
hostname: string,
|
|
1026
|
+
token: string
|
|
1027
|
+
): string {
|
|
873
1028
|
if (!validatePort(port)) {
|
|
874
|
-
throw new SecurityError(
|
|
1029
|
+
throw new SecurityError(
|
|
1030
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
1031
|
+
);
|
|
875
1032
|
}
|
|
876
1033
|
|
|
877
1034
|
// Validate sandbox ID (will throw SecurityError if invalid)
|
|
@@ -893,14 +1050,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
893
1050
|
|
|
894
1051
|
return baseUrl.toString();
|
|
895
1052
|
} catch (error) {
|
|
896
|
-
throw new SecurityError(
|
|
1053
|
+
throw new SecurityError(
|
|
1054
|
+
`Failed to construct preview URL: ${
|
|
1055
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1056
|
+
}`
|
|
1057
|
+
);
|
|
897
1058
|
}
|
|
898
1059
|
}
|
|
899
1060
|
|
|
900
1061
|
// Production subdomain logic - enforce HTTPS
|
|
901
1062
|
try {
|
|
902
1063
|
// Always use HTTPS for production (non-localhost)
|
|
903
|
-
const protocol =
|
|
1064
|
+
const protocol = 'https';
|
|
904
1065
|
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
905
1066
|
|
|
906
1067
|
// Construct subdomain safely with mandatory token
|
|
@@ -909,7 +1070,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
909
1070
|
|
|
910
1071
|
return baseUrl.toString();
|
|
911
1072
|
} catch (error) {
|
|
912
|
-
throw new SecurityError(
|
|
1073
|
+
throw new SecurityError(
|
|
1074
|
+
`Failed to construct preview URL: ${
|
|
1075
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1076
|
+
}`
|
|
1077
|
+
);
|
|
913
1078
|
}
|
|
914
1079
|
}
|
|
915
1080
|
|
|
@@ -928,7 +1093,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
928
1093
|
await this.client.utils.createSession({
|
|
929
1094
|
id: sessionId,
|
|
930
1095
|
env: options?.env,
|
|
931
|
-
cwd: options?.cwd
|
|
1096
|
+
cwd: options?.cwd
|
|
932
1097
|
});
|
|
933
1098
|
|
|
934
1099
|
// Return wrapper that binds sessionId to all operations
|
|
@@ -950,6 +1115,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
950
1115
|
return this.getSessionWrapper(sessionId);
|
|
951
1116
|
}
|
|
952
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
|
+
|
|
953
1146
|
/**
|
|
954
1147
|
* Internal helper to create ExecutionSession wrapper for a given sessionId
|
|
955
1148
|
* Used by both createSession and getSession
|
|
@@ -959,32 +1152,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
959
1152
|
id: sessionId,
|
|
960
1153
|
|
|
961
1154
|
// Command execution - delegate to internal session-aware methods
|
|
962
|
-
exec: (command, options) =>
|
|
963
|
-
|
|
1155
|
+
exec: (command, options) =>
|
|
1156
|
+
this.execWithSession(command, sessionId, options),
|
|
1157
|
+
execStream: (command, options) =>
|
|
1158
|
+
this.execStreamWithSession(command, sessionId, options),
|
|
964
1159
|
|
|
965
1160
|
// Process management
|
|
966
|
-
startProcess: (command, options) =>
|
|
1161
|
+
startProcess: (command, options) =>
|
|
1162
|
+
this.startProcess(command, options, sessionId),
|
|
967
1163
|
listProcesses: () => this.listProcesses(sessionId),
|
|
968
1164
|
getProcess: (id) => this.getProcess(id, sessionId),
|
|
969
1165
|
killProcess: (id, signal) => this.killProcess(id, signal),
|
|
970
1166
|
killAllProcesses: () => this.killAllProcesses(),
|
|
971
1167
|
cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
|
|
972
1168
|
getProcessLogs: (id) => this.getProcessLogs(id),
|
|
973
|
-
streamProcessLogs: (processId, options) =>
|
|
1169
|
+
streamProcessLogs: (processId, options) =>
|
|
1170
|
+
this.streamProcessLogs(processId, options),
|
|
974
1171
|
|
|
975
1172
|
// File operations - pass sessionId via options or parameter
|
|
976
|
-
writeFile: (path, content, options) =>
|
|
977
|
-
|
|
1173
|
+
writeFile: (path, content, options) =>
|
|
1174
|
+
this.writeFile(path, content, { ...options, sessionId }),
|
|
1175
|
+
readFile: (path, options) =>
|
|
1176
|
+
this.readFile(path, { ...options, sessionId }),
|
|
978
1177
|
readFileStream: (path) => this.readFileStream(path, { sessionId }),
|
|
979
1178
|
mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
|
|
980
1179
|
deleteFile: (path) => this.deleteFile(path, sessionId),
|
|
981
|
-
renameFile: (oldPath, newPath) =>
|
|
982
|
-
|
|
983
|
-
|
|
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),
|
|
984
1186
|
exists: (path) => this.exists(path, sessionId),
|
|
985
1187
|
|
|
986
1188
|
// Git operations
|
|
987
|
-
gitCheckout: (repoUrl, options) =>
|
|
1189
|
+
gitCheckout: (repoUrl, options) =>
|
|
1190
|
+
this.gitCheckout(repoUrl, { ...options, sessionId }),
|
|
988
1191
|
|
|
989
1192
|
// Environment management - needs special handling
|
|
990
1193
|
setEnvVars: async (envVars: Record<string, string>) => {
|
|
@@ -994,27 +1197,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
994
1197
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
995
1198
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
996
1199
|
|
|
997
|
-
const result = await this.client.commands.execute(
|
|
1200
|
+
const result = await this.client.commands.execute(
|
|
1201
|
+
exportCommand,
|
|
1202
|
+
sessionId
|
|
1203
|
+
);
|
|
998
1204
|
|
|
999
1205
|
if (result.exitCode !== 0) {
|
|
1000
|
-
throw new Error(
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
1208
|
+
);
|
|
1001
1209
|
}
|
|
1002
1210
|
}
|
|
1003
1211
|
} catch (error) {
|
|
1004
|
-
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
|
+
);
|
|
1005
1217
|
throw error;
|
|
1006
1218
|
}
|
|
1007
1219
|
},
|
|
1008
1220
|
|
|
1009
1221
|
// Code interpreter methods - delegate to sandbox's code interpreter
|
|
1010
|
-
createCodeContext: (options) =>
|
|
1222
|
+
createCodeContext: (options) =>
|
|
1223
|
+
this.codeInterpreter.createCodeContext(options),
|
|
1011
1224
|
runCode: async (code, options) => {
|
|
1012
1225
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
1013
1226
|
return execution.toJSON();
|
|
1014
1227
|
},
|
|
1015
|
-
runCodeStream: (code, options) =>
|
|
1228
|
+
runCodeStream: (code, options) =>
|
|
1229
|
+
this.codeInterpreter.runCodeStream(code, options),
|
|
1016
1230
|
listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
|
|
1017
|
-
deleteCodeContext: (contextId) =>
|
|
1231
|
+
deleteCodeContext: (contextId) =>
|
|
1232
|
+
this.codeInterpreter.deleteCodeContext(contextId)
|
|
1018
1233
|
};
|
|
1019
1234
|
}
|
|
1020
1235
|
|
|
@@ -1022,16 +1237,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
1022
1237
|
// Code interpreter methods - delegate to CodeInterpreter wrapper
|
|
1023
1238
|
// ============================================================================
|
|
1024
1239
|
|
|
1025
|
-
async createCodeContext(
|
|
1240
|
+
async createCodeContext(
|
|
1241
|
+
options?: CreateContextOptions
|
|
1242
|
+
): Promise<CodeContext> {
|
|
1026
1243
|
return this.codeInterpreter.createCodeContext(options);
|
|
1027
1244
|
}
|
|
1028
1245
|
|
|
1029
|
-
async runCode(
|
|
1246
|
+
async runCode(
|
|
1247
|
+
code: string,
|
|
1248
|
+
options?: RunCodeOptions
|
|
1249
|
+
): Promise<ExecutionResult> {
|
|
1030
1250
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
1031
1251
|
return execution.toJSON();
|
|
1032
1252
|
}
|
|
1033
1253
|
|
|
1034
|
-
async runCodeStream(
|
|
1254
|
+
async runCodeStream(
|
|
1255
|
+
code: string,
|
|
1256
|
+
options?: RunCodeOptions
|
|
1257
|
+
): Promise<ReadableStream> {
|
|
1035
1258
|
return this.codeInterpreter.runCodeStream(code, options);
|
|
1036
1259
|
}
|
|
1037
1260
|
|