@cloudflare/sandbox 0.0.0-fddccfd → 0.0.0-ff2fa91
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 +102 -15
- package/Dockerfile +84 -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 -64
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +51 -20
- package/src/clients/git-client.ts +3 -4
- package/src/clients/index.ts +12 -15
- 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 +34 -5
- 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 +34 -21
- package/src/sandbox.ts +443 -144
- package/src/security.ts +21 -6
- package/src/sse-parser.ts +4 -3
- package/src/version.ts +6 -0
- package/tests/base-client.test.ts +116 -80
- package/tests/command-client.test.ts +149 -112
- package/tests/file-client.test.ts +373 -185
- package/tests/file-stream.test.ts +24 -20
- package/tests/get-sandbox.test.ts +149 -0
- 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 +292 -0
- package/tests/sandbox.test.ts +303 -62
- package/tests/sse-parser.test.ts +17 -16
- package/tests/utility-client.test.ts +129 -56
- package/tests/version.test.ts +16 -0
- package/tsdown.config.ts +12 -0
- package/vitest.config.ts +6 -6
- package/dist/chunk-2P3MDMNJ.js +0 -2367
- package/dist/chunk-2P3MDMNJ.js.map +0 -1
- 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-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 -12
- package/dist/request-handler.js.map +0 -1
- package/dist/sandbox-CZTMzV2R.d.ts +0 -587
- package/dist/sandbox.d.ts +0 -4
- package/dist/sandbox.js +0 -12
- 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/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,
|
|
@@ -13,40 +13,65 @@ import type {
|
|
|
13
13
|
ProcessOptions,
|
|
14
14
|
ProcessStatus,
|
|
15
15
|
RunCodeOptions,
|
|
16
|
+
SandboxOptions,
|
|
16
17
|
SessionOptions,
|
|
17
18
|
StreamOptions
|
|
18
|
-
} from
|
|
19
|
-
import { createLogger, runWithLogger, TraceContext } from
|
|
20
|
-
import { type ExecuteResponse, SandboxClient } from
|
|
19
|
+
} from '@repo/shared';
|
|
20
|
+
import { createLogger, runWithLogger, TraceContext } from '@repo/shared';
|
|
21
|
+
import { type ExecuteResponse, SandboxClient } from './clients';
|
|
21
22
|
import type { ErrorResponse } from './errors';
|
|
22
23
|
import { CustomDomainRequiredError, ErrorCode } from './errors';
|
|
23
|
-
import { CodeInterpreter } from
|
|
24
|
-
import { isLocalhostPattern } from
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const stub = getContainer(ns, id);
|
|
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';
|
|
29
|
+
|
|
30
|
+
export function getSandbox(
|
|
31
|
+
ns: DurableObjectNamespace<Sandbox>,
|
|
32
|
+
id: string,
|
|
33
|
+
options?: SandboxOptions
|
|
34
|
+
): Sandbox {
|
|
35
|
+
const stub = getContainer(ns, id) as unknown as Sandbox;
|
|
36
36
|
|
|
37
37
|
// Store the name on first access
|
|
38
38
|
stub.setSandboxName?.(id);
|
|
39
39
|
|
|
40
|
-
if(options?.baseUrl) {
|
|
40
|
+
if (options?.baseUrl) {
|
|
41
41
|
stub.setBaseUrl(options.baseUrl);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
if (options?.sleepAfter !== undefined) {
|
|
45
|
+
stub.setSleepAfter(options.sleepAfter);
|
|
46
|
+
}
|
|
47
|
+
|
|
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
|
+
};
|
|
45
70
|
}
|
|
46
71
|
|
|
47
72
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
48
73
|
defaultPort = 3000; // Default port for the container's Bun server
|
|
49
|
-
sleepAfter =
|
|
74
|
+
sleepAfter: string | number = '10m'; // Sleep the sandbox if no requests are made in this timeframe
|
|
50
75
|
|
|
51
76
|
client: SandboxClient;
|
|
52
77
|
private codeInterpreter: CodeInterpreter;
|
|
@@ -56,14 +81,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
56
81
|
private defaultSession: string | null = null;
|
|
57
82
|
envVars: Record<string, string> = {};
|
|
58
83
|
private logger: ReturnType<typeof createLogger>;
|
|
84
|
+
private keepAliveEnabled: boolean = false;
|
|
59
85
|
|
|
60
|
-
constructor(ctx:
|
|
86
|
+
constructor(ctx: DurableObjectState<{}>, env: Env) {
|
|
61
87
|
super(ctx, env);
|
|
62
88
|
|
|
63
89
|
const envObj = env as any;
|
|
64
90
|
// Set sandbox environment variables from env object
|
|
65
91
|
const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const;
|
|
66
|
-
sandboxEnvKeys.forEach(key => {
|
|
92
|
+
sandboxEnvKeys.forEach((key) => {
|
|
67
93
|
if (envObj?.[key]) {
|
|
68
94
|
this.envVars[key] = envObj[key];
|
|
69
95
|
}
|
|
@@ -77,7 +103,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
77
103
|
this.client = new SandboxClient({
|
|
78
104
|
logger: this.logger,
|
|
79
105
|
port: 3000, // Control plane port
|
|
80
|
-
stub: this
|
|
106
|
+
stub: this
|
|
81
107
|
});
|
|
82
108
|
|
|
83
109
|
// Initialize code interpreter - pass 'this' after client is ready
|
|
@@ -86,9 +112,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
86
112
|
|
|
87
113
|
// Load the sandbox name, port tokens, and default session from storage on initialization
|
|
88
114
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
89
|
-
this.sandboxName =
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
{};
|
|
92
122
|
|
|
93
123
|
// Convert stored tokens back to Map
|
|
94
124
|
this.portTokens = new Map();
|
|
@@ -112,12 +142,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
112
142
|
this.baseUrl = baseUrl;
|
|
113
143
|
await this.ctx.storage.put('baseUrl', baseUrl);
|
|
114
144
|
} else {
|
|
115
|
-
if(this.baseUrl !== baseUrl) {
|
|
116
|
-
throw new Error(
|
|
145
|
+
if (this.baseUrl !== baseUrl) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'Base URL already set and different from one previously provided'
|
|
148
|
+
);
|
|
117
149
|
}
|
|
118
150
|
}
|
|
119
151
|
}
|
|
120
152
|
|
|
153
|
+
// RPC method to set the sleep timeout
|
|
154
|
+
async setSleepAfter(sleepAfter: string | number): Promise<void> {
|
|
155
|
+
this.sleepAfter = sleepAfter;
|
|
156
|
+
}
|
|
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
|
+
|
|
121
172
|
// RPC method to set environment variables
|
|
122
173
|
async setEnvVars(envVars: Record<string, string>): Promise<void> {
|
|
123
174
|
// Update local state for new sessions
|
|
@@ -130,10 +181,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
130
181
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
131
182
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
132
183
|
|
|
133
|
-
const result = await this.client.commands.execute(
|
|
184
|
+
const result = await this.client.commands.execute(
|
|
185
|
+
exportCommand,
|
|
186
|
+
this.defaultSession
|
|
187
|
+
);
|
|
134
188
|
|
|
135
189
|
if (result.exitCode !== 0) {
|
|
136
|
-
throw new Error(
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
192
|
+
);
|
|
137
193
|
}
|
|
138
194
|
}
|
|
139
195
|
}
|
|
@@ -149,6 +205,61 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
149
205
|
|
|
150
206
|
override onStart() {
|
|
151
207
|
this.logger.debug('Sandbox started');
|
|
208
|
+
|
|
209
|
+
// Check version compatibility asynchronously (don't block startup)
|
|
210
|
+
this.checkVersionCompatibility().catch((error) => {
|
|
211
|
+
this.logger.error(
|
|
212
|
+
'Version compatibility check failed',
|
|
213
|
+
error instanceof Error ? error : new Error(String(error))
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check if the container version matches the SDK version
|
|
220
|
+
* Logs a warning if there's a mismatch
|
|
221
|
+
*/
|
|
222
|
+
private async checkVersionCompatibility(): Promise<void> {
|
|
223
|
+
try {
|
|
224
|
+
// Get the SDK version (imported from version.ts)
|
|
225
|
+
const sdkVersion = SDK_VERSION;
|
|
226
|
+
|
|
227
|
+
// Get container version
|
|
228
|
+
const containerVersion = await this.client.utils.getVersion();
|
|
229
|
+
|
|
230
|
+
// If container version is unknown, it's likely an old container without the endpoint
|
|
231
|
+
if (containerVersion === 'unknown') {
|
|
232
|
+
this.logger.warn(
|
|
233
|
+
'Container version check: Container version could not be determined. ' +
|
|
234
|
+
'This may indicate an outdated container image. ' +
|
|
235
|
+
'Please update your container to match SDK version ' +
|
|
236
|
+
sdkVersion
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if versions match
|
|
242
|
+
if (containerVersion !== sdkVersion) {
|
|
243
|
+
const message =
|
|
244
|
+
`Version mismatch detected! SDK version (${sdkVersion}) does not match ` +
|
|
245
|
+
`container version (${containerVersion}). This may cause compatibility issues. ` +
|
|
246
|
+
`Please update your container image to version ${sdkVersion}`;
|
|
247
|
+
|
|
248
|
+
// Log warning - we can't reliably detect dev vs prod environment in Durable Objects
|
|
249
|
+
// so we always use warning level as requested by the user
|
|
250
|
+
this.logger.warn(message);
|
|
251
|
+
} else {
|
|
252
|
+
this.logger.debug('Version check passed', {
|
|
253
|
+
sdkVersion,
|
|
254
|
+
containerVersion
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// Don't fail the sandbox initialization if version check fails
|
|
259
|
+
this.logger.debug('Version compatibility check encountered an error', {
|
|
260
|
+
error: error instanceof Error ? error.message : String(error)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
152
263
|
}
|
|
153
264
|
|
|
154
265
|
override onStop() {
|
|
@@ -156,13 +267,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
156
267
|
}
|
|
157
268
|
|
|
158
269
|
override onError(error: unknown) {
|
|
159
|
-
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
|
+
}
|
|
160
291
|
}
|
|
161
292
|
|
|
162
293
|
// Override fetch to route internal container requests to appropriate ports
|
|
163
294
|
override async fetch(request: Request): Promise<Response> {
|
|
164
295
|
// Extract or generate trace ID from request
|
|
165
|
-
const traceId =
|
|
296
|
+
const traceId =
|
|
297
|
+
TraceContext.fromHeaders(request.headers) || TraceContext.generate();
|
|
166
298
|
|
|
167
299
|
// Create request-specific logger with trace ID
|
|
168
300
|
const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
|
|
@@ -177,7 +309,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
177
309
|
await this.ctx.storage.put('sandboxName', name);
|
|
178
310
|
}
|
|
179
311
|
|
|
180
|
-
//
|
|
312
|
+
// Detect WebSocket upgrade request (RFC 6455 compliant)
|
|
313
|
+
const upgradeHeader = request.headers.get('Upgrade');
|
|
314
|
+
const connectionHeader = request.headers.get('Connection');
|
|
315
|
+
const isWebSocket =
|
|
316
|
+
upgradeHeader?.toLowerCase() === 'websocket' &&
|
|
317
|
+
connectionHeader?.toLowerCase().includes('upgrade');
|
|
318
|
+
|
|
319
|
+
if (isWebSocket) {
|
|
320
|
+
// WebSocket path: Let parent Container class handle WebSocket proxying
|
|
321
|
+
// This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
|
|
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
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Non-WebSocket: Use existing port determination and HTTP routing logic
|
|
181
339
|
const port = this.determinePort(url);
|
|
182
340
|
|
|
183
341
|
// Route to the appropriate port
|
|
@@ -185,6 +343,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
185
343
|
});
|
|
186
344
|
}
|
|
187
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
|
+
|
|
188
351
|
private determinePort(url: URL): number {
|
|
189
352
|
// Extract port from proxy requests (e.g., /proxy/8080/*)
|
|
190
353
|
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
@@ -214,7 +377,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
214
377
|
await this.client.utils.createSession({
|
|
215
378
|
id: sessionId,
|
|
216
379
|
env: this.envVars || {},
|
|
217
|
-
cwd: '/workspace'
|
|
380
|
+
cwd: '/workspace'
|
|
218
381
|
});
|
|
219
382
|
|
|
220
383
|
this.defaultSession = sessionId;
|
|
@@ -223,8 +386,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
223
386
|
this.logger.debug('Default session initialized', { sessionId });
|
|
224
387
|
} catch (error: any) {
|
|
225
388
|
// If session already exists (e.g., after hot reload), reuse it
|
|
226
|
-
if (error?.message?.includes('already exists')
|
|
227
|
-
this.logger.debug('Reusing existing session after reload', {
|
|
389
|
+
if (error?.message?.includes('already exists')) {
|
|
390
|
+
this.logger.debug('Reusing existing session after reload', {
|
|
391
|
+
sessionId
|
|
392
|
+
});
|
|
228
393
|
this.defaultSession = sessionId;
|
|
229
394
|
// Persist to storage in case it wasn't saved before
|
|
230
395
|
await this.ctx.storage.put('defaultSession', sessionId);
|
|
@@ -256,7 +421,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
256
421
|
const startTime = Date.now();
|
|
257
422
|
const timestamp = new Date().toISOString();
|
|
258
423
|
|
|
259
|
-
// Handle timeout
|
|
260
424
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
261
425
|
|
|
262
426
|
try {
|
|
@@ -269,13 +433,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
269
433
|
|
|
270
434
|
if (options?.stream && options?.onOutput) {
|
|
271
435
|
// Streaming with callbacks - we need to collect the final result
|
|
272
|
-
result = await this.executeWithStreaming(
|
|
436
|
+
result = await this.executeWithStreaming(
|
|
437
|
+
command,
|
|
438
|
+
sessionId,
|
|
439
|
+
options,
|
|
440
|
+
startTime,
|
|
441
|
+
timestamp
|
|
442
|
+
);
|
|
273
443
|
} else {
|
|
274
444
|
// Regular execution with session
|
|
275
445
|
const response = await this.client.commands.execute(command, sessionId);
|
|
276
446
|
|
|
277
447
|
const duration = Date.now() - startTime;
|
|
278
|
-
result = this.mapExecuteResponseToExecResult(
|
|
448
|
+
result = this.mapExecuteResponseToExecResult(
|
|
449
|
+
response,
|
|
450
|
+
duration,
|
|
451
|
+
sessionId
|
|
452
|
+
);
|
|
279
453
|
}
|
|
280
454
|
|
|
281
455
|
// Call completion callback if provided
|
|
@@ -307,7 +481,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
307
481
|
let stderr = '';
|
|
308
482
|
|
|
309
483
|
try {
|
|
310
|
-
const stream = await this.client.commands.executeStream(
|
|
484
|
+
const stream = await this.client.commands.executeStream(
|
|
485
|
+
command,
|
|
486
|
+
sessionId
|
|
487
|
+
);
|
|
311
488
|
|
|
312
489
|
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
313
490
|
// Check for cancellation
|
|
@@ -352,7 +529,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
352
529
|
|
|
353
530
|
// If we get here without a complete event, something went wrong
|
|
354
531
|
throw new Error('Stream ended without completion event');
|
|
355
|
-
|
|
356
532
|
} catch (error) {
|
|
357
533
|
if (options.signal?.aborted) {
|
|
358
534
|
throw new Error('Operation was aborted');
|
|
@@ -400,8 +576,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
400
576
|
pid: data.pid,
|
|
401
577
|
command: data.command,
|
|
402
578
|
status: data.status,
|
|
403
|
-
startTime:
|
|
404
|
-
|
|
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,
|
|
405
588
|
exitCode: data.exitCode,
|
|
406
589
|
sessionId,
|
|
407
590
|
|
|
@@ -421,25 +604,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
421
604
|
};
|
|
422
605
|
}
|
|
423
606
|
|
|
424
|
-
|
|
425
607
|
// Background process management
|
|
426
|
-
async startProcess(
|
|
608
|
+
async startProcess(
|
|
609
|
+
command: string,
|
|
610
|
+
options?: ProcessOptions,
|
|
611
|
+
sessionId?: string
|
|
612
|
+
): Promise<Process> {
|
|
427
613
|
// Use the new HttpClient method to start the process
|
|
428
614
|
try {
|
|
429
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
430
|
-
const response = await this.client.processes.startProcess(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
+
);
|
|
443
636
|
|
|
444
637
|
// Call onStart callback if provided
|
|
445
638
|
if (options?.onStart) {
|
|
@@ -447,7 +640,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
447
640
|
}
|
|
448
641
|
|
|
449
642
|
return processObj;
|
|
450
|
-
|
|
451
643
|
} catch (error) {
|
|
452
644
|
if (options?.onError && error instanceof Error) {
|
|
453
645
|
options.onError(error);
|
|
@@ -458,42 +650,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
458
650
|
}
|
|
459
651
|
|
|
460
652
|
async listProcesses(sessionId?: string): Promise<Process[]> {
|
|
461
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
653
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
462
654
|
const response = await this.client.processes.listProcesses();
|
|
463
655
|
|
|
464
|
-
return response.processes.map(processData =>
|
|
465
|
-
this.createProcessFromDTO(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
+
)
|
|
474
669
|
);
|
|
475
670
|
}
|
|
476
671
|
|
|
477
672
|
async getProcess(id: string, sessionId?: string): Promise<Process | null> {
|
|
478
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
673
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
479
674
|
const response = await this.client.processes.getProcess(id);
|
|
480
675
|
if (!response.process) {
|
|
481
676
|
return null;
|
|
482
677
|
}
|
|
483
678
|
|
|
484
679
|
const processData = response.process;
|
|
485
|
-
return this.createProcessFromDTO(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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> {
|
|
497
699
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
498
700
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
499
701
|
await this.client.processes.killProcess(id);
|
|
@@ -511,7 +713,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
511
713
|
return 0;
|
|
512
714
|
}
|
|
513
715
|
|
|
514
|
-
async getProcessLogs(
|
|
716
|
+
async getProcessLogs(
|
|
717
|
+
id: string,
|
|
718
|
+
sessionId?: string
|
|
719
|
+
): Promise<{ stdout: string; stderr: string; processId: string }> {
|
|
515
720
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
516
721
|
const response = await this.client.processes.getProcessLogs(id);
|
|
517
722
|
return {
|
|
@@ -521,9 +726,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
521
726
|
};
|
|
522
727
|
}
|
|
523
728
|
|
|
524
|
-
|
|
525
729
|
// Streaming methods - return ReadableStream for RPC compatibility
|
|
526
|
-
async execStream(
|
|
730
|
+
async execStream(
|
|
731
|
+
command: string,
|
|
732
|
+
options?: StreamOptions
|
|
733
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
527
734
|
// Check for cancellation
|
|
528
735
|
if (options?.signal?.aborted) {
|
|
529
736
|
throw new Error('Operation was aborted');
|
|
@@ -537,7 +744,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
537
744
|
/**
|
|
538
745
|
* Internal session-aware execStream implementation
|
|
539
746
|
*/
|
|
540
|
-
private async execStreamWithSession(
|
|
747
|
+
private async execStreamWithSession(
|
|
748
|
+
command: string,
|
|
749
|
+
sessionId: string,
|
|
750
|
+
options?: StreamOptions
|
|
751
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
541
752
|
// Check for cancellation
|
|
542
753
|
if (options?.signal?.aborted) {
|
|
543
754
|
throw new Error('Operation was aborted');
|
|
@@ -546,7 +757,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
546
757
|
return this.client.commands.executeStream(command, sessionId);
|
|
547
758
|
}
|
|
548
759
|
|
|
549
|
-
|
|
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>> {
|
|
550
767
|
// Check for cancellation
|
|
551
768
|
if (options?.signal?.aborted) {
|
|
552
769
|
throw new Error('Operation was aborted');
|
|
@@ -559,7 +776,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
559
776
|
repoUrl: string,
|
|
560
777
|
options: { branch?: string; targetDir?: string; sessionId?: string }
|
|
561
778
|
) {
|
|
562
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
779
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
563
780
|
return this.client.git.checkout(repoUrl, session, {
|
|
564
781
|
branch: options.branch,
|
|
565
782
|
targetDir: options.targetDir
|
|
@@ -570,8 +787,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
570
787
|
path: string,
|
|
571
788
|
options: { recursive?: boolean; sessionId?: string } = {}
|
|
572
789
|
) {
|
|
573
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
574
|
-
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
|
+
});
|
|
575
794
|
}
|
|
576
795
|
|
|
577
796
|
async writeFile(
|
|
@@ -579,21 +798,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
579
798
|
content: string,
|
|
580
799
|
options: { encoding?: string; sessionId?: string } = {}
|
|
581
800
|
) {
|
|
582
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
583
|
-
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
|
+
});
|
|
584
805
|
}
|
|
585
806
|
|
|
586
807
|
async deleteFile(path: string, sessionId?: string) {
|
|
587
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
808
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
588
809
|
return this.client.files.deleteFile(path, session);
|
|
589
810
|
}
|
|
590
811
|
|
|
591
|
-
async renameFile(
|
|
592
|
-
|
|
593
|
-
newPath: string,
|
|
594
|
-
sessionId?: string
|
|
595
|
-
) {
|
|
596
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
812
|
+
async renameFile(oldPath: string, newPath: string, sessionId?: string) {
|
|
813
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
597
814
|
return this.client.files.renameFile(oldPath, newPath, session);
|
|
598
815
|
}
|
|
599
816
|
|
|
@@ -602,7 +819,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
602
819
|
destinationPath: string,
|
|
603
820
|
sessionId?: string
|
|
604
821
|
) {
|
|
605
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
822
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
606
823
|
return this.client.files.moveFile(sourcePath, destinationPath, session);
|
|
607
824
|
}
|
|
608
825
|
|
|
@@ -610,8 +827,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
610
827
|
path: string,
|
|
611
828
|
options: { encoding?: string; sessionId?: string } = {}
|
|
612
829
|
) {
|
|
613
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
614
|
-
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
|
+
});
|
|
615
834
|
}
|
|
616
835
|
|
|
617
836
|
/**
|
|
@@ -624,7 +843,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
624
843
|
path: string,
|
|
625
844
|
options: { sessionId?: string } = {}
|
|
626
845
|
): Promise<ReadableStream<Uint8Array>> {
|
|
627
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
846
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
628
847
|
return this.client.files.readFileStream(path, session);
|
|
629
848
|
}
|
|
630
849
|
|
|
@@ -636,6 +855,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
636
855
|
return this.client.files.listFiles(path, session, options);
|
|
637
856
|
}
|
|
638
857
|
|
|
858
|
+
async exists(path: string, sessionId?: string) {
|
|
859
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
860
|
+
return this.client.files.exists(path, session);
|
|
861
|
+
}
|
|
862
|
+
|
|
639
863
|
async exposePort(port: number, options: { name?: string; hostname: string }) {
|
|
640
864
|
// Check if hostname is workers.dev domain (doesn't support wildcard subdomains)
|
|
641
865
|
if (options.hostname.endsWith('.workers.dev')) {
|
|
@@ -654,7 +878,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
654
878
|
|
|
655
879
|
// We need the sandbox name to construct preview URLs
|
|
656
880
|
if (!this.sandboxName) {
|
|
657
|
-
throw new Error(
|
|
881
|
+
throw new Error(
|
|
882
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
883
|
+
);
|
|
658
884
|
}
|
|
659
885
|
|
|
660
886
|
// Generate and store token for this port
|
|
@@ -662,18 +888,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
662
888
|
this.portTokens.set(port, token);
|
|
663
889
|
await this.persistPortTokens();
|
|
664
890
|
|
|
665
|
-
const url = this.constructPreviewUrl(
|
|
891
|
+
const url = this.constructPreviewUrl(
|
|
892
|
+
port,
|
|
893
|
+
this.sandboxName,
|
|
894
|
+
options.hostname,
|
|
895
|
+
token
|
|
896
|
+
);
|
|
666
897
|
|
|
667
898
|
return {
|
|
668
899
|
url,
|
|
669
900
|
port,
|
|
670
|
-
name: options?.name
|
|
901
|
+
name: options?.name
|
|
671
902
|
};
|
|
672
903
|
}
|
|
673
904
|
|
|
674
905
|
async unexposePort(port: number) {
|
|
675
906
|
if (!validatePort(port)) {
|
|
676
|
-
throw new SecurityError(
|
|
907
|
+
throw new SecurityError(
|
|
908
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
909
|
+
);
|
|
677
910
|
}
|
|
678
911
|
|
|
679
912
|
const sessionId = await this.ensureDefaultSession();
|
|
@@ -692,32 +925,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
692
925
|
|
|
693
926
|
// We need the sandbox name to construct preview URLs
|
|
694
927
|
if (!this.sandboxName) {
|
|
695
|
-
throw new Error(
|
|
928
|
+
throw new Error(
|
|
929
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
930
|
+
);
|
|
696
931
|
}
|
|
697
932
|
|
|
698
|
-
return response.ports.map(port => {
|
|
933
|
+
return response.ports.map((port) => {
|
|
699
934
|
// Get token for this port - must exist for all exposed ports
|
|
700
935
|
const token = this.portTokens.get(port.port);
|
|
701
936
|
if (!token) {
|
|
702
|
-
throw new Error(
|
|
937
|
+
throw new Error(
|
|
938
|
+
`Port ${port.port} is exposed but has no token. This should not happen.`
|
|
939
|
+
);
|
|
703
940
|
}
|
|
704
941
|
|
|
705
942
|
return {
|
|
706
|
-
url: this.constructPreviewUrl(
|
|
943
|
+
url: this.constructPreviewUrl(
|
|
944
|
+
port.port,
|
|
945
|
+
this.sandboxName!,
|
|
946
|
+
hostname,
|
|
947
|
+
token
|
|
948
|
+
),
|
|
707
949
|
port: port.port,
|
|
708
|
-
status: port.status
|
|
950
|
+
status: port.status
|
|
709
951
|
};
|
|
710
952
|
});
|
|
711
953
|
}
|
|
712
954
|
|
|
713
|
-
|
|
714
955
|
async isPortExposed(port: number): Promise<boolean> {
|
|
715
956
|
try {
|
|
716
957
|
const sessionId = await this.ensureDefaultSession();
|
|
717
958
|
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
718
|
-
return response.ports.some(exposedPort => exposedPort.port === port);
|
|
959
|
+
return response.ports.some((exposedPort) => exposedPort.port === port);
|
|
719
960
|
} catch (error) {
|
|
720
|
-
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
|
+
);
|
|
721
966
|
return false;
|
|
722
967
|
}
|
|
723
968
|
}
|
|
@@ -733,7 +978,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
733
978
|
const storedToken = this.portTokens.get(port);
|
|
734
979
|
if (!storedToken) {
|
|
735
980
|
// This should not happen - all exposed ports must have tokens
|
|
736
|
-
this.logger.error(
|
|
981
|
+
this.logger.error(
|
|
982
|
+
'Port is exposed but has no token - bug detected',
|
|
983
|
+
undefined,
|
|
984
|
+
{ port }
|
|
985
|
+
);
|
|
737
986
|
return false;
|
|
738
987
|
}
|
|
739
988
|
|
|
@@ -749,7 +998,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
749
998
|
|
|
750
999
|
// Convert to base64url format (URL-safe, no padding, lowercase)
|
|
751
1000
|
const base64 = btoa(String.fromCharCode(...array));
|
|
752
|
-
return base64
|
|
1001
|
+
return base64
|
|
1002
|
+
.replace(/\+/g, '-')
|
|
1003
|
+
.replace(/\//g, '_')
|
|
1004
|
+
.replace(/=/g, '')
|
|
1005
|
+
.toLowerCase();
|
|
753
1006
|
}
|
|
754
1007
|
|
|
755
1008
|
private async persistPortTokens(): Promise<void> {
|
|
@@ -761,9 +1014,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
761
1014
|
await this.ctx.storage.put('portTokens', tokensObj);
|
|
762
1015
|
}
|
|
763
1016
|
|
|
764
|
-
private constructPreviewUrl(
|
|
1017
|
+
private constructPreviewUrl(
|
|
1018
|
+
port: number,
|
|
1019
|
+
sandboxId: string,
|
|
1020
|
+
hostname: string,
|
|
1021
|
+
token: string
|
|
1022
|
+
): string {
|
|
765
1023
|
if (!validatePort(port)) {
|
|
766
|
-
throw new SecurityError(
|
|
1024
|
+
throw new SecurityError(
|
|
1025
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
1026
|
+
);
|
|
767
1027
|
}
|
|
768
1028
|
|
|
769
1029
|
// Validate sandbox ID (will throw SecurityError if invalid)
|
|
@@ -785,14 +1045,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
785
1045
|
|
|
786
1046
|
return baseUrl.toString();
|
|
787
1047
|
} catch (error) {
|
|
788
|
-
throw new SecurityError(
|
|
1048
|
+
throw new SecurityError(
|
|
1049
|
+
`Failed to construct preview URL: ${
|
|
1050
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1051
|
+
}`
|
|
1052
|
+
);
|
|
789
1053
|
}
|
|
790
1054
|
}
|
|
791
1055
|
|
|
792
1056
|
// Production subdomain logic - enforce HTTPS
|
|
793
1057
|
try {
|
|
794
1058
|
// Always use HTTPS for production (non-localhost)
|
|
795
|
-
const protocol =
|
|
1059
|
+
const protocol = 'https';
|
|
796
1060
|
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
797
1061
|
|
|
798
1062
|
// Construct subdomain safely with mandatory token
|
|
@@ -801,7 +1065,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
801
1065
|
|
|
802
1066
|
return baseUrl.toString();
|
|
803
1067
|
} catch (error) {
|
|
804
|
-
throw new SecurityError(
|
|
1068
|
+
throw new SecurityError(
|
|
1069
|
+
`Failed to construct preview URL: ${
|
|
1070
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1071
|
+
}`
|
|
1072
|
+
);
|
|
805
1073
|
}
|
|
806
1074
|
}
|
|
807
1075
|
|
|
@@ -820,7 +1088,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
820
1088
|
await this.client.utils.createSession({
|
|
821
1089
|
id: sessionId,
|
|
822
1090
|
env: options?.env,
|
|
823
|
-
cwd: options?.cwd
|
|
1091
|
+
cwd: options?.cwd
|
|
824
1092
|
});
|
|
825
1093
|
|
|
826
1094
|
// Return wrapper that binds sessionId to all operations
|
|
@@ -851,31 +1119,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
851
1119
|
id: sessionId,
|
|
852
1120
|
|
|
853
1121
|
// Command execution - delegate to internal session-aware methods
|
|
854
|
-
exec: (command, options) =>
|
|
855
|
-
|
|
1122
|
+
exec: (command, options) =>
|
|
1123
|
+
this.execWithSession(command, sessionId, options),
|
|
1124
|
+
execStream: (command, options) =>
|
|
1125
|
+
this.execStreamWithSession(command, sessionId, options),
|
|
856
1126
|
|
|
857
1127
|
// Process management
|
|
858
|
-
startProcess: (command, options) =>
|
|
1128
|
+
startProcess: (command, options) =>
|
|
1129
|
+
this.startProcess(command, options, sessionId),
|
|
859
1130
|
listProcesses: () => this.listProcesses(sessionId),
|
|
860
1131
|
getProcess: (id) => this.getProcess(id, sessionId),
|
|
861
1132
|
killProcess: (id, signal) => this.killProcess(id, signal),
|
|
862
1133
|
killAllProcesses: () => this.killAllProcesses(),
|
|
863
1134
|
cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
|
|
864
1135
|
getProcessLogs: (id) => this.getProcessLogs(id),
|
|
865
|
-
streamProcessLogs: (processId, options) =>
|
|
1136
|
+
streamProcessLogs: (processId, options) =>
|
|
1137
|
+
this.streamProcessLogs(processId, options),
|
|
866
1138
|
|
|
867
1139
|
// File operations - pass sessionId via options or parameter
|
|
868
|
-
writeFile: (path, content, options) =>
|
|
869
|
-
|
|
1140
|
+
writeFile: (path, content, options) =>
|
|
1141
|
+
this.writeFile(path, content, { ...options, sessionId }),
|
|
1142
|
+
readFile: (path, options) =>
|
|
1143
|
+
this.readFile(path, { ...options, sessionId }),
|
|
870
1144
|
readFileStream: (path) => this.readFileStream(path, { sessionId }),
|
|
871
1145
|
mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
|
|
872
1146
|
deleteFile: (path) => this.deleteFile(path, sessionId),
|
|
873
|
-
renameFile: (oldPath, newPath) =>
|
|
874
|
-
|
|
875
|
-
|
|
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),
|
|
1153
|
+
exists: (path) => this.exists(path, sessionId),
|
|
876
1154
|
|
|
877
1155
|
// Git operations
|
|
878
|
-
gitCheckout: (repoUrl, options) =>
|
|
1156
|
+
gitCheckout: (repoUrl, options) =>
|
|
1157
|
+
this.gitCheckout(repoUrl, { ...options, sessionId }),
|
|
879
1158
|
|
|
880
1159
|
// Environment management - needs special handling
|
|
881
1160
|
setEnvVars: async (envVars: Record<string, string>) => {
|
|
@@ -885,27 +1164,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
885
1164
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
886
1165
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
887
1166
|
|
|
888
|
-
const result = await this.client.commands.execute(
|
|
1167
|
+
const result = await this.client.commands.execute(
|
|
1168
|
+
exportCommand,
|
|
1169
|
+
sessionId
|
|
1170
|
+
);
|
|
889
1171
|
|
|
890
1172
|
if (result.exitCode !== 0) {
|
|
891
|
-
throw new Error(
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
1175
|
+
);
|
|
892
1176
|
}
|
|
893
1177
|
}
|
|
894
1178
|
} catch (error) {
|
|
895
|
-
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
|
+
);
|
|
896
1184
|
throw error;
|
|
897
1185
|
}
|
|
898
1186
|
},
|
|
899
1187
|
|
|
900
1188
|
// Code interpreter methods - delegate to sandbox's code interpreter
|
|
901
|
-
createCodeContext: (options) =>
|
|
1189
|
+
createCodeContext: (options) =>
|
|
1190
|
+
this.codeInterpreter.createCodeContext(options),
|
|
902
1191
|
runCode: async (code, options) => {
|
|
903
1192
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
904
1193
|
return execution.toJSON();
|
|
905
1194
|
},
|
|
906
|
-
runCodeStream: (code, options) =>
|
|
1195
|
+
runCodeStream: (code, options) =>
|
|
1196
|
+
this.codeInterpreter.runCodeStream(code, options),
|
|
907
1197
|
listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
|
|
908
|
-
deleteCodeContext: (contextId) =>
|
|
1198
|
+
deleteCodeContext: (contextId) =>
|
|
1199
|
+
this.codeInterpreter.deleteCodeContext(contextId)
|
|
909
1200
|
};
|
|
910
1201
|
}
|
|
911
1202
|
|
|
@@ -913,16 +1204,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
913
1204
|
// Code interpreter methods - delegate to CodeInterpreter wrapper
|
|
914
1205
|
// ============================================================================
|
|
915
1206
|
|
|
916
|
-
async createCodeContext(
|
|
1207
|
+
async createCodeContext(
|
|
1208
|
+
options?: CreateContextOptions
|
|
1209
|
+
): Promise<CodeContext> {
|
|
917
1210
|
return this.codeInterpreter.createCodeContext(options);
|
|
918
1211
|
}
|
|
919
1212
|
|
|
920
|
-
async runCode(
|
|
1213
|
+
async runCode(
|
|
1214
|
+
code: string,
|
|
1215
|
+
options?: RunCodeOptions
|
|
1216
|
+
): Promise<ExecutionResult> {
|
|
921
1217
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
922
1218
|
return execution.toJSON();
|
|
923
1219
|
}
|
|
924
1220
|
|
|
925
|
-
async runCodeStream(
|
|
1221
|
+
async runCodeStream(
|
|
1222
|
+
code: string,
|
|
1223
|
+
options?: RunCodeOptions
|
|
1224
|
+
): Promise<ReadableStream> {
|
|
926
1225
|
return this.codeInterpreter.runCodeStream(code, options);
|
|
927
1226
|
}
|
|
928
1227
|
|