@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.
- package/lib/authoring/backend.d.ts +9 -7
- package/lib/authoring/tools.js +98 -1
- package/lib/mcp/tools/agent/attach.js +29 -1
- package/lib/mcp/tools/agent/propose.js +27 -3
- package/lib/mcp/tools/agent/publish.js +7 -5
- package/lib/mcp/tools/agent/shared.js +17 -8
- package/lib/mcp/tools/authenticate/authenticate.d.ts +11 -0
- package/lib/mcp/tools/authenticate/authenticate.js +128 -119
- package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -3
- package/lib/mcp/tools/authenticate/builder-access.js +8 -3
- package/lib/mcp/tools/mode/mode.js +32 -1
- package/lib/old/client.d.ts +9 -0
- package/lib/old/client.js +19 -1
- package/lib/old/to-register.d.ts +17 -6
- package/lib/old/to-register.js +6 -0
- package/lib/old/tools/me-providers.js +3 -1
- package/lib/old/tools/publish.js +82 -38
- package/lib/proof/page-replay.d.ts +36 -0
- package/lib/proof/page-replay.js +76 -0
- package/lib/proof/replay.d.ts +28 -0
- package/lib/proof/replay.js +39 -18
- package/lib/provider/draft.d.ts +3 -1
- package/lib/provider/draft.js +39 -7
- package/lib/provider/matcher.js +116 -7
- package/lib/provider/to-version.d.ts +8 -6
- package/lib/provider/to-version.js +15 -3
- package/package.json +3 -2
- package/skill/SKILL.builder.md +4 -2
- package/skill/SKILL.common.md +10 -1
- package/skill/SKILL.old.md +1 -1
- package/skill/injection-api.md +44 -15
- package/skill/old-mode.md +3 -1
- package/src/authoring/backend.ts +9 -7
- package/src/authoring/tools.ts +119 -1
- package/src/mcp/tools/agent/attach.ts +40 -1
- package/src/mcp/tools/agent/propose.ts +29 -3
- package/src/mcp/tools/agent/publish.ts +7 -6
- package/src/mcp/tools/agent/shared.ts +19 -8
- package/src/mcp/tools/authenticate/authenticate.ts +150 -163
- package/src/mcp/tools/authenticate/builder-access.ts +8 -3
- package/src/mcp/tools/mode/mode.ts +34 -1
- package/src/old/client.ts +30 -1
- package/src/old/to-register.ts +25 -6
- package/src/old/tools/me-providers.ts +3 -1
- package/src/old/tools/publish.ts +86 -40
- package/src/proof/page-replay.ts +131 -0
- package/src/proof/replay.ts +60 -19
- package/src/provider/draft.ts +46 -6
- package/src/provider/matcher.ts +145 -5
- package/src/provider/to-version.ts +24 -9
package/src/authoring/tools.ts
CHANGED
|
@@ -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: {
|
|
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
|
-
'
|
|
273
|
-
'
|
|
274
|
-
'
|
|
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 {
|
|
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-
|
|
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
|
|
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
|
-
//
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
|
|
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',
|