withruntime 0.9.0 → 0.11.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/BLAXEL.md +4 -4
- package/DAYTONA.md +52 -40
- package/E2B.md +65 -62
- package/README.md +15 -7
- package/README.src.md +15 -7
- package/VERCEL.md +34 -22
- package/compatibility-lock.json +16 -16
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +2 -1
- package/dist/blaxel/preview.js +15 -13
- package/dist/blaxel/process.js +56 -10
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +262 -56
- package/dist/client.d.ts +41 -3
- package/dist/client.js +67 -11
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.js +47 -9
- package/dist/cloudflare/worker.d.ts +4 -0
- package/dist/cloudflare/worker.js +166 -16
- package/dist/codesandbox/guest-service.js +57 -20
- package/dist/codesandbox/index.js +42 -8
- package/dist/compat/core.d.ts +9 -8
- package/dist/compat/core.js +60 -21
- package/dist/compat/files.js +22 -4
- package/dist/compat/lease.d.ts +36 -0
- package/dist/compat/lease.js +87 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/computer-use.d.ts +162 -0
- package/dist/daytona/computer-use.js +324 -0
- package/dist/daytona/context.d.ts +10 -0
- package/dist/daytona/context.js +24 -0
- package/dist/daytona/daytona.d.ts +20 -0
- package/dist/daytona/daytona.js +134 -20
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +129 -38
- package/dist/daytona/index.d.ts +3 -27
- package/dist/daytona/index.js +2 -7
- package/dist/daytona/process.d.ts +63 -6
- package/dist/daytona/process.js +224 -23
- package/dist/daytona/sandbox.d.ts +20 -6
- package/dist/daytona/sandbox.js +55 -32
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +6 -2
- package/dist/e2b/commands.d.ts +43 -12
- package/dist/e2b/commands.js +217 -64
- package/dist/e2b/errors.d.ts +7 -0
- package/dist/e2b/errors.js +20 -0
- package/dist/e2b/filesystem.d.ts +2 -1
- package/dist/e2b/filesystem.js +43 -6
- package/dist/e2b/index.d.ts +3 -2
- package/dist/e2b/index.js +2 -1
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +83 -0
- package/dist/e2b/sandbox.d.ts +17 -6
- package/dist/e2b/sandbox.js +239 -43
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +9 -1
- package/dist/freestyle/index.js +38 -5
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/modal/index.js +2 -1
- package/dist/network-cli.js +139 -43
- package/dist/openai-agents/index.js +9 -45
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/browser.d.ts +60 -0
- package/dist/products/browser.js +37 -0
- package/dist/products/desktop.d.ts +2 -2
- package/dist/products/desktop.js +2 -2
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/network-products.d.ts +7 -0
- package/dist/products/network-products.js +3 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +8 -2
- package/dist/products/previews.js +11 -3
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.js +61 -8
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +13 -27
- package/dist/runloop/index.js +149 -52
- package/dist/sandbox.d.ts +59 -8
- package/dist/sandbox.js +479 -68
- package/dist/snapshots.d.ts +9 -4
- package/dist/sprites/index.js +69 -33
- package/dist/tar.d.ts +3 -1
- package/dist/tar.js +140 -14
- package/dist/transport.d.ts +4 -7
- package/dist/transport.js +205 -122
- package/dist/types.d.ts +5 -2
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +3 -2
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +7 -1
- package/dist/vercel/index.d.ts +1 -7
- package/dist/vercel/index.js +1 -3
- package/dist/vercel/sandbox-user.d.ts +70 -0
- package/dist/vercel/sandbox-user.js +206 -0
- package/dist/vercel/sandbox.d.ts +55 -20
- package/dist/vercel/sandbox.js +353 -99
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +2 -1
|
@@ -11,6 +11,8 @@ export interface SandboxContext {
|
|
|
11
11
|
/** Makes /home/daytona (Daytona's home) lead to /workspace (Runtime's),
|
|
12
12
|
* once, the first time something names it. */
|
|
13
13
|
ensureHome(text: string | undefined): Promise<void>;
|
|
14
|
+
/** The Linux user commands run as (create's `user`), when not the owner. */
|
|
15
|
+
readonly user?: string;
|
|
14
16
|
}
|
|
15
17
|
/** Daytona's home, and the working directory relative paths resolve from. */
|
|
16
18
|
export declare const DAYTONA_HOME = "/home/daytona";
|
|
@@ -29,3 +31,11 @@ export declare const WHOLE_OUTPUT: {
|
|
|
29
31
|
export declare function resolvePath(path: string): string;
|
|
30
32
|
/** Quotes a word for bash. */
|
|
31
33
|
export declare function quote(word: string): string;
|
|
34
|
+
/** A command and its environment as Runtime runs them: as given for the
|
|
35
|
+
* sandbox owner; for another user through sudo, its environment carried on
|
|
36
|
+
* the command line (sudo resets it) and umask 002, so files it makes in the
|
|
37
|
+
* shared working directory stay writable by the owner's group. */
|
|
38
|
+
export declare function runAs(user: string | undefined, argv: readonly string[], env?: Record<string, string>): {
|
|
39
|
+
argv: string[];
|
|
40
|
+
env?: Record<string, string>;
|
|
41
|
+
};
|
package/dist/daytona/context.js
CHANGED
|
@@ -24,3 +24,27 @@ export function resolvePath(path) {
|
|
|
24
24
|
export function quote(word) {
|
|
25
25
|
return `'${word.replace(/'/g, `'\\''`)}'`;
|
|
26
26
|
}
|
|
27
|
+
/** A command and its environment as Runtime runs them: as given for the
|
|
28
|
+
* sandbox owner; for another user through sudo, its environment carried on
|
|
29
|
+
* the command line (sudo resets it) and umask 002, so files it makes in the
|
|
30
|
+
* shared working directory stay writable by the owner's group. */
|
|
31
|
+
export function runAs(user, argv, env = {}) {
|
|
32
|
+
if (!user)
|
|
33
|
+
return { argv: [...argv], ...(Object.keys(env).length ? { env } : {}) };
|
|
34
|
+
return {
|
|
35
|
+
argv: [
|
|
36
|
+
"sudo",
|
|
37
|
+
"-u",
|
|
38
|
+
user,
|
|
39
|
+
"-H",
|
|
40
|
+
"--",
|
|
41
|
+
"env",
|
|
42
|
+
...Object.entries(env).map(([name, value]) => `${name}=${value}`),
|
|
43
|
+
"sh",
|
|
44
|
+
"-c",
|
|
45
|
+
'umask 002; exec "$@"',
|
|
46
|
+
"sh",
|
|
47
|
+
...argv,
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -13,6 +13,8 @@ export declare const DEFAULT_RESOURCES: {
|
|
|
13
13
|
export declare const DEFAULT_AUTO_STOP_MINUTES = 15;
|
|
14
14
|
/** Daytona's label for a sandbox's code language. */
|
|
15
15
|
export declare const LANGUAGE_LABEL = "code-toolbox-language";
|
|
16
|
+
/** The Linux user given at create, so get() and list() run as it too. */
|
|
17
|
+
export declare const USER_LABEL = "compat.daytona-user";
|
|
16
18
|
export declare enum CodeLanguage {
|
|
17
19
|
PYTHON = "python",
|
|
18
20
|
TYPESCRIPT = "typescript",
|
|
@@ -69,10 +71,28 @@ export type ForkSandboxParams = {
|
|
|
69
71
|
name?: string;
|
|
70
72
|
};
|
|
71
73
|
export interface ListSandboxesQuery {
|
|
74
|
+
/** Daytona's page size; every sandbox is still listed. */
|
|
72
75
|
limit?: number;
|
|
76
|
+
sort?: "name" | "cpu" | "memoryGib" | "diskGib" | "lastActivityAt" | "createdAt";
|
|
77
|
+
order?: "asc" | "desc";
|
|
78
|
+
/** An id prefix, any case. */
|
|
79
|
+
id?: string;
|
|
80
|
+
/** A name prefix, any case. */
|
|
73
81
|
name?: string;
|
|
74
82
|
labels?: Record<string, string>;
|
|
75
83
|
states?: SandboxState[];
|
|
84
|
+
targets?: string[];
|
|
85
|
+
minCpu?: number;
|
|
86
|
+
maxCpu?: number;
|
|
87
|
+
minMemoryGib?: number;
|
|
88
|
+
maxMemoryGib?: number;
|
|
89
|
+
minDiskGib?: number;
|
|
90
|
+
maxDiskGib?: number;
|
|
91
|
+
isRecoverable?: boolean;
|
|
92
|
+
createdAtAfter?: Date;
|
|
93
|
+
createdAtBefore?: Date;
|
|
94
|
+
lastActivityAfter?: Date;
|
|
95
|
+
lastActivityBefore?: Date;
|
|
76
96
|
[other: string]: unknown;
|
|
77
97
|
}
|
|
78
98
|
/** A Daytona snapshot: on Runtime, an image (or a snapshot made with
|
package/dist/daytona/daytona.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clientFor, } from "./client.js";
|
|
2
2
|
import { DaytonaError, DaytonaNotFoundError, guard, NotSupportedError } from "./errors.js";
|
|
3
|
-
import { networkRules, notFound, retentionDays, Sandbox, windowSeconds, } from "./sandbox.js";
|
|
3
|
+
import { idleSeconds, networkRules, notFound, retentionDays, Sandbox, windowSeconds, } from "./sandbox.js";
|
|
4
4
|
/* Daytona's client over Runtime's. `new Daytona()` then `daytona.create()`
|
|
5
5
|
makes a Runtime sandbox with Daytona's defaults. */
|
|
6
6
|
/** Daytona's default machine: 1 vCPU, 1 GiB memory, 3 GiB disk (checked
|
|
@@ -14,6 +14,10 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
14
14
|
const STOCK_SNAPSHOT = /^daytona(io)?[-/:]|^daytona$/;
|
|
15
15
|
/** Daytona's label for a sandbox's code language. */
|
|
16
16
|
export const LANGUAGE_LABEL = "code-toolbox-language";
|
|
17
|
+
/** The Linux user given at create, so get() and list() run as it too. */
|
|
18
|
+
export const USER_LABEL = "compat.daytona-user";
|
|
19
|
+
/** The sandbox owner's names: Daytona's and Runtime's. */
|
|
20
|
+
const OWNER = new Set(["daytona", "runtime"]);
|
|
17
21
|
export var CodeLanguage;
|
|
18
22
|
(function (CodeLanguage) {
|
|
19
23
|
CodeLanguage["PYTHON"] = "python";
|
|
@@ -105,6 +109,8 @@ export class SnapshotService {
|
|
|
105
109
|
/** Builds a Runtime image named `name` from a registry image or a Daytona
|
|
106
110
|
* Image, streaming the build log to `onLogs`. */
|
|
107
111
|
async create(params, options = {}) {
|
|
112
|
+
if (params.regionId)
|
|
113
|
+
throw new NotSupportedError("Choosing a snapshot region (regionId)", "Omit regionId: Runtime places image builds on its available hosts.");
|
|
108
114
|
if (params.resources)
|
|
109
115
|
throw new NotSupportedError("Resources on a snapshot", "Pass resources when creating each sandbox from it: daytona.create({ snapshot, resources }).");
|
|
110
116
|
if (params.entrypoint)
|
|
@@ -183,18 +189,20 @@ export class Daytona {
|
|
|
183
189
|
const client = params.withruntime?.client ?? this.#client;
|
|
184
190
|
const lifecycle = lifecycleOf(params);
|
|
185
191
|
const resources = { ...DEFAULT_RESOURCES, ...params.resources };
|
|
186
|
-
if (resources.gpu)
|
|
187
|
-
throw new NotSupportedError("GPUs", "Runtime runs sandboxes on CPUs; remove gpu from resources.");
|
|
188
192
|
const source = await this.#source(client, params, options.onSnapshotCreateLogs);
|
|
189
193
|
const volumes = await this.#volumes(client, params.volumes);
|
|
190
194
|
const network = networkRules(params);
|
|
191
195
|
// Daytona keeps the language in this label, so a sandbox found later with
|
|
192
196
|
// get() or list() runs codeRun in the language it was made for.
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
:
|
|
197
|
+
const user = params.user !== undefined && !OWNER.has(params.user) ? params.user : undefined;
|
|
198
|
+
const tagged = {
|
|
199
|
+
...params.labels,
|
|
200
|
+
...(params.language && params.language !== "python"
|
|
201
|
+
? { [LANGUAGE_LABEL]: params.language }
|
|
202
|
+
: {}),
|
|
203
|
+
...(user ? { [USER_LABEL]: user } : {}),
|
|
204
|
+
};
|
|
205
|
+
const labels = Object.keys(tagged).length ? tagged : undefined;
|
|
198
206
|
const input = {
|
|
199
207
|
...(source.snapshot
|
|
200
208
|
? {}
|
|
@@ -216,6 +224,8 @@ export class Daytona {
|
|
|
216
224
|
...params.withruntime?.create,
|
|
217
225
|
};
|
|
218
226
|
const runtime = await guard("sandbox", () => client.sandboxes.create(input));
|
|
227
|
+
if (user)
|
|
228
|
+
await asUser(runtime, user);
|
|
219
229
|
if (lifecycle.autoDeleteInterval > 0)
|
|
220
230
|
await guard("sandbox", () => runtime.setRetention(retentionDays(lifecycle.autoDeleteInterval))).catch(async (error) => {
|
|
221
231
|
await runtime.stop({ wait: false }).catch(() => undefined);
|
|
@@ -233,6 +243,7 @@ export class Daytona {
|
|
|
233
243
|
public: params.public ?? false,
|
|
234
244
|
lifecycle,
|
|
235
245
|
...("snapshot" in params && params.snapshot ? { snapshot: params.snapshot } : {}),
|
|
246
|
+
...(user ? { user } : {}),
|
|
236
247
|
});
|
|
237
248
|
}
|
|
238
249
|
async #source(client, params, onLogs) {
|
|
@@ -267,8 +278,6 @@ export class Daytona {
|
|
|
267
278
|
async #volumes(client, mounts) {
|
|
268
279
|
const out = [];
|
|
269
280
|
for (const mount of mounts ?? []) {
|
|
270
|
-
if (mount.subpath)
|
|
271
|
-
throw new NotSupportedError("Mounting part of a volume (subpath)", "Mount the whole volume.");
|
|
272
281
|
let id = mount.volumeId;
|
|
273
282
|
if (!UUID.test(id)) {
|
|
274
283
|
const found = (await guard("other", () => client.volumes.list({ name: id, limit: 1 })))
|
|
@@ -288,12 +297,15 @@ export class Daytona {
|
|
|
288
297
|
}
|
|
289
298
|
#adopt(runtime) {
|
|
290
299
|
const onLeaseEnd = runtime.info.onLeaseEnd;
|
|
300
|
+
const user = runtime.info.labels[USER_LABEL];
|
|
291
301
|
return new Sandbox(runtime, this.#client, {
|
|
292
302
|
env: {},
|
|
293
303
|
language: runtime.info.labels[LANGUAGE_LABEL] ?? "python",
|
|
294
304
|
public: false,
|
|
305
|
+
...(user ? { user } : {}),
|
|
295
306
|
lifecycle: {
|
|
296
307
|
windowSeconds: runtime.info.timeoutSeconds,
|
|
308
|
+
idleSeconds: runtime.info.timeoutSeconds,
|
|
297
309
|
ephemeral: onLeaseEnd === "stop",
|
|
298
310
|
autoStopInterval: Math.round(runtime.info.timeoutSeconds / 60),
|
|
299
311
|
autoArchiveInterval: 0,
|
|
@@ -304,10 +316,15 @@ export class Daytona {
|
|
|
304
316
|
/** Live and stopped (paused) sandboxes, oldest first. Await it for the old
|
|
305
317
|
* paginated shape, `{ items, total, page, totalPages }`. */
|
|
306
318
|
list(query = {}) {
|
|
307
|
-
const
|
|
308
|
-
const other = Object.keys(query).find((key) => query[key] !== undefined && !known.has(key));
|
|
319
|
+
const other = Object.keys(query).find((key) => query[key] !== undefined && !LISTED.has(key));
|
|
309
320
|
if (other)
|
|
310
|
-
throw new NotSupportedError(`Listing sandboxes by ${other}`,
|
|
321
|
+
throw new NotSupportedError(`Listing sandboxes by ${other}`, other === "isPublic"
|
|
322
|
+
? "Runtime previews are public or private per port: filter by a label you set at create."
|
|
323
|
+
: "Filter by a label you set at create, or narrow the result yourself.");
|
|
324
|
+
if (query.sort !== undefined && !SORTS[query.sort])
|
|
325
|
+
throw new RangeError(`sort is one of ${Object.keys(SORTS).join(", ")}.`);
|
|
326
|
+
if (query.order !== undefined && query.order !== "asc" && query.order !== "desc")
|
|
327
|
+
throw new RangeError('order is "asc" or "desc".');
|
|
311
328
|
const states = query.states?.flatMap((state) => state === "started"
|
|
312
329
|
? ["running"]
|
|
313
330
|
: state === "starting"
|
|
@@ -317,15 +334,24 @@ export class Daytona {
|
|
|
317
334
|
: state === "stopped" || state === "paused"
|
|
318
335
|
? ["paused"]
|
|
319
336
|
: []);
|
|
320
|
-
const client = this.#client;
|
|
321
|
-
const adopt = (runtime) => this.#adopt(runtime);
|
|
322
337
|
const filter = {
|
|
323
338
|
...(query.labels ? { labels: query.labels } : {}),
|
|
324
|
-
...(query.name ? { name: query.name } : {}),
|
|
325
339
|
...(states ? { state: states } : {}),
|
|
326
340
|
...(query.limit ? { limit: Math.min(query.limit, 100) } : {}),
|
|
327
341
|
};
|
|
342
|
+
const here = Object.keys(query).some((key) => query[key] !== undefined && !["limit", "labels", "states"].includes(key));
|
|
343
|
+
const client = this.#client;
|
|
344
|
+
const adopt = (runtime) => this.#adopt(runtime);
|
|
345
|
+
const everything = async () => {
|
|
346
|
+
const page = await guard("other", () => client.sandboxes.list(filter));
|
|
347
|
+
return arrange(await page.toArray(10_000), query);
|
|
348
|
+
};
|
|
328
349
|
async function* iterate() {
|
|
350
|
+
if (here) {
|
|
351
|
+
for (const runtime of await everything())
|
|
352
|
+
yield adopt(runtime);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
329
355
|
const page = await guard("other", () => client.sandboxes.list(filter));
|
|
330
356
|
for await (const runtime of page)
|
|
331
357
|
yield adopt(runtime);
|
|
@@ -334,8 +360,7 @@ export class Daytona {
|
|
|
334
360
|
return Object.assign(iterator, {
|
|
335
361
|
then(onFulfilled, onRejected) {
|
|
336
362
|
const all = (async () => {
|
|
337
|
-
const
|
|
338
|
-
const items = (await page.toArray(10_000)).map(adopt);
|
|
363
|
+
const items = (await everything()).map(adopt);
|
|
339
364
|
return { items, total: items.length, page: 1, totalPages: 1 };
|
|
340
365
|
})();
|
|
341
366
|
return all.then(onFulfilled, onRejected);
|
|
@@ -363,6 +388,15 @@ export class Daytona {
|
|
|
363
388
|
}
|
|
364
389
|
}
|
|
365
390
|
function refuseCreate(params) {
|
|
391
|
+
const gpuType = params.resources?.gpuType;
|
|
392
|
+
if (params.resources?.gpu ||
|
|
393
|
+
(gpuType !== undefined &&
|
|
394
|
+
gpuType !== null &&
|
|
395
|
+
gpuType !== "" &&
|
|
396
|
+
(!Array.isArray(gpuType) || gpuType.length > 0)))
|
|
397
|
+
throw new NotSupportedError("GPUs", "Runtime runs sandboxes on CPUs; remove gpu and gpuType from resources.");
|
|
398
|
+
if (params.volumes?.some((mount) => mount.subpath))
|
|
399
|
+
throw new NotSupportedError("Mounting part of a volume (subpath)", "Mount the whole volume.");
|
|
366
400
|
const refusals = [
|
|
367
401
|
["spot", "Spot GPU sandboxes (spot)", "Remove it: Runtime runs sandboxes on CPUs."],
|
|
368
402
|
[
|
|
@@ -384,8 +418,8 @@ function refuseCreate(params) {
|
|
|
384
418
|
for (const [field, feature, alternative] of refusals)
|
|
385
419
|
if (params[field] !== undefined)
|
|
386
420
|
throw new NotSupportedError(feature, alternative);
|
|
387
|
-
if (params.user !== undefined && params.user
|
|
388
|
-
throw new
|
|
421
|
+
if (params.user !== undefined && !/^[a-z_][a-z0-9_-]{0,31}$/.test(params.user))
|
|
422
|
+
throw new DaytonaError(`Invalid user name ${JSON.stringify(params.user)}.`, 400);
|
|
389
423
|
if (params.language !== undefined &&
|
|
390
424
|
!["python", "javascript", "typescript"].includes(params.language))
|
|
391
425
|
throw new NotSupportedError(`The language ${params.language}`, "Use python, javascript or typescript; run anything else with process.executeCommand.");
|
|
@@ -401,6 +435,7 @@ export function lifecycleOf(params) {
|
|
|
401
435
|
const autoDelete = params.ephemeral ? 0 : (params.autoDeleteInterval ?? -1);
|
|
402
436
|
return {
|
|
403
437
|
windowSeconds: windowSeconds(autoStop),
|
|
438
|
+
idleSeconds: idleSeconds(autoStop),
|
|
404
439
|
...(params.ttlMinutes ? { deadline: Date.now() + params.ttlMinutes * 60_000 } : {}),
|
|
405
440
|
ephemeral: autoDelete === 0,
|
|
406
441
|
autoStopInterval: autoStop,
|
|
@@ -408,6 +443,85 @@ export function lifecycleOf(params) {
|
|
|
408
443
|
autoDeleteInterval: autoDelete,
|
|
409
444
|
};
|
|
410
445
|
}
|
|
446
|
+
/** What list() takes; isPublic, snapshots and autoDestroyAt are refused. */
|
|
447
|
+
const LISTED = new Set([
|
|
448
|
+
"limit",
|
|
449
|
+
"sort",
|
|
450
|
+
"order",
|
|
451
|
+
"id",
|
|
452
|
+
"name",
|
|
453
|
+
"labels",
|
|
454
|
+
"states",
|
|
455
|
+
"targets",
|
|
456
|
+
"minCpu",
|
|
457
|
+
"maxCpu",
|
|
458
|
+
"minMemoryGib",
|
|
459
|
+
"maxMemoryGib",
|
|
460
|
+
"minDiskGib",
|
|
461
|
+
"maxDiskGib",
|
|
462
|
+
"isRecoverable",
|
|
463
|
+
"createdAtAfter",
|
|
464
|
+
"createdAtBefore",
|
|
465
|
+
"lastActivityAfter",
|
|
466
|
+
"lastActivityBefore",
|
|
467
|
+
]);
|
|
468
|
+
const lastActivity = (one) => Date.parse(one.info.lastActiveAt ?? one.info.readyAt ?? one.info.createdAt);
|
|
469
|
+
const SORTS = {
|
|
470
|
+
name: (one) => one.info.name ?? one.id,
|
|
471
|
+
cpu: (one) => one.info.vcpu,
|
|
472
|
+
memoryGib: (one) => one.info.memoryMiB / 1024,
|
|
473
|
+
diskGib: (one) => one.info.diskMiB / 1024,
|
|
474
|
+
lastActivityAt: lastActivity,
|
|
475
|
+
createdAt: (one) => Date.parse(one.info.createdAt),
|
|
476
|
+
};
|
|
477
|
+
/** Daytona's list filters and sorting, applied here: Runtime filters by
|
|
478
|
+
* labels and state only. Sorted descending unless `order` says, as Daytona
|
|
479
|
+
* does; unsorted lists keep Runtime's order, oldest first. */
|
|
480
|
+
function arrange(all, query) {
|
|
481
|
+
const within = (value, min, max) => (min === undefined || value >= min) && (max === undefined || value <= max);
|
|
482
|
+
const time = (date) => (date === undefined ? undefined : date.getTime());
|
|
483
|
+
const kept = all.filter((one) => (query.id === undefined || one.id.toLowerCase().startsWith(query.id.toLowerCase())) &&
|
|
484
|
+
(query.name === undefined ||
|
|
485
|
+
(one.info.name ?? one.id).toLowerCase().startsWith(query.name.toLowerCase())) &&
|
|
486
|
+
(query.targets === undefined || query.targets.includes("us")) &&
|
|
487
|
+
(query.isRecoverable === undefined || query.isRecoverable === false) &&
|
|
488
|
+
within(one.info.vcpu, query.minCpu, query.maxCpu) &&
|
|
489
|
+
within(one.info.memoryMiB / 1024, query.minMemoryGib, query.maxMemoryGib) &&
|
|
490
|
+
within(one.info.diskMiB / 1024, query.minDiskGib, query.maxDiskGib) &&
|
|
491
|
+
within(Date.parse(one.info.createdAt), time(query.createdAtAfter), time(query.createdAtBefore)) &&
|
|
492
|
+
within(lastActivity(one), time(query.lastActivityAfter), time(query.lastActivityBefore)));
|
|
493
|
+
const key = query.sort === undefined ? undefined : SORTS[query.sort];
|
|
494
|
+
if (!key)
|
|
495
|
+
return query.order === "desc" ? kept.reverse() : kept;
|
|
496
|
+
const sign = query.order === "asc" ? 1 : -1;
|
|
497
|
+
return kept.sort((a, b) => {
|
|
498
|
+
const x = key(a);
|
|
499
|
+
const y = key(b);
|
|
500
|
+
return sign * (typeof x === "string" ? x.localeCompare(y) : x - y);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/** Readies a new sandbox to run as `user`: the user must be in the image, as
|
|
504
|
+
* on Daytona. It joins the owner's group, and the working directory takes
|
|
505
|
+
* that group for what is made in it, so both may change each other's files.
|
|
506
|
+
* A user the image lacks ends the sandbox and says how to add it. */
|
|
507
|
+
async function asUser(runtime, user) {
|
|
508
|
+
const ready = await guard("sandbox", () => runtime.exec([
|
|
509
|
+
"sh",
|
|
510
|
+
"-c",
|
|
511
|
+
'id -u "$1" >/dev/null 2>&1 || exit 3; [ "$1" = root ] || { sudo usermod -aG runtime "$1" && sudo chmod 2775 /workspace; }',
|
|
512
|
+
"sh",
|
|
513
|
+
user,
|
|
514
|
+
])).catch(async (error) => {
|
|
515
|
+
await runtime.stop({ wait: false }).catch(() => undefined);
|
|
516
|
+
throw error;
|
|
517
|
+
});
|
|
518
|
+
if (ready.exitCode === 0)
|
|
519
|
+
return;
|
|
520
|
+
await runtime.stop({ wait: false }).catch(() => undefined);
|
|
521
|
+
throw new DaytonaError(ready.exitCode === 3
|
|
522
|
+
? `The user "${user}" does not exist in this sandbox's image. Add it to the image (RUN useradd -m ${user}), or leave user out to run as the sandbox owner, who has passwordless sudo.`
|
|
523
|
+
: `The sandbox could not be readied for the user "${user}": ${ready.stderr.trim() || `exit ${ready.exitCode}`}`, 400);
|
|
524
|
+
}
|
|
411
525
|
/** The sandbox with this Runtime id, or the newest live one with this name. */
|
|
412
526
|
async function find(client, idOrName) {
|
|
413
527
|
if (UUID.test(idOrName))
|
|
@@ -49,6 +49,14 @@ export interface FileDownloadResponse {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
export type UploadSource = Buffer | Uint8Array | string | Readable | ReadableStream<Uint8Array>;
|
|
52
|
+
export type UploadProgress = {
|
|
53
|
+
bytesSent: number;
|
|
54
|
+
};
|
|
55
|
+
export type UploadStreamOptions = {
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
timeout?: number;
|
|
58
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
59
|
+
};
|
|
52
60
|
/** `sandbox.fs`: Daytona's file calls over Runtime's files API. Relative
|
|
53
61
|
* paths resolve from the working directory, /workspace on Runtime. */
|
|
54
62
|
export declare class FileSystem {
|
|
@@ -62,9 +70,12 @@ export declare class FileSystem {
|
|
|
62
70
|
downloadFile(remotePath: string, localPath: string, timeout?: number): Promise<void>;
|
|
63
71
|
/** The file as it arrives, without holding it in memory. It errors with
|
|
64
72
|
* `download_incomplete` if it ends short of the file's length. */
|
|
65
|
-
downloadFileStream(remotePath: string
|
|
73
|
+
downloadFileStream(remotePath: string, timeoutOrOptions?: number | {
|
|
74
|
+
timeout?: number;
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
}): Promise<Readable>;
|
|
66
77
|
/** Several files; a file that fails carries `error` instead of `result`. */
|
|
67
|
-
downloadFiles(files: FileDownloadRequest[]): Promise<FileDownloadResponse[]>;
|
|
78
|
+
downloadFiles(files: FileDownloadRequest[], timeout?: number): Promise<FileDownloadResponse[]>;
|
|
68
79
|
/** Lines matching `pattern` (a grep pattern) in files under `path`. */
|
|
69
80
|
findFiles(path: string, pattern: string): Promise<Match[]>;
|
|
70
81
|
getFileDetails(path: string): Promise<FileInfo>;
|
|
@@ -81,6 +92,8 @@ export declare class FileSystem {
|
|
|
81
92
|
* directories and replacing one that exists. */
|
|
82
93
|
uploadFile(file: Buffer, remotePath: string, timeout?: number): Promise<void>;
|
|
83
94
|
uploadFile(localPath: string, remotePath: string, timeout?: number): Promise<void>;
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
/** Uploads with bounded memory. Unknown-length streams stage in a private
|
|
96
|
+
* temporary file before the native atomic upload; local paths avoid staging. */
|
|
97
|
+
uploadFileStream(source: UploadSource, remotePath: string, options?: UploadStreamOptions): Promise<void>;
|
|
98
|
+
uploadFiles(files: FileUpload[], timeout?: number): Promise<void>;
|
|
86
99
|
}
|
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
import { resolvePath, WHOLE_OUTPUT } from "./context.js";
|
|
2
2
|
import { DaytonaError, DaytonaFileNotFoundError, guard } from "./errors.js";
|
|
3
3
|
const PERMISSIONS = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
|
|
4
|
+
function downloadRequest(timeout, signal) {
|
|
5
|
+
if (!Number.isFinite(timeout) || timeout < 0)
|
|
6
|
+
throw new DaytonaError("Download timeout must be a nonnegative finite number.");
|
|
7
|
+
signal?.throwIfAborted();
|
|
8
|
+
const deadline = timeout > 0 ? AbortSignal.timeout(Math.ceil(timeout * 1000)) : undefined;
|
|
9
|
+
return {
|
|
10
|
+
timeoutMs: 0,
|
|
11
|
+
...(deadline || signal
|
|
12
|
+
? {
|
|
13
|
+
signal: deadline && signal ? AbortSignal.any([deadline, signal]) : (deadline ?? signal),
|
|
14
|
+
}
|
|
15
|
+
: {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Wait on client-local setup within the transfer's existing deadline. The
|
|
19
|
+
* setup may finish later; the caller cannot proceed to an upload after abort. */
|
|
20
|
+
function waitForRequest(pending, request) {
|
|
21
|
+
const signal = request.signal;
|
|
22
|
+
if (!signal)
|
|
23
|
+
return pending;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
|
|
26
|
+
const abort = () => reject(signal.reason);
|
|
27
|
+
if (signal.aborted)
|
|
28
|
+
abort();
|
|
29
|
+
else
|
|
30
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
31
|
+
pending.then((value) => {
|
|
32
|
+
signal.removeEventListener("abort", abort);
|
|
33
|
+
resolve(value);
|
|
34
|
+
}, (error) => {
|
|
35
|
+
signal.removeEventListener("abort", abort);
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- passes the pending call's own failure through unchanged
|
|
37
|
+
reject(error);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
4
41
|
function fileInfo(entry) {
|
|
5
42
|
const bits = (parseInt(entry.mode, 8) || 0) & 0o777;
|
|
6
43
|
const kind = entry.type === "directory" ? "d" : entry.type === "symlink" ? "l" : "-";
|
|
@@ -15,20 +52,6 @@ function fileInfo(entry) {
|
|
|
15
52
|
group: "",
|
|
16
53
|
};
|
|
17
54
|
}
|
|
18
|
-
async function bytesOf(source) {
|
|
19
|
-
if (typeof source === "string") {
|
|
20
|
-
const { readFile } = await import("node:fs/promises");
|
|
21
|
-
return readFile(source);
|
|
22
|
-
}
|
|
23
|
-
if (source instanceof Uint8Array)
|
|
24
|
-
return source;
|
|
25
|
-
if (typeof source.getReader === "function")
|
|
26
|
-
return new Uint8Array(await new Response(source).arrayBuffer());
|
|
27
|
-
const chunks = [];
|
|
28
|
-
for await (const chunk of source)
|
|
29
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
|
|
30
|
-
return Buffer.concat(chunks);
|
|
31
|
-
}
|
|
32
55
|
/** `sandbox.fs`: Daytona's file calls over Runtime's files API. Relative
|
|
33
56
|
* paths resolve from the working directory, /workspace on Runtime. */
|
|
34
57
|
export class FileSystem {
|
|
@@ -58,6 +81,13 @@ export class FileSystem {
|
|
|
58
81
|
async createFolder(path, mode) {
|
|
59
82
|
const target = await this.#path(path);
|
|
60
83
|
await this.#run(["mkdir", "-p", "-m", mode, "--", target]);
|
|
84
|
+
await this.#own(target);
|
|
85
|
+
}
|
|
86
|
+
/** Gives what this client wrote to create's `user`, so its commands may
|
|
87
|
+
* change it; the owner's files stay the owner's. */
|
|
88
|
+
async #own(target) {
|
|
89
|
+
if (this.#ctx.user)
|
|
90
|
+
await this.#run(["sudo", "chown", this.#ctx.user, "--", target]);
|
|
61
91
|
}
|
|
62
92
|
async deleteFile(path, recursive = false) {
|
|
63
93
|
const target = await this.#path(path);
|
|
@@ -66,36 +96,56 @@ export class FileSystem {
|
|
|
66
96
|
if (!removed)
|
|
67
97
|
throw new DaytonaFileNotFoundError(`${target} does not exist.`, 404);
|
|
68
98
|
}
|
|
69
|
-
async downloadFile(remotePath, localOrTimeout) {
|
|
99
|
+
async downloadFile(remotePath, localOrTimeout, timeout = 30 * 60) {
|
|
100
|
+
// The numeric second argument is the published buffer overload. In that
|
|
101
|
+
// overload zero falls back to the default, as the pinned SDK does.
|
|
102
|
+
const request = downloadRequest(typeof localOrTimeout === "number" && localOrTimeout !== 0 ? localOrTimeout : timeout);
|
|
103
|
+
return this.#downloadFile(remotePath, typeof localOrTimeout === "string" ? localOrTimeout : undefined, request);
|
|
104
|
+
}
|
|
105
|
+
async #downloadFile(remotePath, localPath, request) {
|
|
106
|
+
request.signal?.throwIfAborted();
|
|
70
107
|
const target = await this.#path(remotePath);
|
|
71
108
|
const files = await this.#files();
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
request.signal?.throwIfAborted();
|
|
110
|
+
if (localPath === undefined)
|
|
111
|
+
return Buffer.from(await guard("file", () => files.read(target, request)));
|
|
74
112
|
// Streamed to disk, any size, checked against the file's length.
|
|
75
|
-
await guard("file", () => files.download(target,
|
|
113
|
+
await guard("file", () => files.download(target, localPath, request));
|
|
76
114
|
}
|
|
77
115
|
/** The file as it arrives, without holding it in memory. It errors with
|
|
78
116
|
* `download_incomplete` if it ends short of the file's length. */
|
|
79
|
-
async downloadFileStream(remotePath) {
|
|
117
|
+
async downloadFileStream(remotePath, timeoutOrOptions) {
|
|
118
|
+
const options = typeof timeoutOrOptions === "number"
|
|
119
|
+
? { timeout: timeoutOrOptions }
|
|
120
|
+
: (timeoutOrOptions ?? {});
|
|
121
|
+
const request = downloadRequest(options.timeout ?? 30 * 60, options.signal);
|
|
80
122
|
const { Readable } = await import("node:stream");
|
|
81
123
|
const target = await this.#path(remotePath);
|
|
82
124
|
const files = await this.#files();
|
|
83
|
-
|
|
125
|
+
request.signal?.throwIfAborted();
|
|
126
|
+
const stream = await guard("file", () => files.readStream(target, request));
|
|
84
127
|
return Readable.fromWeb(stream);
|
|
85
128
|
}
|
|
86
129
|
/** Several files; a file that fails carries `error` instead of `result`. */
|
|
87
|
-
async downloadFiles(files) {
|
|
130
|
+
async downloadFiles(files, timeout = 30 * 60) {
|
|
131
|
+
const request = downloadRequest(timeout);
|
|
88
132
|
const out = [];
|
|
89
133
|
for (const file of files) {
|
|
134
|
+
request.signal?.throwIfAborted();
|
|
90
135
|
try {
|
|
91
136
|
if (file.destination) {
|
|
92
|
-
await this
|
|
137
|
+
await this.#downloadFile(file.source, file.destination, request);
|
|
93
138
|
out.push({ source: file.source, result: file.destination });
|
|
94
139
|
}
|
|
95
140
|
else
|
|
96
|
-
out.push({
|
|
141
|
+
out.push({
|
|
142
|
+
source: file.source,
|
|
143
|
+
result: (await this.#downloadFile(file.source, undefined, request)),
|
|
144
|
+
});
|
|
97
145
|
}
|
|
98
146
|
catch (error) {
|
|
147
|
+
if (request.signal?.aborted)
|
|
148
|
+
throw error;
|
|
99
149
|
const failure = error;
|
|
100
150
|
out.push({
|
|
101
151
|
source: file.source,
|
|
@@ -185,24 +235,65 @@ export class FileSystem {
|
|
|
185
235
|
target,
|
|
186
236
|
]);
|
|
187
237
|
}
|
|
188
|
-
async uploadFile(source, remotePath) {
|
|
189
|
-
|
|
190
|
-
const bytes = await bytesOf(source);
|
|
191
|
-
const files = await this.#files();
|
|
192
|
-
await guard("file", () => files.write(target, bytes));
|
|
238
|
+
async uploadFile(source, remotePath, timeout = 30 * 60) {
|
|
239
|
+
await this.#uploadFile(source, remotePath, downloadRequest(timeout));
|
|
193
240
|
}
|
|
194
|
-
async
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
241
|
+
async #uploadFile(source, remotePath, request, onProgress) {
|
|
242
|
+
request.signal?.throwIfAborted();
|
|
243
|
+
const readable = typeof source !== "string" && typeof source.destroy === "function"
|
|
244
|
+
? source
|
|
245
|
+
: undefined;
|
|
246
|
+
const stopInput = () => {
|
|
247
|
+
readable?.destroy();
|
|
248
|
+
};
|
|
249
|
+
request.signal?.addEventListener("abort", stopInput, { once: true });
|
|
250
|
+
try {
|
|
251
|
+
const target = await waitForRequest(this.#path(remotePath), request);
|
|
252
|
+
request.signal?.throwIfAborted();
|
|
253
|
+
const files = await waitForRequest(this.#files(), request);
|
|
254
|
+
request.signal?.throwIfAborted();
|
|
255
|
+
// Node Readables can emit strings after setEncoding; Daytona uploads
|
|
256
|
+
// their UTF-8 bytes. The generator retains only the current source chunk.
|
|
257
|
+
const input = typeof source === "string" ||
|
|
258
|
+
source instanceof Uint8Array ||
|
|
259
|
+
typeof source.getReader === "function"
|
|
260
|
+
? source
|
|
261
|
+
: (async function* () {
|
|
262
|
+
for await (const chunk of source)
|
|
263
|
+
yield typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
264
|
+
})();
|
|
265
|
+
await guard("file", () => input instanceof Uint8Array && !onProgress
|
|
266
|
+
? files.write(target, input, request)
|
|
267
|
+
: files.writeStream(target, input, {
|
|
268
|
+
...request,
|
|
269
|
+
...(onProgress ? { onProgress } : {}),
|
|
270
|
+
}));
|
|
271
|
+
await this.#own(target);
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
request.signal?.removeEventListener("abort", stopInput);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** Uploads with bounded memory. Unknown-length streams stage in a private
|
|
278
|
+
* temporary file before the native atomic upload; local paths avoid staging. */
|
|
279
|
+
async uploadFileStream(source, remotePath, options = {}) {
|
|
280
|
+
const cancelled = () => new DaytonaError(`Upload cancelled: ${remotePath}`);
|
|
281
|
+
if (options.signal?.aborted)
|
|
282
|
+
throw cancelled();
|
|
283
|
+
const request = downloadRequest(options.timeout ?? 30 * 60, options.signal);
|
|
284
|
+
try {
|
|
285
|
+
await this.#uploadFile(source, remotePath, request, options.onProgress);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (options.signal?.aborted)
|
|
289
|
+
throw cancelled();
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
199
292
|
}
|
|
200
|
-
async uploadFiles(files) {
|
|
293
|
+
async uploadFiles(files, timeout = 30 * 60) {
|
|
294
|
+
const request = downloadRequest(timeout);
|
|
201
295
|
for (const file of files) {
|
|
202
|
-
|
|
203
|
-
const bytes = await bytesOf(file.source);
|
|
204
|
-
const api = await this.#files();
|
|
205
|
-
await guard("file", () => api.write(target, bytes));
|
|
296
|
+
await this.#uploadFile(file.source, file.destination, request);
|
|
206
297
|
}
|
|
207
298
|
}
|
|
208
299
|
}
|