@agent-relay/sandbox 0.0.0 → 0.1.0
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/README.md +8 -4
- package/dist/daytona/runtime.d.ts +117 -0
- package/dist/daytona/runtime.d.ts.map +1 -0
- package/dist/daytona/runtime.js +765 -0
- package/dist/daytona/runtime.js.map +1 -0
- package/dist/e2b/runtime.d.ts +167 -0
- package/dist/e2b/runtime.d.ts.map +1 -0
- package/dist/e2b/runtime.js +362 -0
- package/dist/e2b/runtime.js.map +1 -0
- package/dist/index.d.ts +26 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/local/runtime.d.ts +63 -0
- package/dist/local/runtime.d.ts.map +1 -0
- package/dist/local/runtime.js +315 -0
- package/dist/local/runtime.js.map +1 -0
- package/dist/mount-script.d.ts +162 -0
- package/dist/mount-script.d.ts.map +1 -0
- package/dist/mount-script.js +461 -0
- package/dist/mount-script.js.map +1 -0
- package/dist/orchestrator.d.ts +110 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +553 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/port.d.ts +126 -0
- package/dist/port.d.ts.map +1 -0
- package/dist/port.js +35 -0
- package/dist/port.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +16 -1
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
export class SnapshotNotFoundError extends Error {
|
|
2
|
+
snapshot;
|
|
3
|
+
constructor(snapshot, cause) {
|
|
4
|
+
super(`Snapshot not found in Daytona: '${snapshot}'. Refusing silent fallback to typescript base — fix DEFAULT_SNAPSHOT or rebuild/publish the snapshot before retrying.`, { cause });
|
|
5
|
+
this.name = 'SnapshotNotFoundError';
|
|
6
|
+
this.snapshot = snapshot;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
// Upper bound on how many trailing bytes of a run's captured log file
|
|
10
|
+
// getScriptLogs pulls back into the polling process. A long run can emit MBs
|
|
11
|
+
// of stdout; `tail -c` bounds the read at the source so the poller never
|
|
12
|
+
// buffers the whole file. The failure is almost always at the END of the run,
|
|
13
|
+
// so the trailing bytes are the useful ones.
|
|
14
|
+
const SCRIPT_LOG_READ_MAX_BYTES = 262_144; // 256 KiB
|
|
15
|
+
const DEFAULT_DAYTONA_LOOKUP_TIMEOUT_MS = 10_000;
|
|
16
|
+
export class DaytonaRuntime {
|
|
17
|
+
id = 'daytona';
|
|
18
|
+
capabilities = {
|
|
19
|
+
pty: false,
|
|
20
|
+
snapshots: true,
|
|
21
|
+
isolation: 'strong',
|
|
22
|
+
persistentHandle: true,
|
|
23
|
+
streamingLogs: true,
|
|
24
|
+
};
|
|
25
|
+
sandboxes = new Map();
|
|
26
|
+
daytona;
|
|
27
|
+
snapshot;
|
|
28
|
+
defaultHomeDir;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.daytona = options.daytona;
|
|
31
|
+
this.snapshot = options.snapshot;
|
|
32
|
+
this.defaultHomeDir = options.defaultHomeDir;
|
|
33
|
+
}
|
|
34
|
+
async launch(options = {}) {
|
|
35
|
+
const sandbox = await this.createSandbox(options);
|
|
36
|
+
const homeDir = await this.resolveHomeDir(sandbox);
|
|
37
|
+
return this.registerSandbox(sandbox, {
|
|
38
|
+
owned: true,
|
|
39
|
+
homeDir,
|
|
40
|
+
workdir: options.workdir,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async launchDetached(options = {}) {
|
|
44
|
+
const sandbox = await this.createSandboxDetached(options);
|
|
45
|
+
if (isRuntimeHandle(sandbox)) {
|
|
46
|
+
return {
|
|
47
|
+
...sandbox,
|
|
48
|
+
...(options.workdir ? { workdir: options.workdir } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return this.registerSandbox(sandbox, {
|
|
52
|
+
owned: true,
|
|
53
|
+
workdir: options.workdir,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async getById(id, options = {}) {
|
|
57
|
+
let sandbox;
|
|
58
|
+
try {
|
|
59
|
+
sandbox = await this.daytona.get(id);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (isDaytonaNotFoundError(error)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const states = options.states === undefined ? null : options.states;
|
|
68
|
+
if (!this.matchesState(sandbox, states)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return this.registerSandbox(sandbox, {
|
|
72
|
+
owned: options.owned ?? false,
|
|
73
|
+
homeDir: options.homeDir,
|
|
74
|
+
workdir: options.workdir,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async findByLabels(labels, options = {}) {
|
|
78
|
+
const limit = options.limit ?? options.pageSize ?? 10;
|
|
79
|
+
const states = options.states === undefined ? ['STARTED'] : options.states;
|
|
80
|
+
const excludedIds = new Set(options.excludeIds ?? []);
|
|
81
|
+
const deadline = lookupDeadline(options.timeoutMs);
|
|
82
|
+
const iterator = this.listSandboxes(labels, { limit, states });
|
|
83
|
+
try {
|
|
84
|
+
for (;;) {
|
|
85
|
+
const next = await awaitLookupOperation(iterator.next(), deadline, 'listing matching sandboxes');
|
|
86
|
+
if (next.done) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const listedSandbox = next.value;
|
|
90
|
+
if (!this.matchesState(listedSandbox, states) || excludedIds.has(listedSandbox.id)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Daytona SDK 0.180 shares mutable client configuration across list()
|
|
94
|
+
// results. Rehydrate only the candidate we are about to return so its
|
|
95
|
+
// filesystem and process clients are pinned to the same sandbox ID.
|
|
96
|
+
let sandbox;
|
|
97
|
+
try {
|
|
98
|
+
sandbox = await awaitLookupOperation(this.daytona.get(listedSandbox.id), deadline, `rehydrating sandbox ${listedSandbox.id}`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (isDaytonaNotFoundError(error)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
if (!this.matchesState(sandbox, states)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const homeDir = options.homeDir ?? await awaitLookupOperation(this.resolveHomeDir(sandbox), deadline, `resolving sandbox ${sandbox.id} home directory`);
|
|
110
|
+
return this.registerSandbox(sandbox, {
|
|
111
|
+
owned: options.owned ?? false,
|
|
112
|
+
homeDir,
|
|
113
|
+
workdir: options.workdir,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
closeAsyncIteratorBestEffort(iterator);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async countByLabels(labels, options = {}) {
|
|
122
|
+
const limit = options.limit ?? options.pageSize ?? 10;
|
|
123
|
+
const states = options.states === undefined ? ['STARTED'] : options.states;
|
|
124
|
+
const maxCount = options.maxCount === undefined
|
|
125
|
+
? Number.POSITIVE_INFINITY
|
|
126
|
+
: Math.max(0, Math.floor(options.maxCount));
|
|
127
|
+
if (maxCount === 0) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const deadline = lookupDeadline(options.timeoutMs);
|
|
131
|
+
const iterator = this.listSandboxes(labels, { limit, states });
|
|
132
|
+
let count = 0;
|
|
133
|
+
try {
|
|
134
|
+
for (;;) {
|
|
135
|
+
const next = await awaitLookupOperation(iterator.next(), deadline, 'counting matching sandboxes');
|
|
136
|
+
if (next.done) {
|
|
137
|
+
return count;
|
|
138
|
+
}
|
|
139
|
+
if (!this.matchesState(next.value, states)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
count += 1;
|
|
143
|
+
if (count >= maxCount) {
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
closeAsyncIteratorBestEffort(iterator);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async findAllByLabels(labels, options = {}) {
|
|
153
|
+
const limit = options.limit ?? options.pageSize ?? 10;
|
|
154
|
+
const states = options.states === undefined ? ['STARTED'] : options.states;
|
|
155
|
+
const handles = [];
|
|
156
|
+
for await (const listedSandbox of this.listSandboxes(labels, { limit, states })) {
|
|
157
|
+
if (!this.matchesState(listedSandbox, states)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Daytona SDK 0.180 reuses one mutable client config for every Sandbox
|
|
161
|
+
// yielded by list() (Daytona.js:375,408). Each Sandbox constructor
|
|
162
|
+
// rewrites its basePath (Sandbox.js:198-199), so serverless filesystem
|
|
163
|
+
// uploads read the last-listed sandbox ID (FileSystem.js:512) while
|
|
164
|
+
// process commands remain bound to the earlier sandbox. get() clones the
|
|
165
|
+
// config per Sandbox (Daytona.js:356); rehydrate before keeping any
|
|
166
|
+
// listed result so upload, verification, and execution share one ID.
|
|
167
|
+
let sandbox;
|
|
168
|
+
try {
|
|
169
|
+
sandbox = await this.daytona.get(listedSandbox.id);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (isDaytonaNotFoundError(error)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
if (!this.matchesState(sandbox, states)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
handles.push(this.registerSandbox(sandbox, {
|
|
181
|
+
owned: options.owned ?? false,
|
|
182
|
+
homeDir: options.homeDir,
|
|
183
|
+
workdir: options.workdir,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
return handles;
|
|
187
|
+
}
|
|
188
|
+
attachSandbox(sandbox, options = {}) {
|
|
189
|
+
return this.registerSandbox(sandbox, {
|
|
190
|
+
owned: options.owned ?? false,
|
|
191
|
+
homeDir: options.homeDir,
|
|
192
|
+
workdir: options.workdir,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async exec(handle, command, options = {}) {
|
|
196
|
+
const sandbox = this.requireSandbox(handle);
|
|
197
|
+
const result = await sandbox.process.executeCommand(command, options.cwd, options.env, this.msToSeconds(options.timeoutMs));
|
|
198
|
+
return {
|
|
199
|
+
output: result.result ?? '',
|
|
200
|
+
exitCode: result.exitCode ?? 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async runScript(handle, options) {
|
|
204
|
+
const sandbox = this.requireSandbox(handle);
|
|
205
|
+
const command = this.buildScriptCommand(options);
|
|
206
|
+
const timeoutSeconds = this.msToSeconds(options.timeoutMs);
|
|
207
|
+
const useSession = options.useSession ?? true;
|
|
208
|
+
if (useSession) {
|
|
209
|
+
if (!this.supportsSessionExec(sandbox)) {
|
|
210
|
+
throw new Error('Daytona session execution is not available on this sandbox');
|
|
211
|
+
}
|
|
212
|
+
const sessionId = options.sessionId ?? `run-${handle.id}-${Date.now()}`;
|
|
213
|
+
await sandbox.process.createSession(sessionId);
|
|
214
|
+
const result = await sandbox.process.executeSessionCommand(sessionId, {
|
|
215
|
+
command,
|
|
216
|
+
runAsync: false,
|
|
217
|
+
suppressInputEcho: options.suppressInputEcho,
|
|
218
|
+
}, timeoutSeconds);
|
|
219
|
+
return {
|
|
220
|
+
output: result.output ?? result.stdout ?? result.stderr ?? '',
|
|
221
|
+
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
222
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
223
|
+
exitCode: typeof result.exitCode === 'number' ? result.exitCode : null,
|
|
224
|
+
...(result.cmdId ? { cmdId: result.cmdId } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const result = await sandbox.process.executeCommand(command, undefined, undefined, timeoutSeconds);
|
|
228
|
+
return {
|
|
229
|
+
output: result.result ?? result.artifacts?.stdout ?? '',
|
|
230
|
+
...(result.artifacts?.stdout ? { stdout: result.artifacts.stdout } : {}),
|
|
231
|
+
exitCode: typeof result.exitCode === 'number' ? result.exitCode : null,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async startScript(handle, options) {
|
|
235
|
+
const sandbox = this.requireSandbox(handle);
|
|
236
|
+
if (!this.supportsSessionExec(sandbox)) {
|
|
237
|
+
throw new Error('Daytona session execution is not available on this sandbox');
|
|
238
|
+
}
|
|
239
|
+
const sessionId = options.sessionId ?? `run-${handle.id}-${Date.now()}`;
|
|
240
|
+
await sandbox.process.createSession(sessionId);
|
|
241
|
+
// Capture the run's combined stdout+stderr to a per-session log file.
|
|
242
|
+
//
|
|
243
|
+
// Daytona's REST `getSessionCommandLogs` snapshot returns EMPTY for
|
|
244
|
+
// `runAsync: true` commands — the command RECORD keeps the exitCode (so
|
|
245
|
+
// getScriptStatus works), but the log BODY is only retrievable via the
|
|
246
|
+
// follow=true WebSocket stream, which the SDK implements with
|
|
247
|
+
// `isomorphic-ws` → node `ws`, so it does NOT run on edge runtimes without
|
|
248
|
+
// WebSocket client support — which is where these runs are typically
|
|
249
|
+
// polled from. Without capture, every poll reads empty output and a
|
|
250
|
+
// failing run surfaces only as the bare "runner.mjs failed" fallback
|
|
251
|
+
// string.
|
|
252
|
+
//
|
|
253
|
+
// A command group redirects only this run's script and leaves the session's
|
|
254
|
+
// stdout/stderr intact for the later synchronous `tail` read. Its exit code
|
|
255
|
+
// is the exit code of the last command in the group, so this still preserves
|
|
256
|
+
// the script status. getScriptLogs reads this file back.
|
|
257
|
+
const logPath = this.scriptLogPath(sessionId);
|
|
258
|
+
const command = `{\n${this.buildScriptCommand(options)}\n} > ${shellSingleQuote(logPath)} 2>&1`;
|
|
259
|
+
const result = await sandbox.process.executeSessionCommand(sessionId, {
|
|
260
|
+
command,
|
|
261
|
+
runAsync: true,
|
|
262
|
+
suppressInputEcho: options.suppressInputEcho,
|
|
263
|
+
}, this.msToSeconds(options.timeoutMs));
|
|
264
|
+
if (!result.cmdId) {
|
|
265
|
+
throw new Error('Daytona async session command did not return a command id');
|
|
266
|
+
}
|
|
267
|
+
return { sessionId, commandId: result.cmdId };
|
|
268
|
+
}
|
|
269
|
+
async getScriptStatus(handle, sessionId, commandId) {
|
|
270
|
+
const sandbox = this.requireSandbox(handle);
|
|
271
|
+
if (!this.supportsSessionExec(sandbox)) {
|
|
272
|
+
throw new Error('Daytona session execution is not available on this sandbox');
|
|
273
|
+
}
|
|
274
|
+
const command = await sandbox.process.getSessionCommand(sessionId, commandId);
|
|
275
|
+
return {
|
|
276
|
+
exitCode: typeof command.exitCode === 'number' ? command.exitCode : null,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async getScriptLogs(handle, sessionId, commandId) {
|
|
280
|
+
const sandbox = this.requireSandbox(handle);
|
|
281
|
+
if (!this.supportsSessionExec(sandbox)) {
|
|
282
|
+
throw new Error('Daytona session execution is not available on this sandbox');
|
|
283
|
+
}
|
|
284
|
+
const logs = await sandbox.process.getSessionCommandLogs(sessionId, commandId);
|
|
285
|
+
let output = logs.output ?? logs.stdout ?? logs.stderr ?? '';
|
|
286
|
+
// Fallback for runAsync commands whose snapshot logs come back empty (see
|
|
287
|
+
// startScript): read the per-session redirect file we captured. Bounded at
|
|
288
|
+
// the source with `tail -c` so a multi-MB run can't pull the whole file
|
|
289
|
+
// into the poller. The read uses the sync (runAsync:false) path, which
|
|
290
|
+
// returns output inline over REST and works on runtimes without WebSocket
|
|
291
|
+
// support. Best-effort: a recycled sandbox / missing file / closed session
|
|
292
|
+
// yields empty — exactly the same blind state as before, never a throw.
|
|
293
|
+
if (!output) {
|
|
294
|
+
try {
|
|
295
|
+
const logPath = this.scriptLogPath(sessionId);
|
|
296
|
+
const fileLogs = await sandbox.process.executeSessionCommand(sessionId, {
|
|
297
|
+
command: `tail -c ${SCRIPT_LOG_READ_MAX_BYTES} ${shellSingleQuote(logPath)} 2>/dev/null || true`,
|
|
298
|
+
runAsync: false,
|
|
299
|
+
});
|
|
300
|
+
output = fileLogs.output ?? fileLogs.stdout ?? fileLogs.stderr ?? '';
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// best-effort; keep the empty snapshot result
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
output,
|
|
308
|
+
...(logs.stdout ? { stdout: logs.stdout } : {}),
|
|
309
|
+
...(logs.stderr ? { stderr: logs.stderr } : {}),
|
|
310
|
+
exitCode: null,
|
|
311
|
+
cmdId: commandId,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
startExec(handle, command, options = {}) {
|
|
315
|
+
return this.startScript(handle, {
|
|
316
|
+
command,
|
|
317
|
+
sessionId: options.sessionId,
|
|
318
|
+
timeoutMs: options.timeoutMs,
|
|
319
|
+
env: options.env,
|
|
320
|
+
useSession: true,
|
|
321
|
+
suppressInputEcho: true,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
getExecStatus(handle, sessionId, commandId) {
|
|
325
|
+
return this.getScriptStatus(handle, sessionId, commandId);
|
|
326
|
+
}
|
|
327
|
+
async getExecLogs(handle, sessionId, commandId) {
|
|
328
|
+
const logs = await this.getScriptLogs(handle, sessionId, commandId);
|
|
329
|
+
return {
|
|
330
|
+
output: logs.output,
|
|
331
|
+
exitCode: logs.exitCode ?? 0,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async uploadFile(handle, source, destination) {
|
|
335
|
+
const sandbox = this.requireSandbox(handle);
|
|
336
|
+
if (typeof source === 'string') {
|
|
337
|
+
await sandbox.fs.uploadFile(source, destination);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
await sandbox.fs.uploadFile(source, destination);
|
|
341
|
+
}
|
|
342
|
+
async uploadBundle(handle, options) {
|
|
343
|
+
await this.ensureUploadParentDirectories(handle, this.uploadParentDirectories(options));
|
|
344
|
+
for (const file of options.files) {
|
|
345
|
+
await this.uploadFile(handle, file.source, file.destination);
|
|
346
|
+
}
|
|
347
|
+
if (options.manifest !== undefined) {
|
|
348
|
+
await this.uploadFile(handle, Buffer.from(JSON.stringify(options.manifest, null, 2), 'utf8'), options.manifestPath ?? '/workspace/manifest.json');
|
|
349
|
+
}
|
|
350
|
+
await this.verifyUploadedBundleFiles(handle, this.uploadDestinations(options));
|
|
351
|
+
}
|
|
352
|
+
async downloadFile(handle, source, destination) {
|
|
353
|
+
const sandbox = this.requireSandbox(handle);
|
|
354
|
+
if (destination) {
|
|
355
|
+
await sandbox.fs.downloadFile(source, destination);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
return sandbox.fs.downloadFile(source);
|
|
359
|
+
}
|
|
360
|
+
async getHomeDir(handle) {
|
|
361
|
+
if (handle.homeDir) {
|
|
362
|
+
return handle.homeDir;
|
|
363
|
+
}
|
|
364
|
+
const sandbox = this.requireSandbox(handle);
|
|
365
|
+
const homeDir = await this.resolveHomeDir(sandbox);
|
|
366
|
+
handle.homeDir = homeDir;
|
|
367
|
+
return homeDir;
|
|
368
|
+
}
|
|
369
|
+
async destroy(handle) {
|
|
370
|
+
const entry = this.sandboxes.get(handle.id);
|
|
371
|
+
if (!entry) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!entry.owned) {
|
|
375
|
+
// For attached (non-owned) sandboxes we never call the remote
|
|
376
|
+
// delete; just drop the local registration so the caller-managed
|
|
377
|
+
// resource isn't tracked here any more.
|
|
378
|
+
this.sandboxes.delete(handle.id);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const client = this.daytona;
|
|
382
|
+
const remove = client.remove ?? client.delete;
|
|
383
|
+
// Order matters: do the remote delete *first*, and only drop the
|
|
384
|
+
// local map entry after it succeeds. If we dropped the entry first
|
|
385
|
+
// and the remote delete then failed, the handle id would be lost
|
|
386
|
+
// and the caller could not retry cleanup safely.
|
|
387
|
+
await remove.call(client, entry.sandbox);
|
|
388
|
+
this.sandboxes.delete(handle.id);
|
|
389
|
+
}
|
|
390
|
+
async stop(handle) {
|
|
391
|
+
const entry = this.sandboxes.get(handle.id);
|
|
392
|
+
if (!entry) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!entry.owned) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const client = this.daytona;
|
|
399
|
+
if (client.stop) {
|
|
400
|
+
await client.stop(entry.sandbox);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
await entry.sandbox.stop?.();
|
|
404
|
+
}
|
|
405
|
+
async start(handle) {
|
|
406
|
+
const entry = this.sandboxes.get(handle.id);
|
|
407
|
+
if (!entry) {
|
|
408
|
+
return handle;
|
|
409
|
+
}
|
|
410
|
+
if (!entry.owned) {
|
|
411
|
+
return handle;
|
|
412
|
+
}
|
|
413
|
+
const client = this.daytona;
|
|
414
|
+
if (client.start) {
|
|
415
|
+
await client.start(entry.sandbox);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
await entry.sandbox.start?.();
|
|
419
|
+
}
|
|
420
|
+
handle.state = 'STARTED';
|
|
421
|
+
return handle;
|
|
422
|
+
}
|
|
423
|
+
async createSandbox(options) {
|
|
424
|
+
const params = this.buildCreateParams(options);
|
|
425
|
+
const createOptions = this.buildCreateOptions(options);
|
|
426
|
+
if (this.snapshot) {
|
|
427
|
+
try {
|
|
428
|
+
return await this.createWithOptions({ snapshot: this.snapshot, ...params }, createOptions);
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
// Only fall back to a fresh sandbox when the snapshot itself is
|
|
432
|
+
// missing. Auth/network/quota errors should bubble — otherwise
|
|
433
|
+
// we silently mask real failures (a 401 ends up creating an
|
|
434
|
+
// unsnapshotted sandbox under whichever credentials worked).
|
|
435
|
+
if (!isSnapshotNotFoundError(err)) {
|
|
436
|
+
throw err;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return this.createWithOptions({ language: 'typescript', ...params }, createOptions);
|
|
441
|
+
}
|
|
442
|
+
async createSandboxDetached(options) {
|
|
443
|
+
const params = this.buildCreateParams(options);
|
|
444
|
+
const createOptions = this.buildCreateOptions(options);
|
|
445
|
+
if (this.snapshot) {
|
|
446
|
+
try {
|
|
447
|
+
return await this.createDetachedWithOptions({ snapshot: this.snapshot, ...params }, createOptions);
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
if (!isSnapshotNotFoundError(err)) {
|
|
451
|
+
throw err;
|
|
452
|
+
}
|
|
453
|
+
throw new SnapshotNotFoundError(this.snapshot, err);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return this.createDetachedWithOptions({ language: 'typescript', ...params }, createOptions);
|
|
457
|
+
}
|
|
458
|
+
buildCreateParams(options) {
|
|
459
|
+
const envVars = options.env && Object.keys(options.env).length > 0 ? options.env : undefined;
|
|
460
|
+
const name = options.name?.trim()
|
|
461
|
+
? options.name.trim()
|
|
462
|
+
: options.label?.trim()
|
|
463
|
+
? options.label.trim()
|
|
464
|
+
: undefined;
|
|
465
|
+
const labels = options.labels && Object.keys(options.labels).length > 0 ? options.labels : undefined;
|
|
466
|
+
return {
|
|
467
|
+
...(envVars ? { envVars } : {}),
|
|
468
|
+
...(name ? { name } : {}),
|
|
469
|
+
...(labels ? { labels } : {}),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
buildCreateOptions(options) {
|
|
473
|
+
if (!options.createTimeoutSeconds || options.createTimeoutSeconds <= 0) {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
return { timeout: Math.ceil(options.createTimeoutSeconds) };
|
|
477
|
+
}
|
|
478
|
+
createWithOptions(params, createOptions) {
|
|
479
|
+
if (createOptions) {
|
|
480
|
+
return this.daytona.create(params, createOptions);
|
|
481
|
+
}
|
|
482
|
+
return this.daytona.create(params);
|
|
483
|
+
}
|
|
484
|
+
async createDetachedWithOptions(params, createOptions) {
|
|
485
|
+
const client = this.daytona;
|
|
486
|
+
const labels = params.labels && typeof params.labels === 'object'
|
|
487
|
+
? { ...params.labels }
|
|
488
|
+
: {};
|
|
489
|
+
const language = typeof params.language === 'string' && params.language.trim()
|
|
490
|
+
? params.language.trim()
|
|
491
|
+
: 'python';
|
|
492
|
+
labels['code-toolbox-language'] = language;
|
|
493
|
+
const response = await client.sandboxApi.createSandbox({
|
|
494
|
+
name: params.name,
|
|
495
|
+
snapshot: params.snapshot,
|
|
496
|
+
env: params.envVars ?? {},
|
|
497
|
+
labels,
|
|
498
|
+
target: client.target,
|
|
499
|
+
}, undefined, createOptions ? { timeout: Math.min(createOptions.timeout, 15) * 1000 } : undefined);
|
|
500
|
+
const handle = {
|
|
501
|
+
id: response.data.id,
|
|
502
|
+
...((response.data.state ?? response.data.status)
|
|
503
|
+
? { state: response.data.state ?? response.data.status }
|
|
504
|
+
: {}),
|
|
505
|
+
};
|
|
506
|
+
if (!this.matchesState(handle, ['STARTED'])) {
|
|
507
|
+
return handle;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
return await client.get(response.data.id);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return { ...handle, state: 'STARTING' };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
listSandboxes(labels, options) {
|
|
517
|
+
const query = {
|
|
518
|
+
labels,
|
|
519
|
+
limit: options.limit,
|
|
520
|
+
};
|
|
521
|
+
if (options.states !== null) {
|
|
522
|
+
query.states = options.states.map(normalizeDaytonaState);
|
|
523
|
+
}
|
|
524
|
+
return this.daytona.list(query);
|
|
525
|
+
}
|
|
526
|
+
registerSandbox(sandbox, options) {
|
|
527
|
+
const handle = {
|
|
528
|
+
id: sandbox.id,
|
|
529
|
+
...(this.readSandboxState(sandbox) ? { state: this.readSandboxState(sandbox) } : {}),
|
|
530
|
+
...(sandbox.createdAt ? { createdAt: sandbox.createdAt } : {}),
|
|
531
|
+
...(sandbox.updatedAt ? { updatedAt: sandbox.updatedAt } : {}),
|
|
532
|
+
...(sandbox.lastActivityAt ? { lastActivityAt: sandbox.lastActivityAt } : {}),
|
|
533
|
+
...(options.homeDir ? { homeDir: options.homeDir } : {}),
|
|
534
|
+
...(options.workdir ? { workdir: options.workdir } : {}),
|
|
535
|
+
};
|
|
536
|
+
this.sandboxes.set(handle.id, {
|
|
537
|
+
sandbox,
|
|
538
|
+
owned: options.owned,
|
|
539
|
+
});
|
|
540
|
+
return handle;
|
|
541
|
+
}
|
|
542
|
+
requireSandbox(handle) {
|
|
543
|
+
const entry = this.sandboxes.get(handle.id);
|
|
544
|
+
if (!entry) {
|
|
545
|
+
throw new Error(`Runtime handle "${handle.id}" is no longer active`);
|
|
546
|
+
}
|
|
547
|
+
return entry.sandbox;
|
|
548
|
+
}
|
|
549
|
+
supportsSessionExec(sandbox) {
|
|
550
|
+
const process = sandbox.process;
|
|
551
|
+
if (!process || typeof process !== 'object') {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
const candidate = process;
|
|
555
|
+
return (typeof candidate.createSession === 'function' &&
|
|
556
|
+
typeof candidate.executeSessionCommand === 'function');
|
|
557
|
+
}
|
|
558
|
+
buildScriptCommand(options) {
|
|
559
|
+
const statements = [];
|
|
560
|
+
if (options.cwd) {
|
|
561
|
+
statements.push(`cd ${shellSingleQuote(options.cwd)}`);
|
|
562
|
+
}
|
|
563
|
+
for (const [key, value] of Object.entries(options.env ?? {})) {
|
|
564
|
+
statements.push(`export ${key}=${shellSingleQuote(value)}`);
|
|
565
|
+
}
|
|
566
|
+
statements.push(options.command);
|
|
567
|
+
return statements.join('\n');
|
|
568
|
+
}
|
|
569
|
+
// Deterministic per-session log path written by startScript's `exec`
|
|
570
|
+
// redirect and read back by getScriptLogs. Keyed by sessionId (known before
|
|
571
|
+
// the command id exists) and filesystem-sanitised. Callers that run one
|
|
572
|
+
// command per session get an unambiguous path back.
|
|
573
|
+
scriptLogPath(sessionId) {
|
|
574
|
+
return `/tmp/.daytona-run-${sessionSafeId(sessionId)}.log`;
|
|
575
|
+
}
|
|
576
|
+
matchesState(sandbox, states) {
|
|
577
|
+
if (states === null) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
const expected = new Set(states.map((state) => state.toUpperCase()));
|
|
581
|
+
const actual = this.readSandboxState(sandbox);
|
|
582
|
+
return actual ? expected.has(actual.toUpperCase()) : false;
|
|
583
|
+
}
|
|
584
|
+
readSandboxState(sandbox) {
|
|
585
|
+
const candidate = sandbox;
|
|
586
|
+
const value = candidate.state
|
|
587
|
+
?? candidate.status
|
|
588
|
+
?? candidate.sandboxState
|
|
589
|
+
?? candidate.info?.state
|
|
590
|
+
?? candidate.info?.status;
|
|
591
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
592
|
+
}
|
|
593
|
+
uploadParentDirectories(options) {
|
|
594
|
+
const directories = new Set();
|
|
595
|
+
for (const destination of this.uploadDestinations(options)) {
|
|
596
|
+
const directory = parentDirectory(destination);
|
|
597
|
+
if (directory) {
|
|
598
|
+
directories.add(directory);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return Array.from(directories).sort();
|
|
602
|
+
}
|
|
603
|
+
uploadDestinations(options) {
|
|
604
|
+
const destinations = options.files.map((file) => file.destination);
|
|
605
|
+
if (options.manifest !== undefined) {
|
|
606
|
+
destinations.push(options.manifestPath ?? '/workspace/manifest.json');
|
|
607
|
+
}
|
|
608
|
+
return destinations;
|
|
609
|
+
}
|
|
610
|
+
async ensureUploadParentDirectories(handle, directories) {
|
|
611
|
+
if (directories.length === 0) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const result = await this.runScript(handle, {
|
|
615
|
+
command: `mkdir -p ${directories.map(shellSingleQuote).join(' ')}`,
|
|
616
|
+
sessionId: `mkdir-${sessionSafeId(handle.id)}-${Date.now()}`,
|
|
617
|
+
timeoutMs: 30_000,
|
|
618
|
+
});
|
|
619
|
+
if (result.exitCode == null || result.exitCode !== 0) {
|
|
620
|
+
throw new Error(`Failed to create upload directories: ${result.output || result.stderr || result.stdout || 'mkdir failed'}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async verifyUploadedBundleFiles(handle, destinations) {
|
|
624
|
+
if (destinations.length === 0) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const checks = destinations
|
|
628
|
+
.map((destination) => `test -f ${shellSingleQuote(destination)}`)
|
|
629
|
+
.join(' && ');
|
|
630
|
+
const result = await this.runScript(handle, {
|
|
631
|
+
command: checks,
|
|
632
|
+
sessionId: `verify-upload-${sessionSafeId(handle.id)}-${Date.now()}`,
|
|
633
|
+
timeoutMs: 30_000,
|
|
634
|
+
});
|
|
635
|
+
if (result.exitCode == null || result.exitCode !== 0) {
|
|
636
|
+
throw new Error(`Failed to verify uploaded bundle files: ${result.output || result.stderr || result.stdout || 'remote file check failed'}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async resolveHomeDir(sandbox) {
|
|
640
|
+
try {
|
|
641
|
+
const home = await sandbox.getUserHomeDir();
|
|
642
|
+
if (home) {
|
|
643
|
+
return home;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// fall through to default
|
|
648
|
+
}
|
|
649
|
+
return this.defaultHomeDir;
|
|
650
|
+
}
|
|
651
|
+
msToSeconds(timeoutMs) {
|
|
652
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
return Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function normalizeDaytonaState(state) {
|
|
659
|
+
return state.toLowerCase();
|
|
660
|
+
}
|
|
661
|
+
function lookupDeadline(timeoutMs) {
|
|
662
|
+
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_DAYTONA_LOOKUP_TIMEOUT_MS;
|
|
663
|
+
const normalizedTimeoutMs = Number.isFinite(effectiveTimeoutMs) && effectiveTimeoutMs > 0
|
|
664
|
+
? Math.max(1, Math.ceil(effectiveTimeoutMs))
|
|
665
|
+
: DEFAULT_DAYTONA_LOOKUP_TIMEOUT_MS;
|
|
666
|
+
return {
|
|
667
|
+
endsAt: Date.now() + normalizedTimeoutMs,
|
|
668
|
+
timeoutMs: normalizedTimeoutMs,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
async function awaitLookupOperation(operation, deadline, description) {
|
|
672
|
+
const remainingMs = deadline.endsAt - Date.now();
|
|
673
|
+
if (remainingMs <= 0) {
|
|
674
|
+
throw new Error(`Daytona sandbox lookup exceeded ${deadline.timeoutMs}ms while ${description}`);
|
|
675
|
+
}
|
|
676
|
+
let timer;
|
|
677
|
+
try {
|
|
678
|
+
return await Promise.race([
|
|
679
|
+
operation,
|
|
680
|
+
new Promise((_resolve, reject) => {
|
|
681
|
+
timer = setTimeout(() => {
|
|
682
|
+
reject(new Error(`Daytona sandbox lookup exceeded ${deadline.timeoutMs}ms while ${description}`));
|
|
683
|
+
}, remainingMs);
|
|
684
|
+
}),
|
|
685
|
+
]);
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
if (timer !== undefined) {
|
|
689
|
+
clearTimeout(timer);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function closeAsyncIteratorBestEffort(iterator) {
|
|
694
|
+
if (!iterator.return) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
void iterator.return().catch(() => undefined);
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
// The lookup result or timeout is authoritative; iterator cleanup is best effort.
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function isRuntimeHandle(value) {
|
|
705
|
+
return !('getUserHomeDir' in value);
|
|
706
|
+
}
|
|
707
|
+
function shellSingleQuote(value) {
|
|
708
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
709
|
+
}
|
|
710
|
+
function sessionSafeId(value) {
|
|
711
|
+
return value.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'sandbox';
|
|
712
|
+
}
|
|
713
|
+
function parentDirectory(destination) {
|
|
714
|
+
const normalized = destination.trim().replace(/\/+$/g, '');
|
|
715
|
+
if (!normalized || normalized === '/' || !normalized.includes('/')) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
const separatorIndex = normalized.lastIndexOf('/');
|
|
719
|
+
if (separatorIndex <= 0) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
const directory = normalized.slice(0, separatorIndex);
|
|
723
|
+
return directory && directory !== '.' ? directory : null;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Heuristic: identify Daytona errors that indicate the snapshot we asked
|
|
727
|
+
* for doesn't exist (so falling back to a fresh sandbox is safe). We look
|
|
728
|
+
* at the HTTP status when the SDK surfaces one, plus a few well-known
|
|
729
|
+
* error-message shapes Daytona emits. Anything else propagates so the
|
|
730
|
+
* caller sees the original error (auth/network/quota/etc.).
|
|
731
|
+
*/
|
|
732
|
+
function isSnapshotNotFoundError(err) {
|
|
733
|
+
if (!err || typeof err !== 'object')
|
|
734
|
+
return false;
|
|
735
|
+
const candidate = err;
|
|
736
|
+
const status = typeof candidate.status === 'number'
|
|
737
|
+
? candidate.status
|
|
738
|
+
: typeof candidate.statusCode === 'number'
|
|
739
|
+
? candidate.statusCode
|
|
740
|
+
: undefined;
|
|
741
|
+
if (status === 404)
|
|
742
|
+
return true;
|
|
743
|
+
const message = typeof candidate.message === 'string' ? candidate.message.toLowerCase() : '';
|
|
744
|
+
if (!message)
|
|
745
|
+
return false;
|
|
746
|
+
return (message.includes('snapshot') &&
|
|
747
|
+
(message.includes('not found') || message.includes('does not exist') || message.includes('no such')));
|
|
748
|
+
}
|
|
749
|
+
function isDaytonaNotFoundError(err) {
|
|
750
|
+
if (!err || typeof err !== 'object')
|
|
751
|
+
return false;
|
|
752
|
+
const candidate = err;
|
|
753
|
+
const status = typeof candidate.status === 'number'
|
|
754
|
+
? candidate.status
|
|
755
|
+
: typeof candidate.statusCode === 'number'
|
|
756
|
+
? candidate.statusCode
|
|
757
|
+
: undefined;
|
|
758
|
+
if (status === 404)
|
|
759
|
+
return true;
|
|
760
|
+
if (candidate.name === 'DaytonaNotFoundError')
|
|
761
|
+
return true;
|
|
762
|
+
const message = typeof candidate.message === 'string' ? candidate.message.toLowerCase() : '';
|
|
763
|
+
return message.includes('sandbox') && message.includes('not found');
|
|
764
|
+
}
|
|
765
|
+
//# sourceMappingURL=runtime.js.map
|