@reclaimprotocol/agent 0.1.5 → 0.1.6

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.
Files changed (117) hide show
  1. package/lib/authoring/backend.d.ts +0 -24
  2. package/lib/authoring/find.d.ts +3 -0
  3. package/lib/authoring/find.js +54 -0
  4. package/lib/authoring/index.d.ts +3 -3
  5. package/lib/authoring/index.js +4 -3
  6. package/lib/authoring/propose.d.ts +3 -0
  7. package/lib/authoring/propose.js +109 -0
  8. package/lib/authoring/replay.d.ts +10 -0
  9. package/lib/authoring/replay.js +34 -0
  10. package/lib/authoring/tools.d.ts +13 -6
  11. package/lib/authoring/tools.js +108 -293
  12. package/lib/authoring/util.d.ts +27 -0
  13. package/lib/authoring/util.js +21 -0
  14. package/lib/consts.d.ts +6 -1
  15. package/lib/consts.js +9 -2
  16. package/lib/mcp/server.d.ts +2 -41
  17. package/lib/mcp/server.js +6 -34
  18. package/lib/mcp/start.js +4 -2
  19. package/lib/mcp/tools/agent/analyze.js +3 -3
  20. package/lib/mcp/tools/agent/build.d.ts +4 -4
  21. package/lib/mcp/tools/agent/build.js +13 -15
  22. package/lib/mcp/tools/agent/capture.d.ts +13 -1
  23. package/lib/mcp/tools/agent/capture.js +18 -85
  24. package/lib/mcp/tools/agent/eval.d.ts +5 -2
  25. package/lib/mcp/tools/agent/eval.js +11 -121
  26. package/lib/mcp/tools/agent/find.d.ts +10 -3
  27. package/lib/mcp/tools/agent/find.js +25 -31
  28. package/lib/mcp/tools/agent/proof.d.ts +13 -3
  29. package/lib/mcp/tools/agent/proof.js +81 -132
  30. package/lib/mcp/tools/agent/propose.d.ts +10 -9
  31. package/lib/mcp/tools/agent/propose.js +87 -64
  32. package/lib/mcp/tools/agent/replay.d.ts +10 -3
  33. package/lib/mcp/tools/agent/replay.js +29 -36
  34. package/lib/mcp/tools/agent/session.d.ts +1 -3
  35. package/lib/mcp/tools/agent/session.js +5 -5
  36. package/lib/mcp/tools/agent/shared.d.ts +5 -3
  37. package/lib/mcp/tools/agent/shared.js +29 -19
  38. package/lib/old/client.d.ts +35 -0
  39. package/lib/old/client.js +18 -0
  40. package/lib/old/login-cdp.d.ts +24 -5
  41. package/lib/old/login-cdp.js +29 -15
  42. package/lib/old/sdk-client.d.ts +39 -0
  43. package/lib/old/sdk-client.js +85 -0
  44. package/lib/old/to-register.d.ts +69 -0
  45. package/lib/old/to-register.js +52 -2
  46. package/lib/old/tools/authenticate.js +3 -7
  47. package/lib/old/tools/build.d.ts +6 -1
  48. package/lib/old/tools/build.js +11 -1
  49. package/lib/old/tools/get-provider-info.d.ts +16 -0
  50. package/lib/old/tools/get-provider-info.js +73 -0
  51. package/lib/old/tools/get-version-config.d.ts +17 -0
  52. package/lib/old/tools/get-version-config.js +47 -0
  53. package/lib/old/tools/publish.js +189 -2
  54. package/lib/old/tools/update-provider-metadata.d.ts +16 -0
  55. package/lib/old/tools/update-provider-metadata.js +61 -0
  56. package/lib/proof/attestor.d.ts +6 -3
  57. package/lib/proof/attestor.js +10 -1
  58. package/lib/proof/debug-response-dump.d.ts +4 -0
  59. package/lib/proof/debug-response-dump.js +55 -0
  60. package/lib/proof/patch-domhandler.d.ts +1 -0
  61. package/lib/proof/patch-domhandler.js +59 -0
  62. package/lib/proof/zk-files.js +7 -6
  63. package/lib/provider/dom-xpath.d.ts +17 -0
  64. package/lib/provider/dom-xpath.js +187 -0
  65. package/lib/provider/draft.d.ts +30 -2
  66. package/lib/provider/draft.js +117 -28
  67. package/lib/provider/index.d.ts +4 -0
  68. package/lib/provider/index.js +7 -0
  69. package/lib/provider/injection-doc.js +6 -0
  70. package/lib/provider/matcher.d.ts +17 -20
  71. package/lib/provider/matcher.js +394 -280
  72. package/lib/provider/schema.d.ts +7 -5
  73. package/lib/tools/registry.d.ts +14 -0
  74. package/lib/tools/registry.js +32 -0
  75. package/package.json +9 -1
  76. package/skill/SKILL.builder.md +42 -23
  77. package/skill/SKILL.old.md +6 -2
  78. package/skill/builder-mode.md +1 -1
  79. package/skill/injection-api.md +117 -0
  80. package/src/authoring/backend.ts +98 -0
  81. package/src/authoring/index.ts +20 -0
  82. package/src/authoring/tools.ts +273 -0
  83. package/src/authoring/util.ts +42 -0
  84. package/src/consts.ts +15 -2
  85. package/src/mcp/server.ts +11 -78
  86. package/src/mcp/start.ts +4 -1
  87. package/src/mcp/tools/agent/analyze.ts +3 -3
  88. package/src/mcp/tools/agent/build.ts +13 -21
  89. package/src/mcp/tools/agent/capture.ts +31 -115
  90. package/src/mcp/tools/agent/eval.ts +19 -155
  91. package/src/mcp/tools/agent/find.ts +33 -36
  92. package/src/mcp/tools/agent/proof.ts +87 -160
  93. package/src/mcp/tools/agent/propose.ts +93 -85
  94. package/src/mcp/tools/agent/replay.ts +32 -42
  95. package/src/mcp/tools/agent/session.ts +4 -5
  96. package/src/mcp/tools/agent/shared.ts +152 -0
  97. package/src/old/client.ts +56 -0
  98. package/src/old/login-cdp.ts +29 -23
  99. package/src/old/sdk-client.ts +105 -0
  100. package/src/old/to-register.ts +98 -6
  101. package/src/old/tools/authenticate.ts +3 -11
  102. package/src/old/tools/build.ts +12 -0
  103. package/src/old/tools/get-provider-info.ts +91 -0
  104. package/src/old/tools/get-version-config.ts +56 -0
  105. package/src/old/tools/publish.ts +244 -1
  106. package/src/old/tools/update-provider-metadata.ts +72 -0
  107. package/src/proof/attestor.ts +16 -4
  108. package/src/proof/debug-response-dump.ts +70 -0
  109. package/src/proof/patch-domhandler.ts +68 -0
  110. package/src/proof/zk-files.ts +7 -6
  111. package/src/provider/dom-xpath.ts +225 -0
  112. package/src/provider/draft.ts +168 -35
  113. package/src/provider/index.ts +8 -0
  114. package/src/provider/injection-doc.ts +6 -0
  115. package/src/provider/matcher.ts +477 -357
  116. package/src/provider/schema.ts +7 -5
  117. package/src/tools/registry.ts +92 -0
@@ -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. `oprf-raw` is what the dev-loop defaults
23
- * to when privacy is requested — the only variant that runs without external
24
- * deps (gnark for `oprf`, a TEE for `oprf-mpc`). It is the weakest of the
25
- * three (the attestor participates in hashing) and is meant to be swapped
26
- * out before production.
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. */
@@ -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;
@@ -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.5",
4
+ "version": "0.1.6",
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": {
@@ -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 open the site yourself.** Call `list_tabs`. If there's already a tab on the right origin, use it. Otherwise pick any tab (the first one is usually fine) and call `navigate(tabId, url)` to send it to the site you've inferred from the dev's request (e.g. "GitHub username" → `https://github.com`). Don't ask the dev to navigate.
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(tabId, ...)` to evaluate a site-specific check.
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(tabId, "<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(tabId, 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.
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(tabId, url)`. Don't ask the dev to 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. **Start capture, then trigger the request.** Call `start_capture(tabId)` first. Then issue a reload so the relevant API call fires while capture is live: `eval_in_page(tabId, "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.
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(captureId, requestId, target, name, hash?)`. Pass `hash` only if the dev opted in. Inspect the returned 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[0]` carry an `xPath` or `jsonPath` anchor? (And if `hash` was requested, also a `regex` with a named capture and a `hash: oprf-…` field?)
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": [{ "xPath": "//script[@id=\"client-env\"]" }],
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(provider, secretParamsRef)`. Three outcomes:
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 an authentication-bound check.** Call `replay_request(provider)` _without_ the `secretParamsRef` (omit it). This sends the request stripped of all secret headers — the same shape an unauthenticated visitor would send.
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 `matchesOriginal: false` (e.g. 401/403, or the value differs) → the endpoint is auth-bound. Good. The proof will actually mean "the holder of these secrets saw this value."
159
- - If `matchesOriginal: true` with the same value → the endpoint is fully public. Tell the dev verbatim: "I verified this endpoint returns the same value with no authentication — the proof won't prove your identity, only that this URL returned the value. Anyone could generate the same proof. Want to keep digging for an auth-bound endpoint, or proceed anyway?" Wait for their decision before continuing.
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(provider, secretParamsRef)`. 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.
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 `secretParamsRef` from `propose_provider` is still valid.
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 (optionally + regex+hash when `hash` is passed) for the redaction. The named-capture regex in the redaction is the synthesizer's job, not yours.
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 is fine, the attestor will reveal the whole matched element which is naturally ≥16 bytes.
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({ provider, ownerAddress: address, … })`. No prompts.
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 `secretParamsRef`). The `.env` flow above avoids that — prefer it.
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 secretParamsRef`:
252
+ If `replay_request` or `run_proof` errors with `unknown draftId` (or `no capture`):
234
253
 
235
- - **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.
236
- - **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.
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
- ## Out of scope (v0)
257
+ ## Recognizing a HAWKEYE / live-interception provider
239
258
 
240
- If the dev needs a provider for a flow that requires multiple sequential requests (CSRF preflight, refresh-token chains), tell them: "multi-request providers are v1. For now, we can produce a single-request provider for the target value — let's see if one exists in this capture."
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.
@@ -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
- ## Out of scope (v0)
227
+ ## Recognizing a HAWKEYE / live-interception provider
224
228
 
225
- If the dev needs a provider for a flow that requires multiple sequential requests (CSRF preflight, refresh-token chains), tell them: "multi-request providers are v1. For now, we can produce a single-request provider for the target value — let's see if one exists in this capture."
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.
@@ -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
 
@@ -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'