@reclaimprotocol/agent 0.1.21 → 0.1.23

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 (50) hide show
  1. package/lib/authoring/backend.d.ts +9 -7
  2. package/lib/authoring/tools.js +98 -1
  3. package/lib/mcp/tools/agent/attach.js +29 -1
  4. package/lib/mcp/tools/agent/propose.js +27 -3
  5. package/lib/mcp/tools/agent/publish.js +7 -5
  6. package/lib/mcp/tools/agent/shared.js +17 -8
  7. package/lib/mcp/tools/authenticate/authenticate.d.ts +11 -0
  8. package/lib/mcp/tools/authenticate/authenticate.js +128 -119
  9. package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -3
  10. package/lib/mcp/tools/authenticate/builder-access.js +8 -3
  11. package/lib/mcp/tools/mode/mode.js +32 -1
  12. package/lib/old/client.d.ts +9 -0
  13. package/lib/old/client.js +19 -1
  14. package/lib/old/to-register.d.ts +17 -6
  15. package/lib/old/to-register.js +6 -0
  16. package/lib/old/tools/me-providers.js +3 -1
  17. package/lib/old/tools/publish.js +82 -38
  18. package/lib/proof/page-replay.d.ts +36 -0
  19. package/lib/proof/page-replay.js +76 -0
  20. package/lib/proof/replay.d.ts +28 -0
  21. package/lib/proof/replay.js +39 -18
  22. package/lib/provider/draft.d.ts +3 -1
  23. package/lib/provider/draft.js +39 -7
  24. package/lib/provider/matcher.js +116 -7
  25. package/lib/provider/to-version.d.ts +8 -6
  26. package/lib/provider/to-version.js +15 -3
  27. package/package.json +3 -2
  28. package/skill/SKILL.builder.md +4 -2
  29. package/skill/SKILL.common.md +10 -1
  30. package/skill/SKILL.old.md +1 -1
  31. package/skill/injection-api.md +44 -15
  32. package/skill/old-mode.md +3 -1
  33. package/src/authoring/backend.ts +9 -7
  34. package/src/authoring/tools.ts +119 -1
  35. package/src/mcp/tools/agent/attach.ts +40 -1
  36. package/src/mcp/tools/agent/propose.ts +29 -3
  37. package/src/mcp/tools/agent/publish.ts +7 -6
  38. package/src/mcp/tools/agent/shared.ts +19 -8
  39. package/src/mcp/tools/authenticate/authenticate.ts +150 -163
  40. package/src/mcp/tools/authenticate/builder-access.ts +8 -3
  41. package/src/mcp/tools/mode/mode.ts +34 -1
  42. package/src/old/client.ts +30 -1
  43. package/src/old/to-register.ts +25 -6
  44. package/src/old/tools/me-providers.ts +3 -1
  45. package/src/old/tools/publish.ts +86 -40
  46. package/src/proof/page-replay.ts +131 -0
  47. package/src/proof/replay.ts +60 -19
  48. package/src/provider/draft.ts +46 -6
  49. package/src/provider/matcher.ts +145 -5
  50. package/src/provider/to-version.ts +24 -9
@@ -179,6 +179,118 @@ export function authoringTools(
179
179
  return { ready: false }
180
180
  },
181
181
  ),
182
+ defineTool<Record<string, unknown>>(
183
+ {
184
+ name: 'test_user_script',
185
+ description:
186
+ 'Dry-run a USER SCRIPT — the same thing as the `jsUserScripts` / '
187
+ + '`customInjection` field on a published provider: plain browser '
188
+ + 'JS that runs before every page load, used to drive navigation '
189
+ + 'for data gated behind login/routing — in the LIVE attached tab, '
190
+ + 'BEFORE publishing. Installs `script` via CDP so it runs on the '
191
+ + 'next navigation exactly like the real field would, then reports '
192
+ + 'whether it ran and any errors it threw. Catches bugs (e.g. a '
193
+ + 'listener registered with setTimeout that never fires because the '
194
+ + "page was already past 'load' by the time it ran) before they "
195
+ + 'surface in a real end-user session instead of after. If '
196
+ + '`navigateUrl` is given, navigates there after installing, then '
197
+ + 'removes the script again once the result is read (self-cleaning '
198
+ + '— safe to call repeatedly). Without `navigateUrl` the script is '
199
+ + 'left installed for you to navigate yourself (any way you like) '
200
+ + 'then re-call this tool, or read '
201
+ + '`window.__reclaimUserScriptResult` via eval_in_page — in that '
202
+ + 'manual mode it is NOT auto-removed and keeps running on every '
203
+ + 'later navigation until a follow-up call with `navigateUrl` '
204
+ + 'removes it, so avoid stacking several manual-mode calls in a '
205
+ + 'row.',
206
+ inputSchema: objectSchema(
207
+ {
208
+ script: { type: 'string' },
209
+ navigateUrl: { type: 'string' },
210
+ timeoutMs: { type: 'number' },
211
+ },
212
+ ['script'],
213
+ ),
214
+ },
215
+ async(args) => {
216
+ const backend = resolveBackend(args)
217
+ const script = String(args.script)
218
+ // The user's script is spliced in as a STRING and compiled at
219
+ // runtime via `new Function`, not as raw top-level source: a
220
+ // syntax error in it would otherwise fail to parse the entire
221
+ // wrapper (result init, error listener, and try/catch included),
222
+ // so the tool would report ran:false / errors:[] instead of the
223
+ // actual SyntaxError.
224
+ const wrapped = `
225
+ window.__reclaimUserScriptResult = { ran: false, errors: [] };
226
+ window.addEventListener('error', function(e) {
227
+ window.__reclaimUserScriptResult.errors.push(String(e.message));
228
+ });
229
+ try {
230
+ (new Function(${JSON.stringify(script)}))();
231
+ window.__reclaimUserScriptResult.ran = true;
232
+ } catch(e) {
233
+ window.__reclaimUserScriptResult.errors.push(
234
+ String((e && e.message) || e),
235
+ );
236
+ }
237
+ `
238
+ const installed = (await backend.sendCdp(
239
+ 'Page.addScriptToEvaluateOnNewDocument', { source: wrapped },
240
+ )) as { identifier?: string }
241
+
242
+ if(typeof args.navigateUrl !== 'string') {
243
+ return {
244
+ installed: true,
245
+ note:
246
+ 'Installed for the NEXT navigation only — it did not run on '
247
+ + 'the current page, and stays installed (runs on every later '
248
+ + 'navigation too) until a follow-up call with navigateUrl '
249
+ + 'removes it. Navigate now (e.g. the navigate tool), then read '
250
+ + 'window.__reclaimUserScriptResult via eval_in_page, or call '
251
+ + 'test_user_script again with navigateUrl.',
252
+ }
253
+ }
254
+
255
+ try {
256
+ const timeoutMs = Number(args.timeoutMs) || 30_000
257
+ await backend.sendCdp('Page.enable')
258
+ const load = backend.waitForEvent('Page.loadEventFired', timeoutMs)
259
+ const nav = (await backend.sendCdp(
260
+ 'Page.navigate', { url: String(args.navigateUrl) },
261
+ )) as { errorText?: string }
262
+ if(nav.errorText) {
263
+ return { installed: true, navigated: false, error: nav.errorText }
264
+ }
265
+
266
+ await load
267
+ const { value, error } = await evalInPage(
268
+ backend, 'window.__reclaimUserScriptResult',
269
+ )
270
+ if(error) {
271
+ return { installed: true, navigated: true, error }
272
+ }
273
+
274
+ const result =
275
+ value as { ran?: boolean, errors?: string[] } | undefined
276
+ return {
277
+ installed: true,
278
+ navigated: true,
279
+ ran: result?.ran ?? false,
280
+ errors: result?.errors ?? [],
281
+ }
282
+ } finally {
283
+ // One-shot test: remove it so it doesn't keep running (and
284
+ // potentially redirecting the tab) on every later navigation.
285
+ if(installed.identifier) {
286
+ await backend.sendCdp(
287
+ 'Page.removeScriptToEvaluateOnNewDocument',
288
+ { identifier: installed.identifier },
289
+ )
290
+ }
291
+ }
292
+ },
293
+ ),
182
294
  defineTool<Record<string, unknown>>(
183
295
  {
184
296
  name: 'get_cookies',
@@ -321,7 +433,13 @@ export function authoringTools(
321
433
  name: 'run_proof',
322
434
  description:
323
435
  'Prove the drafted recipe (zkTLS). Only call after replay_request '
324
- + 'looks right. Returns the proof result.',
436
+ + 'looks right. Returns the proof result. NOTE: unlike '
437
+ + 'replay_request (which re-issues from inside the live attached '
438
+ + 'browser tab), the ATTESTOR calls the target from its OWN '
439
+ + 'network/IP, separate from the browser\'s. A request that '
440
+ + 'replayed fine can still fail here for geo/IP-bound content '
441
+ + '(country-locked pages, IP-allowlisted APIs) — if that happens, '
442
+ + 'it\'s a property of the site, not a bug in the draft.',
325
443
  inputSchema: objectSchema({ draftId: { type: 'string' } }, ['draftId']),
326
444
  },
327
445
  // The backend shapes its own result: the cloud returns a lean,
@@ -70,6 +70,11 @@ export function attachTool(
70
70
  + 'port), builder (quota-accounted Popcorn browser with a '
71
71
  + 'shareable live view), or custom (caller-supplied CDP '
72
72
  + 'websocket). '
73
+ + 'For builder mode, orgId may be omitted: it auto-resolves to '
74
+ + 'the account\'s only organization, or — if there is more '
75
+ + 'than one — returns needsChoice with the full list so the '
76
+ + 'caller can ask the developer which to use and re-call with '
77
+ + 'that orgId. '
73
78
  + 'Builder and custom CDP credentials are held in memory and never '
74
79
  + 'returned.',
75
80
  inputSchema: {
@@ -107,8 +112,42 @@ export function attachTool(
107
112
  }
108
113
  }
109
114
 
115
+ let orgId = parsed.orgId
116
+ if(mode === 'builder' && !orgId) {
117
+ assert(
118
+ builderClient,
119
+ badRequest('Builder browser service is unavailable'),
120
+ )
121
+ const { data } = await builderClient.call('ListOrgs', { query: {} })
122
+ const orgs = data.items ?? []
123
+ assert(
124
+ orgs.length,
125
+ badRequest(
126
+ 'No organizations available for this account — create one '
127
+ + 'first, or authenticate as a user who belongs to one.',
128
+ ),
129
+ )
130
+ if(orgs.length > 1) {
131
+ return {
132
+ needsChoice: true,
133
+ message:
134
+ 'This account belongs to more than one organization. Ask '
135
+ + 'the developer which one to use, then re-call '
136
+ + 'attach_browser with mode "builder" and that orgId.',
137
+ organizations: orgs.map((o) => ({
138
+ id: o.id,
139
+ name: o.name,
140
+ kind: o.kind,
141
+ status: o.status,
142
+ })),
143
+ }
144
+ }
145
+
146
+ orgId = orgs[0].id
147
+ }
148
+
110
149
  const next = await connect(
111
- mode, parsed, persisted, builderClient, dependencies,
150
+ mode, { ...parsed, orgId }, persisted, builderClient, dependencies,
112
151
  )
113
152
  try {
114
153
  await beforeReplace?.()
@@ -23,18 +23,41 @@ export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool {
23
23
  + 'one match + redaction per target. Returns a draftId + recipe '
24
24
  + '(never secrets). Prefer a SINGLE request holding ALL values. '
25
25
  + 'Single-value shorthand: `target` + `name`. Per-target `hash` '
26
- + 'OPRF-hashes that value.',
26
+ + 'OPRF-hashes that value. BY DEFAULT each `name` is shortened to '
27
+ + 'its last alphanumeric token, lowercased, to form the param key '
28
+ + '(e.g. "employer_name" → "name") — pick names that stay distinct '
29
+ + 'after shortening ("firstName"/"surname", not "first_name"/'
30
+ + '"last_name"). Pass `shortenNames: false` to use each `name` '
31
+ + 'VERBATIM as the param key instead — it becomes both a regex '
32
+ + 'capture-group name and a paramValues key, so it must be a plain '
33
+ + 'identifier (letters/digits/underscore, not starting with a '
34
+ + 'digit); we recommend lower_snake_case (e.g. "employer_name"), '
35
+ + 'but you choose the exact name.',
27
36
  inputSchema: objectSchema(
28
37
  {
29
38
  requestId: { type: 'string' },
30
39
  name: { type: 'string' },
40
+ shortenNames: {
41
+ type: 'boolean',
42
+ description:
43
+ 'Default true: shorten each target `name` to its last '
44
+ + 'alphanumeric token, lowercased, to form the param key. '
45
+ + 'Set false to use `name` verbatim instead (must be a plain '
46
+ + 'identifier — lower_snake_case recommended).',
47
+ },
31
48
  targets: {
32
49
  type: 'array',
33
50
  items: {
34
51
  type: 'object',
35
52
  properties: {
36
53
  value: { type: 'string' },
37
- name: { type: 'string' },
54
+ name: {
55
+ type: 'string',
56
+ description:
57
+ 'The param key, or the source of one — see '
58
+ + '`shortenNames`. Must stay distinct from other targets\' '
59
+ + 'names after whichever treatment applies.',
60
+ },
38
61
  hash: {
39
62
  type: 'string',
40
63
  enum: ['oprf-raw', 'oprf', 'oprf-mpc'],
@@ -54,6 +77,9 @@ export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool {
54
77
  const requestId = String(args.requestId)
55
78
  const providerName = String(args.name)
56
79
  const defaultHash = args.hash as OprfMode | undefined
80
+ const shortenNames = typeof args.shortenNames === 'boolean'
81
+ ? args.shortenNames
82
+ : true
57
83
 
58
84
  // Explicit multi-value `targets`, else the single `target` shorthand.
59
85
  const raw = Array.isArray(args.targets) ? args.targets : []
@@ -103,7 +129,7 @@ export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool {
103
129
  }
104
130
 
105
131
  const { provider, secretRefs, extractions } = draftProvider(
106
- captured, targets, providerName, defaultHash,
132
+ captured, targets, providerName, defaultHash, { shortenNames },
107
133
  )
108
134
  // Materialize the real secret header values for later replay/prove;
109
135
  // they stay in the server-side draft, never in this result.
@@ -136,7 +136,6 @@ interface PublishArgs {
136
136
  allowedJsRequests?: AllowedJsRequest[]
137
137
  interceptorOptions?: {
138
138
  interceptorType: 'HAWKEYE' | 'MSWJS' | 'CDP'
139
- isDocumentRequestReplayEnabled?: boolean
140
139
  interceptorSettings?: string
141
140
  }
142
141
  }
@@ -268,17 +267,19 @@ export function createVersionFromCaptureTool(
268
267
  type: 'object',
269
268
  description:
270
269
  'In-app interception settings, stored as ' +
271
- 'webSettings.clientOptions.inapp.interceptorOptions. ' +
272
- '`isDocumentRequestReplayEnabled` is the INVERSE of ' +
273
- 'old-devtools\' `disableRequestReplay`: true replays the ' +
274
- 'main document request via the attestor (default false).',
270
+ 'webSettings.clientOptions.inapp.interceptorOptions — only ' +
271
+ 'needed for a genuine live-interception provider (a real ' +
272
+ 'HAWKEYE/MSWJS script, not the standard capture→replay flow). ' +
273
+ 'Document replay (webSettings.clientOptions.inapp' +
274
+ '.interceptorOptions.isDocumentRequestReplayEnabled) is ' +
275
+ 'ALWAYS published disabled regardless of this field — there ' +
276
+ 'is no way to enable it here.',
275
277
  properties: {
276
278
  interceptorType: {
277
279
  type: 'string',
278
280
  enum: ['HAWKEYE', 'MSWJS', 'CDP'],
279
281
  description: 'How traffic interception is implemented.',
280
282
  },
281
- isDocumentRequestReplayEnabled: { type: 'boolean' },
282
283
  interceptorSettings: {
283
284
  type: 'string',
284
285
  description:
@@ -13,7 +13,7 @@ import {
13
13
  extendTool,
14
14
  } from '../../../authoring/index.ts'
15
15
  import { runAttestorProof } from '../../../proof/attestor.ts'
16
- import { replayProvider } from '../../../proof/replay.ts'
16
+ import { replayProviderInPage } from '../../../proof/page-replay.ts'
17
17
  import type { OprfMode, ReclaimProvider } from '../../../provider/schema.ts'
18
18
  import type { RegisteredTool } from '../../server.ts'
19
19
  import type { CaptureEntry, CaptureStore } from './capture.ts'
@@ -51,14 +51,18 @@ function withLocalOprfMode(provider: ReclaimProvider): ReclaimProvider {
51
51
  }
52
52
 
53
53
  /** The local backend for one capture: drive its tab over CDP, read its
54
- * captured requests, prove via the in-process attestor, replay by re-fetch. */
54
+ * captured requests, prove via the in-process attestor, replay by re-fetching
55
+ * FROM INSIDE the tab (real cookies/session — see proof/page-replay.ts). */
55
56
  function localBackend(entry: CaptureEntry): AuthoringBackend {
56
57
  const { tabClient, session, drafts } = entry
58
+ const sendCdp = (method: string, params?: Record<string, unknown>) => {
59
+ return tabClient.send(method, params ?? {})
60
+ }
57
61
 
58
62
  return {
59
63
  capture: session,
60
64
  drafts,
61
- sendCdp: (method, params) => tabClient.send(method, params ?? {}),
65
+ sendCdp,
62
66
  waitForEvent: (method, timeoutMs) => new Promise((resolve) => {
63
67
  let stopListening = () => {}
64
68
 
@@ -79,10 +83,11 @@ function localBackend(entry: CaptureEntry): AuthoringBackend {
79
83
  stopListening = tabClient.subscribeOnce(method, onEvent)
80
84
  }),
81
85
  replay: async(draft, opts) => {
82
- // Auth-bound check: re-fetch with the secrets dropped. replayProvider
83
- // already does a live Node fetch, so empty secrets == anonymous.
84
- const secrets = opts?.withoutSecrets ? {} : draft.secrets
85
- const result = await replayProvider(draft.provider, secrets)
86
+ // Replayed FROM INSIDE the attached tab (real cookies/session), not
87
+ // a detached Node fetch — see proof/page-replay.ts.
88
+ const result = await replayProviderInPage(
89
+ { sendCdp }, draft.provider, draft.secrets, opts,
90
+ )
86
91
  return {
87
92
  status: result.status,
88
93
  matchesOriginal: result.matchesOriginal,
@@ -127,7 +132,13 @@ const RUN_PROOF_EXTRAS = {
127
132
  + 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
128
133
  + '(the credential to sign with) is REQUIRED — call resolve_owner_key '
129
134
  + 'first. errorKind="missing-owner-key" means that address has no known '
130
- + 'private key; re-run resolve_owner_key or issue/import a credential.',
135
+ + 'private key; re-run resolve_owner_key or issue/import a credential. '
136
+ + 'NOTE: unlike replay_request (which re-issues from inside the live '
137
+ + 'attached browser tab), the ATTESTOR calls the target from its OWN '
138
+ + 'network/IP, separate from the browser\'s. A request that replayed '
139
+ + 'fine can still fail here for geo/IP-bound content (country-locked '
140
+ + "pages, IP-allowlisted APIs) — if that happens, it's a property of "
141
+ + 'the site, not a bug in the draft.',
131
142
  properties: {
132
143
  ownerAddress: {
133
144
  type: 'string',