withruntime 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BLAXEL.md +4 -4
- package/DAYTONA.md +52 -40
- package/E2B.md +65 -62
- package/README.md +15 -7
- package/README.src.md +15 -7
- package/VERCEL.md +34 -22
- package/compatibility-lock.json +16 -16
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +2 -1
- package/dist/blaxel/preview.js +15 -13
- package/dist/blaxel/process.js +56 -10
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +262 -56
- package/dist/client.d.ts +41 -3
- package/dist/client.js +67 -11
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.js +47 -9
- package/dist/cloudflare/worker.d.ts +4 -0
- package/dist/cloudflare/worker.js +166 -16
- package/dist/codesandbox/guest-service.js +57 -20
- package/dist/codesandbox/index.js +42 -8
- package/dist/compat/core.d.ts +9 -8
- package/dist/compat/core.js +60 -21
- package/dist/compat/files.js +22 -4
- package/dist/compat/lease.d.ts +36 -0
- package/dist/compat/lease.js +87 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/computer-use.d.ts +162 -0
- package/dist/daytona/computer-use.js +324 -0
- package/dist/daytona/context.d.ts +10 -0
- package/dist/daytona/context.js +24 -0
- package/dist/daytona/daytona.d.ts +20 -0
- package/dist/daytona/daytona.js +134 -20
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +129 -38
- package/dist/daytona/index.d.ts +3 -27
- package/dist/daytona/index.js +2 -7
- package/dist/daytona/process.d.ts +63 -6
- package/dist/daytona/process.js +224 -23
- package/dist/daytona/sandbox.d.ts +20 -6
- package/dist/daytona/sandbox.js +55 -32
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +6 -2
- package/dist/e2b/commands.d.ts +43 -12
- package/dist/e2b/commands.js +217 -64
- package/dist/e2b/errors.d.ts +7 -0
- package/dist/e2b/errors.js +20 -0
- package/dist/e2b/filesystem.d.ts +2 -1
- package/dist/e2b/filesystem.js +43 -6
- package/dist/e2b/index.d.ts +3 -2
- package/dist/e2b/index.js +2 -1
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +83 -0
- package/dist/e2b/sandbox.d.ts +17 -6
- package/dist/e2b/sandbox.js +239 -43
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +9 -1
- package/dist/freestyle/index.js +38 -5
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/modal/index.js +2 -1
- package/dist/network-cli.js +139 -43
- package/dist/openai-agents/index.js +9 -45
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/browser.d.ts +60 -0
- package/dist/products/browser.js +37 -0
- package/dist/products/desktop.d.ts +2 -2
- package/dist/products/desktop.js +2 -2
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/network-products.d.ts +7 -0
- package/dist/products/network-products.js +3 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +8 -2
- package/dist/products/previews.js +11 -3
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.js +61 -8
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +13 -27
- package/dist/runloop/index.js +149 -52
- package/dist/sandbox.d.ts +59 -8
- package/dist/sandbox.js +479 -68
- package/dist/snapshots.d.ts +9 -4
- package/dist/sprites/index.js +69 -33
- package/dist/tar.d.ts +3 -1
- package/dist/tar.js +140 -14
- package/dist/transport.d.ts +4 -7
- package/dist/transport.js +205 -122
- package/dist/types.d.ts +5 -2
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +3 -2
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +7 -1
- package/dist/vercel/index.d.ts +1 -7
- package/dist/vercel/index.js +1 -3
- package/dist/vercel/sandbox-user.d.ts +70 -0
- package/dist/vercel/sandbox-user.js +206 -0
- package/dist/vercel/sandbox.d.ts +55 -20
- package/dist/vercel/sandbox.js +353 -99
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +2 -1
package/dist/credentials.js
CHANGED
|
@@ -12,6 +12,14 @@ export function connectionOrigins(env) {
|
|
|
12
12
|
auth: apiOrigin(env.RUNTIME_AUTH_URL ?? "https://withruntime.com"),
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
/** Browser-issued production credentials must only reach the production API.
|
|
16
|
+
* CLI and MCP share this boundary, including resumed device requests. */
|
|
17
|
+
export function checkBrowserOrigins(origins) {
|
|
18
|
+
const local = (origin) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname);
|
|
19
|
+
if (!((origins.auth === "https://withruntime.com" && origins.api === "https://api.withruntime.com") ||
|
|
20
|
+
(local(origins.auth) && local(origins.api))))
|
|
21
|
+
throw new Error("Browser login supports Runtime's production endpoints or local test endpoints. Check RUNTIME_AUTH_URL and RUNTIME_API_URL.");
|
|
22
|
+
}
|
|
15
23
|
function defaultDirectory(env) {
|
|
16
24
|
const root = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
17
25
|
if (!isAbsolute(root))
|
|
@@ -93,8 +101,75 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
93
101
|
throw error;
|
|
94
102
|
});
|
|
95
103
|
}
|
|
104
|
+
const lockFile = join(directory, `${name}.lock`);
|
|
105
|
+
/** ARCHITECTURE.md section 3.9: concurrent CLI processes keep each account's
|
|
106
|
+
* connection. Serialize the entire read/modify/write, not only each rename.
|
|
107
|
+
* Never steal a lock: stale cleanup can otherwise delete a new live lock. */
|
|
108
|
+
async function updateAccounts(update) {
|
|
109
|
+
await privateDirectory(true);
|
|
110
|
+
const until = performance.now() + 10_000;
|
|
111
|
+
let handle;
|
|
112
|
+
for (;;) {
|
|
113
|
+
try {
|
|
114
|
+
handle = await open(lockFile, "wx", 0o600);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code !== "EEXIST")
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
const text = await readPrivate(lockFile, 1024, "credential update lock");
|
|
122
|
+
let pid;
|
|
123
|
+
try {
|
|
124
|
+
pid = JSON.parse(text ?? "null")?.pid;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// The holder may not have finished writing its process id yet.
|
|
128
|
+
}
|
|
129
|
+
if (typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code === "ESRCH")
|
|
135
|
+
throw new Error(`A previous Runtime credential update was interrupted. Remove ${lockFile} and retry.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (performance.now() >= until)
|
|
139
|
+
throw new Error(`Another Runtime command is updating saved accounts. Retry when it finishes. If no command is running, remove ${lockFile} and retry.`);
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid }));
|
|
144
|
+
return await update();
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
try {
|
|
148
|
+
await handle.close();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await unlink(lockFile);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
96
155
|
const pendingFile = join(directory, `${name}.pending.json`);
|
|
97
156
|
const accountsFile = join(directory, `${name}.accounts.json`);
|
|
157
|
+
/** Active and archived connections must satisfy the same contract: switching
|
|
158
|
+
* accounts cannot publish a credential the next command refuses to read. */
|
|
159
|
+
function validConnection(value) {
|
|
160
|
+
if (value === null || typeof value !== "object")
|
|
161
|
+
return false;
|
|
162
|
+
const saved = value;
|
|
163
|
+
return (saved.version === 1 &&
|
|
164
|
+
saved.apiOrigin === origins.api &&
|
|
165
|
+
saved.authOrigin === origins.auth &&
|
|
166
|
+
typeof saved.key === "string" &&
|
|
167
|
+
/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) &&
|
|
168
|
+
typeof saved.connectionId === "string" &&
|
|
169
|
+
typeof saved.orgId === "string" &&
|
|
170
|
+
typeof saved.agentName === "string" &&
|
|
171
|
+
(saved.orgName === undefined || typeof saved.orgName === "string"));
|
|
172
|
+
}
|
|
98
173
|
return {
|
|
99
174
|
async read() {
|
|
100
175
|
const text = await readPrivate(file, 4096, "saved connection");
|
|
@@ -107,15 +182,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
107
182
|
catch {
|
|
108
183
|
throw new Error("Runtime's saved connection is invalid. Connect again.");
|
|
109
184
|
}
|
|
110
|
-
if (saved
|
|
111
|
-
saved.apiOrigin !== origins.api ||
|
|
112
|
-
saved.authOrigin !== origins.auth ||
|
|
113
|
-
typeof saved.key !== "string" ||
|
|
114
|
-
!/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) ||
|
|
115
|
-
typeof saved.connectionId !== "string" ||
|
|
116
|
-
typeof saved.orgId !== "string" ||
|
|
117
|
-
typeof saved.agentName !== "string" ||
|
|
118
|
-
(saved.orgName !== undefined && typeof saved.orgName !== "string"))
|
|
185
|
+
if (!validConnection(saved))
|
|
119
186
|
throw new Error("Runtime's saved connection is invalid. Connect again.");
|
|
120
187
|
return saved;
|
|
121
188
|
},
|
|
@@ -125,12 +192,14 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
125
192
|
async save(connection) {
|
|
126
193
|
if (connection.apiOrigin !== origins.api || connection.authOrigin !== origins.auth)
|
|
127
194
|
throw new Error("Connection origins do not match.");
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
195
|
+
return updateAccounts(async () => {
|
|
196
|
+
const previous = await this.read().catch(() => null);
|
|
197
|
+
if (previous && previous.orgId !== connection.orgId) {
|
|
198
|
+
const others = (await this.others()).filter((one) => one.orgId !== previous.orgId && one.orgId !== connection.orgId);
|
|
199
|
+
await writePrivate(accountsFile, JSON.stringify([previous, ...others].slice(0, SAVED_ACCOUNTS)));
|
|
200
|
+
}
|
|
201
|
+
await writePrivate(file, JSON.stringify(connection));
|
|
202
|
+
});
|
|
134
203
|
},
|
|
135
204
|
/** The other accounts this machine has connected, newest first. */
|
|
136
205
|
async others() {
|
|
@@ -145,15 +214,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
145
214
|
return [];
|
|
146
215
|
try {
|
|
147
216
|
const list = JSON.parse(text);
|
|
148
|
-
return Array.isArray(list)
|
|
149
|
-
? list.filter((one) => one &&
|
|
150
|
-
one.version === 1 &&
|
|
151
|
-
one.apiOrigin === origins.api &&
|
|
152
|
-
one.authOrigin === origins.auth &&
|
|
153
|
-
typeof one.key === "string" &&
|
|
154
|
-
/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(one.key) &&
|
|
155
|
-
typeof one.orgId === "string")
|
|
156
|
-
: [];
|
|
217
|
+
return Array.isArray(list) ? list.filter(validConnection) : [];
|
|
157
218
|
}
|
|
158
219
|
catch {
|
|
159
220
|
return [];
|
|
@@ -162,21 +223,29 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
162
223
|
/** Use another saved account's connection, by account id or name. The one
|
|
163
224
|
* in use moves to the saved accounts. Null when none matches. */
|
|
164
225
|
async use(which) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
others.find((one) => one.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
return updateAccounts(async () => {
|
|
227
|
+
const others = await this.others();
|
|
228
|
+
const wanted = which.trim().toLowerCase();
|
|
229
|
+
const chosen = others.find((one) => one.orgId === which) ??
|
|
230
|
+
others.find((one) => one.orgName?.toLowerCase() === wanted) ??
|
|
231
|
+
null;
|
|
232
|
+
if (!chosen)
|
|
233
|
+
return null;
|
|
234
|
+
const current = await this.read().catch(() => null);
|
|
235
|
+
const rest = others.filter((one) => one.orgId !== chosen.orgId);
|
|
236
|
+
await writePrivate(accountsFile, JSON.stringify((current ? [current, ...rest] : rest).slice(0, SAVED_ACCOUNTS)));
|
|
237
|
+
await writePrivate(file, JSON.stringify(chosen));
|
|
238
|
+
return chosen;
|
|
239
|
+
});
|
|
177
240
|
},
|
|
178
|
-
|
|
179
|
-
|
|
241
|
+
/** Forget the expected credential after a remote revoke or refusal. A
|
|
242
|
+
* concurrent login may have saved another connection during that request. */
|
|
243
|
+
async remove(expectedKey) {
|
|
244
|
+
await updateAccounts(async () => {
|
|
245
|
+
if (expectedKey !== undefined && (await this.read())?.key !== expectedKey)
|
|
246
|
+
return;
|
|
247
|
+
await removePrivate(file);
|
|
248
|
+
});
|
|
180
249
|
},
|
|
181
250
|
/** A browser approval that was started and not yet answered, so a command
|
|
182
251
|
* that was stopped while it waited picks up the same link and code rather
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { RuntimeSandbox } from "./client.js";
|
|
2
|
+
type Live = () => Promise<RuntimeSandbox>;
|
|
3
|
+
export interface ScreenshotRegion {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ScreenshotOptions {
|
|
10
|
+
showCursor?: boolean;
|
|
11
|
+
format?: string;
|
|
12
|
+
quality?: number;
|
|
13
|
+
scale?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Position {
|
|
16
|
+
x?: number;
|
|
17
|
+
y?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ScreenshotResponse {
|
|
20
|
+
cursorPosition?: Position;
|
|
21
|
+
/** The image, base64. */
|
|
22
|
+
screenshot?: string;
|
|
23
|
+
sizeBytes?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface DisplayInfo {
|
|
26
|
+
height?: number;
|
|
27
|
+
id?: number;
|
|
28
|
+
isActive?: boolean;
|
|
29
|
+
width?: number;
|
|
30
|
+
x?: number;
|
|
31
|
+
y?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface WindowInfo {
|
|
34
|
+
height?: number;
|
|
35
|
+
id?: number;
|
|
36
|
+
isActive?: boolean;
|
|
37
|
+
title?: string;
|
|
38
|
+
width?: number;
|
|
39
|
+
x?: number;
|
|
40
|
+
y?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ProcessStatus {
|
|
43
|
+
autoRestart?: boolean;
|
|
44
|
+
pid?: number;
|
|
45
|
+
priority?: number;
|
|
46
|
+
running?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface ComputerUseStartResponse {
|
|
49
|
+
message?: string;
|
|
50
|
+
status?: Record<string, ProcessStatus>;
|
|
51
|
+
}
|
|
52
|
+
export type ComputerUseStopResponse = ComputerUseStartResponse;
|
|
53
|
+
export interface ComputerUseStatusResponse {
|
|
54
|
+
status?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface Recording {
|
|
57
|
+
durationSeconds?: number;
|
|
58
|
+
endTime?: string;
|
|
59
|
+
fileName: string;
|
|
60
|
+
filePath: string;
|
|
61
|
+
id: string;
|
|
62
|
+
sizeBytes?: number;
|
|
63
|
+
startTime: string;
|
|
64
|
+
status: string;
|
|
65
|
+
}
|
|
66
|
+
export interface ListRecordingsResponse {
|
|
67
|
+
recordings: Recording[];
|
|
68
|
+
}
|
|
69
|
+
/** `sandbox.computerUse.mouse`. */
|
|
70
|
+
export declare class Mouse {
|
|
71
|
+
#private;
|
|
72
|
+
/** Use sandbox.computerUse.mouse. */
|
|
73
|
+
constructor(live: Live);
|
|
74
|
+
getPosition(): Promise<Position>;
|
|
75
|
+
move(x: number, y: number): Promise<Position>;
|
|
76
|
+
click(x: number, y: number, button?: string, double?: boolean): Promise<Position>;
|
|
77
|
+
drag(startX: number, startY: number, endX: number, endY: number, button?: string): Promise<Position>;
|
|
78
|
+
/** `amount` wheel clicks up or down at (x, y). */
|
|
79
|
+
scroll(x: number, y: number, direction: "up" | "down", amount?: number): Promise<boolean>;
|
|
80
|
+
}
|
|
81
|
+
/** `sandbox.computerUse.keyboard`. Key names are Daytona's (enter, esc,
|
|
82
|
+
* ctrl, cmd, pageup, f5) or xdotool's (Return, Escape, Prior). */
|
|
83
|
+
export declare class Keyboard {
|
|
84
|
+
#private;
|
|
85
|
+
/** Use sandbox.computerUse.keyboard. */
|
|
86
|
+
constructor(live: Live);
|
|
87
|
+
/** Types `text`, `delay` milliseconds between keys. */
|
|
88
|
+
type(text: string, delay?: number): Promise<void>;
|
|
89
|
+
press(key: string, modifiers?: string[]): Promise<void>;
|
|
90
|
+
/** A combination such as "ctrl+c" or "ctrl+shift+t". */
|
|
91
|
+
hotkey(keys: string): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
/** `sandbox.computerUse.screenshot`: the whole screen, PNG or JPEG. */
|
|
94
|
+
export declare class Screenshot {
|
|
95
|
+
#private;
|
|
96
|
+
/** Use sandbox.computerUse.screenshot. */
|
|
97
|
+
constructor(live: Live);
|
|
98
|
+
takeFullScreen(showCursor?: boolean): Promise<ScreenshotResponse>;
|
|
99
|
+
takeRegion(_region: ScreenshotRegion, _showCursor?: boolean): Promise<ScreenshotResponse>;
|
|
100
|
+
takeCompressed(options?: ScreenshotOptions): Promise<ScreenshotResponse>;
|
|
101
|
+
takeCompressedRegion(_region: ScreenshotRegion, _options?: ScreenshotOptions): Promise<ScreenshotResponse>;
|
|
102
|
+
}
|
|
103
|
+
/** `sandbox.computerUse.display`. */
|
|
104
|
+
export declare class Display {
|
|
105
|
+
#private;
|
|
106
|
+
/** Use sandbox.computerUse.display. */
|
|
107
|
+
constructor(live: Live);
|
|
108
|
+
/** The one screen, its size read from a screenshot. */
|
|
109
|
+
getInfo(): Promise<{
|
|
110
|
+
displays?: DisplayInfo[];
|
|
111
|
+
}>;
|
|
112
|
+
getWindows(): Promise<{
|
|
113
|
+
windows?: WindowInfo[];
|
|
114
|
+
}>;
|
|
115
|
+
}
|
|
116
|
+
/** `sandbox.computerUse.recording`: screen recordings to MP4. */
|
|
117
|
+
export declare class RecordingService {
|
|
118
|
+
#private;
|
|
119
|
+
/** Use sandbox.computerUse.recording. */
|
|
120
|
+
constructor(live: Live);
|
|
121
|
+
/** Starts recording. Runtime names the file itself; `label` is not used. */
|
|
122
|
+
start(_label?: string): Promise<Recording>;
|
|
123
|
+
stop(id: string): Promise<Recording>;
|
|
124
|
+
list(): Promise<ListRecordingsResponse>;
|
|
125
|
+
get(id: string): Promise<Recording>;
|
|
126
|
+
delete(id: string): Promise<void>;
|
|
127
|
+
/** Saves the MP4 to `localPath`, making its directory. */
|
|
128
|
+
download(id: string, localPath: string): Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
/** `sandbox.computerUse.accessibility`: Runtime's desktop has no
|
|
131
|
+
* accessibility tree; every method throws NotSupportedError. */
|
|
132
|
+
export declare class Accessibility {
|
|
133
|
+
#private;
|
|
134
|
+
getTree(_options?: unknown): Promise<never>;
|
|
135
|
+
findNodes(_options?: unknown): Promise<never>;
|
|
136
|
+
focusNode(_id: string): Promise<never>;
|
|
137
|
+
invokeNode(_id: string, _action?: string): Promise<never>;
|
|
138
|
+
setNodeValue(_id: string, _value: string): Promise<never>;
|
|
139
|
+
}
|
|
140
|
+
/** `sandbox.computerUse`: a Linux desktop driven like a person would. */
|
|
141
|
+
export declare class ComputerUse {
|
|
142
|
+
#private;
|
|
143
|
+
readonly mouse: Mouse;
|
|
144
|
+
readonly keyboard: Keyboard;
|
|
145
|
+
readonly screenshot: Screenshot;
|
|
146
|
+
readonly display: Display;
|
|
147
|
+
readonly recording: RecordingService;
|
|
148
|
+
readonly accessibility: Accessibility;
|
|
149
|
+
/** Use sandbox.computerUse. */
|
|
150
|
+
constructor(live: Live);
|
|
151
|
+
/** Starts the desktop. A sandbox's first start installs it, which takes a
|
|
152
|
+
* minute or two; this waits for that. */
|
|
153
|
+
start(): Promise<ComputerUseStartResponse>;
|
|
154
|
+
stop(): Promise<ComputerUseStopResponse>;
|
|
155
|
+
/** "active" while the desktop runs, else "inactive". */
|
|
156
|
+
getStatus(): Promise<ComputerUseStatusResponse>;
|
|
157
|
+
getProcessStatus(_processName: string): Promise<never>;
|
|
158
|
+
restartProcess(_processName: string): Promise<never>;
|
|
159
|
+
getProcessLogs(_processName: string): Promise<never>;
|
|
160
|
+
getProcessErrors(_processName: string): Promise<never>;
|
|
161
|
+
}
|
|
162
|
+
export {};
|
|
@@ -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
|
+
}
|