@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
|
@@ -3,8 +3,7 @@ import assert from 'node:assert';
|
|
|
3
3
|
import { connectCdp, connectCdpUrl, } from "../../../cdp/client.js";
|
|
4
4
|
import { dockerAvailable, dockerMissingMessage, startContainer, } from "../../../cdp/container.js";
|
|
5
5
|
import { suggestCountryFromIp } from "../../../cdp/geo.js";
|
|
6
|
-
import { defaultProfileDir,
|
|
7
|
-
import { defaultConfigPath } from "../../../config/store.js";
|
|
6
|
+
import { defaultProfileDir, launchLocalBrowser, waitForCdpReady, } from "../../../cdp/launcher.js";
|
|
8
7
|
import { defineTool } from "../../server.js";
|
|
9
8
|
import { ensureLiveViewUrl } from "./live-view-url.js";
|
|
10
9
|
/**
|
|
@@ -16,84 +15,13 @@ import { ensureLiveViewUrl } from "./live-view-url.js";
|
|
|
16
15
|
* show — one on this machine is watched by looking at it.
|
|
17
16
|
*/
|
|
18
17
|
export const SHAREABLE_MODES = new Set(['container']);
|
|
19
|
-
/** Browser sources returned when the caller omits `mode`. Keep selection detail
|
|
20
|
-
* here instead of repeating it in the always-loaded tool description. */
|
|
21
|
-
const BROWSER_OPTIONS = [
|
|
22
|
-
{
|
|
23
|
-
mode: 'dedicated',
|
|
24
|
-
useCase: 'Working on your own machine',
|
|
25
|
-
label: 'Local Chrome, launched for you',
|
|
26
|
-
detail: 'Launches Chrome with a dedicated profile under '
|
|
27
|
-
+ '~/.reclaim/chrome-profile. Nothing to set up, and the profile '
|
|
28
|
-
+ 'persists, so a site you sign in to stays signed in. The easiest '
|
|
29
|
-
+ 'choice when you are doing the work yourself.',
|
|
30
|
-
requires: [],
|
|
31
|
-
liveView: 'None, and none needed — the browser is on your screen.',
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
mode: 'attach',
|
|
35
|
-
useCase: 'Working on your own machine',
|
|
36
|
-
label: 'The Chrome you already have open',
|
|
37
|
-
detail: 'Connects to a Chrome you started yourself, keeping the tabs, '
|
|
38
|
-
+ 'extensions and logins already in it. That Chrome must have been '
|
|
39
|
-
+ 'started with BOTH --remote-debugging-port AND a non-default '
|
|
40
|
-
+ '--user-data-dir: since Chrome 136 the debug flag is silently '
|
|
41
|
-
+ 'ignored on the default profile.',
|
|
42
|
-
requires: ['port (default 9222)'],
|
|
43
|
-
liveView: 'None, and none needed — the browser is on your screen.',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
mode: 'container',
|
|
47
|
-
useCase: 'Someone else has to sign in, or you need a phone',
|
|
48
|
-
label: 'Reclaim runtime in Docker — free, runs on your machine',
|
|
49
|
-
detail: 'Runs the Reclaim browser runtime (the same image the remote browser '
|
|
50
|
-
+ 'uses) as a container on this machine, and gives you a link you can '
|
|
51
|
-
+ 'send to whoever holds the account. They open it in any browser, '
|
|
52
|
-
+ 'including a phone, and can see and click in it. Free — it is your '
|
|
53
|
-
+ 'Docker and your bandwidth. Costs a 432 MB first-run download and '
|
|
54
|
-
+ 'about 2 GB of RAM. The image is amd64, so Apple Silicon runs it '
|
|
55
|
-
+ 'translated. OrbStack handles that itself; Docker Desktop needs '
|
|
56
|
-
+ 'Rosetta enabled.',
|
|
57
|
-
requires: ['Docker running'],
|
|
58
|
-
liveView: 'Built in. This call returns a local `viewerUrl`, and '
|
|
59
|
-
+ 'share_browser_view turns that into a public link for the person '
|
|
60
|
-
+ 'helping you.',
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
mode: 'builder',
|
|
64
|
-
useCase: 'Someone else has to sign in, or you need a phone',
|
|
65
|
-
label: 'Remote browser, hosted by Reclaim — CHARGEABLE',
|
|
66
|
-
detail: 'A browser allocated in Reclaim\'s cloud, billed against the '
|
|
67
|
-
+ 'organization\'s quota. Nothing to install, and the shareable link '
|
|
68
|
-
+ 'comes back immediately. It is also the only way to browse a site '
|
|
69
|
-
+ 'from a SPECIFIC COUNTRY — its whole egress is routed there. '
|
|
70
|
-
+ 'Geolocation is optional: omit countryCode to be offered a '
|
|
71
|
-
+ 'suggestion, or pass skipCountry to allocate without geo-routing.',
|
|
72
|
-
requires: [
|
|
73
|
-
'Builder authentication',
|
|
74
|
-
'orgId (auto-resolved when you have only one)',
|
|
75
|
-
'quota — this one costs money',
|
|
76
|
-
],
|
|
77
|
-
liveView: 'Included. This call returns a hosted liveViewUrl to pass on; '
|
|
78
|
-
+ 'share_browser_view does not apply.',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
mode: 'custom',
|
|
82
|
-
useCase: 'Advanced',
|
|
83
|
-
label: 'A CDP endpoint you supply',
|
|
84
|
-
detail: 'Connects to any ws:// or wss:// CDP websocket — a browser you run '
|
|
85
|
-
+ 'elsewhere, or another tool\'s. The URL is treated as a credential: '
|
|
86
|
-
+ 'never logged, persisted, or returned. Only pick this if you were '
|
|
87
|
-
+ 'given an endpoint and know why.',
|
|
88
|
-
requires: ['cdpUrl'],
|
|
89
|
-
liveView: 'None, unless you pass your own liveViewUrl alongside the cdpUrl.',
|
|
90
|
-
},
|
|
91
|
-
];
|
|
92
18
|
const DEFAULT_DEPENDENCIES = {
|
|
93
19
|
connectLocal: connectCdp,
|
|
94
20
|
connectRemote: connectCdpUrl,
|
|
95
21
|
dockerAvailable,
|
|
96
22
|
startContainer,
|
|
23
|
+
launchLocal: launchLocalBrowser,
|
|
24
|
+
waitForCdp: waitForCdpReady,
|
|
97
25
|
};
|
|
98
26
|
export function attachTool(state, builderClient, beforeReplace,
|
|
99
27
|
// Partial so a caller can stub one thing without restating the rest — the
|
|
@@ -104,16 +32,15 @@ dependencies = {}) {
|
|
|
104
32
|
};
|
|
105
33
|
return defineTool({
|
|
106
34
|
name: 'attach_browser',
|
|
107
|
-
description: '
|
|
108
|
-
+ '
|
|
109
|
-
+ '
|
|
110
|
-
+ '
|
|
111
|
-
+ '
|
|
112
|
-
+ '
|
|
113
|
-
+ '
|
|
114
|
-
+ '`builder`
|
|
115
|
-
+ '
|
|
116
|
-
+ 'selection, geolocation, and lifecycle.',
|
|
35
|
+
description: 'Opens a visible browser on the developer\'s own machine for '
|
|
36
|
+
+ 'sign-in and capture. Call it with no arguments and do not ask '
|
|
37
|
+
+ 'which browser to use: it launches installed Chrome (else '
|
|
38
|
+
+ 'Chromium, Edge, or a one-time Chrome for Testing download) with '
|
|
39
|
+
+ 'the persistent Reclaim profile. Pass `mode` only when the '
|
|
40
|
+
+ 'developer explicitly asks for that mode: `attach` (their running '
|
|
41
|
+
+ 'Chrome on `port`), `container` (Docker), `custom` (`cdpUrl`), or '
|
|
42
|
+
+ '`builder` (billed). Never offer or pick those yourself. See '
|
|
43
|
+
+ 'how_it_works({ topic: "browser" }).',
|
|
117
44
|
inputSchema: {
|
|
118
45
|
type: 'object',
|
|
119
46
|
properties: {
|
|
@@ -122,6 +49,8 @@ dependencies = {}) {
|
|
|
122
49
|
enum: [
|
|
123
50
|
'dedicated', 'attach', 'container', 'builder', 'custom',
|
|
124
51
|
],
|
|
52
|
+
description: 'Omit for the default local browser (`dedicated`). Set '
|
|
53
|
+
+ 'only to a mode the developer explicitly asked for.',
|
|
125
54
|
},
|
|
126
55
|
attachMode: {
|
|
127
56
|
type: 'string',
|
|
@@ -159,31 +88,12 @@ dependencies = {}) {
|
|
|
159
88
|
annotations: { openWorldHint: true, destructiveHint: false },
|
|
160
89
|
}, async (parsed) => {
|
|
161
90
|
const persisted = state.config.read()?.browser_agent;
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// quota,
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
const mode = parsed.mode ?? parsed.attachMode;
|
|
169
|
-
if (!mode) {
|
|
170
|
-
return {
|
|
171
|
-
needsChoice: true,
|
|
172
|
-
message: 'Ask FIRST whether they will be doing this themselves or '
|
|
173
|
-
+ 'someone else has to sign in — that answer decides half the '
|
|
174
|
-
+ 'list. Themselves: `dedicated` or `attach`. Someone else, or '
|
|
175
|
-
+ 'a phone-only site: `container` (free, needs Docker) or '
|
|
176
|
-
+ '`builder` (no install, chargeable) — the only two that give '
|
|
177
|
-
+ 'you a link to send. Then present the matching options and '
|
|
178
|
-
+ 'call attach_browser again with the `mode` they pick. Do not '
|
|
179
|
-
+ 'pick one for them.',
|
|
180
|
-
options: BROWSER_OPTIONS,
|
|
181
|
-
...(persisted?.attachMode
|
|
182
|
-
? { previouslyUsed: persisted.attachMode }
|
|
183
|
-
: {}),
|
|
184
|
-
configPath: defaultConfigPath(),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
91
|
+
// The default is a visible browser on the developer's own machine,
|
|
92
|
+
// with no question first. Every other mode costs something (Docker,
|
|
93
|
+
// quota, the developer's own profile) and is used only when the
|
|
94
|
+
// developer asked for it by name, so a persisted choice is never
|
|
95
|
+
// reapplied either.
|
|
96
|
+
const mode = parsed.mode ?? parsed.attachMode ?? 'dedicated';
|
|
187
97
|
let orgId = parsed.orgId;
|
|
188
98
|
if (mode === 'builder' && !orgId) {
|
|
189
99
|
assert(builderClient, badRequest('Builder browser service is unavailable'));
|
|
@@ -279,7 +189,17 @@ dependencies = {}) {
|
|
|
279
189
|
...(next.countryCode ? { countryCode: next.countryCode } : {}),
|
|
280
190
|
...(next.containerName ? { containerName: next.containerName } : {}),
|
|
281
191
|
...(canShare ? { liveViewAvailable: true } : {}),
|
|
192
|
+
...(next.launched ? { browser: next.launched.source } : {}),
|
|
282
193
|
_notes: [
|
|
194
|
+
...(next.launched?.source === 'chrome-for-testing'
|
|
195
|
+
? ['Launched Chrome for Testing'
|
|
196
|
+
+ (next.launched.downloaded
|
|
197
|
+
? ', downloaded just now into ~/.reclaim/browsers (later '
|
|
198
|
+
+ 'launches reuse it)'
|
|
199
|
+
: ' from ~/.reclaim/browsers')
|
|
200
|
+
+ `, because: ${next.launched.fallbackReason}. Mention `
|
|
201
|
+
+ 'this to the developer in one line.']
|
|
202
|
+
: []),
|
|
283
203
|
...(next.imagePulled
|
|
284
204
|
? ['The browser runtime image was downloaded just now (about '
|
|
285
205
|
+ '432 MB, about 1.3 GB on disk). Mention it — later attaches '
|
|
@@ -303,9 +223,12 @@ dependencies = {}) {
|
|
|
303
223
|
: []),
|
|
304
224
|
...(!localView && !hostedView
|
|
305
225
|
? ['This browser has no live view: it runs on the developer\'s '
|
|
306
|
-
+ 'own machine, so they watch it by looking at it.
|
|
307
|
-
+ '
|
|
308
|
-
+ '
|
|
226
|
+
+ 'own machine, so they watch it by looking at it. Do not '
|
|
227
|
+
+ 'offer other modes. Tell the developer once, as a side '
|
|
228
|
+
+ 'note, that if a volunteer is helping them sign in, a '
|
|
229
|
+
+ 'shareable link can be set up (`container` mode and '
|
|
230
|
+
+ 'share_browser_view); do not ask or set it up unless they '
|
|
231
|
+
+ 'ask.']
|
|
309
232
|
: []),
|
|
310
233
|
],
|
|
311
234
|
};
|
|
@@ -398,24 +321,41 @@ async function connect(mode, parsed, persisted, builderClient, dependencies) {
|
|
|
398
321
|
}
|
|
399
322
|
}
|
|
400
323
|
const port = parsed.port ?? persisted?.port ?? 9222;
|
|
324
|
+
let launched;
|
|
401
325
|
if (mode === 'dedicated') {
|
|
402
326
|
try {
|
|
403
|
-
|
|
327
|
+
// Reuse the browser an earlier attach launched on this port.
|
|
328
|
+
await dependencies.waitForCdp(port, 500);
|
|
404
329
|
}
|
|
405
330
|
catch {
|
|
406
|
-
|
|
407
|
-
|
|
331
|
+
const browser = await dependencies
|
|
332
|
+
.launchLocal({ port, profileDir: defaultProfileDir() });
|
|
333
|
+
// The browser outlives this attach on purpose, so its process handle
|
|
334
|
+
// is not kept.
|
|
335
|
+
launched = {
|
|
336
|
+
binary: browser.binary,
|
|
337
|
+
source: browser.source,
|
|
338
|
+
downloaded: browser.downloaded,
|
|
339
|
+
...(browser.fallbackReason
|
|
340
|
+
? { fallbackReason: browser.fallbackReason }
|
|
341
|
+
: {}),
|
|
342
|
+
};
|
|
408
343
|
}
|
|
409
344
|
}
|
|
410
345
|
else {
|
|
411
|
-
await
|
|
346
|
+
await dependencies.waitForCdp(port, 2000).catch(() => {
|
|
412
347
|
throw new Error(`attach mode: nothing answering CDP on port ${port}. Chrome must `
|
|
413
348
|
+ `be launched with --remote-debugging-port=${port} AND `
|
|
414
349
|
+ '--user-data-dir=<some-non-default-path>. Chrome 136+ ignores '
|
|
415
350
|
+ 'the debugging flag for the default profile.');
|
|
416
351
|
});
|
|
417
352
|
}
|
|
418
|
-
return {
|
|
353
|
+
return {
|
|
354
|
+
handles: await dependencies.connectLocal(port),
|
|
355
|
+
mode,
|
|
356
|
+
port,
|
|
357
|
+
...(launched ? { launched } : {}),
|
|
358
|
+
};
|
|
419
359
|
}
|
|
420
360
|
async function disposeState(state) {
|
|
421
361
|
try {
|
|
@@ -15,6 +15,7 @@ import { analyzeTool } from "./analyze.js";
|
|
|
15
15
|
import { attachTool } from "./attach.js";
|
|
16
16
|
import { captureTools, resetCaptures } from "./capture.js";
|
|
17
17
|
import { checkAttestorTool } from "./check-attestor.js";
|
|
18
|
+
import { draftTools } from "./draft-tools.js";
|
|
18
19
|
import { listTabsTool } from "./eval.js";
|
|
19
20
|
import { howItWorksTool } from "./how-it-works.js";
|
|
20
21
|
import { liveViewTools } from "./live-view.js";
|
|
@@ -73,6 +74,11 @@ export function buildAgentTools(client, builderClient = client, externalState) {
|
|
|
73
74
|
for (const t of sharedAuthoringTools(state.captures)) {
|
|
74
75
|
tools.push(t);
|
|
75
76
|
}
|
|
77
|
+
// check_matchers / update_draft: offline matcher check + in-place draft
|
|
78
|
+
// edits, keyed by captureId like run_proof_via_pod.
|
|
79
|
+
for (const t of draftTools(state.captures)) {
|
|
80
|
+
tools.push(t);
|
|
81
|
+
}
|
|
76
82
|
if (client) {
|
|
77
83
|
tools.push(startIntegrationTool());
|
|
78
84
|
tools.push(createVersionFromCaptureTool(client, state.attach, state.captures));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { makeDraftStore, } from "../../../authoring/index.js";
|
|
2
|
+
import { DEFAULT_MAX_CHARS, TEXT_WINDOW_PROPERTIES, textWindow, } from "../../../authoring/util.js";
|
|
2
3
|
import { startCapture } from "../../../cdp/capture.js";
|
|
3
4
|
import { defineTool } from "../../server.js";
|
|
4
5
|
export async function resetCaptures(captures) {
|
|
@@ -57,21 +58,27 @@ export function captureTools(attachRef, captures) {
|
|
|
57
58
|
});
|
|
58
59
|
const getTool = defineTool({
|
|
59
60
|
name: 'get_request',
|
|
60
|
-
description: 'Returns the
|
|
61
|
-
+ '
|
|
62
|
-
+
|
|
63
|
-
+ '
|
|
64
|
-
+ '
|
|
61
|
+
description: 'Returns the headers and bodies of one captured request, to inspect '
|
|
62
|
+
+ 'a candidate that `find_requests_containing` ranked. Each body is '
|
|
63
|
+
+ `cut to ${DEFAULT_MAX_CHARS} characters by default; a cut body `
|
|
64
|
+
+ 'comes with `requestBodyTruncation` or `responseBodyTruncation` '
|
|
65
|
+
+ '(total size and `nextOffset`). Page with `offset`, or pass '
|
|
66
|
+
+ '`maxChars` or `full`. Unlike the other tools, headers are raw: '
|
|
67
|
+
+ 'cookie and authorization values are visible. Read them for '
|
|
68
|
+
+ 'diagnosis; do not repeat them to the developer or write them to '
|
|
69
|
+
+ 'a file.',
|
|
65
70
|
inputSchema: {
|
|
66
71
|
type: 'object',
|
|
67
72
|
properties: {
|
|
68
73
|
captureId: { type: 'string' },
|
|
69
74
|
requestId: { type: 'string' },
|
|
75
|
+
...TEXT_WINDOW_PROPERTIES,
|
|
70
76
|
},
|
|
71
77
|
required: ['captureId', 'requestId'],
|
|
72
78
|
},
|
|
73
79
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
74
|
-
}, async (
|
|
80
|
+
}, async (args) => {
|
|
81
|
+
const { captureId, requestId } = args;
|
|
75
82
|
const entry = captures.sessions.get(captureId);
|
|
76
83
|
if (!entry) {
|
|
77
84
|
throw new Error(`no capture ${captureId}`);
|
|
@@ -80,11 +87,27 @@ export function captureTools(attachRef, captures) {
|
|
|
80
87
|
if (!r) {
|
|
81
88
|
throw new Error(`no request ${requestId} in capture ${captureId}`);
|
|
82
89
|
}
|
|
90
|
+
const { requestBody, responseBody, ...meta } = r;
|
|
83
91
|
const body = entry.session.bodies.get(requestId);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
const result = {
|
|
93
|
+
...meta,
|
|
94
|
+
...windowed('requestBody', requestBody, args),
|
|
95
|
+
...windowed('responseBody', body.available ? body.body : responseBody, args),
|
|
96
|
+
};
|
|
97
|
+
return body.available
|
|
98
|
+
? { ...result, bodyAvailable: true }
|
|
99
|
+
: { ...result, bodyAvailable: false, reason: body.reason };
|
|
88
100
|
});
|
|
89
101
|
return [startTool, stopTool, getTool];
|
|
90
102
|
}
|
|
103
|
+
/** `{ [key]: text window }`, plus `[key]Truncation` when the window cut it. */
|
|
104
|
+
function windowed(key, text, args) {
|
|
105
|
+
if (text === undefined) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
const { text: part, truncation } = textWindow(text, args);
|
|
109
|
+
return {
|
|
110
|
+
[key]: part,
|
|
111
|
+
...(truncation ? { [`${key}Truncation`]: truncation } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -11,13 +11,21 @@ export function checkAttestorTool(check = checkAttestor) {
|
|
|
11
11
|
+ 'seconds (about 8 when healthy). Call it once when you set up a '
|
|
12
12
|
+ 'new provider build, and after `run_proof` fails with a network '
|
|
13
13
|
+ 'error. Returns `{ ok, attestorUrl, durationMs, '
|
|
14
|
-
+ 'extractedParameters, error, errorKind }`.
|
|
15
|
-
+ '
|
|
16
|
-
+ '
|
|
17
|
-
+ '
|
|
18
|
-
+ '
|
|
19
|
-
+ '
|
|
20
|
-
+ '
|
|
14
|
+
+ 'extractedParameters, error, errorKind, proofFiles }`. '
|
|
15
|
+
+ '`proofFiles` is the proof file download state: `{ status }` is '
|
|
16
|
+
+ '`idle`, `ready`, `downloading` (with `method`, `elapsedMs`, and '
|
|
17
|
+
+ '`bytesDownloaded`/`totalBytes` when known), or `failed` (with '
|
|
18
|
+
+ '`reason`). If `ok` is true, the attestor is fine and a failing '
|
|
19
|
+
+ 'proof points to the target site. If `errorKind` is '
|
|
20
|
+
+ '`proof-files-downloading`, the one-time proof file download is '
|
|
21
|
+
+ 'still running and the attestor was not contacted: call this tool '
|
|
22
|
+
+ 'again shortly; do not report a network problem. If it is '
|
|
23
|
+
+ '`proof-files-failed`, the download failed: tell the developer '
|
|
24
|
+
+ '`error` (usually no access to github.com or '
|
|
25
|
+
+ 'raw.githubusercontent.com). If it is `attestor-unreachable`, the '
|
|
26
|
+
+ 'proof files are ready but this machine cannot reach the attestor '
|
|
27
|
+
+ '(network, VPN, firewall, or proxy): tell the developer. If it is '
|
|
28
|
+
+ '`setup-failed`, other local setup failed: report `error`. If it is '
|
|
21
29
|
+ '`attestor-error`, the attestor rejected the claim: report '
|
|
22
30
|
+ '`error`.',
|
|
23
31
|
inputSchema: {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `check_matchers` and `update_draft` — inspect and edit a draft's matchers
|
|
3
|
+
* without leaving MCP. `check_matchers` runs a recipe's redactions + matches
|
|
4
|
+
* against a captured response with attestor-core's own engine, offline (see
|
|
5
|
+
* proof/check-matchers.ts); `update_draft` replaces a draft's matchers in place
|
|
6
|
+
* after the same validation and portability gate `propose_provider` applies.
|
|
7
|
+
*/
|
|
8
|
+
import { badRequest } from '@hapi/boom';
|
|
9
|
+
import assert from 'node:assert';
|
|
10
|
+
import { checkMatchersOffline } from "../../../proof/check-matchers.js";
|
|
11
|
+
import { editDraftProvider } from "../../../provider/draft.js";
|
|
12
|
+
import { verificationWarnings } from "../../../provider/verification-checks.js";
|
|
13
|
+
import { defineTool } from "../../server.js";
|
|
14
|
+
const MATCHES_SCHEMA = {
|
|
15
|
+
type: 'array',
|
|
16
|
+
minItems: 1,
|
|
17
|
+
items: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
type: { type: 'string', enum: ['contains', 'regex'] },
|
|
21
|
+
value: { type: 'string' },
|
|
22
|
+
invert: { type: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
required: ['type', 'value'],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const REDACTIONS_SCHEMA = {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
xPath: { type: 'string' },
|
|
33
|
+
jsonPath: { type: 'string' },
|
|
34
|
+
regex: { type: 'string' },
|
|
35
|
+
hash: { type: 'string', enum: ['oprf', 'oprf-raw', 'oprf-mpc'] },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const PARAM_VALUES_SCHEMA = {
|
|
40
|
+
type: 'object',
|
|
41
|
+
additionalProperties: { type: 'string' },
|
|
42
|
+
};
|
|
43
|
+
export function draftTools(captures) {
|
|
44
|
+
const checkMatchers = defineTool({
|
|
45
|
+
name: 'check_matchers',
|
|
46
|
+
description: 'Checks a draft\'s `responseRedactions` and `responseMatches` '
|
|
47
|
+
+ 'against the captured response, offline, with the attestor\'s own '
|
|
48
|
+
+ 'redaction and match engine. No network call. Returns `ok`, '
|
|
49
|
+
+ '`failedAt` and `reason` on failure, `extractedParameters`, and the '
|
|
50
|
+
+ '`revealed` body slices the attestor would see (truncated). Pass '
|
|
51
|
+
+ '`responseMatches`, `responseRedactions`, or `paramValues` to try '
|
|
52
|
+
+ 'an edit without saving it, and `requestId` to check against a '
|
|
53
|
+
+ 'different captured request. Run it before `run_proof`.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
captureId: { type: 'string' },
|
|
58
|
+
draftId: { type: 'string' },
|
|
59
|
+
requestId: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Captured request whose response to check. '
|
|
62
|
+
+ 'Defaults to the request the draft was proposed from.',
|
|
63
|
+
},
|
|
64
|
+
responseMatches: MATCHES_SCHEMA,
|
|
65
|
+
responseRedactions: REDACTIONS_SCHEMA,
|
|
66
|
+
paramValues: PARAM_VALUES_SCHEMA,
|
|
67
|
+
},
|
|
68
|
+
required: ['captureId', 'draftId'],
|
|
69
|
+
},
|
|
70
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
71
|
+
}, async (args) => {
|
|
72
|
+
const { entry, draft } = resolveDraft(captures, args);
|
|
73
|
+
const provider = hasEdit(args)
|
|
74
|
+
? editDraftProvider(draft.provider, draft.extractions, args).provider
|
|
75
|
+
: draft.provider;
|
|
76
|
+
return checkCaptured(entry, args.requestId ?? draft.requestId, provider);
|
|
77
|
+
});
|
|
78
|
+
const updateDraft = defineTool({
|
|
79
|
+
name: 'update_draft',
|
|
80
|
+
description: 'Replaces a draft\'s `responseMatches`, `responseRedactions`, '
|
|
81
|
+
+ '`paramValues`, or `geoLocation` in place; the `draftId` stays '
|
|
82
|
+
+ 'the same. Validates '
|
|
83
|
+
+ 'regexes and selectors and rejects a matcher that bakes a captured '
|
|
84
|
+
+ 'value in as a literal, or redactions that reveal more of the '
|
|
85
|
+
+ 'captured response than the draft did. Returns the secret-free '
|
|
86
|
+
+ 'provider and, unless `check` is false, a `check_matchers` '
|
|
87
|
+
+ 'result for the edit. '
|
|
88
|
+
+ 'Prefer `propose_provider` with a better request or target; use '
|
|
89
|
+
+ 'this to narrow or fix what it drafted.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
captureId: { type: 'string' },
|
|
94
|
+
draftId: { type: 'string' },
|
|
95
|
+
responseMatches: MATCHES_SCHEMA,
|
|
96
|
+
responseRedactions: REDACTIONS_SCHEMA,
|
|
97
|
+
paramValues: PARAM_VALUES_SCHEMA,
|
|
98
|
+
geoLocation: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'ISO country the published version\'s request goes out from, '
|
|
101
|
+
+ 'for example `US`, or `{{DYNAMIC_GEO}}` (the default) for '
|
|
102
|
+
+ 'the verifying user\'s own country. Pin a country when '
|
|
103
|
+
+ 'run_proof only verified with that `geoLocation`.',
|
|
104
|
+
},
|
|
105
|
+
check: {
|
|
106
|
+
type: 'boolean',
|
|
107
|
+
description: 'Default true: run `check_matchers` on the edit.',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ['captureId', 'draftId'],
|
|
111
|
+
},
|
|
112
|
+
annotations: { openWorldHint: false, destructiveHint: false },
|
|
113
|
+
}, async (args) => {
|
|
114
|
+
const { entry, draft } = resolveDraft(captures, args);
|
|
115
|
+
assert(hasEdit(args), badRequest('Pass at least one of responseMatches, responseRedactions, '
|
|
116
|
+
+ 'paramValues, or geoLocation.'));
|
|
117
|
+
const edited = editDraftProvider(draft.provider, draft.extractions, args);
|
|
118
|
+
// Redactions are what the attestor doesn't see, so an edit may narrow
|
|
119
|
+
// them but never reveal more of the captured response than the draft.
|
|
120
|
+
const before = checkCaptured(entry, draft.requestId, draft.provider);
|
|
121
|
+
const check = checkCaptured(entry, draft.requestId, edited.provider);
|
|
122
|
+
assert(check.failedAt !== 'request' && check.failedAt !== 'redaction', badRequest('the edited recipe doesn\'t apply to the captured response '
|
|
123
|
+
+ `(${check.failedAt}): ${check.reason}`));
|
|
124
|
+
const comparable = before.failedAt !== 'request'
|
|
125
|
+
&& before.failedAt !== 'redaction';
|
|
126
|
+
assert(!comparable || check.revealedBytes <= before.revealedBytes, badRequest(`the edit reveals ${check.revealedBytes} bytes of the response `
|
|
127
|
+
+ `to the attestor, more than the draft's ${before.revealedBytes}. `
|
|
128
|
+
+ 'Keep redactions that hide everything except the proved values.'));
|
|
129
|
+
// The recipe changed, so an earlier replay no longer describes it.
|
|
130
|
+
const { lastReplay: _stale, ...rest } = draft;
|
|
131
|
+
void _stale;
|
|
132
|
+
entry.drafts.set(args.draftId, {
|
|
133
|
+
...rest,
|
|
134
|
+
provider: edited.provider,
|
|
135
|
+
extractions: edited.extractions,
|
|
136
|
+
});
|
|
137
|
+
const verification = verificationWarnings(edited.provider);
|
|
138
|
+
return {
|
|
139
|
+
draftId: args.draftId,
|
|
140
|
+
provider: edited.provider,
|
|
141
|
+
secretRefs: draft.secretRefs,
|
|
142
|
+
...(edited.warnings.length ? { warnings: edited.warnings } : {}),
|
|
143
|
+
...(verification.length
|
|
144
|
+
? { verificationWarnings: verification }
|
|
145
|
+
: {}),
|
|
146
|
+
...(args.check === false ? {} : { check }),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
return [checkMatchers, updateDraft];
|
|
150
|
+
}
|
|
151
|
+
function resolveDraft(captures, { captureId, draftId }) {
|
|
152
|
+
const entry = captures.sessions.get(captureId);
|
|
153
|
+
assert(entry, badRequest(`no capture ${captureId} — call start_capture first `
|
|
154
|
+
+ '(it has been reset or never started).'));
|
|
155
|
+
const draft = entry.drafts.get(draftId);
|
|
156
|
+
assert(draft, badRequest(`no draft ${draftId} in capture ${captureId}`));
|
|
157
|
+
return { entry, draft };
|
|
158
|
+
}
|
|
159
|
+
function hasEdit(edit) {
|
|
160
|
+
return edit.responseMatches !== undefined
|
|
161
|
+
|| edit.responseRedactions !== undefined
|
|
162
|
+
|| edit.paramValues !== undefined
|
|
163
|
+
|| edit.geoLocation !== undefined;
|
|
164
|
+
}
|
|
165
|
+
function checkCaptured(entry, requestId, provider) {
|
|
166
|
+
const captured = entry.session.requests.get(requestId);
|
|
167
|
+
assert(captured, badRequest(`no captured request ${requestId}`));
|
|
168
|
+
assert(captured.responseBody, badRequest(`response body unavailable for ${requestId}`));
|
|
169
|
+
return { requestId, ...checkMatchersOffline(provider, captured) };
|
|
170
|
+
}
|
|
@@ -27,7 +27,10 @@ export function podProofTools(attachRef, captures) {
|
|
|
27
27
|
+ 'instead of `run_proof` when the attach mode is "builder": the '
|
|
28
28
|
+ 'proof then reaches the target from the browser\'s own exit IP and '
|
|
29
29
|
+ 'country. Needs no `ownerAddress`; the pod signs with its own TEE '
|
|
30
|
-
+ 'key.',
|
|
30
|
+
+ 'key. It matches the browser\'s exit IP and country, not '
|
|
31
|
+
+ 'necessarily its TLS fingerprint, so it is not a way past bot '
|
|
32
|
+
+ 'protection: if `run_proof` reported '
|
|
33
|
+
+ '`blocked-non-browser-client`, expect the same here.',
|
|
31
34
|
inputSchema: {
|
|
32
35
|
type: 'object',
|
|
33
36
|
properties: {
|
|
@@ -54,6 +57,7 @@ export function podProofTools(attachRef, captures) {
|
|
|
54
57
|
return shapeRichProofResult(result, {
|
|
55
58
|
provider: draft.provider,
|
|
56
59
|
hasSecrets: Object.keys(draft.secrets).length > 0,
|
|
60
|
+
...(draft.lastReplay ? { lastReplay: draft.lastReplay } : {}),
|
|
57
61
|
});
|
|
58
62
|
});
|
|
59
63
|
return [tool];
|
|
@@ -11,7 +11,35 @@ export declare function shapeRichProofResult(result: ProofResult, opts: {
|
|
|
11
11
|
provider: unknown;
|
|
12
12
|
hasSecrets: boolean;
|
|
13
13
|
attestorUrl?: string;
|
|
14
|
+
/** The draft's latest authenticated in-browser replay, when tracked. */
|
|
15
|
+
lastReplay?: {
|
|
16
|
+
status: number;
|
|
17
|
+
ok: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** Where the attestor's request went out from. */
|
|
20
|
+
egress?: {
|
|
21
|
+
geoLocation?: string;
|
|
22
|
+
};
|
|
14
23
|
}): Record<string, unknown>;
|
|
24
|
+
export interface BlockedClientDiagnosis {
|
|
25
|
+
kind: 'blocked-non-browser-client';
|
|
26
|
+
/** HTTP status the attestor's request got, when the error carries one. */
|
|
27
|
+
status?: number;
|
|
28
|
+
/** Set when the same draft's in-browser replay succeeded. */
|
|
29
|
+
replayedInBrowser?: true;
|
|
30
|
+
likelyCause: string;
|
|
31
|
+
next: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Says when a failed proof looks like the site blocking non-browser clients:
|
|
35
|
+
* the attestor's request got 403/406/429/503, or a tiny or challenge-looking
|
|
36
|
+
* page where the browser got the real one. Returns undefined for verified
|
|
37
|
+
* proofs and other failures. The raw `error` stays on the result.
|
|
38
|
+
*/
|
|
39
|
+
export declare function diagnoseBlockedClient(result: Pick<ProofResult, 'verified' | 'error'>, lastReplay?: {
|
|
40
|
+
status: number;
|
|
41
|
+
ok: boolean;
|
|
42
|
+
}, geoLocation?: string): BlockedClientDiagnosis | undefined;
|
|
15
43
|
export interface ProofDiagnosis {
|
|
16
44
|
attestor: AttestorCheckResult;
|
|
17
45
|
likelyCause: string;
|