@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/dist/chunk-YE265ASX.js
DELETED
|
@@ -1,2484 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CodeInterpreter,
|
|
3
|
-
ResultImpl,
|
|
4
|
-
TraceContext,
|
|
5
|
-
createLogger,
|
|
6
|
-
createNoOpLogger,
|
|
7
|
-
runWithLogger
|
|
8
|
-
} from "./chunk-JXZMAU2C.js";
|
|
9
|
-
import {
|
|
10
|
-
SecurityError,
|
|
11
|
-
sanitizeSandboxId,
|
|
12
|
-
validatePort
|
|
13
|
-
} from "./chunk-Z532A7QC.js";
|
|
14
|
-
import {
|
|
15
|
-
parseSSEStream
|
|
16
|
-
} from "./chunk-EKSWCBCA.js";
|
|
17
|
-
import {
|
|
18
|
-
SDK_VERSION
|
|
19
|
-
} from "./chunk-UJ3TV4M6.js";
|
|
20
|
-
|
|
21
|
-
// src/request-handler.ts
|
|
22
|
-
import { switchPort } from "@cloudflare/containers";
|
|
23
|
-
|
|
24
|
-
// src/sandbox.ts
|
|
25
|
-
import { Container, getContainer } from "@cloudflare/containers";
|
|
26
|
-
|
|
27
|
-
// ../shared/dist/errors/codes.js
|
|
28
|
-
var ErrorCode = {
|
|
29
|
-
// File System Errors (404)
|
|
30
|
-
FILE_NOT_FOUND: "FILE_NOT_FOUND",
|
|
31
|
-
// Permission Errors (403)
|
|
32
|
-
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
33
|
-
// File System Errors (409)
|
|
34
|
-
FILE_EXISTS: "FILE_EXISTS",
|
|
35
|
-
// File System Errors (400)
|
|
36
|
-
IS_DIRECTORY: "IS_DIRECTORY",
|
|
37
|
-
NOT_DIRECTORY: "NOT_DIRECTORY",
|
|
38
|
-
// File System Errors (500)
|
|
39
|
-
NO_SPACE: "NO_SPACE",
|
|
40
|
-
TOO_MANY_FILES: "TOO_MANY_FILES",
|
|
41
|
-
RESOURCE_BUSY: "RESOURCE_BUSY",
|
|
42
|
-
READ_ONLY: "READ_ONLY",
|
|
43
|
-
NAME_TOO_LONG: "NAME_TOO_LONG",
|
|
44
|
-
TOO_MANY_LINKS: "TOO_MANY_LINKS",
|
|
45
|
-
FILESYSTEM_ERROR: "FILESYSTEM_ERROR",
|
|
46
|
-
// Command Errors (404)
|
|
47
|
-
COMMAND_NOT_FOUND: "COMMAND_NOT_FOUND",
|
|
48
|
-
// Command Errors (403/400)
|
|
49
|
-
COMMAND_PERMISSION_DENIED: "COMMAND_PERMISSION_DENIED",
|
|
50
|
-
INVALID_COMMAND: "INVALID_COMMAND",
|
|
51
|
-
// Command Errors (500)
|
|
52
|
-
COMMAND_EXECUTION_ERROR: "COMMAND_EXECUTION_ERROR",
|
|
53
|
-
STREAM_START_ERROR: "STREAM_START_ERROR",
|
|
54
|
-
// Process Errors (404)
|
|
55
|
-
PROCESS_NOT_FOUND: "PROCESS_NOT_FOUND",
|
|
56
|
-
// Process Errors (403/500)
|
|
57
|
-
PROCESS_PERMISSION_DENIED: "PROCESS_PERMISSION_DENIED",
|
|
58
|
-
PROCESS_ERROR: "PROCESS_ERROR",
|
|
59
|
-
// Port Errors (409)
|
|
60
|
-
PORT_ALREADY_EXPOSED: "PORT_ALREADY_EXPOSED",
|
|
61
|
-
PORT_IN_USE: "PORT_IN_USE",
|
|
62
|
-
// Port Errors (404)
|
|
63
|
-
PORT_NOT_EXPOSED: "PORT_NOT_EXPOSED",
|
|
64
|
-
// Port Errors (400)
|
|
65
|
-
INVALID_PORT_NUMBER: "INVALID_PORT_NUMBER",
|
|
66
|
-
INVALID_PORT: "INVALID_PORT",
|
|
67
|
-
// Port Errors (502/500)
|
|
68
|
-
SERVICE_NOT_RESPONDING: "SERVICE_NOT_RESPONDING",
|
|
69
|
-
PORT_OPERATION_ERROR: "PORT_OPERATION_ERROR",
|
|
70
|
-
// Port Errors (400)
|
|
71
|
-
CUSTOM_DOMAIN_REQUIRED: "CUSTOM_DOMAIN_REQUIRED",
|
|
72
|
-
// Git Errors (404)
|
|
73
|
-
GIT_REPOSITORY_NOT_FOUND: "GIT_REPOSITORY_NOT_FOUND",
|
|
74
|
-
GIT_BRANCH_NOT_FOUND: "GIT_BRANCH_NOT_FOUND",
|
|
75
|
-
// Git Errors (401)
|
|
76
|
-
GIT_AUTH_FAILED: "GIT_AUTH_FAILED",
|
|
77
|
-
// Git Errors (502)
|
|
78
|
-
GIT_NETWORK_ERROR: "GIT_NETWORK_ERROR",
|
|
79
|
-
// Git Errors (400)
|
|
80
|
-
INVALID_GIT_URL: "INVALID_GIT_URL",
|
|
81
|
-
// Git Errors (500)
|
|
82
|
-
GIT_CLONE_FAILED: "GIT_CLONE_FAILED",
|
|
83
|
-
GIT_CHECKOUT_FAILED: "GIT_CHECKOUT_FAILED",
|
|
84
|
-
GIT_OPERATION_FAILED: "GIT_OPERATION_FAILED",
|
|
85
|
-
// Code Interpreter Errors (503)
|
|
86
|
-
INTERPRETER_NOT_READY: "INTERPRETER_NOT_READY",
|
|
87
|
-
// Code Interpreter Errors (404)
|
|
88
|
-
CONTEXT_NOT_FOUND: "CONTEXT_NOT_FOUND",
|
|
89
|
-
// Code Interpreter Errors (500)
|
|
90
|
-
CODE_EXECUTION_ERROR: "CODE_EXECUTION_ERROR",
|
|
91
|
-
// Validation Errors (400)
|
|
92
|
-
VALIDATION_FAILED: "VALIDATION_FAILED",
|
|
93
|
-
// Generic Errors (400/500)
|
|
94
|
-
INVALID_JSON_RESPONSE: "INVALID_JSON_RESPONSE",
|
|
95
|
-
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
96
|
-
INTERNAL_ERROR: "INTERNAL_ERROR"
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// ../shared/dist/errors/status-map.js
|
|
100
|
-
var ERROR_STATUS_MAP = {
|
|
101
|
-
// 404 Not Found
|
|
102
|
-
[ErrorCode.FILE_NOT_FOUND]: 404,
|
|
103
|
-
[ErrorCode.COMMAND_NOT_FOUND]: 404,
|
|
104
|
-
[ErrorCode.PROCESS_NOT_FOUND]: 404,
|
|
105
|
-
[ErrorCode.PORT_NOT_EXPOSED]: 404,
|
|
106
|
-
[ErrorCode.GIT_REPOSITORY_NOT_FOUND]: 404,
|
|
107
|
-
[ErrorCode.GIT_BRANCH_NOT_FOUND]: 404,
|
|
108
|
-
[ErrorCode.CONTEXT_NOT_FOUND]: 404,
|
|
109
|
-
// 400 Bad Request
|
|
110
|
-
[ErrorCode.IS_DIRECTORY]: 400,
|
|
111
|
-
[ErrorCode.NOT_DIRECTORY]: 400,
|
|
112
|
-
[ErrorCode.INVALID_COMMAND]: 400,
|
|
113
|
-
[ErrorCode.INVALID_PORT_NUMBER]: 400,
|
|
114
|
-
[ErrorCode.INVALID_PORT]: 400,
|
|
115
|
-
[ErrorCode.INVALID_GIT_URL]: 400,
|
|
116
|
-
[ErrorCode.CUSTOM_DOMAIN_REQUIRED]: 400,
|
|
117
|
-
[ErrorCode.INVALID_JSON_RESPONSE]: 400,
|
|
118
|
-
[ErrorCode.NAME_TOO_LONG]: 400,
|
|
119
|
-
[ErrorCode.VALIDATION_FAILED]: 400,
|
|
120
|
-
// 401 Unauthorized
|
|
121
|
-
[ErrorCode.GIT_AUTH_FAILED]: 401,
|
|
122
|
-
// 403 Forbidden
|
|
123
|
-
[ErrorCode.PERMISSION_DENIED]: 403,
|
|
124
|
-
[ErrorCode.COMMAND_PERMISSION_DENIED]: 403,
|
|
125
|
-
[ErrorCode.PROCESS_PERMISSION_DENIED]: 403,
|
|
126
|
-
[ErrorCode.READ_ONLY]: 403,
|
|
127
|
-
// 409 Conflict
|
|
128
|
-
[ErrorCode.FILE_EXISTS]: 409,
|
|
129
|
-
[ErrorCode.PORT_ALREADY_EXPOSED]: 409,
|
|
130
|
-
[ErrorCode.PORT_IN_USE]: 409,
|
|
131
|
-
[ErrorCode.RESOURCE_BUSY]: 409,
|
|
132
|
-
// 502 Bad Gateway
|
|
133
|
-
[ErrorCode.SERVICE_NOT_RESPONDING]: 502,
|
|
134
|
-
[ErrorCode.GIT_NETWORK_ERROR]: 502,
|
|
135
|
-
// 503 Service Unavailable
|
|
136
|
-
[ErrorCode.INTERPRETER_NOT_READY]: 503,
|
|
137
|
-
// 500 Internal Server Error
|
|
138
|
-
[ErrorCode.NO_SPACE]: 500,
|
|
139
|
-
[ErrorCode.TOO_MANY_FILES]: 500,
|
|
140
|
-
[ErrorCode.TOO_MANY_LINKS]: 500,
|
|
141
|
-
[ErrorCode.FILESYSTEM_ERROR]: 500,
|
|
142
|
-
[ErrorCode.COMMAND_EXECUTION_ERROR]: 500,
|
|
143
|
-
[ErrorCode.STREAM_START_ERROR]: 500,
|
|
144
|
-
[ErrorCode.PROCESS_ERROR]: 500,
|
|
145
|
-
[ErrorCode.PORT_OPERATION_ERROR]: 500,
|
|
146
|
-
[ErrorCode.GIT_CLONE_FAILED]: 500,
|
|
147
|
-
[ErrorCode.GIT_CHECKOUT_FAILED]: 500,
|
|
148
|
-
[ErrorCode.GIT_OPERATION_FAILED]: 500,
|
|
149
|
-
[ErrorCode.CODE_EXECUTION_ERROR]: 500,
|
|
150
|
-
[ErrorCode.UNKNOWN_ERROR]: 500,
|
|
151
|
-
[ErrorCode.INTERNAL_ERROR]: 500
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// src/errors/classes.ts
|
|
155
|
-
var SandboxError = class extends Error {
|
|
156
|
-
constructor(errorResponse) {
|
|
157
|
-
super(errorResponse.message);
|
|
158
|
-
this.errorResponse = errorResponse;
|
|
159
|
-
this.name = "SandboxError";
|
|
160
|
-
}
|
|
161
|
-
// Convenience accessors
|
|
162
|
-
get code() {
|
|
163
|
-
return this.errorResponse.code;
|
|
164
|
-
}
|
|
165
|
-
get context() {
|
|
166
|
-
return this.errorResponse.context;
|
|
167
|
-
}
|
|
168
|
-
get httpStatus() {
|
|
169
|
-
return this.errorResponse.httpStatus;
|
|
170
|
-
}
|
|
171
|
-
get operation() {
|
|
172
|
-
return this.errorResponse.operation;
|
|
173
|
-
}
|
|
174
|
-
get suggestion() {
|
|
175
|
-
return this.errorResponse.suggestion;
|
|
176
|
-
}
|
|
177
|
-
get timestamp() {
|
|
178
|
-
return this.errorResponse.timestamp;
|
|
179
|
-
}
|
|
180
|
-
get documentation() {
|
|
181
|
-
return this.errorResponse.documentation;
|
|
182
|
-
}
|
|
183
|
-
// Custom serialization for logging
|
|
184
|
-
toJSON() {
|
|
185
|
-
return {
|
|
186
|
-
name: this.name,
|
|
187
|
-
message: this.message,
|
|
188
|
-
code: this.code,
|
|
189
|
-
context: this.context,
|
|
190
|
-
httpStatus: this.httpStatus,
|
|
191
|
-
operation: this.operation,
|
|
192
|
-
suggestion: this.suggestion,
|
|
193
|
-
timestamp: this.timestamp,
|
|
194
|
-
documentation: this.documentation,
|
|
195
|
-
stack: this.stack
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
var FileNotFoundError = class extends SandboxError {
|
|
200
|
-
constructor(errorResponse) {
|
|
201
|
-
super(errorResponse);
|
|
202
|
-
this.name = "FileNotFoundError";
|
|
203
|
-
}
|
|
204
|
-
// Type-safe accessors
|
|
205
|
-
get path() {
|
|
206
|
-
return this.context.path;
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
var FileExistsError = class extends SandboxError {
|
|
210
|
-
constructor(errorResponse) {
|
|
211
|
-
super(errorResponse);
|
|
212
|
-
this.name = "FileExistsError";
|
|
213
|
-
}
|
|
214
|
-
// Type-safe accessor
|
|
215
|
-
get path() {
|
|
216
|
-
return this.context.path;
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
var FileSystemError = class extends SandboxError {
|
|
220
|
-
constructor(errorResponse) {
|
|
221
|
-
super(errorResponse);
|
|
222
|
-
this.name = "FileSystemError";
|
|
223
|
-
}
|
|
224
|
-
// Type-safe accessors
|
|
225
|
-
get path() {
|
|
226
|
-
return this.context.path;
|
|
227
|
-
}
|
|
228
|
-
get stderr() {
|
|
229
|
-
return this.context.stderr;
|
|
230
|
-
}
|
|
231
|
-
get exitCode() {
|
|
232
|
-
return this.context.exitCode;
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
var PermissionDeniedError = class extends SandboxError {
|
|
236
|
-
constructor(errorResponse) {
|
|
237
|
-
super(errorResponse);
|
|
238
|
-
this.name = "PermissionDeniedError";
|
|
239
|
-
}
|
|
240
|
-
get path() {
|
|
241
|
-
return this.context.path;
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
var CommandNotFoundError = class extends SandboxError {
|
|
245
|
-
constructor(errorResponse) {
|
|
246
|
-
super(errorResponse);
|
|
247
|
-
this.name = "CommandNotFoundError";
|
|
248
|
-
}
|
|
249
|
-
// Type-safe accessor
|
|
250
|
-
get command() {
|
|
251
|
-
return this.context.command;
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
var CommandError = class extends SandboxError {
|
|
255
|
-
constructor(errorResponse) {
|
|
256
|
-
super(errorResponse);
|
|
257
|
-
this.name = "CommandError";
|
|
258
|
-
}
|
|
259
|
-
// Type-safe accessors
|
|
260
|
-
get command() {
|
|
261
|
-
return this.context.command;
|
|
262
|
-
}
|
|
263
|
-
get exitCode() {
|
|
264
|
-
return this.context.exitCode;
|
|
265
|
-
}
|
|
266
|
-
get stdout() {
|
|
267
|
-
return this.context.stdout;
|
|
268
|
-
}
|
|
269
|
-
get stderr() {
|
|
270
|
-
return this.context.stderr;
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
var ProcessNotFoundError = class extends SandboxError {
|
|
274
|
-
constructor(errorResponse) {
|
|
275
|
-
super(errorResponse);
|
|
276
|
-
this.name = "ProcessNotFoundError";
|
|
277
|
-
}
|
|
278
|
-
// Type-safe accessor
|
|
279
|
-
get processId() {
|
|
280
|
-
return this.context.processId;
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
var ProcessError = class extends SandboxError {
|
|
284
|
-
constructor(errorResponse) {
|
|
285
|
-
super(errorResponse);
|
|
286
|
-
this.name = "ProcessError";
|
|
287
|
-
}
|
|
288
|
-
// Type-safe accessors
|
|
289
|
-
get processId() {
|
|
290
|
-
return this.context.processId;
|
|
291
|
-
}
|
|
292
|
-
get pid() {
|
|
293
|
-
return this.context.pid;
|
|
294
|
-
}
|
|
295
|
-
get exitCode() {
|
|
296
|
-
return this.context.exitCode;
|
|
297
|
-
}
|
|
298
|
-
get stderr() {
|
|
299
|
-
return this.context.stderr;
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
var PortAlreadyExposedError = class extends SandboxError {
|
|
303
|
-
constructor(errorResponse) {
|
|
304
|
-
super(errorResponse);
|
|
305
|
-
this.name = "PortAlreadyExposedError";
|
|
306
|
-
}
|
|
307
|
-
// Type-safe accessors
|
|
308
|
-
get port() {
|
|
309
|
-
return this.context.port;
|
|
310
|
-
}
|
|
311
|
-
get portName() {
|
|
312
|
-
return this.context.portName;
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
var PortNotExposedError = class extends SandboxError {
|
|
316
|
-
constructor(errorResponse) {
|
|
317
|
-
super(errorResponse);
|
|
318
|
-
this.name = "PortNotExposedError";
|
|
319
|
-
}
|
|
320
|
-
// Type-safe accessor
|
|
321
|
-
get port() {
|
|
322
|
-
return this.context.port;
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
var InvalidPortError = class extends SandboxError {
|
|
326
|
-
constructor(errorResponse) {
|
|
327
|
-
super(errorResponse);
|
|
328
|
-
this.name = "InvalidPortError";
|
|
329
|
-
}
|
|
330
|
-
// Type-safe accessors
|
|
331
|
-
get port() {
|
|
332
|
-
return this.context.port;
|
|
333
|
-
}
|
|
334
|
-
get reason() {
|
|
335
|
-
return this.context.reason;
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
var ServiceNotRespondingError = class extends SandboxError {
|
|
339
|
-
constructor(errorResponse) {
|
|
340
|
-
super(errorResponse);
|
|
341
|
-
this.name = "ServiceNotRespondingError";
|
|
342
|
-
}
|
|
343
|
-
// Type-safe accessors
|
|
344
|
-
get port() {
|
|
345
|
-
return this.context.port;
|
|
346
|
-
}
|
|
347
|
-
get portName() {
|
|
348
|
-
return this.context.portName;
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
var PortInUseError = class extends SandboxError {
|
|
352
|
-
constructor(errorResponse) {
|
|
353
|
-
super(errorResponse);
|
|
354
|
-
this.name = "PortInUseError";
|
|
355
|
-
}
|
|
356
|
-
// Type-safe accessor
|
|
357
|
-
get port() {
|
|
358
|
-
return this.context.port;
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
var PortError = class extends SandboxError {
|
|
362
|
-
constructor(errorResponse) {
|
|
363
|
-
super(errorResponse);
|
|
364
|
-
this.name = "PortError";
|
|
365
|
-
}
|
|
366
|
-
// Type-safe accessors
|
|
367
|
-
get port() {
|
|
368
|
-
return this.context.port;
|
|
369
|
-
}
|
|
370
|
-
get portName() {
|
|
371
|
-
return this.context.portName;
|
|
372
|
-
}
|
|
373
|
-
get stderr() {
|
|
374
|
-
return this.context.stderr;
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
var CustomDomainRequiredError = class extends SandboxError {
|
|
378
|
-
constructor(errorResponse) {
|
|
379
|
-
super(errorResponse);
|
|
380
|
-
this.name = "CustomDomainRequiredError";
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
var GitRepositoryNotFoundError = class extends SandboxError {
|
|
384
|
-
constructor(errorResponse) {
|
|
385
|
-
super(errorResponse);
|
|
386
|
-
this.name = "GitRepositoryNotFoundError";
|
|
387
|
-
}
|
|
388
|
-
// Type-safe accessor
|
|
389
|
-
get repository() {
|
|
390
|
-
return this.context.repository;
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
var GitAuthenticationError = class extends SandboxError {
|
|
394
|
-
constructor(errorResponse) {
|
|
395
|
-
super(errorResponse);
|
|
396
|
-
this.name = "GitAuthenticationError";
|
|
397
|
-
}
|
|
398
|
-
// Type-safe accessor
|
|
399
|
-
get repository() {
|
|
400
|
-
return this.context.repository;
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
var GitBranchNotFoundError = class extends SandboxError {
|
|
404
|
-
constructor(errorResponse) {
|
|
405
|
-
super(errorResponse);
|
|
406
|
-
this.name = "GitBranchNotFoundError";
|
|
407
|
-
}
|
|
408
|
-
// Type-safe accessors
|
|
409
|
-
get branch() {
|
|
410
|
-
return this.context.branch;
|
|
411
|
-
}
|
|
412
|
-
get repository() {
|
|
413
|
-
return this.context.repository;
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
var GitNetworkError = class extends SandboxError {
|
|
417
|
-
constructor(errorResponse) {
|
|
418
|
-
super(errorResponse);
|
|
419
|
-
this.name = "GitNetworkError";
|
|
420
|
-
}
|
|
421
|
-
// Type-safe accessors
|
|
422
|
-
get repository() {
|
|
423
|
-
return this.context.repository;
|
|
424
|
-
}
|
|
425
|
-
get branch() {
|
|
426
|
-
return this.context.branch;
|
|
427
|
-
}
|
|
428
|
-
get targetDir() {
|
|
429
|
-
return this.context.targetDir;
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
var GitCloneError = class extends SandboxError {
|
|
433
|
-
constructor(errorResponse) {
|
|
434
|
-
super(errorResponse);
|
|
435
|
-
this.name = "GitCloneError";
|
|
436
|
-
}
|
|
437
|
-
// Type-safe accessors
|
|
438
|
-
get repository() {
|
|
439
|
-
return this.context.repository;
|
|
440
|
-
}
|
|
441
|
-
get targetDir() {
|
|
442
|
-
return this.context.targetDir;
|
|
443
|
-
}
|
|
444
|
-
get stderr() {
|
|
445
|
-
return this.context.stderr;
|
|
446
|
-
}
|
|
447
|
-
get exitCode() {
|
|
448
|
-
return this.context.exitCode;
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
var GitCheckoutError = class extends SandboxError {
|
|
452
|
-
constructor(errorResponse) {
|
|
453
|
-
super(errorResponse);
|
|
454
|
-
this.name = "GitCheckoutError";
|
|
455
|
-
}
|
|
456
|
-
// Type-safe accessors
|
|
457
|
-
get branch() {
|
|
458
|
-
return this.context.branch;
|
|
459
|
-
}
|
|
460
|
-
get repository() {
|
|
461
|
-
return this.context.repository;
|
|
462
|
-
}
|
|
463
|
-
get stderr() {
|
|
464
|
-
return this.context.stderr;
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
var InvalidGitUrlError = class extends SandboxError {
|
|
468
|
-
constructor(errorResponse) {
|
|
469
|
-
super(errorResponse);
|
|
470
|
-
this.name = "InvalidGitUrlError";
|
|
471
|
-
}
|
|
472
|
-
// Type-safe accessor
|
|
473
|
-
get validationErrors() {
|
|
474
|
-
return this.context.validationErrors;
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
var GitError = class extends SandboxError {
|
|
478
|
-
constructor(errorResponse) {
|
|
479
|
-
super(errorResponse);
|
|
480
|
-
this.name = "GitError";
|
|
481
|
-
}
|
|
482
|
-
// Type-safe accessors
|
|
483
|
-
get repository() {
|
|
484
|
-
return this.context.repository;
|
|
485
|
-
}
|
|
486
|
-
get branch() {
|
|
487
|
-
return this.context.branch;
|
|
488
|
-
}
|
|
489
|
-
get targetDir() {
|
|
490
|
-
return this.context.targetDir;
|
|
491
|
-
}
|
|
492
|
-
get stderr() {
|
|
493
|
-
return this.context.stderr;
|
|
494
|
-
}
|
|
495
|
-
get exitCode() {
|
|
496
|
-
return this.context.exitCode;
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
var InterpreterNotReadyError = class extends SandboxError {
|
|
500
|
-
constructor(errorResponse) {
|
|
501
|
-
super(errorResponse);
|
|
502
|
-
this.name = "InterpreterNotReadyError";
|
|
503
|
-
}
|
|
504
|
-
// Type-safe accessors
|
|
505
|
-
get retryAfter() {
|
|
506
|
-
return this.context.retryAfter;
|
|
507
|
-
}
|
|
508
|
-
get progress() {
|
|
509
|
-
return this.context.progress;
|
|
510
|
-
}
|
|
511
|
-
};
|
|
512
|
-
var ContextNotFoundError = class extends SandboxError {
|
|
513
|
-
constructor(errorResponse) {
|
|
514
|
-
super(errorResponse);
|
|
515
|
-
this.name = "ContextNotFoundError";
|
|
516
|
-
}
|
|
517
|
-
// Type-safe accessor
|
|
518
|
-
get contextId() {
|
|
519
|
-
return this.context.contextId;
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
var CodeExecutionError = class extends SandboxError {
|
|
523
|
-
constructor(errorResponse) {
|
|
524
|
-
super(errorResponse);
|
|
525
|
-
this.name = "CodeExecutionError";
|
|
526
|
-
}
|
|
527
|
-
// Type-safe accessors
|
|
528
|
-
get contextId() {
|
|
529
|
-
return this.context.contextId;
|
|
530
|
-
}
|
|
531
|
-
get ename() {
|
|
532
|
-
return this.context.ename;
|
|
533
|
-
}
|
|
534
|
-
get evalue() {
|
|
535
|
-
return this.context.evalue;
|
|
536
|
-
}
|
|
537
|
-
get traceback() {
|
|
538
|
-
return this.context.traceback;
|
|
539
|
-
}
|
|
540
|
-
};
|
|
541
|
-
var ValidationFailedError = class extends SandboxError {
|
|
542
|
-
constructor(errorResponse) {
|
|
543
|
-
super(errorResponse);
|
|
544
|
-
this.name = "ValidationFailedError";
|
|
545
|
-
}
|
|
546
|
-
// Type-safe accessor
|
|
547
|
-
get validationErrors() {
|
|
548
|
-
return this.context.validationErrors;
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
// src/errors/adapter.ts
|
|
553
|
-
function createErrorFromResponse(errorResponse) {
|
|
554
|
-
switch (errorResponse.code) {
|
|
555
|
-
// File System Errors
|
|
556
|
-
case ErrorCode.FILE_NOT_FOUND:
|
|
557
|
-
return new FileNotFoundError(errorResponse);
|
|
558
|
-
case ErrorCode.FILE_EXISTS:
|
|
559
|
-
return new FileExistsError(errorResponse);
|
|
560
|
-
case ErrorCode.PERMISSION_DENIED:
|
|
561
|
-
return new PermissionDeniedError(errorResponse);
|
|
562
|
-
case ErrorCode.IS_DIRECTORY:
|
|
563
|
-
case ErrorCode.NOT_DIRECTORY:
|
|
564
|
-
case ErrorCode.NO_SPACE:
|
|
565
|
-
case ErrorCode.TOO_MANY_FILES:
|
|
566
|
-
case ErrorCode.RESOURCE_BUSY:
|
|
567
|
-
case ErrorCode.READ_ONLY:
|
|
568
|
-
case ErrorCode.NAME_TOO_LONG:
|
|
569
|
-
case ErrorCode.TOO_MANY_LINKS:
|
|
570
|
-
case ErrorCode.FILESYSTEM_ERROR:
|
|
571
|
-
return new FileSystemError(errorResponse);
|
|
572
|
-
// Command Errors
|
|
573
|
-
case ErrorCode.COMMAND_NOT_FOUND:
|
|
574
|
-
return new CommandNotFoundError(errorResponse);
|
|
575
|
-
case ErrorCode.COMMAND_PERMISSION_DENIED:
|
|
576
|
-
case ErrorCode.COMMAND_EXECUTION_ERROR:
|
|
577
|
-
case ErrorCode.INVALID_COMMAND:
|
|
578
|
-
case ErrorCode.STREAM_START_ERROR:
|
|
579
|
-
return new CommandError(errorResponse);
|
|
580
|
-
// Process Errors
|
|
581
|
-
case ErrorCode.PROCESS_NOT_FOUND:
|
|
582
|
-
return new ProcessNotFoundError(errorResponse);
|
|
583
|
-
case ErrorCode.PROCESS_PERMISSION_DENIED:
|
|
584
|
-
case ErrorCode.PROCESS_ERROR:
|
|
585
|
-
return new ProcessError(errorResponse);
|
|
586
|
-
// Port Errors
|
|
587
|
-
case ErrorCode.PORT_ALREADY_EXPOSED:
|
|
588
|
-
return new PortAlreadyExposedError(errorResponse);
|
|
589
|
-
case ErrorCode.PORT_NOT_EXPOSED:
|
|
590
|
-
return new PortNotExposedError(errorResponse);
|
|
591
|
-
case ErrorCode.INVALID_PORT_NUMBER:
|
|
592
|
-
case ErrorCode.INVALID_PORT:
|
|
593
|
-
return new InvalidPortError(errorResponse);
|
|
594
|
-
case ErrorCode.SERVICE_NOT_RESPONDING:
|
|
595
|
-
return new ServiceNotRespondingError(errorResponse);
|
|
596
|
-
case ErrorCode.PORT_IN_USE:
|
|
597
|
-
return new PortInUseError(errorResponse);
|
|
598
|
-
case ErrorCode.PORT_OPERATION_ERROR:
|
|
599
|
-
return new PortError(errorResponse);
|
|
600
|
-
case ErrorCode.CUSTOM_DOMAIN_REQUIRED:
|
|
601
|
-
return new CustomDomainRequiredError(errorResponse);
|
|
602
|
-
// Git Errors
|
|
603
|
-
case ErrorCode.GIT_REPOSITORY_NOT_FOUND:
|
|
604
|
-
return new GitRepositoryNotFoundError(errorResponse);
|
|
605
|
-
case ErrorCode.GIT_AUTH_FAILED:
|
|
606
|
-
return new GitAuthenticationError(errorResponse);
|
|
607
|
-
case ErrorCode.GIT_BRANCH_NOT_FOUND:
|
|
608
|
-
return new GitBranchNotFoundError(errorResponse);
|
|
609
|
-
case ErrorCode.GIT_NETWORK_ERROR:
|
|
610
|
-
return new GitNetworkError(errorResponse);
|
|
611
|
-
case ErrorCode.GIT_CLONE_FAILED:
|
|
612
|
-
return new GitCloneError(errorResponse);
|
|
613
|
-
case ErrorCode.GIT_CHECKOUT_FAILED:
|
|
614
|
-
return new GitCheckoutError(errorResponse);
|
|
615
|
-
case ErrorCode.INVALID_GIT_URL:
|
|
616
|
-
return new InvalidGitUrlError(errorResponse);
|
|
617
|
-
case ErrorCode.GIT_OPERATION_FAILED:
|
|
618
|
-
return new GitError(errorResponse);
|
|
619
|
-
// Code Interpreter Errors
|
|
620
|
-
case ErrorCode.INTERPRETER_NOT_READY:
|
|
621
|
-
return new InterpreterNotReadyError(errorResponse);
|
|
622
|
-
case ErrorCode.CONTEXT_NOT_FOUND:
|
|
623
|
-
return new ContextNotFoundError(errorResponse);
|
|
624
|
-
case ErrorCode.CODE_EXECUTION_ERROR:
|
|
625
|
-
return new CodeExecutionError(errorResponse);
|
|
626
|
-
// Validation Errors
|
|
627
|
-
case ErrorCode.VALIDATION_FAILED:
|
|
628
|
-
return new ValidationFailedError(errorResponse);
|
|
629
|
-
// Generic Errors
|
|
630
|
-
case ErrorCode.INVALID_JSON_RESPONSE:
|
|
631
|
-
case ErrorCode.UNKNOWN_ERROR:
|
|
632
|
-
case ErrorCode.INTERNAL_ERROR:
|
|
633
|
-
return new SandboxError(errorResponse);
|
|
634
|
-
default:
|
|
635
|
-
return new SandboxError(errorResponse);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// src/clients/base-client.ts
|
|
640
|
-
var TIMEOUT_MS = 6e4;
|
|
641
|
-
var MIN_TIME_FOR_RETRY_MS = 1e4;
|
|
642
|
-
var BaseHttpClient = class {
|
|
643
|
-
baseUrl;
|
|
644
|
-
options;
|
|
645
|
-
logger;
|
|
646
|
-
constructor(options = {}) {
|
|
647
|
-
this.options = options;
|
|
648
|
-
this.logger = options.logger ?? createNoOpLogger();
|
|
649
|
-
this.baseUrl = this.options.baseUrl;
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Core HTTP request method with automatic retry for container provisioning delays
|
|
653
|
-
*/
|
|
654
|
-
async doFetch(path, options) {
|
|
655
|
-
const startTime = Date.now();
|
|
656
|
-
let attempt = 0;
|
|
657
|
-
while (true) {
|
|
658
|
-
const response = await this.executeFetch(path, options);
|
|
659
|
-
if (response.status === 503) {
|
|
660
|
-
const isContainerProvisioning = await this.isContainerProvisioningError(response);
|
|
661
|
-
if (isContainerProvisioning) {
|
|
662
|
-
const elapsed = Date.now() - startTime;
|
|
663
|
-
const remaining = TIMEOUT_MS - elapsed;
|
|
664
|
-
if (remaining > MIN_TIME_FOR_RETRY_MS) {
|
|
665
|
-
const delay = Math.min(2e3 * 2 ** attempt, 16e3);
|
|
666
|
-
this.logger.info("Container provisioning in progress, retrying", {
|
|
667
|
-
attempt: attempt + 1,
|
|
668
|
-
delayMs: delay,
|
|
669
|
-
remainingSec: Math.floor(remaining / 1e3)
|
|
670
|
-
});
|
|
671
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
672
|
-
attempt++;
|
|
673
|
-
continue;
|
|
674
|
-
} else {
|
|
675
|
-
this.logger.error("Container failed to provision after multiple attempts", new Error(`Failed after ${attempt + 1} attempts over 60s`));
|
|
676
|
-
return response;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
return response;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* Make a POST request with JSON body
|
|
685
|
-
*/
|
|
686
|
-
async post(endpoint, data, responseHandler) {
|
|
687
|
-
const response = await this.doFetch(endpoint, {
|
|
688
|
-
method: "POST",
|
|
689
|
-
headers: {
|
|
690
|
-
"Content-Type": "application/json"
|
|
691
|
-
},
|
|
692
|
-
body: JSON.stringify(data)
|
|
693
|
-
});
|
|
694
|
-
return this.handleResponse(response, responseHandler);
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Make a GET request
|
|
698
|
-
*/
|
|
699
|
-
async get(endpoint, responseHandler) {
|
|
700
|
-
const response = await this.doFetch(endpoint, {
|
|
701
|
-
method: "GET"
|
|
702
|
-
});
|
|
703
|
-
return this.handleResponse(response, responseHandler);
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Make a DELETE request
|
|
707
|
-
*/
|
|
708
|
-
async delete(endpoint, responseHandler) {
|
|
709
|
-
const response = await this.doFetch(endpoint, {
|
|
710
|
-
method: "DELETE"
|
|
711
|
-
});
|
|
712
|
-
return this.handleResponse(response, responseHandler);
|
|
713
|
-
}
|
|
714
|
-
/**
|
|
715
|
-
* Handle HTTP response with error checking and parsing
|
|
716
|
-
*/
|
|
717
|
-
async handleResponse(response, customHandler) {
|
|
718
|
-
if (!response.ok) {
|
|
719
|
-
await this.handleErrorResponse(response);
|
|
720
|
-
}
|
|
721
|
-
if (customHandler) {
|
|
722
|
-
return customHandler(response);
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
return await response.json();
|
|
726
|
-
} catch (error) {
|
|
727
|
-
const errorResponse = {
|
|
728
|
-
code: ErrorCode.INVALID_JSON_RESPONSE,
|
|
729
|
-
message: `Invalid JSON response: ${error instanceof Error ? error.message : "Unknown parsing error"}`,
|
|
730
|
-
context: {},
|
|
731
|
-
httpStatus: response.status,
|
|
732
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
733
|
-
};
|
|
734
|
-
throw createErrorFromResponse(errorResponse);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* Handle error responses with consistent error throwing
|
|
739
|
-
*/
|
|
740
|
-
async handleErrorResponse(response) {
|
|
741
|
-
let errorData;
|
|
742
|
-
try {
|
|
743
|
-
errorData = await response.json();
|
|
744
|
-
} catch {
|
|
745
|
-
errorData = {
|
|
746
|
-
code: ErrorCode.INTERNAL_ERROR,
|
|
747
|
-
message: `HTTP error! status: ${response.status}`,
|
|
748
|
-
context: { statusText: response.statusText },
|
|
749
|
-
httpStatus: response.status,
|
|
750
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
const error = createErrorFromResponse(errorData);
|
|
754
|
-
this.options.onError?.(errorData.message, void 0);
|
|
755
|
-
throw error;
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* Create a streaming response handler for Server-Sent Events
|
|
759
|
-
*/
|
|
760
|
-
async handleStreamResponse(response) {
|
|
761
|
-
if (!response.ok) {
|
|
762
|
-
await this.handleErrorResponse(response);
|
|
763
|
-
}
|
|
764
|
-
if (!response.body) {
|
|
765
|
-
throw new Error("No response body for streaming");
|
|
766
|
-
}
|
|
767
|
-
return response.body;
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* Utility method to log successful operations
|
|
771
|
-
*/
|
|
772
|
-
logSuccess(operation, details) {
|
|
773
|
-
this.logger.info(`${operation} completed successfully`, details ? { details } : void 0);
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* Utility method to log errors intelligently
|
|
777
|
-
* Only logs unexpected errors (5xx), not expected errors (4xx)
|
|
778
|
-
*
|
|
779
|
-
* - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
|
|
780
|
-
* - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
|
|
781
|
-
*/
|
|
782
|
-
logError(operation, error) {
|
|
783
|
-
if (error && typeof error === "object" && "httpStatus" in error) {
|
|
784
|
-
const httpStatus = error.httpStatus;
|
|
785
|
-
if (httpStatus >= 500) {
|
|
786
|
-
this.logger.error(
|
|
787
|
-
`Unexpected error in ${operation}`,
|
|
788
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
789
|
-
{ httpStatus }
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
} else {
|
|
793
|
-
this.logger.error(
|
|
794
|
-
`Error in ${operation}`,
|
|
795
|
-
error instanceof Error ? error : new Error(String(error))
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
/**
|
|
800
|
-
* Check if 503 response is from container provisioning (retryable)
|
|
801
|
-
* vs user application (not retryable)
|
|
802
|
-
*/
|
|
803
|
-
async isContainerProvisioningError(response) {
|
|
804
|
-
try {
|
|
805
|
-
const cloned = response.clone();
|
|
806
|
-
const text = await cloned.text();
|
|
807
|
-
return text.includes("There is no Container instance available");
|
|
808
|
-
} catch (error) {
|
|
809
|
-
this.logger.error("Error checking response body", error instanceof Error ? error : new Error(String(error)));
|
|
810
|
-
return false;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
async executeFetch(path, options) {
|
|
814
|
-
const url = this.options.stub ? `http://localhost:${this.options.port}${path}` : `${this.baseUrl}${path}`;
|
|
815
|
-
try {
|
|
816
|
-
if (this.options.stub) {
|
|
817
|
-
return await this.options.stub.containerFetch(
|
|
818
|
-
url,
|
|
819
|
-
options || {},
|
|
820
|
-
this.options.port
|
|
821
|
-
);
|
|
822
|
-
} else {
|
|
823
|
-
return await fetch(url, options);
|
|
824
|
-
}
|
|
825
|
-
} catch (error) {
|
|
826
|
-
this.logger.error("HTTP request error", error instanceof Error ? error : new Error(String(error)), { method: options?.method || "GET", url });
|
|
827
|
-
throw error;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
// src/clients/command-client.ts
|
|
833
|
-
var CommandClient = class extends BaseHttpClient {
|
|
834
|
-
/**
|
|
835
|
-
* Execute a command and return the complete result
|
|
836
|
-
* @param command - The command to execute
|
|
837
|
-
* @param sessionId - The session ID for this command execution
|
|
838
|
-
* @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
|
|
839
|
-
*/
|
|
840
|
-
async execute(command, sessionId, timeoutMs) {
|
|
841
|
-
try {
|
|
842
|
-
const data = {
|
|
843
|
-
command,
|
|
844
|
-
sessionId,
|
|
845
|
-
...timeoutMs !== void 0 && { timeoutMs }
|
|
846
|
-
};
|
|
847
|
-
const response = await this.post(
|
|
848
|
-
"/api/execute",
|
|
849
|
-
data
|
|
850
|
-
);
|
|
851
|
-
this.logSuccess(
|
|
852
|
-
"Command executed",
|
|
853
|
-
`${command}, Success: ${response.success}`
|
|
854
|
-
);
|
|
855
|
-
this.options.onCommandComplete?.(
|
|
856
|
-
response.success,
|
|
857
|
-
response.exitCode,
|
|
858
|
-
response.stdout,
|
|
859
|
-
response.stderr,
|
|
860
|
-
response.command
|
|
861
|
-
);
|
|
862
|
-
return response;
|
|
863
|
-
} catch (error) {
|
|
864
|
-
this.logError("execute", error);
|
|
865
|
-
this.options.onError?.(
|
|
866
|
-
error instanceof Error ? error.message : String(error),
|
|
867
|
-
command
|
|
868
|
-
);
|
|
869
|
-
throw error;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
/**
|
|
873
|
-
* Execute a command and return a stream of events
|
|
874
|
-
* @param command - The command to execute
|
|
875
|
-
* @param sessionId - The session ID for this command execution
|
|
876
|
-
*/
|
|
877
|
-
async executeStream(command, sessionId) {
|
|
878
|
-
try {
|
|
879
|
-
const data = { command, sessionId };
|
|
880
|
-
const response = await this.doFetch("/api/execute/stream", {
|
|
881
|
-
method: "POST",
|
|
882
|
-
headers: {
|
|
883
|
-
"Content-Type": "application/json"
|
|
884
|
-
},
|
|
885
|
-
body: JSON.stringify(data)
|
|
886
|
-
});
|
|
887
|
-
const stream = await this.handleStreamResponse(response);
|
|
888
|
-
this.logSuccess("Command stream started", command);
|
|
889
|
-
return stream;
|
|
890
|
-
} catch (error) {
|
|
891
|
-
this.logError("executeStream", error);
|
|
892
|
-
this.options.onError?.(
|
|
893
|
-
error instanceof Error ? error.message : String(error),
|
|
894
|
-
command
|
|
895
|
-
);
|
|
896
|
-
throw error;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
// src/clients/file-client.ts
|
|
902
|
-
var FileClient = class extends BaseHttpClient {
|
|
903
|
-
/**
|
|
904
|
-
* Create a directory
|
|
905
|
-
* @param path - Directory path to create
|
|
906
|
-
* @param sessionId - The session ID for this operation
|
|
907
|
-
* @param options - Optional settings (recursive)
|
|
908
|
-
*/
|
|
909
|
-
async mkdir(path, sessionId, options) {
|
|
910
|
-
try {
|
|
911
|
-
const data = {
|
|
912
|
-
path,
|
|
913
|
-
sessionId,
|
|
914
|
-
recursive: options?.recursive ?? false
|
|
915
|
-
};
|
|
916
|
-
const response = await this.post("/api/mkdir", data);
|
|
917
|
-
this.logSuccess("Directory created", `${path} (recursive: ${data.recursive})`);
|
|
918
|
-
return response;
|
|
919
|
-
} catch (error) {
|
|
920
|
-
this.logError("mkdir", error);
|
|
921
|
-
throw error;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* Write content to a file
|
|
926
|
-
* @param path - File path to write to
|
|
927
|
-
* @param content - Content to write
|
|
928
|
-
* @param sessionId - The session ID for this operation
|
|
929
|
-
* @param options - Optional settings (encoding)
|
|
930
|
-
*/
|
|
931
|
-
async writeFile(path, content, sessionId, options) {
|
|
932
|
-
try {
|
|
933
|
-
const data = {
|
|
934
|
-
path,
|
|
935
|
-
content,
|
|
936
|
-
sessionId,
|
|
937
|
-
encoding: options?.encoding ?? "utf8"
|
|
938
|
-
};
|
|
939
|
-
const response = await this.post("/api/write", data);
|
|
940
|
-
this.logSuccess("File written", `${path} (${content.length} chars)`);
|
|
941
|
-
return response;
|
|
942
|
-
} catch (error) {
|
|
943
|
-
this.logError("writeFile", error);
|
|
944
|
-
throw error;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
/**
|
|
948
|
-
* Read content from a file
|
|
949
|
-
* @param path - File path to read from
|
|
950
|
-
* @param sessionId - The session ID for this operation
|
|
951
|
-
* @param options - Optional settings (encoding)
|
|
952
|
-
*/
|
|
953
|
-
async readFile(path, sessionId, options) {
|
|
954
|
-
try {
|
|
955
|
-
const data = {
|
|
956
|
-
path,
|
|
957
|
-
sessionId,
|
|
958
|
-
encoding: options?.encoding ?? "utf8"
|
|
959
|
-
};
|
|
960
|
-
const response = await this.post("/api/read", data);
|
|
961
|
-
this.logSuccess("File read", `${path} (${response.content.length} chars)`);
|
|
962
|
-
return response;
|
|
963
|
-
} catch (error) {
|
|
964
|
-
this.logError("readFile", error);
|
|
965
|
-
throw error;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Stream a file using Server-Sent Events
|
|
970
|
-
* Returns a ReadableStream of SSE events containing metadata, chunks, and completion
|
|
971
|
-
* @param path - File path to stream
|
|
972
|
-
* @param sessionId - The session ID for this operation
|
|
973
|
-
*/
|
|
974
|
-
async readFileStream(path, sessionId) {
|
|
975
|
-
try {
|
|
976
|
-
const data = {
|
|
977
|
-
path,
|
|
978
|
-
sessionId
|
|
979
|
-
};
|
|
980
|
-
const response = await this.doFetch("/api/read/stream", {
|
|
981
|
-
method: "POST",
|
|
982
|
-
headers: {
|
|
983
|
-
"Content-Type": "application/json"
|
|
984
|
-
},
|
|
985
|
-
body: JSON.stringify(data)
|
|
986
|
-
});
|
|
987
|
-
const stream = await this.handleStreamResponse(response);
|
|
988
|
-
this.logSuccess("File stream started", path);
|
|
989
|
-
return stream;
|
|
990
|
-
} catch (error) {
|
|
991
|
-
this.logError("readFileStream", error);
|
|
992
|
-
throw error;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
/**
|
|
996
|
-
* Delete a file
|
|
997
|
-
* @param path - File path to delete
|
|
998
|
-
* @param sessionId - The session ID for this operation
|
|
999
|
-
*/
|
|
1000
|
-
async deleteFile(path, sessionId) {
|
|
1001
|
-
try {
|
|
1002
|
-
const data = { path, sessionId };
|
|
1003
|
-
const response = await this.post("/api/delete", data);
|
|
1004
|
-
this.logSuccess("File deleted", path);
|
|
1005
|
-
return response;
|
|
1006
|
-
} catch (error) {
|
|
1007
|
-
this.logError("deleteFile", error);
|
|
1008
|
-
throw error;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Rename a file
|
|
1013
|
-
* @param path - Current file path
|
|
1014
|
-
* @param newPath - New file path
|
|
1015
|
-
* @param sessionId - The session ID for this operation
|
|
1016
|
-
*/
|
|
1017
|
-
async renameFile(path, newPath, sessionId) {
|
|
1018
|
-
try {
|
|
1019
|
-
const data = { oldPath: path, newPath, sessionId };
|
|
1020
|
-
const response = await this.post("/api/rename", data);
|
|
1021
|
-
this.logSuccess("File renamed", `${path} -> ${newPath}`);
|
|
1022
|
-
return response;
|
|
1023
|
-
} catch (error) {
|
|
1024
|
-
this.logError("renameFile", error);
|
|
1025
|
-
throw error;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Move a file
|
|
1030
|
-
* @param path - Current file path
|
|
1031
|
-
* @param newPath - Destination file path
|
|
1032
|
-
* @param sessionId - The session ID for this operation
|
|
1033
|
-
*/
|
|
1034
|
-
async moveFile(path, newPath, sessionId) {
|
|
1035
|
-
try {
|
|
1036
|
-
const data = { sourcePath: path, destinationPath: newPath, sessionId };
|
|
1037
|
-
const response = await this.post("/api/move", data);
|
|
1038
|
-
this.logSuccess("File moved", `${path} -> ${newPath}`);
|
|
1039
|
-
return response;
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
this.logError("moveFile", error);
|
|
1042
|
-
throw error;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
/**
|
|
1046
|
-
* List files in a directory
|
|
1047
|
-
* @param path - Directory path to list
|
|
1048
|
-
* @param sessionId - The session ID for this operation
|
|
1049
|
-
* @param options - Optional settings (recursive, includeHidden)
|
|
1050
|
-
*/
|
|
1051
|
-
async listFiles(path, sessionId, options) {
|
|
1052
|
-
try {
|
|
1053
|
-
const data = {
|
|
1054
|
-
path,
|
|
1055
|
-
sessionId,
|
|
1056
|
-
options: options || {}
|
|
1057
|
-
};
|
|
1058
|
-
const response = await this.post("/api/list-files", data);
|
|
1059
|
-
this.logSuccess("Files listed", `${path} (${response.count} files)`);
|
|
1060
|
-
return response;
|
|
1061
|
-
} catch (error) {
|
|
1062
|
-
this.logError("listFiles", error);
|
|
1063
|
-
throw error;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Check if a file or directory exists
|
|
1068
|
-
* @param path - Path to check
|
|
1069
|
-
* @param sessionId - The session ID for this operation
|
|
1070
|
-
*/
|
|
1071
|
-
async exists(path, sessionId) {
|
|
1072
|
-
try {
|
|
1073
|
-
const data = {
|
|
1074
|
-
path,
|
|
1075
|
-
sessionId
|
|
1076
|
-
};
|
|
1077
|
-
const response = await this.post("/api/exists", data);
|
|
1078
|
-
this.logSuccess("Path existence checked", `${path} (exists: ${response.exists})`);
|
|
1079
|
-
return response;
|
|
1080
|
-
} catch (error) {
|
|
1081
|
-
this.logError("exists", error);
|
|
1082
|
-
throw error;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
};
|
|
1086
|
-
|
|
1087
|
-
// src/clients/git-client.ts
|
|
1088
|
-
var GitClient = class extends BaseHttpClient {
|
|
1089
|
-
/**
|
|
1090
|
-
* Clone a Git repository
|
|
1091
|
-
* @param repoUrl - URL of the Git repository to clone
|
|
1092
|
-
* @param sessionId - The session ID for this operation
|
|
1093
|
-
* @param options - Optional settings (branch, targetDir)
|
|
1094
|
-
*/
|
|
1095
|
-
async checkout(repoUrl, sessionId, options) {
|
|
1096
|
-
try {
|
|
1097
|
-
let targetDir = options?.targetDir;
|
|
1098
|
-
if (!targetDir) {
|
|
1099
|
-
const repoName = this.extractRepoName(repoUrl);
|
|
1100
|
-
targetDir = `/workspace/${repoName}`;
|
|
1101
|
-
}
|
|
1102
|
-
const data = {
|
|
1103
|
-
repoUrl,
|
|
1104
|
-
sessionId,
|
|
1105
|
-
targetDir
|
|
1106
|
-
};
|
|
1107
|
-
if (options?.branch) {
|
|
1108
|
-
data.branch = options.branch;
|
|
1109
|
-
}
|
|
1110
|
-
const response = await this.post(
|
|
1111
|
-
"/api/git/checkout",
|
|
1112
|
-
data
|
|
1113
|
-
);
|
|
1114
|
-
this.logSuccess(
|
|
1115
|
-
"Repository cloned",
|
|
1116
|
-
`${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`
|
|
1117
|
-
);
|
|
1118
|
-
return response;
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
this.logError("checkout", error);
|
|
1121
|
-
throw error;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
/**
|
|
1125
|
-
* Extract repository name from URL for default directory name
|
|
1126
|
-
*/
|
|
1127
|
-
extractRepoName(repoUrl) {
|
|
1128
|
-
try {
|
|
1129
|
-
const url = new URL(repoUrl);
|
|
1130
|
-
const pathParts = url.pathname.split("/");
|
|
1131
|
-
const repoName = pathParts[pathParts.length - 1];
|
|
1132
|
-
return repoName.replace(/\.git$/, "");
|
|
1133
|
-
} catch {
|
|
1134
|
-
const parts = repoUrl.split("/");
|
|
1135
|
-
const repoName = parts[parts.length - 1];
|
|
1136
|
-
return repoName.replace(/\.git$/, "") || "repo";
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
};
|
|
1140
|
-
|
|
1141
|
-
// src/clients/interpreter-client.ts
|
|
1142
|
-
var InterpreterClient = class extends BaseHttpClient {
|
|
1143
|
-
maxRetries = 3;
|
|
1144
|
-
retryDelayMs = 1e3;
|
|
1145
|
-
async createCodeContext(options = {}) {
|
|
1146
|
-
return this.executeWithRetry(async () => {
|
|
1147
|
-
const response = await this.doFetch("/api/contexts", {
|
|
1148
|
-
method: "POST",
|
|
1149
|
-
headers: { "Content-Type": "application/json" },
|
|
1150
|
-
body: JSON.stringify({
|
|
1151
|
-
language: options.language || "python",
|
|
1152
|
-
cwd: options.cwd || "/workspace",
|
|
1153
|
-
env_vars: options.envVars
|
|
1154
|
-
})
|
|
1155
|
-
});
|
|
1156
|
-
if (!response.ok) {
|
|
1157
|
-
const error = await this.parseErrorResponse(response);
|
|
1158
|
-
throw error;
|
|
1159
|
-
}
|
|
1160
|
-
const data = await response.json();
|
|
1161
|
-
if (!data.success) {
|
|
1162
|
-
throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
|
|
1163
|
-
}
|
|
1164
|
-
return {
|
|
1165
|
-
id: data.contextId,
|
|
1166
|
-
language: data.language,
|
|
1167
|
-
cwd: data.cwd || "/workspace",
|
|
1168
|
-
createdAt: new Date(data.timestamp),
|
|
1169
|
-
lastUsed: new Date(data.timestamp)
|
|
1170
|
-
};
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
async runCodeStream(contextId, code, language, callbacks, timeoutMs) {
|
|
1174
|
-
return this.executeWithRetry(async () => {
|
|
1175
|
-
const response = await this.doFetch("/api/execute/code", {
|
|
1176
|
-
method: "POST",
|
|
1177
|
-
headers: {
|
|
1178
|
-
"Content-Type": "application/json",
|
|
1179
|
-
Accept: "text/event-stream"
|
|
1180
|
-
},
|
|
1181
|
-
body: JSON.stringify({
|
|
1182
|
-
context_id: contextId,
|
|
1183
|
-
code,
|
|
1184
|
-
language,
|
|
1185
|
-
...timeoutMs !== void 0 && { timeout_ms: timeoutMs }
|
|
1186
|
-
})
|
|
1187
|
-
});
|
|
1188
|
-
if (!response.ok) {
|
|
1189
|
-
const error = await this.parseErrorResponse(response);
|
|
1190
|
-
throw error;
|
|
1191
|
-
}
|
|
1192
|
-
if (!response.body) {
|
|
1193
|
-
throw new Error("No response body for streaming execution");
|
|
1194
|
-
}
|
|
1195
|
-
for await (const chunk of this.readLines(response.body)) {
|
|
1196
|
-
await this.parseExecutionResult(chunk, callbacks);
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
async listCodeContexts() {
|
|
1201
|
-
return this.executeWithRetry(async () => {
|
|
1202
|
-
const response = await this.doFetch("/api/contexts", {
|
|
1203
|
-
method: "GET",
|
|
1204
|
-
headers: { "Content-Type": "application/json" }
|
|
1205
|
-
});
|
|
1206
|
-
if (!response.ok) {
|
|
1207
|
-
const error = await this.parseErrorResponse(response);
|
|
1208
|
-
throw error;
|
|
1209
|
-
}
|
|
1210
|
-
const data = await response.json();
|
|
1211
|
-
if (!data.success) {
|
|
1212
|
-
throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
|
|
1213
|
-
}
|
|
1214
|
-
return data.contexts.map((ctx) => ({
|
|
1215
|
-
id: ctx.id,
|
|
1216
|
-
language: ctx.language,
|
|
1217
|
-
cwd: ctx.cwd || "/workspace",
|
|
1218
|
-
createdAt: new Date(data.timestamp),
|
|
1219
|
-
lastUsed: new Date(data.timestamp)
|
|
1220
|
-
}));
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
async deleteCodeContext(contextId) {
|
|
1224
|
-
return this.executeWithRetry(async () => {
|
|
1225
|
-
const response = await this.doFetch(`/api/contexts/${contextId}`, {
|
|
1226
|
-
method: "DELETE",
|
|
1227
|
-
headers: { "Content-Type": "application/json" }
|
|
1228
|
-
});
|
|
1229
|
-
if (!response.ok) {
|
|
1230
|
-
const error = await this.parseErrorResponse(response);
|
|
1231
|
-
throw error;
|
|
1232
|
-
}
|
|
1233
|
-
});
|
|
1234
|
-
}
|
|
1235
|
-
/**
|
|
1236
|
-
* Execute an operation with automatic retry for transient errors
|
|
1237
|
-
*/
|
|
1238
|
-
async executeWithRetry(operation) {
|
|
1239
|
-
let lastError;
|
|
1240
|
-
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
1241
|
-
try {
|
|
1242
|
-
return await operation();
|
|
1243
|
-
} catch (error) {
|
|
1244
|
-
this.logError("executeWithRetry", error);
|
|
1245
|
-
lastError = error;
|
|
1246
|
-
if (this.isRetryableError(error)) {
|
|
1247
|
-
if (attempt < this.maxRetries - 1) {
|
|
1248
|
-
const delay = this.retryDelayMs * 2 ** attempt + Math.random() * 1e3;
|
|
1249
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1250
|
-
continue;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
throw error;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
throw lastError || new Error("Execution failed after retries");
|
|
1257
|
-
}
|
|
1258
|
-
isRetryableError(error) {
|
|
1259
|
-
if (error instanceof InterpreterNotReadyError) {
|
|
1260
|
-
return true;
|
|
1261
|
-
}
|
|
1262
|
-
if (error instanceof Error) {
|
|
1263
|
-
return error.message.includes("not ready") || error.message.includes("initializing");
|
|
1264
|
-
}
|
|
1265
|
-
return false;
|
|
1266
|
-
}
|
|
1267
|
-
async parseErrorResponse(response) {
|
|
1268
|
-
try {
|
|
1269
|
-
const errorData = await response.json();
|
|
1270
|
-
return createErrorFromResponse(errorData);
|
|
1271
|
-
} catch {
|
|
1272
|
-
const errorResponse = {
|
|
1273
|
-
code: ErrorCode.INTERNAL_ERROR,
|
|
1274
|
-
message: `HTTP ${response.status}: ${response.statusText}`,
|
|
1275
|
-
context: {},
|
|
1276
|
-
httpStatus: response.status,
|
|
1277
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1278
|
-
};
|
|
1279
|
-
return createErrorFromResponse(errorResponse);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
async *readLines(stream) {
|
|
1283
|
-
const reader = stream.getReader();
|
|
1284
|
-
let buffer = "";
|
|
1285
|
-
try {
|
|
1286
|
-
while (true) {
|
|
1287
|
-
const { done, value } = await reader.read();
|
|
1288
|
-
if (value) {
|
|
1289
|
-
buffer += new TextDecoder().decode(value);
|
|
1290
|
-
}
|
|
1291
|
-
if (done) break;
|
|
1292
|
-
let newlineIdx = buffer.indexOf("\n");
|
|
1293
|
-
while (newlineIdx !== -1) {
|
|
1294
|
-
yield buffer.slice(0, newlineIdx);
|
|
1295
|
-
buffer = buffer.slice(newlineIdx + 1);
|
|
1296
|
-
newlineIdx = buffer.indexOf("\n");
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
if (buffer.length > 0) {
|
|
1300
|
-
yield buffer;
|
|
1301
|
-
}
|
|
1302
|
-
} finally {
|
|
1303
|
-
reader.releaseLock();
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
async parseExecutionResult(line, callbacks) {
|
|
1307
|
-
if (!line.trim()) return;
|
|
1308
|
-
if (!line.startsWith("data: ")) return;
|
|
1309
|
-
try {
|
|
1310
|
-
const jsonData = line.substring(6);
|
|
1311
|
-
const data = JSON.parse(jsonData);
|
|
1312
|
-
switch (data.type) {
|
|
1313
|
-
case "stdout":
|
|
1314
|
-
if (callbacks.onStdout && data.text) {
|
|
1315
|
-
await callbacks.onStdout({
|
|
1316
|
-
text: data.text,
|
|
1317
|
-
timestamp: data.timestamp || Date.now()
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
|
-
break;
|
|
1321
|
-
case "stderr":
|
|
1322
|
-
if (callbacks.onStderr && data.text) {
|
|
1323
|
-
await callbacks.onStderr({
|
|
1324
|
-
text: data.text,
|
|
1325
|
-
timestamp: data.timestamp || Date.now()
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
break;
|
|
1329
|
-
case "result":
|
|
1330
|
-
if (callbacks.onResult) {
|
|
1331
|
-
const result = new ResultImpl(data);
|
|
1332
|
-
await callbacks.onResult(result);
|
|
1333
|
-
}
|
|
1334
|
-
break;
|
|
1335
|
-
case "error":
|
|
1336
|
-
if (callbacks.onError) {
|
|
1337
|
-
await callbacks.onError({
|
|
1338
|
-
name: data.ename || "Error",
|
|
1339
|
-
message: data.evalue || "Unknown error",
|
|
1340
|
-
traceback: data.traceback || []
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
break;
|
|
1344
|
-
case "execution_complete":
|
|
1345
|
-
break;
|
|
1346
|
-
}
|
|
1347
|
-
} catch (error) {
|
|
1348
|
-
this.logError("parseExecutionResult", error);
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
};
|
|
1352
|
-
|
|
1353
|
-
// src/clients/port-client.ts
|
|
1354
|
-
var PortClient = class extends BaseHttpClient {
|
|
1355
|
-
/**
|
|
1356
|
-
* Expose a port and get a preview URL
|
|
1357
|
-
* @param port - Port number to expose
|
|
1358
|
-
* @param sessionId - The session ID for this operation
|
|
1359
|
-
* @param name - Optional name for the port
|
|
1360
|
-
*/
|
|
1361
|
-
async exposePort(port, sessionId, name) {
|
|
1362
|
-
try {
|
|
1363
|
-
const data = { port, sessionId, name };
|
|
1364
|
-
const response = await this.post(
|
|
1365
|
-
"/api/expose-port",
|
|
1366
|
-
data
|
|
1367
|
-
);
|
|
1368
|
-
this.logSuccess(
|
|
1369
|
-
"Port exposed",
|
|
1370
|
-
`${port} exposed at ${response.url}${name ? ` (${name})` : ""}`
|
|
1371
|
-
);
|
|
1372
|
-
return response;
|
|
1373
|
-
} catch (error) {
|
|
1374
|
-
this.logError("exposePort", error);
|
|
1375
|
-
throw error;
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
/**
|
|
1379
|
-
* Unexpose a port and remove its preview URL
|
|
1380
|
-
* @param port - Port number to unexpose
|
|
1381
|
-
* @param sessionId - The session ID for this operation
|
|
1382
|
-
*/
|
|
1383
|
-
async unexposePort(port, sessionId) {
|
|
1384
|
-
try {
|
|
1385
|
-
const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
|
|
1386
|
-
const response = await this.delete(url);
|
|
1387
|
-
this.logSuccess("Port unexposed", `${port}`);
|
|
1388
|
-
return response;
|
|
1389
|
-
} catch (error) {
|
|
1390
|
-
this.logError("unexposePort", error);
|
|
1391
|
-
throw error;
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
/**
|
|
1395
|
-
* Get all currently exposed ports
|
|
1396
|
-
* @param sessionId - The session ID for this operation
|
|
1397
|
-
*/
|
|
1398
|
-
async getExposedPorts(sessionId) {
|
|
1399
|
-
try {
|
|
1400
|
-
const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
|
|
1401
|
-
const response = await this.get(url);
|
|
1402
|
-
this.logSuccess(
|
|
1403
|
-
"Exposed ports retrieved",
|
|
1404
|
-
`${response.ports.length} ports exposed`
|
|
1405
|
-
);
|
|
1406
|
-
return response;
|
|
1407
|
-
} catch (error) {
|
|
1408
|
-
this.logError("getExposedPorts", error);
|
|
1409
|
-
throw error;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
};
|
|
1413
|
-
|
|
1414
|
-
// src/clients/process-client.ts
|
|
1415
|
-
var ProcessClient = class extends BaseHttpClient {
|
|
1416
|
-
/**
|
|
1417
|
-
* Start a background process
|
|
1418
|
-
* @param command - Command to execute as a background process
|
|
1419
|
-
* @param sessionId - The session ID for this operation
|
|
1420
|
-
* @param options - Optional settings (processId)
|
|
1421
|
-
*/
|
|
1422
|
-
async startProcess(command, sessionId, options) {
|
|
1423
|
-
try {
|
|
1424
|
-
const data = {
|
|
1425
|
-
command,
|
|
1426
|
-
sessionId,
|
|
1427
|
-
processId: options?.processId
|
|
1428
|
-
};
|
|
1429
|
-
const response = await this.post(
|
|
1430
|
-
"/api/process/start",
|
|
1431
|
-
data
|
|
1432
|
-
);
|
|
1433
|
-
this.logSuccess(
|
|
1434
|
-
"Process started",
|
|
1435
|
-
`${command} (ID: ${response.processId})`
|
|
1436
|
-
);
|
|
1437
|
-
return response;
|
|
1438
|
-
} catch (error) {
|
|
1439
|
-
this.logError("startProcess", error);
|
|
1440
|
-
throw error;
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
/**
|
|
1444
|
-
* List all processes (sandbox-scoped, not session-scoped)
|
|
1445
|
-
*/
|
|
1446
|
-
async listProcesses() {
|
|
1447
|
-
try {
|
|
1448
|
-
const url = `/api/process/list`;
|
|
1449
|
-
const response = await this.get(url);
|
|
1450
|
-
this.logSuccess("Processes listed", `${response.processes.length} processes`);
|
|
1451
|
-
return response;
|
|
1452
|
-
} catch (error) {
|
|
1453
|
-
this.logError("listProcesses", error);
|
|
1454
|
-
throw error;
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
/**
|
|
1458
|
-
* Get information about a specific process (sandbox-scoped, not session-scoped)
|
|
1459
|
-
* @param processId - ID of the process to retrieve
|
|
1460
|
-
*/
|
|
1461
|
-
async getProcess(processId) {
|
|
1462
|
-
try {
|
|
1463
|
-
const url = `/api/process/${processId}`;
|
|
1464
|
-
const response = await this.get(url);
|
|
1465
|
-
this.logSuccess("Process retrieved", `ID: ${processId}`);
|
|
1466
|
-
return response;
|
|
1467
|
-
} catch (error) {
|
|
1468
|
-
this.logError("getProcess", error);
|
|
1469
|
-
throw error;
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
/**
|
|
1473
|
-
* Kill a specific process (sandbox-scoped, not session-scoped)
|
|
1474
|
-
* @param processId - ID of the process to kill
|
|
1475
|
-
*/
|
|
1476
|
-
async killProcess(processId) {
|
|
1477
|
-
try {
|
|
1478
|
-
const url = `/api/process/${processId}`;
|
|
1479
|
-
const response = await this.delete(url);
|
|
1480
|
-
this.logSuccess("Process killed", `ID: ${processId}`);
|
|
1481
|
-
return response;
|
|
1482
|
-
} catch (error) {
|
|
1483
|
-
this.logError("killProcess", error);
|
|
1484
|
-
throw error;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
/**
|
|
1488
|
-
* Kill all running processes (sandbox-scoped, not session-scoped)
|
|
1489
|
-
*/
|
|
1490
|
-
async killAllProcesses() {
|
|
1491
|
-
try {
|
|
1492
|
-
const url = `/api/process/kill-all`;
|
|
1493
|
-
const response = await this.delete(url);
|
|
1494
|
-
this.logSuccess(
|
|
1495
|
-
"All processes killed",
|
|
1496
|
-
`${response.cleanedCount} processes terminated`
|
|
1497
|
-
);
|
|
1498
|
-
return response;
|
|
1499
|
-
} catch (error) {
|
|
1500
|
-
this.logError("killAllProcesses", error);
|
|
1501
|
-
throw error;
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
/**
|
|
1505
|
-
* Get logs from a specific process (sandbox-scoped, not session-scoped)
|
|
1506
|
-
* @param processId - ID of the process to get logs from
|
|
1507
|
-
*/
|
|
1508
|
-
async getProcessLogs(processId) {
|
|
1509
|
-
try {
|
|
1510
|
-
const url = `/api/process/${processId}/logs`;
|
|
1511
|
-
const response = await this.get(url);
|
|
1512
|
-
this.logSuccess(
|
|
1513
|
-
"Process logs retrieved",
|
|
1514
|
-
`ID: ${processId}, stdout: ${response.stdout.length} chars, stderr: ${response.stderr.length} chars`
|
|
1515
|
-
);
|
|
1516
|
-
return response;
|
|
1517
|
-
} catch (error) {
|
|
1518
|
-
this.logError("getProcessLogs", error);
|
|
1519
|
-
throw error;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* Stream logs from a specific process (sandbox-scoped, not session-scoped)
|
|
1524
|
-
* @param processId - ID of the process to stream logs from
|
|
1525
|
-
*/
|
|
1526
|
-
async streamProcessLogs(processId) {
|
|
1527
|
-
try {
|
|
1528
|
-
const url = `/api/process/${processId}/stream`;
|
|
1529
|
-
const response = await this.doFetch(url, {
|
|
1530
|
-
method: "GET"
|
|
1531
|
-
});
|
|
1532
|
-
const stream = await this.handleStreamResponse(response);
|
|
1533
|
-
this.logSuccess("Process log stream started", `ID: ${processId}`);
|
|
1534
|
-
return stream;
|
|
1535
|
-
} catch (error) {
|
|
1536
|
-
this.logError("streamProcessLogs", error);
|
|
1537
|
-
throw error;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
};
|
|
1541
|
-
|
|
1542
|
-
// src/clients/utility-client.ts
|
|
1543
|
-
var UtilityClient = class extends BaseHttpClient {
|
|
1544
|
-
/**
|
|
1545
|
-
* Ping the sandbox to check if it's responsive
|
|
1546
|
-
*/
|
|
1547
|
-
async ping() {
|
|
1548
|
-
try {
|
|
1549
|
-
const response = await this.get("/api/ping");
|
|
1550
|
-
this.logSuccess("Ping successful", response.message);
|
|
1551
|
-
return response.message;
|
|
1552
|
-
} catch (error) {
|
|
1553
|
-
this.logError("ping", error);
|
|
1554
|
-
throw error;
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
/**
|
|
1558
|
-
* Get list of available commands in the sandbox environment
|
|
1559
|
-
*/
|
|
1560
|
-
async getCommands() {
|
|
1561
|
-
try {
|
|
1562
|
-
const response = await this.get("/api/commands");
|
|
1563
|
-
this.logSuccess(
|
|
1564
|
-
"Commands retrieved",
|
|
1565
|
-
`${response.count} commands available`
|
|
1566
|
-
);
|
|
1567
|
-
return response.availableCommands;
|
|
1568
|
-
} catch (error) {
|
|
1569
|
-
this.logError("getCommands", error);
|
|
1570
|
-
throw error;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
/**
|
|
1574
|
-
* Create a new execution session
|
|
1575
|
-
* @param options - Session configuration (id, env, cwd)
|
|
1576
|
-
*/
|
|
1577
|
-
async createSession(options) {
|
|
1578
|
-
try {
|
|
1579
|
-
const response = await this.post(
|
|
1580
|
-
"/api/session/create",
|
|
1581
|
-
options
|
|
1582
|
-
);
|
|
1583
|
-
this.logSuccess("Session created", `ID: ${options.id}`);
|
|
1584
|
-
return response;
|
|
1585
|
-
} catch (error) {
|
|
1586
|
-
this.logError("createSession", error);
|
|
1587
|
-
throw error;
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
/**
|
|
1591
|
-
* Get the container version
|
|
1592
|
-
* Returns the version embedded in the Docker image during build
|
|
1593
|
-
*/
|
|
1594
|
-
async getVersion() {
|
|
1595
|
-
try {
|
|
1596
|
-
const response = await this.get("/api/version");
|
|
1597
|
-
this.logSuccess("Version retrieved", response.version);
|
|
1598
|
-
return response.version;
|
|
1599
|
-
} catch (error) {
|
|
1600
|
-
this.logger.debug("Failed to get container version (may be old container)", { error });
|
|
1601
|
-
return "unknown";
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
};
|
|
1605
|
-
|
|
1606
|
-
// src/clients/sandbox-client.ts
|
|
1607
|
-
var SandboxClient = class {
|
|
1608
|
-
commands;
|
|
1609
|
-
files;
|
|
1610
|
-
processes;
|
|
1611
|
-
ports;
|
|
1612
|
-
git;
|
|
1613
|
-
interpreter;
|
|
1614
|
-
utils;
|
|
1615
|
-
constructor(options) {
|
|
1616
|
-
const clientOptions = {
|
|
1617
|
-
baseUrl: "http://localhost:3000",
|
|
1618
|
-
...options
|
|
1619
|
-
};
|
|
1620
|
-
this.commands = new CommandClient(clientOptions);
|
|
1621
|
-
this.files = new FileClient(clientOptions);
|
|
1622
|
-
this.processes = new ProcessClient(clientOptions);
|
|
1623
|
-
this.ports = new PortClient(clientOptions);
|
|
1624
|
-
this.git = new GitClient(clientOptions);
|
|
1625
|
-
this.interpreter = new InterpreterClient(clientOptions);
|
|
1626
|
-
this.utils = new UtilityClient(clientOptions);
|
|
1627
|
-
}
|
|
1628
|
-
};
|
|
1629
|
-
|
|
1630
|
-
// src/sandbox.ts
|
|
1631
|
-
function getSandbox(ns, id, options) {
|
|
1632
|
-
const stub = getContainer(ns, id);
|
|
1633
|
-
stub.setSandboxName?.(id);
|
|
1634
|
-
if (options?.baseUrl) {
|
|
1635
|
-
stub.setBaseUrl(options.baseUrl);
|
|
1636
|
-
}
|
|
1637
|
-
if (options?.sleepAfter !== void 0) {
|
|
1638
|
-
stub.setSleepAfter(options.sleepAfter);
|
|
1639
|
-
}
|
|
1640
|
-
if (options?.keepAlive !== void 0) {
|
|
1641
|
-
stub.setKeepAlive(options.keepAlive);
|
|
1642
|
-
}
|
|
1643
|
-
return stub;
|
|
1644
|
-
}
|
|
1645
|
-
var Sandbox = class extends Container {
|
|
1646
|
-
defaultPort = 3e3;
|
|
1647
|
-
// Default port for the container's Bun server
|
|
1648
|
-
sleepAfter = "10m";
|
|
1649
|
-
// Sleep the sandbox if no requests are made in this timeframe
|
|
1650
|
-
client;
|
|
1651
|
-
codeInterpreter;
|
|
1652
|
-
sandboxName = null;
|
|
1653
|
-
baseUrl = null;
|
|
1654
|
-
portTokens = /* @__PURE__ */ new Map();
|
|
1655
|
-
defaultSession = null;
|
|
1656
|
-
envVars = {};
|
|
1657
|
-
logger;
|
|
1658
|
-
keepAliveEnabled = false;
|
|
1659
|
-
constructor(ctx, env) {
|
|
1660
|
-
super(ctx, env);
|
|
1661
|
-
const envObj = env;
|
|
1662
|
-
const sandboxEnvKeys = ["SANDBOX_LOG_LEVEL", "SANDBOX_LOG_FORMAT"];
|
|
1663
|
-
sandboxEnvKeys.forEach((key) => {
|
|
1664
|
-
if (envObj?.[key]) {
|
|
1665
|
-
this.envVars[key] = envObj[key];
|
|
1666
|
-
}
|
|
1667
|
-
});
|
|
1668
|
-
this.logger = createLogger({
|
|
1669
|
-
component: "sandbox-do",
|
|
1670
|
-
sandboxId: this.ctx.id.toString()
|
|
1671
|
-
});
|
|
1672
|
-
this.client = new SandboxClient({
|
|
1673
|
-
logger: this.logger,
|
|
1674
|
-
port: 3e3,
|
|
1675
|
-
// Control plane port
|
|
1676
|
-
stub: this
|
|
1677
|
-
});
|
|
1678
|
-
this.codeInterpreter = new CodeInterpreter(this);
|
|
1679
|
-
this.ctx.blockConcurrencyWhile(async () => {
|
|
1680
|
-
this.sandboxName = await this.ctx.storage.get("sandboxName") || null;
|
|
1681
|
-
this.defaultSession = await this.ctx.storage.get("defaultSession") || null;
|
|
1682
|
-
const storedTokens = await this.ctx.storage.get("portTokens") || {};
|
|
1683
|
-
this.portTokens = /* @__PURE__ */ new Map();
|
|
1684
|
-
for (const [portStr, token] of Object.entries(storedTokens)) {
|
|
1685
|
-
this.portTokens.set(parseInt(portStr, 10), token);
|
|
1686
|
-
}
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
// RPC method to set the sandbox name
|
|
1690
|
-
async setSandboxName(name) {
|
|
1691
|
-
if (!this.sandboxName) {
|
|
1692
|
-
this.sandboxName = name;
|
|
1693
|
-
await this.ctx.storage.put("sandboxName", name);
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
// RPC method to set the base URL
|
|
1697
|
-
async setBaseUrl(baseUrl) {
|
|
1698
|
-
if (!this.baseUrl) {
|
|
1699
|
-
this.baseUrl = baseUrl;
|
|
1700
|
-
await this.ctx.storage.put("baseUrl", baseUrl);
|
|
1701
|
-
} else {
|
|
1702
|
-
if (this.baseUrl !== baseUrl) {
|
|
1703
|
-
throw new Error("Base URL already set and different from one previously provided");
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
// RPC method to set the sleep timeout
|
|
1708
|
-
async setSleepAfter(sleepAfter) {
|
|
1709
|
-
this.sleepAfter = sleepAfter;
|
|
1710
|
-
}
|
|
1711
|
-
// RPC method to enable keepAlive mode
|
|
1712
|
-
async setKeepAlive(keepAlive) {
|
|
1713
|
-
this.keepAliveEnabled = keepAlive;
|
|
1714
|
-
if (keepAlive) {
|
|
1715
|
-
this.logger.info("KeepAlive mode enabled - container will stay alive until explicitly destroyed");
|
|
1716
|
-
} else {
|
|
1717
|
-
this.logger.info("KeepAlive mode disabled - container will timeout normally");
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
// RPC method to set environment variables
|
|
1721
|
-
async setEnvVars(envVars) {
|
|
1722
|
-
this.envVars = { ...this.envVars, ...envVars };
|
|
1723
|
-
if (this.defaultSession) {
|
|
1724
|
-
for (const [key, value] of Object.entries(envVars)) {
|
|
1725
|
-
const escapedValue = value.replace(/'/g, "'\\''");
|
|
1726
|
-
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
1727
|
-
const result = await this.client.commands.execute(exportCommand, this.defaultSession);
|
|
1728
|
-
if (result.exitCode !== 0) {
|
|
1729
|
-
throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
/**
|
|
1735
|
-
* Cleanup and destroy the sandbox container
|
|
1736
|
-
*/
|
|
1737
|
-
async destroy() {
|
|
1738
|
-
this.logger.info("Destroying sandbox container");
|
|
1739
|
-
await super.destroy();
|
|
1740
|
-
}
|
|
1741
|
-
onStart() {
|
|
1742
|
-
this.logger.debug("Sandbox started");
|
|
1743
|
-
this.checkVersionCompatibility().catch((error) => {
|
|
1744
|
-
this.logger.error("Version compatibility check failed", error instanceof Error ? error : new Error(String(error)));
|
|
1745
|
-
});
|
|
1746
|
-
}
|
|
1747
|
-
/**
|
|
1748
|
-
* Check if the container version matches the SDK version
|
|
1749
|
-
* Logs a warning if there's a mismatch
|
|
1750
|
-
*/
|
|
1751
|
-
async checkVersionCompatibility() {
|
|
1752
|
-
try {
|
|
1753
|
-
const sdkVersion = SDK_VERSION;
|
|
1754
|
-
const containerVersion = await this.client.utils.getVersion();
|
|
1755
|
-
if (containerVersion === "unknown") {
|
|
1756
|
-
this.logger.warn(
|
|
1757
|
-
"Container version check: Container version could not be determined. This may indicate an outdated container image. Please update your container to match SDK version " + sdkVersion
|
|
1758
|
-
);
|
|
1759
|
-
return;
|
|
1760
|
-
}
|
|
1761
|
-
if (containerVersion !== sdkVersion) {
|
|
1762
|
-
const message = `Version mismatch detected! SDK version (${sdkVersion}) does not match container version (${containerVersion}). This may cause compatibility issues. Please update your container image to version ${sdkVersion}`;
|
|
1763
|
-
this.logger.warn(message);
|
|
1764
|
-
} else {
|
|
1765
|
-
this.logger.debug("Version check passed", { sdkVersion, containerVersion });
|
|
1766
|
-
}
|
|
1767
|
-
} catch (error) {
|
|
1768
|
-
this.logger.debug("Version compatibility check encountered an error", {
|
|
1769
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1770
|
-
});
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
onStop() {
|
|
1774
|
-
this.logger.debug("Sandbox stopped");
|
|
1775
|
-
}
|
|
1776
|
-
onError(error) {
|
|
1777
|
-
this.logger.error("Sandbox error", error instanceof Error ? error : new Error(String(error)));
|
|
1778
|
-
}
|
|
1779
|
-
/**
|
|
1780
|
-
* Override onActivityExpired to prevent automatic shutdown when keepAlive is enabled
|
|
1781
|
-
* When keepAlive is disabled, calls parent implementation which stops the container
|
|
1782
|
-
*/
|
|
1783
|
-
async onActivityExpired() {
|
|
1784
|
-
if (this.keepAliveEnabled) {
|
|
1785
|
-
this.logger.debug("Activity expired but keepAlive is enabled - container will stay alive");
|
|
1786
|
-
} else {
|
|
1787
|
-
this.logger.debug("Activity expired - stopping container");
|
|
1788
|
-
await super.onActivityExpired();
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
// Override fetch to route internal container requests to appropriate ports
|
|
1792
|
-
async fetch(request) {
|
|
1793
|
-
const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate();
|
|
1794
|
-
const requestLogger = this.logger.child({ traceId, operation: "fetch" });
|
|
1795
|
-
return await runWithLogger(requestLogger, async () => {
|
|
1796
|
-
const url = new URL(request.url);
|
|
1797
|
-
if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
|
|
1798
|
-
const name = request.headers.get("X-Sandbox-Name");
|
|
1799
|
-
this.sandboxName = name;
|
|
1800
|
-
await this.ctx.storage.put("sandboxName", name);
|
|
1801
|
-
}
|
|
1802
|
-
const upgradeHeader = request.headers.get("Upgrade");
|
|
1803
|
-
const isWebSocket = upgradeHeader?.toLowerCase() === "websocket";
|
|
1804
|
-
if (isWebSocket) {
|
|
1805
|
-
return await super.fetch(request);
|
|
1806
|
-
}
|
|
1807
|
-
const port = this.determinePort(url);
|
|
1808
|
-
return await this.containerFetch(request, port);
|
|
1809
|
-
});
|
|
1810
|
-
}
|
|
1811
|
-
determinePort(url) {
|
|
1812
|
-
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
1813
|
-
if (proxyMatch) {
|
|
1814
|
-
return parseInt(proxyMatch[1], 10);
|
|
1815
|
-
}
|
|
1816
|
-
return 3e3;
|
|
1817
|
-
}
|
|
1818
|
-
/**
|
|
1819
|
-
* Ensure default session exists - lazy initialization
|
|
1820
|
-
* This is called automatically by all public methods that need a session
|
|
1821
|
-
*
|
|
1822
|
-
* The session is persisted to Durable Object storage to survive hot reloads
|
|
1823
|
-
* during development. If a session already exists in the container after reload,
|
|
1824
|
-
* we reuse it instead of trying to create a new one.
|
|
1825
|
-
*/
|
|
1826
|
-
async ensureDefaultSession() {
|
|
1827
|
-
if (!this.defaultSession) {
|
|
1828
|
-
const sessionId = `sandbox-${this.sandboxName || "default"}`;
|
|
1829
|
-
try {
|
|
1830
|
-
await this.client.utils.createSession({
|
|
1831
|
-
id: sessionId,
|
|
1832
|
-
env: this.envVars || {},
|
|
1833
|
-
cwd: "/workspace"
|
|
1834
|
-
});
|
|
1835
|
-
this.defaultSession = sessionId;
|
|
1836
|
-
await this.ctx.storage.put("defaultSession", sessionId);
|
|
1837
|
-
this.logger.debug("Default session initialized", { sessionId });
|
|
1838
|
-
} catch (error) {
|
|
1839
|
-
if (error?.message?.includes("already exists")) {
|
|
1840
|
-
this.logger.debug("Reusing existing session after reload", { sessionId });
|
|
1841
|
-
this.defaultSession = sessionId;
|
|
1842
|
-
await this.ctx.storage.put("defaultSession", sessionId);
|
|
1843
|
-
} else {
|
|
1844
|
-
throw error;
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
return this.defaultSession;
|
|
1849
|
-
}
|
|
1850
|
-
// Enhanced exec method - always returns ExecResult with optional streaming
|
|
1851
|
-
// This replaces the old exec method to match ISandbox interface
|
|
1852
|
-
async exec(command, options) {
|
|
1853
|
-
const session = await this.ensureDefaultSession();
|
|
1854
|
-
return this.execWithSession(command, session, options);
|
|
1855
|
-
}
|
|
1856
|
-
/**
|
|
1857
|
-
* Internal session-aware exec implementation
|
|
1858
|
-
* Used by both public exec() and session wrappers
|
|
1859
|
-
*/
|
|
1860
|
-
async execWithSession(command, sessionId, options) {
|
|
1861
|
-
const startTime = Date.now();
|
|
1862
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1863
|
-
let timeoutId;
|
|
1864
|
-
try {
|
|
1865
|
-
if (options?.signal?.aborted) {
|
|
1866
|
-
throw new Error("Operation was aborted");
|
|
1867
|
-
}
|
|
1868
|
-
let result;
|
|
1869
|
-
if (options?.stream && options?.onOutput) {
|
|
1870
|
-
result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp);
|
|
1871
|
-
} else {
|
|
1872
|
-
const response = await this.client.commands.execute(command, sessionId);
|
|
1873
|
-
const duration = Date.now() - startTime;
|
|
1874
|
-
result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
|
|
1875
|
-
}
|
|
1876
|
-
if (options?.onComplete) {
|
|
1877
|
-
options.onComplete(result);
|
|
1878
|
-
}
|
|
1879
|
-
return result;
|
|
1880
|
-
} catch (error) {
|
|
1881
|
-
if (options?.onError && error instanceof Error) {
|
|
1882
|
-
options.onError(error);
|
|
1883
|
-
}
|
|
1884
|
-
throw error;
|
|
1885
|
-
} finally {
|
|
1886
|
-
if (timeoutId) {
|
|
1887
|
-
clearTimeout(timeoutId);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
async executeWithStreaming(command, sessionId, options, startTime, timestamp) {
|
|
1892
|
-
let stdout = "";
|
|
1893
|
-
let stderr = "";
|
|
1894
|
-
try {
|
|
1895
|
-
const stream = await this.client.commands.executeStream(command, sessionId);
|
|
1896
|
-
for await (const event of parseSSEStream(stream)) {
|
|
1897
|
-
if (options.signal?.aborted) {
|
|
1898
|
-
throw new Error("Operation was aborted");
|
|
1899
|
-
}
|
|
1900
|
-
switch (event.type) {
|
|
1901
|
-
case "stdout":
|
|
1902
|
-
case "stderr":
|
|
1903
|
-
if (event.data) {
|
|
1904
|
-
if (event.type === "stdout") stdout += event.data;
|
|
1905
|
-
if (event.type === "stderr") stderr += event.data;
|
|
1906
|
-
if (options.onOutput) {
|
|
1907
|
-
options.onOutput(event.type, event.data);
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
break;
|
|
1911
|
-
case "complete": {
|
|
1912
|
-
const duration = Date.now() - startTime;
|
|
1913
|
-
return {
|
|
1914
|
-
success: (event.exitCode ?? 0) === 0,
|
|
1915
|
-
exitCode: event.exitCode ?? 0,
|
|
1916
|
-
stdout,
|
|
1917
|
-
stderr,
|
|
1918
|
-
command,
|
|
1919
|
-
duration,
|
|
1920
|
-
timestamp,
|
|
1921
|
-
sessionId
|
|
1922
|
-
};
|
|
1923
|
-
}
|
|
1924
|
-
case "error":
|
|
1925
|
-
throw new Error(event.data || "Command execution failed");
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
throw new Error("Stream ended without completion event");
|
|
1929
|
-
} catch (error) {
|
|
1930
|
-
if (options.signal?.aborted) {
|
|
1931
|
-
throw new Error("Operation was aborted");
|
|
1932
|
-
}
|
|
1933
|
-
throw error;
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
mapExecuteResponseToExecResult(response, duration, sessionId) {
|
|
1937
|
-
return {
|
|
1938
|
-
success: response.success,
|
|
1939
|
-
exitCode: response.exitCode,
|
|
1940
|
-
stdout: response.stdout,
|
|
1941
|
-
stderr: response.stderr,
|
|
1942
|
-
command: response.command,
|
|
1943
|
-
duration,
|
|
1944
|
-
timestamp: response.timestamp,
|
|
1945
|
-
sessionId
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
/**
|
|
1949
|
-
* Create a Process domain object from HTTP client DTO
|
|
1950
|
-
* Centralizes process object creation with bound methods
|
|
1951
|
-
* This eliminates duplication across startProcess, listProcesses, getProcess, and session wrappers
|
|
1952
|
-
*/
|
|
1953
|
-
createProcessFromDTO(data, sessionId) {
|
|
1954
|
-
return {
|
|
1955
|
-
id: data.id,
|
|
1956
|
-
pid: data.pid,
|
|
1957
|
-
command: data.command,
|
|
1958
|
-
status: data.status,
|
|
1959
|
-
startTime: typeof data.startTime === "string" ? new Date(data.startTime) : data.startTime,
|
|
1960
|
-
endTime: data.endTime ? typeof data.endTime === "string" ? new Date(data.endTime) : data.endTime : void 0,
|
|
1961
|
-
exitCode: data.exitCode,
|
|
1962
|
-
sessionId,
|
|
1963
|
-
kill: async (signal) => {
|
|
1964
|
-
await this.killProcess(data.id, signal);
|
|
1965
|
-
},
|
|
1966
|
-
getStatus: async () => {
|
|
1967
|
-
const current = await this.getProcess(data.id);
|
|
1968
|
-
return current?.status || "error";
|
|
1969
|
-
},
|
|
1970
|
-
getLogs: async () => {
|
|
1971
|
-
const logs = await this.getProcessLogs(data.id);
|
|
1972
|
-
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
1973
|
-
}
|
|
1974
|
-
};
|
|
1975
|
-
}
|
|
1976
|
-
// Background process management
|
|
1977
|
-
async startProcess(command, options, sessionId) {
|
|
1978
|
-
try {
|
|
1979
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
1980
|
-
const response = await this.client.processes.startProcess(command, session, {
|
|
1981
|
-
processId: options?.processId
|
|
1982
|
-
});
|
|
1983
|
-
const processObj = this.createProcessFromDTO({
|
|
1984
|
-
id: response.processId,
|
|
1985
|
-
pid: response.pid,
|
|
1986
|
-
command: response.command,
|
|
1987
|
-
status: "running",
|
|
1988
|
-
startTime: /* @__PURE__ */ new Date(),
|
|
1989
|
-
endTime: void 0,
|
|
1990
|
-
exitCode: void 0
|
|
1991
|
-
}, session);
|
|
1992
|
-
if (options?.onStart) {
|
|
1993
|
-
options.onStart(processObj);
|
|
1994
|
-
}
|
|
1995
|
-
return processObj;
|
|
1996
|
-
} catch (error) {
|
|
1997
|
-
if (options?.onError && error instanceof Error) {
|
|
1998
|
-
options.onError(error);
|
|
1999
|
-
}
|
|
2000
|
-
throw error;
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
async listProcesses(sessionId) {
|
|
2004
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
2005
|
-
const response = await this.client.processes.listProcesses();
|
|
2006
|
-
return response.processes.map(
|
|
2007
|
-
(processData) => this.createProcessFromDTO({
|
|
2008
|
-
id: processData.id,
|
|
2009
|
-
pid: processData.pid,
|
|
2010
|
-
command: processData.command,
|
|
2011
|
-
status: processData.status,
|
|
2012
|
-
startTime: processData.startTime,
|
|
2013
|
-
endTime: processData.endTime,
|
|
2014
|
-
exitCode: processData.exitCode
|
|
2015
|
-
}, session)
|
|
2016
|
-
);
|
|
2017
|
-
}
|
|
2018
|
-
async getProcess(id, sessionId) {
|
|
2019
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
2020
|
-
const response = await this.client.processes.getProcess(id);
|
|
2021
|
-
if (!response.process) {
|
|
2022
|
-
return null;
|
|
2023
|
-
}
|
|
2024
|
-
const processData = response.process;
|
|
2025
|
-
return this.createProcessFromDTO({
|
|
2026
|
-
id: processData.id,
|
|
2027
|
-
pid: processData.pid,
|
|
2028
|
-
command: processData.command,
|
|
2029
|
-
status: processData.status,
|
|
2030
|
-
startTime: processData.startTime,
|
|
2031
|
-
endTime: processData.endTime,
|
|
2032
|
-
exitCode: processData.exitCode
|
|
2033
|
-
}, session);
|
|
2034
|
-
}
|
|
2035
|
-
async killProcess(id, signal, sessionId) {
|
|
2036
|
-
await this.client.processes.killProcess(id);
|
|
2037
|
-
}
|
|
2038
|
-
async killAllProcesses(sessionId) {
|
|
2039
|
-
const response = await this.client.processes.killAllProcesses();
|
|
2040
|
-
return response.cleanedCount;
|
|
2041
|
-
}
|
|
2042
|
-
async cleanupCompletedProcesses(sessionId) {
|
|
2043
|
-
return 0;
|
|
2044
|
-
}
|
|
2045
|
-
async getProcessLogs(id, sessionId) {
|
|
2046
|
-
const response = await this.client.processes.getProcessLogs(id);
|
|
2047
|
-
return {
|
|
2048
|
-
stdout: response.stdout,
|
|
2049
|
-
stderr: response.stderr,
|
|
2050
|
-
processId: response.processId
|
|
2051
|
-
};
|
|
2052
|
-
}
|
|
2053
|
-
// Streaming methods - return ReadableStream for RPC compatibility
|
|
2054
|
-
async execStream(command, options) {
|
|
2055
|
-
if (options?.signal?.aborted) {
|
|
2056
|
-
throw new Error("Operation was aborted");
|
|
2057
|
-
}
|
|
2058
|
-
const session = await this.ensureDefaultSession();
|
|
2059
|
-
return this.client.commands.executeStream(command, session);
|
|
2060
|
-
}
|
|
2061
|
-
/**
|
|
2062
|
-
* Internal session-aware execStream implementation
|
|
2063
|
-
*/
|
|
2064
|
-
async execStreamWithSession(command, sessionId, options) {
|
|
2065
|
-
if (options?.signal?.aborted) {
|
|
2066
|
-
throw new Error("Operation was aborted");
|
|
2067
|
-
}
|
|
2068
|
-
return this.client.commands.executeStream(command, sessionId);
|
|
2069
|
-
}
|
|
2070
|
-
/**
|
|
2071
|
-
* Stream logs from a background process as a ReadableStream.
|
|
2072
|
-
*/
|
|
2073
|
-
async streamProcessLogs(processId, options) {
|
|
2074
|
-
if (options?.signal?.aborted) {
|
|
2075
|
-
throw new Error("Operation was aborted");
|
|
2076
|
-
}
|
|
2077
|
-
return this.client.processes.streamProcessLogs(processId);
|
|
2078
|
-
}
|
|
2079
|
-
async gitCheckout(repoUrl, options) {
|
|
2080
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
2081
|
-
return this.client.git.checkout(repoUrl, session, {
|
|
2082
|
-
branch: options.branch,
|
|
2083
|
-
targetDir: options.targetDir
|
|
2084
|
-
});
|
|
2085
|
-
}
|
|
2086
|
-
async mkdir(path, options = {}) {
|
|
2087
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
2088
|
-
return this.client.files.mkdir(path, session, { recursive: options.recursive });
|
|
2089
|
-
}
|
|
2090
|
-
async writeFile(path, content, options = {}) {
|
|
2091
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
2092
|
-
return this.client.files.writeFile(path, content, session, { encoding: options.encoding });
|
|
2093
|
-
}
|
|
2094
|
-
async deleteFile(path, sessionId) {
|
|
2095
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
2096
|
-
return this.client.files.deleteFile(path, session);
|
|
2097
|
-
}
|
|
2098
|
-
async renameFile(oldPath, newPath, sessionId) {
|
|
2099
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
2100
|
-
return this.client.files.renameFile(oldPath, newPath, session);
|
|
2101
|
-
}
|
|
2102
|
-
async moveFile(sourcePath, destinationPath, sessionId) {
|
|
2103
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
2104
|
-
return this.client.files.moveFile(sourcePath, destinationPath, session);
|
|
2105
|
-
}
|
|
2106
|
-
async readFile(path, options = {}) {
|
|
2107
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
2108
|
-
return this.client.files.readFile(path, session, { encoding: options.encoding });
|
|
2109
|
-
}
|
|
2110
|
-
/**
|
|
2111
|
-
* Stream a file from the sandbox using Server-Sent Events
|
|
2112
|
-
* Returns a ReadableStream that can be consumed with streamFile() or collectFile() utilities
|
|
2113
|
-
* @param path - Path to the file to stream
|
|
2114
|
-
* @param options - Optional session ID
|
|
2115
|
-
*/
|
|
2116
|
-
async readFileStream(path, options = {}) {
|
|
2117
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
2118
|
-
return this.client.files.readFileStream(path, session);
|
|
2119
|
-
}
|
|
2120
|
-
async listFiles(path, options) {
|
|
2121
|
-
const session = await this.ensureDefaultSession();
|
|
2122
|
-
return this.client.files.listFiles(path, session, options);
|
|
2123
|
-
}
|
|
2124
|
-
async exists(path, sessionId) {
|
|
2125
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
2126
|
-
return this.client.files.exists(path, session);
|
|
2127
|
-
}
|
|
2128
|
-
async exposePort(port, options) {
|
|
2129
|
-
if (options.hostname.endsWith(".workers.dev")) {
|
|
2130
|
-
const errorResponse = {
|
|
2131
|
-
code: ErrorCode.CUSTOM_DOMAIN_REQUIRED,
|
|
2132
|
-
message: `Port exposure requires a custom domain. .workers.dev domains do not support wildcard subdomains required for port proxying.`,
|
|
2133
|
-
context: { originalError: options.hostname },
|
|
2134
|
-
httpStatus: 400,
|
|
2135
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2136
|
-
};
|
|
2137
|
-
throw new CustomDomainRequiredError(errorResponse);
|
|
2138
|
-
}
|
|
2139
|
-
const sessionId = await this.ensureDefaultSession();
|
|
2140
|
-
await this.client.ports.exposePort(port, sessionId, options?.name);
|
|
2141
|
-
if (!this.sandboxName) {
|
|
2142
|
-
throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
|
|
2143
|
-
}
|
|
2144
|
-
const token = this.generatePortToken();
|
|
2145
|
-
this.portTokens.set(port, token);
|
|
2146
|
-
await this.persistPortTokens();
|
|
2147
|
-
const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname, token);
|
|
2148
|
-
return {
|
|
2149
|
-
url,
|
|
2150
|
-
port,
|
|
2151
|
-
name: options?.name
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
async unexposePort(port) {
|
|
2155
|
-
if (!validatePort(port)) {
|
|
2156
|
-
throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
|
|
2157
|
-
}
|
|
2158
|
-
const sessionId = await this.ensureDefaultSession();
|
|
2159
|
-
await this.client.ports.unexposePort(port, sessionId);
|
|
2160
|
-
if (this.portTokens.has(port)) {
|
|
2161
|
-
this.portTokens.delete(port);
|
|
2162
|
-
await this.persistPortTokens();
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
async getExposedPorts(hostname) {
|
|
2166
|
-
const sessionId = await this.ensureDefaultSession();
|
|
2167
|
-
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
2168
|
-
if (!this.sandboxName) {
|
|
2169
|
-
throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
|
|
2170
|
-
}
|
|
2171
|
-
return response.ports.map((port) => {
|
|
2172
|
-
const token = this.portTokens.get(port.port);
|
|
2173
|
-
if (!token) {
|
|
2174
|
-
throw new Error(`Port ${port.port} is exposed but has no token. This should not happen.`);
|
|
2175
|
-
}
|
|
2176
|
-
return {
|
|
2177
|
-
url: this.constructPreviewUrl(port.port, this.sandboxName, hostname, token),
|
|
2178
|
-
port: port.port,
|
|
2179
|
-
status: port.status
|
|
2180
|
-
};
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
|
-
async isPortExposed(port) {
|
|
2184
|
-
try {
|
|
2185
|
-
const sessionId = await this.ensureDefaultSession();
|
|
2186
|
-
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
2187
|
-
return response.ports.some((exposedPort) => exposedPort.port === port);
|
|
2188
|
-
} catch (error) {
|
|
2189
|
-
this.logger.error("Error checking if port is exposed", error instanceof Error ? error : new Error(String(error)), { port });
|
|
2190
|
-
return false;
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
async validatePortToken(port, token) {
|
|
2194
|
-
const isExposed = await this.isPortExposed(port);
|
|
2195
|
-
if (!isExposed) {
|
|
2196
|
-
return false;
|
|
2197
|
-
}
|
|
2198
|
-
const storedToken = this.portTokens.get(port);
|
|
2199
|
-
if (!storedToken) {
|
|
2200
|
-
this.logger.error("Port is exposed but has no token - bug detected", void 0, { port });
|
|
2201
|
-
return false;
|
|
2202
|
-
}
|
|
2203
|
-
return storedToken === token;
|
|
2204
|
-
}
|
|
2205
|
-
generatePortToken() {
|
|
2206
|
-
const array = new Uint8Array(12);
|
|
2207
|
-
crypto.getRandomValues(array);
|
|
2208
|
-
const base64 = btoa(String.fromCharCode(...array));
|
|
2209
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "").toLowerCase();
|
|
2210
|
-
}
|
|
2211
|
-
async persistPortTokens() {
|
|
2212
|
-
const tokensObj = {};
|
|
2213
|
-
for (const [port, token] of this.portTokens.entries()) {
|
|
2214
|
-
tokensObj[port.toString()] = token;
|
|
2215
|
-
}
|
|
2216
|
-
await this.ctx.storage.put("portTokens", tokensObj);
|
|
2217
|
-
}
|
|
2218
|
-
constructPreviewUrl(port, sandboxId, hostname, token) {
|
|
2219
|
-
if (!validatePort(port)) {
|
|
2220
|
-
throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
|
|
2221
|
-
}
|
|
2222
|
-
const sanitizedSandboxId = sanitizeSandboxId(sandboxId);
|
|
2223
|
-
const isLocalhost = isLocalhostPattern(hostname);
|
|
2224
|
-
if (isLocalhost) {
|
|
2225
|
-
const [host, portStr] = hostname.split(":");
|
|
2226
|
-
const mainPort = portStr || "80";
|
|
2227
|
-
try {
|
|
2228
|
-
const baseUrl = new URL(`http://${host}:${mainPort}`);
|
|
2229
|
-
const subdomainHost = `${port}-${sanitizedSandboxId}-${token}.${host}`;
|
|
2230
|
-
baseUrl.hostname = subdomainHost;
|
|
2231
|
-
return baseUrl.toString();
|
|
2232
|
-
} catch (error) {
|
|
2233
|
-
throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
try {
|
|
2237
|
-
const protocol = "https";
|
|
2238
|
-
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
2239
|
-
const subdomainHost = `${port}-${sanitizedSandboxId}-${token}.${hostname}`;
|
|
2240
|
-
baseUrl.hostname = subdomainHost;
|
|
2241
|
-
return baseUrl.toString();
|
|
2242
|
-
} catch (error) {
|
|
2243
|
-
throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
// ============================================================================
|
|
2247
|
-
// Session Management - Advanced Use Cases
|
|
2248
|
-
// ============================================================================
|
|
2249
|
-
/**
|
|
2250
|
-
* Create isolated execution session for advanced use cases
|
|
2251
|
-
* Returns ExecutionSession with full sandbox API bound to specific session
|
|
2252
|
-
*/
|
|
2253
|
-
async createSession(options) {
|
|
2254
|
-
const sessionId = options?.id || `session-${Date.now()}`;
|
|
2255
|
-
await this.client.utils.createSession({
|
|
2256
|
-
id: sessionId,
|
|
2257
|
-
env: options?.env,
|
|
2258
|
-
cwd: options?.cwd
|
|
2259
|
-
});
|
|
2260
|
-
return this.getSessionWrapper(sessionId);
|
|
2261
|
-
}
|
|
2262
|
-
/**
|
|
2263
|
-
* Get an existing session by ID
|
|
2264
|
-
* Returns ExecutionSession wrapper bound to the specified session
|
|
2265
|
-
*
|
|
2266
|
-
* This is useful for retrieving sessions across different requests/contexts
|
|
2267
|
-
* without storing the ExecutionSession object (which has RPC lifecycle limitations)
|
|
2268
|
-
*
|
|
2269
|
-
* @param sessionId - The ID of an existing session
|
|
2270
|
-
* @returns ExecutionSession wrapper bound to the session
|
|
2271
|
-
*/
|
|
2272
|
-
async getSession(sessionId) {
|
|
2273
|
-
return this.getSessionWrapper(sessionId);
|
|
2274
|
-
}
|
|
2275
|
-
/**
|
|
2276
|
-
* Internal helper to create ExecutionSession wrapper for a given sessionId
|
|
2277
|
-
* Used by both createSession and getSession
|
|
2278
|
-
*/
|
|
2279
|
-
getSessionWrapper(sessionId) {
|
|
2280
|
-
return {
|
|
2281
|
-
id: sessionId,
|
|
2282
|
-
// Command execution - delegate to internal session-aware methods
|
|
2283
|
-
exec: (command, options) => this.execWithSession(command, sessionId, options),
|
|
2284
|
-
execStream: (command, options) => this.execStreamWithSession(command, sessionId, options),
|
|
2285
|
-
// Process management
|
|
2286
|
-
startProcess: (command, options) => this.startProcess(command, options, sessionId),
|
|
2287
|
-
listProcesses: () => this.listProcesses(sessionId),
|
|
2288
|
-
getProcess: (id) => this.getProcess(id, sessionId),
|
|
2289
|
-
killProcess: (id, signal) => this.killProcess(id, signal),
|
|
2290
|
-
killAllProcesses: () => this.killAllProcesses(),
|
|
2291
|
-
cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
|
|
2292
|
-
getProcessLogs: (id) => this.getProcessLogs(id),
|
|
2293
|
-
streamProcessLogs: (processId, options) => this.streamProcessLogs(processId, options),
|
|
2294
|
-
// File operations - pass sessionId via options or parameter
|
|
2295
|
-
writeFile: (path, content, options) => this.writeFile(path, content, { ...options, sessionId }),
|
|
2296
|
-
readFile: (path, options) => this.readFile(path, { ...options, sessionId }),
|
|
2297
|
-
readFileStream: (path) => this.readFileStream(path, { sessionId }),
|
|
2298
|
-
mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
|
|
2299
|
-
deleteFile: (path) => this.deleteFile(path, sessionId),
|
|
2300
|
-
renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId),
|
|
2301
|
-
moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId),
|
|
2302
|
-
listFiles: (path, options) => this.client.files.listFiles(path, sessionId, options),
|
|
2303
|
-
exists: (path) => this.exists(path, sessionId),
|
|
2304
|
-
// Git operations
|
|
2305
|
-
gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, { ...options, sessionId }),
|
|
2306
|
-
// Environment management - needs special handling
|
|
2307
|
-
setEnvVars: async (envVars) => {
|
|
2308
|
-
try {
|
|
2309
|
-
for (const [key, value] of Object.entries(envVars)) {
|
|
2310
|
-
const escapedValue = value.replace(/'/g, "'\\''");
|
|
2311
|
-
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
2312
|
-
const result = await this.client.commands.execute(exportCommand, sessionId);
|
|
2313
|
-
if (result.exitCode !== 0) {
|
|
2314
|
-
throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
} catch (error) {
|
|
2318
|
-
this.logger.error("Failed to set environment variables", error instanceof Error ? error : new Error(String(error)), { sessionId });
|
|
2319
|
-
throw error;
|
|
2320
|
-
}
|
|
2321
|
-
},
|
|
2322
|
-
// Code interpreter methods - delegate to sandbox's code interpreter
|
|
2323
|
-
createCodeContext: (options) => this.codeInterpreter.createCodeContext(options),
|
|
2324
|
-
runCode: async (code, options) => {
|
|
2325
|
-
const execution = await this.codeInterpreter.runCode(code, options);
|
|
2326
|
-
return execution.toJSON();
|
|
2327
|
-
},
|
|
2328
|
-
runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options),
|
|
2329
|
-
listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
|
|
2330
|
-
deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId)
|
|
2331
|
-
};
|
|
2332
|
-
}
|
|
2333
|
-
// ============================================================================
|
|
2334
|
-
// Code interpreter methods - delegate to CodeInterpreter wrapper
|
|
2335
|
-
// ============================================================================
|
|
2336
|
-
async createCodeContext(options) {
|
|
2337
|
-
return this.codeInterpreter.createCodeContext(options);
|
|
2338
|
-
}
|
|
2339
|
-
async runCode(code, options) {
|
|
2340
|
-
const execution = await this.codeInterpreter.runCode(code, options);
|
|
2341
|
-
return execution.toJSON();
|
|
2342
|
-
}
|
|
2343
|
-
async runCodeStream(code, options) {
|
|
2344
|
-
return this.codeInterpreter.runCodeStream(code, options);
|
|
2345
|
-
}
|
|
2346
|
-
async listCodeContexts() {
|
|
2347
|
-
return this.codeInterpreter.listCodeContexts();
|
|
2348
|
-
}
|
|
2349
|
-
async deleteCodeContext(contextId) {
|
|
2350
|
-
return this.codeInterpreter.deleteCodeContext(contextId);
|
|
2351
|
-
}
|
|
2352
|
-
};
|
|
2353
|
-
|
|
2354
|
-
// src/request-handler.ts
|
|
2355
|
-
async function proxyToSandbox(request, env) {
|
|
2356
|
-
const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate();
|
|
2357
|
-
const logger = createLogger({
|
|
2358
|
-
component: "sandbox-do",
|
|
2359
|
-
traceId,
|
|
2360
|
-
operation: "proxy"
|
|
2361
|
-
});
|
|
2362
|
-
try {
|
|
2363
|
-
const url = new URL(request.url);
|
|
2364
|
-
const routeInfo = extractSandboxRoute(url);
|
|
2365
|
-
if (!routeInfo) {
|
|
2366
|
-
return null;
|
|
2367
|
-
}
|
|
2368
|
-
const { sandboxId, port, path, token } = routeInfo;
|
|
2369
|
-
const sandbox = getSandbox(env.Sandbox, sandboxId);
|
|
2370
|
-
if (port !== 3e3) {
|
|
2371
|
-
const isValidToken = await sandbox.validatePortToken(port, token);
|
|
2372
|
-
if (!isValidToken) {
|
|
2373
|
-
logger.warn("Invalid token access blocked", {
|
|
2374
|
-
port,
|
|
2375
|
-
sandboxId,
|
|
2376
|
-
path,
|
|
2377
|
-
hostname: url.hostname,
|
|
2378
|
-
url: request.url,
|
|
2379
|
-
method: request.method,
|
|
2380
|
-
userAgent: request.headers.get("User-Agent") || "unknown"
|
|
2381
|
-
});
|
|
2382
|
-
return new Response(
|
|
2383
|
-
JSON.stringify({
|
|
2384
|
-
error: `Access denied: Invalid token or port not exposed`,
|
|
2385
|
-
code: "INVALID_TOKEN"
|
|
2386
|
-
}),
|
|
2387
|
-
{
|
|
2388
|
-
status: 404,
|
|
2389
|
-
headers: {
|
|
2390
|
-
"Content-Type": "application/json"
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
);
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
const upgradeHeader = request.headers.get("Upgrade");
|
|
2397
|
-
if (upgradeHeader?.toLowerCase() === "websocket") {
|
|
2398
|
-
return await sandbox.fetch(switchPort(request, port));
|
|
2399
|
-
}
|
|
2400
|
-
let proxyUrl;
|
|
2401
|
-
if (port !== 3e3) {
|
|
2402
|
-
proxyUrl = `http://localhost:${port}${path}${url.search}`;
|
|
2403
|
-
} else {
|
|
2404
|
-
proxyUrl = `http://localhost:3000${path}${url.search}`;
|
|
2405
|
-
}
|
|
2406
|
-
const proxyRequest = new Request(proxyUrl, {
|
|
2407
|
-
method: request.method,
|
|
2408
|
-
headers: {
|
|
2409
|
-
...Object.fromEntries(request.headers),
|
|
2410
|
-
"X-Original-URL": request.url,
|
|
2411
|
-
"X-Forwarded-Host": url.hostname,
|
|
2412
|
-
"X-Forwarded-Proto": url.protocol.replace(":", ""),
|
|
2413
|
-
"X-Sandbox-Name": sandboxId
|
|
2414
|
-
// Pass the friendly name
|
|
2415
|
-
},
|
|
2416
|
-
body: request.body,
|
|
2417
|
-
// @ts-expect-error - duplex required for body streaming in modern runtimes
|
|
2418
|
-
duplex: "half"
|
|
2419
|
-
});
|
|
2420
|
-
return await sandbox.containerFetch(proxyRequest, port);
|
|
2421
|
-
} catch (error) {
|
|
2422
|
-
logger.error("Proxy routing error", error instanceof Error ? error : new Error(String(error)));
|
|
2423
|
-
return new Response("Proxy routing error", { status: 500 });
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
function extractSandboxRoute(url) {
|
|
2427
|
-
const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/);
|
|
2428
|
-
if (!subdomainMatch) {
|
|
2429
|
-
return null;
|
|
2430
|
-
}
|
|
2431
|
-
const portStr = subdomainMatch[1];
|
|
2432
|
-
const sandboxId = subdomainMatch[2];
|
|
2433
|
-
const token = subdomainMatch[3];
|
|
2434
|
-
const domain = subdomainMatch[4];
|
|
2435
|
-
const port = parseInt(portStr, 10);
|
|
2436
|
-
if (!validatePort(port)) {
|
|
2437
|
-
return null;
|
|
2438
|
-
}
|
|
2439
|
-
let sanitizedSandboxId;
|
|
2440
|
-
try {
|
|
2441
|
-
sanitizedSandboxId = sanitizeSandboxId(sandboxId);
|
|
2442
|
-
} catch (error) {
|
|
2443
|
-
return null;
|
|
2444
|
-
}
|
|
2445
|
-
if (sandboxId.length > 63) {
|
|
2446
|
-
return null;
|
|
2447
|
-
}
|
|
2448
|
-
return {
|
|
2449
|
-
port,
|
|
2450
|
-
sandboxId: sanitizedSandboxId,
|
|
2451
|
-
path: url.pathname || "/",
|
|
2452
|
-
token
|
|
2453
|
-
};
|
|
2454
|
-
}
|
|
2455
|
-
function isLocalhostPattern(hostname) {
|
|
2456
|
-
if (hostname.startsWith("[")) {
|
|
2457
|
-
if (hostname.includes("]:")) {
|
|
2458
|
-
const ipv6Part = hostname.substring(0, hostname.indexOf("]:") + 1);
|
|
2459
|
-
return ipv6Part === "[::1]";
|
|
2460
|
-
} else {
|
|
2461
|
-
return hostname === "[::1]";
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
if (hostname === "::1") {
|
|
2465
|
-
return true;
|
|
2466
|
-
}
|
|
2467
|
-
const hostPart = hostname.split(":")[0];
|
|
2468
|
-
return hostPart === "localhost" || hostPart === "127.0.0.1" || hostPart === "0.0.0.0";
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
export {
|
|
2472
|
-
CommandClient,
|
|
2473
|
-
FileClient,
|
|
2474
|
-
GitClient,
|
|
2475
|
-
PortClient,
|
|
2476
|
-
ProcessClient,
|
|
2477
|
-
UtilityClient,
|
|
2478
|
-
SandboxClient,
|
|
2479
|
-
proxyToSandbox,
|
|
2480
|
-
isLocalhostPattern,
|
|
2481
|
-
getSandbox,
|
|
2482
|
-
Sandbox
|
|
2483
|
-
};
|
|
2484
|
-
//# sourceMappingURL=chunk-YE265ASX.js.map
|