@reclaimprotocol/agent 0.1.35 → 0.1.37
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 +8 -8
- package/lib/authoring/backend.d.ts +12 -1
- package/lib/authoring/backend.js +3 -0
- package/lib/authoring/tools.js +33 -5
- package/lib/authoring/util.d.ts +43 -0
- package/lib/authoring/util.js +54 -0
- package/lib/cdp/chrome-for-testing.d.ts +14 -0
- package/lib/cdp/chrome-for-testing.js +43 -0
- package/lib/cdp/geo.d.ts +3 -0
- package/lib/cdp/geo.js +27 -15
- package/lib/cdp/launcher.d.ts +79 -1
- package/lib/cdp/launcher.js +244 -24
- package/lib/logger.js +3 -1
- package/lib/mcp/api/operations.gen.js +1 -1
- package/lib/mcp/instructions.js +3 -3
- package/lib/mcp/start.js +17 -2
- package/lib/mcp/tools/agent/attach.d.ts +6 -0
- package/lib/mcp/tools/agent/attach.js +58 -118
- package/lib/mcp/tools/agent/build.js +6 -0
- package/lib/mcp/tools/agent/capture.js +33 -10
- package/lib/mcp/tools/agent/check-attestor.js +15 -7
- package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
- package/lib/mcp/tools/agent/draft-tools.js +170 -0
- package/lib/mcp/tools/agent/pod-proof.js +5 -1
- package/lib/mcp/tools/agent/proof.d.ts +28 -0
- package/lib/mcp/tools/agent/proof.js +72 -0
- package/lib/mcp/tools/agent/shared.d.ts +8 -1
- package/lib/mcp/tools/agent/shared.js +98 -19
- package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
- package/lib/mcp/tools/agent/start-integration.js +10 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
- package/lib/mcp/tools/authenticate/authenticate.js +212 -116
- package/lib/mcp/tools/authenticate/build.d.ts +2 -2
- package/lib/mcp/tools/authenticate/build.js +6 -3
- package/lib/mcp/tools/authenticate/builder-access.js +5 -2
- package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
- package/lib/mcp/tools/authenticate/eth-key.js +29 -5
- package/lib/mcp/tools/authenticate/index.d.ts +1 -1
- package/lib/mcp/tools/authenticate/index.js +1 -1
- package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
- package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
- package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
- package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
- package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
- package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
- package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
- package/lib/mcp/tools/authenticate/session-cache.js +95 -4
- package/lib/mcp/tools/mode/mode.js +6 -6
- package/lib/mcp/tools/verification/proxy-callback-events.d.ts +21 -3
- package/lib/mcp/tools/verification/proxy-callback-events.js +128 -45
- package/lib/old/login-cdp.js +2 -3
- package/lib/proof/attestor.js +2 -3
- package/lib/proof/check-attestor.d.ts +12 -2
- package/lib/proof/check-attestor.js +40 -10
- package/lib/proof/check-matchers.d.ts +36 -0
- package/lib/proof/check-matchers.js +161 -0
- package/lib/proof/zk-files.d.ts +50 -11
- package/lib/proof/zk-files.js +259 -63
- package/lib/provider/draft.d.ts +24 -1
- package/lib/provider/draft.js +135 -0
- package/lib/provider/matcher.js +171 -4
- package/package.json +6 -4
- package/skill/SKILL.builder.md +7 -2
- package/skill/SKILL.common.md +90 -70
- package/skill/SKILL.old.md +3 -3
- package/skill/builder-mode.md +44 -6
- package/skill/old-mode.md +18 -13
- package/skill/start-integration.md +51 -35
- package/skill/verification-client-integration.md +87 -15
- package/src/authoring/backend.ts +12 -1
- package/src/authoring/tools.ts +48 -7
- package/src/authoring/util.ts +64 -0
- package/src/cdp/chrome-for-testing.ts +66 -0
- package/src/cdp/geo.ts +36 -21
- package/src/cdp/launcher.ts +328 -24
- package/src/logger.ts +3 -1
- package/src/mcp/api/operations.gen.ts +1 -1
- package/src/mcp/instructions.ts +3 -3
- package/src/mcp/start.ts +20 -1
- package/src/mcp/tools/agent/attach.ts +65 -128
- package/src/mcp/tools/agent/build.ts +7 -0
- package/src/mcp/tools/agent/capture.ts +54 -11
- package/src/mcp/tools/agent/check-attestor.ts +15 -7
- package/src/mcp/tools/agent/draft-tools.ts +234 -0
- package/src/mcp/tools/agent/pod-proof.ts +5 -1
- package/src/mcp/tools/agent/proof.ts +110 -1
- package/src/mcp/tools/agent/shared.ts +130 -19
- package/src/mcp/tools/agent/start-integration.ts +10 -6
- package/src/mcp/tools/authenticate/authenticate.ts +276 -131
- package/src/mcp/tools/authenticate/build.ts +6 -3
- package/src/mcp/tools/authenticate/builder-access.ts +5 -2
- package/src/mcp/tools/authenticate/eth-key.ts +41 -7
- package/src/mcp/tools/authenticate/index.ts +1 -1
- package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
- package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
- package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
- package/src/mcp/tools/authenticate/session-cache.ts +125 -5
- package/src/mcp/tools/mode/mode.ts +6 -6
- package/src/mcp/tools/verification/proxy-callback-events.ts +151 -41
- package/src/old/login-cdp.ts +2 -3
- package/src/proof/attestor.ts +2 -3
- package/src/proof/check-attestor.ts +55 -12
- package/src/proof/check-matchers.ts +227 -0
- package/src/proof/zk-files.ts +357 -65
- package/src/provider/draft.ts +215 -7
- package/src/provider/matcher.ts +237 -4
package/lib/cdp/launcher.js
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
import { conflict, failedDependency, gatewayTimeout, preconditionFailed, } from '@hapi/boom';
|
|
2
|
+
import assert from 'node:assert';
|
|
1
3
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { homedir, platform } from 'node:os';
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, readlinkSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { homedir, hostname, platform } from 'node:os';
|
|
4
6
|
import { join } from 'node:path';
|
|
5
7
|
import { chromePath, localAppData } from "../consts.js";
|
|
8
|
+
import { LOGGER } from "../logger.js";
|
|
6
9
|
import { reclaimHomeDirPath } from "../paths.js";
|
|
10
|
+
import { installChromeForTesting } from "./chrome-for-testing.js";
|
|
11
|
+
/** How long a launched browser gets to answer CDP. A cold first start on a
|
|
12
|
+
* slow disk, or Chrome for Testing's first run, can take well over 10s. */
|
|
13
|
+
export const CDP_READY_TIMEOUT_MS = 30000;
|
|
14
|
+
/** How much of the browser's stderr to keep for error messages. */
|
|
15
|
+
const STDERR_TAIL_CHARS = 4096;
|
|
16
|
+
/** The files Chrome uses on POSIX to claim a profile. `SingletonLock` is a
|
|
17
|
+
* symlink to `<hostname>-<pid>`; the other two point at the IPC socket and a
|
|
18
|
+
* random cookie, and are only meaningful while that process lives. */
|
|
19
|
+
const SINGLETON_FILES = ['SingletonLock', 'SingletonSocket', 'SingletonCookie'];
|
|
20
|
+
const DEFAULT_DEPENDENCIES = {
|
|
21
|
+
platform,
|
|
22
|
+
env: () => process.env,
|
|
23
|
+
chromePath,
|
|
24
|
+
findInstalled: () => findInstalledBrowsers(),
|
|
25
|
+
installChromeForTesting,
|
|
26
|
+
};
|
|
7
27
|
/**
|
|
8
28
|
* Build the platform-specific list of Chromium-family binaries to probe.
|
|
9
29
|
* Order matters: Chrome first, then Chromium, then Edge (Edge is
|
|
@@ -15,8 +35,8 @@ import { reclaimHomeDirPath } from "../paths.js";
|
|
|
15
35
|
* without admin rights ("Install for me only"). Skipping this was the
|
|
16
36
|
* #1 first-run failure observed in the field.
|
|
17
37
|
*/
|
|
18
|
-
function browserCandidates() {
|
|
19
|
-
switch (
|
|
38
|
+
export function browserCandidates(os = platform()) {
|
|
39
|
+
switch (os) {
|
|
20
40
|
case 'darwin':
|
|
21
41
|
return [
|
|
22
42
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
@@ -50,26 +70,105 @@ function browserCandidates() {
|
|
|
50
70
|
return [];
|
|
51
71
|
}
|
|
52
72
|
}
|
|
73
|
+
/** The candidates that exist on this machine, in preference order. */
|
|
74
|
+
export function findInstalledBrowsers(candidates = browserCandidates()) {
|
|
75
|
+
return candidates.filter((c) => existsSync(c));
|
|
76
|
+
}
|
|
53
77
|
export function defaultProfileDir() {
|
|
54
78
|
return join(reclaimHomeDirPath(), 'chrome-profile');
|
|
55
79
|
}
|
|
56
80
|
export function findChromeBinary() {
|
|
57
81
|
const candidates = browserCandidates();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
82
|
+
const found = chromePath() ?? findInstalledBrowsers(candidates)[0];
|
|
83
|
+
if (found) {
|
|
84
|
+
return found;
|
|
62
85
|
}
|
|
63
86
|
throw new Error(`No Chromium-family browser found on ${platform()}. Tried:\n ` +
|
|
64
87
|
candidates.join('\n ') +
|
|
65
88
|
'\nSet RECLAIM_AGENT_CHROME_PATH to point at ' +
|
|
66
89
|
'your chrome.exe / msedge.exe / chromium binary.');
|
|
67
90
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Why a visible browser cannot open here, or undefined when it can. Only
|
|
93
|
+
* Linux needs a display server named in the environment; an MCP server
|
|
94
|
+
* started outside the desktop session (a sandboxed agent, a service) often
|
|
95
|
+
* has neither variable, and Chrome then exits before CDP ever answers.
|
|
96
|
+
*/
|
|
97
|
+
export function missingDisplayReason(os = platform(), env = process.env) {
|
|
98
|
+
if (os !== 'linux' || env.WAYLAND_DISPLAY || env.DISPLAY) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return 'No display: neither WAYLAND_DISPLAY nor DISPLAY is set in the '
|
|
102
|
+
+ 'Reclaim MCP server\'s environment, so a visible browser cannot open. '
|
|
103
|
+
+ 'Start the agent from the desktop session, or pass the variables to '
|
|
104
|
+
+ 'the MCP server (for example DISPLAY=:0, or WAYLAND_DISPLAY=wayland-0 '
|
|
105
|
+
+ 'with XDG_RUNTIME_DIR), then restart the agent.';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Launch a visible browser on this machine with the given profile, and wait
|
|
109
|
+
* until it answers CDP.
|
|
110
|
+
*
|
|
111
|
+
* Uses `RECLAIM_AGENT_CHROME_PATH` when set, else the first installed browser
|
|
112
|
+
* (Chrome, then Chromium, then Edge). When none is installed, or that one
|
|
113
|
+
* fails to start, it falls back to Chrome for Testing, downloaded once into
|
|
114
|
+
* `~/.reclaim/browsers`. A missing display fails fast instead: no browser can
|
|
115
|
+
* open without one, so a download would only waste time.
|
|
116
|
+
*/
|
|
117
|
+
export async function launchLocalBrowser(opts, dependencies = {}) {
|
|
118
|
+
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
119
|
+
const noDisplay = missingDisplayReason(deps.platform(), deps.env());
|
|
120
|
+
assert(!noDisplay, preconditionFailed(noDisplay));
|
|
121
|
+
// A live owner would make every launch below fail the same way, so refuse
|
|
122
|
+
// once with the reason instead of trying Chrome for Testing too.
|
|
123
|
+
releaseStaleProfileLock(opts.profileDir, deps.platform());
|
|
124
|
+
const override = deps.chromePath();
|
|
125
|
+
const preferred = override ?? deps.findInstalled()[0];
|
|
126
|
+
let fallbackReason = 'No Chrome, Chromium, or Edge is installed.';
|
|
127
|
+
if (preferred) {
|
|
128
|
+
try {
|
|
129
|
+
return {
|
|
130
|
+
binary: preferred,
|
|
131
|
+
source: override ? 'override' : 'installed',
|
|
132
|
+
process: await startChrome(preferred, opts),
|
|
133
|
+
downloaded: false,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
fallbackReason = errorMessage(err);
|
|
138
|
+
LOGGER.warn({ binary: preferred, reason: fallbackReason }, 'local browser failed to start; trying Chrome for Testing');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let cft;
|
|
142
|
+
try {
|
|
143
|
+
cft = await deps.installChromeForTesting();
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
throw failedDependency(`${fallbackReason}\nChrome for Testing could not be installed into `
|
|
147
|
+
+ `~/.reclaim/browsers: ${errorMessage(err)}`);
|
|
72
148
|
}
|
|
149
|
+
try {
|
|
150
|
+
return {
|
|
151
|
+
binary: cft.executablePath,
|
|
152
|
+
source: 'chrome-for-testing',
|
|
153
|
+
process: await startChrome(cft.executablePath, opts),
|
|
154
|
+
downloaded: cft.downloaded,
|
|
155
|
+
fallbackReason,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
throw failedDependency(`${fallbackReason}\nChrome for Testing (${cft.executablePath}) also `
|
|
160
|
+
+ `failed: ${errorMessage(err)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Spawn `binary` on `opts.profileDir` and wait for CDP. On failure the
|
|
165
|
+
* process is stopped and the error says why: it could not be spawned, it
|
|
166
|
+
* exited (with its code and the tail of its stderr), or it never answered.
|
|
167
|
+
*/
|
|
168
|
+
export async function startChrome(binary, opts) {
|
|
169
|
+
const timeoutMs = opts.timeoutMs ?? CDP_READY_TIMEOUT_MS;
|
|
170
|
+
mkdirSync(opts.profileDir, { recursive: true });
|
|
171
|
+
releaseStaleProfileLock(opts.profileDir);
|
|
73
172
|
const args = [
|
|
74
173
|
`--remote-debugging-port=${opts.port}`,
|
|
75
174
|
`--user-data-dir=${opts.profileDir}`,
|
|
@@ -80,24 +179,145 @@ export function launchChrome(opts) {
|
|
|
80
179
|
args.push('--headless=new', '--disable-gpu');
|
|
81
180
|
}
|
|
82
181
|
const child = spawn(binary, args, {
|
|
83
|
-
stdio: 'ignore',
|
|
182
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
84
183
|
detached: false,
|
|
85
184
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
185
|
+
// Drained for the browser's whole life, not just until it is ready: a
|
|
186
|
+
// full pipe would block its writes.
|
|
187
|
+
let stderr = '';
|
|
188
|
+
child.stderr?.setEncoding('utf8');
|
|
189
|
+
child.stderr?.on('data', (chunk) => {
|
|
190
|
+
stderr = (stderr + chunk).slice(-STDERR_TAIL_CHARS);
|
|
191
|
+
});
|
|
192
|
+
let spawnError;
|
|
193
|
+
child.once('error', (err) => {
|
|
194
|
+
spawnError = err;
|
|
195
|
+
});
|
|
89
196
|
const deadline = Date.now() + timeoutMs;
|
|
90
197
|
while (Date.now() < deadline) {
|
|
198
|
+
if (await cdpAnswers(opts.port)) {
|
|
199
|
+
return child;
|
|
200
|
+
}
|
|
201
|
+
if (spawnError) {
|
|
202
|
+
throw failedDependency(`Could not start ${binary}: ${spawnError.message}`);
|
|
203
|
+
}
|
|
204
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
205
|
+
throw failedDependency(`${binary} exited with ${child.exitCode !== null
|
|
206
|
+
? `code ${child.exitCode}`
|
|
207
|
+
: `signal ${child.signalCode}`} before CDP answered on port `
|
|
208
|
+
+ `${opts.port}.${stderrTail()}`);
|
|
209
|
+
}
|
|
210
|
+
await sleep(150);
|
|
211
|
+
}
|
|
212
|
+
await stopProcess(child);
|
|
213
|
+
throw gatewayTimeout(`${binary} did not answer CDP on port ${opts.port} within `
|
|
214
|
+
+ `${timeoutMs}ms.${stderrTail()}`);
|
|
215
|
+
function stderrTail() {
|
|
216
|
+
const tail = stderr.trim();
|
|
217
|
+
return tail ? `\nBrowser stderr (last ${tail.length} chars):\n${tail}` : '';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Remove a dead browser's claim on `profileDir` so a new launch can take it.
|
|
222
|
+
*
|
|
223
|
+
* Chrome on POSIX claims a profile with `SingletonLock -> <hostname>-<pid>`.
|
|
224
|
+
* If that process is gone (a crash, a killed session) the links stay behind
|
|
225
|
+
* and the next launch hands off to the missing owner and exits. A lock whose
|
|
226
|
+
* PID is alive on this host belongs to a running browser, and is left alone:
|
|
227
|
+
* that browser must be closed first. A lock from another hostname is treated
|
|
228
|
+
* as stale: `~/.reclaim` is local, and a sandbox or container that changes the
|
|
229
|
+
* hostname between runs is the usual way to get one. Callers reach this only
|
|
230
|
+
* after nothing answered CDP on the port.
|
|
231
|
+
*/
|
|
232
|
+
export function releaseStaleProfileLock(profileDir, os = platform()) {
|
|
233
|
+
// Windows locks the profile with a plain file the OS releases on exit.
|
|
234
|
+
if (os === 'win32') {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const target = readLink(join(profileDir, 'SingletonLock'));
|
|
238
|
+
const owner = target?.match(/^(.+)-(\d+)$/);
|
|
239
|
+
if (owner?.[1] === hostname() && processAlive(Number(owner[2]))) {
|
|
240
|
+
throw conflict(`The browser profile ${profileDir} is in use by a running browser `
|
|
241
|
+
+ `(PID ${owner[2]}) that is not answering CDP on the requested port. `
|
|
242
|
+
+ 'Close that browser window, then retry.');
|
|
243
|
+
}
|
|
244
|
+
const removed = SINGLETON_FILES.filter((name) => {
|
|
245
|
+
const path = join(profileDir, name);
|
|
246
|
+
if (readLink(path) === undefined) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
91
249
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
250
|
+
unlinkSync(path);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
LOGGER.warn({ err, path }, 'could not remove a stale profile lock');
|
|
255
|
+
return false;
|
|
96
256
|
}
|
|
97
|
-
|
|
98
|
-
|
|
257
|
+
});
|
|
258
|
+
if (removed.length) {
|
|
259
|
+
LOGGER.info({ profileDir, removed, owner: target }, 'removed a stale browser profile lock');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Wait until something answers CDP on `port`, polling every 150ms.
|
|
264
|
+
*/
|
|
265
|
+
export async function waitForCdpReady(port, timeoutMs = CDP_READY_TIMEOUT_MS) {
|
|
266
|
+
const deadline = Date.now() + timeoutMs;
|
|
267
|
+
while (Date.now() < deadline) {
|
|
268
|
+
if (await cdpAnswers(port)) {
|
|
269
|
+
return;
|
|
99
270
|
}
|
|
100
|
-
await
|
|
271
|
+
await sleep(150);
|
|
101
272
|
}
|
|
102
|
-
throw
|
|
273
|
+
throw gatewayTimeout(`Chrome CDP did not become ready on port ${port} within ${timeoutMs}ms`);
|
|
274
|
+
}
|
|
275
|
+
/** A listener that accepts but never answers (a stale non-CDP service on the
|
|
276
|
+
* port) fails the poll after 2s instead of outliving the launch deadline. */
|
|
277
|
+
async function cdpAnswers(port) {
|
|
278
|
+
try {
|
|
279
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
280
|
+
signal: AbortSignal.timeout(2000),
|
|
281
|
+
});
|
|
282
|
+
return res.ok;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** SIGTERM, then SIGKILL if it lingers, and wait for the exit either way, so
|
|
289
|
+
* the profile lock is released before anything launches on it again. */
|
|
290
|
+
async function stopProcess(child) {
|
|
291
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const exited = new Promise((r) => child.once('exit', () => r()));
|
|
295
|
+
child.kill('SIGTERM');
|
|
296
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
|
|
297
|
+
await exited;
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
}
|
|
300
|
+
function readLink(path) {
|
|
301
|
+
try {
|
|
302
|
+
return lstatSync(path).isSymbolicLink() ? readlinkSync(path) : undefined;
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function processAlive(pid) {
|
|
309
|
+
try {
|
|
310
|
+
process.kill(pid, 0);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
// EPERM: it exists, it just belongs to someone else.
|
|
315
|
+
return err instanceof Error && 'code' in err && err.code === 'EPERM';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function errorMessage(err) {
|
|
319
|
+
return err instanceof Error ? err.message : String(err);
|
|
320
|
+
}
|
|
321
|
+
function sleep(ms) {
|
|
322
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
103
323
|
}
|
package/lib/logger.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import pino, { stdTimeFunctions } from 'pino';
|
|
2
2
|
import { LOG_LEVEL } from "./consts.js";
|
|
3
|
+
// stderr, not pino's default stdout: the MCP server speaks JSON-RPC over
|
|
4
|
+
// stdout, so a log line there corrupts the protocol stream.
|
|
3
5
|
export const LOGGER = pino({
|
|
4
6
|
name: '@reclaimprotocol/agent',
|
|
5
7
|
timestamp: stdTimeFunctions.isoTime,
|
|
6
8
|
base: { hostname: undefined, pid: undefined },
|
|
7
|
-
});
|
|
9
|
+
}, pino.destination(2));
|
|
8
10
|
LOGGER.level = LOG_LEVEL;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AUTO-GENERATED by packages/app/scripts/mcp-tools-gen.ts
|
|
2
|
-
// from packages/app/openapi.yaml (spec v0.1.0, source sha256:
|
|
2
|
+
// from packages/app/openapi.yaml (spec v0.1.0, source sha256:96339e8dff91).
|
|
3
3
|
// The hash catches drift the version misses (a spec can change without a
|
|
4
4
|
// version bump) — do not remove it as "redundant" with the version.
|
|
5
5
|
// Run `npm run openapi:gen --workspace=packages/app` to regenerate. Do not edit by hand.
|
package/lib/mcp/instructions.js
CHANGED
|
@@ -51,9 +51,9 @@ const TOPIC_SUMMARIES = {
|
|
|
51
51
|
'auth': 'Signing in to the backend, and which token authorizes what.',
|
|
52
52
|
'devtools-migration': 'Importing a public legacy provider with the same '
|
|
53
53
|
+ 'UUID.',
|
|
54
|
-
'browser': '
|
|
55
|
-
+ 'Docker, a remote Builder browser,
|
|
56
|
-
+ 'geolocation, live view, and launch troubleshooting.',
|
|
54
|
+
'browser': 'The default local browser, the opt-in modes (attach, the '
|
|
55
|
+
+ 'Reclaim runtime in Docker, a remote Builder browser, a custom CDP '
|
|
56
|
+
+ 'endpoint), geolocation, live view, and launch troubleshooting.',
|
|
57
57
|
'capture': 'Tabs, sign-in detection, capturing through the navigation '
|
|
58
58
|
+ 'script, and picking an authenticated request to prove.',
|
|
59
59
|
'draft': 'The OPRF/privacy decision, propose_provider, and the canonical '
|
package/lib/mcp/start.js
CHANGED
|
@@ -10,8 +10,9 @@ import { OLD_MODE_INSTRUCTIONS } from "../old/instructions.js";
|
|
|
10
10
|
import { ReclaimSdkClient } from "../old/sdk-client.js";
|
|
11
11
|
import { buildOldBackendTools } from "../old/tools/build.js";
|
|
12
12
|
import { NAME, VERSION } from "../pkg.js";
|
|
13
|
+
import { startZkFilesDownload } from "../proof/zk-files.js";
|
|
13
14
|
import { registerTools } from "./api/tools.js";
|
|
14
|
-
import { buildAgentState, buildAgentTools, buildBuilderAuthenticationTools, buildBuilderOrganizationTools, buildCredentialTools, buildLocalCredentialTools, buildModeTools, buildVerificationTools, disposeAgentState, readSession, releaseAllListeners, stopAllForwarders, } from "./tools/index.js";
|
|
15
|
+
import { buildAgentState, buildAgentTools, buildBuilderAuthenticationTools, buildBuilderOrganizationTools, buildCredentialTools, buildLocalCredentialTools, buildModeTools, buildVerificationTools, disposeAgentState, readSession, releaseAllListeners, sessionRefreshingFetch, stopAllForwarders, } from "./tools/index.js";
|
|
15
16
|
import { BUILDER_MODE_INSTRUCTIONS } from "./instructions.js";
|
|
16
17
|
import { registerToolList } from "./server.js";
|
|
17
18
|
export async function startMcpServer() {
|
|
@@ -31,11 +32,19 @@ export async function startMcpServer() {
|
|
|
31
32
|
const agentToolsEnabled = !agentDisabled();
|
|
32
33
|
/** The agent state this server registered, for shutdown to release. */
|
|
33
34
|
let agentState;
|
|
35
|
+
const envToken = apiToken();
|
|
34
36
|
const builderClient = new ReclaimClient({
|
|
35
37
|
baseUrl: apiUrl(),
|
|
36
38
|
// Builder access is orthogonal to publishing mode. A legacy-devtools
|
|
37
39
|
// session can authenticate here solely to allocate a remote browser.
|
|
38
|
-
token:
|
|
40
|
+
token: envToken || readSession()?.token,
|
|
41
|
+
// The cached sign-in refreshes itself; an explicit RECLAIM_API_TOKEN
|
|
42
|
+
// takes precedence and is sent as is.
|
|
43
|
+
...(envToken
|
|
44
|
+
? {}
|
|
45
|
+
: {
|
|
46
|
+
fetch: sessionRefreshingFetch(oldMode ? 'authenticate_builder' : 'reclaim_authenticate'),
|
|
47
|
+
}),
|
|
39
48
|
headers: { 'User-Agent': `${NAME}/${VERSION}` },
|
|
40
49
|
});
|
|
41
50
|
// Backend-mode toggle — available in both modes and regardless of whether
|
|
@@ -99,6 +108,12 @@ export async function startMcpServer() {
|
|
|
99
108
|
}
|
|
100
109
|
const transport = new StdioServerTransport();
|
|
101
110
|
await server.connect(transport);
|
|
111
|
+
// Fetch the proof files in the background so the first proof doesn't wait
|
|
112
|
+
// for them. Progress goes to stderr; `check_attestor` reports the state,
|
|
113
|
+
// and every proof still awaits the same download.
|
|
114
|
+
if (agentToolsEnabled) {
|
|
115
|
+
startZkFilesDownload();
|
|
116
|
+
}
|
|
102
117
|
}
|
|
103
118
|
/** How long teardown gets before the process leaves anyway. */
|
|
104
119
|
const SHUTDOWN_GRACE_MS = 10000;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReclaimClient } from '@reclaimprotocol/client/api';
|
|
2
2
|
import { type CdpHandles, connectCdp, connectCdpUrl } from '../../../cdp/client.ts';
|
|
3
3
|
import { dockerAvailable, startContainer } from '../../../cdp/container.ts';
|
|
4
|
+
import { launchLocalBrowser, type LocalBrowser, waitForCdpReady } from '../../../cdp/launcher.ts';
|
|
4
5
|
import type { ConfigStore } from '../../../config/store.ts';
|
|
5
6
|
import { type RegisteredTool } from '../../server.ts';
|
|
6
7
|
export type BrowserMode = 'dedicated' | 'attach' | 'builder' | 'custom' | 'container';
|
|
@@ -34,6 +35,8 @@ export interface AttachState {
|
|
|
34
35
|
viewerPort?: number;
|
|
35
36
|
/** Whether the runtime image had to be downloaded on this attach. */
|
|
36
37
|
imagePulled?: boolean;
|
|
38
|
+
/** `dedicated` mode: the browser this attach launched, if it launched one. */
|
|
39
|
+
launched?: Omit<LocalBrowser, 'process'>;
|
|
37
40
|
disposeRemote?: () => Promise<void>;
|
|
38
41
|
}
|
|
39
42
|
export interface AttachRef {
|
|
@@ -46,6 +49,9 @@ interface AttachDependencies {
|
|
|
46
49
|
/** Injected so container mode is testable without Docker installed. */
|
|
47
50
|
dockerAvailable: typeof dockerAvailable;
|
|
48
51
|
startContainer: typeof startContainer;
|
|
52
|
+
/** Injected so the default launch is testable without a real browser. */
|
|
53
|
+
launchLocal: typeof launchLocalBrowser;
|
|
54
|
+
waitForCdp: typeof waitForCdpReady;
|
|
49
55
|
}
|
|
50
56
|
export declare function attachTool(state: AttachRef, builderClient?: ReclaimClient, beforeReplace?: () => Promise<void>, dependencies?: Partial<AttachDependencies>): RegisteredTool;
|
|
51
57
|
export declare function disposeAttached(state: AttachRef): Promise<void>;
|