@reclaimprotocol/agent 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/authoring/backend.d.ts +0 -24
- package/lib/authoring/find.d.ts +3 -0
- package/lib/authoring/find.js +54 -0
- package/lib/authoring/index.d.ts +3 -3
- package/lib/authoring/index.js +4 -3
- package/lib/authoring/propose.d.ts +3 -0
- package/lib/authoring/propose.js +109 -0
- package/lib/authoring/replay.d.ts +10 -0
- package/lib/authoring/replay.js +34 -0
- package/lib/authoring/tools.d.ts +13 -6
- package/lib/authoring/tools.js +108 -293
- package/lib/authoring/util.d.ts +27 -0
- package/lib/authoring/util.js +21 -0
- package/lib/consts.d.ts +6 -1
- package/lib/consts.js +9 -2
- package/lib/mcp/server.d.ts +2 -41
- package/lib/mcp/server.js +6 -34
- package/lib/mcp/start.js +4 -2
- package/lib/mcp/tools/agent/analyze.js +3 -3
- package/lib/mcp/tools/agent/build.d.ts +4 -4
- package/lib/mcp/tools/agent/build.js +13 -15
- package/lib/mcp/tools/agent/capture.d.ts +13 -1
- package/lib/mcp/tools/agent/capture.js +18 -85
- package/lib/mcp/tools/agent/eval.d.ts +5 -2
- package/lib/mcp/tools/agent/eval.js +11 -121
- package/lib/mcp/tools/agent/find.d.ts +10 -3
- package/lib/mcp/tools/agent/find.js +25 -31
- package/lib/mcp/tools/agent/proof.d.ts +13 -3
- package/lib/mcp/tools/agent/proof.js +81 -132
- package/lib/mcp/tools/agent/propose.d.ts +10 -9
- package/lib/mcp/tools/agent/propose.js +87 -64
- package/lib/mcp/tools/agent/replay.d.ts +10 -3
- package/lib/mcp/tools/agent/replay.js +29 -36
- package/lib/mcp/tools/agent/session.d.ts +1 -3
- package/lib/mcp/tools/agent/session.js +5 -5
- package/lib/mcp/tools/agent/shared.d.ts +5 -3
- package/lib/mcp/tools/agent/shared.js +29 -19
- package/lib/old/client.d.ts +39 -0
- package/lib/old/client.js +18 -0
- package/lib/old/login-cdp.d.ts +24 -5
- package/lib/old/login-cdp.js +29 -15
- package/lib/old/sdk-client.d.ts +39 -0
- package/lib/old/sdk-client.js +85 -0
- package/lib/old/to-register.d.ts +69 -0
- package/lib/old/to-register.js +55 -2
- package/lib/old/tools/authenticate.js +3 -7
- package/lib/old/tools/build.d.ts +6 -1
- package/lib/old/tools/build.js +11 -1
- package/lib/old/tools/get-provider-info.d.ts +16 -0
- package/lib/old/tools/get-provider-info.js +73 -0
- package/lib/old/tools/get-version-config.d.ts +17 -0
- package/lib/old/tools/get-version-config.js +47 -0
- package/lib/old/tools/publish.js +208 -2
- package/lib/old/tools/update-provider-metadata.d.ts +16 -0
- package/lib/old/tools/update-provider-metadata.js +61 -0
- package/lib/proof/attestor.d.ts +6 -3
- package/lib/proof/attestor.js +10 -1
- package/lib/proof/debug-response-dump.d.ts +4 -0
- package/lib/proof/debug-response-dump.js +55 -0
- package/lib/proof/patch-domhandler.d.ts +1 -0
- package/lib/proof/patch-domhandler.js +59 -0
- package/lib/proof/zk-files.js +7 -6
- package/lib/provider/dom-xpath.d.ts +17 -0
- package/lib/provider/dom-xpath.js +187 -0
- package/lib/provider/draft.d.ts +30 -2
- package/lib/provider/draft.js +117 -28
- package/lib/provider/index.d.ts +4 -0
- package/lib/provider/index.js +7 -0
- package/lib/provider/injection-doc.js +6 -0
- package/lib/provider/matcher.d.ts +17 -20
- package/lib/provider/matcher.js +394 -280
- package/lib/provider/schema.d.ts +7 -5
- package/lib/tools/registry.d.ts +14 -0
- package/lib/tools/registry.js +32 -0
- package/package.json +9 -1
- package/skill/SKILL.builder.md +42 -23
- package/skill/SKILL.old.md +6 -2
- package/skill/builder-mode.md +1 -1
- package/skill/injection-api.md +117 -0
- package/src/authoring/backend.ts +98 -0
- package/src/authoring/index.ts +20 -0
- package/src/authoring/tools.ts +273 -0
- package/src/authoring/util.ts +42 -0
- package/src/consts.ts +15 -2
- package/src/mcp/server.ts +11 -78
- package/src/mcp/start.ts +4 -1
- package/src/mcp/tools/agent/analyze.ts +3 -3
- package/src/mcp/tools/agent/build.ts +13 -21
- package/src/mcp/tools/agent/capture.ts +31 -115
- package/src/mcp/tools/agent/eval.ts +19 -155
- package/src/mcp/tools/agent/find.ts +33 -36
- package/src/mcp/tools/agent/proof.ts +87 -160
- package/src/mcp/tools/agent/propose.ts +93 -85
- package/src/mcp/tools/agent/replay.ts +32 -42
- package/src/mcp/tools/agent/session.ts +4 -5
- package/src/mcp/tools/agent/shared.ts +152 -0
- package/src/old/client.ts +60 -0
- package/src/old/login-cdp.ts +29 -23
- package/src/old/sdk-client.ts +105 -0
- package/src/old/to-register.ts +102 -6
- package/src/old/tools/authenticate.ts +3 -11
- package/src/old/tools/build.ts +12 -0
- package/src/old/tools/get-provider-info.ts +91 -0
- package/src/old/tools/get-version-config.ts +56 -0
- package/src/old/tools/publish.ts +264 -1
- package/src/old/tools/update-provider-metadata.ts +72 -0
- package/src/proof/attestor.ts +16 -4
- package/src/proof/debug-response-dump.ts +70 -0
- package/src/proof/patch-domhandler.ts +68 -0
- package/src/proof/zk-files.ts +7 -6
- package/src/provider/dom-xpath.ts +225 -0
- package/src/provider/draft.ts +168 -35
- package/src/provider/index.ts +8 -0
- package/src/provider/injection-doc.ts +6 -0
- package/src/provider/matcher.ts +477 -357
- package/src/provider/schema.ts +7 -5
- package/src/tools/registry.ts +92 -0
package/lib/provider/schema.d.ts
CHANGED
|
@@ -19,11 +19,13 @@ export type ResponseMatch = HttpParams['responseMatches'][number];
|
|
|
19
19
|
*/
|
|
20
20
|
export type ResponseRedaction = NonNullable<HttpParams['responseRedactions']>[number];
|
|
21
21
|
/**
|
|
22
|
-
* OPRF mode for hashed redactions
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* OPRF mode for hashed redactions:
|
|
23
|
+
* - `oprf-mpc` — TEE-based MPC OPRF. The default for the cloud/hosted flow: a
|
|
24
|
+
* TEE attestor accepts (and expects) this and REJECTS `oprf-raw`.
|
|
25
|
+
* - `oprf` — client-side ZK OPRF via gnark (needs the gnark deps).
|
|
26
|
+
* - `oprf-raw` — server-side OPRF, no external deps; the local dev-loop
|
|
27
|
+
* default (Windows / non-TEE). Weakest of the three (the attestor
|
|
28
|
+
* participates in hashing) and not accepted by a TEE attestor.
|
|
27
29
|
*/
|
|
28
30
|
export type OprfMode = NonNullable<ResponseRedaction['hash']>;
|
|
29
31
|
/** A single captured request + response, as we store it server-side. */
|
package/lib/tools/registry.d.ts
CHANGED
|
@@ -34,3 +34,17 @@ export interface RegisteredTool {
|
|
|
34
34
|
* and the validation applied to inbound args cannot drift.
|
|
35
35
|
*/
|
|
36
36
|
export declare function defineTool<TArgs>(definition: ToolDefinition, handler: (args: TArgs) => Promise<unknown>): RegisteredTool;
|
|
37
|
+
/**
|
|
38
|
+
* Adapt a tool to a host by WIDENING its input schema — add properties, mark
|
|
39
|
+
* some required, optionally override the description — without touching the
|
|
40
|
+
* handler. A host maps whichever shared tools it needs through this (e.g. the
|
|
41
|
+
* MCP adds its `captureId` session handle + the credential-aware `run_proof`
|
|
42
|
+
* contract). Args parse loosely, so the extra fields flow through to the
|
|
43
|
+
* handler (its backend resolver reads them); the base tool stays untouched for
|
|
44
|
+
* hosts that don't need them.
|
|
45
|
+
*/
|
|
46
|
+
export declare function extendTool(tool: RegisteredTool, extra: {
|
|
47
|
+
description?: string;
|
|
48
|
+
properties?: Record<string, unknown>;
|
|
49
|
+
required?: string[];
|
|
50
|
+
}): RegisteredTool;
|
package/lib/tools/registry.js
CHANGED
|
@@ -15,3 +15,35 @@ export function defineTool(definition, handler) {
|
|
|
15
15
|
},
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Adapt a tool to a host by WIDENING its input schema — add properties, mark
|
|
20
|
+
* some required, optionally override the description — without touching the
|
|
21
|
+
* handler. A host maps whichever shared tools it needs through this (e.g. the
|
|
22
|
+
* MCP adds its `captureId` session handle + the credential-aware `run_proof`
|
|
23
|
+
* contract). Args parse loosely, so the extra fields flow through to the
|
|
24
|
+
* handler (its backend resolver reads them); the base tool stays untouched for
|
|
25
|
+
* hosts that don't need them.
|
|
26
|
+
*/
|
|
27
|
+
export function extendTool(tool, extra) {
|
|
28
|
+
const base = tool.definition;
|
|
29
|
+
const required = [
|
|
30
|
+
...(base.inputSchema.required ?? []),
|
|
31
|
+
...(extra.required ?? []),
|
|
32
|
+
];
|
|
33
|
+
const inputSchema = {
|
|
34
|
+
...base.inputSchema,
|
|
35
|
+
properties: {
|
|
36
|
+
...base.inputSchema.properties,
|
|
37
|
+
...extra.properties,
|
|
38
|
+
},
|
|
39
|
+
...(required.length ? { required } : {}),
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
definition: {
|
|
43
|
+
name: base.name,
|
|
44
|
+
description: extra.description ?? base.description,
|
|
45
|
+
inputSchema,
|
|
46
|
+
},
|
|
47
|
+
handler: tool.handler,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reclaimprotocol/agent",
|
|
3
3
|
"title": "Reclaim Agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7",
|
|
5
5
|
"description": "Reclaim Protocol agent — SDK, CLI, and MCP server in one package. SDK + CLI talk to the builder backend; MCP server adds Chrome CDP capture, provider synthesis, replay, and zkTLS proof.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"types": "./lib/index.d.ts",
|
|
12
12
|
"import": "./lib/index.js"
|
|
13
13
|
},
|
|
14
|
+
"./authoring": {
|
|
15
|
+
"types": "./lib/authoring/index.d.ts",
|
|
16
|
+
"import": "./lib/authoring/index.js"
|
|
17
|
+
},
|
|
14
18
|
"./provider": {
|
|
15
19
|
"types": "./lib/provider/index.d.ts",
|
|
16
20
|
"import": "./lib/provider/index.js"
|
|
@@ -83,7 +87,11 @@
|
|
|
83
87
|
"@reclaimprotocol/client": "*",
|
|
84
88
|
"@reclaimprotocol/tls": "^0.1.3",
|
|
85
89
|
"chrome-remote-interface": "^0.33.0",
|
|
90
|
+
"domhandler": "^6.0.0",
|
|
91
|
+
"parse5": "^8.0.0",
|
|
92
|
+
"parse5-htmlparser2-tree-adapter": "^8.0.0",
|
|
86
93
|
"pino": "^9.14.0",
|
|
94
|
+
"xpath": "^0.0.34",
|
|
87
95
|
"zod": "^4.4.3"
|
|
88
96
|
},
|
|
89
97
|
"devDependencies": {
|
package/skill/SKILL.builder.md
CHANGED
|
@@ -32,9 +32,9 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
|
|
|
32
32
|
|
|
33
33
|
Re-call with the chosen `attachMode`.
|
|
34
34
|
|
|
35
|
-
2. **Pick a tab and
|
|
35
|
+
2. **Pick a tab and start a capture.** Call `list_tabs` and pick a tab (if one is already on the right origin use it; otherwise the first is usually fine). Then call `start_capture(tabId)` — it returns a **`captureId`** that is your session handle for the rest of the flow: `navigate`, `eval_in_page`, `wait_for_page`, `get_cookies`, `list_requests`, `find_requests_containing`, `propose_provider` all take it as their first argument. Capturing from the start is harmless (it just also records the login navigation). Then `navigate(captureId, url)` to the site you inferred from the dev's request (e.g. "GitHub username" → `https://github.com`). Don't ask the dev to navigate.
|
|
36
36
|
|
|
37
|
-
3. **Check login state, wait if needed.** Use `eval_in_page(
|
|
37
|
+
3. **Check login state, wait if needed.** Use `eval_in_page(captureId, ...)` to evaluate a site-specific check.
|
|
38
38
|
|
|
39
39
|
**Combine a positive signal (logged-in-only element present) with a negative signal (login button absent), AND them together.** Single signals are too prone to false positives — most notably, `<meta>` tags often exist on every page regardless of login state; only their `.content` attribute is populated. The check `!!document.querySelector('meta[name="user-login"]')` returns true even when logged out — DO NOT use existence-only checks on meta tags or other persistent-but-empty elements.
|
|
40
40
|
|
|
@@ -45,13 +45,13 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
|
|
|
45
45
|
|
|
46
46
|
If the result is truthy, skip to step 4.
|
|
47
47
|
|
|
48
|
-
If falsy: **navigate to the site's login URL yourself first**, so the dev lands directly on the login form instead of having to find it. Common patterns: `https://<host>/login`, `https://<host>/signin`, `https://<host>/i/flow/login` (Twitter/X). Pick the one you know works for the site. Call `navigate(
|
|
48
|
+
If falsy: **navigate to the site's login URL yourself first**, so the dev lands directly on the login form instead of having to find it. Common patterns: `https://<host>/login`, `https://<host>/signin`, `https://<host>/i/flow/login` (Twitter/X). Pick the one you know works for the site. Call `navigate(captureId, "<login-url>")`. Then tell the dev once: "I've opened the login page on `<site>` — please log in (with 2FA if needed). I'll detect when you're done." Then call `wait_for_page(captureId, expression="<same composite expression>", timeoutMs=180000)`. The MCP polls every second and returns when the expression goes truthy. **Do not** instruct the dev to "tell me when you're done" — just wait via the tool.
|
|
49
49
|
|
|
50
50
|
**Self-check before trusting the result:** if your detector says "already logged in" on a freshly-launched Chrome profile (cookies-clean), the detector is wrong. Brand-new profile means brand-new cookie jar — there's no way to be already authenticated. If you see that contradiction, your check is matching something that's present regardless of login (a meta tag, a generic class). Rewrite the expression and re-evaluate.
|
|
51
51
|
|
|
52
|
-
4. **Navigate to the page that displays the value.** Use what you know about the site to construct the URL. If you need the dev's username, eval it (e.g. on GitHub: `document.querySelector('meta[name="user-login"]')?.content`). Then `navigate(
|
|
52
|
+
4. **Navigate to the page that displays the value.** Use what you know about the site to construct the URL. If you need the dev's username, eval it (e.g. on GitHub: `document.querySelector('meta[name="user-login"]')?.content`). Then `navigate(captureId, url)`. Don't ask the dev to navigate.
|
|
53
53
|
|
|
54
|
-
5. **
|
|
54
|
+
5. **Trigger the request.** Capture has been live since step 2, so just make the relevant API call fire: issue a reload with `eval_in_page(captureId, "location.reload()")`. For some pages you may need to click a specific element or call a SPA route — use `eval_in_page` to do that programmatically rather than asking the dev to click.
|
|
55
55
|
|
|
56
56
|
6. **Identify the value from the DOM.** `eval_in_page` to read the displayed value (e.g. the logged-in username from `meta[name="user-login"]`'s `content` attribute). If the value is unambiguous, proceed without asking. Only confirm with the dev when there are multiple plausible interpretations (e.g. the page shows both a display name "John Doe" and a handle "@jdoe" and you need to pick which to prove).
|
|
57
57
|
|
|
@@ -83,11 +83,24 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
|
|
|
83
83
|
|
|
84
84
|
**Hard constraint:** OPRF-hashed values are capped at **62 bytes**. If the value is longer and the dev picks any hash, surface that and propose either a tighter `target` (e.g. just the email local-part) or no hash. `propose_provider` will return a clean error if you try anyway.
|
|
85
85
|
|
|
86
|
-
9. **Draft.** Call `propose_provider(
|
|
86
|
+
9. **Draft.** Call `propose_provider`. It returns a **`draftId`** (the handle you pass to `replay_request` and `run_proof`), the secret-free `provider`, and `secretRefs`. The recipe **and the captured secret values** are held server-side under that `draftId` — the secret values never enter the transcript, so you carry the `draftId`, not the provider JSON, into replay/prove.
|
|
87
|
+
|
|
88
|
+
**Extracting MORE THAN ONE value?** Prefer a **single request whose response contains ALL of them**, and draft them together — the provider gets one `responseMatches` + `responseRedactions` entry per value. First locate that request: run `find_requests_containing` for each value and pick a `requestId` that appears for all. Then pass a `targets` array — one entry per value, each with its `value` (exactly as it appears) and a distinct `name` (its parameter):
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
propose_provider(captureId, requestId, name, targets: [
|
|
92
|
+
{ value: "Scratch-net", name: "username" },
|
|
93
|
+
{ value: "Alex", name: "name" },
|
|
94
|
+
])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For a single value, the shorthand `propose_provider(captureId, requestId, target, name, hash?)` still works (one target). Pass `hash` only if the dev opted in (per-target `hash` also works inside `targets`). Inspect the returned provider:
|
|
87
98
|
- Is the right header marked secret? (cookies, Authorization, X-\* tokens)
|
|
88
99
|
- Is `responseMatches[0].value` a `contains` with a `{{name}}` template (matching the captured value's structural context)?
|
|
89
100
|
- Is `paramValues[name]` set to the original captured value?
|
|
90
|
-
- Does `responseRedactions
|
|
101
|
+
- Does each `responseRedactions` entry carry an `xPath` or `jsonPath` anchor **plus** a named-capture `regex` that isolates just the value? The synthesizer adds the regex for BOTH cases — an HTML element reveal is the element markup and a jsonPath reveal is the surrounding `"key":"value"`, so neither is the bare value without the regex. (And if `hash` was requested, the regex also carries a `hash: oprf-…` field.)
|
|
102
|
+
|
|
103
|
+
**Provenance — match the structure, never the bare value.** The `contains` template and the redaction `regex` are always anchored on the value's surrounding context — the JSON key (`"login":"{{username}}"`) or the enclosing element (`<div class="handle">{{username}}</div>`), never just `{{username}}` or `>{{username}}<`. This ties the proof to *where the value legitimately appears*; a bare-value match would let a user surface the same string from any field they control. The synthesizer does this for you.
|
|
91
104
|
|
|
92
105
|
**Canonical drafted shape** (no hash):
|
|
93
106
|
|
|
@@ -96,7 +109,12 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
|
|
|
96
109
|
"responseMatches": [
|
|
97
110
|
{ "type": "contains", "value": "\"login\":\"{{username}}\"" }
|
|
98
111
|
],
|
|
99
|
-
"responseRedactions": [
|
|
112
|
+
"responseRedactions": [
|
|
113
|
+
{
|
|
114
|
+
"xPath": "//script[@id=\"client-env\"]",
|
|
115
|
+
"regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\""
|
|
116
|
+
}
|
|
117
|
+
],
|
|
100
118
|
"paramValues": { "username": "Scratch-net" }
|
|
101
119
|
}
|
|
102
120
|
```
|
|
@@ -147,20 +165,20 @@ replace with `{{context.<name>}}`. E.g. `https://github.com/Scratch-net/repos`
|
|
|
147
165
|
|
|
148
166
|
`propose_provider` returns `verificationWarnings` for the calls only an author can make (currently: a captured value baked literally into the URL, needing the `{{context.<name>}}`-vs-bare-vs-re-capture decision). They are advisory, never blocking — **surface each warning to the dev verbatim** and resolve it before publishing.
|
|
149
167
|
|
|
150
|
-
11. **Replay.** Call `replay_request(
|
|
168
|
+
11. **Replay.** Call `replay_request(captureId, draftId)`. Three outcomes:
|
|
151
169
|
|
|
152
170
|
- `matchesOriginal: true` → proceed.
|
|
153
171
|
- `matchesOriginal: false` with `hints` mentioning auth/csrf → re-trigger the action yourself: navigate or reload via the tools, then loop to step 5. Don't ask the dev to redo it manually.
|
|
154
172
|
- `matchesOriginal: false` with stable diff (timestamps etc.) → tighten the matcher and retry.
|
|
155
173
|
|
|
156
|
-
**After replay succeeds, run
|
|
174
|
+
**After replay succeeds, run the authentication-bound check — this is mandatory.** Call `replay_request(captureId, draftId, withoutSecrets: true)`. This re-issues the request with NO secrets/cookies (an anonymous visitor).
|
|
157
175
|
|
|
158
|
-
- If
|
|
159
|
-
- If
|
|
176
|
+
- If it FAILS to extract (401/403/redirect, or the value is gone) → the endpoint is auth-bound. Good — the proof genuinely means "the holder of these secrets saw this value."
|
|
177
|
+
- If it STILL extracts the value → the endpoint is **PUBLIC**: anyone gets the same value with no login, so the proof binds to nobody. **Do NOT prove it.** Find a request whose response *depends on the session* (an API keyed to the logged-in user, e.g. `/settings/*` or a `viewer`/`currentUser` API), and re-check. If a value only ever appears on public responses, tell the dev it can't be proven as an identity claim rather than proving a public one.
|
|
160
178
|
|
|
161
|
-
12. **Prove.** Call `run_proof(
|
|
179
|
+
12. **Prove.** Call `run_proof(captureId, draftId, ownerAddress)`. On `verified: true`, the full claim has been written to `~/.reclaim/claims/<timestamp>.json` and the tool returns `{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`. The `claim` field is a display-ready version of the proof — its huge `request` payload has been stripped and all byte arrays converted to base64, so it renders cleanly. **Show it to the dev as a fenced JSON code block at the end** along with the provider JSON. That's the final result. On `verified: false`, diagnose using the returned error and iterate.
|
|
162
180
|
|
|
163
|
-
**Missing signing credential (`errorKind: "missing-owner-key"`)** — the dev hasn't set up an eth proof-owner key yet. Tell them what's happening, then drive the bootstrap yourself: call `resolve_owner_key`; if it returns no key, ask once _"shall I mint a fresh eth key via `issue_credentials` (or `import_credentials` if you already hold one)? It's written to the project `.env` (mode 0600, gitignored) for this and future sessions."_; on yes, call `issue_credentials` (or `import_credentials`) and retry the same `run_proof` call. **No restart needed** — the key is read live from `.env` on each `run_proof`, and the existing `
|
|
181
|
+
**Missing signing credential (`errorKind: "missing-owner-key"`)** — the dev hasn't set up an eth proof-owner key yet. Tell them what's happening, then drive the bootstrap yourself: call `resolve_owner_key`; if it returns no key, ask once _"shall I mint a fresh eth key via `issue_credentials` (or `import_credentials` if you already hold one)? It's written to the project `.env` (mode 0600, gitignored) for this and future sessions."_; on yes, call `issue_credentials` (or `import_credentials`) and retry the same `run_proof` call. **No restart needed** — the key is read live from `.env` on each `run_proof`, and the existing `draftId` from `propose_provider` is still valid.
|
|
164
182
|
|
|
165
183
|
**On any `verified: false` response, the result includes (inline):**
|
|
166
184
|
- `error` — the SDK's thrown error message verbatim
|
|
@@ -181,6 +199,7 @@ replace with `{{context.<name>}}`. E.g. `https://github.com/Scratch-net/repos`
|
|
|
181
199
|
|
|
182
200
|
1. **Create the provider if it doesn't exist:** `create_provider(orgId, domain, slug, title, description)` — find `orgId` via `list_orgs`. `domain` e.g. `github.com`; `slug` e.g. `github-username`; `title` e.g. "GitHub Username". Starts PRIVATE with an empty seed version. Skip if the dev already has a `providerId`.
|
|
183
201
|
2. **Persist the version:** `create_provider_version_from_capture(providerId, provider, initialUrl, notes)` — `notes` e.g. `"Initial draft from browser capture"`. The version lands on the `draft` branch.
|
|
202
|
+
- **Multi-request provider** (e.g. a landing-page call plus a details call, or a CSRF-preflight/refresh-token chain): run steps 5–12 ONCE PER REQUEST — a separate `propose_provider` → `replay_request` → `run_proof` cycle for each — then pass `providers: [draftA, draftB, ...]` instead of the single-draft `provider` in this same call. Every entry becomes its own request in the version, and all are required at verification time.
|
|
184
203
|
3. **`requiredContext` is derived for you.** The publish tool scans the recipe's url/body for `{{context.<name>}}` placeholders (step 10) and emits a JSON Schema requiring each one — the Builder then validates every session-create's `context` against it (a clear 400 instead of a late attestor failure). Extra context fields are always allowed. The derived properties are plain strings; to refine types or add human-readable descriptions, call `update_provider_version` with a `requiredContext` override while the version is on `draft`, e.g.:
|
|
185
204
|
|
|
186
205
|
```json
|
|
@@ -200,10 +219,10 @@ When `analyze_request_constraints` returns concerns, repeat each `message` to th
|
|
|
200
219
|
- Never echo a raw `Authorization`, `Cookie`, or `X-*-Token` value back to the dev or write one to any file. The MCP guarantees these don't appear in tool responses — keep that guarantee.
|
|
201
220
|
- Never call `run_proof` before `replay_request` succeeds. Attestor calls are expensive.
|
|
202
221
|
- Never pretend a proof succeeded when `verified: false` came back. Show the error and iterate.
|
|
203
|
-
- **Never hand-craft `responseMatches` or `responseRedactions`.** If you find yourself typing a regex by hand, OR writing `(?<name>...)`-style patterns by hand, OR copying the captured value literally into the match — stop. The synthesizer in `propose_provider` already produces the canonical shape: `contains` template + paramValues for the match, xPath/jsonPath
|
|
222
|
+
- **Never hand-craft `responseMatches` or `responseRedactions`.** If you find yourself typing a regex by hand, OR writing `(?<name>...)`-style patterns by hand, OR copying the captured value literally into the match — stop. The synthesizer in `propose_provider` already produces the canonical shape: `contains` template + paramValues for the match, xPath/jsonPath for the redaction — plus a value-isolating named-capture `regex` (for every xPath/jsonPath anchor — the reveal is a byte range, never the bare value). The named-capture regex in the redaction is the synthesizer's job, not yours.
|
|
204
223
|
- Never write a literal value into `responseMatches.value` — the synthesizer always emits `{{name}}` templates with paramValues. A literal value bakes the original user into the provider.
|
|
205
224
|
- Never pass `hash` to `propose_provider` for a free-form (unicode / spaces / punctuation) target — the synthesizer will error, and the right move is either to use a tighter `target` or accept cleartext (no `hash`).
|
|
206
|
-
- Never accept "the synthesizer's xPath looks too narrow, let me write a regex" — the xPath
|
|
225
|
+
- Never accept "the synthesizer's xPath looks too narrow, let me write a regex" — the synthesizer already pairs the xPath with a structure-anchored named-capture regex (for HTML) that both reveals the element and isolates the value. If extraction still looks wrong, re-run `propose_provider` with a better request/target — don't hand-write patterns.
|
|
207
226
|
|
|
208
227
|
## Common first-time issues — surface these proactively
|
|
209
228
|
|
|
@@ -220,21 +239,21 @@ If `attach_chrome` errors, times out, or `waitForCdpReady` reports the port isn'
|
|
|
220
239
|
**Credential for signing the proof.** `run_proof` requires an `ownerAddress` — the eth proof-owner key whose private half signs the claim. It does **no** resolution itself; get the address from `resolve_owner_key` first. The private key is resolved locally (`RECLAIM_PRIVATE_KEY` env → a `RECLAIM_PRIVATE_KEY=0x…` line in the project `.env` → a file at `RECLAIM_PRIVATE_KEY_FILE`) and never enters the conversation.
|
|
221
240
|
|
|
222
241
|
1. **Call `resolve_owner_key`.** It checks `RECLAIM_PRIVATE_KEY` (MCP env) → the project `.env` → `RECLAIM_PRIVATE_KEY_FILE`, and returns only the derived address.
|
|
223
|
-
- `{ ready: true, address }` → call `run_proof(
|
|
242
|
+
- `{ ready: true, address }` → call `run_proof(captureId, draftId, address)`. No prompts.
|
|
224
243
|
- `{ ready: false }` → no key exists yet. Ask the dev once how to set one up:
|
|
225
244
|
- **Provide their own key** → `import_credentials({ privateKey: "0x…" })` (also accepts a `keyFile` path).
|
|
226
245
|
- **Create a new one** → `issue_credentials({})`. (Fine default for test proofs — owner identity doesn't matter.)
|
|
227
246
|
- Either way the key is written to the project `.env` (mode 0600; `.env` is added to `.gitignore`) — never to `~/.reclaim`.
|
|
228
247
|
- Then call `resolve_owner_key` again and `run_proof` with the returned address.
|
|
229
248
|
2. All fully local — no network, no server registration. The key is read live from `.env` on each `run_proof`, so no restart is needed after issuing/importing.
|
|
230
|
-
- **Advanced:** setting `RECLAIM_PRIVATE_KEY` directly in the MCP server's env also works, but env vars are snapshotted at Claude Code startup, so that route needs a full restart (which invalidates every `
|
|
249
|
+
- **Advanced:** setting `RECLAIM_PRIVATE_KEY` directly in the MCP server's env also works, but env vars are snapshotted at Claude Code startup, so that route needs a full restart (which invalidates every `draftId`). The `.env` flow above avoids that — prefer it.
|
|
231
250
|
- **Multiple MCP config files exist on Windows.** `~/.claude.json` takes precedence over `~/.claude/mcp.json`. When advising config edits, point them at the right file or tell them to use `claude mcp list` / `claude mcp add --scope user` rather than editing JSON by hand.
|
|
232
251
|
|
|
233
|
-
If `replay_request` or `run_proof` errors with `unknown
|
|
252
|
+
If `replay_request` or `run_proof` errors with `unknown draftId` (or `no capture`):
|
|
234
253
|
|
|
235
|
-
- **The
|
|
236
|
-
- **
|
|
254
|
+
- **The draft is bound to its capture, held in the MCP server process.** Anything that restarts the server (Claude Code restart, `reset_session`, machine reboot) drops every capture and its drafts. The fix is always: re-run `start_capture` → interact → `propose_provider` to mint a fresh `draftId`. Don't ask the dev to "re-enter the value" — walk them back through capture.
|
|
255
|
+
- **Auth-bound check:** to replay/prove the request as an unauthenticated visitor (no secret headers), pass `withoutSecrets: true` to `replay_request` — the draft still carries the captured secrets, so you never re-supply them.
|
|
237
256
|
|
|
238
|
-
##
|
|
257
|
+
## Recognizing a HAWKEYE / live-interception provider
|
|
239
258
|
|
|
240
|
-
|
|
259
|
+
Everything above builds the STANDARD flow: capture → draft → replay → prove. There is a second flow — `webSettings.inapp.interceptorOptions.interceptorType: HAWKEYE` (or `MSWJS`/`CDP`) plus a `jsUserScripts` script that calls `window.reclaimInterceptor.addResponseMiddleware(...)` to observe the page's REAL responses as they happen, instead of an independent replay. `create_provider_version_from_capture` never sets `inapp` at all, so you will not author this flow with the current tools — but you may see these fields on a version someone else published outside this flow. `isDocumentRequestReplayEnabled` is a NARROWER, separate toggle from the interception itself — it does not gate `addResponseMiddleware` in general (that fires for every `interceptorType` including `CDP`); it only controls whether the current page's own document/navigation request specifically gets pulled through a non-CDP interceptor, as a site-compatibility escape hatch. See `injection-api.md` (embedded in the `jsUserScripts` field description) for the source-verified breakdown before guessing at what they mean.
|
package/skill/SKILL.old.md
CHANGED
|
@@ -170,7 +170,11 @@ Call `reclaim_authenticate({})` with no arguments. It opens the Reclaim dashboar
|
|
|
170
170
|
Call `create_provider_version_from_capture`:
|
|
171
171
|
- **New provider:** omit `providerId`. It registers a new provider (version 1.0.0). Pass `provider`, `initialUrl`, and optionally `description`, `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
|
|
172
172
|
- **New version of an existing provider:** pass its `providerId` — the patch is auto-bumped above the highest existing version. Pass `notes` (≥10 chars) as the change note.
|
|
173
|
+
- **Multi-request provider** (e.g. a landing-page call plus a details call, or a CSRF-preflight/refresh-token chain): run steps 5–12 ONCE PER REQUEST — a separate `propose_provider` → `replay_request` → `run_proof` cycle for each — then publish all of them together in ONE call by passing `providers: [draftA, draftB, ...]` instead of the single-draft `provider`. Every entry becomes its own parallel `requestData` entry on the SAME version, and all are required at verification time.
|
|
173
174
|
- `get_me_providers({ searchQuery? })` lists the providers you own — use it to find a `providerId` or to confirm the result. It's the only provider-listing tool in this mode.
|
|
175
|
+
- **Before adding a version to an EXISTING provider, call `get_provider_version_config(providerId)` first.** This backend has no partial-patch endpoint — a version replaces the WHOLE `providerConfig` — so seeing what's currently live (`customInjection`, `userAgent`, `pageTitle`, `stepsToFollow`, etc.) tells you what you're about to change vs. carry forward before you commit to it, rather than finding out after. (`create_provider_version_from_capture` carries forward anything you omit automatically, but knowing what that IS before publishing beats being surprised after.)
|
|
176
|
+
- **To rename, redescribe, retag, flip `providerType`, or toggle `isActive` on a provider you own — WITHOUT creating a new version** — use `update_provider_metadata(providerId, {...})` instead. It's a genuine partial update on the provider itself; don't route a pure metadata edit through `create_provider_version_from_capture`.
|
|
177
|
+
- **To look up any published provider (not just ones you own), or to see exactly what the live verification client would fetch** — use `get_provider_info(providerId, versionNumber?)`. It reads the public production SDK backend directly (no auth needed), separate from everything else in this mode.
|
|
174
178
|
- There's no separate "create-then-version" step and no org / public-review workflow here.
|
|
175
179
|
- Consumer-supplied params (step 10) keep the `{{context_<name>}}` form in this mode; there is no `requiredContext` declaration on the legacy backend.
|
|
176
180
|
|
|
@@ -220,6 +224,6 @@ If `replay_request` or `run_proof` errors with `unknown secretParamsRef`:
|
|
|
220
224
|
- **The ref is bound to the MCP server process.** Anything that restarts the server (Claude Code restart, `reset_session`, machine reboot) invalidates every previously-issued ref. The fix is always: re-run `start_capture` → interact → `propose_provider` to mint a fresh ref. Don't ask the dev to "re-enter the value" — walk them back through capture.
|
|
221
225
|
- **Exception:** if the drafted provider has empty `secretRefs: []` (no secret headers were captured), you can call `replay_request` and `run_proof` without `secretParamsRef` at all — both tools accept it as optional in that case.
|
|
222
226
|
|
|
223
|
-
##
|
|
227
|
+
## Recognizing a HAWKEYE / live-interception provider
|
|
224
228
|
|
|
225
|
-
|
|
229
|
+
Everything above builds the STANDARD flow: capture → draft → replay (the backend independently re-sends the request) → prove. There is a second, separate flow — `injectionType: HAWKEYE` (or `MSWJS`/`XHOOK`/`CDP`) plus a `customInjection` script that calls `window.reclaimInterceptor.addResponseMiddleware(...)` to observe the page's REAL responses as they happen, instead of the backend replaying anything. You will not author this flow with the current tools — `create_provider_version_from_capture` only builds the standard one. But you WILL see the fields when reading an existing provider (`get_provider_version_config`, `get_provider_info`): don't assume `injectionType` present-but-not-`NONE` means something is broken — the old backend's own schema DEFAULTS `injectionType` to `HAWKEYE` for every provider, standard-flow ones included; it's inert with no interceptor script installed. `disableRequestReplay` is a NARROWER, separate toggle — it does not gate interception in general (that fires regardless, for every `injectionType` including `CDP`); it only controls whether the current page's own document/navigation request specifically gets pulled through a non-CDP interceptor, and exists as a site-compatibility escape hatch, not an architecture choice. See `injection-api.md` (embedded in the `customInjection` field description) for the full, source-verified breakdown of `injectionType`/`disableRequestReplay`/`bodySniff` before guessing at any of these three.
|
package/skill/builder-mode.md
CHANGED
|
@@ -2,7 +2,7 @@ This server targets the Reclaim BUILDER backend. It exposes three tool layers: g
|
|
|
2
2
|
|
|
3
3
|
**Authentication:** try `get_me` first. On 401, `authenticate({})` opens a device-pairing session — give the user the `attachUrl`, then poll `authenticate({ code })` until LINKED. Tokens are cached ~2 days.
|
|
4
4
|
|
|
5
|
-
**Provider-authoring flow:** `attach_chrome` → `start_capture` → drive the page (`navigate`, `wait_for_page`) → inspect (`list_requests`, `get_request`, `find_requests_containing`) → `propose_provider` → `replay_request` → `run_proof` → resolve a `providerId` (**Reuse before create**, below) → `create_provider_version_from_capture` (lands on the draft branch) → `publish_provider_version` (PRIVATE) or `submit_provider_version_for_review` (PUBLIC).
|
|
5
|
+
**Provider-authoring flow:** `attach_chrome` → `start_capture(tabId)` ⇒ a **`captureId`** (the session handle every browser/inspect tool takes as its first arg) → drive the page (`navigate`, `wait_for_page`, `eval_in_page`, `get_cookies`) → inspect (`list_requests`, `get_request`, `find_requests_containing`) → `propose_provider` ⇒ a **`draftId`** (the recipe + captured secrets held server-side; secrets never enter the transcript) → `replay_request(captureId, draftId)` → `run_proof(captureId, draftId, ownerAddress)` → resolve a `providerId` (**Reuse before create**, below) → `create_provider_version_from_capture` (lands on the draft branch) → `publish_provider_version` (PRIVATE) or `submit_provider_version_for_review` (PUBLIC).
|
|
6
6
|
|
|
7
7
|
**Reuse before create (avoid duplicates):** `create_provider_version_from_capture` requires a `providerId`. Before minting a new one with `create_provider`, search your existing providers with `providers` (by domain / data point) — a site you've already built will be there. If one matches, pass its `providerId` so the capture lands as a NEW VERSION on that provider. Only `create_provider` (then use its returned id) when none exists. Re-running the flow against `create_provider` each time is what stacks duplicate providers in "My providers".
|
|
8
8
|
|
package/skill/injection-api.md
CHANGED
|
@@ -40,3 +40,120 @@ than guessing.
|
|
|
40
40
|
> provider's `initialUrl`), and old-devtools runs `customInjection` the same
|
|
41
41
|
> way. (`window.Reclaim` itself is still only forwarded/available in the cases
|
|
42
42
|
> noted above.)
|
|
43
|
+
|
|
44
|
+
## Interception mode: `injectionType` / `interceptorType`
|
|
45
|
+
|
|
46
|
+
Separate from the script's CONTENT is HOW the client captures network traffic
|
|
47
|
+
for it to consume — a per-version field alongside `customInjection`/
|
|
48
|
+
`jsUserScripts`, not something you write into the script itself. Whichever
|
|
49
|
+
mechanism is selected, the injected script consumes what it caught through
|
|
50
|
+
the SAME API: `window.reclaimInterceptor.addResponseMiddleware(middleware,
|
|
51
|
+
middlewareName?)`. That consumption API fires for any intercepted request
|
|
52
|
+
under EVERY mechanism below, including `CDP` — it is not a HAWKEYE-specific
|
|
53
|
+
thing.
|
|
54
|
+
|
|
55
|
+
- **old-devtools** field: `injectionType`, one of `NONE | MSWJS | XHOOK | CDP |
|
|
56
|
+
HAWKEYE`. Verified from the backend's Prisma schema: **the column default is
|
|
57
|
+
`HAWKEYE`**, not `NONE` — every provider `create_provider_version_from_capture`
|
|
58
|
+
registers gets `injectionType: HAWKEYE` implicitly, since neither this tool
|
|
59
|
+
nor the old backend's register/config controllers ever set it explicitly.
|
|
60
|
+
This is harmless for the normal capture→replay→prove flow: with no
|
|
61
|
+
interceptor script installed, there's nothing calling
|
|
62
|
+
`addResponseMiddleware`, so the standard flow behaves identically
|
|
63
|
+
regardless of this field's value.
|
|
64
|
+
- **builder** field: `webSettings.inapp.interceptorOptions.interceptorType`,
|
|
65
|
+
one of `HAWKEYE | MSWJS | CDP` (no `NONE`/`XHOOK` in this schema).
|
|
66
|
+
`create_provider_version_from_capture` (builder mode) never sets `inapp` at
|
|
67
|
+
all, so this is a no-op for the standard flow there too.
|
|
68
|
+
|
|
69
|
+
**The mechanisms, and why you'd pick one over plain replay:**
|
|
70
|
+
- **`CDP`** — interception at the browser/DevTools-protocol level, below the
|
|
71
|
+
page's own JS. Works uniformly for every request the page makes, INCLUDING
|
|
72
|
+
its own document/navigation load — no caveats, "works best."
|
|
73
|
+
- **`HAWKEYE` / `MSWJS` / `XHOOK`** — interception done IN the page's JS
|
|
74
|
+
context (different underlying libraries; same idea). These see the page's
|
|
75
|
+
ordinary sub-resource/API requests fine, but the page's OWN document
|
|
76
|
+
request is a special case — see `disableRequestReplay` below.
|
|
77
|
+
- Reach for ANY of the above (over the plain replay flow, `injectionType:
|
|
78
|
+
NONE` / no `inapp`) only when the data genuinely can't be reproduced by an
|
|
79
|
+
independent replay — e.g. it depends on one-time server-side state (a
|
|
80
|
+
nonce, a websocket push) that asking again wouldn't give you back. For
|
|
81
|
+
everything else, the standard flow is simpler and is what `propose_provider`
|
|
82
|
+
/ `run_proof` are built for.
|
|
83
|
+
|
|
84
|
+
## `disableRequestReplay` / `isDocumentRequestReplayEnabled`
|
|
85
|
+
|
|
86
|
+
**This does NOT gate `addResponseMiddleware` in general** — that fires for
|
|
87
|
+
any intercepted request regardless of this field. What it actually controls:
|
|
88
|
+
whether the CURRENT PAGE'S OWN document/navigation request (the request that
|
|
89
|
+
loads the page's HTML — not a sub-resource or API call) is sent for real
|
|
90
|
+
over the network at all, for the non-CDP mechanisms above. And it only
|
|
91
|
+
matters if the request you're proving *is* that document request — e.g. the
|
|
92
|
+
target value is embedded straight in the page's own HTML rather than
|
|
93
|
+
fetched via a separate API call.
|
|
94
|
+
|
|
95
|
+
- `false` (old-devtools' schema default) — the document request IS sent for
|
|
96
|
+
real (for `HAWKEYE`/`MSWJS`/`XHOOK`), so `addResponseMiddleware` sees it
|
|
97
|
+
like any other intercepted request. This is what you need if you're
|
|
98
|
+
proving data straight out of the current page's own HTML.
|
|
99
|
+
- `true` — the document request is constructed/synthesized locally instead
|
|
100
|
+
of actually being sent, so the interceptor never observes it (every OTHER
|
|
101
|
+
request the page makes is still intercepted normally). This exists as a
|
|
102
|
+
**compatibility escape hatch**: some websites misbehave when their own
|
|
103
|
+
top-level navigation request gets pulled through a JS-level interceptor —
|
|
104
|
+
flip this to `true` for those sites. It's not a privacy or architecture
|
|
105
|
+
choice, it's a per-site workaround.
|
|
106
|
+
- **`CDP` is exempt from all of this** — it intercepts at the browser level,
|
|
107
|
+
so the document request is never a special case for it.
|
|
108
|
+
`disableRequestReplay`/`isDocumentRequestReplayEnabled` is simply inert
|
|
109
|
+
when `injectionType`/`interceptorType` is `CDP`.
|
|
110
|
+
|
|
111
|
+
Field names, per backend:
|
|
112
|
+
- **old-devtools**: `disableRequestReplay` (boolean, schema default `false`).
|
|
113
|
+
The dashboard's own field hint (verbatim): *"When enabled (default), the
|
|
114
|
+
main document HTTP request is actually sent to get the response. When
|
|
115
|
+
disabled, the expected request and response are constructed locally
|
|
116
|
+
without making the network call."* Note the double negative: the
|
|
117
|
+
dashboard's checkbox is labeled "Enable Document Request Replay" and is
|
|
118
|
+
CHECKED when `disableRequestReplay` is `false` — the checkbox and the field
|
|
119
|
+
it writes are inverted from each other.
|
|
120
|
+
- **builder**: `webSettings.inapp.interceptorOptions.isDocumentRequestReplayEnabled`
|
|
121
|
+
(boolean, positive naming this time — `true` means the document request IS
|
|
122
|
+
sent). Per its openapi description, the schema's own stated default is
|
|
123
|
+
`false`. Don't assume the two backends' defaults line up just because the
|
|
124
|
+
concept is the same — old effectively defaults to "sent", the builder
|
|
125
|
+
schema's stated default is "not sent"; verify against whichever backend
|
|
126
|
+
you're publishing to rather than carrying an assumption across.
|
|
127
|
+
|
|
128
|
+
If you're not authoring a non-CDP interceptor script that specifically needs
|
|
129
|
+
to observe the current page's own document request, leave this alone (the
|
|
130
|
+
default already matches the standard flow on old-devtools; on builder, this
|
|
131
|
+
field isn't touched by `create_provider_version_from_capture` at all).
|
|
132
|
+
|
|
133
|
+
## `bodySniff` — unrelated to the above
|
|
134
|
+
|
|
135
|
+
A per-request field (sits next to `url`/`method` on each `requestData` entry,
|
|
136
|
+
not a version-level setting): `{ enabled: boolean, template: string }`. Per
|
|
137
|
+
the InApp SDK's own type comment, it's "the template used for matching and
|
|
138
|
+
extracting params from `requestBody`" — the REQUEST-body counterpart of
|
|
139
|
+
`responseMatches`/`responseRedactions`, which only ever look at the response.
|
|
140
|
+
It applies regardless of `injectionType`/`interceptorType`. Both publish
|
|
141
|
+
tools in this package already set it for you from `provider.body` — you
|
|
142
|
+
should not need to touch it directly.
|
|
143
|
+
|
|
144
|
+
## Recognizing this on a provider you didn't author
|
|
145
|
+
|
|
146
|
+
`get_provider_version_config` (old-devtools) and `get_provider_info` (public
|
|
147
|
+
SDK backend) both return the raw `providerConfig`. If you see
|
|
148
|
+
`injectionType`/`interceptorType` set to anything other than the inert
|
|
149
|
+
default AND a `customInjection`/`jsUserScripts` that calls
|
|
150
|
+
`reclaimInterceptor.addResponseMiddleware` (or references `MSWJS`/`XHook`),
|
|
151
|
+
that provider is on the live-interception path, not the standard replay
|
|
152
|
+
flow — read the script before assuming you can safely carry it forward or
|
|
153
|
+
edit around it. If, on top of that, the request being proven IS the login/
|
|
154
|
+
landing page itself (its `url` matches `loginUrl`, i.e. it's the document
|
|
155
|
+
request, not a separate API call) and `injectionType`/`interceptorType`
|
|
156
|
+
isn't `CDP`, also check `disableRequestReplay`/
|
|
157
|
+
`isDocumentRequestReplayEnabled` — `true` there means that request was
|
|
158
|
+
deliberately routed around live interception for site-compatibility
|
|
159
|
+
reasons, not an oversight.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend seam for the authoring tools. The tool LOGIC (drive the browser →
|
|
3
|
+
* capture → propose → replay → prove) is written once against this interface;
|
|
4
|
+
* each environment supplies a backend:
|
|
5
|
+
* - local (MCP): a Chrome tab over CDP + the in-process attestor SDK.
|
|
6
|
+
* - remote (cloud): the user's Popcorn browser over CDP + the TEE prover.
|
|
7
|
+
*
|
|
8
|
+
* One backend instance == one live session (a page + its capture). Multi
|
|
9
|
+
* session falls out of instancing: the MCP resolves a backend per tab/capture,
|
|
10
|
+
* the cloud one per run — so the shared tools never carry a session id and hold
|
|
11
|
+
* no cross-session state.
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from 'node:crypto'
|
|
14
|
+
import type { CaptureSession } from '../cdp/capture-engine.ts'
|
|
15
|
+
import type { Extraction } from '../provider/draft.ts'
|
|
16
|
+
import type { ReclaimProvider, SecretParamRef } from '../provider/schema.ts'
|
|
17
|
+
|
|
18
|
+
/** Secret request params (cookie/auth headers), materialized only for the
|
|
19
|
+
* replay/prove call — never surfaced in tool output. */
|
|
20
|
+
export interface SecretParams {
|
|
21
|
+
headers: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A drafted recipe held server-side so the model only handles an opaque
|
|
25
|
+
* `draftId` — the provider is secret-free but the captured secrets never enter
|
|
26
|
+
* the transcript. Shared by propose → replay → run_proof. */
|
|
27
|
+
export interface Draft {
|
|
28
|
+
provider: ReclaimProvider
|
|
29
|
+
secretRefs: SecretParamRef[]
|
|
30
|
+
/** Real secret header values (cookie/auth) for replay/prove — never returned
|
|
31
|
+
* to the model. */
|
|
32
|
+
secrets: Record<string, string>
|
|
33
|
+
/** The captured request this draft was proposed from. */
|
|
34
|
+
requestId: string
|
|
35
|
+
/** One entry per extracted value (value + its redaction), so replay can
|
|
36
|
+
* validate each against its own selector. */
|
|
37
|
+
extractions: Extraction[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Per-session draft store: `propose` puts, `replay`/`run_proof` get by id. */
|
|
41
|
+
export interface DraftStore {
|
|
42
|
+
put(draft: Draft): string
|
|
43
|
+
get(id: string): Draft | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function makeDraftStore(): DraftStore {
|
|
47
|
+
const map = new Map<string, Draft>()
|
|
48
|
+
return {
|
|
49
|
+
put: (draft) => {
|
|
50
|
+
const id = `draft_${randomUUID()}`
|
|
51
|
+
map.set(id, draft)
|
|
52
|
+
return id
|
|
53
|
+
},
|
|
54
|
+
get: (id) => map.get(id),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Options for a proof run. `ownerAddress` selects the local signing credential
|
|
59
|
+
* and `attestorUrl` overrides the attestor endpoint (both MCP-only); the
|
|
60
|
+
* remote TEE backend ignores them (it signs in-enclave, own attestor). */
|
|
61
|
+
export interface ProveOptions {
|
|
62
|
+
ownerAddress?: string
|
|
63
|
+
attestorUrl?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Everything the shared authoring tools need from one live session. Browser ops
|
|
68
|
+
* go through `sendCdp` (+ `waitForEvent` for load-style waits); capture reads
|
|
69
|
+
* use `capture`; proof/replay are whole operations because they genuinely
|
|
70
|
+
* differ (local attestor + live re-fetch vs remote TEE) — each backend shapes
|
|
71
|
+
* its own result, passed straight through as the tool's output.
|
|
72
|
+
*/
|
|
73
|
+
export interface AuthoringBackend {
|
|
74
|
+
/** Send a raw CDP command scoped to this session's page. */
|
|
75
|
+
sendCdp(method: string, params?: Record<string, unknown>): Promise<unknown>
|
|
76
|
+
/** Resolve once `method` (a CDP event) fires on this session's page, or
|
|
77
|
+
* after `timeoutMs`. Used for `Page.loadEventFired`-style waits. */
|
|
78
|
+
waitForEvent(method: string, timeoutMs: number): Promise<'fired' | 'timeout'>
|
|
79
|
+
/** This session's captured requests (the shared capture engine). */
|
|
80
|
+
readonly capture: CaptureSession
|
|
81
|
+
/** This session's drafts (propose → replay/run_proof). */
|
|
82
|
+
readonly drafts: DraftStore
|
|
83
|
+
/** Prove a drafted recipe. Local: attestor SDK (rich claim view). Remote: TEE
|
|
84
|
+
* `/reclaim/prove` (lean, secret-free summary). Returns the environment's
|
|
85
|
+
* display shape. A backend may record the proven recipe internally (e.g.
|
|
86
|
+
* the cloud publish step) — that side effect is the backend's, not ours. */
|
|
87
|
+
prove(draft: Draft, opts?: ProveOptions): Promise<unknown>
|
|
88
|
+
/**
|
|
89
|
+
* Validate/replay a drafted recipe before proving. With `withoutSecrets` it
|
|
90
|
+
* is the AUTH-BOUND check: re-issue the request stripped of the captured
|
|
91
|
+
* secrets (local: a Node re-fetch without them; remote: an anonymous `fetch`
|
|
92
|
+
* in the Popcorn page, `credentials: 'omit'`) and report whether the values
|
|
93
|
+
* are STILL extractable — i.e. the endpoint is public. Without it, validate
|
|
94
|
+
* extraction for the authenticated request. The shape is environment-specific
|
|
95
|
+
* and passed straight through as the tool's result.
|
|
96
|
+
*/
|
|
97
|
+
replay(draft: Draft, opts?: { withoutSecrets?: boolean }): Promise<unknown>
|
|
98
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Shared authoring tools, backend-injectable. Consumed by the MCP server
|
|
2
|
+
// (local Chrome + attestor backend) and the cloud loop (Popcorn + TEE backend).
|
|
3
|
+
export type {
|
|
4
|
+
AuthoringBackend,
|
|
5
|
+
Draft,
|
|
6
|
+
DraftStore,
|
|
7
|
+
ProveOptions,
|
|
8
|
+
SecretParams,
|
|
9
|
+
} from './backend.ts'
|
|
10
|
+
export { makeDraftStore } from './backend.ts'
|
|
11
|
+
export { authoringTools } from './tools.ts'
|
|
12
|
+
// The shared tool primitive — re-exported so consumers (the cloud loop) get the
|
|
13
|
+
// RegisteredTool type/`defineTool`/`extendTool` without importing the MCP
|
|
14
|
+
// subsystem.
|
|
15
|
+
export {
|
|
16
|
+
defineTool,
|
|
17
|
+
extendTool,
|
|
18
|
+
type RegisteredTool,
|
|
19
|
+
type ToolDefinition,
|
|
20
|
+
} from '../tools/registry.ts'
|