@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import './patch-domhandler.ts';
|
|
2
|
+
import type { CapturedRequest, ReclaimProvider } from '../provider/schema.ts';
|
|
3
|
+
export interface RevealedSlice {
|
|
4
|
+
/** Byte offsets in the response body. */
|
|
5
|
+
from: number;
|
|
6
|
+
to: number;
|
|
7
|
+
/** Revealed text, truncated to {@link MAX_SLICE_CHARS}. */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Set when the slice is an OPRF-hashed value (plaintext here). */
|
|
10
|
+
hashed?: true;
|
|
11
|
+
}
|
|
12
|
+
export interface MatcherCheckResult {
|
|
13
|
+
/** True when the attestor would accept this response. */
|
|
14
|
+
ok: boolean;
|
|
15
|
+
/** Where it failed: building the request, selecting what to reveal, or
|
|
16
|
+
* checking `responseMatches` on the revealed bytes. */
|
|
17
|
+
failedAt?: 'request' | 'redaction' | 'match';
|
|
18
|
+
/** Attestor-core's error, truncated. */
|
|
19
|
+
reason?: string;
|
|
20
|
+
/** Params the attestor would extract (regex groups + `paramValues` the
|
|
21
|
+
* matches use). */
|
|
22
|
+
extractedParameters: Record<string, string>;
|
|
23
|
+
/** The response body slices revealed to the attestor. */
|
|
24
|
+
revealed: RevealedSlice[];
|
|
25
|
+
revealedSliceCount: number;
|
|
26
|
+
revealedBytes: number;
|
|
27
|
+
bodyBytes: number;
|
|
28
|
+
/** Set when an OPRF-hashed redaction was checked in plaintext. */
|
|
29
|
+
note?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check `provider`'s matchers against `captured`'s response body, offline.
|
|
33
|
+
* Uses the captured status and content type, so a non-2xx capture fails the
|
|
34
|
+
* same way it would at proof time.
|
|
35
|
+
*/
|
|
36
|
+
export declare function checkMatchersOffline(provider: ReclaimProvider, captured: Pick<CapturedRequest, 'status' | 'contentType' | 'responseBody'>): MatcherCheckResult;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline matcher check: run a recipe's `responseRedactions` +
|
|
3
|
+
* `responseMatches` against a captured response with attestor-core's own HTTP
|
|
4
|
+
* provider, without a network call or a TLS session.
|
|
5
|
+
*
|
|
6
|
+
* It replays the two halves of a real proof on a synthetic transcript:
|
|
7
|
+
* 1. prover side: `providers.http.createRequest` builds the request and
|
|
8
|
+
* `getResponseRedactions` picks the response slices to reveal (xPath /
|
|
9
|
+
* jsonPath / regex, exactly as at proof time);
|
|
10
|
+
* 2. attestor side: `providers.http.assertValidProviderReceipt` checks the
|
|
11
|
+
* redacted transcript against `responseMatches` and extracts params.
|
|
12
|
+
*
|
|
13
|
+
* Hashed (OPRF) slices stay in plaintext here: OPRF needs the attestor, so the
|
|
14
|
+
* check treats the value as its own nullifier. Everything else is the same
|
|
15
|
+
* engine the attestor runs.
|
|
16
|
+
*/
|
|
17
|
+
import { CURRENT_ATTESTOR_VERSION, PROVIDER_CTX, providers, redactSlices, } from '@reclaimprotocol/attestor-core';
|
|
18
|
+
// Side-effect: make external domhandler nodes DOM-like so attestor-core's
|
|
19
|
+
// bundled `xpath` accepts them (see patch-domhandler.ts).
|
|
20
|
+
import "./patch-domhandler.js";
|
|
21
|
+
import { LOGGER } from "../logger.js";
|
|
22
|
+
import { buildHttpParams } from "./proof-core.js";
|
|
23
|
+
/** Longest revealed slice returned; the full slice is still checked. */
|
|
24
|
+
const MAX_SLICE_CHARS = 400;
|
|
25
|
+
/** Most revealed slices returned. */
|
|
26
|
+
const MAX_SLICES = 10;
|
|
27
|
+
/** Longest error message returned (attestor errors can embed the base64 of
|
|
28
|
+
* a whole element). */
|
|
29
|
+
const MAX_REASON_CHARS = 600;
|
|
30
|
+
/** Placeholder secret header: the request builder needs one secret, and the
|
|
31
|
+
* check never sends the request. */
|
|
32
|
+
const OFFLINE_SECRET = { 'X-Reclaim-Offline-Check': '1' };
|
|
33
|
+
/**
|
|
34
|
+
* Check `provider`'s matchers against `captured`'s response body, offline.
|
|
35
|
+
* Uses the captured status and content type, so a non-2xx capture fails the
|
|
36
|
+
* same way it would at proof time.
|
|
37
|
+
*/
|
|
38
|
+
export function checkMatchersOffline(provider, captured) {
|
|
39
|
+
const http = providers.http;
|
|
40
|
+
const params = buildHttpParams(provider);
|
|
41
|
+
const logger = silentLogger();
|
|
42
|
+
const body = Buffer.from(captured.responseBody ?? '', 'utf8');
|
|
43
|
+
const head = Buffer.from(`HTTP/1.1 ${captured.status} ${captured.status < 300 ? 'OK' : 'ERR'}\r\n`
|
|
44
|
+
+ `Content-Type: ${captured.contentType || 'text/plain'}\r\n`
|
|
45
|
+
+ `Content-Length: ${body.length}\r\n\r\n`);
|
|
46
|
+
const response = new Uint8Array(Buffer.concat([head, body]));
|
|
47
|
+
const empty = {
|
|
48
|
+
extractedParameters: {},
|
|
49
|
+
revealed: [],
|
|
50
|
+
revealedSliceCount: 0,
|
|
51
|
+
revealedBytes: 0,
|
|
52
|
+
bodyBytes: body.length,
|
|
53
|
+
};
|
|
54
|
+
let request;
|
|
55
|
+
try {
|
|
56
|
+
const req = http.createRequest({ headers: OFFLINE_SECRET }, structuredClone(params), logger);
|
|
57
|
+
const data = typeof req.data === 'string'
|
|
58
|
+
? new Uint8Array(Buffer.from(req.data))
|
|
59
|
+
: req.data;
|
|
60
|
+
request = redactSlices(data, req.redactions);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return { ...empty, ok: false, failedAt: 'request', reason: reasonOf(err) };
|
|
64
|
+
}
|
|
65
|
+
let slices;
|
|
66
|
+
try {
|
|
67
|
+
slices = http.getResponseRedactions?.({
|
|
68
|
+
response, params: structuredClone(params), logger, ctx: PROVIDER_CTX,
|
|
69
|
+
}) ?? [];
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return { ...empty, ok: false, failedAt: 'redaction', reason: reasonOf(err) };
|
|
73
|
+
}
|
|
74
|
+
const hidden = slices.filter((s) => !s.hash);
|
|
75
|
+
const hashed = slices.filter((s) => s.hash);
|
|
76
|
+
const view = revealedSlices(head.length, response.length, hidden, hashed);
|
|
77
|
+
const redacted = redactSlices(response, hidden);
|
|
78
|
+
const seen = {
|
|
79
|
+
revealed: view.slice(0, MAX_SLICES).map((s) => ({
|
|
80
|
+
from: s.from,
|
|
81
|
+
to: s.to,
|
|
82
|
+
text: truncate(Buffer.from(response.subarray(head.length + s.from, head.length + s.to))
|
|
83
|
+
.toString('utf8'), MAX_SLICE_CHARS),
|
|
84
|
+
...(s.hashed ? { hashed: true } : {}),
|
|
85
|
+
})),
|
|
86
|
+
revealedSliceCount: view.length,
|
|
87
|
+
revealedBytes: view.reduce((n, s) => n + s.to - s.from, 0),
|
|
88
|
+
bodyBytes: body.length,
|
|
89
|
+
...(hashed.length
|
|
90
|
+
? {
|
|
91
|
+
note: 'OPRF-hashed values were checked in plaintext; at proof time '
|
|
92
|
+
+ 'the attestor sees their nullifier instead.',
|
|
93
|
+
}
|
|
94
|
+
: {}),
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
const out = http.assertValidProviderReceipt({
|
|
98
|
+
clientVersion: CURRENT_ATTESTOR_VERSION,
|
|
99
|
+
receipt: [
|
|
100
|
+
{ sender: 'client', message: request },
|
|
101
|
+
{ sender: 'server', message: redacted },
|
|
102
|
+
],
|
|
103
|
+
params: structuredClone(params),
|
|
104
|
+
logger,
|
|
105
|
+
ctx: PROVIDER_CTX,
|
|
106
|
+
});
|
|
107
|
+
// The http provider validates synchronously; a thenable here would mean
|
|
108
|
+
// a provider we don't run offline.
|
|
109
|
+
const extracted = out instanceof Promise ? undefined : out;
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
extractedParameters: extracted?.extractedParameters ?? {},
|
|
113
|
+
...seen,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
failedAt: 'match',
|
|
120
|
+
reason: reasonOf(err),
|
|
121
|
+
extractedParameters: {},
|
|
122
|
+
...seen,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Body-relative revealed ranges: the complement of `hidden` within the body,
|
|
127
|
+
* merged with the hashed slices (revealed as their nullifier at proof time).
|
|
128
|
+
*/
|
|
129
|
+
function revealedSlices(bodyStart, total, hidden, hashed) {
|
|
130
|
+
const out = [];
|
|
131
|
+
let cursor = bodyStart;
|
|
132
|
+
const sorted = [...hidden].sort((a, b) => a.fromIndex - b.fromIndex);
|
|
133
|
+
for (const s of sorted) {
|
|
134
|
+
if (s.fromIndex > cursor) {
|
|
135
|
+
out.push({ from: cursor - bodyStart, to: s.fromIndex - bodyStart });
|
|
136
|
+
}
|
|
137
|
+
cursor = Math.max(cursor, s.toIndex);
|
|
138
|
+
}
|
|
139
|
+
if (cursor < total) {
|
|
140
|
+
out.push({ from: cursor - bodyStart, to: total - bodyStart });
|
|
141
|
+
}
|
|
142
|
+
for (const h of hashed) {
|
|
143
|
+
out.push({
|
|
144
|
+
from: h.fromIndex - bodyStart, to: h.toIndex - bodyStart, hashed: true,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return out
|
|
148
|
+
.filter((s) => s.to > s.from && s.from >= 0)
|
|
149
|
+
.sort((a, b) => a.from - b.from);
|
|
150
|
+
}
|
|
151
|
+
function reasonOf(err) {
|
|
152
|
+
return truncate(err instanceof Error ? err.message : String(err), MAX_REASON_CHARS);
|
|
153
|
+
}
|
|
154
|
+
function truncate(s, max) {
|
|
155
|
+
return s.length > max ? `${s.slice(0, max)}… (${s.length} chars)` : s;
|
|
156
|
+
}
|
|
157
|
+
/** A logger for attestor-core calls that records nothing: the check reports
|
|
158
|
+
* its outcome in the result, and the provider logs whole responses. */
|
|
159
|
+
function silentLogger() {
|
|
160
|
+
return LOGGER.child({ scope: 'check-matchers' }, { level: 'silent' });
|
|
161
|
+
}
|
package/lib/proof/zk-files.d.ts
CHANGED
|
@@ -1,14 +1,53 @@
|
|
|
1
|
+
import { type StdioOptions } from 'node:child_process';
|
|
2
|
+
export type ZkFilesState = {
|
|
3
|
+
status: 'idle';
|
|
4
|
+
} | {
|
|
5
|
+
status: 'downloading';
|
|
6
|
+
/** `stwo-files`: the ~1.5MB stwo files. `full-archive`: the ~350MB
|
|
7
|
+
* repository archive (fallback). */
|
|
8
|
+
method: 'stwo-files' | 'full-archive';
|
|
9
|
+
elapsedMs: number;
|
|
10
|
+
bytesDownloaded?: number;
|
|
11
|
+
totalBytes?: number;
|
|
12
|
+
} | {
|
|
13
|
+
status: 'ready';
|
|
14
|
+
} | {
|
|
15
|
+
status: 'failed';
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
/** Minimal child process surface `download-files.js` is driven through. */
|
|
19
|
+
export interface ChildLike {
|
|
20
|
+
once(event: 'exit', listener: (code: number | null) => void): unknown;
|
|
21
|
+
once(event: 'error', listener: (err: Error) => void): unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface ZkFilesDeps {
|
|
24
|
+
/** The zk-symmetric-crypto package root, or undefined when not installed. */
|
|
25
|
+
resolvePackageDir?: () => string | undefined;
|
|
26
|
+
/** The pinned repository commit the package's resources come from. */
|
|
27
|
+
readCommit?: (pkgDir: string) => Promise<string>;
|
|
28
|
+
fetch?: typeof fetch;
|
|
29
|
+
spawn?: (command: string, args: string[], options: {
|
|
30
|
+
cwd: string;
|
|
31
|
+
stdio: StdioOptions;
|
|
32
|
+
}) => ChildLike;
|
|
33
|
+
/** Progress lines. Defaults to stderr: stdout carries MCP JSON-RPC. */
|
|
34
|
+
log?: (message: string) => void;
|
|
35
|
+
}
|
|
1
36
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
37
|
+
* Build a downloader with its own state. The module exports one default
|
|
38
|
+
* instance; tests build their own with injected `spawn` and `fetch`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function makeZkFiles(deps?: ZkFilesDeps): {
|
|
41
|
+
ensure: () => Promise<void>;
|
|
42
|
+
start: () => void;
|
|
43
|
+
state: () => ZkFilesState;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Ensure the zk circuit resources are on disk before a proof runs. Resolves
|
|
47
|
+
* at once when they're present or the package isn't installed.
|
|
13
48
|
*/
|
|
14
49
|
export declare function ensureZkFiles(): Promise<void>;
|
|
50
|
+
/** Start the download in the background (MCP server launch). */
|
|
51
|
+
export declare function startZkFilesDownload(): void;
|
|
52
|
+
/** Where the download stands: idle, downloading, ready, or failed. */
|
|
53
|
+
export declare function zkFilesState(): ZkFilesState;
|
package/lib/proof/zk-files.js
CHANGED
|
@@ -1,74 +1,270 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// Download of the @reclaimprotocol/zk-symmetric-crypto circuit resources the
|
|
2
|
+
// proof engines read from disk. Never a `postinstall`: that blocked the MCP
|
|
3
|
+
// server's first launch past the connect timeout. Instead the MCP server starts
|
|
4
|
+
// the download in the background at launch (`startZkFilesDownload`), and every
|
|
5
|
+
// proof awaits it (`ensureZkFiles`) as the safety net.
|
|
6
|
+
//
|
|
7
|
+
// Proofs default to the stwo engine. Its resources are ~1.5MB, so we fetch only
|
|
8
|
+
// `resources/stwo` file by file from the package's pinned commit and check each
|
|
9
|
+
// file's git blob hash against the listing for that commit. When that fails
|
|
10
|
+
// (GitHub API rate limit, a changed layout), we fall back to the package's own
|
|
11
|
+
// downloader, which fetches the whole repository archive (~350MB).
|
|
12
|
+
// (In zk-symmetric-crypto 5.1.x the stwo prover loads its WASM from the npm
|
|
13
|
+
// package itself; `resources/stwo` is kept as the readiness signal.)
|
|
14
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
15
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
16
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
17
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return path;
|
|
21
|
+
};
|
|
22
|
+
import { badGateway, internal } from '@hapi/boom';
|
|
23
|
+
import assert from 'node:assert';
|
|
24
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
import { existsSync, statSync } from 'node:fs';
|
|
27
|
+
import { mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises';
|
|
9
28
|
import { createRequire } from 'node:module';
|
|
10
29
|
import { dirname, join } from 'node:path';
|
|
11
|
-
|
|
12
|
-
|
|
30
|
+
import { pathToFileURL } from 'node:url';
|
|
31
|
+
const REPO = 'reclaimprotocol/zk-symmetric-crypto';
|
|
32
|
+
/** The stwo resource dir; its presence means the circuits are ready. */
|
|
33
|
+
const STWO_DIR = join('resources', 'stwo');
|
|
34
|
+
/** Where the package's downloader writes the archive, relative to its cwd. */
|
|
35
|
+
const ARCHIVE_PATH = join('zk-symmetric-crypto-download', 'repo.zip');
|
|
13
36
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* failed attempt clears the latch so a later proof can retry.
|
|
17
|
-
*
|
|
18
|
-
* Resolves silently when the resources are already present (the warm npx cache,
|
|
19
|
-
* or after a manual `npm run download:zk-files`) and when
|
|
20
|
-
* `@reclaimprotocol/zk-symmetric-crypto` isn't installed at all (SDK-only
|
|
21
|
-
* consumers have nothing to prove).
|
|
22
|
-
*
|
|
23
|
-
* All output goes to stderr (fd 2): the MCP server speaks JSON-RPC over stdout,
|
|
24
|
-
* so a stray write to fd 1 mid-session would corrupt the protocol.
|
|
37
|
+
* Build a downloader with its own state. The module exports one default
|
|
38
|
+
* instance; tests build their own with injected `spawn` and `fetch`.
|
|
25
39
|
*/
|
|
26
|
-
export function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
export function makeZkFiles(deps = {}) {
|
|
41
|
+
const resolvePackageDir = deps.resolvePackageDir ?? resolveZkPackageDir;
|
|
42
|
+
const readCommit = deps.readCommit ?? readPinnedCommit;
|
|
43
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
44
|
+
const spawn = deps.spawn ?? nodeSpawn;
|
|
45
|
+
const log = deps.log ?? ((message) => {
|
|
46
|
+
process.stderr.write(`[reclaim-agent] ${message}\n`);
|
|
47
|
+
});
|
|
48
|
+
let inFlight;
|
|
49
|
+
let failure;
|
|
50
|
+
let ready = false;
|
|
51
|
+
let progress;
|
|
52
|
+
return { ensure, start, state };
|
|
53
|
+
/**
|
|
54
|
+
* Ensure the circuit resources are on disk. Idempotent and
|
|
55
|
+
* concurrency-safe: one download runs at a time per process, and a failed
|
|
56
|
+
* attempt clears the latch so a later call retries.
|
|
57
|
+
*/
|
|
58
|
+
function ensure() {
|
|
59
|
+
inFlight ?? (inFlight = download()
|
|
60
|
+
.then(() => {
|
|
61
|
+
ready = true;
|
|
62
|
+
failure = undefined;
|
|
63
|
+
}, (err) => {
|
|
64
|
+
inFlight = undefined;
|
|
65
|
+
failure = messageOf(err);
|
|
66
|
+
throw err;
|
|
67
|
+
})
|
|
68
|
+
.finally(() => {
|
|
69
|
+
progress = undefined;
|
|
70
|
+
}));
|
|
71
|
+
return inFlight;
|
|
40
72
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// the circuits are ready (the downloader fetches all engines together).
|
|
45
|
-
const libDir = dirname(mainPath);
|
|
46
|
-
const pkgDir = dirname(libDir);
|
|
47
|
-
if (existsSync(join(pkgDir, 'resources', 'stwo'))) {
|
|
48
|
-
return;
|
|
73
|
+
/** Start `ensure` in the background; `state` reports the outcome. */
|
|
74
|
+
function start() {
|
|
75
|
+
ensure().catch(() => { });
|
|
49
76
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
function state() {
|
|
78
|
+
if (ready) {
|
|
79
|
+
return { status: 'ready' };
|
|
80
|
+
}
|
|
81
|
+
if (progress) {
|
|
82
|
+
const bytesDownloaded = progress.archivePath
|
|
83
|
+
? fileSize(progress.archivePath)
|
|
84
|
+
: progress.bytesDownloaded;
|
|
85
|
+
return {
|
|
86
|
+
status: 'downloading',
|
|
87
|
+
method: progress.method,
|
|
88
|
+
elapsedMs: Date.now() - progress.startedAt,
|
|
89
|
+
...(bytesDownloaded !== undefined ? { bytesDownloaded } : {}),
|
|
90
|
+
...(progress.totalBytes !== undefined
|
|
91
|
+
? { totalBytes: progress.totalBytes }
|
|
92
|
+
: {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (failure !== undefined) {
|
|
96
|
+
return { status: 'failed', reason: failure };
|
|
97
|
+
}
|
|
98
|
+
return { status: 'idle' };
|
|
55
99
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
100
|
+
async function download() {
|
|
101
|
+
const pkgDir = resolvePackageDir();
|
|
102
|
+
// SDK-only consumers don't install the package: nothing to prove.
|
|
103
|
+
if (!pkgDir || existsSync(join(pkgDir, STWO_DIR))) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
progress = { method: 'stwo-files', startedAt: Date.now() };
|
|
108
|
+
await fetchStwoFiles(pkgDir, progress);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const stwoError = messageOf(err);
|
|
112
|
+
log(`fetching the stwo proof files failed (${stwoError}); `
|
|
113
|
+
+ 'downloading the full circuit archive (~350MB, one-time)…');
|
|
114
|
+
progress = {
|
|
115
|
+
method: 'full-archive',
|
|
116
|
+
startedAt: Date.now(),
|
|
117
|
+
archivePath: join(pkgDir, ARCHIVE_PATH),
|
|
118
|
+
};
|
|
119
|
+
try {
|
|
120
|
+
await runArchiveDownloader(pkgDir);
|
|
121
|
+
}
|
|
122
|
+
catch (archiveErr) {
|
|
123
|
+
throw badGateway(`stwo files: ${stwoError}; full archive: ${messageOf(archiveErr)}`);
|
|
68
124
|
}
|
|
69
|
-
|
|
70
|
-
|
|
125
|
+
}
|
|
126
|
+
assert(existsSync(join(pkgDir, STWO_DIR)), internal(`zk circuit download finished but ${join(pkgDir, STWO_DIR)} `
|
|
127
|
+
+ 'is missing; run `npm run download:zk-files` and retry.'));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Fetch `resources/stwo` at the pinned commit, file by file, into a temp
|
|
131
|
+
* dir inside the package, then rename it into place (same filesystem).
|
|
132
|
+
*/
|
|
133
|
+
async function fetchStwoFiles(pkgDir, stats) {
|
|
134
|
+
const commit = await readCommit(pkgDir);
|
|
135
|
+
assert(/^[0-9a-f]{40}$/.test(commit), internal('invalid pinned commit'));
|
|
136
|
+
const files = await listStwoFiles(commit);
|
|
137
|
+
stats.bytesDownloaded = 0;
|
|
138
|
+
stats.totalBytes = files.reduce((sum, f) => sum + f.size, 0);
|
|
139
|
+
log(`downloading zkTLS stwo proof files (${stats.totalBytes} bytes, `
|
|
140
|
+
+ 'one-time)…');
|
|
141
|
+
const tmp = await mkdtemp(join(pkgDir, '.stwo-download-'));
|
|
142
|
+
try {
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
const res = await fetchFn(`https://raw.githubusercontent.com/${REPO}/${commit}/`
|
|
145
|
+
+ `resources/stwo/${file.name}`);
|
|
146
|
+
assert(res.ok, badGateway(`stwo file ${file.name}: HTTP ${res.status}`));
|
|
147
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
148
|
+
assert(gitBlobSha(bytes) === file.sha, badGateway(`stwo file ${file.name} failed its integrity check`));
|
|
149
|
+
await writeFile(join(tmp, file.name), bytes);
|
|
150
|
+
stats.bytesDownloaded = (stats.bytesDownloaded ?? 0) + bytes.length;
|
|
71
151
|
}
|
|
152
|
+
const dest = join(pkgDir, STWO_DIR);
|
|
153
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
154
|
+
// Another process may have finished first; its copy is as good.
|
|
155
|
+
await rename(tmp, dest).catch((err) => {
|
|
156
|
+
if (!existsSync(dest)) {
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await rm(tmp, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** The file listing of `resources/stwo` at `commit`, with blob hashes. */
|
|
166
|
+
async function listStwoFiles(commit) {
|
|
167
|
+
const res = await fetchFn(`https://api.github.com/repos/${REPO}/contents/resources/stwo`
|
|
168
|
+
+ `?ref=${commit}`, { headers: { Accept: 'application/vnd.github+json' } });
|
|
169
|
+
assert(res.ok, badGateway(`stwo file listing: HTTP ${res.status}`));
|
|
170
|
+
const listing = await res.json();
|
|
171
|
+
assert(Array.isArray(listing) && listing.length > 0, badGateway('stwo file listing is empty'));
|
|
172
|
+
return listing.map((entry) => {
|
|
173
|
+
assert(typeof entry === 'object' && entry !== null
|
|
174
|
+
&& 'type' in entry && entry.type === 'file'
|
|
175
|
+
&& 'name' in entry && typeof entry.name === 'string'
|
|
176
|
+
&& /^[\w.-]+$/.test(entry.name) && !entry.name.startsWith('.')
|
|
177
|
+
&& 'size' in entry && typeof entry.size === 'number'
|
|
178
|
+
&& 'sha' in entry && typeof entry.sha === 'string', badGateway('unexpected entry in the stwo file listing'));
|
|
179
|
+
return { name: entry.name, size: entry.size, sha: entry.sha };
|
|
72
180
|
});
|
|
73
|
-
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Run the package's `download-files.js`. Its temp paths are relative, so
|
|
184
|
+
* cwd is the package dir: the archive and extracted tree then sit on the
|
|
185
|
+
* same filesystem as the final `rename()` (no EXDEV) and never land in the
|
|
186
|
+
* developer's project.
|
|
187
|
+
*/
|
|
188
|
+
async function runArchiveDownloader(pkgDir) {
|
|
189
|
+
const script = join(pkgDir, 'lib', 'scripts', 'download-files.js');
|
|
190
|
+
assert(existsSync(script), internal('zk circuit downloader not found in '
|
|
191
|
+
+ '@reclaimprotocol/zk-symmetric-crypto; '
|
|
192
|
+
+ 'run `npm run download:zk-files` and retry.'));
|
|
193
|
+
await new Promise((resolve, reject) => {
|
|
194
|
+
// stdio fd 1→2: the downloader's progress must never reach the MCP
|
|
195
|
+
// server's JSON-RPC stdout.
|
|
196
|
+
const child = spawn(process.execPath, [script], {
|
|
197
|
+
cwd: pkgDir,
|
|
198
|
+
stdio: ['ignore', 2, 2],
|
|
199
|
+
});
|
|
200
|
+
child.once('error', reject);
|
|
201
|
+
child.once('exit', (code) => {
|
|
202
|
+
if (code === 0) {
|
|
203
|
+
resolve();
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
reject(internal(`zk circuit download exited with code ${code}`));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const DEFAULT = makeZkFiles();
|
|
213
|
+
/**
|
|
214
|
+
* Ensure the zk circuit resources are on disk before a proof runs. Resolves
|
|
215
|
+
* at once when they're present or the package isn't installed.
|
|
216
|
+
*/
|
|
217
|
+
export function ensureZkFiles() {
|
|
218
|
+
return DEFAULT.ensure();
|
|
219
|
+
}
|
|
220
|
+
/** Start the download in the background (MCP server launch). */
|
|
221
|
+
export function startZkFilesDownload() {
|
|
222
|
+
DEFAULT.start();
|
|
223
|
+
}
|
|
224
|
+
/** Where the download stands: idle, downloading, ready, or failed. */
|
|
225
|
+
export function zkFilesState() {
|
|
226
|
+
return DEFAULT.state();
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The zk-symmetric-crypto copy attestor-core loads (it may be nested under
|
|
230
|
+
* attestor-core), else the hoisted one.
|
|
231
|
+
*/
|
|
232
|
+
function resolveZkPackageDir() {
|
|
233
|
+
const require = createRequire(import.meta.url);
|
|
234
|
+
for (const from of [
|
|
235
|
+
() => createRequire(require.resolve('@reclaimprotocol/attestor-core')),
|
|
236
|
+
() => require,
|
|
237
|
+
]) {
|
|
238
|
+
try {
|
|
239
|
+
// <pkg>/lib/index.js → <pkg>
|
|
240
|
+
return dirname(dirname(from().resolve('@reclaimprotocol/zk-symmetric-crypto')));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// try the next resolution root
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
async function readPinnedCommit(pkgDir) {
|
|
249
|
+
const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(join(pkgDir, 'lib', 'index.js')).href));
|
|
250
|
+
assert(typeof mod.GIT_COMMIT_HASH === 'string', internal('zk-symmetric-crypto exports no GIT_COMMIT_HASH'));
|
|
251
|
+
return mod.GIT_COMMIT_HASH;
|
|
252
|
+
}
|
|
253
|
+
/** Git's blob object id: sha1 of `blob <size>\0<content>`. */
|
|
254
|
+
function gitBlobSha(bytes) {
|
|
255
|
+
return createHash('sha1')
|
|
256
|
+
.update(`blob ${bytes.length}\0`)
|
|
257
|
+
.update(bytes)
|
|
258
|
+
.digest('hex');
|
|
259
|
+
}
|
|
260
|
+
function fileSize(path) {
|
|
261
|
+
try {
|
|
262
|
+
return statSync(path).size;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function messageOf(err) {
|
|
269
|
+
return err instanceof Error ? err.message : String(err);
|
|
74
270
|
}
|
package/lib/provider/draft.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CapturedRequest, type OprfMode, type ReclaimProvider, type ResponseMatch, type ResponseRedaction, type SecretParamRef } from './schema.ts';
|
|
2
2
|
/** OPRF mode to apply when the caller passed none: the anti-leak floor —
|
|
3
3
|
* force-hash a value whose SHAPE is unambiguous PII, but ONLY when it's
|
|
4
4
|
* OPRF-hashable AND within the 62-byte MPC-circuit cap (UTF-8). Undefined =
|
|
@@ -63,3 +63,26 @@ export declare function assertNoSecretUrlConflict(url: string, secrets: Record<s
|
|
|
63
63
|
export declare function draftProvider(req: CapturedRequest, targets: DraftTarget[], name: string, hash?: OprfMode, opts?: {
|
|
64
64
|
shortenNames?: boolean;
|
|
65
65
|
}): DraftResult;
|
|
66
|
+
/** Fields `update_draft` may replace on a drafted provider. */
|
|
67
|
+
export interface DraftEdit {
|
|
68
|
+
responseMatches?: ResponseMatch[];
|
|
69
|
+
responseRedactions?: ResponseRedaction[];
|
|
70
|
+
paramValues?: Record<string, string>;
|
|
71
|
+
/** ISO country the request goes out from, or `{{DYNAMIC_GEO}}` /
|
|
72
|
+
* `{{DYNAMIC_GEO_SECRET}}` for the verifying user's own country. */
|
|
73
|
+
geoLocation?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Apply an agent's edit to a drafted provider: validate the new matchers the
|
|
77
|
+
* way the attestor reads them (regexes compile with its `sgi` flags, every
|
|
78
|
+
* redaction has a selector, a hashed redaction has exactly one named group),
|
|
79
|
+
* then run the same portability gate as {@link draftProvider} so an edit can't
|
|
80
|
+
* bake a captured value in as a literal. Returns the updated provider, its
|
|
81
|
+
* extractions (re-paired with the redaction that names each param), and
|
|
82
|
+
* advisory warnings (for example, a `{{param}}` nothing resolves).
|
|
83
|
+
*/
|
|
84
|
+
export declare function editDraftProvider(provider: ReclaimProvider, extractions: Extraction[], edit: DraftEdit): {
|
|
85
|
+
provider: ReclaimProvider;
|
|
86
|
+
extractions: Extraction[];
|
|
87
|
+
warnings: string[];
|
|
88
|
+
};
|