@reclaimprotocol/agent 0.1.35 → 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/api/operations.gen.js +1 -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/mcp/tools/verification/proxy-callback-events.d.ts +21 -3
- package/lib/mcp/tools/verification/proxy-callback-events.js +128 -45
- 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 +44 -6
- package/skill/old-mode.md +18 -13
- package/skill/start-integration.md +51 -35
- package/skill/verification-client-integration.md +87 -15
- 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/api/operations.gen.ts +1 -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/mcp/tools/verification/proxy-callback-events.ts +151 -41
- 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
package/skill/old-mode.md
CHANGED
|
@@ -24,25 +24,30 @@ shorten, or reuse one from memory. When state is missing or ambiguous, inspect
|
|
|
24
24
|
it with the relevant get/list/status tool or call `how_it_works` before
|
|
25
25
|
answering. State uncertainty plainly instead of guessing.
|
|
26
26
|
|
|
27
|
+
**Ask blocking questions by ending your turn.** When you need an answer or an
|
|
28
|
+
action from the developer before you can continue, end your turn with the
|
|
29
|
+
question and every link they need. Don't use a background or asynchronous
|
|
30
|
+
question tool for it: the developer may never see it.
|
|
31
|
+
|
|
27
32
|
**Use the guide as needed.** Call `how_it_works({})` for its topic index, then
|
|
28
33
|
fetch only the topic relevant to the next tool call.
|
|
29
34
|
|
|
30
|
-
**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
**Open the browser without asking.** Call `attach_browser()` with no
|
|
36
|
+
arguments: it opens a visible browser on the developer's machine with the
|
|
37
|
+
persistent Reclaim profile, and falls back to Chrome for Testing itself when
|
|
38
|
+
no installed browser can be attached. Don't ask which browser or who signs in.
|
|
39
|
+
Use another `mode` only when that fails too, or when the developer explicitly
|
|
40
|
+
asks for it (topic `browser`). Tell the developer once, as a side note, that a
|
|
41
|
+
shareable link can be set up if a volunteer is helping them sign in; don't ask
|
|
42
|
+
about it or set it up unless they ask.
|
|
43
|
+
|
|
44
|
+
**The flow:** `reclaim_authenticate` once → `attach_browser()` (no arguments: a
|
|
45
|
+
visible local browser) → `start_capture` ⇒ **`captureId`** →
|
|
41
46
|
drive the page (`navigate`,
|
|
42
47
|
`wait_for_page`, `eval_in_page`) → inspect (`list_requests`, `get_request`,
|
|
43
48
|
`find_requests_containing`) → `propose_provider` ⇒ **`draftId`** →
|
|
44
|
-
`replay_request(captureId, draftId)` → `run_proof(captureId,
|
|
45
|
-
|
|
49
|
+
`replay_request(captureId, draftId)` → `run_proof(captureId,
|
|
50
|
+
draftId)` → `create_provider_version_from_capture`.
|
|
46
51
|
|
|
47
52
|
`reclaim_authenticate` itself always uses a local Chrome over loopback CDP. It
|
|
48
53
|
never uses the container, remote Builder, custom-CDP, or shared authoring
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Follow this plan end to end. Mark each step done as you go. Do not ask the developer whether to integrate: integrating into this codebase is the goal.
|
|
4
4
|
|
|
5
|
+
**Order.** Do the steps that need the developer first: sign in to Builder, attach the browser, capture, and prove the provider (step 2). These need the developer at the browser, and they show early whether a proof is possible on this site. Write app code (step 3) after the proof works, or while you wait on the developer. Never spend a long stretch on app code before the developer has signed in and the browser is attached.
|
|
6
|
+
|
|
7
|
+
**Asking the developer.** When you need an answer or an action from the developer before you can continue, end your turn with the question. Don't use background or asynchronous question tools for blocking questions: the developer may never see them. Put every link the developer needs, such as a sign-in or verification URL, in that same message.
|
|
8
|
+
|
|
5
9
|
## 1. Sign in and pick the organization
|
|
6
10
|
|
|
7
|
-
- [ ] Call `reclaim_authenticate` if you are not signed in.
|
|
11
|
+
- [ ] Call `reclaim_authenticate` if you are not signed in. Give the developer the returned `signInUrl` to approve in their browser (end your turn first if `browserOpened` is false), then call `wait_for_sign_in`.
|
|
8
12
|
<!-- if:orgId -->
|
|
9
13
|
- [ ] Use organization `{{orgId}}` for every call below.
|
|
10
14
|
<!-- endif -->
|
|
@@ -12,63 +16,75 @@ Follow this plan end to end. Mark each step done as you go. Do not ask the devel
|
|
|
12
16
|
- [ ] Call `list_orgs`. Use the only organization, or ask the developer which one when there are several.
|
|
13
17
|
<!-- endif -->
|
|
14
18
|
|
|
15
|
-
## 2.
|
|
16
|
-
|
|
17
|
-
Setup does not need the provider, so it runs in parallel with step 3 or before it.
|
|
18
|
-
|
|
19
|
-
**Delegation.** If you can run subagents, hand this step to one subagent and start step 3 now: step 3 needs the developer at the browser, step 2 does not. Give the subagent a self-contained brief:
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
Goal: let this app's users prove "{{goal}}" with Reclaim.
|
|
23
|
-
Repository: <absolute path>. Organization: <org ID>.
|
|
24
|
-
Task: do step 2 of the plan below and report the file paths you changed, the
|
|
25
|
-
callback route URL, and the env variable that holds the provider ID.
|
|
26
|
-
Do not build the provider. Never print or commit the org key.
|
|
27
|
-
<paste the step 2 checklist>
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Without subagents, do step 2 first.
|
|
31
|
-
|
|
32
|
-
- [ ] Detect the app's framework and package manager from the repository, and use them. Read `how_it_works({ topic: "integration" })`.
|
|
33
|
-
- [ ] Call `issue_org_token` with the organization, this project's absolute path, and the server env file. It writes the key to that file and never returns it. Never print or commit the key. Confirm the env file is in `.gitignore`.
|
|
34
|
-
- [ ] Add a backend route that creates a verification session (`POST /verifications/sessions`) and returns `verificationUrl`. Read the provider ID from server config (an env variable such as `RECLAIM_PROVIDER_ID`), so the route works before the provider exists.
|
|
35
|
-
- [ ] Add a callback route that verifies each result with `results.receive` from `@reclaimprotocol/client`, then stores only the requested data points.
|
|
36
|
-
- [ ] Add a frontend Verify button that opens `verificationUrl`, and shows the stored result when it arrives.
|
|
37
|
-
- [ ] Start the app. Call `proxy_callback_events({ port, path })` with the callback route's port and path. It forwards the org's verification events to that route on localhost, with no public URL.
|
|
38
|
-
<!-- ifnot:providerIds -->
|
|
39
|
-
- [ ] Call `check_attestor` once. If it fails, tell the developer before they spend time on the provider build: this machine cannot reach the attestor.
|
|
40
|
-
<!-- endif -->
|
|
41
|
-
|
|
42
|
-
## 3. Get the provider
|
|
19
|
+
## 2. Get the provider and prove it
|
|
43
20
|
|
|
44
21
|
<!-- if:providerIds -->
|
|
45
22
|
- [ ] Use these existing providers: {{providerIds}}. Read each with `get_provider` and keep only the data points "{{goal}}" needs. Show the developer and use only those; ignore the rest.
|
|
46
|
-
- [ ] If a provider needs any change (a missing data point, inputs the developer didn't ask for, a different value), make your own copy as the server instructions describe ("Choose the provider"). Otherwise
|
|
23
|
+
- [ ] If a provider needs any change (a missing data point, inputs the developer didn't ask for, a different value), make your own copy as the server instructions describe ("Choose the provider"). Otherwise go on to step 3.
|
|
47
24
|
<!-- endif -->
|
|
48
25
|
<!-- ifnot:providerIds -->
|
|
49
26
|
Build a new organization-private provider for exactly "{{goal}}". Do not search published providers first.
|
|
50
27
|
|
|
28
|
+
- [ ] Call `check_attestor` once. If it fails, tell the developer before they spend time on the provider build: this machine cannot reach the attestor.
|
|
51
29
|
- [ ] If the site, the account, or the value to prove is unclear, ask one concise question. Otherwise do not ask.
|
|
52
30
|
- [ ] Choose the minimum data points: only the values the claim needs, usually one or two. Add no profile or identity extras.
|
|
53
31
|
- [ ] If the developer names an existing provider, use only the data points they asked for and ignore the rest. If it needs any change, make your own copy: a new version when the organization owns it, otherwise a new organization provider from a copy of its config.
|
|
54
32
|
- [ ] Call `how_it_works` with topics `overview`, `user-script`, `capture`, and `publish` before you start. Do not repeat their detail here.
|
|
55
|
-
- [ ] Call `attach_browser`
|
|
33
|
+
- [ ] Call `attach_browser()` with no arguments. It opens a visible local browser, falling back to Chrome for Testing when no installed browser can be attached; don't ask which browser to use. Use another mode only if that fails too (topic `browser`). Mention once, as a side note, that a shareable link can be set up if a volunteer is helping the developer sign in; don't ask or set it up unless they ask. Use `start_capture` on the tab.
|
|
56
34
|
- [ ] Open the sign-in page with `navigate`. The developer signs in; use `wait_for_login`.
|
|
57
35
|
- [ ] Write the navigation script first. Signed out, it goes to the sign-in page. After sign-in, it visits each data page in order. It must stay correct across reloads.
|
|
58
36
|
- [ ] Test it with `test_user_script` and `keep: true`. The kept script stays installed for every later navigation and is published with the provider version.
|
|
59
37
|
- [ ] While the script drives the browser, find the request that returns each data point (`find_requests_containing`, then `analyze_request_constraints`).
|
|
60
38
|
- [ ] Call `propose_provider` with only the needed data points.
|
|
61
39
|
- [ ] Call `replay_request`, then `replay_request` with `withoutSecrets: true` to confirm the request is auth-bound.
|
|
62
|
-
- [ ] Call `
|
|
40
|
+
- [ ] Call `run_proof`. It creates a local test signing key if none exists; tell the developer once afterwards.
|
|
63
41
|
- [ ] Call `create_provider_version_from_capture`. It publishes the kept script as `jsUserScripts`. Follow its `_notes`, then publish the version.
|
|
64
42
|
- [ ] Call `dispose_browser` after asking the developer.
|
|
65
43
|
<!-- endif -->
|
|
66
44
|
|
|
45
|
+
## 3. Set up the app
|
|
46
|
+
|
|
47
|
+
Setup does not need the provider. Start it after step 2 has a working proof, or while you wait on the developer during step 2 (for example, while they sign in to the site). Don't let it delay a step that needs the developer.
|
|
48
|
+
|
|
49
|
+
**Delegation.** If you can run subagents, hand this step to one subagent as soon as you are signed in, and keep working on step 2: step 2 needs the developer at the browser, step 3 does not. Give the subagent a self-contained brief:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Goal: let this app's users prove "{{goal}}" with Reclaim.
|
|
53
|
+
Repository: <absolute path>. Organization: <org ID>.
|
|
54
|
+
Task: do step 3 of the plan below and report the file paths you changed, the
|
|
55
|
+
callback route URL, and the env variable that holds the provider ID.
|
|
56
|
+
Do not build the provider. Never print or commit the org key.
|
|
57
|
+
<paste the step 3 checklist>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Without subagents, do step 2 first, and fill waits for the developer with this step.
|
|
61
|
+
|
|
62
|
+
**No app yet?** If the workspace has no app to add verification to (it is empty, or has no backend or frontend), don't write one. Bootstrap from [builder-example](https://github.com/reclaimprotocol/builder-example), a ready-to-run Fastify backend and Vite frontend that already create sessions, verify callbacks, poll status, and render every UI state:
|
|
63
|
+
|
|
64
|
+
- [ ] Clone it: `git clone --depth 1 https://github.com/reclaimprotocol/builder-example <dir>` (use the workspace itself when it is empty), then delete the clone's `.git` directory so the code becomes the developer's own.
|
|
65
|
+
- [ ] Call `issue_org_token` with the clone's absolute path as `projectDir` and `envFile: "packages/backend/.env"`.
|
|
66
|
+
- [ ] Only when the developer asks for session binding or encrypted results: call `issue_org_keypair` with the same `projectDir` and `envFile` (add `encryptResults: true` for encrypted results). It writes `RECLAIM_ORG_ETH_PRIVATE_KEY` and registers the public key. Then pass `orgEthPrivateKey: process.env.RECLAIM_ORG_ETH_PRIVATE_KEY` to `reclaim.sessions.create` in `packages/backend/src/routes/verifications.ts` (binding) and to `reclaim.results.receive` in `packages/backend/src/routes/callback.ts` (decryption and TEE proof checks), and add an empty `RECLAIM_ORG_ETH_PRIVATE_KEY=` line to `packages/backend/example.env`.
|
|
67
|
+
- [ ] From the clone's root, run `npm install`, then `npm install @reclaimprotocol/client@latest -w packages/backend` so the backend uses the latest client rather than the version in the example's lockfile. Then run `npm run dev`. The backend listens on port 3000; the app opens at <http://localhost:5173>.
|
|
68
|
+
- [ ] Call `proxy_callback_events({ port: 3000, path: "/reclaim/callback" })`.
|
|
69
|
+
- [ ] Skip the rest of this step. In step 4, the provider goes in `packages/backend/config.json`: replace the example entry with `{ "id": "<provider ID>", "name": "<what it proves>" }`, then restart the backend, because it reads the file at startup.
|
|
70
|
+
|
|
71
|
+
**Existing app.** Add verification to it:
|
|
72
|
+
|
|
73
|
+
- [ ] Detect the app's framework and package manager from the repository, and use them. Read `how_it_works({ topic: "integration" })`.
|
|
74
|
+
- [ ] Call `issue_org_token` with the organization, this project's absolute path, and the server env file. It writes the key to that file and never returns it. Never print or commit the key. Confirm the env file is in `.gitignore`.
|
|
75
|
+
- [ ] Add a backend route that creates a verification session (`POST /verifications/sessions`) and returns `verificationUrl`. Read the provider ID from server config (an env variable such as `RECLAIM_PROVIDER_ID`), so the route works before the provider exists.
|
|
76
|
+
- [ ] Only when the developer asks for session binding or encrypted results: call `issue_org_keypair` with the server env file (`encryptResults: true` for encrypted results), and pass `orgEthPrivateKey` from that env variable to `sessions.create` and `results.receive`.
|
|
77
|
+
- [ ] Add a callback route that verifies each result with `results.receive` from `@reclaimprotocol/client`, then stores only the requested data points. Store every failure on the verification session record and log it server-side: a failure event (`verification_rejected`, `verification_error`, `verification_cancelled`, `session_expired`), or a thrown `ResultVerificationError` (`error.reason` plus each failing proof's `reason`). Respond `error.retryable ? 503 : 200`. A result that failed verification gets 2xx, because retrying can't fix it. Read the exact pattern in `how_it_works({ topic: "integration" })`.
|
|
78
|
+
- [ ] Add a status route the frontend polls. It returns the stored status, data points, and failure reason. While the app has no result, it also reads `GET /verifications/sessions/{sessionId}`, so a session that ended in Builder never stays pending.
|
|
79
|
+
- [ ] Add a frontend Verify button that opens `verificationUrl`, and render every state in "UI states" in `how_it_works({ topic: "integration" })`: idle, waiting in the other tab, verifying, verified (with the data points), failed (plain-language reason and **Try again**), and expired or cancelled (**Try again**). Never leave a spinner as the only feedback.
|
|
80
|
+
- [ ] Start the app. Call `proxy_callback_events({ port, path })` with the callback route's port and path. It forwards the org's verification events to that route on localhost, with no public URL.
|
|
81
|
+
|
|
67
82
|
## 4. Wire the provider and test end to end
|
|
68
83
|
|
|
69
84
|
- [ ] Set the provider ID in the app's server config.
|
|
70
|
-
- [ ] Confirm the callback route is forwarding: `proxy_callback_events` with no `stop` returns its status.
|
|
71
|
-
- [ ] Tell the developer exactly how to test: open the app, click Verify, sign in, and see the stored result.
|
|
72
|
-
- [ ]
|
|
85
|
+
- [ ] Confirm the callback route is forwarding: `proxy_callback_events` with no `stop` returns its status. If `blocked` is not empty, the app rejected that session's event: read the callback route's response and logs, fix the app, and check again.
|
|
86
|
+
- [ ] Tell the developer exactly how to test: open the app, click Verify, sign in, and see the stored result. Also test one failure, such as cancelling in the verification tab, and confirm the app shows the failed state with **Try again**.
|
|
87
|
+
- [ ] The hosted verification page streams the target site as an image, so text or DOM checks of that page come back empty. Check progress with `list_verification_events` and `get_verification_session`, not by reading the page.
|
|
88
|
+
- [ ] After they click Verify, read the delivered events from `proxy_callback_events` status (`recent`) or `read_callback_events`. If the result did not arrive, or the app shows an unexpected failure, call `list_verification_deliveries` and fix the cause. Repeat until the app shows the stored result.
|
|
73
89
|
- [ ] Ask the developer to set up the production callback. The local listener works only while this coding agent's MCP server runs; once it stops, the deployed app receives no results. Call `create_org_callback_subscription` with the deployed app's public callback route URL for all five session-ending events: `verification_success`, `verification_rejected`, `verification_error`, `verification_cancelled`, and `session_expired`.
|
|
74
90
|
- [ ] If they have no deployed URL yet, tell them: "When you deploy, ask me to run `create_org_callback_subscription` with your public callback URL, or results will not reach the app."
|
|
@@ -7,8 +7,18 @@ plan. Don't ask whether to try, integrate, or create an app. Ask one short
|
|
|
7
7
|
question only when intent is unclear. The steps below are the detail behind
|
|
8
8
|
that plan.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Prove the provider before you write app code: sign in to Builder, attach the
|
|
11
|
+
browser, capture, and run the proof first. Those steps need the developer, and
|
|
12
|
+
they show whether a proof is possible on the site. Write the app code after the
|
|
13
|
+
proof works, or while you wait on the developer. When you need the developer
|
|
14
|
+
to answer or act, end your turn with the question and every link they need;
|
|
15
|
+
don't ask through a background or asynchronous question tool.
|
|
16
|
+
|
|
17
|
+
When the workspace has no app yet, bootstrap from
|
|
18
|
+
[builder-example](https://github.com/reclaimprotocol/builder-example) as the
|
|
19
|
+
`start_integration` plan describes, instead of writing a new backend and
|
|
20
|
+
frontend. Otherwise, inspect the app's frontend, backend, package manager,
|
|
21
|
+
configuration, and callback conventions. Complete the integration in its codebase and run its
|
|
12
22
|
relevant checks. The MCP tools configure Builder resources; your file tools
|
|
13
23
|
edit the app.
|
|
14
24
|
|
|
@@ -27,10 +37,11 @@ server), or migrate to the session flow below. When migrating, remove
|
|
|
27
37
|
production): register a
|
|
28
38
|
public callback URL with `create_org_callback_subscription`, including
|
|
29
39
|
all five session-ending events. If the
|
|
30
|
-
app needs encrypted results
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
the developer
|
|
40
|
+
app needs encrypted results or session binding, call `issue_org_keypair`
|
|
41
|
+
(`encryptResults: true` for encrypted results). It writes the private key to
|
|
42
|
+
the backend's env file and registers only the public key. It returns 409
|
|
43
|
+
when the org already has a different key: ask the developer for that key
|
|
44
|
+
rather than replacing it.
|
|
34
45
|
3. Org key: reuse the `RECLAIM_ORG_SECRET` the app already has. Otherwise an
|
|
35
46
|
organization owner calls `issue_org_token`. It writes the key to the
|
|
36
47
|
project's env file (default `.env`, variable `RECLAIM_ORG_SECRET`) and
|
|
@@ -47,18 +58,32 @@ server), or migrate to the session flow below. When migrating, remove
|
|
|
47
58
|
is not proof of success. Use `@reclaimprotocol/client`
|
|
48
59
|
(`ReclaimVerification.create({ orgSecret })`, `sessions.create`) only when
|
|
49
60
|
the developer asks for it.
|
|
50
|
-
5. Add a backend callback
|
|
61
|
+
5. Add a backend callback route. Persist each delivery idempotently and load
|
|
51
62
|
the expected session ID and org ID from app storage. To verify the result on
|
|
52
|
-
your callback, install `@reclaimprotocol/client` in the backend and call
|
|
63
|
+
your callback route, install `@reclaimprotocol/client` in the backend and call
|
|
53
64
|
`reclaim.results.receive(body, { reclaimSessionId, expectedAud,
|
|
54
65
|
orgEthPrivateKey })`. Use the verified proof data to update app state.
|
|
55
|
-
|
|
66
|
+
Store every failure on the verification session record: failure events, and
|
|
67
|
+
a thrown `ResultVerificationError` (`error.reason` and each
|
|
68
|
+
`error.outcome.proofs[].reason`). Respond `error.retryable ? 503 : 200`.
|
|
69
|
+
A result that failed verification gets 2xx: retrying can't fix it, and an
|
|
70
|
+
unacknowledged delivery holds back that session's later events. Respond 5xx
|
|
71
|
+
for any other temporary problem too, such as a database outage.
|
|
72
|
+
Add a status route that returns the stored status, data points, and failure
|
|
73
|
+
reason. While the app has no result, it also reads
|
|
74
|
+
`GET /verifications/sessions/{sessionId}`, so a session Builder has ended
|
|
75
|
+
never stays pending in the app. Render the frontend per
|
|
76
|
+
[UI states](#ui-states).
|
|
56
77
|
6. Exercise one session through the selected registered Verification Client,
|
|
57
78
|
inspect its Builder events, and test the app's success and failure paths.
|
|
58
79
|
Omit `verificationClientUrl` to use the Portal, or select another client
|
|
59
80
|
from `VerificationClient` in the SDK. Do not send an unregistered URL or
|
|
60
81
|
call a legacy backend for an `api=2` session.
|
|
61
82
|
|
|
83
|
+
The hosted verification page streams the target site as an image, so text
|
|
84
|
+
and DOM checks of that page come back empty. Check a session's progress
|
|
85
|
+
with `list_verification_events` and `get_verification_session` instead.
|
|
86
|
+
|
|
62
87
|
The one-time management calls above use the developer's Builder login. The
|
|
63
88
|
consumer backend uses the separate organization token. The SDK's API-only
|
|
64
89
|
entry point, `@reclaimprotocol/client/api`, is available when an application
|
|
@@ -125,16 +150,37 @@ Receive and verify the result on the callback route. This is the one place the
|
|
|
125
150
|
client package is needed:
|
|
126
151
|
|
|
127
152
|
```js
|
|
128
|
-
import { ReclaimVerification } from '@reclaimprotocol/client'
|
|
153
|
+
import { ReclaimVerification, ResultVerificationError } from '@reclaimprotocol/client'
|
|
129
154
|
|
|
130
155
|
const reclaim = ReclaimVerification.create({
|
|
131
156
|
orgSecret: process.env.RECLAIM_ORG_SECRET,
|
|
132
157
|
})
|
|
133
158
|
|
|
134
159
|
app.post('/reclaim/callback', async(req, res) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
let outcome
|
|
161
|
+
try {
|
|
162
|
+
outcome = await reclaim.results.receive(req.body, {
|
|
163
|
+
reclaimSessionId: sessionId, // the id you stored
|
|
164
|
+
})
|
|
165
|
+
} catch(error) {
|
|
166
|
+
if(!(error instanceof ResultVerificationError)) {
|
|
167
|
+
throw error
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if(error.retryable) {
|
|
171
|
+
return res.sendStatus(503) // temporary: Builder retries the delivery
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Retrying can't fix a result that failed verification. Store the failure
|
|
175
|
+
// where the status route returns it, then acknowledge the delivery.
|
|
176
|
+
const proofReasons = (error.outcome.proofs ?? [])
|
|
177
|
+
.filter((proof) => !proof.proofValid)
|
|
178
|
+
.map((proof) => proof.reason)
|
|
179
|
+
console.warn('result failed verification', error.reason, proofReasons)
|
|
180
|
+
await markSessionFailed(sessionId, error.reason) // your app's storage
|
|
181
|
+
return res.sendStatus(200)
|
|
182
|
+
}
|
|
183
|
+
|
|
138
184
|
if(outcome.kind === 'result') {
|
|
139
185
|
console.log(outcome.result.payload) // verified
|
|
140
186
|
}
|
|
@@ -143,8 +189,34 @@ app.post('/reclaim/callback', async(req, res) => {
|
|
|
143
189
|
})
|
|
144
190
|
```
|
|
145
191
|
|
|
146
|
-
|
|
147
|
-
|
|
192
|
+
### UI states
|
|
193
|
+
|
|
194
|
+
Apply these rules when you write the frontend. Test each state, including one
|
|
195
|
+
failure.
|
|
196
|
+
|
|
197
|
+
| State | Render |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| Idle | The **Verify** button. |
|
|
200
|
+
| Waiting in the other tab | The current step ("Finish verifying in the new tab") and a way to reopen `verificationUrl`. After about 30 seconds, say what is happening and offer **Cancel** or **Try again**. |
|
|
201
|
+
| Verifying | "Checking your result". |
|
|
202
|
+
| Verified | The requested data points. |
|
|
203
|
+
| Failed | A plain-language reason and what to do next, near the **Verify** button, with **Try again**. |
|
|
204
|
+
| Expired or cancelled | Say which, with **Try again**. |
|
|
205
|
+
|
|
206
|
+
- Show the current step, never an indefinite spinner (Nielsen heuristic 1,
|
|
207
|
+
visibility of system status).
|
|
208
|
+
- Write errors in plain language that says what happened and what to do next,
|
|
209
|
+
visible next to the action, with a recovery action (Nielsen Norman Group
|
|
210
|
+
error-message guidelines).
|
|
211
|
+
- Send reason codes (`error.reason`, proof `reason`) to logs or an expandable
|
|
212
|
+
**Details** section, not the headline.
|
|
213
|
+
- Announce changes: `role="status"` (or `aria-live="polite"`) for progress,
|
|
214
|
+
`role="alert"` for failures (WCAG 2.2 SC 4.1.3).
|
|
215
|
+
- Pair color with text or an icon (WCAG 2.2 SC 1.4.1).
|
|
216
|
+
- Stop the waiting state when the status route reports a terminal status.
|
|
217
|
+
|
|
218
|
+
Check or investigate a session from the status route or by hand with `curl`.
|
|
219
|
+
All routes take the org key:
|
|
148
220
|
|
|
149
221
|
| Route | Use |
|
|
150
222
|
| --- | --- |
|
package/src/authoring/backend.ts
CHANGED
|
@@ -35,6 +35,10 @@ export interface Draft {
|
|
|
35
35
|
/** One entry per extracted value (value + its redaction), so replay can
|
|
36
36
|
* validate each against its own selector. */
|
|
37
37
|
extractions: Extraction[]
|
|
38
|
+
/** Outcome of the latest authenticated in-browser replay, recorded by
|
|
39
|
+
* backends that track it. Lets a failed proof tell "the site blocks
|
|
40
|
+
* non-browser clients" apart from a broken recipe. */
|
|
41
|
+
lastReplay?: { status: number, ok: boolean }
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/** A navigation script installed for every later page load of the authoring
|
|
@@ -49,10 +53,12 @@ export interface KeptScriptRef {
|
|
|
49
53
|
keptScript?: KeptScript
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
/** Per-session draft store: `propose` puts, `replay`/`run_proof` get by id
|
|
56
|
+
/** Per-session draft store: `propose` puts, `replay`/`run_proof` get by id,
|
|
57
|
+
* `update_draft` replaces one in place (same id). */
|
|
53
58
|
export interface DraftStore {
|
|
54
59
|
put(draft: Draft): string
|
|
55
60
|
get(id: string): Draft | undefined
|
|
61
|
+
set(id: string, draft: Draft): void
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export function makeDraftStore(): DraftStore {
|
|
@@ -64,6 +70,9 @@ export function makeDraftStore(): DraftStore {
|
|
|
64
70
|
return id
|
|
65
71
|
},
|
|
66
72
|
get: (id) => map.get(id),
|
|
73
|
+
set: (id, draft) => {
|
|
74
|
+
map.set(id, draft)
|
|
75
|
+
},
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
|
|
@@ -73,6 +82,8 @@ export function makeDraftStore(): DraftStore {
|
|
|
73
82
|
export interface ProveOptions {
|
|
74
83
|
ownerAddress?: string
|
|
75
84
|
attestorUrl?: string
|
|
85
|
+
/** ISO country the attestor's request goes out from (local proofs). */
|
|
86
|
+
geoLocation?: string
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
/**
|
package/src/authoring/tools.ts
CHANGED
|
@@ -20,8 +20,15 @@ import { proposeTool } from '../mcp/tools/agent/propose.ts'
|
|
|
20
20
|
import { replayTool } from '../mcp/tools/agent/replay.ts'
|
|
21
21
|
import type { CapturedRequest } from '../provider/schema.ts'
|
|
22
22
|
import { defineTool, type RegisteredTool } from '../tools/registry.ts'
|
|
23
|
-
import type { AuthoringBackend } from './backend.ts'
|
|
24
|
-
import {
|
|
23
|
+
import type { AuthoringBackend, ProveOptions } from './backend.ts'
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_MAX_CHARS,
|
|
26
|
+
objectSchema,
|
|
27
|
+
publicRequest,
|
|
28
|
+
type ResolveBackend,
|
|
29
|
+
TEXT_WINDOW_PROPERTIES,
|
|
30
|
+
textWindow,
|
|
31
|
+
} from './util.ts'
|
|
25
32
|
|
|
26
33
|
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))
|
|
27
34
|
|
|
@@ -155,13 +162,40 @@ export function authoringTools(
|
|
|
155
162
|
+ 'Use it to read a data point from the DOM, check sign-in state, '
|
|
156
163
|
+ 'or trigger the target request (for example, `location.reload()`). '
|
|
157
164
|
+ 'The page cannot reach files or processes on this machine. Return '
|
|
158
|
-
+ 'only the values you need, never cookies or tokens.'
|
|
165
|
+
+ 'only the values you need, never cookies or tokens. A result '
|
|
166
|
+
+ `longer than ${DEFAULT_MAX_CHARS} characters comes back `
|
|
167
|
+
+ 'truncated (objects as JSON text) with `valueTruncation`; page '
|
|
168
|
+
+ 'with `offset`, or pass `maxChars` or `full`.',
|
|
159
169
|
inputSchema: objectSchema(
|
|
160
|
-
{ expression: { type: 'string' } },
|
|
170
|
+
{ expression: { type: 'string' }, ...TEXT_WINDOW_PROPERTIES },
|
|
171
|
+
['expression'],
|
|
161
172
|
),
|
|
162
173
|
annotations: { openWorldHint: true, destructiveHint: false },
|
|
163
174
|
},
|
|
164
|
-
(args) =>
|
|
175
|
+
async(args) => {
|
|
176
|
+
const res = await evalInPage(
|
|
177
|
+
resolveBackend(args), String(args.expression),
|
|
178
|
+
)
|
|
179
|
+
if(res.value === undefined) {
|
|
180
|
+
return res
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const text = typeof res.value === 'string'
|
|
184
|
+
? res.value
|
|
185
|
+
: JSON.stringify(res.value)
|
|
186
|
+
const slice = textWindow(text, args)
|
|
187
|
+
if(!slice.truncation) {
|
|
188
|
+
return res
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
value: slice.text,
|
|
193
|
+
valueTruncation: {
|
|
194
|
+
...slice.truncation,
|
|
195
|
+
...(typeof res.value === 'string' ? {} : { format: 'json' }),
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
},
|
|
165
199
|
),
|
|
166
200
|
defineTool<Record<string, unknown>>(
|
|
167
201
|
{
|
|
@@ -484,7 +518,10 @@ export function authoringTools(
|
|
|
484
518
|
+ 'attestor calls the target from its own network and IP. A request '
|
|
485
519
|
+ 'that replayed can still fail here for geo- or IP-bound content '
|
|
486
520
|
+ '(country-locked pages, IP-allowlisted APIs); that is a property '
|
|
487
|
-
+ 'of the site, not a bug in the draft.'
|
|
521
|
+
+ 'of the site, not a bug in the draft. HTTP 403/406/429/503 or a '
|
|
522
|
+
+ 'challenge page from the attestor while the replay succeeded '
|
|
523
|
+
+ 'means the site blocks non-browser clients: copying headers or '
|
|
524
|
+
+ 'cookies will not help, so try a different endpoint or stop.',
|
|
488
525
|
inputSchema: objectSchema({ draftId: { type: 'string' } }, ['draftId']),
|
|
489
526
|
annotations: { openWorldHint: true, destructiveHint: false },
|
|
490
527
|
},
|
|
@@ -500,7 +537,7 @@ export function authoringTools(
|
|
|
500
537
|
throw new Error(`no draft ${String(args.draftId)}`)
|
|
501
538
|
}
|
|
502
539
|
|
|
503
|
-
const opts:
|
|
540
|
+
const opts: ProveOptions = {}
|
|
504
541
|
if(typeof args.ownerAddress === 'string') {
|
|
505
542
|
opts.ownerAddress = args.ownerAddress
|
|
506
543
|
}
|
|
@@ -509,6 +546,10 @@ export function authoringTools(
|
|
|
509
546
|
opts.attestorUrl = args.attestorUrl
|
|
510
547
|
}
|
|
511
548
|
|
|
549
|
+
if(typeof args.geoLocation === 'string') {
|
|
550
|
+
opts.geoLocation = args.geoLocation
|
|
551
|
+
}
|
|
552
|
+
|
|
512
553
|
return backend.prove(draft, opts)
|
|
513
554
|
},
|
|
514
555
|
),
|
package/src/authoring/util.ts
CHANGED
|
@@ -43,3 +43,67 @@ export function publicRequest(r: CapturedRequest) {
|
|
|
43
43
|
...(r.graphqlOp ? { graphqlOp: r.graphqlOp } : {}),
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Characters a long text field returns by default (about 20 KB of ASCII),
|
|
48
|
+
* so one page body or eval result can't flood the caller's context. */
|
|
49
|
+
export const DEFAULT_MAX_CHARS = 20_000
|
|
50
|
+
|
|
51
|
+
export interface TextWindowArgs {
|
|
52
|
+
maxChars?: unknown
|
|
53
|
+
offset?: unknown
|
|
54
|
+
full?: unknown
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Input properties that let a caller page through, or opt out of, the
|
|
58
|
+
* window {@link textWindow} applies. */
|
|
59
|
+
export const TEXT_WINDOW_PROPERTIES = {
|
|
60
|
+
maxChars: {
|
|
61
|
+
type: 'integer',
|
|
62
|
+
minimum: 1,
|
|
63
|
+
description: `Maximum characters to return. Default ${DEFAULT_MAX_CHARS}.`,
|
|
64
|
+
},
|
|
65
|
+
offset: {
|
|
66
|
+
type: 'integer',
|
|
67
|
+
minimum: 0,
|
|
68
|
+
description: 'Character offset to start from, such as a previous '
|
|
69
|
+
+ '`nextOffset`. Default 0.',
|
|
70
|
+
},
|
|
71
|
+
full: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
description: 'Return the whole text, ignoring `maxChars`. It can be '
|
|
74
|
+
+ 'megabytes, so prefer paging with `offset`.',
|
|
75
|
+
},
|
|
76
|
+
} satisfies Record<string, JSONSchema.BaseSchema>
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns characters `[offset, offset + maxChars)` of `text`. When that isn't
|
|
80
|
+
* the whole text, `truncation` reports the total size and how to get the rest.
|
|
81
|
+
*/
|
|
82
|
+
export function textWindow(text: string, args: TextWindowArgs) {
|
|
83
|
+
const totalChars = text.length
|
|
84
|
+
const offset = Math.min(Math.max(Number(args.offset) || 0, 0), totalChars)
|
|
85
|
+
const maxChars = args.full === true
|
|
86
|
+
? totalChars
|
|
87
|
+
: Math.max(Number(args.maxChars) || DEFAULT_MAX_CHARS, 1)
|
|
88
|
+
const end = Math.min(offset + maxChars, totalChars)
|
|
89
|
+
if(offset === 0 && end === totalChars) {
|
|
90
|
+
return { text }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const part = text.slice(offset, end)
|
|
94
|
+
const more = end < totalChars
|
|
95
|
+
return {
|
|
96
|
+
text: part,
|
|
97
|
+
truncation: {
|
|
98
|
+
totalChars,
|
|
99
|
+
offset,
|
|
100
|
+
returnedChars: part.length,
|
|
101
|
+
...(more ? { nextOffset: end } : {}),
|
|
102
|
+
note: `Returned characters ${offset}-${end} of ${totalChars}.`
|
|
103
|
+
+ (more
|
|
104
|
+
? ` Call again with offset: ${end} for more, or full: true `
|
|
105
|
+
+ 'for the whole text.'
|
|
106
|
+
: ''),
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { failedDependency } from '@hapi/boom'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { arch, platform } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { LOGGER } from '../logger.ts'
|
|
7
|
+
import { reclaimHomeDirPath } from '../paths.ts'
|
|
8
|
+
|
|
9
|
+
export interface ChromeForTestingInstall {
|
|
10
|
+
executablePath: string
|
|
11
|
+
/** True when this call downloaded it; false when an earlier one had. */
|
|
12
|
+
downloaded: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Where downloaded browsers live: `~/.reclaim/browsers`. */
|
|
16
|
+
export function browsersDirPath() {
|
|
17
|
+
return join(reclaimHomeDirPath(), 'browsers')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Chrome for Testing, the fallback when no local Chromium-family browser is
|
|
22
|
+
* installed or the installed one will not start. Downloaded once into
|
|
23
|
+
* `~/.reclaim/browsers` and reused after that, so only the first fallback pays
|
|
24
|
+
* the ~150 MB download.
|
|
25
|
+
*/
|
|
26
|
+
export async function installChromeForTesting(
|
|
27
|
+
cacheDir = browsersDirPath(),
|
|
28
|
+
): Promise<ChromeForTestingInstall> {
|
|
29
|
+
// Loaded on demand: only the fallback path needs it.
|
|
30
|
+
const {
|
|
31
|
+
Browser,
|
|
32
|
+
detectBrowserPlatform,
|
|
33
|
+
getInstalledBrowsers,
|
|
34
|
+
getVersionComparator,
|
|
35
|
+
install,
|
|
36
|
+
resolveBuildId,
|
|
37
|
+
} = await import('@puppeteer/browsers')
|
|
38
|
+
const browserPlatform = detectBrowserPlatform()
|
|
39
|
+
assert(browserPlatform, failedDependency(
|
|
40
|
+
`Chrome for Testing has no build for ${platform()}/${arch()}.`,
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
const compare = getVersionComparator(Browser.CHROME)
|
|
44
|
+
const all = await getInstalledBrowsers({ cacheDir })
|
|
45
|
+
const installed = all
|
|
46
|
+
.filter((b) => b.browser === Browser.CHROME
|
|
47
|
+
&& b.platform === browserPlatform
|
|
48
|
+
&& existsSync(b.executablePath))
|
|
49
|
+
.sort((a, b) => compare(a.buildId, b.buildId))
|
|
50
|
+
const newest = installed.at(-1)
|
|
51
|
+
if(newest) {
|
|
52
|
+
return { executablePath: newest.executablePath, downloaded: false }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const buildId = await resolveBuildId(
|
|
56
|
+
Browser.CHROME, browserPlatform, 'stable',
|
|
57
|
+
)
|
|
58
|
+
LOGGER.info({ buildId, cacheDir }, 'downloading Chrome for Testing')
|
|
59
|
+
const result = await install({
|
|
60
|
+
browser: Browser.CHROME,
|
|
61
|
+
platform: browserPlatform,
|
|
62
|
+
buildId,
|
|
63
|
+
cacheDir,
|
|
64
|
+
})
|
|
65
|
+
return { executablePath: result.executablePath, downloaded: true }
|
|
66
|
+
}
|