@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/lib/proof/replay.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Expand `{{name}}` placeholders from `paramValues`. Matches the attestor
|
|
2
2
|
* SDK's behavior so replay and proof both hit the same final URL. */
|
|
3
|
-
function expandTemplate(s, params) {
|
|
3
|
+
export function expandTemplate(s, params) {
|
|
4
4
|
if (!params) {
|
|
5
5
|
return s;
|
|
6
6
|
}
|
|
@@ -9,7 +9,7 @@ function expandTemplate(s, params) {
|
|
|
9
9
|
return v !== undefined ? v : match;
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
-
export
|
|
12
|
+
export function buildReplayRequest(provider, secrets) {
|
|
13
13
|
const params = provider.paramValues;
|
|
14
14
|
const url = expandTemplate(provider.url, params);
|
|
15
15
|
const expandedHeaders = {};
|
|
@@ -17,21 +17,29 @@ export async function replayProvider(provider, secrets) {
|
|
|
17
17
|
expandedHeaders[k] = expandTemplate(v, params);
|
|
18
18
|
}
|
|
19
19
|
const headers = { ...expandedHeaders, ...secrets };
|
|
20
|
-
const
|
|
20
|
+
const body = provider.body !== undefined
|
|
21
|
+
? expandTemplate(provider.body, params)
|
|
22
|
+
: undefined;
|
|
23
|
+
return {
|
|
24
|
+
url,
|
|
21
25
|
method: provider.method,
|
|
22
26
|
headers,
|
|
23
|
-
|
|
27
|
+
...(body !== undefined ? { body } : {}),
|
|
24
28
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Judge a replayed response against `provider`'s matchers — the part of
|
|
32
|
+
* replay that's identical regardless of HOW the request was sent (detached
|
|
33
|
+
* Node `fetch` or a live in-browser `fetch`). Separated from the actual
|
|
34
|
+
* network call so both transports share one evaluation.
|
|
35
|
+
*/
|
|
36
|
+
export function evaluateReplay(status, body, provider) {
|
|
37
|
+
const params = provider.paramValues;
|
|
30
38
|
const hints = [];
|
|
31
|
-
if (
|
|
39
|
+
if (status === 401 || status === 403) {
|
|
32
40
|
hints.push('auth/permission failed');
|
|
33
41
|
}
|
|
34
|
-
if (
|
|
42
|
+
if (status >= 500) {
|
|
35
43
|
hints.push('upstream server error');
|
|
36
44
|
}
|
|
37
45
|
for (const m of provider.responseMatches) {
|
|
@@ -45,22 +53,35 @@ export async function replayProvider(provider, secrets) {
|
|
|
45
53
|
value: expandTemplate(m.value, params),
|
|
46
54
|
};
|
|
47
55
|
if (!matchesBody(body, expanded)) {
|
|
48
|
-
if (
|
|
56
|
+
if (status >= 200 && status < 300) {
|
|
49
57
|
hints.push(`matcher ${expanded.type} did not match — ` +
|
|
50
58
|
'value may have changed or matcher is too tight');
|
|
51
59
|
}
|
|
52
|
-
const result = {
|
|
53
|
-
status: res.status,
|
|
54
|
-
body,
|
|
55
|
-
matchesOriginal: false,
|
|
56
|
-
};
|
|
60
|
+
const result = { status, body, matchesOriginal: false };
|
|
57
61
|
if (hints.length > 0) {
|
|
58
62
|
result.hints = hints;
|
|
59
63
|
}
|
|
60
64
|
return result;
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
|
-
return { status
|
|
67
|
+
return { status, body, matchesOriginal: status < 400 };
|
|
68
|
+
}
|
|
69
|
+
/** Detached Node-side replay: sends the request directly from this process,
|
|
70
|
+
* NOT from any attached browser tab — its cookies/secrets are exactly the
|
|
71
|
+
* `secrets` passed in, never a real browser session. Used by tests and by
|
|
72
|
+
* any caller with no live browser to replay through (the MCP's authoring
|
|
73
|
+
* backend instead runs the SAME request inside the attached tab — see
|
|
74
|
+
* `proof/page-replay.ts` — so replay reflects real session cookies). */
|
|
75
|
+
export async function replayProvider(provider, secrets) {
|
|
76
|
+
const req = buildReplayRequest(provider, secrets);
|
|
77
|
+
const res = await fetch(req.url, {
|
|
78
|
+
method: req.method,
|
|
79
|
+
headers: req.headers,
|
|
80
|
+
redirect: 'manual',
|
|
81
|
+
...(req.body !== undefined ? { body: req.body } : {}),
|
|
82
|
+
});
|
|
83
|
+
const body = await res.text();
|
|
84
|
+
return evaluateReplay(res.status, body, provider);
|
|
64
85
|
}
|
|
65
86
|
function matchesBody(body, m) {
|
|
66
87
|
if (m.type === 'regex') {
|
package/lib/provider/draft.d.ts
CHANGED
|
@@ -57,4 +57,6 @@ export declare function assertNoOprfUrlConflict(url: string, targets: DraftTarge
|
|
|
57
57
|
* header (same class of problem as {@link assertNoOprfUrlConflict} for
|
|
58
58
|
* OPRF-hashed targets, which has no such budget at all). */
|
|
59
59
|
export declare function assertNoSecretUrlConflict(url: string, secrets: Record<string, string>, writeRedactionMode?: ReclaimProvider['writeRedactionMode']): void;
|
|
60
|
-
export declare function draftProvider(req: CapturedRequest, targets: DraftTarget[], name: string, hash?: OprfMode
|
|
60
|
+
export declare function draftProvider(req: CapturedRequest, targets: DraftTarget[], name: string, hash?: OprfMode, opts?: {
|
|
61
|
+
shortenNames?: boolean;
|
|
62
|
+
}): DraftResult;
|
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
|
@@ -173,7 +173,9 @@ The value of any parameter marked secret is not included in the final proof, omi
|
|
|
173
173
|
|
|
174
174
|
When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
|
|
175
175
|
|
|
176
|
-
- **If a browser is still attached**, `create_provider_version_from_capture`'s response carries a `_notes` reminder about it. Surface that to the dev and ask whether they want to keep the session open for further edits — only call `dispose_browser` once they confirm they're done. Never close it unprompted; a `builder`-mode session is quota-accounted, but the dev may still be mid-workflow.
|
|
176
|
+
- **If a browser is still attached**, `create_provider_version_from_capture`'s response carries a `_notes` reminder about it. Surface that to the dev and ask whether they want to keep the session open for further edits — only call `dispose_browser` once they confirm they're done. Never close it unprompted; a `builder`-mode session is quota-accounted, but the dev may still be mid-workflow. Closing remote browsers
|
|
177
|
+
soon is also important to avoid wasting resources. So as best
|
|
178
|
+
practise, close remote browser session when not needed anymore.
|
|
177
179
|
|
|
178
180
|
<!-- common: concerns-section -->
|
|
179
181
|
|
|
@@ -206,4 +208,4 @@ If `replay_request` or `run_proof` errors with `unknown draftId` (or `no capture
|
|
|
206
208
|
|
|
207
209
|
## Recognizing a HAWKEYE / live-interception provider
|
|
208
210
|
|
|
209
|
-
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,13 +11,22 @@
|
|
|
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 -->
|
|
17
26
|
1. **Attach.** Call `attach_browser`. If it returns `needsChoice: true`, ask the dev which browser source they want:
|
|
18
27
|
- `dedicated` — agent auto-launches Chrome with a dedicated profile under `~/.reclaim/chrome-profile`. Easiest local path; works out of the box.
|
|
19
28
|
- `attach` — connect to a Chrome the dev is already running. **Important Chrome 136+ gotcha:** the dev's Chrome must be launched with BOTH `--remote-debugging-port=9222` AND `--user-data-dir=<some-non-default-path>`. Since Chrome 136 (May 2025), the debug-port flag is silently ignored when the default user profile is in use. If `attach` fails, tell them this verbatim and offer to switch to `dedicated`.
|
|
20
|
-
- `builder` — allocate a quota-accounted remote Popcorn browser.
|
|
29
|
+
- `builder` — allocate a quota-accounted remote Popcorn browser. If Builder answers 401, authenticate with `authenticate_builder` in old-devtools mode or the existing `authenticate` flow in Builder mode, then retry. You can omit `orgId` and call `attach_browser` directly: it auto-resolves to the account's only organization, or — if the account belongs to more than one — comes back with `needsChoice: true` and an `organizations` list; **ask the dev which one to use** (don't guess or default to the first) and re-call `attach_browser` with that `orgId`. (`list_builder_organizations` in old-devtools mode / `list_orgs` in Builder mode still work directly if you want the list up front instead.) Return the FULL `liveViewUrl` to the dev verbatim — on its own line, never abbreviated, elided, or wrapped in prose that truncates it — so it stays one clean, copy-pasteable string; they'll share it with the volunteer operating the browser. This works in both publishing modes and consumes one VERIFICATION-pool use under the chosen organization's normal plan restrictions.
|
|
21
30
|
- `custom` — connect to a `ws://` or `wss://` CDP URL supplied by the dev. The URL is a credential: never repeat it, log it, or persist it. An optional `liveViewUrl` may be returned — if so, share it FULL and verbatim, same as `builder` above.
|
|
22
31
|
|
|
23
32
|
Re-call with the chosen `mode` and its required fields.
|
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
|
}
|