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.
Files changed (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. 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
+ };
@@ -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
@@ -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 labels = params.language && params.language !== "python"
194
- ? { ...params.labels, [LANGUAGE_LABEL]: params.language }
195
- : params.labels && Object.keys(params.labels).length
196
- ? params.labels
197
- : undefined;
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 known = new Set(["limit", "name", "labels", "states"]);
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}`, "Filter by name, labels or states, and sort or narrow the result yourself.");
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 page = await guard("other", () => client.sandboxes.list(filter));
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 !== "daytona")
388
- throw new NotSupportedError(`Running as the user "${params.user}"`, 'Commands run as the sandbox owner with passwordless sudo: prefix a command with "sudo" to run it as root.');
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): Promise<Readable>;
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
- uploadFileStream(source: UploadSource, remotePath: string): Promise<void>;
85
- uploadFiles(files: FileUpload[]): Promise<void>;
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
- if (typeof localOrTimeout !== "string")
73
- return Buffer.from(await guard("file", () => files.read(target)));
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, localOrTimeout));
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
- const stream = await guard("file", () => files.readStream(target));
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.downloadFile(file.source, file.destination);
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({ source: file.source, result: await this.downloadFile(file.source) });
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
- const target = await this.#path(remotePath);
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 uploadFileStream(source, remotePath) {
195
- const target = await this.#path(remotePath);
196
- const bytes = await bytesOf(source);
197
- const files = await this.#files();
198
- await guard("file", () => files.write(target, bytes));
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
- const target = await this.#path(file.destination);
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
  }