withruntime 0.2.2 → 0.3.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/README.md +68 -4
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +1262 -0
- package/dist/client.d.ts +118 -0
- package/dist/client.js +160 -0
- package/dist/credentials.d.ts +44 -0
- package/dist/credentials.js +180 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.js +106 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +25 -0
- package/dist/login.d.ts +119 -0
- package/dist/login.js +367 -0
- package/dist/mcp.d.ts +15 -0
- package/dist/mcp.js +221 -0
- package/dist/page.d.ts +13 -0
- package/dist/page.js +39 -0
- package/dist/products/desktop.d.ts +103 -0
- package/dist/products/desktop.js +38 -0
- package/dist/products/images.d.ts +121 -0
- package/dist/products/images.js +64 -0
- package/dist/products/index.d.ts +29 -0
- package/dist/products/index.js +36 -0
- package/dist/products/interpreter.d.ts +88 -0
- package/dist/products/interpreter.js +81 -0
- package/dist/products/network.d.ts +47 -0
- package/dist/products/network.js +19 -0
- package/dist/products/previews.d.ts +51 -0
- package/dist/products/previews.js +30 -0
- package/dist/products/referrals.d.ts +36 -0
- package/dist/products/referrals.js +8 -0
- package/dist/products/volumes.d.ts +47 -0
- package/dist/products/volumes.js +27 -0
- package/dist/sandbox.d.ts +192 -0
- package/dist/sandbox.js +628 -0
- package/dist/snapshots.d.ts +50 -0
- package/dist/snapshots.js +55 -0
- package/dist/tar.d.ts +3 -0
- package/dist/tar.js +116 -0
- package/dist/transport.d.ts +65 -0
- package/dist/transport.js +261 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.js +1 -0
- package/package.json +53 -8
- package/cli.js +0 -7
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Runtime Cloud: one client for every product. Sandboxes first.
|
|
2
|
+
*
|
|
3
|
+
* import { Sandbox } from "withruntime";
|
|
4
|
+
* await using sbx = await Sandbox.create();
|
|
5
|
+
* console.log((await sbx.exec("echo hello")).stdout);
|
|
6
|
+
*/
|
|
7
|
+
import type { Runtime } from "./client.js";
|
|
8
|
+
import { type RuntimeOptions } from "./client.js";
|
|
9
|
+
import { Sandbox as SandboxClass } from "./sandbox.js";
|
|
10
|
+
import type { CreateSandbox } from "./types.js";
|
|
11
|
+
import type { RequestOptions } from "./transport.js";
|
|
12
|
+
export { Runtime, Sandboxes, FeedbackApi, SupportApi, type RuntimeOptions, type SupportReply, } from "./client.js";
|
|
13
|
+
export { Files, Process, Processes, Terminal } from "./sandbox.js";
|
|
14
|
+
export { Page } from "./page.js";
|
|
15
|
+
export { Snapshots, type Snapshot, type SnapshotOptions } from "./snapshots.js";
|
|
16
|
+
export { Transport, VERSION, apiOrigin, type RequestOptions, type Call } from "./transport.js";
|
|
17
|
+
export * from "./errors.js";
|
|
18
|
+
export type * from "./types.js";
|
|
19
|
+
/** A sandbox. `Sandbox.create()` uses RUNTIME_API_KEY; `runtime.sandboxes`
|
|
20
|
+
* does the same with an explicit client. */
|
|
21
|
+
export declare class Sandbox extends SandboxClass {
|
|
22
|
+
/** Creates a sandbox (every field optional) and waits until it is running. */
|
|
23
|
+
static create(input?: CreateSandbox & {
|
|
24
|
+
wait?: boolean;
|
|
25
|
+
}, options?: RequestOptions & {
|
|
26
|
+
client?: Runtime;
|
|
27
|
+
}): Promise<SandboxClass>;
|
|
28
|
+
/** Reconnects to an existing sandbox by id. */
|
|
29
|
+
static connect(id: string, options?: RequestOptions & {
|
|
30
|
+
client?: Runtime;
|
|
31
|
+
}): Promise<SandboxClass>;
|
|
32
|
+
static list(filter?: Parameters<Runtime["sandboxes"]["list"]>[0], options?: {
|
|
33
|
+
client?: Runtime;
|
|
34
|
+
}): Promise<import("./page.js").Page<SandboxClass>>;
|
|
35
|
+
}
|
|
36
|
+
export type { RuntimeOptions as ClientOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defaultClient } from "./client.js";
|
|
2
|
+
import { Sandbox as SandboxClass } from "./sandbox.js";
|
|
3
|
+
export { Runtime, Sandboxes, FeedbackApi, SupportApi, } from "./client.js";
|
|
4
|
+
export { Files, Process, Processes, Terminal } from "./sandbox.js";
|
|
5
|
+
export { Page } from "./page.js";
|
|
6
|
+
export { Snapshots } from "./snapshots.js";
|
|
7
|
+
export { Transport, VERSION, apiOrigin } from "./transport.js";
|
|
8
|
+
export * from "./errors.js";
|
|
9
|
+
/** A sandbox. `Sandbox.create()` uses RUNTIME_API_KEY; `runtime.sandboxes`
|
|
10
|
+
* does the same with an explicit client. */
|
|
11
|
+
export class Sandbox extends SandboxClass {
|
|
12
|
+
/** Creates a sandbox (every field optional) and waits until it is running. */
|
|
13
|
+
static async create(input = {}, options = {}) {
|
|
14
|
+
const { client, ...rest } = options;
|
|
15
|
+
return (client ?? defaultClient()).sandboxes.create(input, rest);
|
|
16
|
+
}
|
|
17
|
+
/** Reconnects to an existing sandbox by id. */
|
|
18
|
+
static async connect(id, options = {}) {
|
|
19
|
+
const { client, ...rest } = options;
|
|
20
|
+
return (client ?? defaultClient()).sandboxes.get(id, rest);
|
|
21
|
+
}
|
|
22
|
+
static list(filter = {}, options = {}) {
|
|
23
|
+
return (options.client ?? defaultClient()).sandboxes.list(filter);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** How this CLI was started, so what it tells you to type works: through npx
|
|
2
|
+
* the command is `npx withruntime`, installed it is `runtime`. */
|
|
3
|
+
export declare const me: string;
|
|
4
|
+
/** Text that names a command, with the command spelled the way it was run. */
|
|
5
|
+
export declare function named(text: string, name?: string): string;
|
|
6
|
+
type LoginOptions = {
|
|
7
|
+
readKey?: () => Promise<string>;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
sleep?: (ms: number) => Promise<void>;
|
|
10
|
+
notify?: (message: string) => void;
|
|
11
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
12
|
+
/** A person at a terminal, who can wait out the approval; an agent's tool
|
|
13
|
+
* call is not. Defaults to whether standard input and error are terminals. */
|
|
14
|
+
interactive?: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** How long a login run by an agent waits for the approval before it hands
|
|
17
|
+
* back the link: inside the common tool timeouts, long enough for a person
|
|
18
|
+
* at the same machine to press Connect agent. */
|
|
19
|
+
export declare const AGENT_WAIT_MS = 50000;
|
|
20
|
+
export declare function authenticationCommand(args: string[], env: NodeJS.ProcessEnv, options?: LoginOptions): Promise<{
|
|
21
|
+
connected: boolean;
|
|
22
|
+
source: string;
|
|
23
|
+
reason?: undefined;
|
|
24
|
+
agentName?: undefined;
|
|
25
|
+
orgId?: undefined;
|
|
26
|
+
disconnected?: undefined;
|
|
27
|
+
pending?: undefined;
|
|
28
|
+
url?: undefined;
|
|
29
|
+
userCode?: undefined;
|
|
30
|
+
expiresAt?: undefined;
|
|
31
|
+
} | {
|
|
32
|
+
connected: boolean;
|
|
33
|
+
source?: undefined;
|
|
34
|
+
reason?: undefined;
|
|
35
|
+
agentName?: undefined;
|
|
36
|
+
orgId?: undefined;
|
|
37
|
+
disconnected?: undefined;
|
|
38
|
+
pending?: undefined;
|
|
39
|
+
url?: undefined;
|
|
40
|
+
userCode?: undefined;
|
|
41
|
+
expiresAt?: undefined;
|
|
42
|
+
} | {
|
|
43
|
+
connected: boolean;
|
|
44
|
+
reason: string;
|
|
45
|
+
source?: undefined;
|
|
46
|
+
agentName?: undefined;
|
|
47
|
+
orgId?: undefined;
|
|
48
|
+
disconnected?: undefined;
|
|
49
|
+
pending?: undefined;
|
|
50
|
+
url?: undefined;
|
|
51
|
+
userCode?: undefined;
|
|
52
|
+
expiresAt?: undefined;
|
|
53
|
+
} | {
|
|
54
|
+
connected: boolean;
|
|
55
|
+
agentName: string;
|
|
56
|
+
orgId: string;
|
|
57
|
+
source?: undefined;
|
|
58
|
+
reason?: undefined;
|
|
59
|
+
disconnected?: undefined;
|
|
60
|
+
pending?: undefined;
|
|
61
|
+
url?: undefined;
|
|
62
|
+
userCode?: undefined;
|
|
63
|
+
expiresAt?: undefined;
|
|
64
|
+
} | {
|
|
65
|
+
disconnected: boolean;
|
|
66
|
+
connected?: undefined;
|
|
67
|
+
source?: undefined;
|
|
68
|
+
reason?: undefined;
|
|
69
|
+
agentName?: undefined;
|
|
70
|
+
orgId?: undefined;
|
|
71
|
+
pending?: undefined;
|
|
72
|
+
url?: undefined;
|
|
73
|
+
userCode?: undefined;
|
|
74
|
+
expiresAt?: undefined;
|
|
75
|
+
} | {
|
|
76
|
+
connected: boolean;
|
|
77
|
+
agentName: string;
|
|
78
|
+
orgId: string;
|
|
79
|
+
source: string;
|
|
80
|
+
reason?: undefined;
|
|
81
|
+
disconnected?: undefined;
|
|
82
|
+
pending?: undefined;
|
|
83
|
+
url?: undefined;
|
|
84
|
+
userCode?: undefined;
|
|
85
|
+
expiresAt?: undefined;
|
|
86
|
+
} | {
|
|
87
|
+
connected: false;
|
|
88
|
+
pending: true;
|
|
89
|
+
url: string;
|
|
90
|
+
userCode: string;
|
|
91
|
+
expiresAt: string;
|
|
92
|
+
source?: undefined;
|
|
93
|
+
reason?: undefined;
|
|
94
|
+
agentName?: undefined;
|
|
95
|
+
orgId?: undefined;
|
|
96
|
+
disconnected?: undefined;
|
|
97
|
+
}>;
|
|
98
|
+
export type PendingLogin = {
|
|
99
|
+
/** Open this in a browser, where the account owner approves the agent. */
|
|
100
|
+
url: string;
|
|
101
|
+
/** The code the browser shows, to check it is this request. */
|
|
102
|
+
userCode: string;
|
|
103
|
+
expiresAt: number;
|
|
104
|
+
/** An earlier request from this machine, still waiting: its link is already
|
|
105
|
+
* open or shown, so it is not opened again. */
|
|
106
|
+
resumed: boolean;
|
|
107
|
+
/** Asks once whether it was approved: pending, expired, or connected (the
|
|
108
|
+
* key is then saved and confirmed). Throws when it was declined. */
|
|
109
|
+
check(): Promise<"pending" | "expired" | {
|
|
110
|
+
connected: true;
|
|
111
|
+
agentName: string;
|
|
112
|
+
orgId: string;
|
|
113
|
+
}>;
|
|
114
|
+
};
|
|
115
|
+
/** Starts a browser-approved connection and hands back its link, without
|
|
116
|
+
* waiting: `runtime login` waits on it, and the MCP bridge gives the link to
|
|
117
|
+
* the agent, which shows it to the person it works for. */
|
|
118
|
+
export declare function beginDeviceLogin(env: NodeJS.ProcessEnv, agentName: string, options?: LoginOptions): Promise<PendingLogin>;
|
|
119
|
+
export {};
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { generateKeyPair, privateDecrypt } from "node:crypto";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { Runtime } from "./index.js";
|
|
5
|
+
import { connectionOrigins, connectionStore } from "./credentials.js";
|
|
6
|
+
class ConnectionError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
constructor(status, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** How this CLI was started, so what it tells you to type works: through npx
|
|
14
|
+
* the command is `npx withruntime`, installed it is `runtime`. */
|
|
15
|
+
export const me = /[\\/]_npx[\\/]/.test(process.argv[1] ?? "") ? "npx withruntime" : "runtime";
|
|
16
|
+
/** Text that names a command, with the command spelled the way it was run. */
|
|
17
|
+
export function named(text, name = me) {
|
|
18
|
+
return name === "runtime" ? text : text.replace(/(^|[\s`(])runtime(?= [a-z<[])/g, `$1${name}`);
|
|
19
|
+
}
|
|
20
|
+
/** The connection id recorded for a key pasted with `login --with-key`. */
|
|
21
|
+
const PASTED = "pasted";
|
|
22
|
+
/** A key from standard input: piped, or typed at a prompt that does not echo. */
|
|
23
|
+
async function readKey() {
|
|
24
|
+
const stdin = process.stdin;
|
|
25
|
+
if (!stdin.isTTY) {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const chunk of stdin)
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
30
|
+
}
|
|
31
|
+
process.stderr.write("Paste a key from https://withruntime.com/account/keys (it is not shown): ");
|
|
32
|
+
stdin.setRawMode(true);
|
|
33
|
+
stdin.resume();
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
let typed = "";
|
|
36
|
+
const done = (value) => {
|
|
37
|
+
stdin.off("data", onData);
|
|
38
|
+
stdin.setRawMode(false);
|
|
39
|
+
stdin.pause();
|
|
40
|
+
process.stderr.write("\n");
|
|
41
|
+
if (value instanceof Error)
|
|
42
|
+
reject(value);
|
|
43
|
+
else
|
|
44
|
+
resolve(value);
|
|
45
|
+
};
|
|
46
|
+
const onData = (chunk) => {
|
|
47
|
+
for (const char of chunk.toString("utf8")) {
|
|
48
|
+
if (char === "\r" || char === "\n")
|
|
49
|
+
return done(typed);
|
|
50
|
+
if (char === "\u0003")
|
|
51
|
+
return done(new Error("Cancelled."));
|
|
52
|
+
if (char === "\u007f")
|
|
53
|
+
typed = typed.slice(0, -1);
|
|
54
|
+
else
|
|
55
|
+
typed += char;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
stdin.on("data", onData);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** How long a login run by an agent waits for the approval before it hands
|
|
62
|
+
* back the link: inside the common tool timeouts, long enough for a person
|
|
63
|
+
* at the same machine to press Connect agent. */
|
|
64
|
+
export const AGENT_WAIT_MS = 50_000;
|
|
65
|
+
async function browser(url) {
|
|
66
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
67
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
68
|
+
await promisify(execFile)(command, args, { timeout: 10_000 });
|
|
69
|
+
}
|
|
70
|
+
/** The origins, the saved-connection store and the website's connect API. */
|
|
71
|
+
function connection(env, options) {
|
|
72
|
+
const origins = connectionOrigins(env);
|
|
73
|
+
const store = connectionStore(env);
|
|
74
|
+
const fetcher = options.fetch ?? fetch;
|
|
75
|
+
async function request(path, body, key) {
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await fetcher(`${origins.auth}/api/connect/${path}`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
redirect: "error",
|
|
81
|
+
signal: AbortSignal.timeout(20_000),
|
|
82
|
+
headers: {
|
|
83
|
+
"content-type": "application/json",
|
|
84
|
+
...(key ? { authorization: `Bearer ${key}` } : {}),
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body ?? {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new ConnectionError(503, "Runtime could not be reached. Retry the connection.");
|
|
91
|
+
}
|
|
92
|
+
if (!response.ok)
|
|
93
|
+
throw new ConnectionError(response.status, response.status === 401
|
|
94
|
+
? named("This connection is no longer active. Run `runtime login`.")
|
|
95
|
+
: response.status === 429
|
|
96
|
+
? "Too many connection attempts. Try again later."
|
|
97
|
+
: "Runtime could not complete this connection. Try again.");
|
|
98
|
+
return (await response.json());
|
|
99
|
+
}
|
|
100
|
+
return { origins, store, request, fetcher };
|
|
101
|
+
}
|
|
102
|
+
export async function authenticationCommand(args, env, options = {}) {
|
|
103
|
+
const { origins, store, request, fetcher } = connection(env, options);
|
|
104
|
+
const notify = options.notify ?? ((message) => process.stderr.write(`${message}\n`));
|
|
105
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
106
|
+
const [command, ...rest] = args;
|
|
107
|
+
if (command === "auth" && rest.length === 1 && rest[0] === "status") {
|
|
108
|
+
if (env.RUNTIME_API_KEY) {
|
|
109
|
+
await new Runtime({
|
|
110
|
+
apiKey: env.RUNTIME_API_KEY,
|
|
111
|
+
baseUrl: origins.api,
|
|
112
|
+
fetch: fetcher,
|
|
113
|
+
}).usage();
|
|
114
|
+
return { connected: true, source: "environment" };
|
|
115
|
+
}
|
|
116
|
+
const saved = await store.read();
|
|
117
|
+
if (!saved)
|
|
118
|
+
return { connected: false };
|
|
119
|
+
try {
|
|
120
|
+
await request("confirm", undefined, saved.key);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof ConnectionError && error.status === 401)
|
|
124
|
+
return { connected: false, reason: "revoked" };
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
return { connected: true, agentName: saved.agentName, orgId: saved.orgId };
|
|
128
|
+
}
|
|
129
|
+
if (command === "logout" && rest.length === 0) {
|
|
130
|
+
if (env.RUNTIME_API_KEY)
|
|
131
|
+
throw new Error("This shell uses RUNTIME_API_KEY. Remove that override before disconnecting the saved connection.");
|
|
132
|
+
const saved = await store.read();
|
|
133
|
+
if (!saved)
|
|
134
|
+
return { disconnected: true };
|
|
135
|
+
// A pasted key belongs to the website's key list: forget it here, and
|
|
136
|
+
// revoke it there if it should stop working everywhere.
|
|
137
|
+
if (saved.connectionId === PASTED) {
|
|
138
|
+
await store.remove();
|
|
139
|
+
return { disconnected: true };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await request("disconnect", undefined, saved.key);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (!(error instanceof ConnectionError && error.status === 401))
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
await store.remove();
|
|
149
|
+
return { disconnected: true };
|
|
150
|
+
}
|
|
151
|
+
if (command !== "login")
|
|
152
|
+
throw new Error(named("Use `runtime login`, `runtime whoami` or `runtime logout`."));
|
|
153
|
+
let agentName = "My coding agent";
|
|
154
|
+
let noBrowser = false;
|
|
155
|
+
let withKey = false;
|
|
156
|
+
let wait;
|
|
157
|
+
for (let i = 0; i < rest.length; i++) {
|
|
158
|
+
if (rest[i] === "--no-browser")
|
|
159
|
+
noBrowser = true;
|
|
160
|
+
else if (rest[i] === "--with-key")
|
|
161
|
+
withKey = true;
|
|
162
|
+
else if (rest[i] === "--wait")
|
|
163
|
+
wait = true;
|
|
164
|
+
else if (rest[i] === "--no-wait")
|
|
165
|
+
wait = false;
|
|
166
|
+
else if (rest[i] === "--agent-name" && rest[i + 1])
|
|
167
|
+
agentName = rest[++i];
|
|
168
|
+
else
|
|
169
|
+
throw new Error(named("Use `runtime login [--agent-name <name>] [--no-browser] [--wait]`, or `runtime login --with-key` to paste a key."));
|
|
170
|
+
}
|
|
171
|
+
if (!agentName.trim() ||
|
|
172
|
+
agentName.length > 80 ||
|
|
173
|
+
[...agentName].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
174
|
+
throw new Error("Give the agent a name of up to 80 characters.");
|
|
175
|
+
// Never obtain a production key and then direct it at an unrelated API.
|
|
176
|
+
const local = (origin) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname);
|
|
177
|
+
if (!((origins.auth === "https://withruntime.com" && origins.api === "https://api.withruntime.com") ||
|
|
178
|
+
(local(origins.auth) && local(origins.api))))
|
|
179
|
+
throw new Error("Browser login supports Runtime's production endpoints or local test endpoints. Check RUNTIME_AUTH_URL and RUNTIME_API_URL.");
|
|
180
|
+
if (withKey) {
|
|
181
|
+
// The fallback to a browser approval: a key from the website, read from
|
|
182
|
+
// standard input so it never sits in a command line or shell history.
|
|
183
|
+
const key = (await (options.readKey ?? readKey)()).trim();
|
|
184
|
+
if (!/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(key))
|
|
185
|
+
throw new Error("That is not a Runtime key: keys start with rtcloud_. Create one at https://withruntime.com/account/keys.");
|
|
186
|
+
const account = await new Runtime({ apiKey: key, baseUrl: origins.api, fetch: fetcher }).me();
|
|
187
|
+
await store.save({
|
|
188
|
+
version: 1,
|
|
189
|
+
apiOrigin: origins.api,
|
|
190
|
+
authOrigin: origins.auth,
|
|
191
|
+
key,
|
|
192
|
+
connectionId: PASTED,
|
|
193
|
+
orgId: account.orgId,
|
|
194
|
+
agentName: "pasted key",
|
|
195
|
+
});
|
|
196
|
+
return { connected: true, agentName: "pasted key", orgId: account.orgId, source: "key" };
|
|
197
|
+
}
|
|
198
|
+
if (env.RUNTIME_API_KEY) {
|
|
199
|
+
await new Runtime({
|
|
200
|
+
apiKey: env.RUNTIME_API_KEY,
|
|
201
|
+
baseUrl: origins.api,
|
|
202
|
+
fetch: fetcher,
|
|
203
|
+
}).usage();
|
|
204
|
+
return { connected: true, source: "environment" };
|
|
205
|
+
}
|
|
206
|
+
const saved = await store.read();
|
|
207
|
+
if (saved) {
|
|
208
|
+
try {
|
|
209
|
+
await request("confirm", undefined, saved.key);
|
|
210
|
+
return { connected: true, agentName: saved.agentName, orgId: saved.orgId };
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (!(error instanceof ConnectionError && error.status === 401))
|
|
214
|
+
throw error;
|
|
215
|
+
await store.remove();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const pending = await beginDeviceLogin(env, agentName, options);
|
|
219
|
+
// A person at a terminal waits for the approval. An agent's tool call waits
|
|
220
|
+
// briefly and then hands back the link, and --no-browser (the person is on
|
|
221
|
+
// another device) hands it back at once: the request is saved, so the next
|
|
222
|
+
// command, or `login --wait`, finishes it.
|
|
223
|
+
const interactive = options.interactive ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
224
|
+
const budget = wait === true || (wait === undefined && interactive && !noBrowser)
|
|
225
|
+
? Infinity
|
|
226
|
+
: wait === false || noBrowser
|
|
227
|
+
? 0
|
|
228
|
+
: AGENT_WAIT_MS;
|
|
229
|
+
const until = Math.min(pending.expiresAt, Date.now() + budget);
|
|
230
|
+
notify(`Connect your agent: ${pending.url}\nCheck that the browser shows code ${pending.userCode}, then choose Connect agent.${budget > 0 ? "\nWaiting for the approval. Stopping this is safe: running it again keeps the same link." : ""}`);
|
|
231
|
+
if (!noBrowser && !pending.resumed)
|
|
232
|
+
await (options.openBrowser ?? browser)(pending.url).catch(() => notify("Open the connection link in your browser to continue."));
|
|
233
|
+
for (let first = true; first || Date.now() < until; first = false) {
|
|
234
|
+
if (!first || !pending.resumed) {
|
|
235
|
+
if (budget === 0)
|
|
236
|
+
break;
|
|
237
|
+
await sleep(3000);
|
|
238
|
+
}
|
|
239
|
+
const outcome = await pending.check();
|
|
240
|
+
if (outcome === "expired")
|
|
241
|
+
throw new Error(named("The connection expired. Run `runtime login` again."));
|
|
242
|
+
if (outcome !== "pending")
|
|
243
|
+
return outcome;
|
|
244
|
+
}
|
|
245
|
+
if (Date.now() >= pending.expiresAt)
|
|
246
|
+
throw new Error(named("The connection expired. Run `runtime login` again."));
|
|
247
|
+
return {
|
|
248
|
+
connected: false,
|
|
249
|
+
pending: true,
|
|
250
|
+
url: pending.url,
|
|
251
|
+
userCode: pending.userCode,
|
|
252
|
+
expiresAt: new Date(pending.expiresAt).toISOString(),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Starts a browser-approved connection and hands back its link, without
|
|
256
|
+
* waiting: `runtime login` waits on it, and the MCP bridge gives the link to
|
|
257
|
+
* the agent, which shows it to the person it works for. */
|
|
258
|
+
export async function beginDeviceLogin(env, agentName, options = {}) {
|
|
259
|
+
const { origins, store, request } = connection(env, options);
|
|
260
|
+
// A request this machine already started and nobody has answered yet: pick
|
|
261
|
+
// it up, so a command stopped while it waited (an agent's tool timing out)
|
|
262
|
+
// shows the same link and code again rather than a new one to approve.
|
|
263
|
+
let pending = await store.readPending();
|
|
264
|
+
const resumed = pending !== null;
|
|
265
|
+
if (!pending) {
|
|
266
|
+
const pair = await promisify(generateKeyPair)("rsa", {
|
|
267
|
+
modulusLength: 2048,
|
|
268
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
269
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
270
|
+
});
|
|
271
|
+
const started = await request("start", { agentName, publicKey: pair.publicKey });
|
|
272
|
+
if (typeof started.verificationUri !== "string" ||
|
|
273
|
+
typeof started.deviceCode !== "string" ||
|
|
274
|
+
!/^[A-Za-z0-9_-]{43}$/.test(started.deviceCode) ||
|
|
275
|
+
typeof started.userCode !== "string" ||
|
|
276
|
+
!/^[A-F0-9]{4}(-[A-F0-9]{4}){2}$/.test(started.userCode) ||
|
|
277
|
+
typeof started.expiresAt !== "number" ||
|
|
278
|
+
!Number.isFinite(started.expiresAt))
|
|
279
|
+
throw new Error("Runtime returned an invalid connection request.");
|
|
280
|
+
const verification = new URL(started.verificationUri);
|
|
281
|
+
if (verification.origin !== origins.auth ||
|
|
282
|
+
verification.pathname !== "/connect" ||
|
|
283
|
+
verification.username ||
|
|
284
|
+
verification.password ||
|
|
285
|
+
verification.hash ||
|
|
286
|
+
[...verification.searchParams.keys()].some((name) => name !== "code") ||
|
|
287
|
+
verification.searchParams.get("code") !== started.userCode)
|
|
288
|
+
throw new Error("Runtime returned an unexpected connection address.");
|
|
289
|
+
pending = {
|
|
290
|
+
version: 1,
|
|
291
|
+
apiOrigin: origins.api,
|
|
292
|
+
authOrigin: origins.auth,
|
|
293
|
+
privateKey: pair.privateKey,
|
|
294
|
+
deviceCode: started.deviceCode,
|
|
295
|
+
url: verification.href,
|
|
296
|
+
userCode: started.userCode,
|
|
297
|
+
agentName,
|
|
298
|
+
expiresAt: Math.min(started.expiresAt, Date.now() + 15 * 60_000),
|
|
299
|
+
};
|
|
300
|
+
await store.savePending(pending);
|
|
301
|
+
}
|
|
302
|
+
const { deviceCode, privateKey } = pending;
|
|
303
|
+
const name = pending.agentName;
|
|
304
|
+
return {
|
|
305
|
+
url: pending.url,
|
|
306
|
+
userCode: pending.userCode,
|
|
307
|
+
expiresAt: pending.expiresAt,
|
|
308
|
+
resumed,
|
|
309
|
+
async check() {
|
|
310
|
+
let result;
|
|
311
|
+
try {
|
|
312
|
+
result = await request("token", { deviceCode });
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error instanceof ConnectionError && error.status >= 500)
|
|
316
|
+
return "pending";
|
|
317
|
+
await store.removePending();
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
if (result.status === "pending")
|
|
321
|
+
return "pending";
|
|
322
|
+
// Answered either way: the next attempt starts afresh. An approval
|
|
323
|
+
// forgets the request only once its key is saved.
|
|
324
|
+
if (result.status === "denied") {
|
|
325
|
+
await store.removePending();
|
|
326
|
+
throw new Error("The connection was declined.");
|
|
327
|
+
}
|
|
328
|
+
if (result.status === "expired") {
|
|
329
|
+
await store.removePending();
|
|
330
|
+
return "expired";
|
|
331
|
+
}
|
|
332
|
+
if (result.status !== "connected" ||
|
|
333
|
+
typeof result.encryptedKey !== "string" ||
|
|
334
|
+
typeof result.connectionId !== "string" ||
|
|
335
|
+
typeof result.orgId !== "string") {
|
|
336
|
+
await store.removePending();
|
|
337
|
+
throw new Error("Runtime returned an invalid connection result.");
|
|
338
|
+
}
|
|
339
|
+
let key;
|
|
340
|
+
try {
|
|
341
|
+
key = privateDecrypt({ key: privateKey, oaepHash: "sha256" }, Buffer.from(result.encryptedKey, "base64")).toString("utf8");
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
await store.removePending();
|
|
345
|
+
throw new Error("Could not verify Runtime's connection result. Start again.");
|
|
346
|
+
}
|
|
347
|
+
if (!/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(key)) {
|
|
348
|
+
await store.removePending();
|
|
349
|
+
throw new Error("Runtime returned an invalid credential.");
|
|
350
|
+
}
|
|
351
|
+
// Persist before confirmation: losing the response must never lose access
|
|
352
|
+
// or make onboarding falsely report that the agent is connected.
|
|
353
|
+
await store.save({
|
|
354
|
+
version: 1,
|
|
355
|
+
apiOrigin: origins.api,
|
|
356
|
+
authOrigin: origins.auth,
|
|
357
|
+
key,
|
|
358
|
+
connectionId: result.connectionId,
|
|
359
|
+
orgId: result.orgId,
|
|
360
|
+
agentName: name,
|
|
361
|
+
});
|
|
362
|
+
await store.removePending();
|
|
363
|
+
await request("confirm", undefined, key);
|
|
364
|
+
return { connected: true, agentName: name, orgId: result.orgId };
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Bridges an MCP client that speaks stdio to Runtime's hosted MCP endpoint.
|
|
2
|
+
* Newline-delimited JSON-RPC in, the same out; every message is forwarded as
|
|
3
|
+
* it is, so the server's tools, schemas and `instructions` reach the client
|
|
4
|
+
* unchanged, and nothing here keeps a second catalogue. No dependencies:
|
|
5
|
+
* starting it costs a process and one TLS connection.
|
|
6
|
+
*
|
|
7
|
+
* With no key on this machine it does not fail: it serves one tool,
|
|
8
|
+
* runtime_connect, that walks the agent and its person through the browser
|
|
9
|
+
* approval, then connects to Runtime and tells the client its tools changed.
|
|
10
|
+
* So `claude mcp add runtime -- npx -y withruntime mcp` is the whole
|
|
11
|
+
* setup. */
|
|
12
|
+
export declare function serveMcp(env?: NodeJS.ProcessEnv, options?: {
|
|
13
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}): Promise<void>;
|