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.
- package/DAYTONA.md +40 -34
- package/E2B.md +56 -57
- package/VERCEL.md +21 -19
- package/compatibility-lock.json +8 -84
- package/dist/cli.js +73 -2
- package/dist/client.d.ts +12 -0
- package/dist/client.js +27 -0
- package/dist/compat/lease.d.ts +36 -0
- package/dist/compat/lease.js +87 -0
- 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 +123 -16
- package/dist/daytona/filesystem.js +8 -0
- package/dist/daytona/index.d.ts +2 -26
- 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 +19 -5
- package/dist/daytona/sandbox.js +39 -25
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +5 -1
- package/dist/e2b/commands.d.ts +15 -6
- package/dist/e2b/commands.js +83 -18
- 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 +2 -2
- package/dist/e2b/index.js +1 -1
- package/dist/e2b/pty.js +6 -3
- package/dist/e2b/sandbox.d.ts +15 -5
- package/dist/e2b/sandbox.js +190 -28
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/index.d.ts +1 -0
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/network-cli.js +28 -0
- 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 +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/network-products.d.ts +7 -0
- package/dist/products/network-products.js +3 -0
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/sandbox.d.ts +22 -0
- package/dist/sandbox.js +47 -0
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/types.d.ts +5 -2
- 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 +47 -12
- package/dist/vercel/sandbox.js +201 -42
- 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
|
+
};
|
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";
|
|
@@ -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
|
|
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) {
|
|
@@ -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
|
|
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}`,
|
|
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
|
|
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
|
|
395
|
-
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);
|
|
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);
|