@reclaimprotocol/agent 0.1.3 → 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.
Files changed (58) hide show
  1. package/README.md +11 -42
  2. package/lib/authoring/backend.d.ts +95 -0
  3. package/lib/authoring/backend.js +24 -0
  4. package/lib/authoring/index.d.ts +4 -0
  5. package/lib/authoring/index.js +5 -0
  6. package/lib/authoring/tools.d.ts +9 -0
  7. package/lib/authoring/tools.js +378 -0
  8. package/lib/cdp/capture-engine.d.ts +7 -0
  9. package/lib/cdp/capture-engine.js +78 -0
  10. package/lib/cdp/capture.d.ts +1 -1
  11. package/lib/cdp/capture.js +1 -1
  12. package/lib/consts.d.ts +4 -2
  13. package/lib/consts.js +6 -2
  14. package/lib/mcp/api/operations.gen.js +46 -18
  15. package/lib/mcp/instructions.d.ts +2 -0
  16. package/lib/mcp/instructions.js +16 -0
  17. package/lib/mcp/server.d.ts +18 -2
  18. package/lib/mcp/server.js +19 -4
  19. package/lib/mcp/start.js +2 -1
  20. package/lib/mcp/tools/agent/build.js +4 -0
  21. package/lib/mcp/tools/agent/how-it-works.d.ts +11 -0
  22. package/lib/mcp/tools/agent/how-it-works.js +24 -0
  23. package/lib/mcp/tools/agent/publish.js +2 -2
  24. package/lib/mcp/tools/agent/shared.d.ts +6 -0
  25. package/lib/mcp/tools/agent/shared.js +115 -0
  26. package/lib/old/client.d.ts +10 -0
  27. package/lib/old/client.js +27 -0
  28. package/lib/old/login-cdp.d.ts +5 -0
  29. package/lib/old/login-cdp.js +12 -1
  30. package/lib/old/tools/authenticate.js +7 -3
  31. package/lib/old/tools/build.d.ts +2 -2
  32. package/lib/old/tools/build.js +4 -2
  33. package/lib/old/tools/session-analytics-logs.d.ts +9 -0
  34. package/lib/old/tools/session-analytics-logs.js +25 -0
  35. package/lib/proof/proof-core.d.ts +1 -1
  36. package/lib/proof/proof-core.js +1 -1
  37. package/lib/tools/registry.d.ts +36 -0
  38. package/lib/tools/registry.js +17 -0
  39. package/package.json +2 -3
  40. package/skill/{SKILL.md → SKILL.builder.md} +16 -45
  41. package/skill/SKILL.old.md +225 -0
  42. package/skill/old-mode.md +2 -0
  43. package/src/cdp/capture-engine.ts +89 -0
  44. package/src/cdp/capture.ts +5 -1
  45. package/src/consts.ts +7 -2
  46. package/src/mcp/api/operations.gen.ts +46 -18
  47. package/src/mcp/instructions.ts +18 -0
  48. package/src/mcp/server.ts +43 -6
  49. package/src/mcp/start.ts +2 -0
  50. package/src/mcp/tools/agent/build.ts +4 -0
  51. package/src/mcp/tools/agent/how-it-works.ts +32 -0
  52. package/src/mcp/tools/agent/publish.ts +2 -2
  53. package/src/old/client.ts +32 -0
  54. package/src/old/login-cdp.ts +20 -1
  55. package/src/old/tools/authenticate.ts +11 -3
  56. package/src/old/tools/build.ts +4 -2
  57. package/src/old/tools/session-analytics-logs.ts +33 -0
  58. 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 run downloads the ZK circuit files needed by `run_proof` (a `postinstall` step), so allow a moment on first launch.
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
 
@@ -111,13 +111,13 @@ Defaults target the production old-devtools backend, so **no env is needed for t
111
111
 
112
112
  See [Backends](#backends) for builder mode (`USE_OLD_DEVTOOLS=false`) and the full env-var list.
113
113
 
114
- ### Companion skill (optional, recommended)
114
+ ### Provider-authoring guide
115
115
 
116
- The MCP server is all you need to start — but installing the companion skill makes the agent markedly better at the capture → prove → publish flow (login detection, choosing auth-bound endpoints, OPRF decisions, troubleshooting). See [Skill](#skill) for install steps.
116
+ The MCP server is all you need — it ships a `how_it_works` tool that returns the full capture → prove → publish guide (login detection, choosing auth-bound endpoints, OPRF decisions, troubleshooting). Any MCP client can call it on demand; there's nothing extra to install.
117
117
 
118
118
  ## Using it from your agent
119
119
 
120
- Optional but recommended: install the [skill](#skill) so the agent follows the full authoring flow.
120
+ Tip: have your agent call the `how_it_works` tool first so it follows the full authoring flow.
121
121
 
122
122
  Then just ask in plain language, e.g.:
123
123
 
@@ -138,28 +138,9 @@ The agent orchestrates the tools for you:
138
138
 
139
139
  Your only manual steps are signing in when the browser windows open and confirming the value you want to prove.
140
140
 
141
- ## Skill
141
+ ## How it works (guide tool)
142
142
 
143
- Optional but recommended. The companion skill (`skill/SKILL.md`) is a Claude Code **Agent Skill** that teaches the agent the full mode-aware authoring flow — it ships inside the npm package and is also in this repo.
144
-
145
- ```bash
146
- # macOS / Linux — from a global install of the package
147
- npm i -g @reclaimprotocol/agent
148
- mkdir -p ~/.claude/skills/reclaim-provider
149
- cp "$(npm root -g)/@reclaimprotocol/agent/skill/SKILL.md" ~/.claude/skills/reclaim-provider/SKILL.md
150
-
151
- # …or from a checkout of this repo
152
- cp packages/agent/skill/SKILL.md ~/.claude/skills/reclaim-provider/SKILL.md
153
- ```
154
-
155
- ```powershell
156
- # Windows (PowerShell) — from a global install
157
- npm i -g @reclaimprotocol/agent
158
- New-Item -ItemType Directory -Force $env:USERPROFILE\.claude\skills\reclaim-provider | Out-Null
159
- Copy-Item "$(npm root -g)\@reclaimprotocol\agent\skill\SKILL.md" $env:USERPROFILE\.claude\skills\reclaim-provider\SKILL.md
160
- ```
161
-
162
- Claude Code auto-loads it once it's in `~/.claude/skills/`. **Other MCP clients** (Cursor, opencode, Antigravity, …) don't read `SKILL.md` automatically — the server's built-in instructions already cover the essentials, and you can paste or adapt the skill's content into that client's rules / system-prompt for the same depth.
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.
163
144
 
164
145
  ## Provider-authoring tools
165
146
 
@@ -194,13 +175,13 @@ Relevant env vars: `RECLAIM_OLD_API_URL` (default `https://devapi.reclaimprotoco
194
175
 
195
176
  ## Local development (from source)
196
177
 
197
- For working on the package inside the monorepo (consumers should use [Install](#install) instead). A workspace install fetches dependencies and runs this package's postinstall, which downloads the ZK circuit resources needed by `@reclaimprotocol/attestor-core` at proof time:
178
+ For working on the package inside the monorepo (consumers should use [Install](#install) instead). A workspace install just fetches dependencies:
198
179
 
199
180
  ```bash
200
181
  npm install
201
182
  ```
202
183
 
203
- If postinstall is skipped (`npm ci --ignore-scripts`, or a corp policy disabling postinstall), run it manually:
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:
204
185
 
205
186
  ```bash
206
187
  npm run download:zk-files --workspace=packages/agent
@@ -208,7 +189,7 @@ npm run download:zk-files --workspace=packages/agent
208
189
 
209
190
  ## Platform notes
210
191
 
211
- - **Node >=22.18** is required (declared in `engines`). The floor is 22.18 because the package's `postinstall` and helper scripts run as bare `.ts` files, and unflagged TypeScript type-stripping landed in Node 22.18.
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.
212
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`.
213
194
 
214
195
  ## Tests
@@ -285,18 +266,6 @@ claude mcp add reclaim -- node "$(pwd)/packages/agent/lib/mcp-bin.js"
285
266
 
286
267
  To target a specific backend while developing, add an `env` block (Claude Code / `.mcp.json`) or `environment` (opencode) — e.g. `"USE_OLD_DEVTOOLS": "false"` for builder mode. See [Backends](#backends).
287
268
 
288
- ### Skill
289
-
290
- Symlink the source `skill/` folder into `~/.claude/skills/` so edits to `SKILL.md` are picked up live (no re-copy):
291
-
292
- ```bash
293
- # macOS / Linux — from the repo root
294
- ln -s "$(pwd)/packages/agent/skill" ~/.claude/skills/reclaim-provider
295
- ```
296
-
297
- ```powershell
298
- # Windows (PowerShell, Developer Mode or elevated) — from the repo root
299
- New-Item -ItemType SymbolicLink -Path $env:USERPROFILE\.claude\skills\reclaim-provider -Target (Resolve-Path packages\agent\skill)
300
- ```
269
+ ### Authoring guide
301
270
 
302
- Claude Code auto-loads it once linked. opencode doesn't read `SKILL.md` automatically — paste or adapt its content into that client's rules / system-prompt for the same depth.
271
+ `skill/SKILL.builder.md` and `skill/SKILL.old.md` are the sources the `how_it_works` tool serves — one per backend, so an agent is never shown tools its mode doesn't have. The server picks by `USE_OLD_DEVTOOLS`. Edit the relevant file and rebuild (`npm run build`) to update what the tool returns.
@@ -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
+ }
@@ -30,6 +30,13 @@ export interface ResponseReceivedLike {
30
30
  * `:`. Stripping at capture time keeps every downstream consumer
31
31
  * (`get_request`, draft, replay, proof) clean by construction. */
32
32
  export declare function stripPseudoHeaders(headers: Record<string, string>): Record<string, string>;
33
+ /** True when the URL's path names a static asset (by file extension; query and
34
+ * hash are ignored, so `chunk.js?v=1` still counts). The capture engine drops
35
+ * these up front — they never carry JSON/HTML a recipe matches. */
36
+ export declare function isStaticAsset(url: string): boolean;
37
+ /** True when a response Content-Type is a static asset (scripts/styles/fonts/
38
+ * images/media). Parameters (`; charset=…`) and case are ignored. */
39
+ export declare function isStaticAssetMime(mimeType: string | undefined): boolean;
33
40
  export interface CaptureSession {
34
41
  captureId: string;
35
42
  requests: Map<string, CapturedRequest>;