@reclaimprotocol/agent 0.1.36 → 0.1.37
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/README.md +8 -8
- package/lib/authoring/backend.d.ts +12 -1
- package/lib/authoring/backend.js +3 -0
- package/lib/authoring/tools.js +33 -5
- package/lib/authoring/util.d.ts +43 -0
- package/lib/authoring/util.js +54 -0
- package/lib/cdp/chrome-for-testing.d.ts +14 -0
- package/lib/cdp/chrome-for-testing.js +43 -0
- package/lib/cdp/geo.d.ts +3 -0
- package/lib/cdp/geo.js +27 -15
- package/lib/cdp/launcher.d.ts +79 -1
- package/lib/cdp/launcher.js +244 -24
- package/lib/logger.js +3 -1
- package/lib/mcp/instructions.js +3 -3
- package/lib/mcp/start.js +17 -2
- package/lib/mcp/tools/agent/attach.d.ts +6 -0
- package/lib/mcp/tools/agent/attach.js +58 -118
- package/lib/mcp/tools/agent/build.js +6 -0
- package/lib/mcp/tools/agent/capture.js +33 -10
- package/lib/mcp/tools/agent/check-attestor.js +15 -7
- package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
- package/lib/mcp/tools/agent/draft-tools.js +170 -0
- package/lib/mcp/tools/agent/pod-proof.js +5 -1
- package/lib/mcp/tools/agent/proof.d.ts +28 -0
- package/lib/mcp/tools/agent/proof.js +72 -0
- package/lib/mcp/tools/agent/shared.d.ts +8 -1
- package/lib/mcp/tools/agent/shared.js +98 -19
- package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
- package/lib/mcp/tools/agent/start-integration.js +10 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
- package/lib/mcp/tools/authenticate/authenticate.js +212 -116
- package/lib/mcp/tools/authenticate/build.d.ts +2 -2
- package/lib/mcp/tools/authenticate/build.js +6 -3
- package/lib/mcp/tools/authenticate/builder-access.js +5 -2
- package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
- package/lib/mcp/tools/authenticate/eth-key.js +29 -5
- package/lib/mcp/tools/authenticate/index.d.ts +1 -1
- package/lib/mcp/tools/authenticate/index.js +1 -1
- package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
- package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
- package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
- package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
- package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
- package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
- package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
- package/lib/mcp/tools/authenticate/session-cache.js +95 -4
- package/lib/mcp/tools/mode/mode.js +6 -6
- package/lib/old/login-cdp.js +2 -3
- package/lib/proof/attestor.js +2 -3
- package/lib/proof/check-attestor.d.ts +12 -2
- package/lib/proof/check-attestor.js +40 -10
- package/lib/proof/check-matchers.d.ts +36 -0
- package/lib/proof/check-matchers.js +161 -0
- package/lib/proof/zk-files.d.ts +50 -11
- package/lib/proof/zk-files.js +259 -63
- package/lib/provider/draft.d.ts +24 -1
- package/lib/provider/draft.js +135 -0
- package/lib/provider/matcher.js +171 -4
- package/package.json +6 -4
- package/skill/SKILL.builder.md +7 -2
- package/skill/SKILL.common.md +90 -70
- package/skill/SKILL.old.md +3 -3
- package/skill/builder-mode.md +35 -5
- package/skill/old-mode.md +18 -13
- package/skill/start-integration.md +48 -33
- package/skill/verification-client-integration.md +21 -6
- package/src/authoring/backend.ts +12 -1
- package/src/authoring/tools.ts +48 -7
- package/src/authoring/util.ts +64 -0
- package/src/cdp/chrome-for-testing.ts +66 -0
- package/src/cdp/geo.ts +36 -21
- package/src/cdp/launcher.ts +328 -24
- package/src/logger.ts +3 -1
- package/src/mcp/instructions.ts +3 -3
- package/src/mcp/start.ts +20 -1
- package/src/mcp/tools/agent/attach.ts +65 -128
- package/src/mcp/tools/agent/build.ts +7 -0
- package/src/mcp/tools/agent/capture.ts +54 -11
- package/src/mcp/tools/agent/check-attestor.ts +15 -7
- package/src/mcp/tools/agent/draft-tools.ts +234 -0
- package/src/mcp/tools/agent/pod-proof.ts +5 -1
- package/src/mcp/tools/agent/proof.ts +110 -1
- package/src/mcp/tools/agent/shared.ts +130 -19
- package/src/mcp/tools/agent/start-integration.ts +10 -6
- package/src/mcp/tools/authenticate/authenticate.ts +276 -131
- package/src/mcp/tools/authenticate/build.ts +6 -3
- package/src/mcp/tools/authenticate/builder-access.ts +5 -2
- package/src/mcp/tools/authenticate/eth-key.ts +41 -7
- package/src/mcp/tools/authenticate/index.ts +1 -1
- package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
- package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
- package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
- package/src/mcp/tools/authenticate/session-cache.ts +125 -5
- package/src/mcp/tools/mode/mode.ts +6 -6
- package/src/old/login-cdp.ts +2 -3
- package/src/proof/attestor.ts +2 -3
- package/src/proof/check-attestor.ts +55 -12
- package/src/proof/check-matchers.ts +227 -0
- package/src/proof/zk-files.ts +357 -65
- package/src/provider/draft.ts +215 -7
- package/src/provider/matcher.ts +237 -4
|
@@ -15,6 +15,7 @@ import { analyzeTool } from "./analyze.js";
|
|
|
15
15
|
import { attachTool } from "./attach.js";
|
|
16
16
|
import { captureTools, resetCaptures } from "./capture.js";
|
|
17
17
|
import { checkAttestorTool } from "./check-attestor.js";
|
|
18
|
+
import { draftTools } from "./draft-tools.js";
|
|
18
19
|
import { listTabsTool } from "./eval.js";
|
|
19
20
|
import { howItWorksTool } from "./how-it-works.js";
|
|
20
21
|
import { liveViewTools } from "./live-view.js";
|
|
@@ -73,6 +74,11 @@ export function buildAgentTools(client, builderClient = client, externalState) {
|
|
|
73
74
|
for (const t of sharedAuthoringTools(state.captures)) {
|
|
74
75
|
tools.push(t);
|
|
75
76
|
}
|
|
77
|
+
// check_matchers / update_draft: offline matcher check + in-place draft
|
|
78
|
+
// edits, keyed by captureId like run_proof_via_pod.
|
|
79
|
+
for (const t of draftTools(state.captures)) {
|
|
80
|
+
tools.push(t);
|
|
81
|
+
}
|
|
76
82
|
if (client) {
|
|
77
83
|
tools.push(startIntegrationTool());
|
|
78
84
|
tools.push(createVersionFromCaptureTool(client, state.attach, state.captures));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { makeDraftStore, } from "../../../authoring/index.js";
|
|
2
|
+
import { DEFAULT_MAX_CHARS, TEXT_WINDOW_PROPERTIES, textWindow, } from "../../../authoring/util.js";
|
|
2
3
|
import { startCapture } from "../../../cdp/capture.js";
|
|
3
4
|
import { defineTool } from "../../server.js";
|
|
4
5
|
export async function resetCaptures(captures) {
|
|
@@ -57,21 +58,27 @@ export function captureTools(attachRef, captures) {
|
|
|
57
58
|
});
|
|
58
59
|
const getTool = defineTool({
|
|
59
60
|
name: 'get_request',
|
|
60
|
-
description: 'Returns the
|
|
61
|
-
+ '
|
|
62
|
-
+
|
|
63
|
-
+ '
|
|
64
|
-
+ '
|
|
61
|
+
description: 'Returns the headers and bodies of one captured request, to inspect '
|
|
62
|
+
+ 'a candidate that `find_requests_containing` ranked. Each body is '
|
|
63
|
+
+ `cut to ${DEFAULT_MAX_CHARS} characters by default; a cut body `
|
|
64
|
+
+ 'comes with `requestBodyTruncation` or `responseBodyTruncation` '
|
|
65
|
+
+ '(total size and `nextOffset`). Page with `offset`, or pass '
|
|
66
|
+
+ '`maxChars` or `full`. Unlike the other tools, headers are raw: '
|
|
67
|
+
+ 'cookie and authorization values are visible. Read them for '
|
|
68
|
+
+ 'diagnosis; do not repeat them to the developer or write them to '
|
|
69
|
+
+ 'a file.',
|
|
65
70
|
inputSchema: {
|
|
66
71
|
type: 'object',
|
|
67
72
|
properties: {
|
|
68
73
|
captureId: { type: 'string' },
|
|
69
74
|
requestId: { type: 'string' },
|
|
75
|
+
...TEXT_WINDOW_PROPERTIES,
|
|
70
76
|
},
|
|
71
77
|
required: ['captureId', 'requestId'],
|
|
72
78
|
},
|
|
73
79
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
74
|
-
}, async (
|
|
80
|
+
}, async (args) => {
|
|
81
|
+
const { captureId, requestId } = args;
|
|
75
82
|
const entry = captures.sessions.get(captureId);
|
|
76
83
|
if (!entry) {
|
|
77
84
|
throw new Error(`no capture ${captureId}`);
|
|
@@ -80,11 +87,27 @@ export function captureTools(attachRef, captures) {
|
|
|
80
87
|
if (!r) {
|
|
81
88
|
throw new Error(`no request ${requestId} in capture ${captureId}`);
|
|
82
89
|
}
|
|
90
|
+
const { requestBody, responseBody, ...meta } = r;
|
|
83
91
|
const body = entry.session.bodies.get(requestId);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
const result = {
|
|
93
|
+
...meta,
|
|
94
|
+
...windowed('requestBody', requestBody, args),
|
|
95
|
+
...windowed('responseBody', body.available ? body.body : responseBody, args),
|
|
96
|
+
};
|
|
97
|
+
return body.available
|
|
98
|
+
? { ...result, bodyAvailable: true }
|
|
99
|
+
: { ...result, bodyAvailable: false, reason: body.reason };
|
|
88
100
|
});
|
|
89
101
|
return [startTool, stopTool, getTool];
|
|
90
102
|
}
|
|
103
|
+
/** `{ [key]: text window }`, plus `[key]Truncation` when the window cut it. */
|
|
104
|
+
function windowed(key, text, args) {
|
|
105
|
+
if (text === undefined) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
const { text: part, truncation } = textWindow(text, args);
|
|
109
|
+
return {
|
|
110
|
+
[key]: part,
|
|
111
|
+
...(truncation ? { [`${key}Truncation`]: truncation } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -11,13 +11,21 @@ export function checkAttestorTool(check = checkAttestor) {
|
|
|
11
11
|
+ 'seconds (about 8 when healthy). Call it once when you set up a '
|
|
12
12
|
+ 'new provider build, and after `run_proof` fails with a network '
|
|
13
13
|
+ 'error. Returns `{ ok, attestorUrl, durationMs, '
|
|
14
|
-
+ 'extractedParameters, error, errorKind }`.
|
|
15
|
-
+ '
|
|
16
|
-
+ '
|
|
17
|
-
+ '
|
|
18
|
-
+ '
|
|
19
|
-
+ '
|
|
20
|
-
+ '
|
|
14
|
+
+ 'extractedParameters, error, errorKind, proofFiles }`. '
|
|
15
|
+
+ '`proofFiles` is the proof file download state: `{ status }` is '
|
|
16
|
+
+ '`idle`, `ready`, `downloading` (with `method`, `elapsedMs`, and '
|
|
17
|
+
+ '`bytesDownloaded`/`totalBytes` when known), or `failed` (with '
|
|
18
|
+
+ '`reason`). If `ok` is true, the attestor is fine and a failing '
|
|
19
|
+
+ 'proof points to the target site. If `errorKind` is '
|
|
20
|
+
+ '`proof-files-downloading`, the one-time proof file download is '
|
|
21
|
+
+ 'still running and the attestor was not contacted: call this tool '
|
|
22
|
+
+ 'again shortly; do not report a network problem. If it is '
|
|
23
|
+
+ '`proof-files-failed`, the download failed: tell the developer '
|
|
24
|
+
+ '`error` (usually no access to github.com or '
|
|
25
|
+
+ 'raw.githubusercontent.com). If it is `attestor-unreachable`, the '
|
|
26
|
+
+ 'proof files are ready but this machine cannot reach the attestor '
|
|
27
|
+
+ '(network, VPN, firewall, or proxy): tell the developer. If it is '
|
|
28
|
+
+ '`setup-failed`, other local setup failed: report `error`. If it is '
|
|
21
29
|
+ '`attestor-error`, the attestor rejected the claim: report '
|
|
22
30
|
+ '`error`.',
|
|
23
31
|
inputSchema: {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `check_matchers` and `update_draft` — inspect and edit a draft's matchers
|
|
3
|
+
* without leaving MCP. `check_matchers` runs a recipe's redactions + matches
|
|
4
|
+
* against a captured response with attestor-core's own engine, offline (see
|
|
5
|
+
* proof/check-matchers.ts); `update_draft` replaces a draft's matchers in place
|
|
6
|
+
* after the same validation and portability gate `propose_provider` applies.
|
|
7
|
+
*/
|
|
8
|
+
import { badRequest } from '@hapi/boom';
|
|
9
|
+
import assert from 'node:assert';
|
|
10
|
+
import { checkMatchersOffline } from "../../../proof/check-matchers.js";
|
|
11
|
+
import { editDraftProvider } from "../../../provider/draft.js";
|
|
12
|
+
import { verificationWarnings } from "../../../provider/verification-checks.js";
|
|
13
|
+
import { defineTool } from "../../server.js";
|
|
14
|
+
const MATCHES_SCHEMA = {
|
|
15
|
+
type: 'array',
|
|
16
|
+
minItems: 1,
|
|
17
|
+
items: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
type: { type: 'string', enum: ['contains', 'regex'] },
|
|
21
|
+
value: { type: 'string' },
|
|
22
|
+
invert: { type: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
required: ['type', 'value'],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const REDACTIONS_SCHEMA = {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
xPath: { type: 'string' },
|
|
33
|
+
jsonPath: { type: 'string' },
|
|
34
|
+
regex: { type: 'string' },
|
|
35
|
+
hash: { type: 'string', enum: ['oprf', 'oprf-raw', 'oprf-mpc'] },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const PARAM_VALUES_SCHEMA = {
|
|
40
|
+
type: 'object',
|
|
41
|
+
additionalProperties: { type: 'string' },
|
|
42
|
+
};
|
|
43
|
+
export function draftTools(captures) {
|
|
44
|
+
const checkMatchers = defineTool({
|
|
45
|
+
name: 'check_matchers',
|
|
46
|
+
description: 'Checks a draft\'s `responseRedactions` and `responseMatches` '
|
|
47
|
+
+ 'against the captured response, offline, with the attestor\'s own '
|
|
48
|
+
+ 'redaction and match engine. No network call. Returns `ok`, '
|
|
49
|
+
+ '`failedAt` and `reason` on failure, `extractedParameters`, and the '
|
|
50
|
+
+ '`revealed` body slices the attestor would see (truncated). Pass '
|
|
51
|
+
+ '`responseMatches`, `responseRedactions`, or `paramValues` to try '
|
|
52
|
+
+ 'an edit without saving it, and `requestId` to check against a '
|
|
53
|
+
+ 'different captured request. Run it before `run_proof`.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
captureId: { type: 'string' },
|
|
58
|
+
draftId: { type: 'string' },
|
|
59
|
+
requestId: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Captured request whose response to check. '
|
|
62
|
+
+ 'Defaults to the request the draft was proposed from.',
|
|
63
|
+
},
|
|
64
|
+
responseMatches: MATCHES_SCHEMA,
|
|
65
|
+
responseRedactions: REDACTIONS_SCHEMA,
|
|
66
|
+
paramValues: PARAM_VALUES_SCHEMA,
|
|
67
|
+
},
|
|
68
|
+
required: ['captureId', 'draftId'],
|
|
69
|
+
},
|
|
70
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
71
|
+
}, async (args) => {
|
|
72
|
+
const { entry, draft } = resolveDraft(captures, args);
|
|
73
|
+
const provider = hasEdit(args)
|
|
74
|
+
? editDraftProvider(draft.provider, draft.extractions, args).provider
|
|
75
|
+
: draft.provider;
|
|
76
|
+
return checkCaptured(entry, args.requestId ?? draft.requestId, provider);
|
|
77
|
+
});
|
|
78
|
+
const updateDraft = defineTool({
|
|
79
|
+
name: 'update_draft',
|
|
80
|
+
description: 'Replaces a draft\'s `responseMatches`, `responseRedactions`, '
|
|
81
|
+
+ '`paramValues`, or `geoLocation` in place; the `draftId` stays '
|
|
82
|
+
+ 'the same. Validates '
|
|
83
|
+
+ 'regexes and selectors and rejects a matcher that bakes a captured '
|
|
84
|
+
+ 'value in as a literal, or redactions that reveal more of the '
|
|
85
|
+
+ 'captured response than the draft did. Returns the secret-free '
|
|
86
|
+
+ 'provider and, unless `check` is false, a `check_matchers` '
|
|
87
|
+
+ 'result for the edit. '
|
|
88
|
+
+ 'Prefer `propose_provider` with a better request or target; use '
|
|
89
|
+
+ 'this to narrow or fix what it drafted.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
captureId: { type: 'string' },
|
|
94
|
+
draftId: { type: 'string' },
|
|
95
|
+
responseMatches: MATCHES_SCHEMA,
|
|
96
|
+
responseRedactions: REDACTIONS_SCHEMA,
|
|
97
|
+
paramValues: PARAM_VALUES_SCHEMA,
|
|
98
|
+
geoLocation: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'ISO country the published version\'s request goes out from, '
|
|
101
|
+
+ 'for example `US`, or `{{DYNAMIC_GEO}}` (the default) for '
|
|
102
|
+
+ 'the verifying user\'s own country. Pin a country when '
|
|
103
|
+
+ 'run_proof only verified with that `geoLocation`.',
|
|
104
|
+
},
|
|
105
|
+
check: {
|
|
106
|
+
type: 'boolean',
|
|
107
|
+
description: 'Default true: run `check_matchers` on the edit.',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ['captureId', 'draftId'],
|
|
111
|
+
},
|
|
112
|
+
annotations: { openWorldHint: false, destructiveHint: false },
|
|
113
|
+
}, async (args) => {
|
|
114
|
+
const { entry, draft } = resolveDraft(captures, args);
|
|
115
|
+
assert(hasEdit(args), badRequest('Pass at least one of responseMatches, responseRedactions, '
|
|
116
|
+
+ 'paramValues, or geoLocation.'));
|
|
117
|
+
const edited = editDraftProvider(draft.provider, draft.extractions, args);
|
|
118
|
+
// Redactions are what the attestor doesn't see, so an edit may narrow
|
|
119
|
+
// them but never reveal more of the captured response than the draft.
|
|
120
|
+
const before = checkCaptured(entry, draft.requestId, draft.provider);
|
|
121
|
+
const check = checkCaptured(entry, draft.requestId, edited.provider);
|
|
122
|
+
assert(check.failedAt !== 'request' && check.failedAt !== 'redaction', badRequest('the edited recipe doesn\'t apply to the captured response '
|
|
123
|
+
+ `(${check.failedAt}): ${check.reason}`));
|
|
124
|
+
const comparable = before.failedAt !== 'request'
|
|
125
|
+
&& before.failedAt !== 'redaction';
|
|
126
|
+
assert(!comparable || check.revealedBytes <= before.revealedBytes, badRequest(`the edit reveals ${check.revealedBytes} bytes of the response `
|
|
127
|
+
+ `to the attestor, more than the draft's ${before.revealedBytes}. `
|
|
128
|
+
+ 'Keep redactions that hide everything except the proved values.'));
|
|
129
|
+
// The recipe changed, so an earlier replay no longer describes it.
|
|
130
|
+
const { lastReplay: _stale, ...rest } = draft;
|
|
131
|
+
void _stale;
|
|
132
|
+
entry.drafts.set(args.draftId, {
|
|
133
|
+
...rest,
|
|
134
|
+
provider: edited.provider,
|
|
135
|
+
extractions: edited.extractions,
|
|
136
|
+
});
|
|
137
|
+
const verification = verificationWarnings(edited.provider);
|
|
138
|
+
return {
|
|
139
|
+
draftId: args.draftId,
|
|
140
|
+
provider: edited.provider,
|
|
141
|
+
secretRefs: draft.secretRefs,
|
|
142
|
+
...(edited.warnings.length ? { warnings: edited.warnings } : {}),
|
|
143
|
+
...(verification.length
|
|
144
|
+
? { verificationWarnings: verification }
|
|
145
|
+
: {}),
|
|
146
|
+
...(args.check === false ? {} : { check }),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
return [checkMatchers, updateDraft];
|
|
150
|
+
}
|
|
151
|
+
function resolveDraft(captures, { captureId, draftId }) {
|
|
152
|
+
const entry = captures.sessions.get(captureId);
|
|
153
|
+
assert(entry, badRequest(`no capture ${captureId} — call start_capture first `
|
|
154
|
+
+ '(it has been reset or never started).'));
|
|
155
|
+
const draft = entry.drafts.get(draftId);
|
|
156
|
+
assert(draft, badRequest(`no draft ${draftId} in capture ${captureId}`));
|
|
157
|
+
return { entry, draft };
|
|
158
|
+
}
|
|
159
|
+
function hasEdit(edit) {
|
|
160
|
+
return edit.responseMatches !== undefined
|
|
161
|
+
|| edit.responseRedactions !== undefined
|
|
162
|
+
|| edit.paramValues !== undefined
|
|
163
|
+
|| edit.geoLocation !== undefined;
|
|
164
|
+
}
|
|
165
|
+
function checkCaptured(entry, requestId, provider) {
|
|
166
|
+
const captured = entry.session.requests.get(requestId);
|
|
167
|
+
assert(captured, badRequest(`no captured request ${requestId}`));
|
|
168
|
+
assert(captured.responseBody, badRequest(`response body unavailable for ${requestId}`));
|
|
169
|
+
return { requestId, ...checkMatchersOffline(provider, captured) };
|
|
170
|
+
}
|
|
@@ -27,7 +27,10 @@ export function podProofTools(attachRef, captures) {
|
|
|
27
27
|
+ 'instead of `run_proof` when the attach mode is "builder": the '
|
|
28
28
|
+ 'proof then reaches the target from the browser\'s own exit IP and '
|
|
29
29
|
+ 'country. Needs no `ownerAddress`; the pod signs with its own TEE '
|
|
30
|
-
+ 'key.',
|
|
30
|
+
+ 'key. It matches the browser\'s exit IP and country, not '
|
|
31
|
+
+ 'necessarily its TLS fingerprint, so it is not a way past bot '
|
|
32
|
+
+ 'protection: if `run_proof` reported '
|
|
33
|
+
+ '`blocked-non-browser-client`, expect the same here.',
|
|
31
34
|
inputSchema: {
|
|
32
35
|
type: 'object',
|
|
33
36
|
properties: {
|
|
@@ -54,6 +57,7 @@ export function podProofTools(attachRef, captures) {
|
|
|
54
57
|
return shapeRichProofResult(result, {
|
|
55
58
|
provider: draft.provider,
|
|
56
59
|
hasSecrets: Object.keys(draft.secrets).length > 0,
|
|
60
|
+
...(draft.lastReplay ? { lastReplay: draft.lastReplay } : {}),
|
|
57
61
|
});
|
|
58
62
|
});
|
|
59
63
|
return [tool];
|
|
@@ -11,7 +11,35 @@ export declare function shapeRichProofResult(result: ProofResult, opts: {
|
|
|
11
11
|
provider: unknown;
|
|
12
12
|
hasSecrets: boolean;
|
|
13
13
|
attestorUrl?: string;
|
|
14
|
+
/** The draft's latest authenticated in-browser replay, when tracked. */
|
|
15
|
+
lastReplay?: {
|
|
16
|
+
status: number;
|
|
17
|
+
ok: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** Where the attestor's request went out from. */
|
|
20
|
+
egress?: {
|
|
21
|
+
geoLocation?: string;
|
|
22
|
+
};
|
|
14
23
|
}): Record<string, unknown>;
|
|
24
|
+
export interface BlockedClientDiagnosis {
|
|
25
|
+
kind: 'blocked-non-browser-client';
|
|
26
|
+
/** HTTP status the attestor's request got, when the error carries one. */
|
|
27
|
+
status?: number;
|
|
28
|
+
/** Set when the same draft's in-browser replay succeeded. */
|
|
29
|
+
replayedInBrowser?: true;
|
|
30
|
+
likelyCause: string;
|
|
31
|
+
next: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Says when a failed proof looks like the site blocking non-browser clients:
|
|
35
|
+
* the attestor's request got 403/406/429/503, or a tiny or challenge-looking
|
|
36
|
+
* page where the browser got the real one. Returns undefined for verified
|
|
37
|
+
* proofs and other failures. The raw `error` stays on the result.
|
|
38
|
+
*/
|
|
39
|
+
export declare function diagnoseBlockedClient(result: Pick<ProofResult, 'verified' | 'error'>, lastReplay?: {
|
|
40
|
+
status: number;
|
|
41
|
+
ok: boolean;
|
|
42
|
+
}, geoLocation?: string): BlockedClientDiagnosis | undefined;
|
|
15
43
|
export interface ProofDiagnosis {
|
|
16
44
|
attestor: AttestorCheckResult;
|
|
17
45
|
likelyCause: string;
|
|
@@ -117,6 +117,10 @@ export function shapeRichProofResult(result, opts) {
|
|
|
117
117
|
if (result.errorKind !== undefined) {
|
|
118
118
|
out.errorKind = result.errorKind;
|
|
119
119
|
}
|
|
120
|
+
const blocked = diagnoseBlockedClient(result, opts.lastReplay, opts.egress?.geoLocation);
|
|
121
|
+
if (blocked !== undefined) {
|
|
122
|
+
out.diagnosis = blocked;
|
|
123
|
+
}
|
|
120
124
|
// Trim the LLM-visible logs — full stream goes to runLogPath.
|
|
121
125
|
if (result.attestorLogs !== undefined && result.attestorLogs.length > 0) {
|
|
122
126
|
const logs = result.verified
|
|
@@ -158,6 +162,74 @@ export function shapeRichProofResult(result, opts) {
|
|
|
158
162
|
}
|
|
159
163
|
return out;
|
|
160
164
|
}
|
|
165
|
+
/** HTTP statuses a site's bot protection answers non-browser clients with. */
|
|
166
|
+
const BLOCKING_STATUSES = new Set([403, 406, 429, 503]);
|
|
167
|
+
/** Markers of a bot-protection / challenge page rather than real content. */
|
|
168
|
+
const CHALLENGE_BODY = new RegExp([
|
|
169
|
+
'captcha', 'challenge', 'cf-chl', 'cf-ray', 'just a moment',
|
|
170
|
+
'access denied', 'request unsuccessful', '_incapsula_', 'px-captcha',
|
|
171
|
+
'perimeterx', 'datadome', 'akamai', 'bot detection', 'enable javascript',
|
|
172
|
+
'are you a robot', 'unusual traffic',
|
|
173
|
+
].join('|'), 'i');
|
|
174
|
+
/** Below this, a response the browser rendered with the value in it is
|
|
175
|
+
* likely a shell or challenge page served to a non-browser client. */
|
|
176
|
+
const TINY_BODY_BYTES = 4096;
|
|
177
|
+
/**
|
|
178
|
+
* Says when a failed proof looks like the site blocking non-browser clients:
|
|
179
|
+
* the attestor's request got 403/406/429/503, or a tiny or challenge-looking
|
|
180
|
+
* page where the browser got the real one. Returns undefined for verified
|
|
181
|
+
* proofs and other failures. The raw `error` stays on the result.
|
|
182
|
+
*/
|
|
183
|
+
export function diagnoseBlockedClient(result, lastReplay, geoLocation) {
|
|
184
|
+
if (result.verified || !result.error) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const replayedInBrowser = lastReplay?.ok === true;
|
|
188
|
+
const statusMatch = new RegExp('Expected status 2xx, got (\\d{3})|Provider returned error (\\d{3})').exec(result.error);
|
|
189
|
+
const status = Number(statusMatch?.[1] ?? statusMatch?.[2]) || undefined;
|
|
190
|
+
const blockedStatus = status !== undefined && BLOCKING_STATUSES.has(status);
|
|
191
|
+
if (!blockedStatus && !(replayedInBrowser && challengeBody(result.error))) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
kind: 'blocked-non-browser-client',
|
|
196
|
+
...(status !== undefined ? { status } : {}),
|
|
197
|
+
...(replayedInBrowser ? { replayedInBrowser: true } : {}),
|
|
198
|
+
likelyCause: (replayedInBrowser
|
|
199
|
+
? 'The same request succeeded from inside the browser, but '
|
|
200
|
+
: '')
|
|
201
|
+
+ 'the site answered the attestor'
|
|
202
|
+
+ (geoLocation
|
|
203
|
+
? `, whose request went out from ${geoLocation}, with `
|
|
204
|
+
: ', whose request went out from its own network, with ')
|
|
205
|
+
+ (blockedStatus ? `HTTP ${status}` : 'a challenge or shell page')
|
|
206
|
+
+ '. The site blocks traffic from that country, or non-browser '
|
|
207
|
+
+ 'clients (bot protection or TLS fingerprinting). Copying headers or '
|
|
208
|
+
+ 'cookies will not help.',
|
|
209
|
+
next: 'If the site serves one country (for example, a US airline), retry '
|
|
210
|
+
+ 'run_proof once with `geoLocation` set to that country. If that '
|
|
211
|
+
+ 'verifies, pin it with update_draft({ geoLocation }) so the '
|
|
212
|
+
+ 'published version goes out from there too. Otherwise, try a '
|
|
213
|
+
+ 'different endpoint that returns the value, such as a JSON API the '
|
|
214
|
+
+ 'page calls (find_requests_containing), then propose and prove '
|
|
215
|
+
+ 'that. If none works, stop and report to the developer that this '
|
|
216
|
+
+ 'site blocks the attestor. Do not tweak headers.',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** True when an attestor error embeds a response element (base64, from a
|
|
220
|
+
* regex redaction that didn't match) that is tiny or a challenge page. */
|
|
221
|
+
function challengeBody(error) {
|
|
222
|
+
const b64 = /does not match found element '([A-Za-z0-9+/=]*)/.exec(error)?.[1];
|
|
223
|
+
if (b64 === undefined) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
// "Tiny" only means something for a whole document; an xPath-selected
|
|
227
|
+
// element is small by design.
|
|
228
|
+
const text = Buffer.from(b64, 'base64').toString('utf8');
|
|
229
|
+
const tinyDocument = Buffer.byteLength(text) < TINY_BODY_BYTES
|
|
230
|
+
&& /<!doctype|<html/i.test(text);
|
|
231
|
+
return tinyDocument || CHALLENGE_BODY.test(text);
|
|
232
|
+
}
|
|
161
233
|
/**
|
|
162
234
|
* When an unverified proof failed with a network-like error, runs the
|
|
163
235
|
* attestor check once and says whether the attestor or the target site is the
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { runAttestorProof } from '../../../proof/attestor.ts';
|
|
1
2
|
import { checkAttestor } from '../../../proof/check-attestor.ts';
|
|
2
3
|
import type { RegisteredTool } from '../../server.ts';
|
|
3
4
|
import type { CaptureStore } from './capture.ts';
|
|
5
|
+
export interface ProofEgress {
|
|
6
|
+
/** ISO country (or param template) the attestor's request goes out from;
|
|
7
|
+
* absent when it goes out from the attestor's own network. */
|
|
8
|
+
geoLocation?: string;
|
|
9
|
+
source: 'argument' | 'draft' | 'developer-ip' | 'none';
|
|
10
|
+
}
|
|
4
11
|
/** The shared authoring tools mapped to the MCP's shape: resolve each call's
|
|
5
12
|
* local backend from `args.captureId`, and widen the input schema — every tool
|
|
6
13
|
* takes `captureId`; `run_proof` also gets the credential-aware contract. The
|
|
7
14
|
* tools are authored plain in `../../../authoring`; this only wraps them with
|
|
8
15
|
* `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
|
|
9
|
-
export declare function sharedAuthoringTools(captures: CaptureStore, check?: typeof checkAttestor): RegisteredTool[];
|
|
16
|
+
export declare function sharedAuthoringTools(captures: CaptureStore, check?: typeof checkAttestor, runProof?: typeof runAttestorProof, lookupCountry?: () => Promise<string | undefined>): RegisteredTool[];
|