@reclaimprotocol/agent 0.1.33 → 0.1.35

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/README.md +3 -3
  2. package/lib/authoring/backend.d.ts +13 -0
  3. package/lib/authoring/index.d.ts +1 -1
  4. package/lib/authoring/tools.js +78 -42
  5. package/lib/config/store.d.ts +10 -0
  6. package/lib/mcp/api/operations.gen.d.ts +238 -4
  7. package/lib/mcp/api/operations.gen.js +6967 -672
  8. package/lib/mcp/api/tools.js +21 -8
  9. package/lib/mcp/instructions.js +12 -12
  10. package/lib/mcp/server.d.ts +1 -1
  11. package/lib/mcp/server.js +3 -0
  12. package/lib/mcp/start.js +8 -7
  13. package/lib/mcp/success-response.js +5 -1
  14. package/lib/mcp/tools/agent/analyze.js +5 -4
  15. package/lib/mcp/tools/agent/attach.js +11 -7
  16. package/lib/mcp/tools/agent/build.js +5 -1
  17. package/lib/mcp/tools/agent/capture.d.ts +2 -2
  18. package/lib/mcp/tools/agent/capture.js +15 -11
  19. package/lib/mcp/tools/agent/check-attestor.d.ts +5 -0
  20. package/lib/mcp/tools/agent/check-attestor.js +37 -0
  21. package/lib/mcp/tools/agent/eval.js +4 -3
  22. package/lib/mcp/tools/agent/find.js +9 -10
  23. package/lib/mcp/tools/agent/how-it-works.js +1 -0
  24. package/lib/mcp/tools/agent/live-view.js +49 -22
  25. package/lib/mcp/tools/agent/pod-proof.js +7 -11
  26. package/lib/mcp/tools/agent/proof.d.ts +12 -0
  27. package/lib/mcp/tools/agent/proof.js +32 -0
  28. package/lib/mcp/tools/agent/propose.js +4 -3
  29. package/lib/mcp/tools/agent/publish.d.ts +2 -1
  30. package/lib/mcp/tools/agent/publish.js +12 -6
  31. package/lib/mcp/tools/agent/replay.js +10 -7
  32. package/lib/mcp/tools/agent/session.js +16 -8
  33. package/lib/mcp/tools/agent/shared.d.ts +2 -1
  34. package/lib/mcp/tools/agent/shared.js +29 -24
  35. package/lib/mcp/tools/agent/start-integration.d.ts +15 -0
  36. package/lib/mcp/tools/agent/start-integration.js +62 -0
  37. package/lib/mcp/tools/authenticate/authenticate.js +9 -11
  38. package/lib/mcp/tools/authenticate/build.js +2 -0
  39. package/lib/mcp/tools/authenticate/builder-access.js +1 -0
  40. package/lib/mcp/tools/authenticate/eth-key.d.ts +8 -0
  41. package/lib/mcp/tools/authenticate/eth-key.js +27 -15
  42. package/lib/mcp/tools/authenticate/import.js +18 -31
  43. package/lib/mcp/tools/authenticate/issue-org-token.d.ts +8 -0
  44. package/lib/mcp/tools/authenticate/issue-org-token.js +60 -0
  45. package/lib/mcp/tools/authenticate/issue.js +9 -9
  46. package/lib/mcp/tools/authenticate/org-key-store.d.ts +10 -0
  47. package/lib/mcp/tools/authenticate/org-key-store.js +23 -0
  48. package/lib/mcp/tools/authenticate/resolve-owner-key.js +10 -12
  49. package/lib/mcp/tools/mode/mode.js +2 -0
  50. package/lib/mcp/tools/verification/build.d.ts +3 -1
  51. package/lib/mcp/tools/verification/build.js +10 -2
  52. package/lib/mcp/tools/verification/callback-listener.d.ts +51 -0
  53. package/lib/mcp/tools/verification/callback-listener.js +163 -0
  54. package/lib/mcp/tools/verification/index.d.ts +2 -0
  55. package/lib/mcp/tools/verification/index.js +2 -0
  56. package/lib/mcp/tools/verification/proxy-callback-events.d.ts +59 -0
  57. package/lib/mcp/tools/verification/proxy-callback-events.js +300 -0
  58. package/lib/mcp/tools/verification/read-callback-events.d.ts +3 -0
  59. package/lib/mcp/tools/verification/read-callback-events.js +135 -0
  60. package/lib/mcp/tools/verification/result.d.ts +6 -0
  61. package/lib/mcp/tools/verification/result.js +11 -8
  62. package/lib/proof/check-attestor.d.ts +67 -0
  63. package/lib/proof/check-attestor.js +156 -0
  64. package/lib/tools/registry.d.ts +12 -0
  65. package/lib/tools/registry.js +1 -0
  66. package/package.json +2 -2
  67. package/skill/SKILL.builder.md +84 -75
  68. package/skill/SKILL.common.md +23 -10
  69. package/skill/builder-mode.md +140 -138
  70. package/skill/start-integration.md +74 -0
  71. package/skill/user-script.md +64 -0
  72. package/skill/verification-client-integration.md +40 -28
  73. package/src/authoring/backend.ts +15 -0
  74. package/src/authoring/index.ts +2 -0
  75. package/src/authoring/tools.ts +86 -42
  76. package/src/config/store.ts +11 -0
  77. package/src/mcp/api/operations.gen.ts +7011 -704
  78. package/src/mcp/api/tools.ts +21 -8
  79. package/src/mcp/instructions.ts +12 -12
  80. package/src/mcp/server.ts +4 -0
  81. package/src/mcp/start.ts +9 -7
  82. package/src/mcp/success-response.ts +5 -1
  83. package/src/mcp/tools/agent/analyze.ts +5 -4
  84. package/src/mcp/tools/agent/attach.ts +11 -7
  85. package/src/mcp/tools/agent/build.ts +7 -1
  86. package/src/mcp/tools/agent/capture.ts +20 -12
  87. package/src/mcp/tools/agent/check-attestor.ts +49 -0
  88. package/src/mcp/tools/agent/eval.ts +4 -3
  89. package/src/mcp/tools/agent/find.ts +9 -10
  90. package/src/mcp/tools/agent/how-it-works.ts +1 -0
  91. package/src/mcp/tools/agent/live-view.ts +49 -22
  92. package/src/mcp/tools/agent/pod-proof.ts +7 -11
  93. package/src/mcp/tools/agent/proof.ts +55 -0
  94. package/src/mcp/tools/agent/propose.ts +4 -3
  95. package/src/mcp/tools/agent/publish.ts +12 -4
  96. package/src/mcp/tools/agent/replay.ts +10 -7
  97. package/src/mcp/tools/agent/session.ts +16 -8
  98. package/src/mcp/tools/agent/shared.ts +39 -24
  99. package/src/mcp/tools/agent/start-integration.ts +84 -0
  100. package/src/mcp/tools/authenticate/authenticate.ts +9 -11
  101. package/src/mcp/tools/authenticate/build.ts +2 -0
  102. package/src/mcp/tools/authenticate/builder-access.ts +1 -0
  103. package/src/mcp/tools/authenticate/eth-key.ts +48 -18
  104. package/src/mcp/tools/authenticate/import.ts +21 -36
  105. package/src/mcp/tools/authenticate/issue-org-token.ts +84 -0
  106. package/src/mcp/tools/authenticate/issue.ts +9 -9
  107. package/src/mcp/tools/authenticate/org-key-store.ts +25 -0
  108. package/src/mcp/tools/authenticate/resolve-owner-key.ts +10 -12
  109. package/src/mcp/tools/mode/mode.ts +2 -0
  110. package/src/mcp/tools/verification/build.ts +10 -2
  111. package/src/mcp/tools/verification/callback-listener.ts +203 -0
  112. package/src/mcp/tools/verification/index.ts +2 -0
  113. package/src/mcp/tools/verification/proxy-callback-events.ts +374 -0
  114. package/src/mcp/tools/verification/read-callback-events.ts +160 -0
  115. package/src/mcp/tools/verification/result.ts +11 -8
  116. package/src/proof/check-attestor.ts +209 -0
  117. package/src/tools/registry.ts +14 -0
@@ -1,4 +1,5 @@
1
1
  import { type ProofResult } from '../../../proof/attestor.ts';
2
+ import { type AttestorCheckResult, type CheckAttestorOptions } from '../../../proof/check-attestor.ts';
2
3
  /**
3
4
  * The MCP's rich `run_proof` view: persist the full claim to disk, return
4
5
  * identifier + extractedParameters + a prettified claim (request dropped, byte
@@ -11,3 +12,14 @@ export declare function shapeRichProofResult(result: ProofResult, opts: {
11
12
  hasSecrets: boolean;
12
13
  attestorUrl?: string;
13
14
  }): Record<string, unknown>;
15
+ export interface ProofDiagnosis {
16
+ attestor: AttestorCheckResult;
17
+ likelyCause: string;
18
+ next: string;
19
+ }
20
+ /**
21
+ * When an unverified proof failed with a network-like error, runs the
22
+ * attestor check once and says whether the attestor or the target site is the
23
+ * likely cause. Returns `undefined` for verified proofs and other errors.
24
+ */
25
+ export declare function diagnoseNetworkFailure(result: Pick<ProofResult, 'verified' | 'error' | 'errorKind'>, check: (opts: CheckAttestorOptions) => Promise<AttestorCheckResult>, attestorUrl?: string): Promise<ProofDiagnosis | undefined>;
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { agentTraceEnabled } from "../../../consts.js";
4
4
  import { reclaimHomeDirPath } from "../../../paths.js";
5
5
  import { tailLogs, warnsAndErrors, } from "../../../proof/attestor.js";
6
+ import { isNetworkError, } from "../../../proof/check-attestor.js";
6
7
  function claimsDir() {
7
8
  return join(reclaimHomeDirPath(), 'claims');
8
9
  }
@@ -157,3 +158,34 @@ export function shapeRichProofResult(result, opts) {
157
158
  }
158
159
  return out;
159
160
  }
161
+ /**
162
+ * When an unverified proof failed with a network-like error, runs the
163
+ * attestor check once and says whether the attestor or the target site is the
164
+ * likely cause. Returns `undefined` for verified proofs and other errors.
165
+ */
166
+ export async function diagnoseNetworkFailure(result, check, attestorUrl) {
167
+ if (result.verified
168
+ || result.errorKind !== undefined
169
+ || !isNetworkError(result.error)) {
170
+ return undefined;
171
+ }
172
+ const attestor = await check(attestorUrl ? { attestorUrl } : {});
173
+ if (attestor.ok) {
174
+ return {
175
+ attestor,
176
+ likelyCause: 'The attestor works from this machine, so the target site likely '
177
+ + 'blocks or drops the attestor\'s network (geo or IP restriction).',
178
+ next: 'If the browser attach mode is "builder", call `run_proof_via_pod`. '
179
+ + 'Otherwise tell the developer and suggest asking the Reclaim team. '
180
+ + 'Do not retry the same proof.',
181
+ };
182
+ }
183
+ return {
184
+ attestor,
185
+ likelyCause: 'The attestor check also failed, so this machine cannot reach the '
186
+ + 'attestor (network, VPN, firewall, or proxy).',
187
+ next: 'Tell the developer. To see the connection details, suggest '
188
+ + 'restarting the MCP server with `RECLAIM_AGENT_TRACE=1`. Do not '
189
+ + 'retry the same proof.',
190
+ };
191
+ }
@@ -14,9 +14,9 @@ export function proposeTool(resolveBackend) {
14
14
  return defineTool({
15
15
  name: 'propose_provider',
16
16
  description: 'Drafts a provider from one authenticated captured request and '
17
- + 'keeps captured secrets server-side. Use `targets` for every value '
18
- + 'available in the same response; the result contains a `draftId` '
19
- + 'and secret-free recipe. Set `shortenNames: false` to preserve an '
17
+ + 'keeps captured secrets server-side. Use `targets` for the data '
18
+ + 'points the claim needs from that response; returns a `draftId` '
19
+ + 'and a secret-free recipe. Set `shortenNames: false` to preserve an '
20
20
  + 'explicit parameter name, including a hidden `REQ_` or `URL_` '
21
21
  + 'prefix. For matching and naming guidance, call '
22
22
  + 'how_it_works({ topic: "draft" }) and topic `params`.',
@@ -54,6 +54,7 @@ export function proposeTool(resolveBackend) {
54
54
  target: { type: 'string' },
55
55
  hash: { type: 'string', enum: ['oprf-raw', 'oprf', 'oprf-mpc'] },
56
56
  }, ['requestId', 'name']),
57
+ annotations: { openWorldHint: false, destructiveHint: false },
57
58
  }, async (args) => {
58
59
  const backend = resolveBackend(args);
59
60
  const requestId = String(args.requestId);
@@ -1,6 +1,7 @@
1
1
  import type { ReclaimClient } from '@reclaimprotocol/client/api';
2
+ import type { KeptScriptRef } from '../../../authoring/index.ts';
2
3
  import { type RegisteredTool } from '../../server.ts';
3
4
  import type { AttachState } from './attach.ts';
4
5
  export declare function createVersionFromCaptureTool(client: ReclaimClient, attachRef?: {
5
6
  current?: AttachState;
6
- }): RegisteredTool;
7
+ }, kept?: KeptScriptRef): RegisteredTool;
@@ -165,11 +165,13 @@ const BROWSER_REQUEST_ACTION_SCHEMA = {
165
165
  },
166
166
  required: ['id', 'origin', 'path', 'method'],
167
167
  };
168
- export function createVersionFromCaptureTool(client, attachRef) {
168
+ export function createVersionFromCaptureTool(client, attachRef, kept) {
169
169
  return defineTool({
170
170
  name: 'create_provider_version_from_capture',
171
171
  description: 'Creates a new immutable provider version after `run_proof` succeeds. '
172
- + 'Pass exactly one `provider` or `providers` value returned by '
172
+ + 'Publishes the navigation script kept by `test_user_script` as '
173
+ + '`jsUserScripts` unless `jsUserScripts` is passed. Pass exactly '
174
+ + 'one `provider` or `providers` value returned by '
173
175
  + '`propose_provider`; do not rebuild the recipe. The default is a '
174
176
  + 'patch bump, or use `bump` or an explicitly requested higher '
175
177
  + '`version`. The new version remains a draft until published or '
@@ -230,7 +232,9 @@ export function createVersionFromCaptureTool(client, attachRef) {
230
232
  },
231
233
  jsUserScripts: {
232
234
  type: 'string',
233
- description: USER_SCRIPT_HINT +
235
+ description: 'Defaults to the script kept by `test_user_script` with ' +
236
+ '`keep: true`; pass it only to override. ' +
237
+ USER_SCRIPT_HINT +
234
238
  ' Stored as webSettings.jsUserScripts; the Builder ' +
235
239
  'verification client runs it before every page load ' +
236
240
  '(including before navigating to initialUrl), matching ' +
@@ -288,7 +292,9 @@ export function createVersionFromCaptureTool(client, attachRef) {
288
292
  },
289
293
  required: ['providerId', 'initialUrl'],
290
294
  },
295
+ annotations: { openWorldHint: true, destructiveHint: false },
291
296
  }, async (args) => {
297
+ const jsUserScripts = args.jsUserScripts ?? kept?.keptScript?.script;
292
298
  // Exactly one of `provider` / `providers` — normalize to an array.
293
299
  const hasMany = Array.isArray(args.providers) && args.providers.length > 0;
294
300
  const hasOne = !!args.provider;
@@ -306,7 +312,7 @@ export function createVersionFromCaptureTool(client, attachRef) {
306
312
  notes: args.notes,
307
313
  version: ver,
308
314
  requiredContext: args.requiredContext,
309
- jsUserScripts: args.jsUserScripts,
315
+ jsUserScripts,
310
316
  allowedJsRequests: args.allowedJsRequests,
311
317
  browserRequestActions: args.browserRequestActions,
312
318
  interceptorOptions: args.interceptorOptions,
@@ -332,11 +338,11 @@ export function createVersionFromCaptureTool(client, attachRef) {
332
338
  const attached = attachRef?.current;
333
339
  notes.push(...postPublishBrowserNotes(attached, {
334
340
  initialUrl: args.initialUrl,
335
- hasUserScript: !!args.jsUserScripts?.trim(),
341
+ hasUserScript: !!jsUserScripts?.trim(),
336
342
  hasInterceptor: !!args.interceptorOptions,
337
343
  }));
338
344
  // Let the developer know how and when the script runs.
339
- if (args.jsUserScripts?.trim()) {
345
+ if (jsUserScripts?.trim()) {
340
346
  notes.push('jsUserScripts will run in the Builder verification client before ' +
341
347
  'every page load (before navigating to initialUrl), matching ' +
342
348
  'old-devtool customInjection. window.Reclaim is available for ' +
@@ -10,17 +10,20 @@ import { defineTool } from "../../../tools/registry.js";
10
10
  export function replayTool(resolveBackend) {
11
11
  return defineTool({
12
12
  name: 'replay_request',
13
- description: 'Validate the drafted recipe before proving — confirm its selectors '
14
- + 'extract the target value. Returns diagnostics; run before '
15
- + 'proving. Set `withoutSecrets: true` for the AUTH-BOUND check: it '
16
- + 're-issues the request with NO secrets/cookies (an anonymous '
17
- + 'visitor). If the values still extract (valid:true), the endpoint '
18
- + 'is PUBLIC and the proof would not bind to the user — reject it '
19
- + 'and find an auth-bound request instead.',
13
+ description: 'Re-sends the drafted request from inside the attached authoring '
14
+ + 'browser (not from this machine) and checks that its selectors '
15
+ + 'extract the target data points. Captured secrets stay server-side '
16
+ + 'and are not returned. Returns diagnostics. Run it before '
17
+ + '`run_proof`. With `withoutSecrets: true`, sends the request with '
18
+ + 'no cookies or auth headers, like an anonymous visitor. If the '
19
+ + 'values still extract (`valid: true`), the endpoint is public and '
20
+ + 'the proof would not bind to the user: choose an auth-bound '
21
+ + 'request instead.',
20
22
  inputSchema: objectSchema({
21
23
  draftId: { type: 'string' },
22
24
  withoutSecrets: { type: 'boolean' },
23
25
  }, ['draftId']),
26
+ annotations: { readOnlyHint: true, openWorldHint: true },
24
27
  }, async (args) => {
25
28
  const backend = resolveBackend(args);
26
29
  const draft = backend.drafts.get(String(args.draftId));
@@ -5,10 +5,13 @@ export function sessionTools(attachRef, captures) {
5
5
  return [
6
6
  defineTool({
7
7
  name: 'get_session',
8
- description: 'Report the current attach state, the live captureIds, and the '
9
- + 'persisted browser config. Call it to check whether a browser is '
10
- + 'still attached before reattaching, or to recover a captureId.',
8
+ description: 'Returns the current attach state, the live captureIds, the '
9
+ + 'persisted browser config, and the navigation script kept by '
10
+ + '`test_user_script`. Call it to check whether a browser is still '
11
+ + 'attached before reattaching, or to recover a captureId or the '
12
+ + 'kept script.',
11
13
  inputSchema: { type: 'object', properties: {} },
14
+ annotations: { readOnlyHint: true, openWorldHint: false },
12
15
  }, async () => ({
13
16
  config: attachRef.config.read()?.browser_agent,
14
17
  attached: Boolean(attachRef.current),
@@ -19,22 +22,27 @@ export function sessionTools(attachRef, captures) {
19
22
  orgId: attachRef.current?.orgId,
20
23
  captures: [...captures.sessions.keys()],
21
24
  activeTab: captures.activeTab,
25
+ keptUserScript: captures.keptScript?.script,
22
26
  })),
23
27
  defineTool({
24
28
  name: 'dispose_browser',
25
- description: 'Stop captures, disconnect the current browser, and immediately '
26
- + 'delete a Builder-hosted Popcorn session. Custom CDP sessions '
27
- + 'are disconnected but never deleted by Reclaim.',
29
+ description: 'Stops captures, disconnects the current browser, and deletes a '
30
+ + 'Builder-hosted browser session at once. Clears the kept '
31
+ + 'navigation script. Reclaim disconnects a custom CDP browser but '
32
+ + 'never deletes it.',
28
33
  inputSchema: { type: 'object', properties: {} },
34
+ annotations: { destructiveHint: true, openWorldHint: true },
29
35
  }, async () => {
30
36
  await resetAll(attachRef, captures);
31
37
  return { disposed: true };
32
38
  }),
33
39
  defineTool({
34
40
  name: 'reset_session',
35
- description: 'Stop all captures and clear capture state — drops every '
36
- + 'session\'s drafts and their in-memory secrets.',
41
+ description: 'Stops all captures and clears capture state, dropping every '
42
+ + 'draft and its in-memory secrets. Also disconnects the browser '
43
+ + 'and clears the kept navigation script.',
37
44
  inputSchema: { type: 'object', properties: {} },
45
+ annotations: { destructiveHint: true, openWorldHint: true },
38
46
  }, async () => {
39
47
  await resetAll(attachRef, captures);
40
48
  return { reset: true };
@@ -1,3 +1,4 @@
1
+ import { checkAttestor } from '../../../proof/check-attestor.ts';
1
2
  import type { RegisteredTool } from '../../server.ts';
2
3
  import type { CaptureStore } from './capture.ts';
3
4
  /** The shared authoring tools mapped to the MCP's shape: resolve each call's
@@ -5,4 +6,4 @@ import type { CaptureStore } from './capture.ts';
5
6
  * takes `captureId`; `run_proof` also gets the credential-aware contract. The
6
7
  * tools are authored plain in `../../../authoring`; this only wraps them with
7
8
  * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
8
- export declare function sharedAuthoringTools(captures: CaptureStore): RegisteredTool[];
9
+ export declare function sharedAuthoringTools(captures: CaptureStore, check?: typeof checkAttestor): RegisteredTool[];
@@ -1,7 +1,8 @@
1
1
  import { authoringTools, extendTool, } from "../../../authoring/index.js";
2
2
  import { runAttestorProof } from "../../../proof/attestor.js";
3
+ import { checkAttestor } from "../../../proof/check-attestor.js";
3
4
  import { replayProviderInPage } from "../../../proof/page-replay.js";
4
- import { shapeRichProofResult } from "./proof.js";
5
+ import { diagnoseNetworkFailure, shapeRichProofResult } from "./proof.js";
5
6
  /**
6
7
  * OPRF mode this LOCAL (non-TEE) attestor can actually run. Recipes carry
7
8
  * `oprf-mpc` — the canonical/publish mode a TEE expects — but the local dev
@@ -32,12 +33,13 @@ function withLocalOprfMode(provider) {
32
33
  /** The local backend for one capture: drive its tab over CDP, read its
33
34
  * captured requests, prove via the in-process attestor, replay by re-fetching
34
35
  * FROM INSIDE the tab (real cookies/session — see proof/page-replay.ts). */
35
- function localBackend(entry) {
36
+ function localBackend(entry, kept, check) {
36
37
  const { tabClient, session, drafts } = entry;
37
38
  const sendCdp = (method, params) => {
38
39
  return tabClient.send(method, params ?? {});
39
40
  };
40
41
  return {
42
+ kept,
41
43
  capture: session,
42
44
  drafts,
43
45
  sendCdp,
@@ -75,11 +77,13 @@ function localBackend(entry) {
75
77
  ...(opts?.ownerAddress ? { ownerAddress: opts.ownerAddress } : {}),
76
78
  ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
77
79
  });
78
- return shapeRichProofResult(result, {
80
+ const shaped = shapeRichProofResult(result, {
79
81
  provider: draft.provider,
80
82
  hasSecrets: Object.keys(draft.secrets).length > 0,
81
83
  ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
82
84
  });
85
+ const diagnosis = await diagnoseNetworkFailure(result, check, opts?.attestorUrl);
86
+ return diagnosis ? { ...shaped, diagnosis } : shaped;
83
87
  },
84
88
  };
85
89
  }
@@ -87,25 +91,26 @@ function localBackend(entry) {
87
91
  * tool by the host below (the cloud's TEE signs in-enclave, so it needs none
88
92
  * of this). */
89
93
  const RUN_PROOF_EXTRAS = {
90
- description: 'Run a full zkTLS proof of the drafted recipe through the Reclaim '
91
- + 'attestor SDK. Use only after replay_request looks right — attestor '
92
- + 'calls are expensive. Writes the full claim to '
93
- + '~/.reclaim/claims/<timestamp>.json and returns identifier + '
94
- + 'extractedParameters + claimPath + a prettified `claim` (request '
95
- + 'dropped, byte arrays as base64). With RECLAIM_AGENT_TRACE=1, also '
96
- + 'writes a per-call diagnostic dump (input provider, sentParams, full '
97
- + 'attestor log stream, result) and returns it as `runLogPath`. '
98
- + '`attestorUrl` overrides the default only when the dev supplies a '
99
- + 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
100
- + '(the credential to sign with) is REQUIRED — call resolve_owner_key '
101
- + 'first. errorKind="missing-owner-key" means that address has no known '
102
- + 'private key; re-run resolve_owner_key or issue/import a credential. '
103
- + 'NOTE: unlike replay_request (which re-issues from inside the live '
104
- + 'attached browser tab), the ATTESTOR calls the target from its OWN '
105
- + 'network/IP, separate from the browser\'s. A request that replayed '
106
- + 'fine can still fail here for geo/IP-bound content (country-locked '
107
- + "pages, IP-allowlisted APIs) — if that happens, it's a property of "
108
- + 'the site, not a bug in the draft.',
94
+ description: 'Proves the drafted recipe with a full zkTLS proof through the attestor '
95
+ + 'SDK. Call it after `replay_request` looks right; attestor calls are '
96
+ + 'expensive. Call `resolve_owner_key` first and pass its address as '
97
+ + '`ownerAddress`. Writes the full claim to '
98
+ + '`~/.reclaim/claims/<timestamp>.json`. Returns `identifier`, '
99
+ + '`extractedParameters`, `claimPath`, and a readable `claim` (request '
100
+ + 'removed, byte arrays as base64). Set `RECLAIM_AGENT_TRACE=1` to also '
101
+ + 'get `runLogPath`, a diagnostic dump with the attestor log stream. '
102
+ + 'Set `attestorUrl` only if the developer supplied one; never invent '
103
+ + 'backup URLs. If `errorKind` is `missing-owner-key`, the address has '
104
+ + 'no known private key: run `resolve_owner_key` again, or issue or '
105
+ + 'import a credential. Unlike `replay_request`, which re-sends from '
106
+ + 'inside the attached browser tab, the attestor calls the target from '
107
+ + 'its own network and IP. A request that replayed can still fail here '
108
+ + 'for geo- or IP-bound content, such as country-locked pages or '
109
+ + 'IP-allowlisted APIs. That is a property of the site, not a draft '
110
+ + 'bug. After a network-like failure ("connection closed", '
111
+ + '`ERROR_NETWORK_ERROR`, timeout), the result includes `diagnosis` '
112
+ + 'with `attestor` (a `check_attestor` result), `likelyCause`, and '
113
+ + '`next`. Follow `next`; do not retry the same proof.',
109
114
  properties: {
110
115
  ownerAddress: {
111
116
  type: 'string',
@@ -123,7 +128,7 @@ const RUN_PROOF_EXTRAS = {
123
128
  * takes `captureId`; `run_proof` also gets the credential-aware contract. The
124
129
  * tools are authored plain in `../../../authoring`; this only wraps them with
125
130
  * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
126
- export function sharedAuthoringTools(captures) {
131
+ export function sharedAuthoringTools(captures, check = checkAttestor) {
127
132
  const resolveBackend = (args) => {
128
133
  const captureId = String(args.captureId);
129
134
  const entry = captures.sessions.get(captureId);
@@ -131,7 +136,7 @@ export function sharedAuthoringTools(captures) {
131
136
  throw new Error(`no capture ${captureId} — call start_capture first `
132
137
  + '(it has been reset or never started).');
133
138
  }
134
- return localBackend(entry);
139
+ return localBackend(entry, captures, check);
135
140
  };
136
141
  return authoringTools(resolveBackend).map((tool) => {
137
142
  if (tool.definition.name === 'run_proof') {
@@ -0,0 +1,15 @@
1
+ import { type RegisteredTool } from '../../server.ts';
2
+ interface StartIntegrationArgs {
3
+ goal: string;
4
+ orgId?: string;
5
+ providerIds?: string[];
6
+ }
7
+ /**
8
+ * Returns the plan a coding agent follows to add Reclaim verification to the
9
+ * developer's app, from sign-in to a tested Verify button. The plan lives in
10
+ * `skill/start-integration.md`; this tool fills in the inputs.
11
+ */
12
+ export declare function startIntegrationTool(): RegisteredTool;
13
+ /** Fill the plan template. Exported for tests. */
14
+ export declare function renderStartIntegration(args: StartIntegrationArgs): string;
15
+ export {};
@@ -0,0 +1,62 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { defineTool } from "../../server.js";
4
+ /**
5
+ * Returns the plan a coding agent follows to add Reclaim verification to the
6
+ * developer's app, from sign-in to a tested Verify button. The plan lives in
7
+ * `skill/start-integration.md`; this tool fills in the inputs.
8
+ */
9
+ export function startIntegrationTool() {
10
+ return defineTool({
11
+ name: 'start_integration',
12
+ description: 'Returns a step-by-step plan for adding Reclaim verification to the '
13
+ + 'current codebase: sign in, set up the app (session route, callback, '
14
+ + 'Verify button, local event proxy) in parallel with building or '
15
+ + 'choosing the provider, then wire the provider and test. Call it '
16
+ + 'first when a developer wants their users to prove something, and '
17
+ + 'follow the plan to the end. Changes nothing itself.',
18
+ inputSchema: {
19
+ type: 'object',
20
+ required: ['goal'],
21
+ properties: {
22
+ goal: {
23
+ type: 'string',
24
+ minLength: 1,
25
+ maxLength: 300,
26
+ description: 'What the developer\'s users prove, for example, "they have '
27
+ + 'a GitHub account with 100 followers".',
28
+ },
29
+ orgId: {
30
+ type: 'string',
31
+ format: 'uuid',
32
+ description: 'Organization to use. Omit to choose from the list.',
33
+ },
34
+ providerIds: {
35
+ type: 'array',
36
+ maxItems: 10,
37
+ items: { type: 'string' },
38
+ description: 'Existing provider IDs to use. Omit to build a new provider.',
39
+ },
40
+ },
41
+ },
42
+ annotations: { readOnlyHint: true, openWorldHint: false },
43
+ }, async (args) => renderStartIntegration(args));
44
+ }
45
+ /** Fill the plan template. Exported for tests. */
46
+ export function renderStartIntegration(args) {
47
+ const providerIds = args.providerIds?.length
48
+ ? args.providerIds.map((id) => `\`${id}\``).join(', ')
49
+ : undefined;
50
+ const values = {
51
+ goal: args.goal,
52
+ orgId: args.orgId,
53
+ providerIds,
54
+ };
55
+ const template = readFileSync(fileURLToPath(new URL('../../../../skill/start-integration.md', import.meta.url)), 'utf8');
56
+ return template
57
+ // Keep a conditional block only when its input was given (or not given).
58
+ .replace(/<!-- (if|ifnot):(\w+) -->\n([\s\S]*?)<!-- endif -->\n/g, (_, kind, name, body) => {
59
+ return (kind === 'if') === Boolean(values[name]) ? body : '';
60
+ })
61
+ .replace(/\{\{(\w+)\}\}/g, (_, name) => values[name] ?? '');
62
+ }
@@ -16,18 +16,16 @@ export function authenticateTool(client, name = 'reclaim_authenticate') {
16
16
  + 'get_me_providers; those need the separate old-devtools login, '
17
17
  + 'reclaim_authenticate. Check both with get_devtools_mode. '
18
18
  : '')
19
- + 'Authorize the CLI / MCP to make authenticated API calls. Opens '
20
- + 'the '
21
- + 'browser to the builder sign-in page (standard OAuth 2.0 ' +
22
- 'authorization-code + PKCE with a localhost redirect) and waits for ' +
23
- 'the user to sign in and approve. On success the access token is ' +
24
- 'cached in ~/.reclaim/config.json and activated for this session.\n\n' +
25
- 'Call with no arguments. This bearer token authenticates ' +
26
- 'dashboard/org-management calls (for example, list_orgs, or ' +
27
- 'issuing an org secret). Programmatic verification calls use a ' +
28
- 'separate org secret ' +
29
- '(rorg_…, set as RECLAIM_ORG_SECRET).',
19
+ + 'Signs the developer in to Builder. Opens the Builder sign-in page '
20
+ + 'in their browser (OAuth 2.0 authorization code with PKCE, '
21
+ + 'redirecting to a localhost port this server opens) and waits for '
22
+ + 'them to approve. Saves the access token in ~/.reclaim/config.json '
23
+ + 'with mode 0600 and uses it for this session; the token is never '
24
+ + 'returned. The token authorizes organization-management tools such '
25
+ + 'as `list_orgs`. Programmatic verification tools use a separate '
26
+ + 'organization key; see `issue_org_token`. Call with no arguments.',
30
27
  inputSchema: { type: 'object', properties: {} },
28
+ annotations: { openWorldHint: true, idempotentHint: true },
31
29
  }, async () => runOAuthLoopback(client));
32
30
  }
33
31
  const _customFetchForOauth = async (url, options) => {
@@ -1,6 +1,7 @@
1
1
  import { authenticateTool } from "./authenticate.js";
2
2
  import { importTool } from "./import.js";
3
3
  import { issueTool } from "./issue.js";
4
+ import { issueOrgTokenTool } from "./issue-org-token.js";
4
5
  import { resolveOwnerKeyTool } from "./resolve-owner-key.js";
5
6
  /**
6
7
  * Local-only key tools — no backend client needed. These manage the eth
@@ -23,6 +24,7 @@ export function buildLocalCredentialTools() {
23
24
  export function buildCredentialTools(client) {
24
25
  return [
25
26
  authenticateTool(client),
27
+ issueOrgTokenTool(client),
26
28
  ...buildLocalCredentialTools(),
27
29
  ];
28
30
  }
@@ -26,6 +26,7 @@ export function buildBuilderOrganizationTools(client) {
26
26
  cursor: { type: 'string', maxLength: 500 },
27
27
  },
28
28
  },
29
+ annotations: { readOnlyHint: true, openWorldHint: true },
29
30
  }, async ({ cursor }) => {
30
31
  const { data } = await client.call('ListOrgs', {
31
32
  query: { ...(cursor ? { cursor } : {}) },
@@ -19,6 +19,14 @@ export declare function normalizeEthHex(value: string): string;
19
19
  * ask for one (see `resolve_owner_key`).
20
20
  */
21
21
  export declare function resolveEthKey(projectDir?: string): ResolvedEthKey | undefined;
22
+ /** Append `entry` to the project `.gitignore` if not already present. */
23
+ export declare function ensureGitignored(projectDir: string, entry: string): void;
24
+ /**
25
+ * Set `name=value` in a dotenv file, replacing an existing line or appending
26
+ * one. Writes with mode 0600 and adds the file to the project `.gitignore`.
27
+ * `envFile` is relative to `projectDir`.
28
+ */
29
+ export declare function upsertEnvVar(projectDir: string, envFile: string, name: string, value: string): string;
22
30
  /**
23
31
  * Persist a raw eth private key to the project `.env` as
24
32
  * `RECLAIM_PRIVATE_KEY=0x…` (upsert; mode 0600; `.env` added to `.gitignore`).
@@ -16,8 +16,8 @@
16
16
  */
17
17
  import { deriveFromPrivateKey } from '@reclaimprotocol/client';
18
18
  import assert from 'node:assert';
19
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
20
- import { isAbsolute, join } from 'node:path';
19
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
20
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
21
21
  import { privateKey, privateKeyFile } from "../../../consts.js";
22
22
  import { findProjectRoot } from "../../../paths.js";
23
23
  const ENV_VAR = 'RECLAIM_PRIVATE_KEY';
@@ -80,7 +80,7 @@ export function resolveEthKey(projectDir = findProjectRoot(process.cwd())) {
80
80
  return undefined;
81
81
  }
82
82
  /** Append `entry` to the project `.gitignore` if not already present. */
83
- function ensureGitignored(projectDir, entry) {
83
+ export function ensureGitignored(projectDir, entry) {
84
84
  const path = join(projectDir, '.gitignore');
85
85
  const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
86
86
  if (current.split('\n').some((l) => l.trim() === entry)) {
@@ -90,26 +90,38 @@ function ensureGitignored(projectDir, entry) {
90
90
  writeFileSync(path, current + prefix + entry + '\n');
91
91
  }
92
92
  /**
93
- * Persist a raw eth private key to the project `.env` as
94
- * `RECLAIM_PRIVATE_KEY=0x…` (upsert; mode 0600; `.env` added to `.gitignore`).
95
- * Used by `issue_credentials` (generate) and `import_credentials` (supplied
96
- * hex). Returns the derived address and the `.env` path.
93
+ * Set `name=value` in a dotenv file, replacing an existing line or appending
94
+ * one. Writes with mode 0600 and adds the file to the project `.gitignore`.
95
+ * `envFile` is relative to `projectDir`.
97
96
  */
98
- export function writeEthKey(privateKeyHex, projectDir = findProjectRoot(process.cwd())) {
99
- const hex = normalizeEthHex(privateKeyHex);
100
- const envPath = join(projectDir, ENV_FILE);
101
- const line = `${ENV_VAR}=${hex}`;
97
+ export function upsertEnvVar(projectDir, envFile, name, value) {
98
+ const envPath = resolve(projectDir, envFile);
99
+ const line = `${name}=${value}`;
102
100
  const existing = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
103
- const re = new RegExp(`^\\s*${ENV_VAR}\\s*=.*$`, 'm');
101
+ const re = new RegExp(`^\\s*${name}\\s*=.*$`, 'm');
104
102
  let next;
105
103
  if (re.test(existing)) {
106
- next = existing.replace(re, line);
104
+ // A function replacer keeps `$` sequences in the value literal.
105
+ next = existing.replace(re, () => line);
107
106
  }
108
107
  else {
109
108
  const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
110
109
  next = existing + prefix + line + '\n';
111
110
  }
111
+ mkdirSync(dirname(envPath), { recursive: true });
112
112
  writeFileSync(envPath, next, { mode: 0o600 });
113
- ensureGitignored(projectDir, ENV_FILE);
114
- return { path: envPath, address: deriveFromPrivateKey(hex).address };
113
+ chmodSync(envPath, 0o600);
114
+ ensureGitignored(projectDir, relative(projectDir, envPath));
115
+ return envPath;
116
+ }
117
+ /**
118
+ * Persist a raw eth private key to the project `.env` as
119
+ * `RECLAIM_PRIVATE_KEY=0x…` (upsert; mode 0600; `.env` added to `.gitignore`).
120
+ * Used by `issue_credentials` (generate) and `import_credentials` (supplied
121
+ * hex). Returns the derived address and the `.env` path.
122
+ */
123
+ export function writeEthKey(privateKeyHex, projectDir = findProjectRoot(process.cwd())) {
124
+ const hex = normalizeEthHex(privateKeyHex);
125
+ const path = upsertEnvVar(projectDir, ENV_FILE, ENV_VAR, hex);
126
+ return { path, address: deriveFromPrivateKey(hex).address };
115
127
  }