@reclaimprotocol/agent 0.1.22 → 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/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 +1 -1
- package/skill/SKILL.common.md +9 -0
- 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/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/lib/provider/draft.js
CHANGED
|
@@ -192,7 +192,32 @@ function deriveParamName(providerName) {
|
|
|
192
192
|
const last = tokens[tokens.length - 1] ?? 'value';
|
|
193
193
|
return last.toLowerCase();
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
/** A `paramName` doubles as a regex NAMED CAPTURE GROUP (`(?<name>...)`) and a
|
|
196
|
+
* `paramValues` object key — both require a plain identifier. Nothing
|
|
197
|
+
* downstream validates this: an invalid name builds a syntactically broken
|
|
198
|
+
* regex that silently fails to extract at verification time instead of
|
|
199
|
+
* erroring here, at draft time, where it's actually fixable. */
|
|
200
|
+
const VALID_PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
201
|
+
/** Resolve a target's `paramName`: shortened (the default — last alphanumeric
|
|
202
|
+
* token, lowercased) or, with `shorten: false`, the name verbatim — the
|
|
203
|
+
* caller's choice, validated so it's still safe as a capture-group
|
|
204
|
+
* identifier. */
|
|
205
|
+
function resolveParamName(targetName, shorten) {
|
|
206
|
+
if (!shorten) {
|
|
207
|
+
if (!VALID_PARAM_NAME.test(targetName)) {
|
|
208
|
+
throw new Error(`target name "${targetName}" isn't safe as a param name with `
|
|
209
|
+
+ 'shortenNames:false — it becomes a regex capture-group name AND '
|
|
210
|
+
+ 'a paramValues key, so it must start with a letter/underscore and '
|
|
211
|
+
+ 'contain only letters, digits, and underscores (we recommend '
|
|
212
|
+
+ 'lower_snake_case, e.g. "employer_name"). Rename it, or drop '
|
|
213
|
+
+ '`shortenNames: false` to auto-derive a safe one.');
|
|
214
|
+
}
|
|
215
|
+
return targetName;
|
|
216
|
+
}
|
|
217
|
+
return deriveParamName(targetName);
|
|
218
|
+
}
|
|
219
|
+
export function draftProvider(req, targets, name, hash, opts) {
|
|
220
|
+
const shortenNames = opts?.shortenNames ?? true;
|
|
196
221
|
if (!req.responseBody) {
|
|
197
222
|
throw new Error(`captured request ${req.requestId} has no response body`);
|
|
198
223
|
}
|
|
@@ -207,14 +232,21 @@ export function draftProvider(req, targets, name, hash) {
|
|
|
207
232
|
const extractions = [];
|
|
208
233
|
const effectiveHashes = [];
|
|
209
234
|
let paramValues;
|
|
210
|
-
const seenParams = new
|
|
235
|
+
const seenParams = new Map();
|
|
211
236
|
for (const t of targets) {
|
|
212
|
-
const paramName =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
237
|
+
const paramName = resolveParamName(t.name, shortenNames);
|
|
238
|
+
const priorName = seenParams.get(paramName);
|
|
239
|
+
if (priorName !== undefined) {
|
|
240
|
+
throw new Error(`targets "${priorName}" and "${t.name}" both resolve to the same `
|
|
241
|
+
+ `param name "${paramName}"`
|
|
242
|
+
+ (shortenNames
|
|
243
|
+
? ' (names are shortened to their last alphanumeric token) — '
|
|
244
|
+
+ 'give them more distinct names, e.g. "firstName"/"surname" '
|
|
245
|
+
+ 'instead of "first_name"/"last_name"'
|
|
246
|
+
: ' — give them distinct names, or drop `shortenNames: false` '
|
|
247
|
+
+ 'to auto-derive distinct ones'));
|
|
216
248
|
}
|
|
217
|
-
seenParams.
|
|
249
|
+
seenParams.set(paramName, t.name);
|
|
218
250
|
// Precedence: explicit per-target hash → explicit request-wide hash →
|
|
219
251
|
// the anti-leak floor (force-OPRF values whose SHAPE is unambiguous PII).
|
|
220
252
|
// The model makes the contextual calls (which named fields are sensitive)
|
package/lib/provider/matcher.js
CHANGED
|
@@ -117,18 +117,34 @@ function jsonMatcher(body, target, paramName, valueClass, hash) {
|
|
|
117
117
|
if (!jsonPath) {
|
|
118
118
|
return undefined;
|
|
119
119
|
}
|
|
120
|
-
const anchor = jsonKeyPrefix(body, target);
|
|
120
|
+
const anchor = jsonKeyPrefix(body, target, lastPathKey(jsonPath));
|
|
121
121
|
if (!anchor) {
|
|
122
122
|
return undefined;
|
|
123
123
|
}
|
|
124
124
|
return keyValueResult(anchor.prefix, anchor.close, target, paramName, valueClass, { jsonPath }, hash);
|
|
125
125
|
}
|
|
126
|
+
/** The trailing object-property name of a jsonPath (`$.total_records` →
|
|
127
|
+
* `"total_records"`), or undefined for an array-index/bracket leaf, which
|
|
128
|
+
* has no literal key to anchor a search on. */
|
|
129
|
+
function lastPathKey(jsonPath) {
|
|
130
|
+
const m = jsonPath ? /\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(jsonPath) : null;
|
|
131
|
+
return m?.[1];
|
|
132
|
+
}
|
|
126
133
|
/**
|
|
127
134
|
* The literal `"key":"` (or `"key":` for a raw number/bool) immediately before
|
|
128
135
|
* `target` in `text`, plus the closing quote (`"` for a string value, `''`
|
|
129
136
|
* otherwise). Returns undefined when the value isn't preceded by a JSON key.
|
|
137
|
+
*
|
|
138
|
+
* When `key` is given, anchors the search on occurrences of the KEY (not the
|
|
139
|
+
* value) — so a short/common value elsewhere in the body (e.g. the digit "5"
|
|
140
|
+
* inside `"investment_amount":354.85`) can't be mistaken for this field. Only
|
|
141
|
+
* falls back to a whole-body value search when there's no resolvable key
|
|
142
|
+
* (e.g. `findJsonPath` returned an array-index leaf).
|
|
130
143
|
*/
|
|
131
|
-
function jsonKeyPrefix(text, target) {
|
|
144
|
+
function jsonKeyPrefix(text, target, key) {
|
|
145
|
+
if (key !== undefined) {
|
|
146
|
+
return jsonKeyPrefixByKey(text, target, key);
|
|
147
|
+
}
|
|
132
148
|
const { idx } = firstIndex(text, target);
|
|
133
149
|
if (idx < 0) {
|
|
134
150
|
return undefined;
|
|
@@ -157,6 +173,61 @@ function jsonKeyPrefix(text, target) {
|
|
|
157
173
|
}
|
|
158
174
|
return { prefix: text.slice(keyStart, idx), close: quoted ? '"' : '' };
|
|
159
175
|
}
|
|
176
|
+
/** `jsonKeyPrefix`'s key-anchored path: scan occurrences of `"key"`, and for
|
|
177
|
+
* each, check whether the value that follows its `:` matches `target` (or a
|
|
178
|
+
* normalized candidate) before accepting it. */
|
|
179
|
+
function jsonKeyPrefixByKey(text, target, key) {
|
|
180
|
+
const keyToken = `"${key}"`;
|
|
181
|
+
let from = 0;
|
|
182
|
+
for (;;) {
|
|
183
|
+
const keyIdx = text.indexOf(keyToken, from);
|
|
184
|
+
if (keyIdx < 0) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
from = keyIdx + keyToken.length;
|
|
188
|
+
let i = from;
|
|
189
|
+
while (i < text.length && /\s/.test(text[i] ?? '')) {
|
|
190
|
+
i++;
|
|
191
|
+
}
|
|
192
|
+
if (text[i] !== ':') {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
i++;
|
|
196
|
+
while (i < text.length && /\s/.test(text[i] ?? '')) {
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
const matched = matchJsonValueAt(text, i, target);
|
|
200
|
+
if (matched) {
|
|
201
|
+
return {
|
|
202
|
+
prefix: text.slice(keyIdx, matched.valueStart),
|
|
203
|
+
close: matched.close,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Whether the JSON value starting at `pos` in `text` — quoted string or a
|
|
209
|
+
* raw number/bool/null token — equals `target` (or a normalized candidate).
|
|
210
|
+
* Returns the position right after any opening quote (matching `firstIndex`'s
|
|
211
|
+
* convention) and the delimiter to close the match template with. */
|
|
212
|
+
function matchJsonValueAt(text, pos, target) {
|
|
213
|
+
const candidates = [target, ...normalizeCandidates(target)];
|
|
214
|
+
if (text[pos] === '"') {
|
|
215
|
+
let j = pos + 1;
|
|
216
|
+
while (j < text.length && text[j] !== '"') {
|
|
217
|
+
j += text[j] === '\\' ? 2 : 1;
|
|
218
|
+
}
|
|
219
|
+
const raw = text.slice(pos + 1, j);
|
|
220
|
+
return candidates.includes(raw)
|
|
221
|
+
? { valueStart: pos + 1, close: '"' }
|
|
222
|
+
: undefined;
|
|
223
|
+
}
|
|
224
|
+
let j = pos;
|
|
225
|
+
while (j < text.length && !/[,}\]\s]/.test(text[j] ?? '')) {
|
|
226
|
+
j++;
|
|
227
|
+
}
|
|
228
|
+
const raw = text.slice(pos, j);
|
|
229
|
+
return candidates.includes(raw) ? { valueStart: pos, close: '' } : undefined;
|
|
230
|
+
}
|
|
160
231
|
function findJsonPath(body, target) {
|
|
161
232
|
let parsed;
|
|
162
233
|
try {
|
|
@@ -226,7 +297,9 @@ function htmlMatcher(body, target, paramName, valueClass, hash) {
|
|
|
226
297
|
const scriptJson = scriptJsonContent(revealSlice);
|
|
227
298
|
if (scriptJson) {
|
|
228
299
|
const jsonPath = findJsonPath(scriptJson, target);
|
|
229
|
-
const kp = jsonPath
|
|
300
|
+
const kp = jsonPath
|
|
301
|
+
? jsonKeyPrefix(scriptJson, target, lastPathKey(jsonPath))
|
|
302
|
+
: undefined;
|
|
230
303
|
if (jsonPath && kp) {
|
|
231
304
|
return keyValueResult(kp.prefix, kp.close, target, paramName, valueClass, { xPath: anchor.xPath, jsonPath }, hash);
|
|
232
305
|
}
|
|
@@ -396,7 +469,7 @@ function contentMatcher(body, target, paramName, valueClass, hash) {
|
|
|
396
469
|
return {
|
|
397
470
|
match: {
|
|
398
471
|
type: 'contains',
|
|
399
|
-
value: buildContextualTemplate(body, target, paramName),
|
|
472
|
+
value: buildContextualTemplate(body, target, paramName, valueClass),
|
|
400
473
|
},
|
|
401
474
|
redaction,
|
|
402
475
|
paramValues: { [paramName]: target },
|
|
@@ -421,12 +494,47 @@ function contentMatcher(body, target, paramName, valueClass, hash) {
|
|
|
421
494
|
* must CONTAIN the match template (buildContextualTemplate), so the trailing
|
|
422
495
|
* delimiter is kept unless it's whitespace (which the template also drops).
|
|
423
496
|
*/
|
|
497
|
+
/** Indices where `cand` occurs in `body` as a WHOLE match of `valueClass` —
|
|
498
|
+
* i.e. not a substring inside a longer run of the same class (the digit "5"
|
|
499
|
+
* inside "354.85" is never a whole `\d+` match; "354" and "85" are). Only
|
|
500
|
+
* meaningful for a structural (non-free-form) class. */
|
|
501
|
+
function wholeTokenIndices(body, cand, valueClass) {
|
|
502
|
+
const indices = [];
|
|
503
|
+
let re;
|
|
504
|
+
try {
|
|
505
|
+
re = new RegExp(valueClass, 'g');
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return indices;
|
|
509
|
+
}
|
|
510
|
+
let m;
|
|
511
|
+
while ((m = re.exec(body))) {
|
|
512
|
+
if (m[0] === cand) {
|
|
513
|
+
indices.push(m.index);
|
|
514
|
+
}
|
|
515
|
+
if (re.lastIndex === m.index) {
|
|
516
|
+
re.lastIndex++;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return indices;
|
|
520
|
+
}
|
|
424
521
|
function buildContentRegex(body, target, paramName, valueClass) {
|
|
522
|
+
const structural = inferValueClass(target) !== undefined;
|
|
425
523
|
for (const cand of normalizeCandidates(target)) {
|
|
426
|
-
const
|
|
524
|
+
const occurrences = structural
|
|
525
|
+
? wholeTokenIndices(body, cand, valueClass)
|
|
526
|
+
: undefined;
|
|
527
|
+
const idx = occurrences ? occurrences[0] ?? -1 : body.indexOf(cand);
|
|
427
528
|
if (idx < 0) {
|
|
428
529
|
continue;
|
|
429
530
|
}
|
|
531
|
+
if (occurrences && occurrences.length > 1
|
|
532
|
+
&& cand.replace(/[^A-Za-z0-9]/g, '').length < 4) {
|
|
533
|
+
throw new Error(`target value "${target}" occurs ${occurrences.length} times as a `
|
|
534
|
+
+ 'standalone token with no JSON key or HTML anchor to '
|
|
535
|
+
+ "disambiguate — capture a request where it's unique, or use a "
|
|
536
|
+
+ 'longer/more specific target.');
|
|
537
|
+
}
|
|
430
538
|
const endIdx = idx + cand.length;
|
|
431
539
|
const rawRight = findRightContext(body, endIdx);
|
|
432
540
|
const right = /^\s/.test(rawRight) ? '' : escapeRegexFlexWs(rawRight);
|
|
@@ -456,8 +564,9 @@ function buildContentRegex(body, target, paramName, valueClass) {
|
|
|
456
564
|
* literal contains can't flex whitespace); stable context (a JSON key, an HTML
|
|
457
565
|
* opening tag) is kept so the match anchors on it.
|
|
458
566
|
*/
|
|
459
|
-
function buildContextualTemplate(body, target, paramName) {
|
|
460
|
-
const idx = body
|
|
567
|
+
function buildContextualTemplate(body, target, paramName, valueClass) {
|
|
568
|
+
const idx = wholeTokenIndices(body, target, valueClass)[0]
|
|
569
|
+
?? body.indexOf(target);
|
|
461
570
|
if (idx < 0) {
|
|
462
571
|
return `{{${paramName}}}`;
|
|
463
572
|
}
|
|
@@ -54,14 +54,16 @@ export interface CreateVersionOptions {
|
|
|
54
54
|
jsUserScripts?: string;
|
|
55
55
|
/** Requests the injection script may fire, as `RequestSelectionTemplate`s. */
|
|
56
56
|
allowedJsRequests?: AllowedJsRequest[];
|
|
57
|
-
/** In-app interception options, mapped
|
|
58
|
-
* `webSettings.clientOptions.inapp.interceptorOptions
|
|
59
|
-
* is
|
|
60
|
-
* `
|
|
61
|
-
* `
|
|
57
|
+
/** In-app interception options, mapped to
|
|
58
|
+
* `webSettings.clientOptions.inapp.interceptorOptions` — ONLY when this
|
|
59
|
+
* is explicitly passed (builder's schema has no `NONE` interceptorType;
|
|
60
|
+
* omitting `inapp` entirely IS the "no interception" signal, so omitting
|
|
61
|
+
* this must omit `inapp`, not default it to HAWKEYE). When it IS passed,
|
|
62
|
+
* `isDocumentRequestReplayEnabled` is still ALWAYS force-disabled
|
|
63
|
+
* regardless of what's passed here — see `providerToCreateVersionRequest`;
|
|
64
|
+
* there is no author-facing way to enable it. */
|
|
62
65
|
interceptorOptions?: {
|
|
63
66
|
interceptorType: 'HAWKEYE' | 'MSWJS' | 'CDP';
|
|
64
|
-
isDocumentRequestReplayEnabled?: boolean;
|
|
65
67
|
interceptorSettings?: string;
|
|
66
68
|
};
|
|
67
69
|
}
|
|
@@ -130,14 +130,26 @@ export function providerToCreateVersionRequest(providers, initialUrl, opts = {})
|
|
|
130
130
|
geoLocation: pickGeoLocation(providers),
|
|
131
131
|
clientOptions: {
|
|
132
132
|
portal: { useProxy: true },
|
|
133
|
+
// Builder's schema has NO "NONE" interceptorType — the real
|
|
134
|
+
// no-interception signal is OMITTING `inapp` entirely (it's not
|
|
135
|
+
// `required` on `InAppClientOptions`). So `inapp` is only built at
|
|
136
|
+
// ALL when the author explicitly opts into a real interceptorType;
|
|
137
|
+
// omitting `interceptorOptions` must omit `inapp` too, or every
|
|
138
|
+
// standard (non-interception) provider would incorrectly publish
|
|
139
|
+
// as HAWKEYE. Within that block, `isDocumentRequestReplayEnabled`
|
|
140
|
+
// is still force-disabled — see `CreateVersionOptions
|
|
141
|
+
// .interceptorOptions`'s doc comment; there is no way to enable it.
|
|
133
142
|
...(interceptorOptions
|
|
134
143
|
? {
|
|
135
144
|
inapp: {
|
|
136
145
|
interceptorOptions: {
|
|
137
|
-
|
|
138
|
-
// requires the key even when the author omits it.
|
|
146
|
+
interceptorType: interceptorOptions.interceptorType,
|
|
139
147
|
isDocumentRequestReplayEnabled: false,
|
|
140
|
-
...interceptorOptions
|
|
148
|
+
...(interceptorOptions.interceptorSettings
|
|
149
|
+
? {
|
|
150
|
+
interceptorSettings: interceptorOptions.interceptorSettings,
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
141
153
|
},
|
|
142
154
|
},
|
|
143
155
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reclaimprotocol/agent",
|
|
3
3
|
"title": "Reclaim Agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.23",
|
|
5
5
|
"description": "Reclaim Protocol's tools for agents — Reclaim Protocol lets your app verify **data points** about its users — a follower count, an account balance, an order history. This package gives your AI coding agent the tools to **find, build, test and fix the _data verification providers_** (the recipes that verify a data point) by inspecting and replaying captured requests, checking sessions, evaluate proofs, publishing updates, and much more.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -83,10 +83,11 @@
|
|
|
83
83
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
84
84
|
"@noble/hashes": "^2.2.0",
|
|
85
85
|
"@reclaimprotocol/attestor-core": "^5.0.8",
|
|
86
|
-
"@reclaimprotocol/client": "0.1.
|
|
86
|
+
"@reclaimprotocol/client": "0.1.23",
|
|
87
87
|
"@reclaimprotocol/tls": "^0.1.3",
|
|
88
88
|
"chrome-remote-interface": "^0.33.0",
|
|
89
89
|
"domhandler": "^6.0.0",
|
|
90
|
+
"openid-client": "^6.8.5",
|
|
90
91
|
"parse5": "^8.0.0",
|
|
91
92
|
"parse5-htmlparser2-tree-adapter": "^8.0.0",
|
|
92
93
|
"pino": "^9.14.0",
|
package/skill/SKILL.builder.md
CHANGED
|
@@ -208,4 +208,4 @@ If `replay_request` or `run_proof` errors with `unknown draftId` (or `no capture
|
|
|
208
208
|
|
|
209
209
|
## Recognizing a HAWKEYE / live-interception provider
|
|
210
210
|
|
|
211
|
-
Everything above builds the STANDARD flow: capture → draft → replay → prove. There is a second flow — `webSettings.inapp.interceptorOptions.interceptorType: HAWKEYE` (or `MSWJS`/`CDP`) plus a `jsUserScripts` script that calls `window.reclaimInterceptor.addResponseMiddleware(...)` to observe the page's REAL responses as they happen, instead of an independent replay. `create_provider_version_from_capture`
|
|
211
|
+
Everything above builds the STANDARD flow: capture → draft → replay → prove. There is a second flow — `webSettings.inapp.interceptorOptions.interceptorType: HAWKEYE` (or `MSWJS`/`CDP`) plus a `jsUserScripts` script that calls `window.reclaimInterceptor.addResponseMiddleware(...)` to observe the page's REAL responses as they happen, instead of an independent replay. `create_provider_version_from_capture` exposes `interceptorOptions.interceptorType` as an author-settable input for exactly this — pass it (plus a script that calls `addResponseMiddleware`) to opt INTO real interception. Omit `interceptorOptions` entirely for the standard flow (the default) — the tool correctly omits `inapp` rather than defaulting it to `HAWKEYE`, since builder's schema has no `NONE` interceptorType and omitting the whole object IS the no-interception signal. `isDocumentRequestReplayEnabled` is a NARROWER, separate toggle from the interception itself — it does not gate `addResponseMiddleware` in general (that fires for every `interceptorType` including `CDP`); it only controls whether the current page's own document/navigation request specifically gets pulled through a non-CDP interceptor, as a site-compatibility escape hatch. The tool always force-disables it (`false`) whenever `interceptorOptions` is passed at all — there is no way to enable it through `create_provider_version_from_capture`. See `injection-api.md` (embedded in the `jsUserScripts` field description) for the source-verified breakdown before guessing at what they mean.
|
package/skill/SKILL.common.md
CHANGED
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
> If a developer calls the recipe a "verification," gently use "provider"
|
|
12
12
|
> yourself rather than correcting them.
|
|
13
13
|
|
|
14
|
+
> **Terminology note:** "JS injection", "custom injection", "user script",
|
|
15
|
+
> and "web script" all refer to the SAME thing when talking about a
|
|
16
|
+
> provider — the plain browser JS that runs before every page load to drive
|
|
17
|
+
> navigation for data gated behind login/routing. Builder's schema field is
|
|
18
|
+
> `jsUserScripts`; old-devtools' equivalent field is `customInjection`
|
|
19
|
+
> (identical semantics, different name). Prefer **"user script"** in your
|
|
20
|
+
> own prose regardless of which backend you're on; see `injection-api.md`
|
|
21
|
+
> for the full field-level reference.
|
|
22
|
+
|
|
14
23
|
You drive the browser through the capture → draft → replay → prove loop using the `reclaim-agent` MCP server. **Your default stance is driver, not orchestrator.** Only pause to ask the dev for things that genuinely require a human: typing credentials, completing 2FA, solving a captcha, confirming an ambiguous value. Navigation, reloads, DOM inspection, request capture — all yours.
|
|
15
24
|
|
|
16
25
|
<!-- section: step-attach -->
|
package/skill/SKILL.old.md
CHANGED
|
@@ -174,4 +174,4 @@ If `replay_request` or `run_proof` errors with `unknown secretParamsRef`:
|
|
|
174
174
|
|
|
175
175
|
## Recognizing a HAWKEYE / live-interception provider
|
|
176
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.
|
|
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. `create_provider_version_from_capture` now exposes `injectionType` as an author-settable input — pass one of `HAWKEYE`/`MSWJS`/`XHOOK`/`CDP` (plus a `customInjection` script that calls `addResponseMiddleware`) to author this flow. Omit it for the standard flow: there's no forced default, so the old backend's own schema default (`HAWKEYE`) applies, same as before this field was exposed — that's fine, it's inert with no interceptor script installed. Pass `NONE` explicitly instead if you want certainty rather than relying on the backend default. Don't assume `injectionType` present-but-not-`NONE` on an EXISTING provider (`get_provider_version_config`, `get_provider_info`) means something is broken — HAWKEYE-by-default is normal and harmless for the standard flow on both new and old providers. `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. `create_provider_version_from_capture` always force-publishes this `true` now (auto-following a fresh register with an add-version call to apply it, since register hardcodes it `false`) — there is no author-facing way to leave it off. 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.
|
package/skill/injection-api.md
CHANGED
|
@@ -53,18 +53,30 @@ under EVERY mechanism below, including `CDP` — it is not a HAWKEYE-specific
|
|
|
53
53
|
thing.
|
|
54
54
|
|
|
55
55
|
- **old-devtools** field: `injectionType`, one of `NONE | MSWJS | XHOOK | CDP |
|
|
56
|
-
HAWKEYE
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
interceptor script installed,
|
|
62
|
-
`addResponseMiddleware
|
|
63
|
-
|
|
56
|
+
HAWKEYE` — confirmed the literal field name against a real snapshot of
|
|
57
|
+
legacy-backend provider data (`packages/app/scripts/data/
|
|
58
|
+
provider-configs.snapshot.json`, captured straight from the old-devtools
|
|
59
|
+
Mongo `Provider`/`ProviderVersion.providerConfig`). The backend's own
|
|
60
|
+
column default is `HAWKEYE`, not `NONE` — harmless for the standard flow
|
|
61
|
+
(no interceptor script installed, so nothing calls
|
|
62
|
+
`addResponseMiddleware`). `create_provider_version_from_capture`
|
|
63
|
+
(old-devtools mode) now exposes `injectionType` as an author-settable
|
|
64
|
+
input, with NO forced default — omit it and the backend's own `HAWKEYE`
|
|
65
|
+
default applies, same as before this field was exposed at all; pass an
|
|
66
|
+
explicit value (including `NONE`) if you want certainty either way. When
|
|
67
|
+
passed, it's sent on both register and add-version; since it's unverified
|
|
68
|
+
whether the register controller actually persists it (no source access to
|
|
69
|
+
confirm), the tool's existing always-on add-version follow-up (see
|
|
70
|
+
`disableRequestReplay` below) reapplies it regardless, so an explicit value
|
|
71
|
+
is guaranteed correct by the time the follow-up version lands.
|
|
64
72
|
- **builder** field: `webSettings.inapp.interceptorOptions.interceptorType`,
|
|
65
|
-
one of `HAWKEYE | MSWJS | CDP` (no `NONE`/`XHOOK` in this schema
|
|
66
|
-
`
|
|
67
|
-
|
|
73
|
+
one of `HAWKEYE | MSWJS | CDP` (no `NONE`/`XHOOK` in this schema — omitting
|
|
74
|
+
`interceptorOptions` entirely is builder's NONE-equivalent).
|
|
75
|
+
`create_provider_version_from_capture` (builder mode) exposes
|
|
76
|
+
`interceptorOptions.interceptorType` as an author-settable input; passing
|
|
77
|
+
it is how you opt INTO real interception. Omit it (the default) for the
|
|
78
|
+
standard flow, and the tool correctly omits `inapp` — it does NOT default
|
|
79
|
+
to `HAWKEYE` (that was a bug in an earlier version of this tool; fixed).
|
|
68
80
|
|
|
69
81
|
**The mechanisms, and why you'd pick one over plain replay:**
|
|
70
82
|
- **`CDP`** — interception at the browser/DevTools-protocol level, below the
|
|
@@ -83,6 +95,24 @@ thing.
|
|
|
83
95
|
|
|
84
96
|
## `disableRequestReplay` / `isDocumentRequestReplayEnabled`
|
|
85
97
|
|
|
98
|
+
**`create_provider_version_from_capture` always force-disables this now, in
|
|
99
|
+
BOTH backends** — old-devtools always publishes `disableRequestReplay: true`
|
|
100
|
+
(the tool auto-follows a fresh register with an add-version call to apply
|
|
101
|
+
it, since the register endpoint hardcodes it `false` regardless of what's
|
|
102
|
+
sent). Builder always publishes `isDocumentRequestReplayEnabled: false`
|
|
103
|
+
under `webSettings.clientOptions.inapp.interceptorOptions` **whenever you
|
|
104
|
+
pass `interceptorOptions` at all** — but if you don't (the standard,
|
|
105
|
+
non-interception flow), the tool omits `inapp` entirely rather than forcing
|
|
106
|
+
a default `interceptorType`, because builder's schema has no `NONE`
|
|
107
|
+
interceptorType: omitting the whole object IS its no-interception signal,
|
|
108
|
+
and defaulting it to (even inert) `HAWKEYE` would misrepresent the
|
|
109
|
+
provider. There is no author-facing way to turn document replay back on
|
|
110
|
+
through either tool once `interceptorOptions`/`injectionType` is in play.
|
|
111
|
+
The rest of this section explains the underlying platform semantics for
|
|
112
|
+
context (e.g. reading an existing provider someone else published outside
|
|
113
|
+
these tools) — the replay sub-flag itself is no longer something you
|
|
114
|
+
configure here.
|
|
115
|
+
|
|
86
116
|
**This does NOT gate `addResponseMiddleware` in general** — that fires for
|
|
87
117
|
any intercepted request regardless of this field. What it actually controls:
|
|
88
118
|
whether the CURRENT PAGE'S OWN document/navigation request (the request that
|
|
@@ -125,10 +155,9 @@ Field names, per backend:
|
|
|
125
155
|
schema's stated default is "not sent"; verify against whichever backend
|
|
126
156
|
you're publishing to rather than carrying an assumption across.
|
|
127
157
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
field isn't touched by `create_provider_version_from_capture` at all).
|
|
158
|
+
There is nothing to configure here through `create_provider_version_from_capture`
|
|
159
|
+
any more (see the note at the top of this section) — both backends always
|
|
160
|
+
publish with document replay disabled.
|
|
132
161
|
|
|
133
162
|
## `bodySniff` — unrelated to the above
|
|
134
163
|
|
package/skill/old-mode.md
CHANGED
|
@@ -2,7 +2,7 @@ This server is running against the devtools backend, so its capabilities are int
|
|
|
2
2
|
|
|
3
3
|
**Provider-authoring flow:**
|
|
4
4
|
|
|
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.
|
|
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. (Note: this old-devtools login is separate from `authenticate_builder`, which is needed only for the two Builder-exclusive features below — remote-browser allocation and org listing — see the auth troubleshooting note below.)
|
|
6
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.
|
|
@@ -10,6 +10,8 @@ This server is running against the devtools backend, so its capabilities are int
|
|
|
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
|
+
**Two independent auth systems — don't confuse them:** `authenticate_builder` and `reclaim_authenticate` log into DIFFERENT backends and are NOT interchangeable. In THIS mode (old-devtools), `authenticate_builder` is needed only for two Builder-exclusive features — allocating a Popcorn remote browser (`attach_browser mode="builder"`) and listing Builder organizations (`list_builder_organizations`, since old-devtools has no generated org tool of its own). It does not authorize `create_provider_version_from_capture`, `get_me_providers`, or any other old-devtools call — those need their own `reclaim_authenticate` login. (Builder mode, by contrast, needs no separate login at all — its one native auth tool covers everything, org listing included via `list_orgs`.) `get_devtools_mode` reports the presence of both. If a publish or `get_me_providers` call fails with an auth error even though you (or the dev) already ran `reclaim_authenticate` once before, the old-devtools token has likely expired (Firebase tokens are short-lived, ~1h) — re-run `reclaim_authenticate` and retry; don't assume `authenticate_builder` fixes it.
|
|
14
|
+
|
|
13
15
|
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
16
|
|
|
15
17
|
**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).
|
package/src/authoring/backend.ts
CHANGED
|
@@ -90,13 +90,15 @@ export interface AuthoringBackend {
|
|
|
90
90
|
* the cloud publish step) — that side effect is the backend's, not ours. */
|
|
91
91
|
prove(draft: Draft, opts?: ProveOptions): Promise<unknown>
|
|
92
92
|
/**
|
|
93
|
-
* Validate/replay a drafted recipe before proving.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
93
|
+
* Validate/replay a drafted recipe before proving. Both local and remote
|
|
94
|
+
* backends re-issue the request FROM INSIDE the live attached page (a
|
|
95
|
+
* `fetch` run over CDP), never a detached out-of-band call, so replay
|
|
96
|
+
* always reflects the real browser session. With `withoutSecrets` it is
|
|
97
|
+
* the AUTH-BOUND check: re-issue anonymously (`credentials: 'omit'`, no
|
|
98
|
+
* secret headers) and report whether the values are STILL extractable —
|
|
99
|
+
* i.e. the endpoint is public. Without it, validate extraction for the
|
|
100
|
+
* authenticated request. The shape is environment-specific and passed
|
|
101
|
+
* straight through as the tool's result.
|
|
100
102
|
*/
|
|
101
103
|
replay(draft: Draft, opts?: { withoutSecrets?: boolean }): Promise<unknown>
|
|
102
104
|
}
|
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,
|