withruntime 0.3.1 → 0.5.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 +129 -0
- package/E2B.md +146 -0
- package/README.md +84 -12
- package/VERCEL.md +127 -0
- package/dist/ai/index.d.ts +33 -0
- package/dist/ai/index.js +38 -0
- package/dist/claude-agent-sdk/index.d.ts +7 -0
- package/dist/claude-agent-sdk/index.js +65 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +949 -105
- package/dist/client.d.ts +25 -2
- package/dist/client.js +14 -1
- package/dist/credentials.d.ts +10 -0
- package/dist/credentials.js +56 -1
- package/dist/daytona/client.d.ts +44 -0
- package/dist/daytona/client.js +40 -0
- package/dist/daytona/context.d.ts +26 -0
- package/dist/daytona/context.js +22 -0
- package/dist/daytona/daytona.d.ts +185 -0
- package/dist/daytona/daytona.js +416 -0
- package/dist/daytona/errors.d.ts +106 -0
- package/dist/daytona/errors.js +182 -0
- package/dist/daytona/filesystem.d.ts +84 -0
- package/dist/daytona/filesystem.js +206 -0
- package/dist/daytona/git.d.ts +33 -0
- package/dist/daytona/git.js +128 -0
- package/dist/daytona/image.d.ts +45 -0
- package/dist/daytona/image.js +140 -0
- package/dist/daytona/index.d.ts +50 -0
- package/dist/daytona/index.js +50 -0
- package/dist/daytona/process.d.ts +86 -0
- package/dist/daytona/process.js +339 -0
- package/dist/daytona/sandbox.d.ts +196 -0
- package/dist/daytona/sandbox.js +448 -0
- package/dist/e2b/client.d.ts +58 -0
- package/dist/e2b/client.js +66 -0
- package/dist/e2b/code-interpreter.d.ts +144 -0
- package/dist/e2b/code-interpreter.js +244 -0
- package/dist/e2b/commands.d.ts +100 -0
- package/dist/e2b/commands.js +268 -0
- package/dist/e2b/errors.d.ts +72 -0
- package/dist/e2b/errors.js +151 -0
- package/dist/e2b/filesystem.d.ts +82 -0
- package/dist/e2b/filesystem.js +143 -0
- package/dist/e2b/index.d.ts +28 -0
- package/dist/e2b/index.js +61 -0
- package/dist/e2b/sandbox.d.ts +203 -0
- package/dist/e2b/sandbox.js +490 -0
- package/dist/e2b/unsupported.d.ts +19 -0
- package/dist/e2b/unsupported.js +27 -0
- package/dist/errors.d.ts +10 -4
- package/dist/errors.js +26 -6
- package/dist/image-context.d.ts +39 -0
- package/dist/image-context.js +176 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +17 -2
- package/dist/keys.d.ts +43 -0
- package/dist/keys.js +196 -0
- package/dist/login.js +27 -6
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +4 -3
- package/dist/openai-agents/index.d.ts +132 -0
- package/dist/openai-agents/index.js +791 -0
- package/dist/products/audit.d.ts +48 -0
- package/dist/products/audit.js +10 -0
- package/dist/products/images.d.ts +113 -7
- package/dist/products/images.js +186 -27
- package/dist/products/index.d.ts +11 -0
- package/dist/products/index.js +17 -1
- package/dist/products/observability.d.ts +186 -0
- package/dist/products/observability.js +117 -0
- package/dist/products/secrets.d.ts +40 -0
- package/dist/products/secrets.js +24 -0
- package/dist/products/switching.d.ts +89 -0
- package/dist/products/switching.js +25 -0
- package/dist/proxy.d.ts +30 -0
- package/dist/proxy.js +178 -0
- package/dist/sandbox.d.ts +30 -2
- package/dist/sandbox.js +101 -1
- package/dist/ssh.d.ts +21 -0
- package/dist/ssh.js +312 -0
- package/dist/tar.d.ts +1 -0
- package/dist/tar.js +4 -4
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.js +180 -0
- package/dist/transport.d.ts +12 -2
- package/dist/transport.js +76 -16
- package/dist/tunnel.d.ts +67 -0
- package/dist/tunnel.js +311 -0
- package/dist/types.d.ts +57 -1
- package/dist/vercel/client.d.ts +40 -0
- package/dist/vercel/client.js +39 -0
- package/dist/vercel/command.d.ts +70 -0
- package/dist/vercel/command.js +148 -0
- package/dist/vercel/errors.d.ts +53 -0
- package/dist/vercel/errors.js +113 -0
- package/dist/vercel/filesystem.d.ts +150 -0
- package/dist/vercel/filesystem.js +248 -0
- package/dist/vercel/index.d.ts +30 -0
- package/dist/vercel/index.js +38 -0
- package/dist/vercel/sandbox.d.ts +317 -0
- package/dist/vercel/sandbox.js +726 -0
- package/dist/vercel/snapshot.d.ts +93 -0
- package/dist/vercel/snapshot.js +123 -0
- package/package.json +69 -5
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { gzipSync } from "node:zlib";
|
|
4
|
+
import { tarHeader } from "./tar.js";
|
|
5
|
+
/* A Dockerfile's build context, made from a folder the way `docker build`
|
|
6
|
+
* makes it: every file the .dockerignore does not exclude, in one gzipped tar
|
|
7
|
+
* archive, with a list of the files and their SHA-256 for the server to
|
|
8
|
+
* resolve COPY against. The archive is deterministic (sorted, no times, no
|
|
9
|
+
* owners), so an unchanged folder gives the same chunks and a rebuild
|
|
10
|
+
* uploads nothing. Node and Bun only: it reads the file system. */
|
|
11
|
+
export const CONTEXT_LIMITS = {
|
|
12
|
+
/** The archive, compressed: what is uploaded. */
|
|
13
|
+
bytes: 100 * 1_048_576,
|
|
14
|
+
files: 20_000,
|
|
15
|
+
chunkBytes: 1_048_576,
|
|
16
|
+
};
|
|
17
|
+
/** A .dockerignore as Docker reads it: `#` comments, `!` re-includes, `**`
|
|
18
|
+
* crosses directories, a pattern naming a directory excludes what is in it,
|
|
19
|
+
* and the last matching pattern decides. The same rules as the server's
|
|
20
|
+
* (packages/cloud/src/images/dockerfile.ts `dockerignoreFilter`). */
|
|
21
|
+
export function dockerignoreFilter(text) {
|
|
22
|
+
if (text === undefined || text.trim() === "")
|
|
23
|
+
return () => false;
|
|
24
|
+
const rules = [];
|
|
25
|
+
for (const raw of text.replace(/\r\n?/g, "\n").split("\n")) {
|
|
26
|
+
let line = raw.trim();
|
|
27
|
+
if (!line || line.startsWith("#"))
|
|
28
|
+
continue;
|
|
29
|
+
const negate = line.startsWith("!");
|
|
30
|
+
if (negate)
|
|
31
|
+
line = line.slice(1).trim();
|
|
32
|
+
line = normalize(line.replace(/^\/+/, "")).replace(/\/+$/, "");
|
|
33
|
+
if (!line || line === ".")
|
|
34
|
+
continue;
|
|
35
|
+
let source = "";
|
|
36
|
+
for (let i = 0; i < line.length; i++) {
|
|
37
|
+
const char = line[i];
|
|
38
|
+
if (char === "*" && line[i + 1] === "*") {
|
|
39
|
+
if (line[i + 2] === "/") {
|
|
40
|
+
source += "(?:.*/)?";
|
|
41
|
+
i += 2;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
source += ".*";
|
|
45
|
+
i += 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (char === "*")
|
|
49
|
+
source += "[^/]*";
|
|
50
|
+
else if (char === "?")
|
|
51
|
+
source += "[^/]";
|
|
52
|
+
else if (char === "[") {
|
|
53
|
+
const end = line.indexOf("]", i + 1);
|
|
54
|
+
if (end < 0)
|
|
55
|
+
source += "\\[";
|
|
56
|
+
else {
|
|
57
|
+
source += `[${line
|
|
58
|
+
.slice(i + 1, end)
|
|
59
|
+
.replace(/^!/, "^")
|
|
60
|
+
.replace(/\\/g, "\\\\")}]`;
|
|
61
|
+
i = end;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (char === "\\" && i + 1 < line.length) {
|
|
65
|
+
source += line[++i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
source += char.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
rules.push({ negate, regex: new RegExp(`^${source}$`) });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error(`.dockerignore: invalid pattern ${JSON.stringify(raw.trim())}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return (path) => {
|
|
78
|
+
const parts = path.split("/");
|
|
79
|
+
let excluded = false;
|
|
80
|
+
for (const rule of rules) {
|
|
81
|
+
let hit = false;
|
|
82
|
+
for (let n = parts.length; n >= 1 && !hit; n--)
|
|
83
|
+
hit = rule.regex.test(parts.slice(0, n).join("/"));
|
|
84
|
+
if (hit)
|
|
85
|
+
excluded = !rule.negate;
|
|
86
|
+
}
|
|
87
|
+
return excluded;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** posix.normalize, without importing a path module into this function. */
|
|
91
|
+
function normalize(path) {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const part of path.split("/")) {
|
|
94
|
+
if (part === "" || part === ".")
|
|
95
|
+
continue;
|
|
96
|
+
if (part === "..")
|
|
97
|
+
out.pop();
|
|
98
|
+
else
|
|
99
|
+
out.push(part);
|
|
100
|
+
}
|
|
101
|
+
return out.join("/") || ".";
|
|
102
|
+
}
|
|
103
|
+
async function sha256(bytes) {
|
|
104
|
+
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
|
105
|
+
return Buffer.from(digest).toString("hex");
|
|
106
|
+
}
|
|
107
|
+
/** The context of a folder: its .dockerignore (or `<Dockerfile>.dockerignore`)
|
|
108
|
+
* applied, `.git` left out when there is none. Symbolic links are left out:
|
|
109
|
+
* COPY reads files. */
|
|
110
|
+
export async function packContext(folder, options = {}) {
|
|
111
|
+
const root = resolve(folder);
|
|
112
|
+
let dockerignore = options.dockerignore;
|
|
113
|
+
if (dockerignore === undefined)
|
|
114
|
+
for (const candidate of [
|
|
115
|
+
...(options.dockerfile ? [`${resolve(root, options.dockerfile)}.dockerignore`] : []),
|
|
116
|
+
join(root, ".dockerignore"),
|
|
117
|
+
]) {
|
|
118
|
+
dockerignore = await readFile(candidate, "utf8").catch(() => undefined);
|
|
119
|
+
if (dockerignore !== undefined)
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const ignored = dockerignoreFilter(dockerignore ?? ".git\n");
|
|
123
|
+
const reincludes = /^\s*!/m.test(dockerignore ?? "");
|
|
124
|
+
const files = [];
|
|
125
|
+
const parts = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
async function walk(directory) {
|
|
128
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
129
|
+
const full = join(directory, name);
|
|
130
|
+
const path = relative(root, full).split(sep).join("/");
|
|
131
|
+
const info = await lstat(full);
|
|
132
|
+
if (info.isDirectory()) {
|
|
133
|
+
// A directory excluded outright is not walked, unless a later `!`
|
|
134
|
+
// rule could bring something in it back.
|
|
135
|
+
if (ignored(path) && !reincludes)
|
|
136
|
+
continue;
|
|
137
|
+
await walk(full);
|
|
138
|
+
}
|
|
139
|
+
else if (info.isFile()) {
|
|
140
|
+
if (ignored(path))
|
|
141
|
+
continue;
|
|
142
|
+
if (files.length >= CONTEXT_LIMITS.files)
|
|
143
|
+
throw new Error(`The build context has more than ${CONTEXT_LIMITS.files} files. Leave some out with a .dockerignore.`);
|
|
144
|
+
const data = new Uint8Array(await readFile(full));
|
|
145
|
+
total += data.length;
|
|
146
|
+
const mode = info.mode & 0o777;
|
|
147
|
+
files.push({ path, sha256: await sha256(data), size: data.length, mode });
|
|
148
|
+
parts.push(tarHeader(path, data.length, mode, "0", "", 0), data);
|
|
149
|
+
const pad = (512 - (data.length % 512)) % 512;
|
|
150
|
+
if (pad)
|
|
151
|
+
parts.push(new Uint8Array(pad));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
await walk(root);
|
|
156
|
+
parts.push(new Uint8Array(1024));
|
|
157
|
+
const tar = new Uint8Array(parts.reduce((n, p) => n + p.length, 0) || 1024);
|
|
158
|
+
let offset = 0;
|
|
159
|
+
for (const part of parts) {
|
|
160
|
+
tar.set(part, offset);
|
|
161
|
+
offset += part.length;
|
|
162
|
+
}
|
|
163
|
+
const archive = new Uint8Array(gzipSync(tar, { level: 6 }));
|
|
164
|
+
if (archive.length > CONTEXT_LIMITS.bytes)
|
|
165
|
+
throw new Error(`The build context is ${Math.ceil(archive.length / 1_048_576)} MiB compressed (${Math.ceil(total / 1_048_576)} MiB of files); the most is ${CONTEXT_LIMITS.bytes / 1_048_576} MiB. Leave build outputs and dependencies out with a .dockerignore.`);
|
|
166
|
+
return { archive, files, ...(dockerignore === undefined ? {} : { dockerignore }) };
|
|
167
|
+
}
|
|
168
|
+
/** The archive cut into the 1 MiB chunks the server stores by digest. */
|
|
169
|
+
export async function chunkArchive(archive) {
|
|
170
|
+
const chunks = [];
|
|
171
|
+
for (let at = 0; at < archive.length; at += CONTEXT_LIMITS.chunkBytes) {
|
|
172
|
+
const bytes = archive.subarray(at, Math.min(archive.length, at + CONTEXT_LIMITS.chunkBytes));
|
|
173
|
+
chunks.push({ sha256: await sha256(bytes), bytes });
|
|
174
|
+
}
|
|
175
|
+
return { sha256: await sha256(archive), size: archive.length, chunks };
|
|
176
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
import type { Runtime } from "./client.js";
|
|
8
8
|
import { type RuntimeOptions } from "./client.js";
|
|
9
9
|
import { Sandbox as SandboxClass } from "./sandbox.js";
|
|
10
|
-
import type { CreateSandbox } from "./types.js";
|
|
10
|
+
import type { CreateSandbox, KeepAliveOptions } from "./types.js";
|
|
11
11
|
import type { RequestOptions } from "./transport.js";
|
|
12
12
|
export { Runtime, Sandboxes, FeedbackApi, SupportApi, type RuntimeOptions, type SupportReply, } from "./client.js";
|
|
13
13
|
export { Files, Process, Processes, Terminal } from "./sandbox.js";
|
|
14
|
+
export { Tunnel, TunnelStream, type PortForward } from "./tunnel.js";
|
|
14
15
|
export { Page } from "./page.js";
|
|
15
16
|
export type { KeyLimits } from "./products/limits.js";
|
|
17
|
+
export type { AuditEvent, AuditPage } from "./products/audit.js";
|
|
18
|
+
export type { Secret, SetSecret } from "./products/secrets.js";
|
|
19
|
+
export { verifyWebhook, WebhookVerificationError, type MetricPoint, type MetricRange, type OtelExport, type RuntimeEvent, type SandboxMetrics, type Webhook, type WebhookDelivery, type WebhookEventType, } from "./products/observability.js";
|
|
16
20
|
export { Snapshots, type Snapshot, type SnapshotOptions } from "./snapshots.js";
|
|
17
21
|
export { Transport, VERSION, apiOrigin, type RequestOptions, type Call } from "./transport.js";
|
|
18
22
|
export * from "./errors.js";
|
|
@@ -20,9 +24,22 @@ export type * from "./types.js";
|
|
|
20
24
|
/** A sandbox. `Sandbox.create()` uses RUNTIME_API_KEY; `runtime.sandboxes`
|
|
21
25
|
* does the same with an explicit client. */
|
|
22
26
|
export declare class Sandbox extends SandboxClass {
|
|
23
|
-
/** Creates a sandbox (every field optional) and waits until it is running.
|
|
27
|
+
/** Creates a sandbox (every field optional) and waits until it is running.
|
|
28
|
+
* `keepAlive: true` (or its options) extends its lease in the background
|
|
29
|
+
* until you stop it; see `sandbox.keepAlive()`. */
|
|
24
30
|
static create(input?: CreateSandbox & {
|
|
25
31
|
wait?: boolean;
|
|
32
|
+
keepAlive?: boolean | KeepAliveOptions;
|
|
33
|
+
}, options?: RequestOptions & {
|
|
34
|
+
client?: Runtime;
|
|
35
|
+
}): Promise<SandboxClass>;
|
|
36
|
+
/** The sandbox named `name` in this account, ready to use: running as it
|
|
37
|
+
* is, woken if it is paused, restarted if it is stopped and persistent, or
|
|
38
|
+
* created with `input` when no sandbox has the name. `sandbox.info.reused`
|
|
39
|
+
* says which. The other fields apply only when it is created. */
|
|
40
|
+
static getOrCreate(name: string, input?: Omit<CreateSandbox, "name" | "getOrCreate"> & {
|
|
41
|
+
wait?: boolean;
|
|
42
|
+
keepAlive?: boolean | KeepAliveOptions;
|
|
26
43
|
}, options?: RequestOptions & {
|
|
27
44
|
client?: Runtime;
|
|
28
45
|
}): Promise<SandboxClass>;
|
package/dist/index.js
CHANGED
|
@@ -2,17 +2,32 @@ import { defaultClient } from "./client.js";
|
|
|
2
2
|
import { Sandbox as SandboxClass } from "./sandbox.js";
|
|
3
3
|
export { Runtime, Sandboxes, FeedbackApi, SupportApi, } from "./client.js";
|
|
4
4
|
export { Files, Process, Processes, Terminal } from "./sandbox.js";
|
|
5
|
+
export { Tunnel, TunnelStream } from "./tunnel.js";
|
|
5
6
|
export { Page } from "./page.js";
|
|
7
|
+
export { verifyWebhook, WebhookVerificationError, } from "./products/observability.js";
|
|
6
8
|
export { Snapshots } from "./snapshots.js";
|
|
7
9
|
export { Transport, VERSION, apiOrigin } from "./transport.js";
|
|
8
10
|
export * from "./errors.js";
|
|
9
11
|
/** A sandbox. `Sandbox.create()` uses RUNTIME_API_KEY; `runtime.sandboxes`
|
|
10
12
|
* does the same with an explicit client. */
|
|
11
13
|
export class Sandbox extends SandboxClass {
|
|
12
|
-
/** Creates a sandbox (every field optional) and waits until it is running.
|
|
14
|
+
/** Creates a sandbox (every field optional) and waits until it is running.
|
|
15
|
+
* `keepAlive: true` (or its options) extends its lease in the background
|
|
16
|
+
* until you stop it; see `sandbox.keepAlive()`. */
|
|
13
17
|
static async create(input = {}, options = {}) {
|
|
14
18
|
const { client, ...rest } = options;
|
|
15
|
-
|
|
19
|
+
const { keepAlive, ...body } = input;
|
|
20
|
+
const sandbox = await (client ?? defaultClient()).sandboxes.create(body, rest);
|
|
21
|
+
if (keepAlive)
|
|
22
|
+
sandbox.keepAlive(keepAlive === true ? {} : keepAlive);
|
|
23
|
+
return sandbox;
|
|
24
|
+
}
|
|
25
|
+
/** The sandbox named `name` in this account, ready to use: running as it
|
|
26
|
+
* is, woken if it is paused, restarted if it is stopped and persistent, or
|
|
27
|
+
* created with `input` when no sandbox has the name. `sandbox.info.reused`
|
|
28
|
+
* says which. The other fields apply only when it is created. */
|
|
29
|
+
static async getOrCreate(name, input = {}, options = {}) {
|
|
30
|
+
return Sandbox.create({ ...input, name, getOrCreate: true }, options);
|
|
16
31
|
}
|
|
17
32
|
/** Reconnects to an existing sandbox by id. */
|
|
18
33
|
static async connect(id, options = {}) {
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type KeyAccess = "full" | "read";
|
|
2
|
+
export type KeyRequest = {
|
|
3
|
+
/** The key's name, and its agent's. */
|
|
4
|
+
name: string;
|
|
5
|
+
access: KeyAccess;
|
|
6
|
+
/** The most its agent may spend in any 24 hours, or null for none. */
|
|
7
|
+
dailyLimitMicros: number | null;
|
|
8
|
+
};
|
|
9
|
+
export type CreatedKey = KeyRequest & {
|
|
10
|
+
key: string;
|
|
11
|
+
keyId: string;
|
|
12
|
+
agentId: string;
|
|
13
|
+
orgId: string;
|
|
14
|
+
/** Always until revoked, as on the keys page. */
|
|
15
|
+
expires: null;
|
|
16
|
+
};
|
|
17
|
+
export type KeyRequestOptions = {
|
|
18
|
+
fetch?: typeof fetch;
|
|
19
|
+
sleep?: (ms: number) => Promise<void>;
|
|
20
|
+
/** Where the link, the code and progress go: standard error, never stdout. */
|
|
21
|
+
notify?: (message: string) => void;
|
|
22
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
23
|
+
/** Whether a person is at this terminal, so a browser is worth opening. */
|
|
24
|
+
interactive?: boolean;
|
|
25
|
+
/** What this computer calls itself, shown to the owner beside the code. */
|
|
26
|
+
machine?: string;
|
|
27
|
+
/** Runs `cancel` if the command is interrupted before the key arrives;
|
|
28
|
+
* returns a function that stops listening. */
|
|
29
|
+
onInterrupt?: (cancel: () => Promise<void>) => () => void;
|
|
30
|
+
};
|
|
31
|
+
/** Whole microdollars from a daily limit typed in dollars ("25", "7.5",
|
|
32
|
+
* "$1,000.10"), read as digits rather than through a float, from $0.01 to
|
|
33
|
+
* $1,000,000: the keys page's own rule (apps/cloud-web/src/app/account/keys/limit.ts). */
|
|
34
|
+
export declare function dailyLimitMicros(typed: string): number;
|
|
35
|
+
/**
|
|
36
|
+
* Asks for a key, waits for an owner to approve it in the browser, and hands
|
|
37
|
+
* it back with `confirm`, which tells the website it arrived. Call `confirm`
|
|
38
|
+
* after the key is shown: it clears the sealed copy the website held for
|
|
39
|
+
* delivery and turns the owner's page to "Key created".
|
|
40
|
+
*/
|
|
41
|
+
export declare function createKey(request: KeyRequest, env: NodeJS.ProcessEnv, options?: KeyRequestOptions): Promise<CreatedKey & {
|
|
42
|
+
confirm: () => Promise<void>;
|
|
43
|
+
}>;
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { generateKeyPair, privateDecrypt } from "node:crypto";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { connectionOrigins } from "./credentials.js";
|
|
6
|
+
import { RuntimeError } from "./errors.js";
|
|
7
|
+
import { named } from "./login.js";
|
|
8
|
+
import { describeRoute, envFetch } from "./proxy.js";
|
|
9
|
+
const KEY = /^rtcloud_([a-f0-9-]{36})_[A-Za-z0-9_-]{43}$/;
|
|
10
|
+
/** How often to ask whether the owner has answered. The website answers
|
|
11
|
+
* "pending" to anything faster than every 4.5 seconds. */
|
|
12
|
+
const POLL_MS = 5000;
|
|
13
|
+
/** Whole microdollars from a daily limit typed in dollars ("25", "7.5",
|
|
14
|
+
* "$1,000.10"), read as digits rather than through a float, from $0.01 to
|
|
15
|
+
* $1,000,000: the keys page's own rule (apps/cloud-web/src/app/account/keys/limit.ts). */
|
|
16
|
+
export function dailyLimitMicros(typed) {
|
|
17
|
+
const text = typed.trim().replace(/^\$/, "").replace(/,/g, "");
|
|
18
|
+
const parts = text.match(/^(\d{1,7})(?:\.(\d{1,2}))?$/);
|
|
19
|
+
const cents = parts ? Number(parts[1]) * 100 + Number((parts[2] ?? "").padEnd(2, "0")) : 0;
|
|
20
|
+
if (cents < 1 || cents > 100_000_000)
|
|
21
|
+
throw new RuntimeError({
|
|
22
|
+
code: "usage",
|
|
23
|
+
status: 0,
|
|
24
|
+
message: "--daily-limit is an amount in US dollars from 0.01 to 1000000, such as 25.",
|
|
25
|
+
});
|
|
26
|
+
return cents * 10_000;
|
|
27
|
+
}
|
|
28
|
+
function failure(code, message, status = 0, hint) {
|
|
29
|
+
return new RuntimeError({ code, status, message, ...(hint ? { hint } : {}) });
|
|
30
|
+
}
|
|
31
|
+
async function browser(url) {
|
|
32
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
33
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
34
|
+
await promisify(execFile)(command, args, { timeout: 10_000 });
|
|
35
|
+
}
|
|
36
|
+
function interrupted(cancel) {
|
|
37
|
+
const handler = () => {
|
|
38
|
+
process.stderr.write("\nCanceling the key request.\n");
|
|
39
|
+
void cancel()
|
|
40
|
+
.catch(() => undefined)
|
|
41
|
+
.finally(() => process.exit(130));
|
|
42
|
+
};
|
|
43
|
+
process.once("SIGINT", handler);
|
|
44
|
+
return () => void process.off("SIGINT", handler);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Asks for a key, waits for an owner to approve it in the browser, and hands
|
|
48
|
+
* it back with `confirm`, which tells the website it arrived. Call `confirm`
|
|
49
|
+
* after the key is shown: it clears the sealed copy the website held for
|
|
50
|
+
* delivery and turns the owner's page to "Key created".
|
|
51
|
+
*/
|
|
52
|
+
export async function createKey(request, env, options = {}) {
|
|
53
|
+
const name = request.name.trim();
|
|
54
|
+
if (!name ||
|
|
55
|
+
request.name.length > 80 ||
|
|
56
|
+
[...request.name].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
57
|
+
throw failure("usage", "Give the key a name of up to 80 characters.");
|
|
58
|
+
if (request.access === "read" && request.dailyLimitMicros !== null)
|
|
59
|
+
throw failure("usage", "A read-only key cannot spend, so it takes no --daily-limit.");
|
|
60
|
+
const origins = connectionOrigins(env);
|
|
61
|
+
const fetcher = options.fetch ?? envFetch;
|
|
62
|
+
const notify = options.notify ?? ((message) => process.stderr.write(`${message}\n`));
|
|
63
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
64
|
+
// A key asked for here is approved on the page this address serves: only
|
|
65
|
+
// Runtime's own website, or one on this computer for testing.
|
|
66
|
+
const local = (origin) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname);
|
|
67
|
+
if (!(origins.auth === "https://withruntime.com" || local(origins.auth)))
|
|
68
|
+
throw failure("usage", "Keys are approved at https://withruntime.com, or at a local test address. Check RUNTIME_AUTH_URL.");
|
|
69
|
+
async function post(path, body, key) {
|
|
70
|
+
const address = `${origins.auth}/api/connect/${path}`;
|
|
71
|
+
let response;
|
|
72
|
+
try {
|
|
73
|
+
response = await fetcher(address, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
redirect: "error",
|
|
76
|
+
signal: AbortSignal.timeout(20_000),
|
|
77
|
+
headers: {
|
|
78
|
+
"content-type": "application/json",
|
|
79
|
+
...(key ? { authorization: `Bearer ${key}` } : {}),
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(body),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (cause) {
|
|
85
|
+
const route = options.fetch ? { via: "" } : describeRoute(address, cause, env);
|
|
86
|
+
throw failure("network_error", `Runtime could not be reached at ${origins.auth}${route.via}.`, 0, route.hint);
|
|
87
|
+
}
|
|
88
|
+
const result = (await response.json().catch(() => ({})));
|
|
89
|
+
if (!response.ok)
|
|
90
|
+
throw failure(response.status === 429 ? "rate_limited" : "key_request_failed", typeof result.error === "string" ? result.error : "Runtime could not take this request.", response.status);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
const pair = await promisify(generateKeyPair)("rsa", {
|
|
94
|
+
modulusLength: 2048,
|
|
95
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
96
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
97
|
+
});
|
|
98
|
+
const started = await post("start", {
|
|
99
|
+
purpose: "key",
|
|
100
|
+
agentName: name,
|
|
101
|
+
access: request.access,
|
|
102
|
+
dailyLimitMicros: request.dailyLimitMicros,
|
|
103
|
+
machine: options.machine ?? hostname(),
|
|
104
|
+
publicKey: pair.publicKey,
|
|
105
|
+
});
|
|
106
|
+
if (typeof started.verificationUri !== "string" ||
|
|
107
|
+
typeof started.deviceCode !== "string" ||
|
|
108
|
+
!/^[A-Za-z0-9_-]{43}$/.test(started.deviceCode) ||
|
|
109
|
+
typeof started.userCode !== "string" ||
|
|
110
|
+
!/^[A-F0-9]{4}(-[A-F0-9]{4}){2}$/.test(started.userCode) ||
|
|
111
|
+
typeof started.expiresAt !== "number" ||
|
|
112
|
+
!Number.isFinite(started.expiresAt))
|
|
113
|
+
throw failure("key_request_failed", "Runtime returned an invalid key request.");
|
|
114
|
+
const link = new URL(started.verificationUri);
|
|
115
|
+
if (link.origin !== origins.auth ||
|
|
116
|
+
link.pathname !== "/connect" ||
|
|
117
|
+
link.username ||
|
|
118
|
+
link.password ||
|
|
119
|
+
link.hash ||
|
|
120
|
+
[...link.searchParams.keys()].some((key) => key !== "code") ||
|
|
121
|
+
link.searchParams.get("code") !== started.userCode)
|
|
122
|
+
throw failure("key_request_failed", "Runtime returned an unexpected approval address.");
|
|
123
|
+
const deviceCode = started.deviceCode;
|
|
124
|
+
const expiresAt = Math.min(started.expiresAt, Date.now() + 15 * 60_000);
|
|
125
|
+
notify(`Approve this key in your browser: ${link.href}\nCheck that the page shows code ${started.userCode}. An account owner chooses Create key.\nWaiting for the approval, up to 15 minutes. Stopping this cancels the request.`);
|
|
126
|
+
if (options.interactive ?? Boolean(process.stderr.isTTY))
|
|
127
|
+
await (options.openBrowser ?? browser)(link.href).catch(() => notify("Open the link in a browser where an account owner is signed in."));
|
|
128
|
+
const stop = (options.onInterrupt ?? interrupted)(async () => {
|
|
129
|
+
await post("token", { deviceCode, action: "cancel" });
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
// One poll past the expiry, because an approval given in its last seconds
|
|
133
|
+
// is still delivered.
|
|
134
|
+
for (;;) {
|
|
135
|
+
await sleep(POLL_MS);
|
|
136
|
+
const last = Date.now() >= expiresAt;
|
|
137
|
+
let result;
|
|
138
|
+
try {
|
|
139
|
+
result = await post("token", { deviceCode });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// A dropped connection or a busy website is not an answer: ask again.
|
|
143
|
+
if (!last &&
|
|
144
|
+
error instanceof RuntimeError &&
|
|
145
|
+
(error.code === "network_error" || error.status >= 500))
|
|
146
|
+
continue;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
if (result.status === "pending") {
|
|
150
|
+
if (last)
|
|
151
|
+
break;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (result.status === "denied")
|
|
155
|
+
throw failure("key_request_declined", "The key request was declined. No key was created.");
|
|
156
|
+
if (result.status === "canceled")
|
|
157
|
+
throw failure("key_request_canceled", "The key request was canceled. No key was issued.");
|
|
158
|
+
if (result.status === "expired")
|
|
159
|
+
break;
|
|
160
|
+
if (result.status !== "connected" ||
|
|
161
|
+
typeof result.encryptedKey !== "string" ||
|
|
162
|
+
typeof result.agentId !== "string" ||
|
|
163
|
+
typeof result.orgId !== "string")
|
|
164
|
+
throw failure("key_request_failed", "Runtime returned an invalid key.");
|
|
165
|
+
let key;
|
|
166
|
+
try {
|
|
167
|
+
key = privateDecrypt({ key: pair.privateKey, oaepHash: "sha256" }, Buffer.from(result.encryptedKey, "base64")).toString("utf8");
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw failure("key_request_failed", "Could not open the key Runtime returned. Start again.");
|
|
171
|
+
}
|
|
172
|
+
const match = KEY.exec(key);
|
|
173
|
+
if (!match)
|
|
174
|
+
throw failure("key_request_failed", "Runtime returned an invalid key.");
|
|
175
|
+
// From here the key is this command's: an interruption must not revoke it.
|
|
176
|
+
stop();
|
|
177
|
+
return {
|
|
178
|
+
name,
|
|
179
|
+
access: request.access,
|
|
180
|
+
dailyLimitMicros: request.dailyLimitMicros,
|
|
181
|
+
key,
|
|
182
|
+
keyId: match[1],
|
|
183
|
+
agentId: result.agentId,
|
|
184
|
+
orgId: result.orgId,
|
|
185
|
+
expires: null,
|
|
186
|
+
confirm: async () => {
|
|
187
|
+
await post("confirm", {}, key).catch(() => notify("The key works, but Runtime did not hear that it arrived. The approval page may still say Approved."));
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
stop();
|
|
194
|
+
}
|
|
195
|
+
throw failure("key_request_expired", named("The key request expired before it was approved. Run `runtime keys create` again."));
|
|
196
|
+
}
|
package/dist/login.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { generateKeyPair, privateDecrypt } from "node:crypto";
|
|
3
|
+
import { hostname } from "node:os";
|
|
3
4
|
import { promisify } from "node:util";
|
|
4
5
|
import { Runtime } from "./index.js";
|
|
5
6
|
import { connectionOrigins, connectionStore } from "./credentials.js";
|
|
7
|
+
import { describeRoute, envFetch } from "./proxy.js";
|
|
6
8
|
class ConnectionError extends Error {
|
|
7
9
|
status;
|
|
8
10
|
constructor(status, message) {
|
|
@@ -71,11 +73,12 @@ async function browser(url) {
|
|
|
71
73
|
function connection(env, options) {
|
|
72
74
|
const origins = connectionOrigins(env);
|
|
73
75
|
const store = connectionStore(env);
|
|
74
|
-
const fetcher = options.fetch ??
|
|
76
|
+
const fetcher = options.fetch ?? envFetch;
|
|
75
77
|
async function request(path, body, key) {
|
|
76
78
|
let response;
|
|
79
|
+
const address = `${origins.auth}/api/connect/${path}`;
|
|
77
80
|
try {
|
|
78
|
-
response = await fetcher(
|
|
81
|
+
response = await fetcher(address, {
|
|
79
82
|
method: "POST",
|
|
80
83
|
redirect: "error",
|
|
81
84
|
signal: AbortSignal.timeout(20_000),
|
|
@@ -86,8 +89,15 @@ function connection(env, options) {
|
|
|
86
89
|
body: JSON.stringify(body ?? {}),
|
|
87
90
|
});
|
|
88
91
|
}
|
|
89
|
-
catch {
|
|
90
|
-
|
|
92
|
+
catch (cause) {
|
|
93
|
+
const route = options.fetch ? { via: "" } : describeRoute(address, cause, env);
|
|
94
|
+
throw new ConnectionError(503, route.hint
|
|
95
|
+
? `Runtime could not be reached at ${origins.auth}${route.via}. ${route.hint}`
|
|
96
|
+
: "Runtime could not be reached. Retry the connection.");
|
|
97
|
+
}
|
|
98
|
+
if (response.status === 407 && !options.fetch) {
|
|
99
|
+
const route = describeRoute(address, 407, env);
|
|
100
|
+
throw new ConnectionError(503, `Runtime could not be reached at ${origins.auth}${route.via}. ${route.hint ?? ""}`.trim());
|
|
91
101
|
}
|
|
92
102
|
if (!response.ok)
|
|
93
103
|
throw new ConnectionError(response.status, response.status === 401
|
|
@@ -154,9 +164,14 @@ export async function authenticationCommand(args, env, options = {}) {
|
|
|
154
164
|
let noBrowser = false;
|
|
155
165
|
let withKey = false;
|
|
156
166
|
let wait;
|
|
167
|
+
// `runtime account add`: connect another account even though this machine
|
|
168
|
+
// is connected already; the one in use is kept among the saved accounts.
|
|
169
|
+
let another = false;
|
|
157
170
|
for (let i = 0; i < rest.length; i++) {
|
|
158
171
|
if (rest[i] === "--no-browser")
|
|
159
172
|
noBrowser = true;
|
|
173
|
+
else if (rest[i] === "--another-account")
|
|
174
|
+
another = true;
|
|
160
175
|
else if (rest[i] === "--with-key")
|
|
161
176
|
withKey = true;
|
|
162
177
|
else if (rest[i] === "--wait")
|
|
@@ -203,7 +218,7 @@ export async function authenticationCommand(args, env, options = {}) {
|
|
|
203
218
|
}).usage();
|
|
204
219
|
return { connected: true, source: "environment" };
|
|
205
220
|
}
|
|
206
|
-
const saved = await store.read();
|
|
221
|
+
const saved = another ? null : await store.read();
|
|
207
222
|
if (saved) {
|
|
208
223
|
try {
|
|
209
224
|
await request("confirm", undefined, saved.key);
|
|
@@ -268,7 +283,13 @@ export async function beginDeviceLogin(env, agentName, options = {}) {
|
|
|
268
283
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
269
284
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
270
285
|
});
|
|
271
|
-
|
|
286
|
+
// The computer's own name goes to the approval page beside the code, so an
|
|
287
|
+
// owner can tell this request from a link somebody else started.
|
|
288
|
+
const started = await request("start", {
|
|
289
|
+
agentName,
|
|
290
|
+
publicKey: pair.publicKey,
|
|
291
|
+
machine: hostname(),
|
|
292
|
+
});
|
|
272
293
|
if (typeof started.verificationUri !== "string" ||
|
|
273
294
|
typeof started.deviceCode !== "string" ||
|
|
274
295
|
!/^[A-Za-z0-9_-]{43}$/.test(started.deviceCode) ||
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
|
|
2
2
|
* Newline-delimited JSON-RPC in, the same out; every message is forwarded as
|
|
3
3
|
* it is, so the server's tools, schemas and `instructions` reach the client
|
|
4
|
-
* unchanged, and nothing here keeps a second catalogue.
|
|
5
|
-
*
|
|
4
|
+
* unchanged, and nothing here keeps a second catalogue. Starting it costs a
|
|
5
|
+
* process and one TLS connection (undici loads only behind a proxy).
|
|
6
6
|
*
|
|
7
7
|
* With no key on this machine it does not fail: it serves one tool,
|
|
8
8
|
* runtime_connect, that walks the agent and its person through the browser
|
package/dist/mcp.js
CHANGED
|
@@ -2,6 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import { resolveCredential } from "./credentials.js";
|
|
3
3
|
import { RuntimeError } from "./errors.js";
|
|
4
4
|
import { beginDeviceLogin } from "./login.js";
|
|
5
|
+
import { envFetch } from "./proxy.js";
|
|
5
6
|
import { apiOrigin, VERSION } from "./transport.js";
|
|
6
7
|
const CONNECT_INSTRUCTIONS = "Runtime is not connected on this machine yet. Call runtime_connect: it gives you a link and a code for the person you work for to approve in their browser (no key to copy). Once they have, call runtime_connect again; Runtime's tools then appear (sandboxes, commands, files and the rest).";
|
|
7
8
|
const connectTool = {
|
|
@@ -13,8 +14,8 @@ const connectTool = {
|
|
|
13
14
|
/** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
|
|
14
15
|
* Newline-delimited JSON-RPC in, the same out; every message is forwarded as
|
|
15
16
|
* it is, so the server's tools, schemas and `instructions` reach the client
|
|
16
|
-
* unchanged, and nothing here keeps a second catalogue.
|
|
17
|
-
*
|
|
17
|
+
* unchanged, and nothing here keeps a second catalogue. Starting it costs a
|
|
18
|
+
* process and one TLS connection (undici loads only behind a proxy).
|
|
18
19
|
*
|
|
19
20
|
* With no key on this machine it does not fail: it serves one tool,
|
|
20
21
|
* runtime_connect, that walks the agent and its person through the browser
|
|
@@ -22,7 +23,7 @@ const connectTool = {
|
|
|
22
23
|
* So `claude mcp add runtime -- npx -y withruntime mcp` is the whole
|
|
23
24
|
* setup. */
|
|
24
25
|
export async function serveMcp(env = process.env, options = {}) {
|
|
25
|
-
const fetcher = options.fetch ??
|
|
26
|
+
const fetcher = options.fetch ?? envFetch;
|
|
26
27
|
let key;
|
|
27
28
|
try {
|
|
28
29
|
key = await resolveCredential(env);
|