withruntime 0.10.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 (65) hide show
  1. package/DAYTONA.md +40 -34
  2. package/E2B.md +56 -57
  3. package/VERCEL.md +21 -19
  4. package/compatibility-lock.json +8 -84
  5. package/dist/cli.js +73 -2
  6. package/dist/client.d.ts +12 -0
  7. package/dist/client.js +27 -0
  8. package/dist/compat/lease.d.ts +36 -0
  9. package/dist/compat/lease.js +87 -0
  10. package/dist/daytona/computer-use.d.ts +162 -0
  11. package/dist/daytona/computer-use.js +324 -0
  12. package/dist/daytona/context.d.ts +10 -0
  13. package/dist/daytona/context.js +24 -0
  14. package/dist/daytona/daytona.d.ts +20 -0
  15. package/dist/daytona/daytona.js +123 -16
  16. package/dist/daytona/filesystem.js +8 -0
  17. package/dist/daytona/index.d.ts +2 -26
  18. package/dist/daytona/index.js +2 -7
  19. package/dist/daytona/process.d.ts +63 -6
  20. package/dist/daytona/process.js +224 -23
  21. package/dist/daytona/sandbox.d.ts +19 -5
  22. package/dist/daytona/sandbox.js +39 -25
  23. package/dist/e2b/client.d.ts +5 -0
  24. package/dist/e2b/client.js +5 -1
  25. package/dist/e2b/commands.d.ts +15 -6
  26. package/dist/e2b/commands.js +83 -18
  27. package/dist/e2b/errors.d.ts +7 -0
  28. package/dist/e2b/errors.js +20 -0
  29. package/dist/e2b/filesystem.d.ts +2 -1
  30. package/dist/e2b/filesystem.js +43 -6
  31. package/dist/e2b/index.d.ts +2 -2
  32. package/dist/e2b/index.js +1 -1
  33. package/dist/e2b/pty.js +6 -3
  34. package/dist/e2b/sandbox.d.ts +15 -5
  35. package/dist/e2b/sandbox.js +190 -28
  36. package/dist/e2b/users.d.ts +30 -0
  37. package/dist/e2b/users.js +185 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/mcp.d.ts +2 -1
  40. package/dist/mcp.js +6 -1
  41. package/dist/network-cli.js +28 -0
  42. package/dist/products/browser.d.ts +60 -0
  43. package/dist/products/browser.js +37 -0
  44. package/dist/products/desktop.d.ts +2 -2
  45. package/dist/products/desktop.js +2 -2
  46. package/dist/products/images.d.ts +3 -1
  47. package/dist/products/images.js +14 -2
  48. package/dist/products/index.d.ts +2 -0
  49. package/dist/products/index.js +2 -0
  50. package/dist/products/network-products.d.ts +7 -0
  51. package/dist/products/network-products.js +3 -0
  52. package/dist/products/private-network.d.ts +22 -0
  53. package/dist/products/private-network.js +10 -0
  54. package/dist/sandbox.d.ts +22 -0
  55. package/dist/sandbox.js +47 -0
  56. package/dist/transport.d.ts +1 -1
  57. package/dist/transport.js +1 -1
  58. package/dist/types.d.ts +5 -2
  59. package/dist/vercel/index.d.ts +1 -7
  60. package/dist/vercel/index.js +1 -3
  61. package/dist/vercel/sandbox-user.d.ts +70 -0
  62. package/dist/vercel/sandbox-user.js +206 -0
  63. package/dist/vercel/sandbox.d.ts +47 -12
  64. package/dist/vercel/sandbox.js +201 -42
  65. package/package.json +1 -1
@@ -0,0 +1,324 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { basename, dirname } from "node:path";
3
+ import { RuntimeError } from "../errors.js";
4
+ import { guard, NotSupportedError } from "./errors.js";
5
+ const ALTERNATIVE_STREAM = "Open the live desktop from `(await sandbox.withruntime.desktop.start()).streamUrl`.";
6
+ async function desktop(live) {
7
+ return (await live()).desktop;
8
+ }
9
+ /** `sandbox.computerUse.mouse`. */
10
+ export class Mouse {
11
+ #live;
12
+ /** Use sandbox.computerUse.mouse. */
13
+ constructor(live) {
14
+ this.#live = live;
15
+ }
16
+ async getPosition() {
17
+ const { x, y } = await guard("other", async () => (await desktop(this.#live)).cursor());
18
+ return { x, y };
19
+ }
20
+ async move(x, y) {
21
+ await guard("other", async () => (await desktop(this.#live)).move(x, y));
22
+ return { x, y };
23
+ }
24
+ async click(x, y, button = "left", double = false) {
25
+ await guard("other", async () => (await desktop(this.#live)).click(x, y, { button: buttonOf(button), double }));
26
+ return { x, y };
27
+ }
28
+ async drag(startX, startY, endX, endY, button = "left") {
29
+ const which = buttonOf(button);
30
+ await guard("other", async () => {
31
+ const screen = await desktop(this.#live);
32
+ if (which === "left")
33
+ return screen.drag([startX, startY], [endX, endY]);
34
+ await screen.move(startX, startY);
35
+ await screen.mouseDown(which);
36
+ await screen.move(endX, endY);
37
+ return screen.mouseUp(which);
38
+ });
39
+ return { x: endX, y: endY };
40
+ }
41
+ /** `amount` wheel clicks up or down at (x, y). */
42
+ async scroll(x, y, direction, amount = 1) {
43
+ if (direction !== "up" && direction !== "down")
44
+ throw new RangeError(`direction is "up" or "down", not ${JSON.stringify(direction)}.`);
45
+ await guard("other", async () => (await desktop(this.#live)).scroll(direction === "down" ? amount : -amount, { x, y }));
46
+ return true;
47
+ }
48
+ }
49
+ /** `sandbox.computerUse.keyboard`. Key names are Daytona's (enter, esc,
50
+ * ctrl, cmd, pageup, f5) or xdotool's (Return, Escape, Prior). */
51
+ export class Keyboard {
52
+ #live;
53
+ /** Use sandbox.computerUse.keyboard. */
54
+ constructor(live) {
55
+ this.#live = live;
56
+ }
57
+ /** Types `text`, `delay` milliseconds between keys. */
58
+ async type(text, delay) {
59
+ await guard("other", async () => (await desktop(this.#live)).type(text, delay === undefined ? {} : { delayMs: delay }));
60
+ }
61
+ async press(key, modifiers = []) {
62
+ await guard("other", async () => (await desktop(this.#live)).press([...modifiers, key].map(keyOf).join("+")));
63
+ }
64
+ /** A combination such as "ctrl+c" or "ctrl+shift+t". */
65
+ async hotkey(keys) {
66
+ await guard("other", async () => (await desktop(this.#live)).press(keys.split("+").map(keyOf).join("+")));
67
+ }
68
+ }
69
+ /** `sandbox.computerUse.screenshot`: the whole screen, PNG or JPEG. */
70
+ export class Screenshot {
71
+ #live;
72
+ /** Use sandbox.computerUse.screenshot. */
73
+ constructor(live) {
74
+ this.#live = live;
75
+ }
76
+ takeFullScreen(showCursor = false) {
77
+ return this.takeCompressed({ showCursor, format: "png" });
78
+ }
79
+ takeRegion(_region, _showCursor) {
80
+ return Promise.reject(region());
81
+ }
82
+ async takeCompressed(options = {}) {
83
+ if (options.showCursor)
84
+ throw new NotSupportedError("Drawing the cursor into a screenshot (showCursor)", "Take it without; sandbox.computerUse.mouse.getPosition() says where the cursor is.");
85
+ if (options.scale !== undefined && options.scale !== 1)
86
+ throw new NotSupportedError("Scaling a screenshot", "Take it at full size and scale it yourself, or start the desktop smaller: sandbox.withruntime.desktop.start({ width, height }).");
87
+ const format = (options.format ?? "png").toLowerCase();
88
+ if (format !== "png" && format !== "jpeg" && format !== "jpg")
89
+ throw new NotSupportedError(`Screenshots as ${format}`, 'Use format "png" or "jpeg".');
90
+ const bytes = await guard("other", async () => (await desktop(this.#live)).screenshot(format === "png"
91
+ ? {}
92
+ : {
93
+ format: "jpeg",
94
+ ...(options.quality === undefined ? {} : { quality: options.quality }),
95
+ }));
96
+ return { screenshot: Buffer.from(bytes).toString("base64"), sizeBytes: bytes.byteLength };
97
+ }
98
+ takeCompressedRegion(_region, _options) {
99
+ return Promise.reject(region());
100
+ }
101
+ }
102
+ /** `sandbox.computerUse.display`. */
103
+ export class Display {
104
+ #live;
105
+ /** Use sandbox.computerUse.display. */
106
+ constructor(live) {
107
+ this.#live = live;
108
+ }
109
+ /** The one screen, its size read from a screenshot. */
110
+ async getInfo() {
111
+ const png = await guard("other", async () => (await desktop(this.#live)).screenshot());
112
+ const { width, height } = pngSize(png);
113
+ return { displays: [{ id: 0, x: 0, y: 0, width, height, isActive: true }] };
114
+ }
115
+ async getWindows() {
116
+ const windows = await guard("other", async () => (await desktop(this.#live)).windows());
117
+ return {
118
+ windows: windows.map((one) => ({
119
+ id: Number(one.id),
120
+ title: one.title,
121
+ x: one.x,
122
+ y: one.y,
123
+ width: one.width,
124
+ height: one.height,
125
+ })),
126
+ };
127
+ }
128
+ }
129
+ /** `sandbox.computerUse.recording`: screen recordings to MP4. */
130
+ export class RecordingService {
131
+ #live;
132
+ /** Use sandbox.computerUse.recording. */
133
+ constructor(live) {
134
+ this.#live = live;
135
+ }
136
+ /** Starts recording. Runtime names the file itself; `label` is not used. */
137
+ async start(_label) {
138
+ return recordingOf(await guard("other", async () => (await desktop(this.#live)).recordings.start()));
139
+ }
140
+ async stop(id) {
141
+ return recordingOf(await guard("other", async () => (await desktop(this.#live)).recordings.stop(id)));
142
+ }
143
+ async list() {
144
+ const all = await guard("other", async () => (await desktop(this.#live)).recordings.list());
145
+ return { recordings: all.map(recordingOf) };
146
+ }
147
+ async get(id) {
148
+ return recordingOf(await guard("other", async () => (await desktop(this.#live)).recordings.get(id)));
149
+ }
150
+ async delete(id) {
151
+ await guard("other", async () => (await desktop(this.#live)).recordings.delete(id));
152
+ }
153
+ /** Saves the MP4 to `localPath`, making its directory. */
154
+ async download(id, localPath) {
155
+ const bytes = await guard("other", async () => (await desktop(this.#live)).recordings.download(id));
156
+ await mkdir(dirname(localPath), { recursive: true });
157
+ await writeFile(localPath, bytes);
158
+ }
159
+ }
160
+ /** `sandbox.computerUse.accessibility`: Runtime's desktop has no
161
+ * accessibility tree; every method throws NotSupportedError. */
162
+ export class Accessibility {
163
+ #refuse() {
164
+ return Promise.reject(new NotSupportedError("The desktop's accessibility tree", "Find things on screen with a screenshot (sandbox.computerUse.screenshot.takeFullScreen()) and act with the mouse and keyboard."));
165
+ }
166
+ getTree(_options) {
167
+ return this.#refuse();
168
+ }
169
+ findNodes(_options) {
170
+ return this.#refuse();
171
+ }
172
+ focusNode(_id) {
173
+ return this.#refuse();
174
+ }
175
+ invokeNode(_id, _action) {
176
+ return this.#refuse();
177
+ }
178
+ setNodeValue(_id, _value) {
179
+ return this.#refuse();
180
+ }
181
+ }
182
+ /** `sandbox.computerUse`: a Linux desktop driven like a person would. */
183
+ export class ComputerUse {
184
+ mouse;
185
+ keyboard;
186
+ screenshot;
187
+ display;
188
+ recording;
189
+ accessibility;
190
+ #live;
191
+ /** Use sandbox.computerUse. */
192
+ constructor(live) {
193
+ this.#live = live;
194
+ this.mouse = new Mouse(live);
195
+ this.keyboard = new Keyboard(live);
196
+ this.screenshot = new Screenshot(live);
197
+ this.display = new Display(live);
198
+ this.recording = new RecordingService(live);
199
+ this.accessibility = new Accessibility();
200
+ }
201
+ /** Starts the desktop. A sandbox's first start installs it, which takes a
202
+ * minute or two; this waits for that. */
203
+ async start() {
204
+ await guard("other", async () => (await desktop(this.#live)).start());
205
+ return {
206
+ message: "Computer use processes started successfully",
207
+ status: { desktop: { running: true } },
208
+ };
209
+ }
210
+ async stop() {
211
+ await guard("other", async () => (await desktop(this.#live)).stop());
212
+ return {
213
+ message: "Computer use processes stopped successfully",
214
+ status: { desktop: { running: false } },
215
+ };
216
+ }
217
+ /** "active" while the desktop runs, else "inactive". */
218
+ async getStatus() {
219
+ try {
220
+ await (await desktop(this.#live)).cursor();
221
+ return { status: "active" };
222
+ }
223
+ catch (error) {
224
+ if (error instanceof RuntimeError &&
225
+ (error.code === "desktop_not_running" || error.code === "desktop_not_installed"))
226
+ return { status: "inactive" };
227
+ return guard("other", () => {
228
+ throw error;
229
+ });
230
+ }
231
+ }
232
+ getProcessStatus(_processName) {
233
+ return processes();
234
+ }
235
+ restartProcess(_processName) {
236
+ return processes();
237
+ }
238
+ getProcessLogs(_processName) {
239
+ return processes();
240
+ }
241
+ getProcessErrors(_processName) {
242
+ return processes();
243
+ }
244
+ }
245
+ function processes() {
246
+ return Promise.reject(new NotSupportedError("Managing the desktop's processes one by one", `Use computerUse.getStatus(), stop() and start(). ${ALTERNATIVE_STREAM}`));
247
+ }
248
+ function region() {
249
+ return new NotSupportedError("Screenshots of part of the screen", "Take the whole screen with takeFullScreen() or takeCompressed() and crop it yourself.");
250
+ }
251
+ function buttonOf(button) {
252
+ if (button === "left" || button === "middle" || button === "right")
253
+ return button;
254
+ throw new RangeError(`button is "left", "middle" or "right", not ${JSON.stringify(button)}.`);
255
+ }
256
+ /** Daytona's key names as xdotool's; anything else passes as given. */
257
+ const KEYS = {
258
+ enter: "Return",
259
+ return: "Return",
260
+ esc: "Escape",
261
+ escape: "Escape",
262
+ backspace: "BackSpace",
263
+ delete: "Delete",
264
+ del: "Delete",
265
+ tab: "Tab",
266
+ space: "space",
267
+ up: "Up",
268
+ down: "Down",
269
+ left: "Left",
270
+ right: "Right",
271
+ home: "Home",
272
+ end: "End",
273
+ pageup: "Prior",
274
+ pagedown: "Next",
275
+ insert: "Insert",
276
+ capslock: "Caps_Lock",
277
+ control: "ctrl",
278
+ ctrl: "ctrl",
279
+ shift: "shift",
280
+ alt: "alt",
281
+ cmd: "super",
282
+ command: "super",
283
+ meta: "super",
284
+ win: "super",
285
+ super: "super",
286
+ };
287
+ function keyOf(key) {
288
+ const name = key.trim();
289
+ const lower = name.toLowerCase();
290
+ if (KEYS[lower])
291
+ return KEYS[lower];
292
+ if (/^f([1-9]|1[0-2])$/.test(lower))
293
+ return lower.toUpperCase();
294
+ return name;
295
+ }
296
+ const STATUS = {
297
+ starting: "recording",
298
+ installing: "recording",
299
+ recording: "recording",
300
+ finished: "completed",
301
+ failed: "failed",
302
+ };
303
+ function recordingOf(recording) {
304
+ const start = recording.startedAt ?? null;
305
+ return {
306
+ id: recording.id,
307
+ fileName: basename(recording.path),
308
+ filePath: recording.path,
309
+ startTime: start === null ? "" : new Date(start).toISOString(),
310
+ ...(start !== null && recording.seconds !== undefined && STATUS[recording.state] !== "recording"
311
+ ? { endTime: new Date(start + recording.seconds * 1000).toISOString() }
312
+ : {}),
313
+ status: STATUS[recording.state] ?? recording.state,
314
+ sizeBytes: recording.bytes,
315
+ ...(recording.seconds === undefined ? {} : { durationSeconds: recording.seconds }),
316
+ };
317
+ }
318
+ /** Width and height from a PNG's header. */
319
+ function pngSize(png) {
320
+ const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
321
+ if (png.byteLength < 24 || view.getUint32(12) !== 0x49484452)
322
+ throw new RangeError("The desktop's screenshot was not a PNG.");
323
+ return { width: view.getUint32(16), height: view.getUint32(20) };
324
+ }
@@ -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";
@@ -190,11 +194,15 @@ export class Daytona {
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) {
@@ -286,12 +297,15 @@ export class Daytona {
286
297
  }
287
298
  #adopt(runtime) {
288
299
  const onLeaseEnd = runtime.info.onLeaseEnd;
300
+ const user = runtime.info.labels[USER_LABEL];
289
301
  return new Sandbox(runtime, this.#client, {
290
302
  env: {},
291
303
  language: runtime.info.labels[LANGUAGE_LABEL] ?? "python",
292
304
  public: false,
305
+ ...(user ? { user } : {}),
293
306
  lifecycle: {
294
307
  windowSeconds: runtime.info.timeoutSeconds,
308
+ idleSeconds: runtime.info.timeoutSeconds,
295
309
  ephemeral: onLeaseEnd === "stop",
296
310
  autoStopInterval: Math.round(runtime.info.timeoutSeconds / 60),
297
311
  autoArchiveInterval: 0,
@@ -302,10 +316,15 @@ export class Daytona {
302
316
  /** Live and stopped (paused) sandboxes, oldest first. Await it for the old
303
317
  * paginated shape, `{ items, total, page, totalPages }`. */
304
318
  list(query = {}) {
305
- const known = new Set(["limit", "name", "labels", "states"]);
306
- 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));
307
320
  if (other)
308
- 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".');
309
328
  const states = query.states?.flatMap((state) => state === "started"
310
329
  ? ["running"]
311
330
  : state === "starting"
@@ -315,15 +334,24 @@ export class Daytona {
315
334
  : state === "stopped" || state === "paused"
316
335
  ? ["paused"]
317
336
  : []);
318
- const client = this.#client;
319
- const adopt = (runtime) => this.#adopt(runtime);
320
337
  const filter = {
321
338
  ...(query.labels ? { labels: query.labels } : {}),
322
- ...(query.name ? { name: query.name } : {}),
323
339
  ...(states ? { state: states } : {}),
324
340
  ...(query.limit ? { limit: Math.min(query.limit, 100) } : {}),
325
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
+ };
326
349
  async function* iterate() {
350
+ if (here) {
351
+ for (const runtime of await everything())
352
+ yield adopt(runtime);
353
+ return;
354
+ }
327
355
  const page = await guard("other", () => client.sandboxes.list(filter));
328
356
  for await (const runtime of page)
329
357
  yield adopt(runtime);
@@ -332,8 +360,7 @@ export class Daytona {
332
360
  return Object.assign(iterator, {
333
361
  then(onFulfilled, onRejected) {
334
362
  const all = (async () => {
335
- const page = await guard("other", () => client.sandboxes.list(filter));
336
- const items = (await page.toArray(10_000)).map(adopt);
363
+ const items = (await everything()).map(adopt);
337
364
  return { items, total: items.length, page: 1, totalPages: 1 };
338
365
  })();
339
366
  return all.then(onFulfilled, onRejected);
@@ -391,8 +418,8 @@ function refuseCreate(params) {
391
418
  for (const [field, feature, alternative] of refusals)
392
419
  if (params[field] !== undefined)
393
420
  throw new NotSupportedError(feature, alternative);
394
- if (params.user !== undefined && params.user !== "daytona")
395
- 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);
396
423
  if (params.language !== undefined &&
397
424
  !["python", "javascript", "typescript"].includes(params.language))
398
425
  throw new NotSupportedError(`The language ${params.language}`, "Use python, javascript or typescript; run anything else with process.executeCommand.");
@@ -408,6 +435,7 @@ export function lifecycleOf(params) {
408
435
  const autoDelete = params.ephemeral ? 0 : (params.autoDeleteInterval ?? -1);
409
436
  return {
410
437
  windowSeconds: windowSeconds(autoStop),
438
+ idleSeconds: idleSeconds(autoStop),
411
439
  ...(params.ttlMinutes ? { deadline: Date.now() + params.ttlMinutes * 60_000 } : {}),
412
440
  ephemeral: autoDelete === 0,
413
441
  autoStopInterval: autoStop,
@@ -415,6 +443,85 @@ export function lifecycleOf(params) {
415
443
  autoDeleteInterval: autoDelete,
416
444
  };
417
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
+ }
418
525
  /** The sandbox with this Runtime id, or the newest live one with this name. */
419
526
  async function find(client, idOrName) {
420
527
  if (UUID.test(idOrName))
@@ -81,6 +81,13 @@ export class FileSystem {
81
81
  async createFolder(path, mode) {
82
82
  const target = await this.#path(path);
83
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]);
84
91
  }
85
92
  async deleteFile(path, recursive = false) {
86
93
  const target = await this.#path(path);
@@ -261,6 +268,7 @@ export class FileSystem {
261
268
  ...request,
262
269
  ...(onProgress ? { onProgress } : {}),
263
270
  }));
271
+ await this.#own(target);
264
272
  }
265
273
  finally {
266
274
  request.signal?.removeEventListener("abort", stopInput);