@reclaimprotocol/agent 0.1.0-dev.1

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 (191) hide show
  1. package/README.md +287 -0
  2. package/lib/cdp/capture.d.ts +22 -0
  3. package/lib/cdp/capture.js +159 -0
  4. package/lib/cdp/client.d.ts +9 -0
  5. package/lib/cdp/client.js +11 -0
  6. package/lib/cdp/eviction.d.ts +16 -0
  7. package/lib/cdp/eviction.js +38 -0
  8. package/lib/cdp/launcher.d.ts +14 -0
  9. package/lib/cdp/launcher.js +102 -0
  10. package/lib/cdp/tabs.d.ts +6 -0
  11. package/lib/cdp/tabs.js +10 -0
  12. package/lib/cli/args.d.ts +14 -0
  13. package/lib/cli/args.js +19 -0
  14. package/lib/cli/commands.d.ts +1 -0
  15. package/lib/cli/commands.js +19 -0
  16. package/lib/cli.d.ts +1 -0
  17. package/lib/cli.js +7 -0
  18. package/lib/config/store.d.ts +45 -0
  19. package/lib/config/store.js +46 -0
  20. package/lib/index.d.ts +3 -0
  21. package/lib/index.js +7 -0
  22. package/lib/logger.d.ts +2 -0
  23. package/lib/logger.js +7 -0
  24. package/lib/mcp/api/operations.gen.d.ts +152 -0
  25. package/lib/mcp/api/operations.gen.js +2300 -0
  26. package/lib/mcp/api/tools.d.ts +3 -0
  27. package/lib/mcp/api/tools.js +36 -0
  28. package/lib/mcp/remap_errors.d.ts +2 -0
  29. package/lib/mcp/remap_errors.js +27 -0
  30. package/lib/mcp/server.d.ts +38 -0
  31. package/lib/mcp/server.js +76 -0
  32. package/lib/mcp/start.d.ts +1 -0
  33. package/lib/mcp/start.js +63 -0
  34. package/lib/mcp/tools/agent/analyze.d.ts +3 -0
  35. package/lib/mcp/tools/agent/analyze.js +26 -0
  36. package/lib/mcp/tools/agent/attach.d.ts +11 -0
  37. package/lib/mcp/tools/agent/attach.js +72 -0
  38. package/lib/mcp/tools/agent/build.d.ts +33 -0
  39. package/lib/mcp/tools/agent/build.js +74 -0
  40. package/lib/mcp/tools/agent/capture.d.ts +10 -0
  41. package/lib/mcp/tools/agent/capture.js +142 -0
  42. package/lib/mcp/tools/agent/eval.d.ts +5 -0
  43. package/lib/mcp/tools/agent/eval.js +127 -0
  44. package/lib/mcp/tools/agent/find.d.ts +3 -0
  45. package/lib/mcp/tools/agent/find.js +60 -0
  46. package/lib/mcp/tools/agent/proof.d.ts +3 -0
  47. package/lib/mcp/tools/agent/proof.js +214 -0
  48. package/lib/mcp/tools/agent/propose.d.ts +9 -0
  49. package/lib/mcp/tools/agent/propose.js +75 -0
  50. package/lib/mcp/tools/agent/publish.d.ts +3 -0
  51. package/lib/mcp/tools/agent/publish.js +122 -0
  52. package/lib/mcp/tools/agent/replay.d.ts +3 -0
  53. package/lib/mcp/tools/agent/replay.js +46 -0
  54. package/lib/mcp/tools/agent/session.d.ts +10 -0
  55. package/lib/mcp/tools/agent/session.js +29 -0
  56. package/lib/mcp/tools/credentials/authenticate.d.ts +3 -0
  57. package/lib/mcp/tools/credentials/authenticate.js +108 -0
  58. package/lib/mcp/tools/credentials/build.d.ts +16 -0
  59. package/lib/mcp/tools/credentials/build.js +33 -0
  60. package/lib/mcp/tools/credentials/cache.d.ts +70 -0
  61. package/lib/mcp/tools/credentials/cache.js +114 -0
  62. package/lib/mcp/tools/credentials/env-file.d.ts +25 -0
  63. package/lib/mcp/tools/credentials/env-file.js +71 -0
  64. package/lib/mcp/tools/credentials/import.d.ts +2 -0
  65. package/lib/mcp/tools/credentials/import.js +52 -0
  66. package/lib/mcp/tools/credentials/issue.d.ts +2 -0
  67. package/lib/mcp/tools/credentials/issue.js +84 -0
  68. package/lib/mcp/tools/credentials/list-cached.d.ts +2 -0
  69. package/lib/mcp/tools/credentials/list-cached.js +55 -0
  70. package/lib/mcp/tools/credentials/register.d.ts +18 -0
  71. package/lib/mcp/tools/credentials/register.js +30 -0
  72. package/lib/mcp/tools/credentials/resolve-owner-key.d.ts +12 -0
  73. package/lib/mcp/tools/credentials/resolve-owner-key.js +68 -0
  74. package/lib/mcp/tools/credentials/session-cache.d.ts +14 -0
  75. package/lib/mcp/tools/credentials/session-cache.js +19 -0
  76. package/lib/mcp/tools/credentials/status.d.ts +3 -0
  77. package/lib/mcp/tools/credentials/status.js +62 -0
  78. package/lib/mcp-bin.d.ts +1 -0
  79. package/lib/mcp-bin.js +7 -0
  80. package/lib/old/client.d.ts +67 -0
  81. package/lib/old/client.js +146 -0
  82. package/lib/old/flag.d.ts +11 -0
  83. package/lib/old/flag.js +13 -0
  84. package/lib/old/identity.d.ts +22 -0
  85. package/lib/old/identity.js +47 -0
  86. package/lib/old/instructions.d.ts +7 -0
  87. package/lib/old/instructions.js +29 -0
  88. package/lib/old/login-cdp.d.ts +23 -0
  89. package/lib/old/login-cdp.js +125 -0
  90. package/lib/old/to-register.d.ts +47 -0
  91. package/lib/old/to-register.js +133 -0
  92. package/lib/old/tools/authenticate.d.ts +18 -0
  93. package/lib/old/tools/authenticate.js +127 -0
  94. package/lib/old/tools/build.d.ts +9 -0
  95. package/lib/old/tools/build.js +16 -0
  96. package/lib/old/tools/me-providers.d.ts +10 -0
  97. package/lib/old/tools/me-providers.js +32 -0
  98. package/lib/old/tools/publish.d.ts +16 -0
  99. package/lib/old/tools/publish.js +97 -0
  100. package/lib/paths.d.ts +11 -0
  101. package/lib/paths.js +32 -0
  102. package/lib/pkg.d.ts +3 -0
  103. package/lib/pkg.js +13 -0
  104. package/lib/proof/attestor.d.ts +39 -0
  105. package/lib/proof/attestor.js +207 -0
  106. package/lib/proof/replay.d.ts +8 -0
  107. package/lib/proof/replay.js +73 -0
  108. package/lib/provider/constraints.d.ts +2 -0
  109. package/lib/provider/constraints.js +122 -0
  110. package/lib/provider/draft.d.ts +6 -0
  111. package/lib/provider/draft.js +68 -0
  112. package/lib/provider/graphql.d.ts +2 -0
  113. package/lib/provider/graphql.js +35 -0
  114. package/lib/provider/matcher.d.ts +36 -0
  115. package/lib/provider/matcher.js +436 -0
  116. package/lib/provider/normalize.d.ts +4 -0
  117. package/lib/provider/normalize.js +51 -0
  118. package/lib/provider/schema.d.ts +76 -0
  119. package/lib/provider/schema.js +1 -0
  120. package/lib/provider/secrets.d.ts +5 -0
  121. package/lib/provider/secrets.js +22 -0
  122. package/lib/provider/to-version.d.ts +22 -0
  123. package/lib/provider/to-version.js +68 -0
  124. package/lib/types/openapi.d.ts +4 -0
  125. package/lib/types/openapi.js +1 -0
  126. package/package.json +88 -0
  127. package/scripts/download-zk-files.ts +42 -0
  128. package/scripts/prepend-shebang.ts +17 -0
  129. package/skill/SKILL.md +248 -0
  130. package/src/cdp/capture.ts +215 -0
  131. package/src/cdp/client.ts +20 -0
  132. package/src/cdp/eviction.ts +52 -0
  133. package/src/cdp/launcher.ts +133 -0
  134. package/src/cdp/tabs.ts +22 -0
  135. package/src/cli/args.ts +20 -0
  136. package/src/cli/commands.ts +27 -0
  137. package/src/cli.ts +7 -0
  138. package/src/config/store.ts +86 -0
  139. package/src/index.ts +12 -0
  140. package/src/logger.ts +8 -0
  141. package/src/mcp/api/operations.gen.ts +2397 -0
  142. package/src/mcp/api/tools.ts +45 -0
  143. package/src/mcp/remap_errors.ts +35 -0
  144. package/src/mcp/server.ts +119 -0
  145. package/src/mcp/start.ts +78 -0
  146. package/src/mcp/tools/agent/analyze.ts +34 -0
  147. package/src/mcp/tools/agent/attach.ts +95 -0
  148. package/src/mcp/tools/agent/build.ts +99 -0
  149. package/src/mcp/tools/agent/capture.ts +202 -0
  150. package/src/mcp/tools/agent/eval.ts +161 -0
  151. package/src/mcp/tools/agent/find.ts +73 -0
  152. package/src/mcp/tools/agent/proof.ts +272 -0
  153. package/src/mcp/tools/agent/propose.ts +111 -0
  154. package/src/mcp/tools/agent/publish.ts +155 -0
  155. package/src/mcp/tools/agent/replay.ts +58 -0
  156. package/src/mcp/tools/agent/session.ts +46 -0
  157. package/src/mcp/tools/credentials/authenticate.ts +135 -0
  158. package/src/mcp/tools/credentials/build.ts +37 -0
  159. package/src/mcp/tools/credentials/cache.ts +161 -0
  160. package/src/mcp/tools/credentials/env-file.ts +101 -0
  161. package/src/mcp/tools/credentials/import.ts +72 -0
  162. package/src/mcp/tools/credentials/issue.ts +107 -0
  163. package/src/mcp/tools/credentials/list-cached.ts +66 -0
  164. package/src/mcp/tools/credentials/register.ts +49 -0
  165. package/src/mcp/tools/credentials/resolve-owner-key.ts +78 -0
  166. package/src/mcp/tools/credentials/session-cache.ts +31 -0
  167. package/src/mcp/tools/credentials/status.ts +82 -0
  168. package/src/mcp-bin.ts +7 -0
  169. package/src/old/client.ts +236 -0
  170. package/src/old/flag.ts +13 -0
  171. package/src/old/identity.ts +56 -0
  172. package/src/old/instructions.ts +30 -0
  173. package/src/old/login-cdp.ts +153 -0
  174. package/src/old/to-register.ts +234 -0
  175. package/src/old/tools/authenticate.ts +152 -0
  176. package/src/old/tools/build.ts +21 -0
  177. package/src/old/tools/me-providers.ts +42 -0
  178. package/src/old/tools/publish.ts +135 -0
  179. package/src/paths.ts +37 -0
  180. package/src/pkg.ts +24 -0
  181. package/src/proof/attestor.ts +286 -0
  182. package/src/proof/replay.ts +102 -0
  183. package/src/provider/constraints.ts +150 -0
  184. package/src/provider/draft.ts +105 -0
  185. package/src/provider/graphql.ts +42 -0
  186. package/src/provider/matcher.ts +616 -0
  187. package/src/provider/normalize.ts +65 -0
  188. package/src/provider/schema.ts +92 -0
  189. package/src/provider/secrets.ts +29 -0
  190. package/src/provider/to-version.ts +90 -0
  191. package/src/types/openapi.ts +7 -0
@@ -0,0 +1,76 @@
1
+ import type { ProviderParams } from '@reclaimprotocol/attestor-core';
2
+ /** The attestor's HTTP provider parameters — the canonical source for the
3
+ * request/match/redaction shapes we draft. */
4
+ type HttpParams = ProviderParams<'http'>;
5
+ /** HTTP methods the attestor accepts. */
6
+ export type HttpMethod = HttpParams['method'];
7
+ /**
8
+ * A pattern the attestor checks against the revealed slice of the response.
9
+ * The prover selectively reveals portions of the TLS response (via
10
+ * responseRedactions), so the attestor only ever sees a substring —
11
+ * jsonPath/xPath belong on the redaction, not here.
12
+ */
13
+ export type ResponseMatch = HttpParams['responseMatches'][number];
14
+ /**
15
+ * Selector telling the prover which portion of the response to reveal:
16
+ * `xPath`/`jsonPath` narrows the slice, `regex` (with named groups) matches
17
+ * within, and `hash` applies an OPRF so the attestor sees a nullifier
18
+ * instead of the cleartext value.
19
+ */
20
+ export type ResponseRedaction = NonNullable<HttpParams['responseRedactions']>[number];
21
+ /**
22
+ * OPRF mode for hashed redactions. `oprf-raw` is what the dev-loop defaults
23
+ * to when privacy is requested — the only variant that runs without external
24
+ * deps (gnark for `oprf`, a TEE for `oprf-mpc`). It is the weakest of the
25
+ * three (the attestor participates in hashing) and is meant to be swapped
26
+ * out before production.
27
+ */
28
+ export type OprfMode = NonNullable<ResponseRedaction['hash']>;
29
+ /** A single captured request + response, as we store it server-side. */
30
+ export interface CapturedRequest {
31
+ requestId: string;
32
+ method: string;
33
+ url: string;
34
+ status: number;
35
+ contentType: string;
36
+ size: number;
37
+ requestHeaders: Record<string, string>;
38
+ requestBody?: string;
39
+ responseHeaders: Record<string, string>;
40
+ responseBody?: string;
41
+ /** Parsed GraphQL operationName if request body is a GraphQL query. */
42
+ graphqlOp?: string;
43
+ /** ms since epoch when the response finished. */
44
+ finishedAt: number;
45
+ }
46
+ /**
47
+ * Reclaim provider JSON shape we draft, then translate to the attestor's HTTP
48
+ * params (proof/attestor.ts) and the API version request (to-version.ts). The
49
+ * request fields derive from the attestor's own `HttpProviderParameters`, so
50
+ * the method enum and match/redaction shapes can't drift from what the
51
+ * attestor accepts. `body` is narrowed to `string` (the SDK also allows
52
+ * `Uint8Array`, but we only ever template text bodies).
53
+ */
54
+ export interface ReclaimProvider {
55
+ name: string;
56
+ url: string;
57
+ method: HttpMethod;
58
+ headers?: Record<string, string>;
59
+ body?: string;
60
+ responseMatches: ResponseMatch[];
61
+ responseRedactions?: ResponseRedaction[];
62
+ /** Public param refs (templated into URL/body), populated by draft layer. */
63
+ paramValues?: Record<string, string>;
64
+ }
65
+ /** A reference to a secret param. Raw value lives only in server memory. */
66
+ export interface SecretParamRef {
67
+ name: string;
68
+ source: 'header' | 'cookie' | 'query';
69
+ redacted: true;
70
+ }
71
+ /** Concerns flagged about a captured request. Informational, never blocking. */
72
+ export interface RequestConcern {
73
+ code: 'signedRequest' | 'wasmSigned' | 'botChallenge' | 'csrfDependency' | 'shortLivedToken' | 'encryptedBody' | 'graphql';
74
+ message: string;
75
+ }
76
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare function classifyHeader(name: string): boolean;
2
+ export declare function splitSecrets(headers: Record<string, string>): {
3
+ secrets: Record<string, string>;
4
+ publics: Record<string, string>;
5
+ };
@@ -0,0 +1,22 @@
1
+ const SECRET_EXACT = new Set(['cookie', 'authorization', 'proxy-authorization']);
2
+ const SECRET_SUFFIXES = ['-token', '-key', '-secret', '-auth'];
3
+ export function classifyHeader(name) {
4
+ const lower = name.toLowerCase();
5
+ if (SECRET_EXACT.has(lower)) {
6
+ return true;
7
+ }
8
+ return SECRET_SUFFIXES.some((s) => lower.endsWith(s));
9
+ }
10
+ export function splitSecrets(headers) {
11
+ const secrets = {};
12
+ const publics = {};
13
+ for (const [k, v] of Object.entries(headers)) {
14
+ if (classifyHeader(k)) {
15
+ secrets[k] = v;
16
+ }
17
+ else {
18
+ publics[k] = v;
19
+ }
20
+ }
21
+ return { secrets, publics };
22
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReclaimClient } from '@reclaimprotocol/client';
2
+ import type { CreateProviderVersionRequest as CreateVersionBody, ProviderVersionRef as VersionRef } from '../types/openapi.ts';
3
+ import type { ReclaimProvider } from './schema.ts';
4
+ /**
5
+ * Translate a drafted ReclaimProvider into the body for
6
+ * POST /providers/{providerId}/versions.
7
+ *
8
+ * `initialUrl` is the landing page the user navigated to during
9
+ * capture — distinct from `provider.url` which is the specific
10
+ * API URL the attestor replays.
11
+ *
12
+ * Secrets are NOT persisted — the verification client re-supplies
13
+ * them at runtime via `credentials: 'include'`.
14
+ */
15
+ export declare function providerToCreateVersionRequest(provider: ReclaimProvider, initialUrl: string, notes?: string, version?: VersionRef): CreateVersionBody;
16
+ /**
17
+ * Fetch the current versions for a provider and return the next version above
18
+ * the highest existing one (bumping the patch number), or 1.0.0 when the
19
+ * provider has no versions yet. A failed lookup propagates — silently
20
+ * resetting to 1.0.0 would re-publish a v3 provider as v1.0.0.
21
+ */
22
+ export declare function nextVersion(client: ReclaimClient, providerId: string): Promise<VersionRef>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Translate a drafted ReclaimProvider into the body for
3
+ * POST /providers/{providerId}/versions.
4
+ *
5
+ * `initialUrl` is the landing page the user navigated to during
6
+ * capture — distinct from `provider.url` which is the specific
7
+ * API URL the attestor replays.
8
+ *
9
+ * Secrets are NOT persisted — the verification client re-supplies
10
+ * them at runtime via `credentials: 'include'`.
11
+ */
12
+ export function providerToCreateVersionRequest(provider, initialUrl, notes, version) {
13
+ // Inline responseRedactions into matches[*].extract (1-to-1 by index).
14
+ const matches = (provider.responseMatches ?? []).map((match, i) => {
15
+ const redaction = provider.responseRedactions?.[i];
16
+ return {
17
+ type: match.type,
18
+ value: match.value,
19
+ isOptional: false,
20
+ ...(redaction ? { extract: redaction } : {}),
21
+ };
22
+ });
23
+ return {
24
+ version: version ?? { major: 1, minor: 0, patch: 0 },
25
+ initialUrl,
26
+ requests: [
27
+ {
28
+ url: provider.url,
29
+ method: provider.method,
30
+ credentials: 'include',
31
+ ...(provider.body ? { requestBodyTemplate: provider.body } : {}),
32
+ ...(matches.length > 0 ? { matches } : {}),
33
+ },
34
+ ],
35
+ allowedJsRequests: [],
36
+ webSettings: { geoLocation: '{{DYNAMIC_GEO}}' },
37
+ ...(provider.paramValues
38
+ ? { requiredParameters: provider.paramValues }
39
+ : {}),
40
+ ...(notes ? { notes } : {}),
41
+ };
42
+ }
43
+ /**
44
+ * Fetch the current versions for a provider and return the next version above
45
+ * the highest existing one (bumping the patch number), or 1.0.0 when the
46
+ * provider has no versions yet. A failed lookup propagates — silently
47
+ * resetting to 1.0.0 would re-publish a v3 provider as v1.0.0.
48
+ */
49
+ export async function nextVersion(client, providerId) {
50
+ const list = (await client.call('ListProviderVersions', {
51
+ params: { providerId },
52
+ query: {},
53
+ }));
54
+ if (!list.items?.length) {
55
+ return { major: 1, minor: 0, patch: 0 };
56
+ }
57
+ // Find the highest version and bump its patch
58
+ let hi = list.items[0].version;
59
+ for (const item of list.items) {
60
+ const v = item.version;
61
+ if (v.major > hi.major ||
62
+ (v.major === hi.major && v.minor > hi.minor) ||
63
+ (v.major === hi.major && v.minor === hi.minor && v.patch > hi.patch)) {
64
+ hi = v;
65
+ }
66
+ }
67
+ return { major: hi.major, minor: hi.minor, patch: hi.patch + 1 };
68
+ }
@@ -0,0 +1,4 @@
1
+ import type { components } from '@reclaimprotocol/client';
2
+ export type CredentialAttachSession = components['schemas']['CredentialAttachSession'];
3
+ export type CreateProviderVersionRequest = components['schemas']['CreateProviderVersionRequest'];
4
+ export type ProviderVersionRef = components['schemas']['ProviderVersionRef'];
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@reclaimprotocol/agent",
3
+ "title": "Reclaim Agent",
4
+ "version": "0.1.0-dev.1",
5
+ "description": "Reclaim Protocol agent — SDK, CLI, and MCP server in one package. SDK + CLI talk to the builder backend; MCP server adds Chrome CDP capture, provider synthesis, replay, and zkTLS proof.",
6
+ "type": "module",
7
+ "main": "./lib/index.js",
8
+ "types": "./lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "import": "./lib/index.js"
13
+ },
14
+ "./mcp/register": {
15
+ "types": "./lib/mcp/register.d.ts",
16
+ "import": "./lib/mcp/register.js"
17
+ },
18
+ "./skill": "./skill/SKILL.md"
19
+ },
20
+ "bin": {
21
+ "reclaim": "./lib/cli.js",
22
+ "reclaim-mcp-server": "./lib/mcp-bin.js"
23
+ },
24
+ "files": [
25
+ "lib",
26
+ "src",
27
+ "scripts",
28
+ "skill",
29
+ "README.md"
30
+ ],
31
+ "engines": {
32
+ "node": ">=22.18"
33
+ },
34
+ "repository": {
35
+ "url": "https://github.com/reclaimprotocol/builder",
36
+ "type": "git"
37
+ },
38
+ "websiteUrl": "https://docs.reclaimprotocol.org",
39
+ "scripts": {
40
+ "build": "tsc && node scripts/prepend-shebang.ts && chmod 755 lib/cli.js lib/mcp-bin.js",
41
+ "openapi:typegen": "node ../app/scripts/mcp-tools-gen.ts",
42
+ "prepublishOnly": "npm run build",
43
+ "dev": "tsc --watch",
44
+ "test": "node --test tests/*.test.ts",
45
+ "start:cli": "node lib/cli.js",
46
+ "start:mcp": "node lib/mcp-bin.js",
47
+ "inspect": "npx @modelcontextprotocol/inspector node lib/mcp-bin.js",
48
+ "download:zk-files": "node scripts/download-zk-files.ts",
49
+ "postinstall": "node scripts/download-zk-files.ts"
50
+ },
51
+ "keywords": [
52
+ "mcp",
53
+ "model-context-protocol",
54
+ "sdk",
55
+ "cli",
56
+ "ai",
57
+ "agent",
58
+ "chrome-devtools-protocol",
59
+ "reclaimprotocol",
60
+ "zktls",
61
+ "provider-authoring",
62
+ "zero-knowledge"
63
+ ],
64
+ "author": {
65
+ "name": "Reclaim Protocol",
66
+ "email": "support@reclaimprotocol.org"
67
+ },
68
+ "license": "MIT",
69
+ "dependencies": {
70
+ "@hapi/boom": "^9.1.4",
71
+ "@modelcontextprotocol/sdk": "^1.29.0",
72
+ "@noble/hashes": "^2.2.0",
73
+ "@noble/secp256k1": "^3.1.0",
74
+ "@reclaimprotocol/attestor-core": "^5.0.6",
75
+ "@reclaimprotocol/client": "*",
76
+ "chrome-remote-interface": "^0.33.0",
77
+ "pino": "^9.14.0",
78
+ "zod": "^3.25.76",
79
+ "zod-from-json-schema": "^0.1.0"
80
+ },
81
+ "devDependencies": {
82
+ "@types/chrome-remote-interface": "^0.31.0",
83
+ "@types/node": "^24.0.0",
84
+ "openapi-typescript": "^7.13.0",
85
+ "typescript": "^5.9.0",
86
+ "yaml": "^2.6.0"
87
+ }
88
+ }
@@ -0,0 +1,42 @@
1
+ // Download the @reclaimprotocol/zk-symmetric-crypto circuit resources that
2
+ // @reclaimprotocol/attestor-core needs at proof time. Runs as the agent's
3
+ // `postinstall` and as `npm run download:zk-files`.
4
+ //
5
+ // Resolves the package via Node's algorithm (`createRequire().resolve`)
6
+ // rather than a literal `node_modules/...` path: the package is commonly
7
+ // hoisted to the workspace root, where a literal relative path from
8
+ // packages/agent (npm's cwd for a workspace postinstall) would miss.
9
+
10
+ import { spawn } from 'node:child_process'
11
+ import { existsSync } from 'node:fs'
12
+ import { createRequire } from 'node:module'
13
+ import { dirname, join } from 'node:path'
14
+
15
+ const require = createRequire(import.meta.url)
16
+
17
+ let mainPath: string
18
+ try {
19
+ mainPath = require.resolve('@reclaimprotocol/zk-symmetric-crypto')
20
+ } catch{
21
+ // Dependency not installed (e.g. a consumer that only wants the SDK
22
+ // surface). Nothing to download; don't fail the install.
23
+ process.exit(0)
24
+ }
25
+
26
+ // mainPath is <pkg>/lib/index.js → its scripts live at <pkg>/lib/scripts.
27
+ const downloadScript = join(dirname(mainPath), 'scripts', 'download-files.js')
28
+
29
+ if(!existsSync(downloadScript)) {
30
+ console.warn(
31
+ `[reclaim-agent] zk download script not found at ${downloadScript}; ` +
32
+ 'skipping. Run `npm run download:zk-files` once the package is installed.',
33
+ )
34
+ process.exit(0)
35
+ }
36
+
37
+ const child = spawn(process.execPath, [downloadScript], { stdio: 'inherit' })
38
+ child.on('exit', (code) => process.exit(code ?? 0))
39
+ child.on('error', (err) => {
40
+ console.warn(`[reclaim-agent] zk download failed: ${err.message}`)
41
+ process.exit(0)
42
+ })
@@ -0,0 +1,17 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const SHEBANG = '#!/usr/bin/env node\n'
6
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
7
+ const BIN_FILES = ['cli.js', 'mcp-bin.js']
8
+
9
+ for(const file of BIN_FILES) {
10
+ const path = join(ROOT, 'lib', file)
11
+ const content = await readFile(path, 'utf8')
12
+ if(content.startsWith('#!')) {
13
+ continue
14
+ }
15
+
16
+ await writeFile(path, SHEBANG + content)
17
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,248 @@
1
+ ---
2
+ name: reclaim-provider
3
+ description: Activate when the developer asks how to verify a value from a website (e.g. "create a provider for X", "verify my GitHub username", "prove my X follower count"). Drives the reclaim-agent MCP server to capture browser traffic, draft a Reclaim provider JSON, replay it, and run an end-to-end zkTLS proof.
4
+ ---
5
+
6
+ # Authoring a Reclaim Provider / Verification
7
+
8
+ > **Terminology note:** "provider" and "verification" refer to the same
9
+ > thing. The API and tool parameters use **provider** / **providerId**
10
+ > (the technical term). Conversations with developers prefer
11
+ > **verification** (the user-facing term). Either is acceptable — never
12
+ > correct the developer for using one or the other.
13
+
14
+ You drive the browser through the capture → draft → replay → prove loop using the `reclaim-agent` MCP server. **Your default stance is driver, not orchestrator.** Only pause to ask the dev for things that genuinely require a human: typing credentials, completing 2FA, solving a captcha, confirming an ambiguous value. Navigation, reloads, DOM inspection, request capture — all yours.
15
+
16
+ ## Modes
17
+
18
+ This MCP server targets one of two backends. Detect which from the tools available to you:
19
+
20
+ - **Old devtools (the default).** `get_me_providers` is present; the generated API tools (`get_me`, `create_verification`, `list_orgs`, …) are absent.
21
+ - **Builder.** The generated API tools are present; `get_me_providers` is absent.
22
+
23
+ Only **authentication** (below) and **publishing** (the final step) differ between modes. The capture → draft → replay → prove loop in between is identical, and OPRF is optional and supported in both.
24
+
25
+ ## Getting authenticated
26
+
27
+ ### Old devtools mode
28
+
29
+ Call `reclaim_authenticate({})` with no arguments. It opens the Reclaim dashboard in Chrome, waits for the dev to sign in, reads the token from the page automatically, and closes the tab — a still-valid cached identity is reused first, so it's often instant. Tell the dev once: _"A browser window is opening the Reclaim dashboard — please sign in and I'll continue automatically."_
30
+
31
+ - Force a fresh login (ignore the cached identity): `reclaim_authenticate({ useBrowser: true })`.
32
+ - Skip the browser: `reclaim_authenticate({ token: "<firebase-id-token>" })`, or `reclaim_authenticate({ ethAddress: "0x..." })` for an already-provisioned eth user.
33
+ - Tokens are short-lived (~1h). If a later call returns 401, call `reclaim_authenticate({})` again.
34
+
35
+ ### Builder mode
36
+
37
+ **Check first:** try `get_me`. If it returns the user's profile, you're authenticated — proceed to the loop.
38
+
39
+ **On 401:** call `authenticate({})` to start a device-pairing session:
40
+
41
+ 1. It returns immediately with a `code` and `attachUrl` — it does NOT poll.
42
+ 2. Tell the user: _"Open `<attachUrl>`, sign in, and click 'Authorize this device'."_
43
+ 3. After they confirm, call `authenticate({ code: "<code>" })`: `LINKED` → proceed; `PENDING` → wait and call again; `EXPIRED` → start a new session.
44
+
45
+ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (`RECLAIM_API_TOKEN` in the server env overrides it). To link an ETH credential to an org: `list_orgs` (ask which), then `link_credential({ address: "0x...", orgId })` — only needed to use the credential in production.
46
+
47
+ ## The loop
48
+
49
+ 1. **Attach.** Call `attach_chrome`. If it returns `needsChoice: true`, ask the dev:
50
+ - `dedicated` — agent auto-launches Chrome with a dedicated profile under `~/.reclaim/chrome-profile`. Easiest path; works out of the box.
51
+ - `attach` — connect to a Chrome the dev is already running. **Important Chrome 136+ gotcha:** the dev's Chrome must be launched with BOTH `--remote-debugging-port=9222` AND `--user-data-dir=<some-non-default-path>`. Since Chrome 136 (May 2025), the debug-port flag is silently ignored when the default user profile is in use. If `attach` fails, tell them this verbatim and offer to switch to `dedicated`.
52
+
53
+ Re-call with the chosen `attachMode`.
54
+
55
+ 2. **Pick a tab and open the site yourself.** Call `list_tabs`. If there's already a tab on the right origin, use it. Otherwise pick any tab (the first one is usually fine) and call `navigate(tabId, url)` to send it to the site you've inferred from the dev's request (e.g. "GitHub username" → `https://github.com`). Don't ask the dev to navigate.
56
+
57
+ 3. **Check login state, wait if needed.** Use `eval_in_page(tabId, ...)` to evaluate a site-specific check.
58
+
59
+ **Combine a positive signal (logged-in-only element present) with a negative signal (login button absent), AND them together.** Single signals are too prone to false positives — most notably, `<meta>` tags often exist on every page regardless of login state; only their `.content` attribute is populated. The check `!!document.querySelector('meta[name="user-login"]')` returns true even when logged out — DO NOT use existence-only checks on meta tags or other persistent-but-empty elements.
60
+
61
+ Examples (always verify against the live page if these break — sites change selectors):
62
+ - GitHub: `(document.querySelector('meta[name="user-login"]')?.content?.length || 0) > 0 && !document.querySelector('a.HeaderMenu-link[href="/login"]')`
63
+ - X / Twitter: `!!document.querySelector('[data-testid="AppTabBar_Profile_Link"]') && !document.querySelector('[data-testid="loginButton"]')`
64
+ - Generic shape: `<element-present-only-when-logged-in> && !<login-button-present-only-when-logged-out>`
65
+
66
+ If the result is truthy, skip to step 4.
67
+
68
+ If falsy: **navigate to the site's login URL yourself first**, so the dev lands directly on the login form instead of having to find it. Common patterns: `https://<host>/login`, `https://<host>/signin`, `https://<host>/i/flow/login` (Twitter/X). Pick the one you know works for the site. Call `navigate(tabId, "<login-url>")`. Then tell the dev once: "I've opened the login page on `<site>` — please log in (with 2FA if needed). I'll detect when you're done." Then call `wait_for_page(tabId, expression="<same composite expression>", timeoutMs=180000)`. The MCP polls every second and returns when the expression goes truthy. **Do not** instruct the dev to "tell me when you're done" — just wait via the tool.
69
+
70
+ **Self-check before trusting the result:** if your detector says "already logged in" on a freshly-launched Chrome profile (cookies-clean), the detector is wrong. Brand-new profile means brand-new cookie jar — there's no way to be already authenticated. If you see that contradiction, your check is matching something that's present regardless of login (a meta tag, a generic class). Rewrite the expression and re-evaluate.
71
+
72
+ 4. **Navigate to the page that displays the value.** Use what you know about the site to construct the URL. If you need the dev's username, eval it (e.g. on GitHub: `document.querySelector('meta[name="user-login"]')?.content`). Then `navigate(tabId, url)`. Don't ask the dev to navigate.
73
+
74
+ 5. **Start capture, then trigger the request.** Call `start_capture(tabId)` first. Then issue a reload so the relevant API call fires while capture is live: `eval_in_page(tabId, "location.reload()")`. For some pages you may need to click a specific element or call a SPA route — use `eval_in_page` to do that programmatically rather than asking the dev to click.
75
+
76
+ 6. **Identify the value from the DOM.** `eval_in_page` to read the displayed value (e.g. the logged-in username from `meta[name="user-login"]`'s `content` attribute). If the value is unambiguous, proceed without asking. Only confirm with the dev when there are multiple plausible interpretations (e.g. the page shows both a display name "John Doe" and a handle "@jdoe" and you need to pick which to prove).
77
+
78
+ 7. **Find the request.** Call `find_requests_containing(captureId, value)`. For each top candidate (limit ≤ 5), call `analyze_request_constraints` and surface its `concerns` to the dev verbatim. Skip candidates flagged `signedRequest` or `botChallenge` unless none others are available. Concerns flagged `csrfDependency`, `shortLivedToken`, or `graphql` are informational — surface them and proceed.
79
+
80
+ **Strongly prefer authenticated, user-specific endpoints over public pages.** A proof against a public endpoint (e.g. `https://github.com/<username>`) attests only that "this URL returned this value at this time" — anyone in the world could fetch the same URL and prove the same thing. The proof says nothing about who the prover _is_. To prove a fact about the dev, the captured request needs to be one whose **response varies with authentication** — typically an API endpoint that returns the logged-in user's data (e.g. `GET https://api.github.com/user`, `https://twitter.com/i/api/.../UserByScreenName?...&forCurrentUser=true`).
81
+
82
+ When ranking candidates from `find_requests_containing`, prefer ones that:
83
+ - Have `Cookie`, `Authorization`, or other secret headers (analyze_request_constraints will surface these via `splitSecrets`).
84
+ - Have URL paths suggesting authenticated data: `/me`, `/user`, `/settings`, `/api/...`, `/graphql` with viewer queries.
85
+ - Return JSON keyed to the dev's identity (login/username/id fields next to the target value).
86
+
87
+ If the only candidates are public-page HTML responses, tell the dev: "The value appears on a public page, so a proof against this endpoint won't bind to your identity — anyone could prove the same fact. I can still build it if you just want to attest the value, but for an identity-bound proof we need to capture an authenticated request. Want to keep digging, or proceed anyway?" Then wait for their call.
88
+
89
+ 8. **Decide whether to OPRF the value** (privacy from the attestor). Before calling `propose_provider`, decide if the captured value should be hashed end-to-end so the attestor never sees it in cleartext. Four options:
90
+ - **No hash** (default). Value travels via `paramValues` + `{{name}}` template; attestor sees the cleartext substring. Simplest, fewest moving parts. Right call for non-sensitive values (public IPs from `ifconfig.me`, public usernames the dev is happy to expose, public repo names).
91
+ - **`oprf-raw`**. Server-side OPRF. **The only variant that runs on Windows / without external deps.** Use this when the dev wants any privacy and you're in a normal dev loop. Privacy is the weakest of the three (attestor still participates in hashing) — surface that honestly.
92
+ - **`oprf`**. Client-side ZK OPRF via gnark. Strongest privacy. Requires gnark install (Linux/macOS in practice). Don't suggest this on Windows.
93
+ - **`oprf-mpc`**. TEE-based MPC OPRF. Requires a TEE attestor deployment. Niche; only suggest if the dev brings it up.
94
+
95
+ **Default skip-the-question heuristic.** If the value is obviously non-sensitive (public IP, public username, public repo name, public follower/star counts on sites where the captured endpoint is auth-bound), default to **no hash** and proceed without asking. If the value is or could be sensitive (email, real name, private ID, anything the dev wouldn't post publicly), surface the choice verbatim:
96
+
97
+ > "Do you want this value kept private from the attestor via OPRF?
98
+ >
99
+ > - **No** (default) — value visible to attestor
100
+ > - **`oprf-raw`** — works on Windows / without extra deps; weakest of the OPRF variants
101
+ > - **`oprf`** — gnark/ZK; strongest privacy but requires Linux/macOS + gnark install
102
+ > - **`oprf-mpc`** — TEE-based; requires a TEE attestor"
103
+
104
+ **Hard constraint:** OPRF-hashed values are capped at **62 bytes**. If the value is longer and the dev picks any hash, surface that and propose either a tighter `target` (e.g. just the email local-part) or no hash. `propose_provider` will return a clean error if you try anyway.
105
+
106
+ 9. **Draft.** Call `propose_provider(captureId, requestId, target, name, hash?)`. Pass `hash` only if the dev opted in. Inspect the returned provider:
107
+ - Is the right header marked secret? (cookies, Authorization, X-\* tokens)
108
+ - Is `responseMatches[0].value` a `contains` with a `{{name}}` template (matching the captured value's structural context)?
109
+ - Is `paramValues[name]` set to the original captured value?
110
+ - Does `responseRedactions[0]` carry an `xPath` or `jsonPath` anchor? (And if `hash` was requested, also a `regex` with a named capture and a `hash: oprf-…` field?)
111
+
112
+ **Canonical drafted shape** (no hash):
113
+
114
+ ```json
115
+ {
116
+ "responseMatches": [
117
+ { "type": "contains", "value": "\"login\":\"{{username}}\"" }
118
+ ],
119
+ "responseRedactions": [{ "xPath": "//script[@id=\"client-env\"]" }],
120
+ "paramValues": { "username": "Scratch-net" }
121
+ }
122
+ ```
123
+
124
+ **Canonical drafted shape** (with `hash: 'oprf-raw'`):
125
+
126
+ ```json
127
+ {
128
+ "responseMatches": [
129
+ { "type": "contains", "value": "\"login\":\"{{username}}\"" }
130
+ ],
131
+ "responseRedactions": [
132
+ {
133
+ "xPath": "//script[@id=\"client-env\"]",
134
+ "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\"",
135
+ "hash": "oprf-raw"
136
+ }
137
+ ],
138
+ "paramValues": { "username": "Scratch-net" }
139
+ }
140
+ ```
141
+
142
+ **Do NOT rewrite the shape.** The synthesizer's output is the canonical Reclaim shape — `contains` template + paramValues + xPath/jsonPath redaction (optionally + regex+hash). Specifically:
143
+ - Do not replace the `contains` template with a hand-crafted regex.
144
+ - Do not hand-write a named-capture regex unless `hash` was requested AND the synthesizer's output doesn't already have one. (It will, for any digit/slug/email-shaped target.)
145
+ - Do not remove `paramValues` — that's how the template resolves at proof time.
146
+ - The redaction's xPath/jsonPath is what the prover reveals; the match's template is what the attestor verifies post-substitution. They serve different purposes — don't conflate them.
147
+
148
+ Permitted edits: narrowing the `contains` template, picking a more distinctive xPath class/id if the synthesizer's pick is too generic, fixing the `name` field, adding/removing secret headers. If you're tempted to rewrite the provider from scratch, call `propose_provider` again with different inputs instead.
149
+
150
+ 10. **Parameterize user-specific URL segments** so the provider isn't user-specific at the URL level. The matcher already populates `paramValues` with the _captured value_ (e.g. `{username: "Scratch-net"}` for a username target). If the URL ALSO contains user-identifying segments, add those as additional paramValues entries:
151
+
152
+ - Path segments that identify the dev (username, user-id, slug) → replace with `{{name}}`. E.g. `https://github.com/Scratch-net/repos` → `https://github.com/{{username}}/repos`, with `paramValues: { ..., username: "Scratch-net" }` (extending the matcher's existing entry, not replacing it).
153
+ - Query parameters with user-specific values → same treatment.
154
+ - Generic segments (`/api/v3`, `/users`, `/repos`) stay literal.
155
+ - If the URL has no user-identifying segments (e.g. `/settings/profile`), leave it untouched — the matcher already handled the value via paramValues.
156
+
157
+ 11. **Replay.** Call `replay_request(provider, secretParamsRef)`. Three outcomes:
158
+
159
+ - `matchesOriginal: true` → proceed.
160
+ - `matchesOriginal: false` with `hints` mentioning auth/csrf → re-trigger the action yourself: navigate or reload via the tools, then loop to step 5. Don't ask the dev to redo it manually.
161
+ - `matchesOriginal: false` with stable diff (timestamps etc.) → tighten the matcher and retry.
162
+
163
+ **After replay succeeds, run an authentication-bound check.** Call `replay_request(provider)` _without_ the `secretParamsRef` (omit it). This sends the request stripped of all secret headers — the same shape an unauthenticated visitor would send.
164
+
165
+ - If `matchesOriginal: false` (e.g. 401/403, or the value differs) → the endpoint is auth-bound. Good. The proof will actually mean "the holder of these secrets saw this value."
166
+ - If `matchesOriginal: true` with the same value → the endpoint is fully public. Tell the dev verbatim: "I verified this endpoint returns the same value with no authentication — the proof won't prove your identity, only that this URL returned the value. Anyone could generate the same proof. Want to keep digging for an auth-bound endpoint, or proceed anyway?" Wait for their decision before continuing.
167
+
168
+ 12. **Prove.** Call `run_proof(provider, secretParamsRef)`. On `verified: true`, the full claim has been written to `~/.reclaim/claims/<timestamp>.json` and the tool returns `{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`. The `claim` field is a display-ready version of the proof — its huge `request` payload has been stripped and all byte arrays converted to base64, so it renders cleanly. **Show it to the dev as a fenced JSON code block at the end** along with the provider JSON. That's the final result. On `verified: false`, diagnose using the returned error and iterate.
169
+
170
+ **Missing signing credential (`errorKind: "missing-owner-key"`)** — the dev hasn't set up a credential yet. Tell them what's happening, then drive the bootstrap yourself: call `list_cached_credentials` to see if anything's already cached; if not, ask once _"shall I mint a fresh credential via `issue_credentials`? Persisted to `~/.reclaim/config.json` for this and future sessions."_; on yes, call `issue_credentials` and retry the same `run_proof` call. **No restart needed** — the credential is read live from the cache on each `run_proof`, and the existing `secretParamsRef` from `propose_provider` is still valid.
171
+
172
+ **On any `verified: false` response, the result includes (inline):**
173
+ - `error` — the SDK's thrown error message verbatim
174
+ - `sentParams` — the exact `params` object the SDK saw, post-template substitution (lets you spot mutation / substitution surprises)
175
+
176
+ **Deeper tracing (opt-in).** If the dev launches Claude Code with `RECLAIM_AGENT_TRACE=1` in the MCP server's env, `run_proof` ALSO captures the SDK's full pino log stream into `result.attestorLogs` and persists a per-call dump to `~/.reclaim/proof-runs/<timestamp>.json` (surfaced as `runLogPath`). Off by default because the trace stream is verbose. Suggest turning it on when investigating a recurring proof failure.
177
+
178
+ **The attestor is reliable. If it errors, the cause is in the provider, the network, or the prover-side code — not "the attestor can't handle this site".** Do not invent failure modes like "HTML is too large", "TLS records are chunked", "the attestor matches against gzipped bytes" (it doesn't if the provider has `Accept-Encoding: identity`, which `propose_provider` always sets), or "xPath doesn't work on this DOM". These are wrong and they waste the dev's time. Instead:
179
+ 1. Show the `error` field to the dev verbatim.
180
+ 2. If `sentParams` differs from what you expected — e.g. `{{name}}` substituted to the wrong value, headers got mutated, redactions look different from `propose_provider`'s output — surface that.
181
+ 3. Only then propose a hypothesis, and tie it to that evidence. If the diagnosis is unclear, suggest the dev set `RECLAIM_AGENT_TRACE=1` and rerun for full attestor logs.
182
+
183
+ **Do not auto-retry with a different `attestorUrl`.** There is one live public attestor right now; backup hostnames (`attestor-2.*`, etc.) **do not exist**. `attestorUrl` is only for when the dev explicitly supplies a private/staging URL.
184
+
185
+ **Missing ZK circuit files:** if the error says "ZK circuit resources are missing" and points at a `circuit.wasm` path, the npm postinstall didn't run (commonly because the dev used `npm install --ignore-scripts` or a corp policy disabled it). Tell the dev to run `npm run download:zk-files` from the project root and then retry `run_proof` — no need to redo capture or propose. The MCP server doesn't need a restart either; the next `run_proof` call will see the files.
186
+
187
+ 13. **Publish the proved provider.** This is the step that differs by mode (see [Modes](#modes)). In both, `provider` is the exact JSON from step 9 with the step-10 URL parameterisation applied, and `initialUrl` is the page where the user logs in / the value is displayed (e.g. `https://github.com/Syed`) — **not** the API endpoint URL inside the provider.
188
+
189
+ **Old devtools mode** — call `create_provider_version_from_capture`:
190
+ - **New verification:** omit `providerId`. It registers a new provider (version 1.0.0). Pass `provider`, `initialUrl`, and optionally `description`, `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
191
+ - **New version of an existing verification:** pass its `providerId` — the patch is auto-bumped above the highest existing version. Pass `notes` (≥10 chars) as the change note.
192
+ - `get_me_providers({ searchQuery? })` lists the verifications you own — use it to find a `providerId` or to confirm the result. It's the only provider-listing tool in this mode.
193
+ - There's no separate "create-then-version" step and no org / public-review workflow here.
194
+
195
+ **Builder mode:**
196
+ 1. **Create the verification if it doesn't exist:** `create_verification(orgId, domain, slug, title, description)` — find `orgId` via `list_orgs`. `domain` e.g. `github.com`; `slug` e.g. `github-username`; `title` e.g. "GitHub Username". Starts PRIVATE with an empty seed version. Skip if the dev already has a `providerId`.
197
+ 2. **Persist the version:** `create_provider_version_from_capture(providerId, provider, initialUrl, notes)` — `notes` e.g. `"Initial draft from browser capture"`. PRIVATE → live immediately; PUBLIC → lands on the `draft` branch.
198
+ 3. **For PUBLIC verifications:** `submit_provider_version_for_review(providerId, version)` (the semver returned above). Tell the dev it's awaiting admin review.
199
+
200
+ When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
201
+
202
+ ## Concerns the developer should see verbatim
203
+
204
+ When `analyze_request_constraints` returns concerns, repeat each `message` to the dev exactly. Don't paraphrase, don't omit. These are the dev's signal that a provider is or isn't worth pursuing.
205
+
206
+ ## Things to never do
207
+
208
+ - Never echo a raw `Authorization`, `Cookie`, or `X-*-Token` value back to the dev or write one to any file. The MCP guarantees these don't appear in tool responses — keep that guarantee.
209
+ - Never call `run_proof` before `replay_request` succeeds. Attestor calls are expensive.
210
+ - Never pretend a proof succeeded when `verified: false` came back. Show the error and iterate.
211
+ - **Never hand-craft `responseMatches` or `responseRedactions`.** If you find yourself typing a regex by hand, OR writing `(?<name>...)`-style patterns by hand, OR copying the captured value literally into the match — stop. The synthesizer in `propose_provider` already produces the canonical shape: `contains` template + paramValues for the match, xPath/jsonPath (optionally + regex+hash when `hash` is passed) for the redaction. The named-capture regex in the redaction is the synthesizer's job, not yours.
212
+ - Never write a literal value into `responseMatches.value` — the synthesizer always emits `{{name}}` templates with paramValues. A literal value bakes the original user into the provider.
213
+ - Never pass `hash` to `propose_provider` for a free-form (unicode / spaces / punctuation) target — the synthesizer will error, and the right move is either to use a tighter `target` or accept cleartext (no `hash`).
214
+ - Never accept "the synthesizer's xPath looks too narrow, let me write a regex" — the xPath is fine, the attestor will reveal the whole matched element which is naturally ≥16 bytes.
215
+
216
+ ## Common first-time issues — surface these proactively
217
+
218
+ If `attach_chrome` errors, times out, or `waitForCdpReady` reports the port isn't answering, walk through these in order before retrying:
219
+
220
+ - **Browser binary not found.** If the error lists candidate paths it tried (Program Files Chrome, LOCALAPPDATA Chrome, Edge), every one missed. Either the dev has Chrome installed somewhere unusual, or doesn't have Chrome/Edge at all. Walk them through setting `RECLAIM_AGENT_CHROME_PATH` to the actual binary path:
221
+ ```powershell
222
+ setx RECLAIM_AGENT_CHROME_PATH "C:\path\to\chrome.exe"
223
+ ```
224
+ `setx` (not `$env:`) writes to the user-level Windows env, so future shells and future Claude Code launches inherit it. **Then fully restart Claude Code** — MCP servers snapshot env vars at launch, so the running reclaim-agent has the old empty value. After restart, the new MCP process picks up the env. On macOS/Linux: `export RECLAIM_AGENT_CHROME_PATH=/path/to/chrome` in the shell, then restart Claude Code from that same shell so the var propagates.
225
+ - **Pre-existing `chrome.exe` processes hijack the new launch (Windows).** If any Chrome is already running, a new launch with `--remote-debugging-port` gets delegated to the existing process group and the debug port silently doesn't open. Tell the dev to kill all `chrome.exe` first (`taskkill /F /IM chrome.exe` in cmd/PowerShell, or via Task Manager), then retry. Affects both `attach` and `dedicated` modes when the existing instance was launched with the same profile dir.
226
+ - **Chrome 136+ silently disables remote debugging on the default user profile.** Already noted in step 1, but worth raising again here: the only fix is a non-default `--user-data-dir`. `dedicated` mode is always safe; `attach` mode needs the dev to have launched with both flags.
227
+
228
+ **Credential for signing the proof.** `run_proof` requires an `ownerAddress` — the app credential whose key signs the claim. It does **no** resolution itself; get the address from `resolve_owner_key` first. The private key is resolved locally (cache → env → `.env`) and never enters the conversation.
229
+
230
+ 1. **Call `resolve_owner_key`.** It checks `RECLAIM_PRIVATE_KEY` (MCP env) → the project `.env` → the best cached credential for this directory.
231
+ - `{ ready: true, address }` → call `run_proof({ provider, ownerAddress: address, … })`. No prompts.
232
+ - `{ ready: false }` → no credential exists yet. Ask the dev once how to set one up:
233
+ - **Provide their own key** → `import_credentials({ privateKey: "0x…" })`.
234
+ - **Create a new one** → `issue_credentials({})`. (Fine default for test proofs — owner identity doesn't matter.)
235
+ - **Persist or memory-only?** Writing the key to `.env` is opt-in — **never write `.env` without asking.** Pass `writeEnv: false` to keep it in the local cache (memory) only. Either way it's cached, so `resolve_owner_key` finds it next.
236
+ - Then call `resolve_owner_key` again and `run_proof` with the returned address.
237
+ 2. All fully local — no network, no server registration. `list_cached_credentials` shows what's cached if the dev wants to pick a specific one.
238
+ - **Advanced:** setting `RECLAIM_PRIVATE_KEY` directly in the MCP server's env also works, but env vars are snapshotted at Claude Code startup, so that route needs a full restart (which invalidates every `secretParamsRef`). The cache/`.env` flow above avoids that — prefer it.
239
+ - **Multiple MCP config files exist on Windows.** `~/.claude.json` takes precedence over `~/.claude/mcp.json`. When advising config edits, point them at the right file or tell them to use `claude mcp list` / `claude mcp add --scope user` rather than editing JSON by hand.
240
+
241
+ If `replay_request` or `run_proof` errors with `unknown secretParamsRef`:
242
+
243
+ - **The ref is bound to the MCP server process.** Anything that restarts the server (Claude Code restart, `reset_session`, machine reboot) invalidates every previously-issued ref. The fix is always: re-run `start_capture` → interact → `propose_provider` to mint a fresh ref. Don't ask the dev to "re-enter the value" — walk them back through capture.
244
+ - **Exception:** if the drafted provider has empty `secretRefs: []` (no secret headers were captured), you can call `replay_request` and `run_proof` without `secretParamsRef` at all — both tools accept it as optional in that case.
245
+
246
+ ## Out of scope (v0)
247
+
248
+ If the dev needs a provider for a flow that requires multiple sequential requests (CSRF preflight, refresh-token chains), tell them: "multi-request providers are v1. For now, we can produce a single-request provider for the target value — let's see if one exists in this capture."