withruntime 0.14.0 → 0.16.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 +97 -32
- package/COMPATIBILITY.md +120 -5
- package/DAYTONA.md +34 -3
- package/E2B.md +34 -5
- package/README.md +33 -0
- package/README.src.md +33 -0
- package/VERCEL.md +52 -5
- package/compatibility-lock.json +71 -111
- package/dist/blaxel/client.d.ts +11 -0
- package/dist/blaxel/client.js +28 -0
- package/dist/blaxel/errors.d.ts +65 -0
- package/dist/blaxel/errors.js +114 -0
- package/dist/blaxel/index.d.ts +10 -3
- package/dist/blaxel/index.js +23 -4
- package/dist/blaxel/preview.d.ts +29 -4
- package/dist/blaxel/preview.js +79 -5
- package/dist/blaxel/sandbox.js +1 -0
- package/dist/blaxel/session.js +4 -2
- package/dist/cli-extras.js +2 -1
- package/dist/cli-words.js +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +54 -21
- package/dist/client.d.ts +1 -1
- package/dist/daytona/client.d.ts +4 -0
- package/dist/daytona/client.js +8 -3
- package/dist/daytona/daytona.d.ts +3 -0
- package/dist/daytona/daytona.js +2 -1
- package/dist/e2b/sandbox.d.ts +6 -5
- package/dist/e2b/sandbox.js +9 -12
- package/dist/freestyle/index.d.ts +77 -5
- package/dist/freestyle/index.js +380 -48
- package/dist/identity.js +36 -18
- package/dist/index.d.ts +2 -2
- package/dist/machine.d.ts +24 -12
- package/dist/machine.js +46 -14
- package/dist/mcp.js +16 -3
- package/dist/modal/index.d.ts +15 -0
- package/dist/modal/index.js +35 -7
- package/dist/morph/index.d.ts +255 -0
- package/dist/morph/index.js +641 -0
- package/dist/network-cli.js +2 -2
- package/dist/product-names.d.ts +2 -2
- package/dist/product-names.js +2 -2
- package/dist/products/limits.d.ts +16 -2
- package/dist/products/limits.js +2 -2
- package/dist/products/mcp.d.ts +5 -0
- package/dist/products/network-products.d.ts +21 -3
- package/dist/products/network-products.js +3 -1
- package/dist/products/previews.d.ts +6 -3
- package/dist/products/previews.js +1 -1
- package/dist/snapshots.d.ts +4 -0
- package/dist/ssh.d.ts +35 -0
- package/dist/ssh.js +93 -1
- package/dist/transport.d.ts +11 -8
- package/dist/transport.js +46 -12
- package/dist/types.d.ts +24 -0
- package/dist/vercel/command.d.ts +48 -1
- package/dist/vercel/command.js +237 -6
- package/dist/vercel/sandbox-user.js +2 -1
- package/dist/vercel/sandbox.d.ts +16 -2
- package/dist/vercel/sandbox.js +45 -2
- package/package.json +6 -1
|
@@ -18,9 +18,23 @@ export type KeyLimits = {
|
|
|
18
18
|
/** Machine time the account can still run without credit, the same
|
|
19
19
|
* figures as `usage.get()`'s `trial`, or null when it has none. */
|
|
20
20
|
trial: Usage["trial"];
|
|
21
|
+
/** Every limit the account is held to, with what it uses of each now: the
|
|
22
|
+
* numbers a create past one is refused `quota_exceeded` by (its
|
|
23
|
+
* `details.limit` names the row). Null when they can't be read. */
|
|
24
|
+
account: AccountLimit[] | null;
|
|
21
25
|
};
|
|
22
|
-
/**
|
|
23
|
-
|
|
26
|
+
/** One of the account's limits, as https://withruntime.com/account/limits shows it. */
|
|
27
|
+
export type AccountLimit = {
|
|
28
|
+
/** `<kind>.<dimension>`: sandbox.count, sandbox.vcpu, sandbox.memory_mib, volume.count… */
|
|
29
|
+
limit: string;
|
|
30
|
+
label: string;
|
|
31
|
+
/** What `max` and `used` count: things, or MiB. */
|
|
32
|
+
unit: "count" | "mib";
|
|
33
|
+
max: number;
|
|
34
|
+
used: number;
|
|
35
|
+
};
|
|
36
|
+
/** `runtime.limits.get()`: whether this key is read-only, what its agent
|
|
37
|
+
* may still spend today, and the account's limits with what it uses of each. An owner sets the limit at
|
|
24
38
|
* https://withruntime.com/account/keys; a key can read it, never change it.
|
|
25
39
|
* Past it, a create, wake, extension or renewal fails with
|
|
26
40
|
* `spending_limit_reached` (HTTP 402). */
|
package/dist/products/limits.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** `runtime.limits.get()`: whether this key is read-only,
|
|
2
|
-
* may still spend today. An owner sets the limit at
|
|
1
|
+
/** `runtime.limits.get()`: whether this key is read-only, what its agent
|
|
2
|
+
* may still spend today, and the account's limits with what it uses of each. An owner sets the limit at
|
|
3
3
|
* https://withruntime.com/account/keys; a key can read it, never change it.
|
|
4
4
|
* Past it, a create, wake, extension or renewal fails with
|
|
5
5
|
* `spending_limit_reached` (HTTP 402). */
|
package/dist/products/mcp.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export type McpServerRequest = {
|
|
|
38
38
|
export type McpGateway = {
|
|
39
39
|
running: boolean;
|
|
40
40
|
port: number | null;
|
|
41
|
+
/** The URLs are a public preview: no link token, no expiry. */
|
|
42
|
+
public?: boolean;
|
|
41
43
|
/** Send it as `Authorization: Bearer <token>`. */
|
|
42
44
|
token?: string | null;
|
|
43
45
|
headers?: Record<string, string>;
|
|
@@ -65,6 +67,9 @@ export declare function sandboxMcp(t: Transport, sandbox: Sandbox): {
|
|
|
65
67
|
start: (servers: McpServerRequest[], input?: {
|
|
66
68
|
port?: number;
|
|
67
69
|
replace?: boolean;
|
|
70
|
+
/** URLs that never expire, guarded by the header alone, which is kept
|
|
71
|
+
* across starts until `stop()`. Paid accounts only. */
|
|
72
|
+
public?: boolean;
|
|
68
73
|
}, options?: RequestOptions) => Promise<McpGateway>;
|
|
69
74
|
/** Their state, and fresh URLs. */
|
|
70
75
|
get: (options?: RequestOptions) => Promise<McpGateway>;
|
|
@@ -8,7 +8,10 @@ export type DnsRecord = {
|
|
|
8
8
|
export type Domain = {
|
|
9
9
|
id: string;
|
|
10
10
|
hostname: string;
|
|
11
|
-
|
|
11
|
+
/** The sandbox it serves; null for an App's. */
|
|
12
|
+
sandboxId: string | null;
|
|
13
|
+
/** The App it serves, through every deploy and rollback; null for a sandbox's. */
|
|
14
|
+
appId?: string | null;
|
|
12
15
|
port: number;
|
|
13
16
|
/** pending until the TXT record proves the name is yours, then active. */
|
|
14
17
|
state: "pending" | "active" | "released";
|
|
@@ -20,6 +23,13 @@ export type Domain = {
|
|
|
20
23
|
verifiedAt: string | null;
|
|
21
24
|
checkedAt: string | null;
|
|
22
25
|
checkError: string | null;
|
|
26
|
+
/** An App's domain's certificate, issued before visitors arrive: point the
|
|
27
|
+
* name at the App once `ready` is true. Null for a sandbox's. */
|
|
28
|
+
certificate?: {
|
|
29
|
+
ready: boolean;
|
|
30
|
+
expiresAt: string | null;
|
|
31
|
+
issuers: string[];
|
|
32
|
+
} | null;
|
|
23
33
|
createdAt: string;
|
|
24
34
|
};
|
|
25
35
|
export type CheckedDomain = Domain & {
|
|
@@ -97,12 +107,20 @@ export type TunnelPeerCreated = {
|
|
|
97
107
|
/** `runtime.domains`: serve a sandbox's port at your own hostname, with HTTPS. */
|
|
98
108
|
export declare function domains(t: Transport): {
|
|
99
109
|
/** Claims the hostname and answers the DNS records to set. Calling it again
|
|
100
|
-
* with another sandbox or port points the name there.
|
|
110
|
+
* with another sandbox or port points the name there. With `appId`, an App
|
|
111
|
+
* serves it: add the `_acme-challenge` CNAME too, and point the name at
|
|
112
|
+
* the App once `certificate.ready` is true. */
|
|
101
113
|
add: (input: {
|
|
102
114
|
hostname: string;
|
|
115
|
+
} & ({
|
|
103
116
|
sandboxId: string;
|
|
104
117
|
port: number;
|
|
105
|
-
|
|
118
|
+
appId?: never;
|
|
119
|
+
} | {
|
|
120
|
+
appId: string;
|
|
121
|
+
sandboxId?: never;
|
|
122
|
+
port?: never;
|
|
123
|
+
}), options?: RequestOptions) => Promise<CheckedDomain>;
|
|
106
124
|
/** Checks the TXT record now; the name goes live when it matches. */
|
|
107
125
|
verify: (hostname: string, options?: RequestOptions) => Promise<CheckedDomain>;
|
|
108
126
|
get: (hostname: string, options?: RequestOptions) => Promise<Domain>;
|
|
@@ -5,7 +5,9 @@ const enc = encodeURIComponent;
|
|
|
5
5
|
export function domains(t) {
|
|
6
6
|
return {
|
|
7
7
|
/** Claims the hostname and answers the DNS records to set. Calling it again
|
|
8
|
-
* with another sandbox or port points the name there.
|
|
8
|
+
* with another sandbox or port points the name there. With `appId`, an App
|
|
9
|
+
* serves it: add the `_acme-challenge` CNAME too, and point the name at
|
|
10
|
+
* the App once `certificate.ready` is true. */
|
|
9
11
|
add: (input, options) => t.json({ method: "POST", path: "/v1/domains", body: input, ...options }),
|
|
10
12
|
/** Checks the TXT record now; the name goes live when it matches. */
|
|
11
13
|
verify: (hostname, options) => t.json({
|
|
@@ -9,9 +9,12 @@ export type Preview = {
|
|
|
9
9
|
/** `https://<port>-<sandbox>.<preview domain>/`. A private preview also
|
|
10
10
|
* needs the token. */
|
|
11
11
|
url: string;
|
|
12
|
-
/** Private previews: a
|
|
13
|
-
* `x-runtime-preview-token` header.
|
|
12
|
+
/** Private previews: a token signed for this answer. Send it as the
|
|
13
|
+
* `x-runtime-preview-token` header. Every create and read signs its own;
|
|
14
|
+
* one signed before keeps working until its own expiry or a rotate. */
|
|
14
15
|
token: string | null;
|
|
16
|
+
/** When this answer's token stops working. Not stored on the preview: a
|
|
17
|
+
* later read signs another token and leaves this one as it is. */
|
|
15
18
|
tokenExpiresAt: string | null;
|
|
16
19
|
/** Private previews: a link that carries the token once, for a browser. */
|
|
17
20
|
urlWithToken: string | null;
|
|
@@ -48,7 +51,7 @@ export declare function sandboxPreviews(t: Transport, sandbox: Sandbox): {
|
|
|
48
51
|
/** Shares `port`, or changes its visibility and embedOrigins if it is
|
|
49
52
|
* shared already. */
|
|
50
53
|
create: (port: number, input?: CreatePreview, options?: RequestOptions) => Promise<Preview>;
|
|
51
|
-
/** Every shared port, each private one with a fresh token. */
|
|
54
|
+
/** Every shared port, each private one with a fresh token that lasts a day; tokens signed before keep their own expiry. */
|
|
52
55
|
list: (options?: RequestOptions) => Promise<Preview[]>;
|
|
53
56
|
/** One preview, with a fresh private token. `expiresAt` bounds its expiry
|
|
54
57
|
* to an absolute ISO deadline; with no TTL, that deadline is used exactly. */
|
|
@@ -10,7 +10,7 @@ export function sandboxPreviews(t, sandbox) {
|
|
|
10
10
|
/** Shares `port`, or changes its visibility and embedOrigins if it is
|
|
11
11
|
* shared already. */
|
|
12
12
|
create: (port, input = {}, options) => t.json({ method: "POST", path: base(), body: { port, ...input }, ...options }),
|
|
13
|
-
/** Every shared port, each private one with a fresh token. */
|
|
13
|
+
/** Every shared port, each private one with a fresh token that lasts a day; tokens signed before keep their own expiry. */
|
|
14
14
|
list: async (options) => (await t.json({ method: "GET", path: base(), ...options })).data,
|
|
15
15
|
/** One preview, with a fresh private token. `expiresAt` bounds its expiry
|
|
16
16
|
* to an absolute ISO deadline; with no TTL, that deadline is used exactly. */
|
package/dist/snapshots.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ export type Snapshot = {
|
|
|
37
37
|
durableAt: string | null;
|
|
38
38
|
storedBytes: number | null;
|
|
39
39
|
};
|
|
40
|
+
/** An opaque tag for the server it is kept on, like `pl_3f9a0c1b2d4e5f60`.
|
|
41
|
+
* The same tag as a sandbox of yours means the same server. Null before it
|
|
42
|
+
* is placed. Absent from older servers. */
|
|
43
|
+
placement?: string | null;
|
|
40
44
|
error: string | null;
|
|
41
45
|
createdAt: string;
|
|
42
46
|
readyAt: string | null;
|
package/dist/ssh.d.ts
CHANGED
|
@@ -16,6 +16,41 @@ export declare function sshKey(env: NodeJS.ProcessEnv): Promise<{
|
|
|
16
16
|
export declare function resolveSandbox(runtime: Runtime, target: string): Promise<Sandbox>;
|
|
17
17
|
export declare function sshConfig(env: NodeJS.ProcessEnv): Promise<string>;
|
|
18
18
|
export declare function sshCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
|
|
19
|
+
/** The editors `runtime sandboxes code` opens: each one's command line program
|
|
20
|
+
* and the URL scheme its app registers. All three are VS Code underneath and
|
|
21
|
+
* name an SSH remote the same way, `ssh-remote+<host>`. */
|
|
22
|
+
export declare const EDITORS: {
|
|
23
|
+
readonly vscode: {
|
|
24
|
+
readonly name: "VS Code";
|
|
25
|
+
readonly bin: "code";
|
|
26
|
+
readonly scheme: "vscode";
|
|
27
|
+
};
|
|
28
|
+
readonly cursor: {
|
|
29
|
+
readonly name: "Cursor";
|
|
30
|
+
readonly bin: "cursor";
|
|
31
|
+
readonly scheme: "cursor";
|
|
32
|
+
};
|
|
33
|
+
readonly windsurf: {
|
|
34
|
+
readonly name: "Windsurf";
|
|
35
|
+
readonly bin: "windsurf";
|
|
36
|
+
readonly scheme: "windsurf";
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export type EditorName = keyof typeof EDITORS;
|
|
40
|
+
/** What opens a sandbox's folder in an editor over the `*.runtime` ssh host:
|
|
41
|
+
* the editor's own program and its arguments, or, without the program on the
|
|
42
|
+
* PATH, the link its app answers. */
|
|
43
|
+
export declare function editorLaunch(editor: EditorName, sandboxId: string, path: string): {
|
|
44
|
+
host: string;
|
|
45
|
+
bin: "code" | "cursor" | "windsurf";
|
|
46
|
+
args: string[];
|
|
47
|
+
link: string;
|
|
48
|
+
};
|
|
49
|
+
/** `runtime sandboxes code <id|name> [--editor cursor] [--path /workspace] [--print]`:
|
|
50
|
+
* a sandbox opened in VS Code, Cursor or Windsurf in one step. It makes sure
|
|
51
|
+
* the sandbox has an SSH server and this machine has the `*.runtime` ssh
|
|
52
|
+
* entry, then hands the editor the SSH remote. */
|
|
53
|
+
export declare function editorCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
|
|
19
54
|
/** `runtime sandboxes port-forward <id> 5432 8080:3000 ...` */
|
|
20
55
|
export declare function portForwardCommand(argv: string[], out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
|
|
21
56
|
export {};
|
package/dist/ssh.js
CHANGED
|
@@ -11,6 +11,9 @@ ${me} sandboxes ssh config [--install] The ssh config for \`ssh <id|nam
|
|
|
11
11
|
VS Code Remote-SSH and JetBrains Gateway; --install adds
|
|
12
12
|
it to ~/.ssh/config
|
|
13
13
|
${me} sandboxes ssh proxy <id|name>[.runtime] ProxyCommand mode: SSH on stdin and stdout
|
|
14
|
+
${me} sandboxes code <id|name> [--editor vscode|cursor|windsurf] [--path /workspace] [--print]
|
|
15
|
+
Open it in your editor over SSH; installs the ssh config
|
|
16
|
+
above once. --print gives the link instead
|
|
14
17
|
${me} sandboxes port-forward <id|name> <port|local:remote>... [--address 127.0.0.1]
|
|
15
18
|
Forward local ports to ports in the sandbox, until Ctrl-C
|
|
16
19
|
|
|
@@ -98,7 +101,7 @@ Host *.runtime
|
|
|
98
101
|
async function installConfig(env) {
|
|
99
102
|
const file = join(configDirectory(env), "ssh", "config");
|
|
100
103
|
await writeFile(file, await sshConfig(env), { mode: 0o600 });
|
|
101
|
-
const sshDir = join(homedir(), ".ssh");
|
|
104
|
+
const sshDir = join(env.HOME || homedir(), ".ssh");
|
|
102
105
|
await mkdir(sshDir, { recursive: true, mode: 0o700 });
|
|
103
106
|
const main = join(sshDir, "config");
|
|
104
107
|
const current = await readFile(main, "utf8").catch(() => "");
|
|
@@ -236,6 +239,95 @@ function usageError(message, me) {
|
|
|
236
239
|
hint: `Run \`${me} sandboxes ssh help\`.`,
|
|
237
240
|
});
|
|
238
241
|
}
|
|
242
|
+
/** The editors `runtime sandboxes code` opens: each one's command line program
|
|
243
|
+
* and the URL scheme its app registers. All three are VS Code underneath and
|
|
244
|
+
* name an SSH remote the same way, `ssh-remote+<host>`. */
|
|
245
|
+
export const EDITORS = {
|
|
246
|
+
vscode: { name: "VS Code", bin: "code", scheme: "vscode" },
|
|
247
|
+
cursor: { name: "Cursor", bin: "cursor", scheme: "cursor" },
|
|
248
|
+
windsurf: { name: "Windsurf", bin: "windsurf", scheme: "windsurf" },
|
|
249
|
+
};
|
|
250
|
+
/** What opens a sandbox's folder in an editor over the `*.runtime` ssh host:
|
|
251
|
+
* the editor's own program and its arguments, or, without the program on the
|
|
252
|
+
* PATH, the link its app answers. */
|
|
253
|
+
export function editorLaunch(editor, sandboxId, path) {
|
|
254
|
+
const { bin, scheme } = EDITORS[editor];
|
|
255
|
+
const remote = `ssh-remote+${sandboxId}.runtime`;
|
|
256
|
+
return {
|
|
257
|
+
host: `${sandboxId}.runtime`,
|
|
258
|
+
bin,
|
|
259
|
+
args: ["--remote", remote, path],
|
|
260
|
+
link: `${scheme}://vscode-remote/${remote}${path}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Runs a program, answering whether it started and exited 0; a program
|
|
264
|
+
* missing from the PATH is false, not an error. */
|
|
265
|
+
function launched(bin, args, env) {
|
|
266
|
+
return new Promise((resolve) => {
|
|
267
|
+
const child = spawn(bin, args, {
|
|
268
|
+
env,
|
|
269
|
+
stdio: "ignore",
|
|
270
|
+
// `code` and `cursor` are .cmd scripts on Windows.
|
|
271
|
+
shell: process.platform === "win32",
|
|
272
|
+
});
|
|
273
|
+
child.on("error", () => resolve(false));
|
|
274
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/** `runtime sandboxes code <id|name> [--editor cursor] [--path /workspace] [--print]`:
|
|
278
|
+
* a sandbox opened in VS Code, Cursor or Windsurf in one step. It makes sure
|
|
279
|
+
* the sandbox has an SSH server and this machine has the `*.runtime` ssh
|
|
280
|
+
* entry, then hands the editor the SSH remote. */
|
|
281
|
+
export async function editorCommand(argv, env, out, connect, me) {
|
|
282
|
+
const valueOf = (flag) => {
|
|
283
|
+
const at = argv.indexOf(flag);
|
|
284
|
+
return at === -1 ? undefined : argv[at + 1];
|
|
285
|
+
};
|
|
286
|
+
const editor = (valueOf("--editor") ?? "vscode").toLowerCase().replace(/^code$/, "vscode");
|
|
287
|
+
const path = valueOf("--path") ?? "/workspace";
|
|
288
|
+
const valued = new Set(["--editor", "--path"]);
|
|
289
|
+
const [target] = argv.filter((word, i) => !word.startsWith("--") && !valued.has(argv[i - 1] ?? ""));
|
|
290
|
+
if (!target || target === "help") {
|
|
291
|
+
out.write(SSH_HELP(me));
|
|
292
|
+
return target ? 0 : 64;
|
|
293
|
+
}
|
|
294
|
+
if (!(editor in EDITORS))
|
|
295
|
+
throw new RuntimeError({
|
|
296
|
+
message: `${editor} is not an editor this opens.`,
|
|
297
|
+
code: "usage",
|
|
298
|
+
status: 0,
|
|
299
|
+
hint: `Use --editor ${Object.keys(EDITORS).join(", --editor ")}; JetBrains Gateway connects to <id>.runtime after \`${me} sandboxes ssh config --install\`.`,
|
|
300
|
+
});
|
|
301
|
+
if (!path.startsWith("/"))
|
|
302
|
+
throw usageError(`--path is a folder in the sandbox, from /: ${path}`, me);
|
|
303
|
+
const sbx = await resolveSandbox(await connect(), target);
|
|
304
|
+
await ensureSshd(sbx, out);
|
|
305
|
+
await installConfig(env);
|
|
306
|
+
const how = editorLaunch(editor, sbx.id, path);
|
|
307
|
+
const { name } = EDITORS[editor];
|
|
308
|
+
const print = argv.includes("--print");
|
|
309
|
+
// Without the editor's program on the PATH (VS Code's `code` is opt-in on a
|
|
310
|
+
// Mac), the desktop opens the editor's link.
|
|
311
|
+
const [opener, ...openerArgs] = process.platform === "darwin"
|
|
312
|
+
? ["open", how.link]
|
|
313
|
+
: process.platform === "win32"
|
|
314
|
+
? ["cmd", "/c", "start", '""', how.link]
|
|
315
|
+
: ["xdg-open", how.link];
|
|
316
|
+
const opened = print
|
|
317
|
+
? false
|
|
318
|
+
: (await launched(how.bin, how.args, env))
|
|
319
|
+
? "program"
|
|
320
|
+
: (await launched(opener, openerArgs, env))
|
|
321
|
+
? "link"
|
|
322
|
+
: false;
|
|
323
|
+
if (out.json)
|
|
324
|
+
out.write(JSON.stringify({ sandboxId: sbx.id, editor, host: how.host, path, link: how.link, opened }));
|
|
325
|
+
else if (opened)
|
|
326
|
+
out.write(`Opening ${path} of ${sbx.id} in ${name} over SSH (host ${how.host}). It needs ${name === "VS Code" ? "the Remote - SSH extension" : "its Remote - SSH extension"}; the first connection installs the editor's server in the sandbox.`);
|
|
327
|
+
else
|
|
328
|
+
out.write(`${print ? "" : `Could not start ${how.bin} or open a link here. `}Open this link on this machine:\n${how.link}\nor run: ${how.bin} ${how.args.join(" ")}`);
|
|
329
|
+
return opened || print ? 0 : 1;
|
|
330
|
+
}
|
|
239
331
|
/** `runtime sandboxes port-forward <id> 5432 8080:3000 ...` */
|
|
240
332
|
export async function portForwardCommand(argv, out, connect, me) {
|
|
241
333
|
const at = argv.indexOf("--address");
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.16.0";
|
|
3
3
|
export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
4
4
|
/** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
|
|
5
5
|
* own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
|
|
@@ -54,14 +54,17 @@ export type Call = RequestOptions & {
|
|
|
54
54
|
/** How long to keep retrying, with the same key and input, a refusal that
|
|
55
55
|
* clears when a sandbox stops or a host frees room (WAITS_FOR_ROOM): a
|
|
56
56
|
* full slots without credit, a full quota, a full region. Then the refusal is thrown as
|
|
57
|
-
* it came.
|
|
57
|
+
* it came. A create, wake or restart sets it; 0 or absent fails at once. */
|
|
58
58
|
waitForCapacityMs?: number;
|
|
59
59
|
/** Called each time a call waits for room, with the refusal it is waiting
|
|
60
60
|
* out and how long it will sleep before sending the call again. */
|
|
61
61
|
onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
|
|
62
|
-
/** Internal: when the call's own deadline ends (performance.now()),
|
|
63
|
-
* once the call is prepared; absent when it has none. */
|
|
64
|
-
deadlineAt?: number;
|
|
62
|
+
/** Internal: when the call's own deadline ends now (performance.now()),
|
|
63
|
+
* set once the call is prepared; absent when it has none. */
|
|
64
|
+
deadlineAt?: () => number;
|
|
65
|
+
/** Internal: moves the call's deadline on by a wait for room, so a wait
|
|
66
|
+
* for capacity never spends the caller's own `timeoutMs`. */
|
|
67
|
+
extendDeadline?: (ms: number) => void;
|
|
65
68
|
};
|
|
66
69
|
/** How long one attempt waits for its answer to start. The API starts every
|
|
67
70
|
* answer within 120 s: Caddy answers 503 itself past that, and a longer call
|
|
@@ -69,8 +72,8 @@ export type Call = RequestOptions & {
|
|
|
69
72
|
* with no answer by then is talking to a server that is gone, such as a
|
|
70
73
|
* controller that died with the call in flight while its address moved to
|
|
71
74
|
* the new leader: a client that only waits sends nothing the new leader could
|
|
72
|
-
* refuse, so it waited out the whole deadline and failed (
|
|
73
|
-
*
|
|
75
|
+
* refuse, so it waited out the whole deadline and failed (a controller
|
|
76
|
+
* failover drill, 4 October 2026). It is sent again under the same key. */
|
|
74
77
|
export declare const ANSWER_START_MS = 125000;
|
|
75
78
|
export declare function apiOrigin(value: string): string;
|
|
76
79
|
export declare function encodeQuery(query: Query | undefined): string;
|
|
@@ -90,7 +93,7 @@ export declare const DEFAULT_MAX_CONNECTIONS = 48;
|
|
|
90
93
|
/** AbortSignal.timeout in whole milliseconds, rounded up, never under 1.
|
|
91
94
|
* Node throws ERR_OUT_OF_RANGE on a fraction (Bun accepts one), so a
|
|
92
95
|
* customer's timeoutMs of 1500.5 would be a RangeError instead of a request;
|
|
93
|
-
* the live promise probe broke this way on 9 October 2026
|
|
96
|
+
* the live promise probe broke this way on 9 October 2026. Every
|
|
94
97
|
* deadline the SDK makes from a customer's or a computed value goes through
|
|
95
98
|
* this. */
|
|
96
99
|
export declare function timeoutSignal(ms: number): AbortSignal;
|
package/dist/transport.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
|
|
2
2
|
import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
|
|
3
3
|
import { warningText } from "./product-renames.js";
|
|
4
|
-
export const VERSION = "0.
|
|
4
|
+
export const VERSION = "0.16.0";
|
|
5
5
|
export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
6
6
|
/** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
|
|
7
7
|
* own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
|
|
@@ -48,8 +48,8 @@ const NOTHING_RAN_MS = 300_000;
|
|
|
48
48
|
* with no answer by then is talking to a server that is gone, such as a
|
|
49
49
|
* controller that died with the call in flight while its address moved to
|
|
50
50
|
* the new leader: a client that only waits sends nothing the new leader could
|
|
51
|
-
* refuse, so it waited out the whole deadline and failed (
|
|
52
|
-
*
|
|
51
|
+
* refuse, so it waited out the whole deadline and failed (a controller
|
|
52
|
+
* failover drill, 4 October 2026). It is sent again under the same key. */
|
|
53
53
|
export const ANSWER_START_MS = 125_000;
|
|
54
54
|
export function apiOrigin(value) {
|
|
55
55
|
const url = new URL(value);
|
|
@@ -179,12 +179,36 @@ const KEPT_FOR_CALLS = 8;
|
|
|
179
179
|
/** AbortSignal.timeout in whole milliseconds, rounded up, never under 1.
|
|
180
180
|
* Node throws ERR_OUT_OF_RANGE on a fraction (Bun accepts one), so a
|
|
181
181
|
* customer's timeoutMs of 1500.5 would be a RangeError instead of a request;
|
|
182
|
-
* the live promise probe broke this way on 9 October 2026
|
|
182
|
+
* the live promise probe broke this way on 9 October 2026. Every
|
|
183
183
|
* deadline the SDK makes from a customer's or a computed value goes through
|
|
184
184
|
* this. */
|
|
185
185
|
export function timeoutSignal(ms) {
|
|
186
186
|
return AbortSignal.timeout(Math.max(1, Math.ceil(ms)));
|
|
187
187
|
}
|
|
188
|
+
/** A deadline `ms` from now that `extend` moves later; it aborts with a
|
|
189
|
+
* TimeoutError, as AbortSignal.timeout does. */
|
|
190
|
+
function movableDeadline(ms) {
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
let at = performance.now() + ms;
|
|
193
|
+
let timer;
|
|
194
|
+
const arm = () => {
|
|
195
|
+
timer = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), Math.max(1, Math.ceil(at - performance.now())));
|
|
196
|
+
// Never keeps a process alive by itself, as AbortSignal.timeout does not.
|
|
197
|
+
timer.unref?.();
|
|
198
|
+
};
|
|
199
|
+
arm();
|
|
200
|
+
return {
|
|
201
|
+
signal: controller.signal,
|
|
202
|
+
at: () => at,
|
|
203
|
+
extend: (by) => {
|
|
204
|
+
if (controller.signal.aborted || by <= 0)
|
|
205
|
+
return;
|
|
206
|
+
at += by;
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
arm();
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
188
212
|
export class Transport {
|
|
189
213
|
baseUrl;
|
|
190
214
|
#apiKey;
|
|
@@ -263,22 +287,29 @@ export class Transport {
|
|
|
263
287
|
* signal through every retry and the complete response body. */
|
|
264
288
|
#prepare(call) {
|
|
265
289
|
const timeoutMs = call.timeoutMs ?? this.#timeoutMs;
|
|
266
|
-
|
|
267
|
-
|
|
290
|
+
// The deadline is the caller's timeoutMs plus only the time actually
|
|
291
|
+
// spent waiting for room (WAITS_FOR_ROOM), which moves it on as it waits:
|
|
292
|
+
// a wake given timeoutMs 20 on a server that never answers still ends at
|
|
293
|
+
// 20 ms, not two minutes later (lifecycle-deadlines.test.ts).
|
|
294
|
+
const deadline = timeoutMs === 0 ? undefined : movableDeadline(timeoutMs);
|
|
268
295
|
const signal = deadline
|
|
269
296
|
? call.signal
|
|
270
|
-
? AbortSignal.any([call.signal, deadline])
|
|
271
|
-
: deadline
|
|
297
|
+
? AbortSignal.any([call.signal, deadline.signal])
|
|
298
|
+
: deadline.signal
|
|
272
299
|
: call.signal;
|
|
273
|
-
|
|
300
|
+
const prepared = {
|
|
274
301
|
...call,
|
|
275
302
|
timeoutMs: 0,
|
|
276
|
-
...(timeoutMs === 0 ? {} : { deadlineAt: performance.now() + timeoutMs + roomMs }),
|
|
277
303
|
...(signal ? { signal } : {}),
|
|
278
304
|
...(call.method !== "GET"
|
|
279
305
|
? { idempotencyKey: call.idempotencyKey ?? crypto.randomUUID() }
|
|
280
306
|
: {}),
|
|
281
307
|
};
|
|
308
|
+
if (deadline) {
|
|
309
|
+
prepared.deadlineAt = deadline.at;
|
|
310
|
+
prepared.extendDeadline = deadline.extend;
|
|
311
|
+
}
|
|
312
|
+
return prepared;
|
|
282
313
|
}
|
|
283
314
|
#connectionError(call, cause) {
|
|
284
315
|
const cancelled = call.signal?.aborted === true;
|
|
@@ -343,7 +374,9 @@ export class Transport {
|
|
|
343
374
|
const retry = call.retry ?? true;
|
|
344
375
|
let attempt = 0;
|
|
345
376
|
// An answer that ran nothing is sent again until the call's deadline.
|
|
346
|
-
|
|
377
|
+
// Read at each use: a wait for room moves the call's deadline on.
|
|
378
|
+
const idleFallback = performance.now() + NOTHING_RAN_MS;
|
|
379
|
+
const idleUntil = () => call.deadlineAt?.() ?? idleFallback;
|
|
347
380
|
let idleAttempt = 0;
|
|
348
381
|
// The last try never left, and was sent again at once.
|
|
349
382
|
let resentAtOnce = false;
|
|
@@ -434,6 +467,7 @@ export class Transport {
|
|
|
434
467
|
inCallback = true;
|
|
435
468
|
call.onCapacityWait?.(error, pause);
|
|
436
469
|
inCallback = false;
|
|
470
|
+
call.extendDeadline?.(pause);
|
|
437
471
|
await this.#retryAfter(error, pause, signal, gate);
|
|
438
472
|
continue;
|
|
439
473
|
}
|
|
@@ -448,7 +482,7 @@ export class Transport {
|
|
|
448
482
|
const header = Number(response.headers.get("retry-after"));
|
|
449
483
|
const base = error.retryAfterMs ?? (Number.isFinite(header) && header > 0 ? header * 1000 : 250);
|
|
450
484
|
const pause = Math.min(30_000, base * 2 ** Math.min(idleAttempt++, 3)) * (0.9 + Math.random() * 0.2);
|
|
451
|
-
if (pause >= idleUntil - performance.now())
|
|
485
|
+
if (pause >= idleUntil() - performance.now())
|
|
452
486
|
throw error;
|
|
453
487
|
await this.#retryAfter(error, pause, signal, gate);
|
|
454
488
|
continue;
|
package/dist/types.d.ts
CHANGED
|
@@ -62,6 +62,11 @@ export type SandboxInfo = {
|
|
|
62
62
|
/** The names of its own environment variables (`env`), on a create, an
|
|
63
63
|
* update and a read of one sandbox. Values are never shown. */
|
|
64
64
|
envNames?: string[];
|
|
65
|
+
/** On an update with labels by a key that may read secrets, when one of
|
|
66
|
+
* the account's egress secrets is scoped: true once every server running its sandboxes holds the change,
|
|
67
|
+
* so the next request uses the new scope; false while one has not
|
|
68
|
+
* confirmed (it takes it within 15 seconds). */
|
|
69
|
+
secretsEnforced?: boolean;
|
|
65
70
|
/** On a create from an image with a start command: whether it started
|
|
66
71
|
* (and was still running half a second later), became ready (readyMs),
|
|
67
72
|
* timed out, or exited first (exitCode, and stderr's last lines). The create's answer
|
|
@@ -78,6 +83,11 @@ export type SandboxInfo = {
|
|
|
78
83
|
};
|
|
79
84
|
/** Present and true when getOrCreate answered a sandbox that already held the name. */
|
|
80
85
|
reused?: boolean;
|
|
86
|
+
/** An opaque tag for the server it is on, like `pl_3f9a0c1b2d4e5f60`. Your
|
|
87
|
+
* sandboxes and snapshots with the same tag are on the same server. It is
|
|
88
|
+
* never the server's name, and other accounts see unrelated tags. Null
|
|
89
|
+
* before it is placed. Absent from older servers. */
|
|
90
|
+
placement?: string | null;
|
|
81
91
|
[key: string]: unknown;
|
|
82
92
|
};
|
|
83
93
|
export type CreateSandbox = {
|
|
@@ -142,6 +152,13 @@ export type CreateSandbox = {
|
|
|
142
152
|
image?: string;
|
|
143
153
|
/** A ready snapshot's id: the sandbox starts as a copy of it, with its shape. Not with image. */
|
|
144
154
|
snapshot?: string;
|
|
155
|
+
/** One to 16 ids of your own sandboxes or snapshots. The new sandbox starts
|
|
156
|
+
* on a server none of them is on, and stays apart if its start is moved.
|
|
157
|
+
* With no other server that has room, the create answers 409 `no_capacity`
|
|
158
|
+
* and nothing is made or charged. A copy of a snapshot starts only where that
|
|
159
|
+
* snapshot is kept, so a `snapshot` with `spreadFrom` naming its server also
|
|
160
|
+
* answers `no_capacity`. */
|
|
161
|
+
spreadFrom?: string[];
|
|
145
162
|
/** Up to four volumes: read-write ("rw", one sandbox at a time) or a
|
|
146
163
|
* read-only "snapshot" copy. */
|
|
147
164
|
volumes?: {
|
|
@@ -430,6 +447,13 @@ export type SandboxSettings = {
|
|
|
430
447
|
persistent?: boolean;
|
|
431
448
|
/** Lifetime cap in microdollars; null removes it. */
|
|
432
449
|
maxTotalCostMicros?: number | null;
|
|
450
|
+
/** A new time limit counted from now, 60 to 86400 seconds, sooner or later
|
|
451
|
+
* than the one it had; 0 takes the limit away. A paused sandbox counts it
|
|
452
|
+
* from its next wake. Not for a persistent sandbox. */
|
|
453
|
+
timeoutSeconds?: number;
|
|
454
|
+
/** What the end of its time limit does: pause, stop (keep the disk) or
|
|
455
|
+
* delete. */
|
|
456
|
+
onTimeout?: "pause" | "stop" | "delete";
|
|
433
457
|
};
|
|
434
458
|
/** A deleted sandbox, as `delete()` answers it the first time and every time
|
|
435
459
|
* after. It reads nowhere else afterwards. */
|
package/dist/vercel/command.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
1
2
|
import type { Process } from "../sandbox.js";
|
|
2
3
|
/** One piece of a command's output, as Vercel's `logs()` yields it. */
|
|
3
4
|
export type LogOutputLine = {
|
|
@@ -21,6 +22,8 @@ type Init = {
|
|
|
21
22
|
sandboxName: string;
|
|
22
23
|
process?: Process;
|
|
23
24
|
lines?: LogOutputLine[];
|
|
25
|
+
/** Each line as it arrives, for an attached command's stdout/stderr. */
|
|
26
|
+
onLine?: (line: LogOutputLine) => void;
|
|
24
27
|
};
|
|
25
28
|
/** A command in a sandbox, as Vercel's `Command`. A detached one runs on:
|
|
26
29
|
* `wait()` for its end, `logs()` for its output as it happens. `cmdId` is
|
|
@@ -30,6 +33,8 @@ export declare class Command {
|
|
|
30
33
|
exitCode: number | null;
|
|
31
34
|
durationMs?: number;
|
|
32
35
|
protected readonly init: Init;
|
|
36
|
+
/** Set when the command was started with a `Readable` for stdin. */
|
|
37
|
+
stdinPipe: StdinPipe | null;
|
|
33
38
|
constructor(init: Init);
|
|
34
39
|
get cmdId(): string;
|
|
35
40
|
get cwd(): string;
|
|
@@ -41,7 +46,8 @@ export declare class Command {
|
|
|
41
46
|
}): AsyncGenerator<LogOutputLine, void, void> & Disposable & {
|
|
42
47
|
close(): void;
|
|
43
48
|
};
|
|
44
|
-
/** Waits for the command to end.
|
|
49
|
+
/** Waits for the command to end. With a `Readable` piped to its stdin,
|
|
50
|
+
* rejects if piping fails, and stops piping once the command exits. */
|
|
45
51
|
wait(params?: {
|
|
46
52
|
signal?: AbortSignal;
|
|
47
53
|
}): Promise<CommandFinished>;
|
|
@@ -60,11 +66,52 @@ export declare class Command {
|
|
|
60
66
|
kill(signal?: Signal, opts?: {
|
|
61
67
|
abortSignal?: AbortSignal;
|
|
62
68
|
}): Promise<void>;
|
|
69
|
+
/** Writes to the stdin of a command started with `stdin: true` and
|
|
70
|
+
* `detached: true`. Writes reach the process in call order and resolve once
|
|
71
|
+
* it has accepted the data; Runtime's input offsets make every resend exact,
|
|
72
|
+
* so no byte is written twice. If a write fails, part of it may have been
|
|
73
|
+
* written, and later writes are refused; closeStdin still works. */
|
|
74
|
+
writeStdin(data: string | Uint8Array, opts?: {
|
|
75
|
+
abortSignal?: AbortSignal;
|
|
76
|
+
}): Promise<void>;
|
|
77
|
+
/** Closes the command's stdin, so it reads EOF once it has consumed what
|
|
78
|
+
* was written. */
|
|
79
|
+
closeStdin(opts?: {
|
|
80
|
+
abortSignal?: AbortSignal;
|
|
81
|
+
}): Promise<void>;
|
|
63
82
|
}
|
|
83
|
+
/** A `Readable` being written to a command's stdin. */
|
|
84
|
+
export interface StdinPipe {
|
|
85
|
+
/** Resolves once the stream has ended and stdin is closed, or the command
|
|
86
|
+
* stops accepting input; rejects if a write fails or the stream errors. */
|
|
87
|
+
done: Promise<void>;
|
|
88
|
+
/** Stops reading the stream, for when the command has exited. */
|
|
89
|
+
stop(): void;
|
|
90
|
+
}
|
|
91
|
+
/** Writes a stream to a command's stdin in order, closing stdin when it
|
|
92
|
+
* ends. What is read while a write is in flight goes in the next write, and
|
|
93
|
+
* reading pauses while too much waits. The stream is never ended or
|
|
94
|
+
* destroyed, and every listener is removed once piping stops. */
|
|
95
|
+
export declare function pipeStdin(command: Command, stream: Readable, signal?: AbortSignal): StdinPipe;
|
|
96
|
+
/** Waits for a command to finish, rejecting early if piping its stdin fails,
|
|
97
|
+
* and stops piping once it has exited. Aborting `signal` stops waiting but
|
|
98
|
+
* leaves the pipe running. */
|
|
99
|
+
export declare function waitWithStdinPipe<T>(finished: Promise<T>, pipe: StdinPipe | null, opts?: {
|
|
100
|
+
signal?: AbortSignal;
|
|
101
|
+
onPipeError?: () => void;
|
|
102
|
+
}): Promise<T>;
|
|
64
103
|
/** A command that has ended; `exitCode` is set. */
|
|
65
104
|
export declare class CommandFinished extends Command {
|
|
66
105
|
exitCode: number;
|
|
67
106
|
constructor(init: Init, exitCode: number, durationMs?: number);
|
|
68
107
|
wait(): Promise<CommandFinished>;
|
|
108
|
+
/** Always throws: the command has already exited. */
|
|
109
|
+
writeStdin(_data: string | Uint8Array, _opts?: {
|
|
110
|
+
abortSignal?: AbortSignal;
|
|
111
|
+
}): Promise<never>;
|
|
112
|
+
/** Always throws: the command has already exited. */
|
|
113
|
+
closeStdin(_opts?: {
|
|
114
|
+
abortSignal?: AbortSignal;
|
|
115
|
+
}): Promise<never>;
|
|
69
116
|
}
|
|
70
117
|
export {};
|