@reclaimprotocol/agent 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +8 -8
  2. package/lib/authoring/backend.d.ts +12 -1
  3. package/lib/authoring/backend.js +3 -0
  4. package/lib/authoring/tools.js +33 -5
  5. package/lib/authoring/util.d.ts +43 -0
  6. package/lib/authoring/util.js +54 -0
  7. package/lib/cdp/chrome-for-testing.d.ts +14 -0
  8. package/lib/cdp/chrome-for-testing.js +43 -0
  9. package/lib/cdp/geo.d.ts +3 -0
  10. package/lib/cdp/geo.js +27 -15
  11. package/lib/cdp/launcher.d.ts +79 -1
  12. package/lib/cdp/launcher.js +244 -24
  13. package/lib/logger.js +3 -1
  14. package/lib/mcp/api/operations.gen.js +1 -1
  15. package/lib/mcp/instructions.js +3 -3
  16. package/lib/mcp/start.js +17 -2
  17. package/lib/mcp/tools/agent/attach.d.ts +6 -0
  18. package/lib/mcp/tools/agent/attach.js +58 -118
  19. package/lib/mcp/tools/agent/build.js +6 -0
  20. package/lib/mcp/tools/agent/capture.js +33 -10
  21. package/lib/mcp/tools/agent/check-attestor.js +15 -7
  22. package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
  23. package/lib/mcp/tools/agent/draft-tools.js +170 -0
  24. package/lib/mcp/tools/agent/pod-proof.js +5 -1
  25. package/lib/mcp/tools/agent/proof.d.ts +28 -0
  26. package/lib/mcp/tools/agent/proof.js +72 -0
  27. package/lib/mcp/tools/agent/shared.d.ts +8 -1
  28. package/lib/mcp/tools/agent/shared.js +98 -19
  29. package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
  30. package/lib/mcp/tools/agent/start-integration.js +10 -6
  31. package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
  32. package/lib/mcp/tools/authenticate/authenticate.js +212 -116
  33. package/lib/mcp/tools/authenticate/build.d.ts +2 -2
  34. package/lib/mcp/tools/authenticate/build.js +6 -3
  35. package/lib/mcp/tools/authenticate/builder-access.js +5 -2
  36. package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
  37. package/lib/mcp/tools/authenticate/eth-key.js +29 -5
  38. package/lib/mcp/tools/authenticate/index.d.ts +1 -1
  39. package/lib/mcp/tools/authenticate/index.js +1 -1
  40. package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
  41. package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
  42. package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
  43. package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
  44. package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
  45. package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
  46. package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
  47. package/lib/mcp/tools/authenticate/session-cache.js +95 -4
  48. package/lib/mcp/tools/mode/mode.js +6 -6
  49. package/lib/mcp/tools/verification/proxy-callback-events.d.ts +21 -3
  50. package/lib/mcp/tools/verification/proxy-callback-events.js +128 -45
  51. package/lib/old/login-cdp.js +2 -3
  52. package/lib/proof/attestor.js +2 -3
  53. package/lib/proof/check-attestor.d.ts +12 -2
  54. package/lib/proof/check-attestor.js +40 -10
  55. package/lib/proof/check-matchers.d.ts +36 -0
  56. package/lib/proof/check-matchers.js +161 -0
  57. package/lib/proof/zk-files.d.ts +50 -11
  58. package/lib/proof/zk-files.js +259 -63
  59. package/lib/provider/draft.d.ts +24 -1
  60. package/lib/provider/draft.js +135 -0
  61. package/lib/provider/matcher.js +171 -4
  62. package/package.json +6 -4
  63. package/skill/SKILL.builder.md +7 -2
  64. package/skill/SKILL.common.md +90 -70
  65. package/skill/SKILL.old.md +3 -3
  66. package/skill/builder-mode.md +44 -6
  67. package/skill/old-mode.md +18 -13
  68. package/skill/start-integration.md +51 -35
  69. package/skill/verification-client-integration.md +87 -15
  70. package/src/authoring/backend.ts +12 -1
  71. package/src/authoring/tools.ts +48 -7
  72. package/src/authoring/util.ts +64 -0
  73. package/src/cdp/chrome-for-testing.ts +66 -0
  74. package/src/cdp/geo.ts +36 -21
  75. package/src/cdp/launcher.ts +328 -24
  76. package/src/logger.ts +3 -1
  77. package/src/mcp/api/operations.gen.ts +1 -1
  78. package/src/mcp/instructions.ts +3 -3
  79. package/src/mcp/start.ts +20 -1
  80. package/src/mcp/tools/agent/attach.ts +65 -128
  81. package/src/mcp/tools/agent/build.ts +7 -0
  82. package/src/mcp/tools/agent/capture.ts +54 -11
  83. package/src/mcp/tools/agent/check-attestor.ts +15 -7
  84. package/src/mcp/tools/agent/draft-tools.ts +234 -0
  85. package/src/mcp/tools/agent/pod-proof.ts +5 -1
  86. package/src/mcp/tools/agent/proof.ts +110 -1
  87. package/src/mcp/tools/agent/shared.ts +130 -19
  88. package/src/mcp/tools/agent/start-integration.ts +10 -6
  89. package/src/mcp/tools/authenticate/authenticate.ts +276 -131
  90. package/src/mcp/tools/authenticate/build.ts +6 -3
  91. package/src/mcp/tools/authenticate/builder-access.ts +5 -2
  92. package/src/mcp/tools/authenticate/eth-key.ts +41 -7
  93. package/src/mcp/tools/authenticate/index.ts +1 -1
  94. package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
  95. package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
  96. package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
  97. package/src/mcp/tools/authenticate/session-cache.ts +125 -5
  98. package/src/mcp/tools/mode/mode.ts +6 -6
  99. package/src/mcp/tools/verification/proxy-callback-events.ts +151 -41
  100. package/src/old/login-cdp.ts +2 -3
  101. package/src/proof/attestor.ts +2 -3
  102. package/src/proof/check-attestor.ts +55 -12
  103. package/src/proof/check-matchers.ts +227 -0
  104. package/src/proof/zk-files.ts +357 -65
  105. package/src/provider/draft.ts +215 -7
  106. package/src/provider/matcher.ts +237 -4
@@ -1,3 +1,5 @@
1
+ import { badRequest } from '@hapi/boom';
2
+ import assert from 'node:assert';
1
3
  import { elementAnchor } from "./dom-xpath.js";
2
4
  import { normalizeCandidates } from "./normalize.js";
3
5
  /** Reclaim attestor caps OPRF-hashed values at 62 bytes (TOPRF marker
@@ -53,6 +55,12 @@ export function synthesizeMatcher(body, contentType, target, paramName, hash) {
53
55
  return r;
54
56
  }
55
57
  }
58
+ // Inline JSON / JS-object text outside an HTML element (a JS bundle, a
59
+ // non-JSON text body): anchor on the key name right before the value.
60
+ const inline = inlineKeyMatcher(body, target, paramName, valueClass, hash);
61
+ if (inline) {
62
+ return inline;
63
+ }
56
64
  // No JSON key and no stable xPath anchor: a content-anchored regex, matched
57
65
  // by CONTENT so it reproduces regardless of DOM position at proof time (a
58
66
  // positional `(//tag)[N]` resolves to the wrong node at proof — see 9fd7770).
@@ -308,7 +316,133 @@ function htmlMatcher(body, target, paramName, valueClass, hash) {
308
316
  return keyValueResult(kp.prefix, kp.close, target, paramName, valueClass, { xPath: anchor.xPath, jsonPath }, hash);
309
317
  }
310
318
  }
311
- return elementResult(anchor.xPath, revealSlice, target, paramName, valueClass, hash);
319
+ // Inline JS-object / JSON text inside the element (a SvelteKit/Next/Nuxt
320
+ // hydration `<script>` that isn't valid JSON): anchor on the key name
321
+ // nearest the value (`currentTierDescription:"{{v}}"`), scoped by the xPath.
322
+ const inline = inlineKeyMatcher(revealSlice, target, paramName, valueClass, hash, anchor.xPath);
323
+ if (inline) {
324
+ return inline;
325
+ }
326
+ const result = elementResult(anchor.xPath, revealSlice, target, paramName, valueClass, hash);
327
+ const [left = '', right = ''] = result?.match.value
328
+ .split(`{{${paramName}}}`) ?? [];
329
+ const hits = result?.redaction?.regex
330
+ ? countMatches(revealSlice, result.redaction.regex)
331
+ : 0;
332
+ assert(!result || !isGenericWindow(left, right) || hits <= 1, badRequest(`target value "${target}" sits in a <${elementTag(revealSlice)}> `
333
+ + 'with no key name or markup next to it. The only literal context, '
334
+ + `"${result?.match.value}", matches ${hits} values in that element, `
335
+ + 'so it would not pin this one. Pick a request where the value has '
336
+ + 'a key (for example, a JSON API response) or sits in its own '
337
+ + 'element.'));
338
+ return result;
339
+ }
340
+ /** How many times `regex` matches in `text` (0 for an invalid regex). */
341
+ function countMatches(text, regex) {
342
+ try {
343
+ return [...text.matchAll(new RegExp(regex, 'g'))].length;
344
+ }
345
+ catch {
346
+ return 0;
347
+ }
348
+ }
349
+ /** Tag name of the element markup starting `markup` (`script`, `div`, …). */
350
+ function elementTag(markup) {
351
+ return /^<([A-Za-z][\w-]*)/.exec(markup)?.[1] ?? 'element';
352
+ }
353
+ /** A literal window with no letter or digit on either side (for example,
354
+ * `:"{{v}}",`): it pins a value only when the element holds nothing else. */
355
+ function isGenericWindow(left, right) {
356
+ return !/[A-Za-z0-9]/.test(left + right);
357
+ }
358
+ // ---------------------------------------------------------------------------
359
+ // Inline key: `key:"value"` / `"key":"value"` in JS-object or JSON text
360
+ // ---------------------------------------------------------------------------
361
+ /** How far left of a value to look for its key. */
362
+ const MAX_KEY_LOOKBACK = 128;
363
+ /** The key directly before a value: JSON `"key":`, `'key':`, or a bare JS
364
+ * identifier `key:`, then the value's opening quote (if any) at the end. */
365
+ const KEY_BEFORE_VALUE = new RegExp('(?:"([A-Za-z_$][\\w$-]*)"|\'([A-Za-z_$][\\w$-]*)\''
366
+ + '|(?<![\\w$"\'])([A-Za-z_$][\\w$]*))\\s*:\\s*(["\']?)$');
367
+ /**
368
+ * Matcher for a value written as `key:"value"` (JS object literal, as SvelteKit
369
+ * and similar frameworks inline into SSR pages) or `"key":"value"` inside text
370
+ * that isn't parseable JSON. The window is the key name plus the value's
371
+ * quotes, so the match pins this field and nothing user-specific:
372
+ * `currentTierDescription:"{{v}}"`.
373
+ *
374
+ * Rejects a key that appears more than once in `scope` with a different value:
375
+ * the regex would extract the first one, which may not be this field. When the
376
+ * value has a key but it's ambiguous, throws rather than falling back to a
377
+ * looser window. Returns undefined when no occurrence has a key before it.
378
+ */
379
+ function inlineKeyMatcher(scope, target, paramName, valueClass, hash, xPath) {
380
+ const candidates = [target, ...normalizeCandidates(target)]
381
+ .filter((c) => c && !c.startsWith('"'));
382
+ let ambiguous;
383
+ for (const cand of new Set(candidates)) {
384
+ for (let idx = scope.indexOf(cand); idx >= 0; idx = scope.indexOf(cand, idx + 1)) {
385
+ const kv = keyBefore(scope, idx, cand);
386
+ if (!kv) {
387
+ continue;
388
+ }
389
+ const others = valuesForKey(scope, kv.key)
390
+ .filter((v) => v !== cand);
391
+ if (others.length) {
392
+ ambiguous ?? (ambiguous = { key: kv.key, count: others.length + 1 });
393
+ continue;
394
+ }
395
+ // A single-quoted free-form value must stop at its own quote.
396
+ const cls = kv.close === '\'' && valueClass === '[^"<>\\n]+'
397
+ ? '[^\'<>\\n]+'
398
+ : valueClass;
399
+ return keyValueResult(kv.prefix, kv.close, cand, paramName, cls, xPath ? { xPath } : {}, hash);
400
+ }
401
+ }
402
+ assert(!ambiguous, badRequest(`target value "${target}" sits under key "${ambiguous?.key}", but that `
403
+ + `key occurs ${ambiguous?.count} times with different values, so a `
404
+ + 'match on it would not pin this value. Pick a request where the key '
405
+ + 'is unique (for example, a JSON API that returns only this object).'));
406
+ return undefined;
407
+ }
408
+ /** The key + opening-quote prefix directly before the value at `idx`, when the
409
+ * value is a complete quoted string (or a bare token) under that key. */
410
+ function keyBefore(scope, idx, value) {
411
+ const head = scope.slice(Math.max(0, idx - MAX_KEY_LOOKBACK), idx);
412
+ const m = KEY_BEFORE_VALUE.exec(head);
413
+ const key = m?.[1] ?? m?.[2] ?? m?.[3];
414
+ if (!m || !key || key.length < 2) {
415
+ return undefined;
416
+ }
417
+ // A bare key before a bare value (`score: 5`) is prose as often as code;
418
+ // require a quoted key or a quoted value.
419
+ const quote = m[4] ?? '';
420
+ const after = scope[idx + value.length] ?? '';
421
+ const complete = quote ? after === quote : /^$|[\s,;}\]]/.test(after);
422
+ if (!complete || (!quote && m[3])) {
423
+ return undefined;
424
+ }
425
+ return { key, prefix: m[0], close: quote };
426
+ }
427
+ /** Every value written under `key` in `scope` (JSON or JS-object syntax),
428
+ * raw — quoted strings without their quotes, bare tokens as-is. */
429
+ function valuesForKey(scope, key) {
430
+ const re = new RegExp(`(?<![\\w$])(["']?)${escapeRegex(key)}\\1\\s*:\\s*`, 'g');
431
+ const values = [];
432
+ for (const m of scope.matchAll(re)) {
433
+ const start = m.index + m[0].length;
434
+ const quote = scope[start];
435
+ if (quote === '"' || quote === '\'') {
436
+ let j = start + 1;
437
+ while (j < scope.length && scope[j] !== quote) {
438
+ j += scope[j] === '\\' ? 2 : 1;
439
+ }
440
+ values.push(scope.slice(start + 1, j));
441
+ continue;
442
+ }
443
+ values.push(/^[^\s,;}\]]*/.exec(scope.slice(start))?.[0] ?? '');
444
+ }
445
+ return values;
312
446
  }
313
447
  /**
314
448
  * `contains` a PORTABLE window of the element around the value, with the value
@@ -547,14 +681,25 @@ function buildContentRegex(body, target, paramName, valueClass) {
547
681
  const from = Math.max(0, start);
548
682
  const before = escapeRegexFlexWs(body.slice(from, idx));
549
683
  const re = `${before}(?<${paramName}>${valueClass})${right}`;
684
+ let found = false;
550
685
  try {
551
- if (new RegExp(re).exec(body)?.groups?.[paramName] === cand) {
552
- return re;
553
- }
686
+ found = new RegExp(re).exec(body)?.groups?.[paramName] === cand;
554
687
  }
555
688
  catch {
556
689
  break;
557
690
  }
691
+ if (found) {
692
+ const problem = contextProblem(body.slice(from, idx), right ? rawRight : '');
693
+ assert(!problem, badRequest(`target value "${target}" has no key name or element next to `
694
+ + `it; the shortest window that pins it, "${body.slice(from, idx)}`
695
+ + `{{${paramName}}}${right ? rawRight : ''}", ${problem}. That `
696
+ + 'would pin the provider to this user or match the wrong value. '
697
+ + 'This happens with positional arrays such as SvelteKit '
698
+ + '`__data.json`. Use a request where the value sits under its '
699
+ + 'own key (the HTML page or another JSON API), or stop and '
700
+ + 'report.'));
701
+ return re;
702
+ }
558
703
  if (from === 0) {
559
704
  break;
560
705
  }
@@ -562,6 +707,28 @@ function buildContentRegex(body, target, paramName, valueClass) {
562
707
  }
563
708
  return undefined;
564
709
  }
710
+ /** A standalone number token (`33` in `:33},`) — a points balance, count, or
711
+ * id that differs per user; not a digit inside a word or class (`mb-2`). */
712
+ const VOLATILE_NUMBER = /(?<![\w.-])\d+(?:\.\d+)?(?![\w.])/;
713
+ /** The closing quote of a string followed by a separator — the end of a
714
+ * neighbouring VALUE (keys are followed by `:`), usually user-specific. */
715
+ const NEIGHBOUR_STRING = /"\s*[,\]}]/;
716
+ /** Why a content window around a value isn't portable, or undefined when it
717
+ * is: only punctuation (pins nothing), a number next to the value, or a
718
+ * neighbouring string value. */
719
+ function contextProblem(left, right) {
720
+ if (VOLATILE_NUMBER.test(left) || VOLATILE_NUMBER.test(right)) {
721
+ return 'contains a number next to the value, which is likely '
722
+ + 'user-specific (for example, a points balance)';
723
+ }
724
+ if (NEIGHBOUR_STRING.test(left)) {
725
+ return 'contains a neighbouring value, which is likely user-specific';
726
+ }
727
+ if (!/[A-Za-z]/.test(left + right)) {
728
+ return 'is only punctuation and matches almost any value';
729
+ }
730
+ return undefined;
731
+ }
565
732
  /**
566
733
  * The literal `contains` template: surrounding structural context with the
567
734
  * value replaced by `{{paramName}}`. Whitespace-adjacent context is dropped (a
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reclaimprotocol/agent",
3
3
  "title": "Reclaim Agent",
4
- "version": "0.1.35",
4
+ "version": "0.1.37",
5
5
  "description": "Reclaim Protocol tools for AI coding agents. Reclaim lets your app verify data points about its users, such as a follower count, an account balance, or an order history. This package is the Reclaim MCP server: it gives your agent the tools to find, build, test, and fix data verification providers by capturing and replaying browser requests, drafting provider recipes, running zkTLS proofs, and publishing provider versions.",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -44,7 +44,7 @@
44
44
  "README.md"
45
45
  ],
46
46
  "engines": {
47
- "node": ">=22.0.0"
47
+ "node": ">=22.12.0"
48
48
  },
49
49
  "repository": {
50
50
  "url": "https://github.com/reclaimprotocol/builder",
@@ -82,8 +82,9 @@
82
82
  "@hapi/boom": "^9.1.4",
83
83
  "@modelcontextprotocol/sdk": "^1.29.0",
84
84
  "@noble/hashes": "^2.2.0",
85
- "@reclaimprotocol/attestor-core": "^5.1.5",
86
- "@reclaimprotocol/client": "0.1.35",
85
+ "@puppeteer/browsers": "3.2.4",
86
+ "@reclaimprotocol/attestor-core": "^5.1.6",
87
+ "@reclaimprotocol/client": "0.1.37",
87
88
  "@reclaimprotocol/tls": "^0.1.3",
88
89
  "chrome-remote-interface": "^0.33.0",
89
90
  "domhandler": "^6.0.0",
@@ -92,6 +93,7 @@
92
93
  "parse5-htmlparser2-tree-adapter": "^8.0.0",
93
94
  "pino": "^9.14.0",
94
95
  "xpath": "^0.0.34",
96
+ "yauzl": "3.4.0",
95
97
  "zod": "^4.4.3"
96
98
  },
97
99
  "devDependencies": {
@@ -15,6 +15,11 @@ Build a new org-private provider for the claim the developer described. Ask
15
15
  one question if the site, account, or value is ambiguous. Copy this checklist
16
16
  and check off each step. Read the linked topic before a step you don't know.
17
17
 
18
+ Steps 2 through 10 need the developer at the browser, so do them before other
19
+ work, such as app code. When you need the developer to answer or act, end
20
+ your turn with the question and every link they need. Don't use a background
21
+ or asynchronous question tool for a blocking question.
22
+
18
23
  - [ ] 1. Name the data points: only the values the claim must prove, usually
19
24
  one or two. No identity or profile fields.
20
25
  - [ ] 2. `attach_browser`, then `list_tabs` → `start_capture(tabId)` ⇒
@@ -34,7 +39,7 @@ and check off each step. Read the linked topic before a step you don't know.
34
39
  → topic `params`
35
40
  - [ ] 9. `replay_request(captureId, draftId)`, then again with
36
41
  `withoutSecrets: true`. → topic `prove`
37
- - [ ] 10. `run_proof(captureId, draftId, ownerAddress)`.
42
+ - [ ] 10. `run_proof(captureId, draftId)`.
38
43
  → topics `prove`, `credentials`
39
44
  - [ ] 11. `create_provider` (or a new version of a provider your org owns) →
40
45
  `create_provider_version_from_capture` (publishes the kept script as
@@ -303,7 +308,7 @@ with no secrets/cookies, as an anonymous visitor.
303
308
  `viewer`/`currentUser` API) and re-check. If the value only ever appears on
304
309
  public responses, tell the developer it can't be proven as an identity claim.
305
310
 
306
- **3. Prove.** `run_proof(captureId, draftId, ownerAddress)`. On
311
+ **3. Prove.** `run_proof(captureId, draftId)`. On
307
312
  `verified: true` the full claim is written to
308
313
  `~/.reclaim/claims/<timestamp>.json` and you get
309
314
  `{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`.
@@ -28,45 +28,37 @@ already names it.
28
28
  - [Container lifetime and isolation](#how-containers-are-shared-and-when-they-close)
29
29
  - [Container startup problems](#when-the-container-will-not-start)
30
30
 
31
- **Ask one question first, before you list anything:**
32
-
33
- > Will you be doing this yourself, or does someone else need to sign in — the
34
- > person whose account it is, say?
35
-
36
- That answer decides half the list, and a developer can answer it without knowing
37
- what CDP is. Only then offer the matching pair, call `attach_browser` ONCE with
38
- the `mode` they pick, and never pick for them or assume last session's choice
39
- still applies.
40
-
41
- **They're doing it themselves** — no link, no Docker, nothing to install:
42
-
43
- | mode | what it is | needs |
31
+ **Default: call `attach_browser()` with no arguments. Don't ask anything
32
+ first.** It opens a visible browser on the developer's own machine with the
33
+ persistent Reclaim profile (`~/.reclaim/chrome-profile`), so a site they sign
34
+ in to stays signed in. It uses installed Chrome, then Chromium, then Edge. If
35
+ none is installed, or the installed one won't start, it downloads Chrome for
36
+ Testing once into `~/.reclaim/browsers` and uses that. When the result says
37
+ Chrome for Testing was used, tell the developer in one line.
38
+
39
+ **Side note on sharing.** Tell the developer once, in passing, that if a
40
+ volunteer is helping them sign in, you can set up a shareable link to the
41
+ browser (`container` mode and `share_browser_view`). Don't ask about it, and
42
+ don't set it up unless they ask.
43
+
44
+ **Other modes are opt-in.** Pass `mode` only when the developer explicitly asks
45
+ for one of these, or when `attach_browser()` fails even with Chrome for
46
+ Testing (for example, no display on Linux). In that case, use `container` if
47
+ Docker is running and say why in one line. Otherwise don't offer them, list
48
+ them, or pick one yourself, and don't reuse a mode from an earlier session.
49
+
50
+ | mode | when the developer asks for | needs |
44
51
  | --- | --- | --- |
45
- | `dedicated` | Chrome the agent launches with its own profile (`~/.reclaim/chrome-profile`), which persists, so a site stays signed in. The default choice. | nothing |
46
- | `attach` | The Chrome they already have open, with its tabs, extensions and logins. | `port` (default 9222) |
47
-
48
- Neither can be shared, and neither needs to be: the browser is on their screen.
49
-
50
- **Someone else has to sign in, or the site only works on a phone** — these two
51
- are the only modes that produce a link another person can open:
52
-
53
- | mode | what it is | needs | costs |
54
- | --- | --- | --- | --- |
55
- | `container` | The Reclaim runtime in Docker, on their machine. `share_browser_view` turns its viewer into a public link. | Docker running | free — their Docker and bandwidth |
56
- | `builder` | A browser in Reclaim's cloud. The link comes back immediately, with nothing to install. Also the only way to browse from a **specific country**. | Builder auth, `orgId` | **chargeable** — billed against the org's quota |
57
-
58
- **Say which one costs money.** They are otherwise interchangeable from the
59
- developer's point of view, so price and "do you have Docker" are the whole
60
- decision. Do not quietly pick the billed one.
61
-
62
- `custom` (any `ws://`/`wss://` CDP endpoint they supply) exists for someone who
63
- was handed an endpoint and knows why. Don't offer it unprompted; it cannot be
64
- shared either.
52
+ | `attach` | The Chrome they already have open, with its tabs, extensions, and logins | `port` (default 9222) |
53
+ | `container` | Someone else signs in, or a phone-only site. The Reclaim runtime runs in Docker on their machine; `share_browser_view` turns its viewer into a public link | Docker running |
54
+ | `builder` | A browser hosted by Reclaim, or browsing from a specific country. **Billed** against the org's quota; say so before you call it | Builder auth, `orgId` |
55
+ | `custom` | A `ws://` or `wss://` CDP endpoint they supply | `cdpUrl` |
65
56
 
66
57
  **`attach` gotcha (Chrome 136 and later):** the developer's Chrome must be
67
58
  started with BOTH `--remote-debugging-port=9222` AND
68
59
  `--user-data-dir=<non-default-path>`. The debug flag is silently ignored on the
69
- default profile. If `attach` fails, say this verbatim and offer `dedicated`.
60
+ default profile. If `attach` fails, say this verbatim. The default call with no
61
+ `mode` always works without it.
70
62
 
71
63
  **`builder` specifics:**
72
64
 
@@ -81,9 +73,7 @@ default profile. If `attach` fails, say this verbatim and offer `dedicated`.
81
73
  the suggested country, a country the site must be seen from, or skipping geo
82
74
  entirely (`skipCountry: true`, the deployment's default egress). The country
83
75
  is **fixed for the session's lifetime** and can't be changed after allocation.
84
- When a site is geo-restricted or a recipe pins a `geoLocation`, recommend a
85
- remote browser in that country rather than working around it locally. If the
86
- deployment has no proxy configured the browser is still allocated, just
76
+ If the deployment has no proxy configured the browser is still allocated, just
87
77
  without the requested country.
88
78
  - Costs one VERIFICATION-pool use under the org's normal plan restrictions.
89
79
  - Return the FULL `liveViewUrl` to the developer **verbatim, on its own line** —
@@ -129,8 +119,8 @@ If they say yes:
129
119
 
130
120
  - **`container` is the only mode this applies to.** `builder` returns a hosted
131
121
  `liveViewUrl` from `attach_browser` — relay that instead, full and verbatim.
132
- `dedicated`, `attach`, and `custom` have no view to share at all; if someone
133
- else needs to see the browser, say so and offer `container`.
122
+ `dedicated`, `attach`, and `custom` have no view to share at all. If the
123
+ developer asks to let someone else in, re-attach with `mode: "container"`.
134
124
  - **The URL is the ONLY credential.** The hostname Cloudflare hands out is the
135
125
  whole secret: there is no token and no login behind it, so whoever learns the
136
126
  URL has the browser, as the signed-in user. Give it to one person, say plainly
@@ -141,8 +131,8 @@ If they say yes:
141
131
  - **It shows the whole browser, not one tab.** A sign-in that opens a separate
142
132
  window is visible too.
143
133
  - **Opening it on a phone lays the page out for that phone**, with touch,
144
- pinch-zoom and the soft keyboard. That is the reason to reach for `container`
145
- when a site only works on mobile.
134
+ pinch-zoom and the soft keyboard. That is why `container` suits a site that
135
+ only works on mobile, when the developer asks for it.
146
136
  - **Stop a cloudflared process you started as soon as they're done.** If they
147
137
  started it, tell them to stop it. The browser keeps running. `dispose_browser` and `reset_session` stop the
148
138
  container itself, but not your cloudflared process.
@@ -181,18 +171,30 @@ If they say yes:
181
171
  - A container left over from a crashed run is **reused if it is still serving**,
182
172
  and replaced if it is not — a second attach does not need a different port.
183
173
 
184
- **When a local attach errors or `waitForCdpReady` reports a dead port**, walk
185
- these before retrying:
186
-
187
- - **Chrome binary not found** (the error lists the paths it tried). Set
188
- `RECLAIM_AGENT_CHROME_PATH` to the real binary — on Windows
189
- `setx RECLAIM_AGENT_CHROME_PATH "C:\path\to\chrome.exe"` (`setx`, not `$env:`,
190
- so future shells inherit it); on macOS/Linux `export` it. **Then fully restart
191
- Claude Code** — MCP servers snapshot env vars at launch.
174
+ **When the default local launch fails**, the error names the reason. Act on
175
+ it rather than switching modes:
176
+
177
+ - **No display** (Linux, neither `WAYLAND_DISPLAY` nor `DISPLAY` set). The MCP
178
+ server was started outside the desktop session. Ask the developer to add the
179
+ variable to the MCP server's environment (for example `DISPLAY=:0`, or
180
+ `WAYLAND_DISPLAY=wayland-0` with `XDG_RUNTIME_DIR`) and restart the agent.
181
+ - **Exited with code N**, followed by the browser's own stderr. Read the
182
+ stderr: it usually names the cause (a missing library, a sandbox error).
183
+ Chrome for Testing was already tried as a fallback when the error mentions
184
+ it.
185
+ - **Profile in use by a running browser (PID N).** A browser on
186
+ `~/.reclaim/chrome-profile` is open without the debugging port. Ask the
187
+ developer to close that window, then retry. Stale locks left by a crashed
188
+ browser are cleared automatically.
189
+ - **Did not answer CDP within 30000ms.** Retry once; a first start on a slow
190
+ disk can be slow. If it repeats, quote the stderr in the error.
191
+ - **Wrong browser picked.** Set `RECLAIM_AGENT_CHROME_PATH` to the binary — on
192
+ Windows `setx RECLAIM_AGENT_CHROME_PATH "C:\path\to\chrome.exe"` (`setx`, not
193
+ `$env:`, so future shells inherit it); on macOS/Linux `export` it. **Then
194
+ fully restart the agent** — MCP servers snapshot env vars at launch.
192
195
  - **Pre-existing `chrome.exe` hijacks the launch (Windows).** A new launch with
193
196
  `--remote-debugging-port` gets delegated to the running process group and the
194
197
  port silently never opens. `taskkill /F /IM chrome.exe`, then retry.
195
- - **Chrome 136+ default-profile rule** (above) — `dedicated` is always safe.
196
198
 
197
199
  <!-- section: login-detect -->
198
200
  **Check login state, wait if needed.** `eval_in_page(captureId, ...)` with a
@@ -310,9 +312,18 @@ won't help.
310
312
 
311
313
  Permitted edits: narrowing the `contains` template, a more distinctive
312
314
  xPath class/id when the synthesizer's pick is too generic, fixing `name`,
313
- adding/removing secret headers. Tempted to rewrite from scratch? Call
315
+ adding/removing secret headers. Make matcher edits with `update_draft`
316
+ (never by importing the agent's internals), then confirm them offline with
317
+ `check_matchers`, which runs the attestor's own redaction and match engine
318
+ on the captured response. Tempted to rewrite from scratch? Call
314
319
  `propose_provider` again with different inputs instead.
315
320
 
321
+ If `propose_provider` says the value has no key name next to it, or that
322
+ the window holds a number or a neighbouring value, the request can't give
323
+ a portable matcher (typical of SvelteKit `__data.json` positional arrays).
324
+ Pick the HTML page or another JSON API where the value sits under its own
325
+ key.
326
+
316
327
  <!-- section: secrets -->
317
328
  ### Protect secret values
318
329
 
@@ -393,6 +404,15 @@ supplies are context params.
393
404
  `check_attestor` after the developer fixes their network. Do not loop
394
405
  retries; one `run_proof` per change.
395
406
 
407
+ **Blocked non-browser clients** (`diagnosis.kind` is
408
+ `blocked-non-browser-client`: HTTP 403/406/429/503 or a challenge page,
409
+ often after `replay_request` succeeded): the site blocks clients that
410
+ aren't a real browser (bot protection or TLS fingerprint). Copying
411
+ headers or cookies won't help, and `run_proof_via_pod` doesn't change the
412
+ TLS fingerprint. Try a different endpoint that returns the value (a JSON
413
+ API the page calls), or stop and report. Run `check_matchers` first so a
414
+ recipe bug isn't mistaken for blocking.
415
+
396
416
  **Do not auto-retry with a different `attestorUrl`.** There is one live
397
417
  public attestor; backup hostnames (`attestor-2.*`, and so on) **do not
398
418
  exist**. `attestorUrl` is only for a developer-supplied private/staging URL.
@@ -421,30 +441,30 @@ supplies are context params.
421
441
  match — all stop signs. And never accept "the xPath looks too narrow, let me
422
442
  write a regex": the synthesizer already pairs the anchor with a
423
443
  structure-anchored named-capture regex. If extraction looks wrong, re-run
424
- `propose_provider` with a better request/target.
444
+ `propose_provider` with a better request/target. The permitted narrowing
445
+ edits go through `update_draft` and `check_matchers`.
425
446
  - Never rename or `%CTX%`-prefix the match-template param (see `params`).
426
447
 
427
448
  <!-- section: credentials -->
428
449
  ## Signing credential for the proof
429
450
 
430
- `run_proof` needs an `ownerAddress` — the eth proof-owner key whose private half
431
- signs the claim. It does **no** resolution itself. The private key is resolved
432
- locally (`RECLAIM_PRIVATE_KEY` env → a `RECLAIM_PRIVATE_KEY=0x…` line in the
433
- project `.env` → a file at `RECLAIM_PRIVATE_KEY_FILE`) and never enters the
434
- conversation.
435
-
436
- 1. **`resolve_owner_key`** returns only the derived address.
437
- - `{ ready: true, address }` → call `run_proof` with it. No prompts.
438
- - `{ ready: false }` → ask the developer once: **their own key** →
439
- `import_credentials({ keyFile })` (a path to a file holding the key; never
440
- ask them to paste it),
441
- or **a fresh one** → `issue_credentials({})` (a fine default for test
442
- proofs — owner identity doesn't matter). Either writes to the project
443
- `.env` (mode 0600, added to `.gitignore`), never to `~/.reclaim`. Then
444
- re-run `resolve_owner_key`.
445
- 2. Fully local — no network, no registration. The key is read live from `.env`
446
- on every `run_proof`, so **no restart** is needed after issuing/importing,
447
- and the existing draftId stays valid.
451
+ `run_proof` signs the claim with the eth proof-owner key. It resolves the key
452
+ itself (`RECLAIM_PRIVATE_KEY` env → a `RECLAIM_PRIVATE_KEY=0x…` line in the
453
+ project `.env` → a file at `RECLAIM_PRIVATE_KEY_FILE`); the private key never
454
+ enters the conversation. Don't pass `ownerAddress` and don't ask the developer
455
+ for a key.
456
+
457
+ 1. **No key yet:** `run_proof` (or `resolve_owner_key`) creates a local test
458
+ key in the project `.env` (mode 0600, added to `.gitignore`) and returns
459
+ `source: "generated"` with a `note`. Tell the developer once, after the
460
+ fact, that you created a local test signing key. Don't ask for approval:
461
+ owner identity doesn't matter for test proofs.
462
+ 2. **Developer wants their own key:** only when they say so,
463
+ `import_credentials({ keyFile })` (a path to a file holding the key; never
464
+ ask them to paste it).
465
+ 3. Fully local — no network, no registration. The key is read live from `.env`
466
+ on every `run_proof`, so **no restart** is needed after importing, and the
467
+ existing draftId stays valid.
448
468
 
449
469
  - **Advanced:** `RECLAIM_PRIVATE_KEY` in the MCP server env works too, but env
450
470
  vars are snapshotted at Claude Code startup — that route needs a full restart,
@@ -9,7 +9,7 @@ Copy this checklist and check off each step as you finish it. Read the linked
9
9
  topic before a step you aren't already fluent in.
10
10
 
11
11
  - [ ] 1. `reclaim_authenticate` once. → topic `auth`
12
- - [ ] 2. `attach_browser` — pick a browser source with the developer.
12
+ - [ ] 2. `attach_browser()` with no arguments — opens a visible local browser.
13
13
  → topic `browser`
14
14
  - [ ] 3. `list_tabs` → `start_capture(tabId)` ⇒ **`captureId`**, your handle for
15
15
  everything below. → topic `capture`
@@ -24,7 +24,7 @@ topic before a step you aren't already fluent in.
24
24
  - [ ] 7. Parameterize user-specific URL segments; mark secrets. → topic `params`
25
25
  - [ ] 8. `replay_request(captureId, draftId)`, then again with
26
26
  `withoutSecrets: true`. → topic `prove`
27
- - [ ] 9. `run_proof(captureId, draftId, ownerAddress)`.
27
+ - [ ] 9. `run_proof(captureId, draftId)`.
28
28
  → topics `prove`, `credentials`
29
29
  - [ ] 10. `create_provider_version_from_capture`. → topic `publish`
30
30
 
@@ -241,7 +241,7 @@ with no secrets/cookies, as an anonymous visitor.
241
241
  session and re-check. If the value only ever appears on public responses, tell
242
242
  the developer it can't be proven as an identity claim.
243
243
 
244
- **3. Prove.** `run_proof(captureId, draftId, ownerAddress)`. On
244
+ **3. Prove.** `run_proof(captureId, draftId)`. On
245
245
  `verified: true` the full claim is written to
246
246
  `~/.reclaim/claims/<timestamp>.json` and you get
247
247
  `{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`.
@@ -17,6 +17,21 @@ Never call a provider a "verification."**
17
17
  "Choose the provider".
18
18
  - **Intent unclear:** ask one short question.
19
19
 
20
+ ### Work order
21
+
22
+ Do the steps that need the developer first: sign in to Builder, attach the
23
+ browser, capture, and prove the provider. They show early whether a proof is
24
+ possible on the site. Write app code after the proof works, or while you wait
25
+ on the developer.
26
+
27
+ ### Asking the developer
28
+
29
+ When you need an answer or an action from the developer before you can
30
+ continue, end your turn with the question. Don't ask a blocking question
31
+ through a background or asynchronous question tool: the developer may never
32
+ see it. Put every link they need, such as a sign-in or verification URL, in
33
+ that same message.
34
+
20
35
  ### Choose the provider
21
36
 
22
37
  Default: build a new provider for the claim the developer described. Use an
@@ -49,6 +64,9 @@ check and holds results for you to read. No tunnel or public URL is needed.
49
64
  1. Call `read_callback_events`.
50
65
  2. `create_verification_session`, then give the developer `verificationUrl`.
51
66
  3. Poll `get_verification_session` until it is terminal.
67
+ `list_verification_events` shows progress. Don't read the hosted
68
+ verification page for progress: it streams the site as an image, so text
69
+ and DOM checks of it come back empty.
52
70
  4. Call `read_callback_events({ sessionId })` for the verified result.
53
71
 
54
72
  When you set up a new provider build, call `check_attestor` once. If it fails,
@@ -67,7 +85,8 @@ tell the developer before they spend time on the build.
67
85
 
68
86
  ## Integrating into an app
69
87
 
70
- Call `start_integration`, then read `how_it_works({ topic: "integration" })`
88
+ Call `start_integration` and follow its order: prove the provider first, then
89
+ write app code. Read `how_it_works({ topic: "integration" })`
71
90
  for the HTTP session workflow, the `api=2` boundary, result verification, and
72
91
  TEE binding. Edit the app with your file tools. Use the HTTP API
73
92
  (`POST /verifications/sessions`, open `verificationUrl`, poll
@@ -75,9 +94,17 @@ TEE binding. Edit the app with your file tools. Use the HTTP API
75
94
  `@reclaimprotocol/client`. The only exception is `results.receive` on the
76
95
  callback, or when the developer asks for the client package.
77
96
 
97
+ The app's frontend renders every state and every failure, never only a
98
+ spinner. Follow "UI states" in `how_it_works({ topic: "integration" })`. The
99
+ callback route responds `error.retryable ? 503 : 200` for a thrown
100
+ `ResultVerificationError`; a result that failed verification gets 2xx.
101
+
78
102
  For local development, call `proxy_callback_events({ port, path })` to forward
79
103
  events to the app's callback route on localhost. The local listener works only
80
- while this MCP server runs. Production needs a webhook:
104
+ while this MCP server runs. It keeps events in order per session and retries
105
+ a session the app rejects with backoff, so other sessions keep flowing. When
106
+ its status lists `blocked` sessions, read that session's callback route
107
+ response and logs, and fix the app. Production needs a webhook:
81
108
  `create_org_callback_subscription` with the deployed app's public URL. Without
82
109
  one, session creation returns 409.
83
110
 
@@ -114,13 +141,24 @@ script → `find_requests_containing` → `propose_provider` ⇒ `draftId` →
114
141
 
115
142
  - **Data points:** propose only values the claim needs, usually one or two.
116
143
  Never add identity or profile fields. Name each for what it proves.
117
- - **Browser:** ask who signs in. The developer themselves: `dedicated` or
118
- `attach` (local). Someone else, or a phone-only site: `container` (Docker,
119
- free, shareable with `share_browser_view`) or `builder` (hosted,
120
- **chargeable**). Say which costs money.
144
+ - **Browser:** call `attach_browser()` with no arguments. It opens a visible
145
+ local browser with the persistent Reclaim profile, and falls back to Chrome
146
+ for Testing itself when no installed browser can be attached. Don't ask
147
+ which browser or who signs in. Use another `mode` only when that fails too,
148
+ or when the developer explicitly asks for it (topic `browser`). Tell the
149
+ developer once, as a side note, that a shareable link can be set up if a
150
+ volunteer is helping them sign in; don't ask about it or set it up unless
151
+ they ask.
121
152
  - **Public endpoints:** after `replay_request` succeeds, re-run it with
122
153
  `withoutSecrets: true`. If the value still extracts, the endpoint is public
123
154
  and the proof binds to nobody. Find an auth-bound request.
155
+ - **Country:** `run_proof` sends the attestor's request from the developer's
156
+ own IP country, which is what `{{DYNAMIC_GEO}}` becomes when they verify;
157
+ `egress` in the result says which country. If a site that serves one
158
+ country (for example, a US airline) blocks the attestor, retry once with
159
+ `geoLocation: "US"`. If that verifies, pin it with
160
+ `update_draft({ geoLocation: "US" })` so the published version goes out
161
+ from there too.
124
162
  - **Drafting limits** (topics `draft`, `params`): never `hash` a value that
125
163
  also appears in the request URL. A param named with `SECRET` may appear in a
126
164
  URL only within a small character budget (2 under `key-update`, 12 under