@reclaimprotocol/agent 0.1.22 → 0.1.24

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 (163) hide show
  1. package/README.md +55 -20
  2. package/lib/authoring/backend.d.ts +13 -10
  3. package/lib/authoring/tools.js +106 -9
  4. package/lib/cdp/browser-proxy.d.ts +78 -0
  5. package/lib/cdp/browser-proxy.js +186 -0
  6. package/lib/cdp/capture-engine.js +8 -7
  7. package/lib/cdp/client.d.ts +10 -0
  8. package/lib/cdp/geo.d.ts +10 -0
  9. package/lib/cdp/geo.js +36 -0
  10. package/lib/cdp/index.d.ts +1 -0
  11. package/lib/cdp/index.js +1 -0
  12. package/lib/live/cloudflared.d.ts +15 -0
  13. package/lib/live/cloudflared.js +268 -0
  14. package/lib/live/page.d.ts +20 -0
  15. package/lib/live/page.js +134 -0
  16. package/lib/live/tunnel.d.ts +23 -0
  17. package/lib/live/tunnel.js +120 -0
  18. package/lib/live/viewer.d.ts +28 -0
  19. package/lib/live/viewer.js +232 -0
  20. package/lib/mcp/api/operations.gen.js +34 -26
  21. package/lib/mcp/api/to-zod.d.ts +1 -1
  22. package/lib/mcp/api/to-zod.js +1 -1
  23. package/lib/mcp/api/tools.js +1 -1
  24. package/lib/mcp/instructions.d.ts +15 -2
  25. package/lib/mcp/instructions.js +101 -38
  26. package/lib/mcp/start.js +4 -3
  27. package/lib/mcp/tools/agent/analyze.js +5 -1
  28. package/lib/mcp/tools/agent/attach.d.ts +8 -1
  29. package/lib/mcp/tools/agent/attach.js +166 -22
  30. package/lib/mcp/tools/agent/build.d.ts +13 -8
  31. package/lib/mcp/tools/agent/build.js +22 -11
  32. package/lib/mcp/tools/agent/capture.js +5 -1
  33. package/lib/mcp/tools/agent/eval.js +3 -1
  34. package/lib/mcp/tools/agent/find.js +6 -5
  35. package/lib/mcp/tools/agent/how-it-works.d.ts +15 -7
  36. package/lib/mcp/tools/agent/how-it-works.js +57 -17
  37. package/lib/mcp/tools/agent/live-view.d.ts +21 -0
  38. package/lib/mcp/tools/agent/live-view.js +153 -0
  39. package/lib/mcp/tools/agent/pod-proof.d.ts +8 -0
  40. package/lib/mcp/tools/agent/pod-proof.js +64 -0
  41. package/lib/mcp/tools/agent/propose.js +29 -3
  42. package/lib/mcp/tools/agent/proxy.d.ts +3 -0
  43. package/lib/mcp/tools/agent/proxy.js +199 -0
  44. package/lib/mcp/tools/agent/publish.js +49 -77
  45. package/lib/mcp/tools/agent/session.d.ts +2 -1
  46. package/lib/mcp/tools/agent/session.js +26 -7
  47. package/lib/mcp/tools/agent/shared.js +17 -8
  48. package/lib/mcp/tools/authenticate/authenticate.d.ts +11 -0
  49. package/lib/mcp/tools/authenticate/authenticate.js +143 -121
  50. package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -3
  51. package/lib/mcp/tools/authenticate/builder-access.js +8 -3
  52. package/lib/mcp/tools/mode/mode.d.ts +1 -1
  53. package/lib/mcp/tools/mode/mode.js +26 -7
  54. package/lib/old/client.d.ts +9 -0
  55. package/lib/old/client.js +19 -1
  56. package/lib/old/instructions.d.ts +9 -1
  57. package/lib/old/instructions.js +4 -6
  58. package/lib/old/login-cdp.d.ts +2 -2
  59. package/lib/old/login-cdp.js +46 -6
  60. package/lib/old/to-register.d.ts +20 -8
  61. package/lib/old/to-register.js +7 -1
  62. package/lib/old/tools/authenticate.js +9 -1
  63. package/lib/old/tools/get-provider-info.js +6 -5
  64. package/lib/old/tools/issue-app-credentials.js +3 -2
  65. package/lib/old/tools/me-providers.js +3 -1
  66. package/lib/old/tools/publish.d.ts +1 -1
  67. package/lib/old/tools/publish.js +132 -129
  68. package/lib/old/tools/update-provider-metadata.d.ts +1 -1
  69. package/lib/old/tools/update-provider-metadata.js +1 -1
  70. package/lib/old/tools/verify-sdk-setup.js +1 -1
  71. package/lib/proof/attestor.d.ts +3 -3
  72. package/lib/proof/debug-response-dump.js +4 -4
  73. package/lib/proof/index.d.ts +1 -0
  74. package/lib/proof/index.js +1 -0
  75. package/lib/proof/page-replay.d.ts +36 -0
  76. package/lib/proof/page-replay.js +76 -0
  77. package/lib/proof/pod-prover.d.ts +47 -0
  78. package/lib/proof/pod-prover.js +155 -0
  79. package/lib/proof/replay.d.ts +28 -0
  80. package/lib/proof/replay.js +39 -18
  81. package/lib/provider/dom-xpath.js +11 -9
  82. package/lib/provider/draft.d.ts +13 -8
  83. package/lib/provider/draft.js +47 -13
  84. package/lib/provider/injection-doc.d.ts +23 -1
  85. package/lib/provider/injection-doc.js +34 -21
  86. package/lib/provider/match-redaction-warning.d.ts +2 -2
  87. package/lib/provider/match-redaction-warning.js +3 -3
  88. package/lib/provider/matcher.js +122 -12
  89. package/lib/provider/secrets.d.ts +3 -3
  90. package/lib/provider/secrets.js +3 -3
  91. package/lib/provider/to-version.d.ts +12 -9
  92. package/lib/provider/to-version.js +16 -4
  93. package/lib/tools/registry.d.ts +5 -5
  94. package/lib/tools/registry.js +5 -5
  95. package/package.json +4 -3
  96. package/samples/demo-server/README.md +7 -6
  97. package/skill/SKILL.builder.md +356 -189
  98. package/skill/SKILL.common.md +314 -79
  99. package/skill/SKILL.old.md +340 -155
  100. package/skill/builder-mode.md +58 -17
  101. package/skill/hash-validation.md +170 -0
  102. package/skill/interception.md +235 -0
  103. package/skill/js-sdk-integration.md +13 -7
  104. package/skill/old-mode.md +53 -12
  105. package/skill/user-script.md +61 -0
  106. package/src/authoring/backend.ts +13 -10
  107. package/src/authoring/tools.ts +128 -9
  108. package/src/cdp/capture-engine.ts +8 -7
  109. package/src/cdp/client.ts +8 -0
  110. package/src/cdp/geo.ts +52 -0
  111. package/src/cdp/index.ts +1 -0
  112. package/src/consts.ts +1 -0
  113. package/src/live/cloudflared.ts +348 -0
  114. package/src/live/page.ts +145 -0
  115. package/src/live/tunnel.ts +157 -0
  116. package/src/live/viewer.ts +293 -0
  117. package/src/mcp/api/operations.gen.ts +34 -26
  118. package/src/mcp/api/to-zod.ts +1 -1
  119. package/src/mcp/api/tools.ts +1 -1
  120. package/src/mcp/instructions.ts +121 -48
  121. package/src/mcp/start.ts +4 -3
  122. package/src/mcp/tools/agent/analyze.ts +5 -1
  123. package/src/mcp/tools/agent/attach.ts +194 -22
  124. package/src/mcp/tools/agent/build.ts +26 -15
  125. package/src/mcp/tools/agent/capture.ts +5 -1
  126. package/src/mcp/tools/agent/eval.ts +4 -1
  127. package/src/mcp/tools/agent/find.ts +6 -5
  128. package/src/mcp/tools/agent/how-it-works.ts +62 -21
  129. package/src/mcp/tools/agent/live-view.ts +198 -0
  130. package/src/mcp/tools/agent/pod-proof.ts +90 -0
  131. package/src/mcp/tools/agent/propose.ts +31 -3
  132. package/src/mcp/tools/agent/publish.ts +54 -78
  133. package/src/mcp/tools/agent/session.ts +26 -5
  134. package/src/mcp/tools/agent/shared.ts +19 -8
  135. package/src/mcp/tools/authenticate/authenticate.ts +167 -165
  136. package/src/mcp/tools/authenticate/builder-access.ts +8 -3
  137. package/src/mcp/tools/mode/mode.ts +28 -7
  138. package/src/old/client.ts +30 -1
  139. package/src/old/instructions.ts +4 -10
  140. package/src/old/login-cdp.ts +50 -6
  141. package/src/old/to-register.ts +28 -8
  142. package/src/old/tools/authenticate.ts +9 -1
  143. package/src/old/tools/get-provider-info.ts +6 -5
  144. package/src/old/tools/issue-app-credentials.ts +3 -2
  145. package/src/old/tools/me-providers.ts +3 -1
  146. package/src/old/tools/publish.ts +140 -130
  147. package/src/old/tools/update-provider-metadata.ts +1 -1
  148. package/src/old/tools/verify-sdk-setup.ts +1 -1
  149. package/src/proof/attestor.ts +3 -3
  150. package/src/proof/debug-response-dump.ts +4 -4
  151. package/src/proof/index.ts +8 -0
  152. package/src/proof/page-replay.ts +131 -0
  153. package/src/proof/pod-prover.ts +231 -0
  154. package/src/proof/replay.ts +60 -19
  155. package/src/provider/dom-xpath.ts +11 -9
  156. package/src/provider/draft.ts +56 -13
  157. package/src/provider/injection-doc.ts +37 -24
  158. package/src/provider/match-redaction-warning.ts +3 -3
  159. package/src/provider/matcher.ts +151 -10
  160. package/src/provider/secrets.ts +3 -3
  161. package/src/provider/to-version.ts +30 -13
  162. package/src/tools/registry.ts +5 -5
  163. package/skill/injection-api.md +0 -335
@@ -1,177 +1,362 @@
1
- <!-- common: header -->
2
-
1
+ <!-- topic: overview -->
2
+ # Authoring a Reclaim provider (old-devtools backend)
3
+
4
+ <!-- common: terminology -->
5
+
6
+ ## The loop, at a glance
7
+
8
+ Copy this checklist and check off each step as you finish it. Read the linked
9
+ topic before a step you aren't already fluent in.
10
+
11
+ - [ ] 1. `reclaim_authenticate` once. → topic `auth`
12
+ - [ ] 2. `attach_browser` — pick a browser source with the developer.
13
+ → topic `browser`
14
+ - [ ] 3. `list_tabs` → `start_capture(tabId)` ⇒ **`captureId`**, your handle for
15
+ everything below. → topic `capture`
16
+ - [ ] 4. Drive the page: `navigate`, `wait_for_page`, `eval_in_page`,
17
+ `get_cookies`. Detect login, land on the page showing the value, trigger
18
+ the request. → topic `capture`
19
+ - [ ] 5. `find_requests_containing` → `analyze_request_constraints` → pick an
20
+ **authenticated** request. → topic `capture`
21
+ - [ ] 6. `propose_provider(captureId, requestId, …)` ⇒ **`draftId`** (recipe +
22
+ captured secrets, held server-side — secrets never enter the transcript).
23
+ → topic `draft`
24
+ - [ ] 7. Parameterize user-specific URL segments; mark secrets. → topic `params`
25
+ - [ ] 8. `replay_request(captureId, draftId)`, then again with
26
+ `withoutSecrets: true`. → topic `prove`
27
+ - [ ] 9. `run_proof(captureId, draftId, ownerAddress)`.
28
+ → topics `prove`, `credentials`
29
+ - [ ] 10. `create_provider_version_from_capture`. → topic `publish`
30
+
31
+ Multi-request provider? Run steps 6–9 once per request, then publish every
32
+ proven draft in one call. See topic `publish`.
33
+
34
+ **This mode's limits.** Only GET/POST requests can be captured into providers.
35
+ There is no organization management, credential-linking, or public-review
36
+ workflow — the narrow `authenticate_builder` / `list_builder_organizations`
37
+ exception exists only to allocate Builder-hosted authoring browsers.
38
+ `get_me_providers` is the only provider-listing tool here.
39
+
40
+ **Diagnosing a session:** `session_analytics_logs` fetches a verification
41
+ session's analytics logs by id (matches, claims, failures).
42
+
43
+ <!-- topic: auth -->
3
44
  ## Getting authenticated
4
45
 
5
- 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."_
6
-
7
- - Force a fresh login (ignore the cached identity): `reclaim_authenticate({ useBrowser: true })`.
8
- - Skip the browser: `reclaim_authenticate({ token: "<firebase-id-token>" })`, or `reclaim_authenticate({ ethAddress: "0x..." })` for an already-provisioned eth user.
9
- - Tokens are short-lived (~1h). If a later call returns 401, call `reclaim_authenticate({})` again.
10
-
11
- ## The loop
12
-
13
- <!-- common: step-attach -->
14
-
15
- 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.
16
-
17
- 3. **Check login state, wait if needed.** Use `eval_in_page(tabId, ...)` to evaluate a site-specific check.
18
-
19
- **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.
20
-
21
- Examples (always verify against the live page if these break — sites change selectors):
22
- - GitHub: `(document.querySelector('meta[name="user-login"]')?.content?.length || 0) > 0 && !document.querySelector('a.HeaderMenu-link[href="/login"]')`
23
- - X / Twitter: `!!document.querySelector('[data-testid="AppTabBar_Profile_Link"]') && !document.querySelector('[data-testid="loginButton"]')`
24
- - Generic shape: `<element-present-only-when-logged-in> && !<login-button-present-only-when-logged-out>`
25
-
26
- If the result is truthy, skip to step 4.
27
-
28
- 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.
29
-
30
- **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.
31
-
32
- 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.
33
-
34
- 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.
35
-
36
- <!-- common: step-identify-value -->
37
-
38
- <!-- common: step-find-request -->
39
-
40
- 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:
41
- - **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).
42
- - **`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.
43
- - **`oprf`**. Client-side ZK OPRF via gnark. Strongest privacy. Requires gnark install (Linux/macOS in practice). Don't suggest this on Windows.
44
- - **`oprf-mpc`**. TEE-based MPC OPRF. Requires a TEE attestor deployment. Niche; only suggest if the dev brings it up.
45
-
46
- **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:
47
-
48
- > "Do you want this value kept private from the attestor via OPRF?
49
- >
50
- > - **No** (default) — value visible to attestor
51
- > - **`oprf-raw`** — works on Windows / without extra deps; weakest of the OPRF variants
52
- > - **`oprf`** — gnark/ZK; strongest privacy but requires Linux/macOS + gnark install
53
- > - **`oprf-mpc`** — TEE-based; requires a TEE attestor"
54
-
55
- **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.
56
-
57
- **Hard constraint: OPRF and the request URL are mutually exclusive for the same value.** The entire point of `hash` is that the attestor never sees the value in cleartext — but any value templated into the request URL (`{{name}}`, `{{context_<name>}}`, or left literal) is always signed into the attestor's context in the clear, regardless of hashing. So if the target's value ALSO appears anywhere in the request URL, hashing it is not just pointless, it doesn't verify. Check this before requesting `hash` for a target: if that value shows up in the captured URL too, either drop `hash` for that target or capture a different request whose URL doesn't carry the value. `propose_provider` throws a clean error if you try anyway — same as the byte cap above.
58
-
59
- 9. **Draft.** Call `propose_provider(captureId, requestId, target, name, hash?)`. Pass `hash` only if the dev opted in. Inspect the returned provider:
60
- - Is the right header marked secret? (cookies, Authorization, X-\* tokens)
61
- - Is `responseMatches[0].value` a `contains` with a `{{name}}` template (matching the captured value's structural context)?
62
- - Is `paramValues[name]` set to the original captured value?
63
- - 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?)
46
+ Call `reclaim_authenticate({})` with no arguments. It opens the Reclaim
47
+ dashboard in Chrome, waits for the developer to sign in, reads the token from
48
+ the page and closes the tab — a still-valid cached identity is reused first, so
49
+ it's often instant. Tell the developer once: _"A browser window is opening the
50
+ Reclaim dashboard — please sign in and I'll continue automatically."_
51
+
52
+ - Force a fresh login: `reclaim_authenticate({ useBrowser: true })`.
53
+ - Skip the browser: `reclaim_authenticate({ token: "<firebase-id-token>" })`, or
54
+ `reclaim_authenticate({ ethAddress: "0x…" })` for a provisioned eth user.
55
+ - Tokens are short-lived (~1h). Any later 401 → call `reclaim_authenticate({})`
56
+ again.
57
+
58
+ ### Two independent auth systems — don't confuse them
59
+
60
+ `reclaim_authenticate` and `authenticate_builder` log into DIFFERENT backends
61
+ and are NOT interchangeable.
62
+
63
+ - **`reclaim_authenticate`** authorizes everything in this mode:
64
+ `create_provider_version_from_capture`, `get_me_providers`, and the rest.
65
+ - **`authenticate_builder`** covers exactly two Builder-exclusive features:
66
+ allocating a Popcorn remote browser (`attach_browser mode="builder"`) and
67
+ `list_builder_organizations`. It authorizes nothing else.
68
+
69
+ `get_devtools_mode` reports the presence of both. If a publish or
70
+ `get_me_providers` call fails with an auth error even though
71
+ `reclaim_authenticate` ran earlier, the old-devtools token has expired — re-run
72
+ it. Don't assume `authenticate_builder` fixes it.
73
+
74
+ <!-- topic: browser -->
75
+ <!-- common: browser -->
76
+
77
+ <!-- topic: capture -->
78
+ ## Capture
79
+
80
+ 1. **Attach** — topic `browser`.
81
+
82
+ 2. **Pick a tab and start the capture.** `list_tabs`, then
83
+ `start_capture(tabId)` ⇒ a **`captureId`**: the session handle every browser
84
+ and inspect tool takes as its first argument (`navigate`, `eval_in_page`,
85
+ `wait_for_page`, `get_cookies`, `list_requests`, `find_requests_containing`,
86
+ `propose_provider`). If a tab is already on the right origin use it,
87
+ otherwise the first is usually fine. Capturing from the start is harmless —
88
+ it also records the login navigation. Then `navigate(captureId, url)` to
89
+ the site you inferred from the developer's request ("GitHub username" →
90
+ `https://github.com`). Don't ask the developer to navigate.
91
+
92
+ 3. <!-- common: login-detect -->
93
+
94
+ 4. **Navigate to the page that displays the value.** Construct the URL from what
95
+ you know about the site; eval for anything you need (for example, GitHub's
96
+ `meta[name="user-login"]`'s `content`), then `navigate(captureId, url)`.
97
+
98
+ 5. **Trigger the request.** Capture has been live since step 2, so make the
99
+ call fire: `eval_in_page(captureId, "location.reload()")`, or click an
100
+ element / hit a SPA route programmatically. Don't ask the developer to click.
101
+
102
+ 6. <!-- common: identify-value -->
103
+
104
+ 7. <!-- common: find-request -->
105
+
106
+ <!-- topic: draft -->
107
+ ## Draft the provider
108
+
109
+ <!-- common: oprf -->
110
+
111
+ **Call `propose_provider`.** It returns a **`draftId`** — the handle for
112
+ `replay_request` and `run_proof` — plus the secret-free `provider` and
113
+ `secretRefs`. The recipe **and the captured secret values** live server-side
114
+ under that `draftId`; the secrets never enter the transcript, so you carry the
115
+ `draftId`, not the provider JSON, into replay/prove.
116
+
117
+ **Extracting MORE THAN ONE value?** Prefer a **single request whose response
118
+ contains all of them** and draft them together — one `responseMatches` +
119
+ `responseRedactions` pair per value. Find that request by running
120
+ `find_requests_containing` for each value and picking a `requestId` common to
121
+ all. Then pass a `targets` array, each entry with its `value` (exactly as it
122
+ appears) and a distinct `name`:
123
+
124
+ ```
125
+ propose_provider(captureId, requestId, name, targets: [
126
+ { value: "Scratch-net", name: "username" },
127
+ { value: "Alex", name: "name" },
128
+ ])
129
+ ```
130
+
131
+ The shorthand `propose_provider(captureId, requestId, target, name, hash?)`
132
+ still works for one target.
133
+
134
+ **Inspect what comes back:**
135
+
136
+ - Is the right header marked secret? (cookies, `Authorization`, `X-*` tokens)
137
+ - Is `responseMatches[0].value` a `contains` with a `{{name}}` template matching
138
+ the captured value's structural context?
139
+ - Is `paramValues[name]` the original captured value?
140
+ - Does each `responseRedactions` entry carry an `xPath`/`jsonPath` anchor
141
+ **plus** a named-capture `regex` isolating the value? (With `hash`, the regex
142
+ also carries `hash: oprf-…`.)
143
+
144
+ **Provenance — match the structure, never the bare value.** The `contains`
145
+ template and the redaction regex are always anchored on the value's surrounding
146
+ context — the JSON key (`"login":"{{username}}"`) or the enclosing element —
147
+ never `{{username}}` alone. That ties the proof to *where the value legitimately
148
+ appears*; a bare-value match would let a user surface the same string from any
149
+ field they control. The synthesizer does this for you.
150
+
151
+ **Canonical drafted shape** (no hash):
152
+
153
+ ```json
154
+ {
155
+ "responseMatches": [
156
+ { "type": "contains", "value": "\"login\":\"{{username}}\"" }
157
+ ],
158
+ "responseRedactions": [
159
+ {
160
+ "xPath": "//script[@id=\"client-env\"]",
161
+ "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\""
162
+ }
163
+ ],
164
+ "paramValues": { "username": "Scratch-net" }
165
+ }
166
+ ```
167
+
168
+ With `hash: 'oprf-raw'`, the redaction gains `"hash": "oprf-raw"`. Nothing else
169
+ changes.
64
170
 
65
- **Canonical drafted shape** (no hash):
66
-
67
- ```json
68
- {
69
- "responseMatches": [
70
- { "type": "contains", "value": "\"login\":\"{{username}}\"" }
71
- ],
72
- "responseRedactions": [{ "xPath": "//script[@id=\"client-env\"]" }],
73
- "paramValues": { "username": "Scratch-net" }
74
- }
75
- ```
76
-
77
- **Canonical drafted shape** (with `hash: 'oprf-raw'`):
171
+ <!-- common: rewrite-shape-block -->
78
172
 
79
- ```json
80
- {
81
- "responseMatches": [
82
- { "type": "contains", "value": "\"login\":\"{{username}}\"" }
83
- ],
84
- "responseRedactions": [
85
- {
86
- "xPath": "//script[@id=\"client-env\"]",
87
- "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\"",
88
- "hash": "oprf-raw"
89
- }
90
- ],
91
- "paramValues": { "username": "Scratch-net" }
92
- }
93
- ```
173
+ <!-- topic: params -->
174
+ ## Parameters: extracted, consumer-supplied, and secret
94
175
 
95
- <!-- common: rewrite-shape-block -->
176
+ **Parameterize user-specific URL segments** so the provider isn't pinned to one
177
+ user. The matcher already puts the *captured* value in `paramValues`. If the URL
178
+ *also* carries user-identifying segments, those are **consumer-supplied context
179
+ params** — their values come from the consumer at verification time, not from
180
+ extraction.
96
181
 
97
- 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, those are **consumer-supplied context params** — at verification time their values come from the Consumer's session `context`, not from extraction. They use a namespaced placeholder, and the namespace differs by mode:
182
+ Replace them with `{{context_<name>}}` and add
183
+ `paramValues: { …, "context_username": "Scratch-net" }`. **This backend does not
184
+ support the dotted `{{context.<name>}}` form the Builder uses**, and there is no
185
+ `requiredContext` declaration here — the convention is the whole mechanism.
98
186
 
99
- - Replace user-specific segments with `{{context_<name>}}` and `paramValues: { ..., "context_username": "Scratch-net" }`.
100
- - Query parameters with user-specific values → same treatment.
187
+ - Query params with user-specific values → same treatment.
101
188
  - Generic segments (`/api/v3`, `/users`, `/repos`) stay literal.
102
- - If the URL has no user-identifying segments (e.g. `/settings/profile`), leave it untouched — the matcher already handled the value via paramValues.
189
+ - No user-identifying segments (for example, `/settings/profile`) → leave it
190
+ alone.
103
191
 
104
- **Marking a value secret:** name ANY param with `SECRET` anywhere in the key (case-insensitive) — a bare `{{SECRET_x}}` (e.g. a value extracted earlier in a multi-request flow) just as much as a consumer-supplied `{{context_SECRET_apiKey}}` — and it's routed to `secretParams.paramValues` (`classifyParamName`/`splitSecretParamValues`), same naming convention `reclaim-inapp-sdk`/`reclaim-portal` use for their `paramValues`. The `context_` prefix is a namespace convention (consumer-supplied vs extracted-from-traffic); the attestor itself just looks up `{{whatever}}` in `paramValues`/`secretParams.paramValues` and doesn't care about the prefix. Headers are secret by the OPPOSITE rule — an ALLOWLIST, not a name check: any header not in a small fixed public list (`user-agent`, `accept`, `accept-language`, `accept-encoding`, `sec-fetch-*`, `origin`, `x-requested-with`, `sec-ch-ua*`) is secret by default, including `Cookie`/`Authorization` and anything custom (`classifyHeader`/`splitSecrets`).
192
+ <!-- common: secrets -->
105
193
 
106
- A secret value CAN be used in the URL, geoLocation, request body, response match, or response redactions — write-redaction hides it there. The value of any parameter marked secret is omitted from the final proof entirely. There's still a limitation specific to the URL: only up to a small TOTAL character budget summed across every secret value used in that URL (`secretUrlCharBudget`), not a per-value allowance — 2 characters under `key-update` (the default), 12 under `zk`. Using a longer-than-budget secret in the URL causes the attestor's claim creation to fail outright.
194
+ The `context_` prefix is a naming convention (consumer-supplied vs
195
+ extracted-from-traffic). The attestor itself only looks up `{{whatever}}` in
196
+ `paramValues`/`secretParams.paramValues` and doesn't care about the prefix.
107
197
 
108
198
  <!-- common: no-rename-param -->
109
199
 
110
- `propose_provider` returns `verificationWarnings` for the calls only an author can make (currently: a captured value baked literally into the URL, needing the `{{context_<name>}}`-vs-bare-vs-re-capture decision). They are advisory, never blocking — **surface each warning to the dev verbatim** and resolve it before publishing.
111
-
112
- 11. **Replay.** Call `replay_request(provider, secretParamsRef)`. Three outcomes:
200
+ `propose_provider` returns `verificationWarnings` for calls only an author can
201
+ make (currently: a captured value baked literally into the URL, needing the
202
+ `{{context_<name>}}`-vs-bare-vs-recapture decision). Advisory, never blocking —
203
+ **surface each one verbatim** and resolve it before publishing.
113
204
 
114
- - `matchesOriginal: true` → proceed.
115
- - `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.
116
- - `matchesOriginal: false` with stable diff (timestamps etc.) → tighten the matcher and retry.
205
+ <!-- topic: prove -->
206
+ ## Replay, then prove
117
207
 
118
- **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.
208
+ **1. Replay.** `replay_request(captureId, draftId)`:
119
209
 
120
- - 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."
121
- - 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.
122
-
123
- 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.
124
-
125
- **Missing signing credential (`errorKind: "missing-owner-key"`)** — the dev hasn't set up an eth proof-owner key yet. Tell them what's happening, then drive the bootstrap yourself: call `resolve_owner_key`; if it returns no key, ask once _"shall I mint a fresh eth key via `issue_credentials` (or `import_credentials` if you already hold one)? It's written to the project `.env` (mode 0600, gitignored) for this and future sessions."_; on yes, call `issue_credentials` (or `import_credentials`) and retry the same `run_proof` call. **No restart needed** — the key is read live from `.env` on each `run_proof`, and the existing `secretParamsRef` from `propose_provider` is still valid.
210
+ - `matchesOriginal: true` → proceed.
211
+ - `false` with `hints` mentioning auth/csrf → re-trigger the action yourself
212
+ (navigate or reload with the tools) and redraft. Don't ask the developer to
213
+ redo it.
214
+ - `false` with a stable diff (timestamps, and so on) → tighten the matcher
215
+ and retry.
216
+
217
+ **2. The authentication-bound check — mandatory.**
218
+ `replay_request(captureId, draftId, withoutSecrets: true)` re-issues the request
219
+ with no secrets/cookies, as an anonymous visitor.
220
+
221
+ - FAILS to extract (401/403/redirect, or the value is gone) → the endpoint is
222
+ auth-bound. Good: the proof genuinely means "the holder of these secrets saw
223
+ this value."
224
+ - STILL extracts the value → the endpoint is **PUBLIC**; the proof binds to
225
+ nobody. **Do NOT prove it.** Find a request whose response depends on the
226
+ session and re-check. If the value only ever appears on public responses, tell
227
+ the developer it can't be proven as an identity claim.
228
+
229
+ **3. Prove.** `run_proof(captureId, draftId, ownerAddress)`. On
230
+ `verified: true` the full claim is written to
231
+ `~/.reclaim/claims/<timestamp>.json` and you get
232
+ `{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`.
233
+ `claim` is display-ready (huge `request` payload stripped, byte arrays base64'd)
234
+ — **show it to the developer as a fenced JSON block at the end**, with the
235
+ provider JSON. That's the final result.
236
+
237
+ **`errorKind: "missing-owner-key"`** → no eth proof-owner key yet. Tell the
238
+ developer what's happening and drive the bootstrap yourself — see topic
239
+ `credentials`. No restart needed, and the existing `draftId` stays valid.
126
240
 
127
241
  <!-- common: prove-tail -->
128
242
 
129
- 13. **Publish the proved provider.** `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.
130
-
131
- Call `create_provider_version_from_capture`:
132
- - **New provider:** omit `providerId`. It registers a new provider (version 1.0.0). Pass `provider`, `initialUrl`, and optionally `description`, `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
133
- - **New version of an existing provider:** pass its `providerId` — the patch is auto-bumped above the highest existing version. Pass `notes` (≥10 chars) as the change note.
134
- - **Multi-request provider** (e.g. a landing-page call plus a details call, or a CSRF-preflight/refresh-token chain): run steps 5–12 ONCE PER REQUEST — a separate `propose_provider` → `replay_request` → `run_proof` cycle for each — then publish all of them together in ONE call by passing `providers: [draftA, draftB, ...]` instead of the single-draft `provider`. Every entry becomes its own parallel `requestData` entry on the SAME version, and all are required at verification time.
135
- - `get_me_providers({ searchQuery? })` lists the providers you own — use it to find a `providerId` or to confirm the result. It's the only provider-listing tool in this mode.
136
- - **Before adding a version to an EXISTING provider, call `get_provider_version_config(providerId)` first.** This backend has no partial-patch endpoint — a version replaces the WHOLE `providerConfig` — so seeing what's currently live (`customInjection`, `userAgent`, `pageTitle`, `stepsToFollow`, etc.) tells you what you're about to change vs. carry forward before you commit to it, rather than finding out after. (`create_provider_version_from_capture` carries forward anything you omit automatically, but knowing what that IS before publishing beats being surprised after.)
137
- - **To rename, redescribe, retag, flip `providerType`, or toggle `isActive` on a provider you own — WITHOUT creating a new version** — use `update_provider_metadata(providerId, {...})` instead. It's a genuine partial update on the provider itself; don't route a pure metadata edit through `create_provider_version_from_capture`.
138
- - **To look up any published provider (not just ones you own), or to see exactly what the live verification client would fetch** — use `get_provider_info(providerId, versionNumber?)`. It reads the public production SDK backend directly (no auth needed), separate from everything else in this mode.
139
- - There's no separate "create-then-version" step and no org / public-review workflow here.
140
- - Consumer-supplied params (step 10) keep the `{{context_<name>}}` form in this mode; there is no `requiredContext` declaration on the legacy backend.
141
-
142
- When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
143
-
144
- - **If a browser is still attached**, `create_provider_version_from_capture`'s response carries a `_notes` reminder about it. Surface that to the dev and ask whether they want to keep the session open for further edits — only call `dispose_browser` once they confirm they're done. Never close it unprompted; a `builder`-mode session is quota-accounted, but the dev may still be mid-workflow.
145
-
146
- <!-- common: concerns-section -->
147
-
243
+ <!-- topic: publish -->
244
+ ## Publish
245
+
246
+ `provider` is the JSON from the draft step with the `params` URL
247
+ parameterization applied. `initialUrl` is the page where the user logs in / the
248
+ value is displayed (for example, `https://github.com/Syed`) — **not** the API
249
+ endpoint URL inside the provider. It maps to the provider's `loginUrl`.
250
+
251
+ Call `create_provider_version_from_capture`:
252
+
253
+ - **New provider:** omit `providerId`. Registers a new provider at version
254
+ 1.0.0. Pass `provider`, `initialUrl`, optionally `description`,
255
+ `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
256
+ ⚠️ Re-running without `providerId` registers ANOTHER provider — that's how
257
+ duplicates appear. Check `get_me_providers` first.
258
+ - **New version of an existing provider:** pass its `providerId`; the patch is
259
+ auto-bumped above the highest existing version. Pass `notes` (≥10 chars).
260
+ - **Multi-request provider** (landing call + details call, a
261
+ CSRF-preflight/refresh-token chain, …): run capture→draft→replay→prove ONCE
262
+ PER REQUEST, then pass `providers: [draftA, draftB, …]` instead of the single
263
+ `provider`. Each becomes a parallel `requestData` entry on the SAME version
264
+ and **all** are required at verification time.
265
+ - **Login/navigation-gated data:** supply `customInjection` (plain page JS) and
266
+ `allowedInjectedRequestData` (extra requests the script may fire).
267
+ → topic `user-script`. Before populating `allowedInjectedRequestData` for a
268
+ `verifyProof({ providerId })` use case, read topic `hash-validation`.
269
+
270
+ **Ownership — you can only add a version to a provider YOU own.** The
271
+ `providerId` must come from `get_me_providers`. Versioning a public provider you
272
+ don't own fails with a permission error; that's expected. To publish your own
273
+ version of such a site, re-run **without** `providerId` to register your own
274
+ copy. If versioning a provider you *do* own fails, confirm you authenticated as
275
+ the account that created it.
276
+
277
+ **Before adding a version to an existing provider, read the current one with
278
+ `get_provider_info(providerId)` — this is not optional.** This backend has no
279
+ partial-patch endpoint: every version replaces the WHOLE `providerConfig` with
280
+ exactly what you pass in THIS call, and the tool carries **nothing** forward.
281
+ Any field you want to survive must be re-supplied here — `customInjection`,
282
+ `userAgent`, `pageTitle`, `allowedInjectedRequestData`, `stepsToFollow`,
283
+ `useIncognitoWebview`, `extensionConfig`, and `requestData` (through
284
+ `provider`/`providers`). Anything omitted is published blank. So to change one
285
+ field: read the live config, then pass the full desired config (existing values
286
+ plus your change) back in.
287
+
288
+ `allowedInjectedRequestData`, `useIncognitoWebview` and `extensionConfig` take
289
+ effect on add-version only — the register endpoint ignores them, so on a fresh
290
+ register the tool automatically follows up with an add-version call that
291
+ applies them.
292
+
293
+ **Related tools:**
294
+
295
+ - `update_provider_metadata(providerId, {…})` — rename, redescribe, retag, flip
296
+ `providerType`, toggle `isActive`, **without** creating a version. A genuine
297
+ partial update; don't route a pure metadata edit through a new version.
298
+ - `get_provider_info(providerId, versionNumber?)` — look up ANY published
299
+ provider (not just yours) and see exactly what the live verification client
300
+ would fetch. Reads the public production SDK backend directly, no auth.
301
+
302
+ There's no create-then-version step and no org / public-review workflow here.
303
+ When done, tell the developer the version is live and that they can integrate
304
+ using the `providerId`.
305
+
306
+ **The browser is still open.** The response's `_notes` says so. Surface it and
307
+ ask whether the developer wants to keep the session for further edits — call
308
+ `dispose_browser` only once they confirm. Never close it unprompted; a
309
+ `builder`-mode session is quota-accounted.
310
+
311
+ <!-- topic: never-do -->
148
312
  ## Things to never do
149
313
 
150
314
  <!-- common: never-do-common -->
151
- - **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.
152
- - 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.
153
- - Mark ANY param secret (not just consumer-supplied `context_*` ones) by naming it with `SECRET` (e.g. `{{SECRET_x}}` or `{{context_SECRET_apiKey}}`). It CAN be used in the URL, geoLocation, request body, response match, response redactions — write-redaction hides it there. Any value of a parameter marked secret is not included in the proof. There's also a limitation for secret-marked values used in a URL: only up to a small TOTAL character budget across every secret value in that URL (2 chars under `key-update`, the default; 12 under `zk`). That's checked at verification time against the real value, not at publish time (see step 10). Headers are secret by allowlist, not by name — anything not on a small fixed public list is secret by default.
154
-
155
- <!-- common: first-time-issues-intro -->
156
-
157
- <!-- common: credential-intro -->
158
-
159
- 1. **Call `resolve_owner_key`.** It checks `RECLAIM_PRIVATE_KEY` (MCP env) → the project `.env` → `RECLAIM_PRIVATE_KEY_FILE`, and returns only the derived address.
160
- - `{ ready: true, address }` → call `run_proof({ provider, ownerAddress: address, … })`. No prompts.
161
- - `{ ready: false }` → no key exists yet. Ask the dev once how to set one up:
162
- - **Provide their own key** → `import_credentials({ privateKey: "0x…" })` (also accepts a `keyFile` path).
163
- - **Create a new one** → `issue_credentials({})`. (Fine default for test proofs — owner identity doesn't matter.)
164
- - Either way the key is written to the project `.env` (mode 0600; `.env` is added to `.gitignore`) — never to `~/.reclaim`.
165
- - Then call `resolve_owner_key` again and `run_proof` with the returned address.
166
- <!-- common: credential-step2 -->
167
- - **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 `.env` flow above avoids that — prefer it.
168
- <!-- common: mcp-config-bullet -->
169
-
170
- If `replay_request` or `run_proof` errors with `unknown secretParamsRef`:
171
-
172
- - **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.
173
- - **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.
174
-
175
- ## Recognizing a HAWKEYE / live-interception provider
176
-
177
- Everything above builds the STANDARD flow: capture → draft → replay (the backend independently re-sends the request) → prove. There is a second, separate flow — `injectionType: HAWKEYE` (or `MSWJS`/`XHOOK`/`CDP`) plus a `customInjection` script that calls `window.reclaimInterceptor.addResponseMiddleware(...)` to observe the page's REAL responses as they happen, instead of the backend replaying anything. You will not author this flow with the current tools — `create_provider_version_from_capture` only builds the standard one. But you WILL see the fields when reading an existing provider (`get_provider_version_config`, `get_provider_info`): don't assume `injectionType` present-but-not-`NONE` means something is broken — the old backend's own schema DEFAULTS `injectionType` to `HAWKEYE` for every provider, standard-flow ones included; it's inert with no interceptor script installed. `disableRequestReplay` is a NARROWER, separate toggle — it does not gate interception in general (that fires regardless, for every `injectionType` including `CDP`); it only controls whether the current page's own document/navigation request specifically gets pulled through a non-CDP interceptor, and exists as a site-compatibility escape hatch, not an architecture choice. See `injection-api.md` (embedded in the `customInjection` field description) for the full, source-verified breakdown of `injectionType`/`disableRequestReplay`/`bodySniff` before guessing at any of these three.
315
+ - Never request `hash` for a target whose value also appears in the request URL
316
+ (topic `draft`), and never `hash` a free-form target — `propose_provider`
317
+ throws either way; retrying won't help.
318
+
319
+ <!-- topic: credentials -->
320
+ <!-- common: credentials -->
321
+
322
+ <!-- topic: troubleshooting -->
323
+ ## Troubleshooting
324
+
325
+ **`unknown draftId` / `no capture`.** The draft is bound to its capture, held in
326
+ the MCP server process. Anything that restarts the server (Claude Code restart,
327
+ `reset_session`, reboot) drops every capture and its drafts. The fix is always
328
+ `start_capture` → interact → `propose_provider` for a fresh `draftId`. Don't ask
329
+ the developer to "re-enter the value" — walk them back through capture.
330
+
331
+ **Replaying as an anonymous visitor.** Pass `withoutSecrets: true` to
332
+ `replay_request`; the draft still holds the captured secrets, so you never
333
+ re-supply them.
334
+
335
+ **Auth errors on publish / `get_me_providers`** → the old-devtools token
336
+ expired. Re-run `reclaim_authenticate` (topic `auth`), not
337
+ `authenticate_builder`.
338
+
339
+ **`reclaim_authenticate` seems to hang.** On a FRESH Chrome profile the
340
+ dashboard is a large cold download, so the login form can take tens of seconds
341
+ to become usable — that time comes out of the sign-in budget. Tell the
342
+ developer it is loading and that the window is waiting for them, and DON'T
343
+ re-run the tool while it is still waiting: a second call restarts the whole
344
+ wait and is what turns a slow load into an apparent multi-minute hang. If it
345
+ does time out, the error says whether the page ever became interactive —
346
+ "never became interactive" means the load, not the human.
347
+
348
+ **Site won't load, won't let the user sign in, or breaks right after
349
+ sign-in.** Check WHICH client the session targets first: on `builder` (the
350
+ default) and `portals` interception happens over CDP, so it is never the
351
+ cause — look for a captcha instead. On `inapp-sdk`, `verifier-app`, or the
352
+ browser extension, suspect interception before the recipe, and bisect: FIRST
353
+ turn
354
+ document replay off and keep the interceptor (`disableRequestReplay: true`, or
355
+ `isDocumentRequestReplayEnabled: false` on builder) — often that alone is what
356
+ breaks the page, and it's the smallest change. Only if that doesn't help, turn
357
+ interception off entirely (`injectionType: NONE`, or omit
358
+ `interceptorOptions`). Full checklist and remedies: topic `interception`.
359
+
360
+ **Proof failures / attestor errors** → topic `prove`.
361
+ **Browser won't launch or attach** → topic `browser`.
362
+ **Missing signing key** → topic `credentials`.