@reclaimprotocol/agent 0.1.4 → 0.1.5
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 +5 -5
- package/lib/authoring/backend.d.ts +95 -0
- package/lib/authoring/backend.js +24 -0
- package/lib/authoring/index.d.ts +4 -0
- package/lib/authoring/index.js +5 -0
- package/lib/authoring/tools.d.ts +9 -0
- package/lib/authoring/tools.js +378 -0
- package/lib/cdp/capture-engine.js +1 -1
- package/lib/mcp/api/operations.gen.js +46 -18
- package/lib/mcp/server.d.ts +18 -2
- package/lib/mcp/server.js +19 -4
- package/lib/mcp/tools/agent/publish.js +2 -2
- package/lib/mcp/tools/agent/shared.d.ts +6 -0
- package/lib/mcp/tools/agent/shared.js +115 -0
- package/lib/old/login-cdp.d.ts +5 -0
- package/lib/old/login-cdp.js +12 -1
- package/lib/old/tools/authenticate.js +7 -3
- package/lib/proof/proof-core.d.ts +1 -1
- package/lib/proof/proof-core.js +1 -1
- package/lib/tools/registry.d.ts +36 -0
- package/lib/tools/registry.js +17 -0
- package/package.json +1 -1
- package/skill/SKILL.builder.md +13 -9
- package/skill/SKILL.old.md +14 -10
- package/src/cdp/capture-engine.ts +1 -1
- package/src/mcp/api/operations.gen.ts +46 -18
- package/src/mcp/server.ts +43 -6
- package/src/mcp/tools/agent/publish.ts +2 -2
- package/src/old/login-cdp.ts +20 -1
- package/src/old/tools/authenticate.ts +11 -3
- package/src/proof/proof-core.ts +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ This package **is** the Reclaim MCP server (bin: `reclaim-mcp-server`). It depen
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
**Prerequisites:** Node ≥ 22.18 and a local Chrome / Chromium / Edge (used for capture and the dashboard login). The first
|
|
9
|
+
**Prerequisites:** Node ≥ 22.18 and a local Chrome / Chromium / Edge (used for capture and the dashboard login). The first time you call `run_proof`, the agent downloads the ZK circuit files it needs (~280 MB) — so allow a moment for that first proof. The download is deferred to first proof on purpose (not an install step), so the MCP server itself starts immediately.
|
|
10
10
|
|
|
11
11
|
It runs as a **stdio MCP server**. Point any MCP-capable agent at this command:
|
|
12
12
|
|
|
@@ -140,7 +140,7 @@ Your only manual steps are signing in when the browser windows open and confirmi
|
|
|
140
140
|
|
|
141
141
|
## How it works (guide tool)
|
|
142
142
|
|
|
143
|
-
The full, mode-aware authoring guide (`skill/SKILL.md`) is served on demand by the **`how_it_works`** MCP tool — call it from any MCP client (Claude Code, Cursor, opencode, Antigravity, …) to get the deep capture → prove → publish workflow, decision frameworks, and troubleshooting. The server's always-on `instructions` cover the essentials; `how_it_works` is the in-depth version, with nothing to install.
|
|
143
|
+
The full, mode-aware authoring guide (`skill/SKILL.builder.md` / `skill/SKILL.old.md`, one per backend) is served on demand by the **`how_it_works`** MCP tool — call it from any MCP client (Claude Code, Cursor, opencode, Antigravity, …) to get the deep capture → prove → publish workflow, decision frameworks, and troubleshooting. The server's always-on `instructions` cover the essentials; `how_it_works` is the in-depth version, with nothing to install.
|
|
144
144
|
|
|
145
145
|
## Provider-authoring tools
|
|
146
146
|
|
|
@@ -175,13 +175,13 @@ Relevant env vars: `RECLAIM_OLD_API_URL` (default `https://devapi.reclaimprotoco
|
|
|
175
175
|
|
|
176
176
|
## Local development (from source)
|
|
177
177
|
|
|
178
|
-
For working on the package inside the monorepo (consumers should use [Install](#install) instead). A workspace install fetches dependencies
|
|
178
|
+
For working on the package inside the monorepo (consumers should use [Install](#install) instead). A workspace install just fetches dependencies:
|
|
179
179
|
|
|
180
180
|
```bash
|
|
181
181
|
npm install
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
The ZK circuit resources needed by `@reclaimprotocol/attestor-core` are **not** fetched at install time — `run_proof` downloads them on the first proof (see the [first-run note](#install)). To pre-fetch them instead of paying the cost on that first proof, run:
|
|
185
185
|
|
|
186
186
|
```bash
|
|
187
187
|
npm run download:zk-files --workspace=packages/agent
|
|
@@ -189,7 +189,7 @@ npm run download:zk-files --workspace=packages/agent
|
|
|
189
189
|
|
|
190
190
|
## Platform notes
|
|
191
191
|
|
|
192
|
-
- **Node >=22.18** is required (declared in `engines`). The floor is 22.18 because the
|
|
192
|
+
- **Node >=22.18** is required (declared in `engines`). The floor is 22.18 because the test suite and dev workflow run bare `.ts` files directly, and unflagged TypeScript type-stripping landed in Node 22.18.
|
|
193
193
|
- **Chrome 136+ silently disables `--remote-debugging-port`** when launched against the default user profile. `dedicated` mode (the default) sidesteps this by always passing `--user-data-dir=~/.reclaim/chrome-profile`.
|
|
194
194
|
|
|
195
195
|
## Tests
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CaptureSession } from '../cdp/capture-engine.ts';
|
|
2
|
+
import type { Extraction } from '../provider/draft.ts';
|
|
3
|
+
import type { ReclaimProvider, SecretParamRef } from '../provider/schema.ts';
|
|
4
|
+
/** Secret request params (cookie/auth headers), materialized only for the
|
|
5
|
+
* replay/prove call — never surfaced in tool output. */
|
|
6
|
+
export interface SecretParams {
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
/** A drafted recipe held server-side so the model only handles an opaque
|
|
10
|
+
* `draftId` — the provider is secret-free but the captured secrets never enter
|
|
11
|
+
* the transcript. Shared by propose → replay → run_proof. */
|
|
12
|
+
export interface Draft {
|
|
13
|
+
provider: ReclaimProvider;
|
|
14
|
+
secretRefs: SecretParamRef[];
|
|
15
|
+
/** Real secret header values (cookie/auth) for replay/prove — never returned
|
|
16
|
+
* to the model. */
|
|
17
|
+
secrets: Record<string, string>;
|
|
18
|
+
/** The captured request this draft was proposed from. */
|
|
19
|
+
requestId: string;
|
|
20
|
+
/** One entry per extracted value (value + its redaction), so replay can
|
|
21
|
+
* validate each against its own selector. */
|
|
22
|
+
extractions: Extraction[];
|
|
23
|
+
}
|
|
24
|
+
/** Per-session draft store: `propose` puts, `replay`/`run_proof` get by id. */
|
|
25
|
+
export interface DraftStore {
|
|
26
|
+
put(draft: Draft): string;
|
|
27
|
+
get(id: string): Draft | undefined;
|
|
28
|
+
}
|
|
29
|
+
export declare function makeDraftStore(): DraftStore;
|
|
30
|
+
/** Options for a proof run. `ownerAddress` selects the local signing credential
|
|
31
|
+
* and `attestorUrl` overrides the attestor endpoint (both MCP-only); the
|
|
32
|
+
* remote TEE backend ignores them (it signs in-enclave, own attestor). */
|
|
33
|
+
export interface ProveOptions {
|
|
34
|
+
ownerAddress?: string;
|
|
35
|
+
attestorUrl?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Everything the shared authoring tools need from one live session. Browser ops
|
|
39
|
+
* go through `sendCdp` (+ `waitForEvent` for load-style waits); capture reads
|
|
40
|
+
* use `capture`; proof/replay are whole operations because they genuinely
|
|
41
|
+
* differ (local attestor + live re-fetch vs remote TEE) — each backend shapes
|
|
42
|
+
* its own result, passed straight through as the tool's output.
|
|
43
|
+
*/
|
|
44
|
+
export interface AuthoringBackend {
|
|
45
|
+
/** Send a raw CDP command scoped to this session's page. */
|
|
46
|
+
sendCdp(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
47
|
+
/** Resolve once `method` (a CDP event) fires on this session's page, or
|
|
48
|
+
* after `timeoutMs`. Used for `Page.loadEventFired`-style waits. */
|
|
49
|
+
waitForEvent(method: string, timeoutMs: number): Promise<'fired' | 'timeout'>;
|
|
50
|
+
/** This session's captured requests (the shared capture engine). */
|
|
51
|
+
readonly capture: CaptureSession;
|
|
52
|
+
/** This session's drafts (propose → replay/run_proof). */
|
|
53
|
+
readonly drafts: DraftStore;
|
|
54
|
+
/** Prove a drafted recipe. Local: attestor SDK (rich claim view). Remote: TEE
|
|
55
|
+
* `/reclaim/prove` (lean, secret-free summary). Returns the environment's
|
|
56
|
+
* display shape. A backend may record the proven recipe internally (e.g.
|
|
57
|
+
* the cloud publish step) — that side effect is the backend's, not ours. */
|
|
58
|
+
prove(draft: Draft, opts?: ProveOptions): Promise<unknown>;
|
|
59
|
+
/**
|
|
60
|
+
* Validate/replay a drafted recipe before proving. With `withoutSecrets` it
|
|
61
|
+
* is the AUTH-BOUND check: re-issue the request stripped of the captured
|
|
62
|
+
* secrets (local: a Node re-fetch without them; remote: an anonymous `fetch`
|
|
63
|
+
* in the Popcorn page, `credentials: 'omit'`) and report whether the values
|
|
64
|
+
* are STILL extractable — i.e. the endpoint is public. Without it, validate
|
|
65
|
+
* extraction for the authenticated request. The shape is environment-specific
|
|
66
|
+
* and passed straight through as the tool's result.
|
|
67
|
+
*/
|
|
68
|
+
replay(draft: Draft, opts?: {
|
|
69
|
+
withoutSecrets?: boolean;
|
|
70
|
+
}): Promise<unknown>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* How a host (MCP / cloud) supplies its environment to the shared authoring
|
|
74
|
+
* tools, which are authored once with the common `defineTool` primitive and
|
|
75
|
+
* built per host by `buildAuthoringTools(host)`:
|
|
76
|
+
* - `resolveBackend`: hand back the backend for one call. The MCP resolves it
|
|
77
|
+
* from `args.captureId` (multi-session); the cloud returns its one run
|
|
78
|
+
* backend.
|
|
79
|
+
* - `sessionSchema`: input-schema props merged into EVERY tool (the MCP adds
|
|
80
|
+
* `captureId`; the cloud adds nothing — the backend is implicit).
|
|
81
|
+
* - `toolExtras`: per-tool schema/description additions (the MCP's rich,
|
|
82
|
+
* credential-aware `run_proof` contract).
|
|
83
|
+
*/
|
|
84
|
+
export interface AuthoringHost {
|
|
85
|
+
resolveBackend(args: Record<string, unknown>): AuthoringBackend;
|
|
86
|
+
sessionSchema?: {
|
|
87
|
+
properties: Record<string, unknown>;
|
|
88
|
+
required?: string[];
|
|
89
|
+
};
|
|
90
|
+
toolExtras?: Record<string, {
|
|
91
|
+
description?: string;
|
|
92
|
+
properties?: Record<string, unknown>;
|
|
93
|
+
required?: string[];
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend seam for the authoring tools. The tool LOGIC (drive the browser →
|
|
3
|
+
* capture → propose → replay → prove) is written once against this interface;
|
|
4
|
+
* each environment supplies a backend:
|
|
5
|
+
* - local (MCP): a Chrome tab over CDP + the in-process attestor SDK.
|
|
6
|
+
* - remote (cloud): the user's Popcorn browser over CDP + the TEE prover.
|
|
7
|
+
*
|
|
8
|
+
* One backend instance == one live session (a page + its capture). Multi
|
|
9
|
+
* session falls out of instancing: the MCP resolves a backend per tab/capture,
|
|
10
|
+
* the cloud one per run — so the shared tools never carry a session id and hold
|
|
11
|
+
* no cross-session state.
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
export function makeDraftStore() {
|
|
15
|
+
const map = new Map();
|
|
16
|
+
return {
|
|
17
|
+
put: (draft) => {
|
|
18
|
+
const id = `draft_${randomUUID()}`;
|
|
19
|
+
map.set(id, draft);
|
|
20
|
+
return id;
|
|
21
|
+
},
|
|
22
|
+
get: (id) => map.get(id),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { AuthoringBackend, AuthoringHost, Draft, DraftStore, ProveOptions, SecretParams, } from './backend.ts';
|
|
2
|
+
export { makeDraftStore } from './backend.ts';
|
|
3
|
+
export { buildAuthoringTools } from './tools.ts';
|
|
4
|
+
export { defineTool, type RegisteredTool, type ToolDefinition, } from '../tools/registry.ts';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { makeDraftStore } from "./backend.js";
|
|
2
|
+
export { buildAuthoringTools } from "./tools.js";
|
|
3
|
+
// The shared tool primitive — re-exported so consumers (the cloud loop) get the
|
|
4
|
+
// RegisteredTool type/`defineTool` without importing the MCP subsystem.
|
|
5
|
+
export { defineTool, } from "../tools/registry.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RegisteredTool } from '../tools/registry.ts';
|
|
2
|
+
import type { AuthoringHost } from './backend.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Build the authoring tools for a host as plain {@link RegisteredTool}s — the
|
|
5
|
+
* same primitive the MCP-only tools use. Each handler resolves the backend via
|
|
6
|
+
* `host.resolveBackend(args)`; the schema merges the host's `sessionSchema`
|
|
7
|
+
* (e.g. `captureId`) and any per-tool `toolExtras`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildAuthoringTools(host: AuthoringHost): RegisteredTool[];
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { draftProvider } from "../provider/draft.js";
|
|
2
|
+
import { OPRF_MAX_BYTES } from "../provider/matcher.js";
|
|
3
|
+
import { normalizeCandidates } from "../provider/normalize.js";
|
|
4
|
+
import { verificationWarnings } from "../provider/verification-checks.js";
|
|
5
|
+
import { defineTool } from "../tools/registry.js";
|
|
6
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7
|
+
/** Secret-free projection of a captured request for tool output. */
|
|
8
|
+
function publicRequest(r) {
|
|
9
|
+
return {
|
|
10
|
+
requestId: r.requestId,
|
|
11
|
+
method: r.method,
|
|
12
|
+
url: r.url,
|
|
13
|
+
status: r.status,
|
|
14
|
+
contentType: r.contentType,
|
|
15
|
+
size: r.size,
|
|
16
|
+
...(r.graphqlOp ? { graphqlOp: r.graphqlOp } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function matchesFilter(r, f) {
|
|
20
|
+
if (f.method && r.method !== f.method.toUpperCase()) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (f.urlGlob && !globMatch(f.urlGlob, r.url)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (f.statusMin !== undefined && r.status < f.statusMin) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (f.statusMax !== undefined && r.status > f.statusMax) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (f.contentType && !r.contentType.includes(f.contentType)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (f.graphqlOp && r.graphqlOp !== f.graphqlOp) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function globMatch(pattern, str) {
|
|
41
|
+
const re = new RegExp('^'
|
|
42
|
+
+ pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
|
|
43
|
+
+ '$');
|
|
44
|
+
return re.test(str);
|
|
45
|
+
}
|
|
46
|
+
async function evalInPage(backend, expression) {
|
|
47
|
+
const res = (await backend.sendCdp('Runtime.evaluate', {
|
|
48
|
+
expression, returnByValue: true, awaitPromise: true,
|
|
49
|
+
}));
|
|
50
|
+
if (res.exceptionDetails) {
|
|
51
|
+
return {
|
|
52
|
+
error: res.exceptionDetails.exception?.description
|
|
53
|
+
?? res.exceptionDetails.text
|
|
54
|
+
?? 'evaluation error',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { value: res.result?.value };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the authoring tools for a host as plain {@link RegisteredTool}s — the
|
|
61
|
+
* same primitive the MCP-only tools use. Each handler resolves the backend via
|
|
62
|
+
* `host.resolveBackend(args)`; the schema merges the host's `sessionSchema`
|
|
63
|
+
* (e.g. `captureId`) and any per-tool `toolExtras`.
|
|
64
|
+
*/
|
|
65
|
+
export function buildAuthoringTools(host) {
|
|
66
|
+
const mk = (spec) => {
|
|
67
|
+
const extra = host.toolExtras?.[spec.name];
|
|
68
|
+
const required = [
|
|
69
|
+
...(host.sessionSchema?.required ?? []),
|
|
70
|
+
...(spec.required ?? []),
|
|
71
|
+
...(extra?.required ?? []),
|
|
72
|
+
];
|
|
73
|
+
const inputSchema = {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
...(host.sessionSchema?.properties ?? {}),
|
|
77
|
+
...spec.properties,
|
|
78
|
+
...(extra?.properties ?? {}),
|
|
79
|
+
},
|
|
80
|
+
...(required.length ? { required } : {}),
|
|
81
|
+
};
|
|
82
|
+
return defineTool({
|
|
83
|
+
name: spec.name,
|
|
84
|
+
description: extra?.description ?? spec.description,
|
|
85
|
+
inputSchema,
|
|
86
|
+
}, (args) => spec.run(host.resolveBackend(args), args));
|
|
87
|
+
};
|
|
88
|
+
return [
|
|
89
|
+
mk({
|
|
90
|
+
name: 'navigate',
|
|
91
|
+
description: 'Navigate the browser tab to a URL, waiting for load.',
|
|
92
|
+
properties: {
|
|
93
|
+
url: { type: 'string' },
|
|
94
|
+
timeoutMs: { type: 'number' },
|
|
95
|
+
},
|
|
96
|
+
required: ['url'],
|
|
97
|
+
run: async (backend, args) => {
|
|
98
|
+
const timeoutMs = Number(args.timeoutMs) || 30_000;
|
|
99
|
+
await backend.sendCdp('Page.enable');
|
|
100
|
+
// Start listening BEFORE navigating so the load event isn't missed.
|
|
101
|
+
const load = backend.waitForEvent('Page.loadEventFired', timeoutMs);
|
|
102
|
+
const nav = (await backend.sendCdp('Page.navigate', { url: String(args.url) }));
|
|
103
|
+
if (nav.errorText) {
|
|
104
|
+
return { ok: false, error: nav.errorText };
|
|
105
|
+
}
|
|
106
|
+
const loadStatus = await load;
|
|
107
|
+
const final = await evalInPage(backend, 'location.href');
|
|
108
|
+
return { ok: true, loadStatus, finalUrl: final.value };
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
mk({
|
|
112
|
+
name: 'eval_in_page',
|
|
113
|
+
description: 'Evaluate a JavaScript expression in the page and return its value '
|
|
114
|
+
+ '(use to read the displayed value, check login state, etc.).',
|
|
115
|
+
properties: { expression: { type: 'string' } },
|
|
116
|
+
required: ['expression'],
|
|
117
|
+
run: (backend, args) => evalInPage(backend, String(args.expression)),
|
|
118
|
+
}),
|
|
119
|
+
mk({
|
|
120
|
+
name: 'wait_for_page',
|
|
121
|
+
description: 'Poll a JavaScript expression until it is truthy or the timeout '
|
|
122
|
+
+ 'elapses (e.g. wait for the user to finish logging in).',
|
|
123
|
+
properties: {
|
|
124
|
+
expression: { type: 'string' },
|
|
125
|
+
timeoutMs: { type: 'number' },
|
|
126
|
+
},
|
|
127
|
+
required: ['expression'],
|
|
128
|
+
run: async (backend, args) => {
|
|
129
|
+
const expr = String(args.expression);
|
|
130
|
+
const deadline = Date.now() + (Number(args.timeoutMs) || 180_000);
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
const { value } = await evalInPage(backend, expr);
|
|
133
|
+
if (value) {
|
|
134
|
+
return { ready: true };
|
|
135
|
+
}
|
|
136
|
+
await sleep(1000);
|
|
137
|
+
}
|
|
138
|
+
return { ready: false };
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
mk({
|
|
142
|
+
name: 'get_cookies',
|
|
143
|
+
description: 'List the NAMES of cookies currently set for the page (values are '
|
|
144
|
+
+ 'NEVER returned). The most reliable, language- and DOM-independent '
|
|
145
|
+
+ 'login signal: after the user logs in, an auth/session cookie '
|
|
146
|
+
+ 'appears (e.g. session/sid/token/auth/jwt/_user/logged_in). '
|
|
147
|
+
+ 'Includes httpOnly cookies the page JS cannot see.',
|
|
148
|
+
properties: {},
|
|
149
|
+
run: async (backend) => {
|
|
150
|
+
const res = (await backend.sendCdp('Network.getCookies'));
|
|
151
|
+
return (res.cookies ?? []).map((c) => ({
|
|
152
|
+
name: c.name,
|
|
153
|
+
...(c.domain ? { domain: c.domain } : {}),
|
|
154
|
+
...(c.session ? { session: true } : {}),
|
|
155
|
+
...(c.httpOnly ? { httpOnly: true } : {}),
|
|
156
|
+
}));
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
mk({
|
|
160
|
+
name: 'list_requests',
|
|
161
|
+
description: 'List captured network requests (method/url/status/size), newest '
|
|
162
|
+
+ 'last. Optionally filter by method / urlGlob / status range / '
|
|
163
|
+
+ 'contentType / graphqlOp. Metadata only — secrets never included.',
|
|
164
|
+
properties: {
|
|
165
|
+
filter: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
method: { type: 'string' },
|
|
169
|
+
urlGlob: { type: 'string' },
|
|
170
|
+
statusMin: { type: 'number' },
|
|
171
|
+
statusMax: { type: 'number' },
|
|
172
|
+
contentType: { type: 'string' },
|
|
173
|
+
graphqlOp: { type: 'string' },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
run: async (backend, args) => {
|
|
178
|
+
const filter = (args.filter ?? {});
|
|
179
|
+
return [...backend.capture.requests.values()]
|
|
180
|
+
.filter((r) => matchesFilter(r, filter))
|
|
181
|
+
.map(publicRequest);
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
mk({
|
|
185
|
+
name: 'find_requests_containing',
|
|
186
|
+
description: 'Search captured responses for any encoding of `value` (raw, '
|
|
187
|
+
+ 'URL/JSON/HTML-escaped, …). Returns a ranked candidate list with '
|
|
188
|
+
+ 'the matched variant, score, and a short snippet — used to locate '
|
|
189
|
+
+ 'the request that returns a target. Server-side; bodies stay here.',
|
|
190
|
+
properties: {
|
|
191
|
+
value: { type: 'string' },
|
|
192
|
+
limit: { type: 'number' },
|
|
193
|
+
},
|
|
194
|
+
required: ['value'],
|
|
195
|
+
run: async (backend, args) => {
|
|
196
|
+
const variants = normalizeCandidates(String(args.value));
|
|
197
|
+
const ranked = [];
|
|
198
|
+
for (const r of backend.capture.requests.values()) {
|
|
199
|
+
if (!r.responseBody) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
let hit;
|
|
203
|
+
for (const v of variants) {
|
|
204
|
+
const idx = r.responseBody.indexOf(v);
|
|
205
|
+
if (idx >= 0) {
|
|
206
|
+
const start = Math.max(0, idx - 40);
|
|
207
|
+
const end = Math.min(r.responseBody.length, idx + v.length + 40);
|
|
208
|
+
hit = {
|
|
209
|
+
snippet: r.responseBody.slice(start, end), variant: v,
|
|
210
|
+
};
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!hit) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
// Prefer JSON responses + smaller bodies (likely API endpoints).
|
|
218
|
+
const score = (r.contentType.includes('json') ? 10 : 0)
|
|
219
|
+
+ Math.max(0, 5 - Math.log10(Math.max(r.size, 1)));
|
|
220
|
+
ranked.push({
|
|
221
|
+
...publicRequest(r),
|
|
222
|
+
matchedVariant: hit.variant,
|
|
223
|
+
snippet: hit.snippet,
|
|
224
|
+
score,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
228
|
+
const limit = Number(args.limit) || 10;
|
|
229
|
+
return { candidates: ranked.slice(0, limit) };
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
mk({
|
|
233
|
+
name: 'propose_provider',
|
|
234
|
+
description: 'Draft a provider from ONE captured request. Pass `targets` (an '
|
|
235
|
+
+ 'array): one entry per value to extract, each with its `value` '
|
|
236
|
+
+ '(verbatim) and a distinct `name` (its param). The provider gets '
|
|
237
|
+
+ 'one match + redaction per target. Returns a draftId + recipe '
|
|
238
|
+
+ '(never secrets). Prefer a SINGLE request holding ALL values. '
|
|
239
|
+
+ 'Single-value shorthand: `target` + `name`. Per-target `hash` '
|
|
240
|
+
+ 'OPRF-hashes that value.',
|
|
241
|
+
properties: {
|
|
242
|
+
requestId: { type: 'string' },
|
|
243
|
+
name: { type: 'string' },
|
|
244
|
+
targets: {
|
|
245
|
+
type: 'array',
|
|
246
|
+
items: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
value: { type: 'string' },
|
|
250
|
+
name: { type: 'string' },
|
|
251
|
+
hash: {
|
|
252
|
+
type: 'string',
|
|
253
|
+
enum: ['oprf-raw', 'oprf', 'oprf-mpc'],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
required: ['value', 'name'],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
target: { type: 'string' },
|
|
260
|
+
hash: { type: 'string', enum: ['oprf-raw', 'oprf', 'oprf-mpc'] },
|
|
261
|
+
},
|
|
262
|
+
required: ['requestId', 'name'],
|
|
263
|
+
run: async (backend, args) => {
|
|
264
|
+
const requestId = String(args.requestId);
|
|
265
|
+
const providerName = String(args.name);
|
|
266
|
+
const defaultHash = args.hash;
|
|
267
|
+
// Explicit multi-value `targets`, else the single `target` shorthand.
|
|
268
|
+
const raw = Array.isArray(args.targets) ? args.targets : [];
|
|
269
|
+
const targets = raw.length
|
|
270
|
+
? raw.map((t) => {
|
|
271
|
+
const o = t;
|
|
272
|
+
return {
|
|
273
|
+
value: String(o.value),
|
|
274
|
+
name: String(o.name),
|
|
275
|
+
...(typeof o.hash === 'string'
|
|
276
|
+
? { hash: o.hash }
|
|
277
|
+
: {}),
|
|
278
|
+
};
|
|
279
|
+
})
|
|
280
|
+
: typeof args.target === 'string'
|
|
281
|
+
? [{
|
|
282
|
+
value: args.target,
|
|
283
|
+
name: providerName,
|
|
284
|
+
...(defaultHash ? { hash: defaultHash } : {}),
|
|
285
|
+
}]
|
|
286
|
+
: [];
|
|
287
|
+
if (targets.length === 0) {
|
|
288
|
+
throw new Error('propose_provider needs a `target` value or a non-empty '
|
|
289
|
+
+ '`targets` array');
|
|
290
|
+
}
|
|
291
|
+
for (const t of targets) {
|
|
292
|
+
if ((t.hash ?? defaultHash) !== undefined
|
|
293
|
+
&& t.value.length > OPRF_MAX_BYTES) {
|
|
294
|
+
throw new Error(`OPRF caps hashed values at ${OPRF_MAX_BYTES} bytes; got `
|
|
295
|
+
+ `${t.value.length}-byte "${t.value}". Use a tighter `
|
|
296
|
+
+ 'target or drop `hash`.');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const captured = backend.capture.requests.get(requestId);
|
|
300
|
+
if (!captured) {
|
|
301
|
+
throw new Error(`no captured request ${requestId}`);
|
|
302
|
+
}
|
|
303
|
+
if (!captured.responseBody) {
|
|
304
|
+
throw new Error(`request body unavailable for ${requestId}`);
|
|
305
|
+
}
|
|
306
|
+
const { provider, secretRefs, extractions } = draftProvider(captured, targets, providerName, defaultHash);
|
|
307
|
+
// Materialize the real secret header values for later replay/prove;
|
|
308
|
+
// they stay in the server-side draft, never in this result.
|
|
309
|
+
const secrets = {};
|
|
310
|
+
for (const ref of secretRefs) {
|
|
311
|
+
const real = captured.requestHeaders[ref.name];
|
|
312
|
+
if (real !== undefined) {
|
|
313
|
+
secrets[ref.name] = real;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const draftId = backend.drafts.put({
|
|
317
|
+
provider, secretRefs, secrets, requestId, extractions,
|
|
318
|
+
});
|
|
319
|
+
const warnings = verificationWarnings(provider);
|
|
320
|
+
return {
|
|
321
|
+
draftId,
|
|
322
|
+
provider,
|
|
323
|
+
secretRefs,
|
|
324
|
+
...(warnings.length ? { verificationWarnings: warnings } : {}),
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
mk({
|
|
329
|
+
name: 'replay_request',
|
|
330
|
+
description: 'Validate the drafted recipe before proving — confirm its selectors '
|
|
331
|
+
+ 'extract the target value. Returns diagnostics; run before '
|
|
332
|
+
+ 'proving. Set `withoutSecrets: true` for the AUTH-BOUND check: it '
|
|
333
|
+
+ 're-issues the request with NO secrets/cookies (an anonymous '
|
|
334
|
+
+ 'visitor). If the values still extract (valid:true), the endpoint '
|
|
335
|
+
+ 'is PUBLIC and the proof would not bind to the user — reject it and '
|
|
336
|
+
+ 'find an auth-bound request instead.',
|
|
337
|
+
properties: {
|
|
338
|
+
draftId: { type: 'string' },
|
|
339
|
+
withoutSecrets: { type: 'boolean' },
|
|
340
|
+
},
|
|
341
|
+
required: ['draftId'],
|
|
342
|
+
run: async (backend, args) => {
|
|
343
|
+
const draft = backend.drafts.get(String(args.draftId));
|
|
344
|
+
if (!draft) {
|
|
345
|
+
throw new Error(`no draft ${String(args.draftId)}`);
|
|
346
|
+
}
|
|
347
|
+
return backend.replay(draft, {
|
|
348
|
+
withoutSecrets: Boolean(args.withoutSecrets),
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
mk({
|
|
353
|
+
name: 'run_proof',
|
|
354
|
+
description: 'Prove the drafted recipe (zkTLS). Only call after replay_request '
|
|
355
|
+
+ 'looks right. Returns the proof result.',
|
|
356
|
+
properties: { draftId: { type: 'string' } },
|
|
357
|
+
required: ['draftId'],
|
|
358
|
+
// The backend shapes its own result: the cloud returns a lean,
|
|
359
|
+
// secret-free summary (and records the proven recipe for publish); the
|
|
360
|
+
// MCP persists the claim + returns the rich developer view. The MCP adds
|
|
361
|
+
// `ownerAddress`/`attestorUrl` to the schema via the host's toolExtras.
|
|
362
|
+
run: async (backend, args) => {
|
|
363
|
+
const draft = backend.drafts.get(String(args.draftId));
|
|
364
|
+
if (!draft) {
|
|
365
|
+
throw new Error(`no draft ${String(args.draftId)}`);
|
|
366
|
+
}
|
|
367
|
+
const opts = {};
|
|
368
|
+
if (typeof args.ownerAddress === 'string') {
|
|
369
|
+
opts.ownerAddress = args.ownerAddress;
|
|
370
|
+
}
|
|
371
|
+
if (typeof args.attestorUrl === 'string') {
|
|
372
|
+
opts.attestorUrl = args.attestorUrl;
|
|
373
|
+
}
|
|
374
|
+
return backend.prove(draft, opts);
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
];
|
|
378
|
+
}
|
|
@@ -45,7 +45,7 @@ export function isStaticAsset(url) {
|
|
|
45
45
|
path = new URL(url).pathname;
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
48
|
-
path = url.split(/[?#]/)[0]
|
|
48
|
+
path = url.split(/[?#]/)[0] || url;
|
|
49
49
|
}
|
|
50
50
|
// Extension of the last path segment only, so `/v1.2/data` isn't misread.
|
|
51
51
|
const base = path.slice(path.lastIndexOf('/') + 1);
|