withruntime 0.5.0 → 0.6.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 +25 -25
- package/E2B.md +1 -1
- package/LICENSE +55 -0
- package/README.md +3 -2
- package/dist/cli-extras.d.ts +24 -0
- package/dist/cli-extras.js +205 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +430 -37
- package/dist/daytona/context.d.ts +5 -0
- package/dist/daytona/context.js +3 -0
- package/dist/daytona/filesystem.js +2 -2
- package/dist/daytona/process.js +3 -1
- package/dist/e2b/commands.js +6 -2
- package/dist/e2b/filesystem.d.ts +26 -1
- package/dist/e2b/filesystem.js +30 -2
- package/dist/e2b/index.d.ts +1 -1
- package/dist/e2b/index.js +1 -1
- package/dist/errors.js +2 -0
- package/dist/identity.d.ts +27 -0
- package/dist/identity.js +56 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/mcp.js +16 -3
- package/dist/network-cli.d.ts +15 -0
- package/dist/network-cli.js +323 -0
- package/dist/products/desktop.d.ts +41 -4
- package/dist/products/desktop.js +73 -3
- package/dist/products/index.d.ts +12 -0
- package/dist/products/index.js +12 -0
- package/dist/products/interpreter.d.ts +5 -3
- package/dist/products/interpreter.js +2 -1
- package/dist/products/mcp.d.ts +79 -0
- package/dist/products/mcp.js +37 -0
- package/dist/products/mounts.d.ts +49 -0
- package/dist/products/mounts.js +21 -0
- package/dist/products/network-products.d.ts +167 -0
- package/dist/products/network-products.js +82 -0
- package/dist/products/network.d.ts +1 -1
- package/dist/products/sso.d.ts +44 -0
- package/dist/products/sso.js +8 -0
- package/dist/products/switching.d.ts +7 -0
- package/dist/products/volumes.d.ts +68 -5
- package/dist/products/volumes.js +52 -4
- package/dist/products/watch.d.ts +91 -0
- package/dist/products/watch.js +135 -0
- package/dist/sandbox.d.ts +32 -2
- package/dist/sandbox.js +93 -16
- package/dist/snapshots.d.ts +11 -1
- package/dist/ssh.js +13 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +4 -1
- package/dist/tunnel.d.ts +4 -1
- package/dist/tunnel.js +8 -3
- package/dist/wireguard.d.ts +7 -0
- package/dist/wireguard.js +13 -0
- package/package.json +7 -3
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { chmod, writeFile } from "node:fs/promises";
|
|
2
|
+
import { RuntimeError } from "./errors.js";
|
|
3
|
+
import { named } from "./login.js";
|
|
4
|
+
import { generateWireGuardKeyPair } from "./wireguard.js";
|
|
5
|
+
function fundingState(value) {
|
|
6
|
+
if (value.funded === false)
|
|
7
|
+
return "Traffic disabled. Check balance, spending limits and account permissions. Reservation kept until you release it.";
|
|
8
|
+
if (value.funded !== true)
|
|
9
|
+
return "Funding status unavailable.";
|
|
10
|
+
return value.fundedUntil
|
|
11
|
+
? `Funded until ${value.fundedUntil}.`
|
|
12
|
+
: "Funding active; no deadline reported.";
|
|
13
|
+
}
|
|
14
|
+
export const NETWORK_PRODUCTS = ["domain", "port", "address", "tunnel"];
|
|
15
|
+
export const NETWORK_HELP = {
|
|
16
|
+
domain: `runtime domain <command>
|
|
17
|
+
|
|
18
|
+
add <hostname> <sandbox> <port> Serve a sandbox's port at your own hostname, with HTTPS
|
|
19
|
+
Prints the DNS records to set: a TXT that proves the
|
|
20
|
+
name is yours, and a CNAME (an A for an apex name)
|
|
21
|
+
verify <hostname> Check the TXT record now; the name goes live when it matches
|
|
22
|
+
ls Your domains
|
|
23
|
+
get <hostname> One domain and its DNS records
|
|
24
|
+
rm <hostname> Stop serving it
|
|
25
|
+
Paid accounts only. The first visit gets the certificate, in a few seconds.
|
|
26
|
+
`,
|
|
27
|
+
port: `runtime port <command>
|
|
28
|
+
|
|
29
|
+
open <sandbox> <port> A public TCP port to a port in the sandbox: Postgres,
|
|
30
|
+
Redis, a game server, anything that is not HTTP
|
|
31
|
+
Prints address:port for your client
|
|
32
|
+
ls [--sandbox <id>] Your open ports
|
|
33
|
+
close <portId> Close one; open connections end within seconds
|
|
34
|
+
Paid accounts only. TCP, carried as it is: use your service's own password and TLS.
|
|
35
|
+
`,
|
|
36
|
+
address: `runtime address <command>
|
|
37
|
+
|
|
38
|
+
reserve [--ipv6] A dedicated outbound address: every sandbox of your
|
|
39
|
+
account sends from it, for allow-lists
|
|
40
|
+
ls Your addresses
|
|
41
|
+
release <addressId> Give it back; sandboxes use the shared addresses again
|
|
42
|
+
Paid accounts only.
|
|
43
|
+
`,
|
|
44
|
+
tunnel: `runtime tunnel <command>
|
|
45
|
+
|
|
46
|
+
create [--subnet 10.250.0.0/16] A WireGuard tunnel from your network into your sandboxes
|
|
47
|
+
get The tunnel, its peers, and each sandbox's address in it
|
|
48
|
+
rm Delete the tunnel and every peer
|
|
49
|
+
peer add <name> [--route 10.0.0.0/16]... [--out runtime.conf] [--public-key <key>]
|
|
50
|
+
Add a machine or router of yours; writes its wg-quick
|
|
51
|
+
file. The key pair is made here and the private key
|
|
52
|
+
never leaves this machine. Then: sudo wg-quick up ./runtime.conf
|
|
53
|
+
peer rotate <peerId> [--out runtime.conf]
|
|
54
|
+
A new key; the old one stops working within seconds
|
|
55
|
+
peer rm <peerId> Remove it; its tunnel ends within seconds
|
|
56
|
+
--route: your own ranges behind the peer that sandboxes may reach. Paid accounts only.
|
|
57
|
+
`,
|
|
58
|
+
};
|
|
59
|
+
function usage(product, message) {
|
|
60
|
+
return new RuntimeError({
|
|
61
|
+
message,
|
|
62
|
+
code: "usage",
|
|
63
|
+
status: 0,
|
|
64
|
+
hint: `Run \`${named("runtime")} ${product} help\`.`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function table(rows) {
|
|
68
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
|
|
69
|
+
return rows
|
|
70
|
+
.map((row) => row
|
|
71
|
+
.map((cell, column) => cell.padEnd(widths[column]))
|
|
72
|
+
.join(" ")
|
|
73
|
+
.trimEnd())
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
function portNumber(product, text) {
|
|
77
|
+
const value = Number(text);
|
|
78
|
+
if (!text || !/^\d+$/.test(text) || value < 1 || value > 65_535)
|
|
79
|
+
throw usage(product, "Give a port from 1 to 65535.");
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
/** Writes a wg-quick file readable by its owner alone, or prints it. */
|
|
83
|
+
async function deliver(out, config, path, hint, ready = true) {
|
|
84
|
+
if (!config) {
|
|
85
|
+
out.error(hint);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (path) {
|
|
89
|
+
await writeFile(path, config, { mode: 0o600 });
|
|
90
|
+
await chmod(path, 0o600);
|
|
91
|
+
out.error(ready
|
|
92
|
+
? `Wrote ${path}. Bring it up with: sudo wg-quick up ${path.startsWith("/") ? path : `./${path}`}`
|
|
93
|
+
: `Wrote ${path}. ${hint}`);
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
out.write(config);
|
|
97
|
+
}
|
|
98
|
+
export async function networkProductCommand(product, argv, args, out, client) {
|
|
99
|
+
const [verb] = argv;
|
|
100
|
+
const flag = (name) => args.flags.get(name)?.at(-1);
|
|
101
|
+
const has = (name) => flag(name) === "true";
|
|
102
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
103
|
+
const rest = args.positional.slice(1);
|
|
104
|
+
const need = (value, what) => {
|
|
105
|
+
if (!value)
|
|
106
|
+
throw usage(product, `Give ${what}.`);
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
if (!verb || verb === "help" || verb === "--help") {
|
|
110
|
+
print(named(NETWORK_HELP[product]), { usage: named(NETWORK_HELP[product]) });
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
const runtime = await client();
|
|
114
|
+
if (product === "domain") {
|
|
115
|
+
const records = (domain) => table([
|
|
116
|
+
["TYPE", "NAME", "VALUE"],
|
|
117
|
+
...domain.records.map((record) => [record.type, record.name, record.value]),
|
|
118
|
+
]);
|
|
119
|
+
switch (verb) {
|
|
120
|
+
case "add": {
|
|
121
|
+
const found = await runtime.domains.add({
|
|
122
|
+
hostname: need(rest[0], "a hostname, such as app.example.com"),
|
|
123
|
+
sandboxId: need(rest[1], "a sandbox id"),
|
|
124
|
+
port: portNumber(product, rest[2]),
|
|
125
|
+
});
|
|
126
|
+
print(`${found.hostname}: ${found.state}\n\n${records(found)}\n\n${found.hint}`, found);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
case "verify": {
|
|
130
|
+
const found = await runtime.domains.verify(need(rest[0], "a hostname"));
|
|
131
|
+
print(`${found.hostname}: ${found.state}. ${found.hint}${found.checkError ? `\n${found.checkError}` : ""}`, found);
|
|
132
|
+
return found.state === "active" ? 0 : 1;
|
|
133
|
+
}
|
|
134
|
+
case "ls":
|
|
135
|
+
case "list": {
|
|
136
|
+
const all = await runtime.domains.list();
|
|
137
|
+
print(all.length
|
|
138
|
+
? table([
|
|
139
|
+
["HOSTNAME", "STATE", "SANDBOX", "PORT"],
|
|
140
|
+
...all.map((d) => [
|
|
141
|
+
d.hostname,
|
|
142
|
+
d.disabled ? "taken down" : d.state,
|
|
143
|
+
d.sandboxId,
|
|
144
|
+
String(d.port),
|
|
145
|
+
]),
|
|
146
|
+
])
|
|
147
|
+
: named("No domains. Add one: runtime domain add app.example.com <sandbox> 3000"), all);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
case "get": {
|
|
151
|
+
const found = await runtime.domains.get(need(rest[0], "a hostname"));
|
|
152
|
+
print(`${found.hostname}: ${found.state}\n\n${records(found)}`, found);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
case "rm":
|
|
156
|
+
case "remove": {
|
|
157
|
+
const gone = await runtime.domains.remove(need(rest[0], "a hostname"));
|
|
158
|
+
print(`${gone.hostname} is no longer served. Remove its DNS records too.`, gone);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (product === "port") {
|
|
164
|
+
switch (verb) {
|
|
165
|
+
case "open": {
|
|
166
|
+
const opened = await runtime.ports.open({
|
|
167
|
+
sandboxId: need(rest[0], "a sandbox id"),
|
|
168
|
+
port: portNumber(product, rest[1]),
|
|
169
|
+
});
|
|
170
|
+
if (!out.json)
|
|
171
|
+
out.error(opened.hint);
|
|
172
|
+
print(opened.connect, opened);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
case "ls":
|
|
176
|
+
case "list": {
|
|
177
|
+
const sandboxId = flag("sandbox");
|
|
178
|
+
const all = await runtime.ports.list(sandboxId ? { sandboxId } : {});
|
|
179
|
+
print(all.length
|
|
180
|
+
? table([
|
|
181
|
+
["ID", "CONNECT", "SANDBOX", "PORT"],
|
|
182
|
+
...all.map((p) => [p.id, p.connect, p.sandboxId, String(p.port)]),
|
|
183
|
+
])
|
|
184
|
+
: named("No open ports. Open one: runtime port open <sandbox> 5432"), all);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
case "close":
|
|
188
|
+
case "rm": {
|
|
189
|
+
const gone = await runtime.ports.close(need(rest[0], "a port id"));
|
|
190
|
+
print(`Closed ${gone.id}.`, gone);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (product === "address") {
|
|
196
|
+
switch (verb) {
|
|
197
|
+
case "reserve": {
|
|
198
|
+
const reserved = await runtime.addresses.reserve({ family: has("ipv6") ? 6 : 4 });
|
|
199
|
+
if (!out.json)
|
|
200
|
+
out.error(reserved.funded === true
|
|
201
|
+
? `Every sandbox of this account sends from it. ${fundingState(reserved)}`
|
|
202
|
+
: fundingState(reserved));
|
|
203
|
+
print(reserved.address, reserved);
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
case "ls":
|
|
207
|
+
case "list": {
|
|
208
|
+
const all = await runtime.addresses.list();
|
|
209
|
+
print(all.length
|
|
210
|
+
? table([
|
|
211
|
+
["ID", "ADDRESS", "FAMILY", "FUNDING"],
|
|
212
|
+
...all.map((a) => [a.id, a.address, `IPv${a.family}`, fundingState(a)]),
|
|
213
|
+
])
|
|
214
|
+
: named("No dedicated address. Reserve one: runtime address reserve"), all);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
case "release":
|
|
218
|
+
case "rm": {
|
|
219
|
+
const gone = await runtime.addresses.release(need(rest[0], "an address id"));
|
|
220
|
+
print(`Released ${gone.address}.`, gone);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (product === "tunnel") {
|
|
226
|
+
const view = (tunnel) => [
|
|
227
|
+
fundingState(tunnel),
|
|
228
|
+
`Endpoint ${tunnel.endpoint}, subnet ${tunnel.subnet}, gateway key ${tunnel.gatewayPublicKey ?? "(starting)"}`,
|
|
229
|
+
"",
|
|
230
|
+
tunnel.peers.length
|
|
231
|
+
? table([
|
|
232
|
+
["PEER", "NAME", "ADDRESS", "ROUTES"],
|
|
233
|
+
...tunnel.peers.map((p) => [p.id, p.name, p.address, p.routes.join(",") || "-"]),
|
|
234
|
+
])
|
|
235
|
+
: named("No peers. Add one: runtime tunnel peer add office --route 10.0.0.0/16"),
|
|
236
|
+
"",
|
|
237
|
+
tunnel.sandboxes.length
|
|
238
|
+
? table([
|
|
239
|
+
["SANDBOX", "NAME", "ADDRESS", "STATE"],
|
|
240
|
+
...tunnel.sandboxes.map((s) => [s.sandboxId, s.name ?? "-", s.address, s.state]),
|
|
241
|
+
])
|
|
242
|
+
: "No sandboxes yet.",
|
|
243
|
+
].join("\n");
|
|
244
|
+
switch (verb) {
|
|
245
|
+
case "create": {
|
|
246
|
+
const made = await runtime.tunnel.create(flag("subnet") ? { subnet: flag("subnet") } : {});
|
|
247
|
+
print(view(made), made);
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
case "get":
|
|
251
|
+
case "ls": {
|
|
252
|
+
const found = await runtime.tunnel.get();
|
|
253
|
+
print(view(found), found);
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
case "rm":
|
|
257
|
+
case "delete": {
|
|
258
|
+
const gone = await runtime.tunnel.delete();
|
|
259
|
+
print(`Deleted the tunnel ${gone.id}.`, gone);
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
case "peer":
|
|
263
|
+
case "peers": {
|
|
264
|
+
const action = rest[0];
|
|
265
|
+
if (action === "add" || action === "rotate") {
|
|
266
|
+
// The pair is made here unless a key is given: the private key never
|
|
267
|
+
// leaves this machine, and goes only into the file written below.
|
|
268
|
+
const given = flag("public-key");
|
|
269
|
+
const pair = given ? undefined : generateWireGuardKeyPair();
|
|
270
|
+
const publicKey = given ?? pair.publicKey;
|
|
271
|
+
const answer = action === "add"
|
|
272
|
+
? await runtime.tunnel.addPeer({
|
|
273
|
+
name: need(rest[1], "the peer's name, such as office"),
|
|
274
|
+
publicKey,
|
|
275
|
+
routes: args.flags.get("route") ?? [],
|
|
276
|
+
})
|
|
277
|
+
: await runtime.tunnel.rotatePeer(need(rest[1], "a peer id"), { publicKey });
|
|
278
|
+
const configReady = answer.configReady ?? !!answer.config;
|
|
279
|
+
const draftHint = "Keep this file: it preserves your private key. Read runtime tunnel get when the gateway is ready, replace PublicKey in [Peer] with gatewayPublicKey, then run wg-quick up.";
|
|
280
|
+
// Older servers returned no config while the gateway was starting.
|
|
281
|
+
// Preserve the locally generated key in a draft rather than discard it.
|
|
282
|
+
const supplied = answer.config ??
|
|
283
|
+
[
|
|
284
|
+
"# DRAFT: gateway key missing. Keep this file; it preserves your private key.",
|
|
285
|
+
"[Interface]",
|
|
286
|
+
`PrivateKey = ${pair?.privateKey ?? "<your private key>"}`,
|
|
287
|
+
`Address = ${answer.peer.address}/32`,
|
|
288
|
+
"",
|
|
289
|
+
"[Peer]",
|
|
290
|
+
`PublicKey = ${answer.tunnel.gatewayPublicKey ?? "<gatewayPublicKey from runtime tunnel get>"}`,
|
|
291
|
+
`Endpoint = ${answer.tunnel.endpoint}`,
|
|
292
|
+
`AllowedIPs = ${answer.tunnel.subnet}`,
|
|
293
|
+
"PersistentKeepalive = 25",
|
|
294
|
+
"",
|
|
295
|
+
].join("\n");
|
|
296
|
+
const config = pair
|
|
297
|
+
? supplied.replace(/^PrivateKey = .*$/m, `PrivateKey = ${pair.privateKey}`)
|
|
298
|
+
: supplied;
|
|
299
|
+
if (out.json) {
|
|
300
|
+
print("", {
|
|
301
|
+
...answer,
|
|
302
|
+
config,
|
|
303
|
+
configReady,
|
|
304
|
+
...(!configReady ? { hint: draftHint } : {}),
|
|
305
|
+
});
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
if (answer.tunnel.funded === false)
|
|
309
|
+
out.error(fundingState(answer.tunnel));
|
|
310
|
+
await deliver(out, config, flag("out") ?? (pair ? "runtime.conf" : undefined), configReady ? answer.hint : draftHint, configReady);
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
if (action === "rm" || action === "remove") {
|
|
314
|
+
const after = await runtime.tunnel.removePeer(need(rest[1], "a peer id"));
|
|
315
|
+
print(`Removed. ${after.peers.length} peer(s) left.`, after);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
throw usage(product, "Use peer add, peer rotate or peer rm.");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
throw usage(product, `Unknown ${product} command ${verb}.`);
|
|
323
|
+
}
|
|
@@ -21,6 +21,27 @@ export type DesktopStart = {
|
|
|
21
21
|
streamUrl: string;
|
|
22
22
|
preview: Preview;
|
|
23
23
|
};
|
|
24
|
+
export type DesktopRecording = {
|
|
25
|
+
id: string;
|
|
26
|
+
state: "starting" | "installing" | "recording" | "finished" | "failed";
|
|
27
|
+
/** The MP4 file in the sandbox. */
|
|
28
|
+
path: string;
|
|
29
|
+
bytes: number;
|
|
30
|
+
seconds?: number;
|
|
31
|
+
/** Why it ended: stopped, max-seconds, max-bytes, disk-low, desktop-gone, error. */
|
|
32
|
+
reason: string | null;
|
|
33
|
+
startedAt?: number | null;
|
|
34
|
+
};
|
|
35
|
+
export type RecordOptions = {
|
|
36
|
+
/** Frames a second, 1 to 30. Default 10. */
|
|
37
|
+
fps?: number;
|
|
38
|
+
/** Stop after this much video, up to 7200. Default 1800. */
|
|
39
|
+
maxSeconds?: number;
|
|
40
|
+
/** The file never grows past this, 16 to 4096. Default 512. */
|
|
41
|
+
maxMiB?: number;
|
|
42
|
+
/** Quality, 18 (sharp, large) to 45 (small). Default 30. */
|
|
43
|
+
crf?: number;
|
|
44
|
+
};
|
|
24
45
|
/** `sbx.desktop`: a Linux desktop in the sandbox, driven like a person would.
|
|
25
46
|
*
|
|
26
47
|
* const { streamUrl } = await sbx.desktop.start();
|
|
@@ -31,10 +52,12 @@ export type DesktopStart = {
|
|
|
31
52
|
*
|
|
32
53
|
* Coordinates are pixels from the top left of the screen. */
|
|
33
54
|
export declare function sandboxDesktop(t: Transport, sandbox: Sandbox): {
|
|
34
|
-
start
|
|
55
|
+
/** Starts the desktop. The first start in a sandbox installs it, which
|
|
56
|
+
* takes a minute or two; this waits for that (up to 10 minutes). */
|
|
57
|
+
start(size?: {
|
|
35
58
|
width?: number;
|
|
36
59
|
height?: number;
|
|
37
|
-
}, options?: RequestOptions)
|
|
60
|
+
}, options?: RequestOptions): Promise<DesktopStart>;
|
|
38
61
|
stop: (options?: RequestOptions) => Promise<{
|
|
39
62
|
ok: boolean;
|
|
40
63
|
}>;
|
|
@@ -92,12 +115,26 @@ export declare function sandboxDesktop(t: Transport, sandbox: Sandbox): {
|
|
|
92
115
|
focus: (windowId: string, options?: RequestOptions) => Promise<{
|
|
93
116
|
ok: true;
|
|
94
117
|
}>;
|
|
95
|
-
/** Opens `url` in Firefox on the desktop.
|
|
96
|
-
|
|
118
|
+
/** Opens `url` in Firefox on the desktop. Right after a sandbox's first
|
|
119
|
+
* start, Firefox may still be installing; this waits for it. */
|
|
120
|
+
open(url: string, options?: RequestOptions): Promise<{
|
|
97
121
|
ok: true;
|
|
98
122
|
}>;
|
|
99
123
|
/** Starts a program on the desktop, detached. */
|
|
100
124
|
launch: (argv: string[], options?: RequestOptions) => Promise<{
|
|
101
125
|
ok: true;
|
|
102
126
|
}>;
|
|
127
|
+
/** Screen recordings to MP4: `const rec = await sbx.desktop.recordings.start();`
|
|
128
|
+
* ... `await sbx.desktop.recordings.stop(rec.id)`, then `download(rec.id)`. */
|
|
129
|
+
recordings: {
|
|
130
|
+
start: (input?: RecordOptions, options?: RequestOptions) => Promise<DesktopRecording>;
|
|
131
|
+
stop: (id: string, options?: RequestOptions) => Promise<DesktopRecording>;
|
|
132
|
+
get: (id: string, options?: RequestOptions) => Promise<DesktopRecording>;
|
|
133
|
+
list: (options?: RequestOptions) => Promise<DesktopRecording[]>;
|
|
134
|
+
/** The MP4 bytes. */
|
|
135
|
+
download: (id: string, options?: RequestOptions) => Promise<Uint8Array<ArrayBufferLike>>;
|
|
136
|
+
delete: (id: string, options?: RequestOptions) => Promise<{
|
|
137
|
+
deleted: boolean;
|
|
138
|
+
}>;
|
|
139
|
+
};
|
|
103
140
|
};
|
package/dist/products/desktop.js
CHANGED
|
@@ -11,7 +11,27 @@ export function sandboxDesktop(t, sandbox) {
|
|
|
11
11
|
const base = () => `/v1/sandboxes/${encodeURIComponent(sandbox.id)}/desktop`;
|
|
12
12
|
const act = (body, options) => t.json({ method: "POST", path: `${base()}:act`, body, ...options });
|
|
13
13
|
return {
|
|
14
|
-
|
|
14
|
+
/** Starts the desktop. The first start in a sandbox installs it, which
|
|
15
|
+
* takes a minute or two; this waits for that (up to 10 minutes). */
|
|
16
|
+
async start(size = {}, options) {
|
|
17
|
+
const deadline = Date.now() + 600_000;
|
|
18
|
+
for (;;) {
|
|
19
|
+
try {
|
|
20
|
+
return await t.json({
|
|
21
|
+
method: "POST",
|
|
22
|
+
path: `${base()}:start`,
|
|
23
|
+
body: size,
|
|
24
|
+
...options,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = error.code;
|
|
29
|
+
if (code !== "desktop_installing" || Date.now() > deadline)
|
|
30
|
+
throw error;
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, error.retryAfterMs ?? 10_000));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
15
35
|
stop: (options) => t.json({ method: "POST", path: `${base()}:stop`, ...options }),
|
|
16
36
|
/** PNG bytes, or JPEG with `format: "jpeg"`. */
|
|
17
37
|
screenshot: (input = {}, options) => t.bytes({ method: "GET", path: `${base()}/screenshot`, query: input, ...options }),
|
|
@@ -30,9 +50,59 @@ export function sandboxDesktop(t, sandbox) {
|
|
|
30
50
|
cursor: (options) => act({ action: "cursor" }, options),
|
|
31
51
|
windows: async (options) => (await act({ action: "windows" }, options)).windows,
|
|
32
52
|
focus: (windowId, options) => act({ action: "focus", windowId }, options),
|
|
33
|
-
/** Opens `url` in Firefox on the desktop.
|
|
34
|
-
|
|
53
|
+
/** Opens `url` in Firefox on the desktop. Right after a sandbox's first
|
|
54
|
+
* start, Firefox may still be installing; this waits for it. */
|
|
55
|
+
async open(url, options) {
|
|
56
|
+
const deadline = Date.now() + 600_000;
|
|
57
|
+
for (;;) {
|
|
58
|
+
try {
|
|
59
|
+
return await act({ action: "open", url }, options);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const code = error.code;
|
|
63
|
+
if (code !== "desktop_installing" || Date.now() > deadline)
|
|
64
|
+
throw error;
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, error.retryAfterMs ?? 10_000));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
35
69
|
/** Starts a program on the desktop, detached. */
|
|
36
70
|
launch: (argv, options) => act({ action: "launch", argv }, options),
|
|
71
|
+
/** Screen recordings to MP4: `const rec = await sbx.desktop.recordings.start();`
|
|
72
|
+
* ... `await sbx.desktop.recordings.stop(rec.id)`, then `download(rec.id)`. */
|
|
73
|
+
recordings: {
|
|
74
|
+
start: (input = {}, options) => t.json({
|
|
75
|
+
method: "POST",
|
|
76
|
+
path: `${base()}/recordings`,
|
|
77
|
+
body: input,
|
|
78
|
+
...options,
|
|
79
|
+
}),
|
|
80
|
+
stop: (id, options) => t.json({
|
|
81
|
+
method: "POST",
|
|
82
|
+
path: `${base()}/recordings/${encodeURIComponent(id)}:stop`,
|
|
83
|
+
...options,
|
|
84
|
+
}),
|
|
85
|
+
get: (id, options) => t.json({
|
|
86
|
+
method: "GET",
|
|
87
|
+
path: `${base()}/recordings/${encodeURIComponent(id)}`,
|
|
88
|
+
...options,
|
|
89
|
+
}),
|
|
90
|
+
list: async (options) => (await t.json({
|
|
91
|
+
method: "GET",
|
|
92
|
+
path: `${base()}/recordings`,
|
|
93
|
+
...options,
|
|
94
|
+
})).data,
|
|
95
|
+
/** The MP4 bytes. */
|
|
96
|
+
download: (id, options) => t.bytes({
|
|
97
|
+
method: "GET",
|
|
98
|
+
path: `${base()}/recordings/${encodeURIComponent(id)}/video`,
|
|
99
|
+
...options,
|
|
100
|
+
}),
|
|
101
|
+
delete: (id, options) => t.json({
|
|
102
|
+
method: "DELETE",
|
|
103
|
+
path: `${base()}/recordings/${encodeURIComponent(id)}`,
|
|
104
|
+
...options,
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
37
107
|
};
|
|
38
108
|
}
|
package/dist/products/index.d.ts
CHANGED
|
@@ -4,13 +4,17 @@ import { images } from "./images.js";
|
|
|
4
4
|
import { sandboxDesktop } from "./desktop.js";
|
|
5
5
|
import { sandboxInterpreter } from "./interpreter.js";
|
|
6
6
|
import { sandboxNetwork } from "./network.js";
|
|
7
|
+
import { addresses, domains, ports, tunnel } from "./network-products.js";
|
|
7
8
|
import { sandboxPreviews } from "./previews.js";
|
|
8
9
|
import { referrals } from "./referrals.js";
|
|
9
10
|
import { audit } from "./audit.js";
|
|
11
|
+
import { sso } from "./sso.js";
|
|
10
12
|
import { switching } from "./switching.js";
|
|
11
13
|
import { limits } from "./limits.js";
|
|
12
14
|
import { volumes } from "./volumes.js";
|
|
15
|
+
import { sandboxMounts } from "./mounts.js";
|
|
13
16
|
import { secrets } from "./secrets.js";
|
|
17
|
+
import { mcp, sandboxMcp } from "./mcp.js";
|
|
14
18
|
import { events, otel, sandboxMetrics, webhooks } from "./observability.js";
|
|
15
19
|
export declare const clientExtensions: {
|
|
16
20
|
images: typeof images;
|
|
@@ -23,13 +27,21 @@ export declare const clientExtensions: {
|
|
|
23
27
|
otel: typeof otel;
|
|
24
28
|
events: typeof events;
|
|
25
29
|
audit: typeof audit;
|
|
30
|
+
mcp: typeof mcp;
|
|
31
|
+
sso: typeof sso;
|
|
32
|
+
domains: typeof domains;
|
|
33
|
+
ports: typeof ports;
|
|
34
|
+
addresses: typeof addresses;
|
|
35
|
+
tunnel: typeof tunnel;
|
|
26
36
|
};
|
|
27
37
|
export declare const sandboxExtensions: {
|
|
28
38
|
interpreter: typeof sandboxInterpreter;
|
|
29
39
|
previews: typeof sandboxPreviews;
|
|
30
40
|
network: typeof sandboxNetwork;
|
|
31
41
|
desktop: typeof sandboxDesktop;
|
|
42
|
+
mounts: typeof sandboxMounts;
|
|
32
43
|
metrics: typeof sandboxMetrics;
|
|
44
|
+
mcp: typeof sandboxMcp;
|
|
33
45
|
};
|
|
34
46
|
export type ClientExtensions = {
|
|
35
47
|
readonly [K in keyof typeof clientExtensions]: ReturnType<(typeof clientExtensions)[K]>;
|
package/dist/products/index.js
CHANGED
|
@@ -2,13 +2,17 @@ import { images } from "./images.js";
|
|
|
2
2
|
import { sandboxDesktop } from "./desktop.js";
|
|
3
3
|
import { sandboxInterpreter } from "./interpreter.js";
|
|
4
4
|
import { sandboxNetwork } from "./network.js";
|
|
5
|
+
import { addresses, domains, ports, tunnel } from "./network-products.js";
|
|
5
6
|
import { sandboxPreviews } from "./previews.js";
|
|
6
7
|
import { referrals } from "./referrals.js";
|
|
7
8
|
import { audit } from "./audit.js";
|
|
9
|
+
import { sso } from "./sso.js";
|
|
8
10
|
import { switching } from "./switching.js";
|
|
9
11
|
import { limits } from "./limits.js";
|
|
10
12
|
import { volumes } from "./volumes.js";
|
|
13
|
+
import { sandboxMounts } from "./mounts.js";
|
|
11
14
|
import { secrets } from "./secrets.js";
|
|
15
|
+
import { mcp, sandboxMcp } from "./mcp.js";
|
|
12
16
|
import { events, otel, sandboxMetrics, webhooks } from "./observability.js";
|
|
13
17
|
/* HOW A LANE ADDS A PRODUCT TO THE SDK
|
|
14
18
|
*
|
|
@@ -36,13 +40,21 @@ export const clientExtensions = {
|
|
|
36
40
|
otel,
|
|
37
41
|
events,
|
|
38
42
|
audit,
|
|
43
|
+
mcp,
|
|
44
|
+
sso,
|
|
45
|
+
domains,
|
|
46
|
+
ports,
|
|
47
|
+
addresses,
|
|
48
|
+
tunnel,
|
|
39
49
|
};
|
|
40
50
|
export const sandboxExtensions = {
|
|
41
51
|
interpreter: sandboxInterpreter,
|
|
42
52
|
previews: sandboxPreviews,
|
|
43
53
|
network: sandboxNetwork,
|
|
44
54
|
desktop: sandboxDesktop,
|
|
55
|
+
mounts: sandboxMounts,
|
|
45
56
|
metrics: sandboxMetrics,
|
|
57
|
+
mcp: sandboxMcp,
|
|
46
58
|
};
|
|
47
59
|
/** The factories as plain pairs, for the constructors that install them. */
|
|
48
60
|
export function clientFactories() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Sandbox } from "../sandbox.js";
|
|
2
2
|
import type { RequestOptions, Transport } from "../transport.js";
|
|
3
|
-
export type InterpreterLanguage = "python" | "javascript";
|
|
3
|
+
export type InterpreterLanguage = "python" | "javascript" | "typescript" | "r" | "java" | "bash" | "go";
|
|
4
4
|
export type ResultRef = {
|
|
5
5
|
path: string;
|
|
6
6
|
bytes: number;
|
|
@@ -47,7 +47,8 @@ export type InterpreterContext = {
|
|
|
47
47
|
};
|
|
48
48
|
export type RunOptions = RequestOptions & {
|
|
49
49
|
language?: InterpreterLanguage;
|
|
50
|
-
/** A context id; "python"
|
|
50
|
+
/** A context id; each language's own name ("python", "typescript", "r"...) is
|
|
51
|
+
* its default context, started on first use. */
|
|
51
52
|
context?: string;
|
|
52
53
|
timeoutMs?: number;
|
|
53
54
|
onStdout?: (text: string) => void;
|
|
@@ -59,7 +60,8 @@ export type RunOptions = RequestOptions & {
|
|
|
59
60
|
traceback: string;
|
|
60
61
|
}) => void;
|
|
61
62
|
};
|
|
62
|
-
/** A stateful
|
|
63
|
+
/** A stateful interpreter in the sandbox, in Python, JavaScript, TypeScript,
|
|
64
|
+
* R, Java, Bash or Go:
|
|
63
65
|
* `await sbx.interpreter.run("import pandas as pd; pd.DataFrame({'a': [1]})")`.
|
|
64
66
|
* Variables persist between runs of the same context, like a notebook. */
|
|
65
67
|
export declare function sandboxInterpreter(t: Transport, sandbox: Sandbox): {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** A stateful
|
|
1
|
+
/** A stateful interpreter in the sandbox, in Python, JavaScript, TypeScript,
|
|
2
|
+
* R, Java, Bash or Go:
|
|
2
3
|
* `await sbx.interpreter.run("import pandas as pd; pd.DataFrame({'a': [1]})")`.
|
|
3
4
|
* Variables persist between runs of the same context, like a notebook. */
|
|
4
5
|
export function sandboxInterpreter(t, sandbox) {
|