@reclaimprotocol/agent 0.1.18 → 0.1.20

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 (158) hide show
  1. package/README.md +6 -2
  2. package/lib/authoring/backend.d.ts +4 -0
  3. package/lib/authoring/tools.js +4 -1
  4. package/lib/cdp/capture.d.ts +2 -2
  5. package/lib/cdp/capture.js +25 -21
  6. package/lib/cdp/client.d.ts +39 -1
  7. package/lib/cdp/client.js +125 -1
  8. package/lib/mcp/api/operations.gen.js +33 -9
  9. package/lib/mcp/instructions.js +27 -3
  10. package/lib/mcp/start.js +12 -8
  11. package/lib/mcp/tools/agent/attach.d.ts +18 -4
  12. package/lib/mcp/tools/agent/attach.js +142 -52
  13. package/lib/mcp/tools/agent/build.d.ts +1 -1
  14. package/lib/mcp/tools/agent/build.js +5 -3
  15. package/lib/mcp/tools/agent/capture.d.ts +1 -0
  16. package/lib/mcp/tools/agent/capture.js +8 -1
  17. package/lib/mcp/tools/agent/eval.js +2 -3
  18. package/lib/mcp/tools/agent/how-it-works.d.ts +1 -1
  19. package/lib/mcp/tools/agent/how-it-works.js +1 -1
  20. package/lib/mcp/tools/agent/publish.js +22 -1
  21. package/lib/mcp/tools/agent/session.d.ts +2 -2
  22. package/lib/mcp/tools/agent/session.js +21 -5
  23. package/lib/mcp/tools/agent/shared.js +4 -6
  24. package/lib/mcp/tools/authenticate/authenticate.d.ts +1 -1
  25. package/lib/mcp/tools/authenticate/authenticate.js +10 -4
  26. package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -0
  27. package/lib/mcp/tools/authenticate/builder-access.js +31 -0
  28. package/lib/mcp/tools/authenticate/index.d.ts +1 -0
  29. package/lib/mcp/tools/authenticate/index.js +1 -0
  30. package/lib/old/login-cdp.js +11 -3
  31. package/lib/old/tools/publish.js +23 -1
  32. package/lib/provider/draft.d.ts +20 -0
  33. package/lib/provider/draft.js +75 -51
  34. package/lib/provider/index.d.ts +1 -1
  35. package/lib/provider/index.js +1 -1
  36. package/lib/provider/secrets.d.ts +37 -0
  37. package/lib/provider/secrets.js +147 -6
  38. package/lib/provider/to-version.d.ts +7 -0
  39. package/lib/provider/to-version.js +11 -1
  40. package/package.json +2 -2
  41. package/skill/SKILL.builder.md +43 -95
  42. package/skill/SKILL.common.md +102 -0
  43. package/skill/SKILL.old.md +20 -74
  44. package/skill/builder-mode.md +2 -2
  45. package/skill/injection-api.md +95 -10
  46. package/skill/old-mode.md +2 -2
  47. package/src/authoring/backend.ts +4 -0
  48. package/src/authoring/tools.ts +4 -1
  49. package/src/cdp/capture.ts +24 -24
  50. package/src/cdp/client.ts +197 -2
  51. package/src/mcp/api/operations.gen.ts +33 -9
  52. package/src/mcp/instructions.ts +36 -3
  53. package/src/mcp/start.ts +13 -8
  54. package/src/mcp/tools/agent/attach.ts +198 -57
  55. package/src/mcp/tools/agent/build.ts +9 -2
  56. package/src/mcp/tools/agent/capture.ts +10 -1
  57. package/src/mcp/tools/agent/eval.ts +2 -3
  58. package/src/mcp/tools/agent/how-it-works.ts +1 -1
  59. package/src/mcp/tools/agent/publish.ts +22 -1
  60. package/src/mcp/tools/agent/session.ts +29 -8
  61. package/src/mcp/tools/agent/shared.ts +5 -10
  62. package/src/mcp/tools/authenticate/authenticate.ts +14 -4
  63. package/src/mcp/tools/authenticate/builder-access.ts +42 -0
  64. package/src/mcp/tools/authenticate/index.ts +4 -0
  65. package/src/old/login-cdp.ts +13 -3
  66. package/src/old/tools/publish.ts +25 -3
  67. package/src/provider/draft.ts +98 -56
  68. package/src/provider/index.ts +9 -1
  69. package/src/provider/secrets.ts +171 -6
  70. package/src/provider/to-version.ts +12 -1
  71. package/lib/authoring/find.d.ts +0 -3
  72. package/lib/authoring/find.js +0 -54
  73. package/lib/authoring/propose.d.ts +0 -3
  74. package/lib/authoring/propose.js +0 -109
  75. package/lib/authoring/replay.d.ts +0 -10
  76. package/lib/authoring/replay.js +0 -34
  77. package/lib/cli/args.d.ts +0 -14
  78. package/lib/cli/args.js +0 -19
  79. package/lib/cli/commands.d.ts +0 -1
  80. package/lib/cli/commands.js +0 -19
  81. package/lib/cli.d.ts +0 -1
  82. package/lib/cli.js +0 -7
  83. package/lib/mcp/credentials/authenticate.d.ts +0 -3
  84. package/lib/mcp/credentials/authenticate.js +0 -108
  85. package/lib/mcp/credentials/build.d.ts +0 -5
  86. package/lib/mcp/credentials/build.js +0 -18
  87. package/lib/mcp/credentials/cache.d.ts +0 -70
  88. package/lib/mcp/credentials/cache.js +0 -131
  89. package/lib/mcp/credentials/env-file.d.ts +0 -25
  90. package/lib/mcp/credentials/env-file.js +0 -71
  91. package/lib/mcp/credentials/import.d.ts +0 -2
  92. package/lib/mcp/credentials/import.js +0 -52
  93. package/lib/mcp/credentials/issue.d.ts +0 -2
  94. package/lib/mcp/credentials/issue.js +0 -84
  95. package/lib/mcp/credentials/keypair.d.ts +0 -17
  96. package/lib/mcp/credentials/keypair.js +0 -48
  97. package/lib/mcp/credentials/link.d.ts +0 -3
  98. package/lib/mcp/credentials/link.js +0 -107
  99. package/lib/mcp/credentials/list-cached.d.ts +0 -2
  100. package/lib/mcp/credentials/list-cached.js +0 -55
  101. package/lib/mcp/credentials/owner-key.d.ts +0 -23
  102. package/lib/mcp/credentials/owner-key.js +0 -58
  103. package/lib/mcp/credentials/paths.d.ts +0 -12
  104. package/lib/mcp/credentials/paths.js +0 -35
  105. package/lib/mcp/credentials/poll.d.ts +0 -27
  106. package/lib/mcp/credentials/poll.js +0 -61
  107. package/lib/mcp/credentials/register.d.ts +0 -18
  108. package/lib/mcp/credentials/register.js +0 -30
  109. package/lib/mcp/credentials/resolve-owner-key.d.ts +0 -12
  110. package/lib/mcp/credentials/resolve-owner-key.js +0 -68
  111. package/lib/mcp/credentials/session-cache.d.ts +0 -14
  112. package/lib/mcp/credentials/session-cache.js +0 -42
  113. package/lib/mcp/credentials/status.d.ts +0 -3
  114. package/lib/mcp/credentials/status.js +0 -76
  115. package/lib/mcp/register.d.ts +0 -38
  116. package/lib/mcp/register.js +0 -80
  117. package/lib/mcp/tools/analyze.d.ts +0 -3
  118. package/lib/mcp/tools/analyze.js +0 -26
  119. package/lib/mcp/tools/attach.d.ts +0 -11
  120. package/lib/mcp/tools/attach.js +0 -72
  121. package/lib/mcp/tools/capture.d.ts +0 -10
  122. package/lib/mcp/tools/capture.js +0 -142
  123. package/lib/mcp/tools/eval.d.ts +0 -5
  124. package/lib/mcp/tools/eval.js +0 -127
  125. package/lib/mcp/tools/find.d.ts +0 -3
  126. package/lib/mcp/tools/find.js +0 -60
  127. package/lib/mcp/tools/owner-key.d.ts +0 -2
  128. package/lib/mcp/tools/owner-key.js +0 -63
  129. package/lib/mcp/tools/proof.d.ts +0 -3
  130. package/lib/mcp/tools/proof.js +0 -214
  131. package/lib/mcp/tools/propose.d.ts +0 -9
  132. package/lib/mcp/tools/propose.js +0 -75
  133. package/lib/mcp/tools/publish.d.ts +0 -3
  134. package/lib/mcp/tools/publish.js +0 -59
  135. package/lib/mcp/tools/replay.d.ts +0 -3
  136. package/lib/mcp/tools/replay.js +0 -46
  137. package/lib/mcp/tools/session.d.ts +0 -10
  138. package/lib/mcp/tools/session.js +0 -29
  139. package/lib/mcp/tools/verification/diagnose.d.ts +0 -11
  140. package/lib/mcp/tools/verification/diagnose.js +0 -132
  141. package/lib/mcp/verifications/build.d.ts +0 -5
  142. package/lib/mcp/verifications/build.js +0 -14
  143. package/lib/mcp/verifications/create.d.ts +0 -3
  144. package/lib/mcp/verifications/create.js +0 -57
  145. package/lib/mcp/verifications/find.d.ts +0 -3
  146. package/lib/mcp/verifications/find.js +0 -37
  147. package/lib/mcp/verifications/get.d.ts +0 -3
  148. package/lib/mcp/verifications/get.js +0 -26
  149. package/lib/mcp/verifications/update.d.ts +0 -3
  150. package/lib/mcp/verifications/update.js +0 -78
  151. package/lib/old/tools/get-version-config.d.ts +0 -17
  152. package/lib/old/tools/get-version-config.js +0 -47
  153. package/lib/routes.gen.d.ts +0 -271
  154. package/lib/routes.gen.js +0 -73
  155. package/lib/types/openapi.d.ts +0 -4
  156. package/lib/types/openapi.gen.d.ts +0 -3967
  157. package/lib/types/openapi.gen.js +0 -5
  158. package/lib/types/openapi.js +0 -1
@@ -221,16 +221,101 @@ body's structure is pinned down by the hash anymore (any body at all would
221
221
  hash the same), so prefer the minimal-placeholder approach above whenever
222
222
  you can actually identify which fields vary.
223
223
 
224
- Caveat: I confirmed `hashProviderParams` treats `body` as an opaque string
225
- with no `{{}}` resolution directly from the attestor-core bundle shipped in
226
- this repo's `node_modules`. I did NOT find, in this same bundle, the code
227
- path that actually invokes `hashProviderParams` against a live submitted
228
- claim to gate proof acceptance — that caller lives elsewhere (the mobile/
229
- InApp SDK client and/or a server-side verifier not checked into this repo).
230
- So this is strong but not 100% end-to-end-traced confirmation; treat it as
231
- "very likely correct, empirically consistent with real proof/verifyProof
232
- behavior observed in practice" rather than "certain from first-hand
233
- tracing of the whole call chain."
224
+ The same opaque-string hashing (confirmed directly from the attestor-core
225
+ bundle shipped in this repo's `node_modules`) also applies to fields beyond
226
+ `body`. The caller that invokes it against a live submitted claim to gate
227
+ proof acceptance is `@reclaimprotocol/js-sdk`'s `verifyProof` — the next
228
+ section covers how that gate works and how to configure a provider that
229
+ satisfies it.
230
+
231
+ ## Provider-hash validation (`verifyProof(proof, { providerId })`) — how it gates, and how to configure a provider that passes it
232
+
233
+ The caller for the hashing described above is `@reclaimprotocol/js-sdk`'s
234
+ `verifyProof`. When called with `{ providerId }` (the SDK's own
235
+ integration guide treats this as the normal case), traced directly from
236
+ that package's source (`src/utils/proofValidationUtils.ts`,
237
+ `src/utils/providerUtils.ts`, `src/witness.ts`):
238
+
239
+ 1. `assertValidateProof` → `fetchProviderHashRequirementsBy(providerId, …)`
240
+ fetches this provider's CURRENT PUBLISHED CONFIG and builds a list of
241
+ "expected hashes" — one per entry in `requestData` PLUS one per entry in
242
+ `allowedInjectedRequestData` (old-devtools) / `allowedJsRequests`
243
+ (builder) — same concept, different field name per backend; don't
244
+ assume a doc or error naming one applies verbatim to the other.
245
+ 2. Each expected hash is `hashRequestSpec` → `hashProofClaimParams({ url,
246
+ method, body, responseMatches, responseRedactions })` — the SAME
247
+ opaque-string hashing described above for `body`, applied to the WHOLE
248
+ spec at once.
249
+ 3. The REAL submitted proof's own `claimData.parameters` is hashed the
250
+ SAME way, with `isOptional`-driven 2^N combinatorial expansion over
251
+ `responseMatches`/`responseRedactions` PAIRS so an optional field can be
252
+ either present or absent (`getProviderParamsAsCanonicalizedString`) —
253
+ but this expansion ONLY touches match/redaction pairs, never `url`.
254
+ 4. `assertValidProofsByHash` requires the real proof's hash to exactly
255
+ equal ONE expected hash. No match → `verifyProof` rejects, even for a
256
+ perfectly valid, correctly-signed proof whose `responseMatches` are
257
+ 100% correct in shape.
258
+
259
+ **Key constraint: `url` is hashed VERBATIM, with ZERO template/regex
260
+ resolution — even when `urlType` is `REGEX` or `TEMPLATE`.** Confirmed
261
+ directly from `generateSpecsFromRequestSpecTemplate` (the function that DOES
262
+ resolve `${var}` placeholders in `responseMatches`/`responseRedactions`
263
+ before hashing): it spreads every other field from the template unchanged,
264
+ and never touches `url`. `urlType` is consumed by a DIFFERENT validation
265
+ path entirely (matching an incoming live request against this spec at
266
+ capture time) — never by this hash computation.
267
+
268
+ **Check this before populating `allowedInjectedRequestData`/
269
+ `allowedJsRequests` for a `verifyProof({ providerId })` use case:** if the
270
+ real captured request URL contains ANY component that varies between two
271
+ otherwise-identical sessions — a tracking nonce, a CSRF token, a
272
+ cache-busting timestamp, a per-request trace id — the hash computed from
273
+ your declared spec can NEVER equal the hash of any real future proof, no
274
+ matter how correct `responseMatches`/`responseRedactions`/`body` are. This
275
+ is common on JS-heavy sites: e.g. Slack regenerates `_x_id`, `_x_csid`, and
276
+ `fp` on every single API call, so a captured `users.profile.getSections`
277
+ URL is never identical twice, even for the same user seconds apart.
278
+ Populating this field for such a provider is actively worse than leaving
279
+ it empty — it converts "no provider-hash check was attempted" into
280
+ "`verifyProof({ providerId })` rejects every real proof from this
281
+ provider, unconditionally."
282
+
283
+ **Verify before wiring this up**: capture the same endpoint twice, in two
284
+ separate sessions (ideally two different users, or the same user on two
285
+ different days), and diff the full request URL byte-for-byte.
286
+ - Identical every time → safe to populate with `urlType: CONSTANT` and the
287
+ literal URL.
288
+ - Not identical → do not populate this field expecting it to gate
289
+ `verifyProof({ providerId })`. There is no way to make a session-unique
290
+ URL satisfy this specific mechanism, on either backend — this is a
291
+ property of the hashing itself, not something fixable by editing
292
+ `responseMatches`, `responseRedactions`, or `body` more carefully. See
293
+ the fallback below.
294
+
295
+ Also note, independent of the `url` constraint above: even with a stable URL,
296
+ `responseMatches`/`responseRedactions` must be an EXACT structural match —
297
+ not just similar in spirit. A provider whose extracted field SET varies
298
+ per user (e.g. optional profile fields present for some users, absent for
299
+ others) needs every such field marked `isOptional: true` on BOTH the match
300
+ and its paired redaction, so the 2^N expansion produces a valid hash for
301
+ the "field absent" case too. A field that's sometimes-present but never
302
+ marked optional breaks the hash for any user missing it — the same failure
303
+ mode as the `url` constraint above, just scoped to one field instead of the
304
+ whole spec.
305
+
306
+ **Fallback if provider-hash validation keeps causing problems.** Whether
307
+ it's an unstable `url`, an un-marked optional field, or any other
308
+ persistent hash mismatch you can't pin down — the way out on the verifying
309
+ side is `verifyProof(proof, { dangerouslyDisableContentValidation: true })`.
310
+ That skips step 1-4 above entirely and trusts only the attestor's
311
+ cryptographic signature over the proof; it does NOT check that the proof's
312
+ `url`/`method`/`body`/`responseMatches`/`responseRedactions` match anything
313
+ you declared. A caller-supplied `{ hashes: [...] }` computed some other way
314
+ is the alternative when you still want hash-equality enforced, just not
315
+ derived from this provider's published config. Reach for
316
+ `dangerouslyDisableContentValidation` deliberately, not as a silent
317
+ default — it trades away the guarantee that the proof is shaped the way
318
+ this provider's spec says it should be.
234
319
 
235
320
  ## Recognizing this on a provider you didn't author
236
321
 
package/skill/old-mode.md CHANGED
@@ -3,14 +3,14 @@ This server is running against the devtools backend, so its capabilities are int
3
3
  **Provider-authoring flow:**
4
4
 
5
5
  1. `reclaim_authenticate` — by default opens the dashboard in Chrome, waits for you to sign in, captures the token automatically, and closes the tab (or paste a `token` / pass an eth `ethAddress` to skip the browser). Do this once before publishing or listing.
6
- 2. `attach_chrome` → `start_capture` → drive the page (`navigate`, `wait_for_page`) → inspect (`list_requests`, `get_request`, `find_requests_containing`).
6
+ 2. `attach_browser` → `start_capture` → drive the page (`navigate`, `wait_for_page`) → inspect (`list_requests`, `get_request`, `find_requests_containing`). Browser source is independent of this publishing mode: local Chrome works as before; `builder` uses `authenticate_builder` + `list_builder_organizations` to allocate a quota-accounted Popcorn browser and returns a shareable live-view URL; `custom` accepts a caller-supplied CDP websocket.
7
7
  3. `propose_provider` to draft a provider, `replay_request` to pre-flight, `run_proof` to verify with the attestor. OPRF/`hash` redactions are optional and supported. When the data spans MULTIPLE requests (e.g. a landing call + a details call), propose + `run_proof` EACH request to get one proven draft per request.
8
8
  4. `create_provider_version_from_capture` to publish. With NO `providerId` it REGISTERS A NEW provider (re-running creates a duplicate); pass a `providerId` from `get_me_providers` to add a new version to an existing provider instead. For a MULTI-REQUEST provider, pass `providers: [draftA, draftB, …]` (every proven draft) so EACH becomes a parallel `requestData` entry on the one version — passing a single `provider` saves only that one request.
9
9
  5. `get_me_providers` lists the providers you own (the only provider-listing tool available in this mode).
10
10
 
11
11
  **Editing / new versions (ownership):** you can only add a version to a provider **you own** — the `providerId` must come from `get_me_providers`. Trying to version a public provider you don't own fails with a permission error; that is expected. To publish your own version of such a site, re-run `create_provider_version_from_capture` **without** `providerId` to register your own copy (a separate provider you own). If versioning a provider you *do* own fails, confirm you authenticated as the account that created it.
12
12
 
13
- Only GET/POST endpoints can be captured into providers here. There is no organization, credential-linking, or public-review workflow in this mode.
13
+ Only GET/POST endpoints can be captured into providers here. There is no Builder organization-management, credential-linking, or public-review workflow in this mode; the narrow `authenticate_builder` and `list_builder_organizations` exception exists only to allocate Builder-hosted authoring browsers.
14
14
 
15
15
  **Diagnosing a session:** `session_analytics_logs` fetches the analytics logs for a verification session by id (from the logs service) — use it to see what happened in a session (matches, claims, failures).
16
16
 
@@ -71,6 +71,10 @@ export interface ProveOptions {
71
71
  * its own result, passed straight through as the tool's output.
72
72
  */
73
73
  export interface AuthoringBackend {
74
+ /** Ask Chrome to reject page evaluation when side effects cannot be ruled
75
+ * out. Cloud authoring enables this; local MCP keeps its existing
76
+ * behavior. */
77
+ readonly readOnlyPageEval?: boolean
74
78
  /** Send a raw CDP command scoped to this session's page. */
75
79
  sendCdp(method: string, params?: Record<string, unknown>): Promise<unknown>
76
80
  /** Resolve once `method` (a CDP event) fires on this session's page, or
@@ -80,7 +80,10 @@ async function evalInPage(
80
80
  expression: string,
81
81
  ): Promise<{ value?: unknown, error?: string }> {
82
82
  const res = (await backend.sendCdp('Runtime.evaluate', {
83
- expression, returnByValue: true, awaitPromise: true,
83
+ expression,
84
+ returnByValue: true,
85
+ awaitPromise: true,
86
+ ...(backend.readOnlyPageEval ? { throwOnSideEffect: true } : {}),
84
87
  })) as EvalResult
85
88
  if(res.exceptionDetails) {
86
89
  return {
@@ -4,12 +4,12 @@
4
4
  * request/response bookkeeping lives in `capture-engine.ts`, shared with the
5
5
  * app's remote-Popcorn transport.
6
6
  */
7
- import type CDP from 'chrome-remote-interface'
8
7
  import {
9
8
  type CaptureSession,
10
9
  createCaptureEngine,
11
10
  type StartCaptureOptions,
12
11
  } from './capture-engine.ts'
12
+ import type { CdpTabClient } from './client.ts'
13
13
 
14
14
  export {
15
15
  isStaticAsset,
@@ -19,41 +19,41 @@ export {
19
19
  export type { CaptureSession, StartCaptureOptions } from './capture-engine.ts'
20
20
 
21
21
  export async function startCapture(
22
- tab: CDP.Client,
22
+ tab: CdpTabClient,
23
23
  opts: StartCaptureOptions = {},
24
24
  ): Promise<CaptureSession> {
25
25
  const engine = createCaptureEngine({
26
26
  ...(opts.maxBodyBytes !== undefined && { maxBodyBytes: opts.maxBodyBytes }),
27
- fetchBody: (requestId) => tab.Network.getResponseBody({ requestId }),
27
+ fetchBody: tab.getResponseBody,
28
28
  })
29
29
 
30
- const { Network } = tab
31
- await Network.enable()
32
-
33
- Network.requestWillBeSent(engine.onRequestWillBeSent)
34
- // CDP intentionally omits `Cookie` (and other browser-injected headers)
35
- // from `requestWillBeSent.request.headers` for privacy. The real
36
- // outgoing headers — including Cookie — arrive on the matching
37
- // `requestWillBeSentExtraInfo` event.
38
- Network.requestWillBeSentExtraInfo(({ requestId, headers }) => {
39
- engine.mergeRequestHeaders(requestId, headers)
40
- })
41
- Network.responseReceived(engine.onResponseReceived)
42
- // Same privacy carve-out applies to `Set-Cookie` and other browser-
43
- // visible response headers; they arrive on responseReceivedExtraInfo.
44
- Network.responseReceivedExtraInfo(({ requestId, headers }) => {
45
- engine.mergeResponseHeaders(requestId, headers)
46
- })
47
- Network.loadingFinished(({ requestId }) => {
48
- void engine.onLoadingFinished(requestId)
49
- })
30
+ const unsubscribe = [
31
+ tab.subscribe(
32
+ 'Network.requestWillBeSent', engine.onRequestWillBeSent,
33
+ ),
34
+ tab.subscribe('Network.requestWillBeSentExtraInfo', (event) => {
35
+ engine.mergeRequestHeaders(event.requestId, event.headers)
36
+ }),
37
+ tab.subscribe('Network.responseReceived', engine.onResponseReceived),
38
+ tab.subscribe('Network.responseReceivedExtraInfo', (event) => {
39
+ engine.mergeResponseHeaders(event.requestId, event.headers)
40
+ }),
41
+ tab.subscribe('Network.loadingFinished', (event) => {
42
+ void engine.onLoadingFinished(event.requestId)
43
+ }),
44
+ ]
45
+ await tab.send('Network.enable', {})
50
46
 
51
47
  return {
52
48
  captureId: engine.captureId,
53
49
  requests: engine.requests,
54
50
  bodies: engine.bodies,
55
51
  stop: async() => {
56
- await Network.disable()
52
+ for(const stopListening of unsubscribe) {
53
+ stopListening()
54
+ }
55
+
56
+ await tab.send('Network.disable', {}).catch(() => {})
57
57
  },
58
58
  }
59
59
  }
package/src/cdp/client.ts CHANGED
@@ -1,20 +1,215 @@
1
1
  import CDP from 'chrome-remote-interface'
2
+ import assert from 'node:assert'
3
+ import { EventEmitter } from 'node:events'
4
+ import type {
5
+ RequestWillBeSentLike,
6
+ ResponseReceivedLike,
7
+ } from './capture-engine.ts'
8
+ import type { TabInfo } from './tabs.ts'
9
+ import { listTabs } from './tabs.ts'
10
+
11
+ const DIRECT_TARGET_ID = '__direct_cdp_target__'
12
+
13
+ interface CdpEventMessage {
14
+ method: string
15
+ params: object
16
+ sessionId?: string
17
+ }
18
+
19
+ interface CdpTabEventMap {
20
+ 'Network.requestWillBeSent': RequestWillBeSentLike
21
+ 'Network.requestWillBeSentExtraInfo': {
22
+ requestId: string
23
+ headers: Record<string, string>
24
+ }
25
+ 'Network.responseReceived': ResponseReceivedLike & {
26
+ response: ResponseReceivedLike['response'] & { url: string }
27
+ }
28
+ 'Network.responseReceivedExtraInfo': {
29
+ requestId: string
30
+ headers: Record<string, string>
31
+ }
32
+ 'Network.loadingFinished': { requestId: string }
33
+ }
34
+
35
+ export interface CdpTabClient {
36
+ send: (method: string, params?: object) => Promise<unknown>
37
+ getResponseBody: (
38
+ requestId: string,
39
+ ) => Promise<{ body: string, base64Encoded: boolean }>
40
+ subscribe: <K extends keyof CdpTabEventMap>(
41
+ method: K,
42
+ listener: (params: CdpTabEventMap[K]) => void,
43
+ ) => () => void
44
+ subscribeOnce: (method: string, listener: () => void) => () => void
45
+ close: () => Promise<void>
46
+ }
2
47
 
3
48
  export interface CdpHandles {
4
49
  /** Browser-level connection (target list, attach). */
5
50
  browser: CDP.Client
6
51
  /** Returns a per-target session bound to a tab. */
7
- attachToTab: (targetId: string) => Promise<CDP.Client>
52
+ attachToTab: (targetId: string) => Promise<CdpTabClient>
53
+ /** Enumerate page targets without assuming a localhost discovery endpoint. */
54
+ listTabs: () => Promise<TabInfo[]>
8
55
  close: () => Promise<void>
9
56
  }
10
57
 
11
58
  export async function connectCdp(port: number): Promise<CdpHandles> {
12
59
  const browser = await CDP({ port })
60
+ const tabs = new Set<CdpTabClient>()
61
+ return {
62
+ browser,
63
+ attachToTab: async(targetId) => {
64
+ const tab = adaptCdpClient(await CDP({ port, target: targetId }))
65
+ tabs.add(tab)
66
+ return tab
67
+ },
68
+ listTabs: () => listTabs(port),
69
+ close: async() => {
70
+ for(const tab of tabs) {
71
+ await tab.close().catch(() => {})
72
+ }
73
+
74
+ await browser.close()
75
+ },
76
+ }
77
+ }
78
+
79
+ /** Connect to a browser-level or page-level CDP websocket. Browser-level
80
+ * endpoints (including Popcorn) use flattened Target sessions; a page-level
81
+ * custom endpoint is exposed as one synthetic tab and used directly. */
82
+ export async function connectCdpUrl(webSocketUrl: string): Promise<CdpHandles> {
83
+ const browser = await CDP({ target: webSocketUrl, local: true })
84
+ let browserLevel = true
85
+ try {
86
+ await browser.send('Target.getTargets')
87
+ } catch{
88
+ browserLevel = false
89
+ }
90
+
91
+ const tabs = new Set<CdpTabClient>()
92
+ const directTab = browserLevel ? undefined : adaptCdpClient(browser)
13
93
  return {
14
94
  browser,
15
- attachToTab: (targetId) => CDP({ port, target: targetId }),
95
+ listTabs: async() => {
96
+ if(!browserLevel) {
97
+ return [{
98
+ tabId: DIRECT_TARGET_ID, url: '', title: 'Direct CDP target',
99
+ }]
100
+ }
101
+
102
+ const { targetInfos } = await browser.send('Target.getTargets')
103
+ return targetInfos
104
+ .filter((target) => target.type === 'page')
105
+ .map((target) => ({
106
+ tabId: target.targetId,
107
+ url: target.url,
108
+ title: target.title,
109
+ }))
110
+ },
111
+ attachToTab: async(targetId) => {
112
+ if(!browserLevel) {
113
+ if(targetId !== DIRECT_TARGET_ID) {
114
+ throw new Error(`Unknown direct CDP target ${targetId}`)
115
+ }
116
+
117
+ assert(directTab)
118
+ return directTab
119
+ }
120
+
121
+ const { sessionId } = await browser.send('Target.attachToTarget', {
122
+ targetId,
123
+ flatten: true,
124
+ })
125
+ const tab = flattenedSessionClient(browser, sessionId)
126
+ tabs.add(tab)
127
+ return tab
128
+ },
16
129
  close: async() => {
130
+ for(const tab of tabs) {
131
+ await tab.close().catch(() => {})
132
+ }
133
+
17
134
  await browser.close()
18
135
  },
19
136
  }
20
137
  }
138
+
139
+ /** Adapt a flattened browser Target session to the small tab-client surface
140
+ * consumed by capture and authoring. Events are filtered by session id so
141
+ * concurrent tabs cannot observe each other's traffic. */
142
+ function flattenedSessionClient(
143
+ browser: CDP.Client,
144
+ sessionId: string,
145
+ ): CdpTabClient {
146
+ const events = new EventEmitter()
147
+ // The declaration only accepts literal protocol method names; this adapter
148
+ // deliberately forwards the shared authoring layer's dynamic method string.
149
+ const sender = browser.send.bind(browser) as <T>(
150
+ method: string, params: object, sessionId?: string,
151
+ ) => Promise<T>
152
+ const onEvent = (message: CdpEventMessage) => {
153
+ if(message.sessionId !== sessionId) {
154
+ return
155
+ }
156
+
157
+ events.emit('event', message)
158
+ events.emit(message.method, message.params)
159
+ }
160
+
161
+ browser.on('event', onEvent)
162
+
163
+ return tabClient(events, (method, params) => {
164
+ return sender(method, params ?? {}, sessionId)
165
+ }, (requestId) => {
166
+ return sender<{ body: string, base64Encoded: boolean }>(
167
+ 'Network.getResponseBody', { requestId }, sessionId,
168
+ )
169
+ }, async() => {
170
+ EventEmitter.prototype.removeListener.call(browser, 'event', onEvent)
171
+ await browser.send(
172
+ 'Target.detachFromTarget', { sessionId },
173
+ ).catch(() => {})
174
+ })
175
+ }
176
+
177
+ function adaptCdpClient(client: CDP.Client): CdpTabClient {
178
+ const events = new EventEmitter()
179
+ const onEvent = (message: CdpEventMessage) => {
180
+ events.emit(message.method, message.params)
181
+ }
182
+
183
+ client.on('event', onEvent)
184
+
185
+ // CRI intentionally types send against protocol literals. This adapter is
186
+ // the single boundary where shared authoring commands arrive dynamically.
187
+ const send = client.send.bind(client)
188
+ return tabClient(events, send, (requestId) => {
189
+ return client.send('Network.getResponseBody', { requestId })
190
+ }, async() => {
191
+ EventEmitter.prototype.removeListener.call(client, 'event', onEvent)
192
+ await client.close()
193
+ })
194
+ }
195
+
196
+ function tabClient(
197
+ events: EventEmitter,
198
+ send: CdpTabClient['send'],
199
+ getResponseBody: CdpTabClient['getResponseBody'],
200
+ close: CdpTabClient['close'],
201
+ ): CdpTabClient {
202
+ return {
203
+ send,
204
+ getResponseBody,
205
+ subscribe(method, listener) {
206
+ events.on(method, listener)
207
+ return () => events.removeListener(method, listener)
208
+ },
209
+ subscribeOnce(method, listener) {
210
+ events.once(method, listener)
211
+ return () => events.removeListener(method, listener)
212
+ },
213
+ close,
214
+ }
215
+ }
@@ -173,7 +173,7 @@ export const MCP_TOOLS = {
173
173
  }),
174
174
  },
175
175
  "AdminSessions": {
176
- description: "Explore verification sessions (admin-only)\n\nAdmin only. Search verification sessions across all organizations — paste a session id, provider id, or consumer ETH address into `q`, optionally filter by `status`. Rows link to the org-scoped session detail page (admins can open any org's pages). Capped at 100, newest first.",
176
+ description: "Explore verification sessions (admin-only)\n\nAdmin only. Search verification sessions across all organizations — filter by `sessionId` (prefix match, at least 5 characters), `providerId`, `claimantClientId`, `orgId` (all exact match), `status`, and a `createdAfter`/`createdBefore` time range. Rows link to the org-scoped session detail page (admins can open any org's pages). Capped at 100, newest first.",
177
177
  inputSchema: toZod({
178
178
  "type": "object",
179
179
  "properties": {
@@ -186,7 +186,19 @@ export const MCP_TOOLS = {
186
186
  "query": {
187
187
  "type": "object",
188
188
  "properties": {
189
- "q": {
189
+ "sessionId": {
190
+ "type": "string",
191
+ "maxLength": 100
192
+ },
193
+ "providerId": {
194
+ "type": "string",
195
+ "maxLength": 100
196
+ },
197
+ "claimantClientId": {
198
+ "type": "string",
199
+ "maxLength": 100
200
+ },
201
+ "orgId": {
190
202
  "type": "string",
191
203
  "maxLength": 100
192
204
  },
@@ -200,6 +212,14 @@ export const MCP_TOOLS = {
200
212
  "error",
201
213
  "expired"
202
214
  ]
215
+ },
216
+ "createdAfter": {
217
+ "type": "string",
218
+ "format": "date-time"
219
+ },
220
+ "createdBefore": {
221
+ "type": "string",
222
+ "format": "date-time"
203
223
  }
204
224
  }
205
225
  },
@@ -623,9 +643,9 @@ export const MCP_TOOLS = {
623
643
  },
624
644
  "prompt": {
625
645
  "type": "string",
626
- "description": "Extraction prompt, used (and required) when `verificationType` is `llm`.",
646
+ "description": "Extraction prompt, used (and required) when `verificationType` is `llm`. Gated by the prompt guard before it is stored (docs/prompt-guard.md).",
627
647
  "minLength": 0,
628
- "maxLength": 1000000
648
+ "maxLength": 4000
629
649
  },
630
650
  "version": {
631
651
  "type": "object",
@@ -2058,7 +2078,7 @@ export const MCP_TOOLS = {
2058
2078
  }),
2059
2079
  },
2060
2080
  "ListOrgVerificationSessions": {
2061
- description: "List an organization's verification sessions\n\nReturns verification sessions created under the organization, newest first. Optionally filter by `providerId`, `status`, `resolvedVersion`, `claimantClientId`, and a `createdAfter`/`createdBefore` time range. Caller must be an active member of the org. Opaque cursor pagination.\n\nNote: `status` filters on the stored column. A session that has passed `expiresAt` but has not yet been swept is reported as `expired` in its body while still stored as `pending`, so `status=expired` may not match such sessions until the sweep runs.",
2081
+ description: "List an organization's verification sessions\n\nReturns verification sessions created under the organization, newest first. Optionally filter by `sessionId` (prefix match, at least 5 characters), `providerId`, `status`, `resolvedVersion`, `claimantClientId`, and a `createdAfter`/`createdBefore` time range. Caller must be an active member of the org. Opaque cursor pagination.\n\nNote: `status` filters on the stored column. A session that has passed `expiresAt` but has not yet been swept is reported as `expired` in its body while still stored as `pending`, so `status=expired` may not match such sessions until the sweep runs.",
2062
2082
  inputSchema: toZod({
2063
2083
  "type": "object",
2064
2084
  "properties": {
@@ -2078,9 +2098,13 @@ export const MCP_TOOLS = {
2078
2098
  "query": {
2079
2099
  "type": "object",
2080
2100
  "properties": {
2101
+ "sessionId": {
2102
+ "type": "string",
2103
+ "maxLength": 100
2104
+ },
2081
2105
  "providerId": {
2082
2106
  "type": "string",
2083
- "format": "uuid"
2107
+ "maxLength": 100
2084
2108
  },
2085
2109
  "status": {
2086
2110
  "type": "string",
@@ -2099,7 +2123,7 @@ export const MCP_TOOLS = {
2099
2123
  },
2100
2124
  "claimantClientId": {
2101
2125
  "type": "string",
2102
- "format": "uuid"
2126
+ "maxLength": 100
2103
2127
  },
2104
2128
  "createdAfter": {
2105
2129
  "type": "string",
@@ -2967,9 +2991,9 @@ export const MCP_TOOLS = {
2967
2991
  },
2968
2992
  "prompt": {
2969
2993
  "type": "string",
2970
- "description": "Extraction prompt, used when `verificationType` is `llm`.",
2994
+ "description": "Extraction prompt, used when `verificationType` is `llm`. Gated by the prompt guard before it is stored (docs/prompt-guard.md).",
2971
2995
  "minLength": 0,
2972
- "maxLength": 1000000
2996
+ "maxLength": 4000
2973
2997
  },
2974
2998
  "requests": {
2975
2999
  "type": "array",
@@ -6,6 +6,7 @@
6
6
  * `lib/mcp` (published) — both are two levels below the package root.
7
7
  * Old-devtools mode has its own reduced set in `../old/instructions.ts`.
8
8
  */
9
+ import assert from 'node:assert'
9
10
  import { readFileSync } from 'node:fs'
10
11
  import { fileURLToPath } from 'node:url'
11
12
 
@@ -14,6 +15,32 @@ export const BUILDER_MODE_INSTRUCTIONS = readFileSync(
14
15
  'utf8',
15
16
  ).trim()
16
17
 
18
+ /**
19
+ * `SKILL.builder.md` and `SKILL.old.md` share a lot of prose (steps that are
20
+ * identical regardless of backend, "never do" rules, common troubleshooting).
21
+ * That shared text lives once in `skill/SKILL.common.md`, split into named
22
+ * `<!-- section: name -->`-delimited blocks; each mode file splices a block
23
+ * back in at `<!-- common: name -->`. Parsed once at module load.
24
+ */
25
+ const COMMON_SECTIONS = parseCommonSections(
26
+ readFileSync(
27
+ fileURLToPath(new URL('../../skill/SKILL.common.md', import.meta.url)),
28
+ 'utf8',
29
+ ),
30
+ )
31
+
32
+ function parseCommonSections(text: string): Record<string, string> {
33
+ const sections: Record<string, string> = {}
34
+ const re = /<!-- section: (\S+) -->\n([\s\S]*?)(?=\n<!-- section: |$)/g
35
+ for(const match of text.matchAll(re)) {
36
+ // Trim blank LINES only, not inline indentation — some sections are
37
+ // list-continuation text that's intentionally indented on its first line.
38
+ sections[match[1]] = match[2].replace(/^\n+/, '').replace(/\n+$/, '')
39
+ }
40
+
41
+ return sections
42
+ }
43
+
17
44
  /**
18
45
  * The full provider-authoring guide — the deep capture→prove→publish workflow,
19
46
  * decision frameworks, and troubleshooting. Served on demand by the
@@ -24,9 +51,15 @@ export const BUILDER_MODE_INSTRUCTIONS = readFileSync(
24
51
  * the instructions above; any leading frontmatter is stripped defensively.
25
52
  */
26
53
  function readGuide(file: string): string {
27
- return readFileSync(fileURLToPath(new URL(file, import.meta.url)), 'utf8')
28
- .replace(/^---\n[\s\S]*?\n---\n/, '')
29
- .trim()
54
+ const url = new URL(file, import.meta.url)
55
+ const raw = readFileSync(fileURLToPath(url), 'utf8')
56
+ const stripped = raw.replace(/^---\n[\s\S]*?\n---\n/, '').trim()
57
+
58
+ return stripped.replace(/<!-- common: (\S+) -->/g, (whole, name: string) => {
59
+ const section = COMMON_SECTIONS[name]
60
+ assert(section, `${file}: no SKILL.common.md section named "${name}"`)
61
+ return section
62
+ })
30
63
  }
31
64
 
32
65
  export const PROVIDER_GUIDE_BUILDER = readGuide('../../skill/SKILL.builder.md')
package/src/mcp/start.ts CHANGED
@@ -20,6 +20,8 @@ import { NAME, VERSION } from '../pkg.ts'
20
20
  import { registerTools } from './api/tools.ts'
21
21
  import {
22
22
  buildAgentTools,
23
+ buildBuilderAuthenticationTools,
24
+ buildBuilderOrganizationTools,
23
25
  buildCredentialTools,
24
26
  buildLocalCredentialTools,
25
27
  buildModeTools,
@@ -49,7 +51,13 @@ export async function startMcpServer() {
49
51
  // deployments — set RECLAIM_AGENT_DISABLED=1 to register only the API
50
52
  // surface.
51
53
  const agentToolsEnabled = !agentDisabled()
52
-
54
+ const builderClient = new ReclaimClient({
55
+ baseUrl: apiUrl(),
56
+ // Builder access is orthogonal to publishing mode. A legacy-devtools
57
+ // session can authenticate here solely to allocate a remote browser.
58
+ token: apiToken() || readSession()?.token,
59
+ headers: { 'User-Agent': `${NAME}/${VERSION}` },
60
+ })
53
61
  // Backend-mode toggle — available in both modes and regardless of whether
54
62
  // the agent tools are enabled, so a user can always switch either direction.
55
63
  registerToolList(server, buildModeTools(config, oldMode))
@@ -66,20 +74,17 @@ export async function startMcpServer() {
66
74
  })
67
75
  const sdkClient = new ReclaimSdkClient({ baseUrl: sdkApiUrl() })
68
76
 
77
+ registerToolList(server, buildBuilderAuthenticationTools(builderClient))
78
+ registerToolList(server, buildBuilderOrganizationTools(builderClient))
69
79
  registerToolList(server, buildLocalCredentialTools())
70
80
  registerToolList(server, buildOldBackendTools(oldClient, sdkClient))
71
81
  if(agentToolsEnabled) {
72
82
  // No builder client → no builder publish tool (old mode has its own).
73
- registerToolList(server, buildAgentTools())
83
+ registerToolList(server, buildAgentTools(undefined, builderClient))
74
84
  }
75
85
  // NOTE: registerTools() is intentionally NOT called — generated tools off.
76
86
  } else {
77
- const client = new ReclaimClient({
78
- baseUrl: apiUrl(),
79
- // Priority: explicit env var → cached session token → unauthenticated
80
- token: apiToken() || readSession()?.token,
81
- headers: { 'User-Agent': `${NAME}/${VERSION}` },
82
- })
87
+ const client = builderClient
83
88
 
84
89
  // Tool names must stay disjoint across these three groups: hand-written
85
90
  // tools are higher-level orchestrations (multi-call flows, ReclaimProvider