@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
@@ -7,6 +7,11 @@ import {
7
7
  tailLogs,
8
8
  warnsAndErrors,
9
9
  } from '../../../proof/attestor.ts'
10
+ import {
11
+ type AttestorCheckResult,
12
+ type CheckAttestorOptions,
13
+ isNetworkError,
14
+ } from '../../../proof/check-attestor.ts'
10
15
 
11
16
  function claimsDir(): string {
12
17
  return join(reclaimHomeDirPath(), 'claims')
@@ -192,3 +197,53 @@ export function shapeRichProofResult(
192
197
 
193
198
  return out
194
199
  }
200
+
201
+ export interface ProofDiagnosis {
202
+ attestor: AttestorCheckResult
203
+ likelyCause: string
204
+ next: string
205
+ }
206
+
207
+ /**
208
+ * When an unverified proof failed with a network-like error, runs the
209
+ * attestor check once and says whether the attestor or the target site is the
210
+ * likely cause. Returns `undefined` for verified proofs and other errors.
211
+ */
212
+ export async function diagnoseNetworkFailure(
213
+ result: Pick<ProofResult, 'verified' | 'error' | 'errorKind'>,
214
+ check: (opts: CheckAttestorOptions) => Promise<AttestorCheckResult>,
215
+ attestorUrl?: string,
216
+ ): Promise<ProofDiagnosis | undefined> {
217
+ if(
218
+ result.verified
219
+ || result.errorKind !== undefined
220
+ || !isNetworkError(result.error)
221
+ ) {
222
+ return undefined
223
+ }
224
+
225
+ const attestor = await check(attestorUrl ? { attestorUrl } : {})
226
+ if(attestor.ok) {
227
+ return {
228
+ attestor,
229
+ likelyCause:
230
+ 'The attestor works from this machine, so the target site likely '
231
+ + 'blocks or drops the attestor\'s network (geo or IP restriction).',
232
+ next:
233
+ 'If the browser attach mode is "builder", call `run_proof_via_pod`. '
234
+ + 'Otherwise tell the developer and suggest asking the Reclaim team. '
235
+ + 'Do not retry the same proof.',
236
+ }
237
+ }
238
+
239
+ return {
240
+ attestor,
241
+ likelyCause:
242
+ 'The attestor check also failed, so this machine cannot reach the '
243
+ + 'attestor (network, VPN, firewall, or proxy).',
244
+ next:
245
+ 'Tell the developer. To see the connection details, suggest '
246
+ + 'restarting the MCP server with `RECLAIM_AGENT_TRACE=1`. Do not '
247
+ + 'retry the same proof.',
248
+ }
249
+ }
@@ -18,9 +18,9 @@ export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool {
18
18
  name: 'propose_provider',
19
19
  description:
20
20
  'Drafts a provider from one authenticated captured request and '
21
- + 'keeps captured secrets server-side. Use `targets` for every value '
22
- + 'available in the same response; the result contains a `draftId` '
23
- + 'and secret-free recipe. Set `shortenNames: false` to preserve an '
21
+ + 'keeps captured secrets server-side. Use `targets` for the data '
22
+ + 'points the claim needs from that response; returns a `draftId` '
23
+ + 'and a secret-free recipe. Set `shortenNames: false` to preserve an '
24
24
  + 'explicit parameter name, including a hidden `REQ_` or `URL_` '
25
25
  + 'prefix. For matching and naming guidance, call '
26
26
  + 'how_it_works({ topic: "draft" }) and topic `params`.',
@@ -63,6 +63,7 @@ export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool {
63
63
  },
64
64
  ['requestId', 'name'],
65
65
  ),
66
+ annotations: { openWorldHint: false, destructiveHint: false },
66
67
  },
67
68
  async(args) => {
68
69
  const backend = resolveBackend(args)
@@ -5,6 +5,7 @@ import type {
5
5
  } from '@reclaimprotocol/client/openapi'
6
6
  import assert from 'node:assert'
7
7
  import type { JSONSchema } from 'zod/v4/core'
8
+ import type { KeptScriptRef } from '../../../authoring/index.ts'
8
9
  import {
9
10
  ALLOWED_REQUESTS_HINT,
10
11
  COOKIE_ATTACHMENT_HINT,
@@ -232,13 +233,16 @@ interface PublishArgs {
232
233
  export function createVersionFromCaptureTool(
233
234
  client: ReclaimClient,
234
235
  attachRef?: { current?: AttachState },
236
+ kept?: KeptScriptRef,
235
237
  ): RegisteredTool {
236
238
  return defineTool<PublishArgs>(
237
239
  {
238
240
  name: 'create_provider_version_from_capture',
239
241
  description:
240
242
  'Creates a new immutable provider version after `run_proof` succeeds. '
241
- + 'Pass exactly one `provider` or `providers` value returned by '
243
+ + 'Publishes the navigation script kept by `test_user_script` as '
244
+ + '`jsUserScripts` unless `jsUserScripts` is passed. Pass exactly '
245
+ + 'one `provider` or `providers` value returned by '
242
246
  + '`propose_provider`; do not rebuild the recipe. The default is a '
243
247
  + 'patch bump, or use `bump` or an explicitly requested higher '
244
248
  + '`version`. The new version remains a draft until published or '
@@ -306,6 +310,8 @@ export function createVersionFromCaptureTool(
306
310
  jsUserScripts: {
307
311
  type: 'string',
308
312
  description:
313
+ 'Defaults to the script kept by `test_user_script` with ' +
314
+ '`keep: true`; pass it only to override. ' +
309
315
  USER_SCRIPT_HINT +
310
316
  ' Stored as webSettings.jsUserScripts; the Builder ' +
311
317
  'verification client runs it before every page load ' +
@@ -369,8 +375,10 @@ export function createVersionFromCaptureTool(
369
375
  },
370
376
  required: ['providerId', 'initialUrl'],
371
377
  },
378
+ annotations: { openWorldHint: true, destructiveHint: false },
372
379
  },
373
380
  async(args) => {
381
+ const jsUserScripts = args.jsUserScripts ?? kept?.keptScript?.script
374
382
  // Exactly one of `provider` / `providers` — normalize to an array.
375
383
  const hasMany = Array.isArray(args.providers) && args.providers.length > 0
376
384
  const hasOne = !!args.provider
@@ -396,7 +404,7 @@ export function createVersionFromCaptureTool(
396
404
  notes: args.notes,
397
405
  version: ver,
398
406
  requiredContext: args.requiredContext,
399
- jsUserScripts: args.jsUserScripts,
407
+ jsUserScripts,
400
408
  allowedJsRequests: args.allowedJsRequests,
401
409
  browserRequestActions: args.browserRequestActions,
402
410
  interceptorOptions: args.interceptorOptions,
@@ -428,12 +436,12 @@ export function createVersionFromCaptureTool(
428
436
  const attached = attachRef?.current
429
437
  notes.push(...postPublishBrowserNotes(attached, {
430
438
  initialUrl: args.initialUrl,
431
- hasUserScript: !!args.jsUserScripts?.trim(),
439
+ hasUserScript: !!jsUserScripts?.trim(),
432
440
  hasInterceptor: !!args.interceptorOptions,
433
441
  }))
434
442
 
435
443
  // Let the developer know how and when the script runs.
436
- if(args.jsUserScripts?.trim()) {
444
+ if(jsUserScripts?.trim()) {
437
445
  notes.push(
438
446
  'jsUserScripts will run in the Builder verification client before ' +
439
447
  'every page load (before navigating to initialUrl), matching ' +
@@ -13,13 +13,15 @@ export function replayTool(resolveBackend: ResolveBackend): RegisteredTool {
13
13
  {
14
14
  name: 'replay_request',
15
15
  description:
16
- 'Validate the drafted recipe before proving — confirm its selectors '
17
- + 'extract the target value. Returns diagnostics; run before '
18
- + 'proving. Set `withoutSecrets: true` for the AUTH-BOUND check: it '
19
- + 're-issues the request with NO secrets/cookies (an anonymous '
20
- + 'visitor). If the values still extract (valid:true), the endpoint '
21
- + 'is PUBLIC and the proof would not bind to the user — reject it '
22
- + 'and find an auth-bound request instead.',
16
+ 'Re-sends the drafted request from inside the attached authoring '
17
+ + 'browser (not from this machine) and checks that its selectors '
18
+ + 'extract the target data points. Captured secrets stay server-side '
19
+ + 'and are not returned. Returns diagnostics. Run it before '
20
+ + '`run_proof`. With `withoutSecrets: true`, sends the request with '
21
+ + 'no cookies or auth headers, like an anonymous visitor. If the '
22
+ + 'values still extract (`valid: true`), the endpoint is public and '
23
+ + 'the proof would not bind to the user: choose an auth-bound '
24
+ + 'request instead.',
23
25
  inputSchema: objectSchema(
24
26
  {
25
27
  draftId: { type: 'string' },
@@ -27,6 +29,7 @@ export function replayTool(resolveBackend: ResolveBackend): RegisteredTool {
27
29
  },
28
30
  ['draftId'],
29
31
  ),
32
+ annotations: { readOnlyHint: true, openWorldHint: true },
30
33
  },
31
34
  async(args) => {
32
35
  const backend = resolveBackend(args)
@@ -12,10 +12,13 @@ export function sessionTools(
12
12
  {
13
13
  name: 'get_session',
14
14
  description:
15
- 'Report the current attach state, the live captureIds, and the '
16
- + 'persisted browser config. Call it to check whether a browser is '
17
- + 'still attached before reattaching, or to recover a captureId.',
15
+ 'Returns the current attach state, the live captureIds, the '
16
+ + 'persisted browser config, and the navigation script kept by '
17
+ + '`test_user_script`. Call it to check whether a browser is still '
18
+ + 'attached before reattaching, or to recover a captureId or the '
19
+ + 'kept script.',
18
20
  inputSchema: { type: 'object', properties: {} },
21
+ annotations: { readOnlyHint: true, openWorldHint: false },
19
22
  },
20
23
  async() => ({
21
24
  config: attachRef.config.read()?.browser_agent,
@@ -27,16 +30,19 @@ export function sessionTools(
27
30
  orgId: attachRef.current?.orgId,
28
31
  captures: [...captures.sessions.keys()],
29
32
  activeTab: captures.activeTab,
33
+ keptUserScript: captures.keptScript?.script,
30
34
  }),
31
35
  ),
32
36
  defineTool<Record<string, never>>(
33
37
  {
34
38
  name: 'dispose_browser',
35
39
  description:
36
- 'Stop captures, disconnect the current browser, and immediately '
37
- + 'delete a Builder-hosted Popcorn session. Custom CDP sessions '
38
- + 'are disconnected but never deleted by Reclaim.',
40
+ 'Stops captures, disconnects the current browser, and deletes a '
41
+ + 'Builder-hosted browser session at once. Clears the kept '
42
+ + 'navigation script. Reclaim disconnects a custom CDP browser but '
43
+ + 'never deletes it.',
39
44
  inputSchema: { type: 'object', properties: {} },
45
+ annotations: { destructiveHint: true, openWorldHint: true },
40
46
  },
41
47
  async() => {
42
48
  await resetAll(attachRef, captures)
@@ -47,9 +53,11 @@ export function sessionTools(
47
53
  {
48
54
  name: 'reset_session',
49
55
  description:
50
- 'Stop all captures and clear capture state — drops every '
51
- + 'session\'s drafts and their in-memory secrets.',
56
+ 'Stops all captures and clears capture state, dropping every '
57
+ + 'draft and its in-memory secrets. Also disconnects the browser '
58
+ + 'and clears the kept navigation script.',
52
59
  inputSchema: { type: 'object', properties: {} },
60
+ annotations: { destructiveHint: true, openWorldHint: true },
53
61
  },
54
62
  async() => {
55
63
  await resetAll(attachRef, captures)
@@ -12,13 +12,15 @@ import {
12
12
  type AuthoringBackend,
13
13
  authoringTools,
14
14
  extendTool,
15
+ type KeptScriptRef,
15
16
  } from '../../../authoring/index.ts'
16
17
  import { runAttestorProof } from '../../../proof/attestor.ts'
18
+ import { checkAttestor } from '../../../proof/check-attestor.ts'
17
19
  import { replayProviderInPage } from '../../../proof/page-replay.ts'
18
20
  import type { OprfMode, ReclaimProvider } from '../../../provider/schema.ts'
19
21
  import type { RegisteredTool } from '../../server.ts'
20
22
  import type { CaptureEntry, CaptureStore } from './capture.ts'
21
- import { shapeRichProofResult } from './proof.ts'
23
+ import { diagnoseNetworkFailure, shapeRichProofResult } from './proof.ts'
22
24
 
23
25
  /**
24
26
  * OPRF mode this LOCAL (non-TEE) attestor can actually run. Recipes carry
@@ -54,13 +56,18 @@ function withLocalOprfMode(provider: ReclaimProvider): ReclaimProvider {
54
56
  /** The local backend for one capture: drive its tab over CDP, read its
55
57
  * captured requests, prove via the in-process attestor, replay by re-fetching
56
58
  * FROM INSIDE the tab (real cookies/session — see proof/page-replay.ts). */
57
- function localBackend(entry: CaptureEntry): AuthoringBackend {
59
+ function localBackend(
60
+ entry: CaptureEntry,
61
+ kept: KeptScriptRef,
62
+ check: typeof checkAttestor,
63
+ ): AuthoringBackend {
58
64
  const { tabClient, session, drafts } = entry
59
65
  const sendCdp = (method: string, params?: Record<string, unknown>) => {
60
66
  return tabClient.send(method, params ?? {})
61
67
  }
62
68
 
63
69
  return {
70
+ kept,
64
71
  capture: session,
65
72
  drafts,
66
73
  sendCdp,
@@ -107,11 +114,15 @@ function localBackend(entry: CaptureEntry): AuthoringBackend {
107
114
  ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
108
115
  },
109
116
  )
110
- return shapeRichProofResult(result, {
117
+ const shaped = shapeRichProofResult(result, {
111
118
  provider: draft.provider,
112
119
  hasSecrets: Object.keys(draft.secrets).length > 0,
113
120
  ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
114
121
  })
122
+ const diagnosis = await diagnoseNetworkFailure(
123
+ result, check, opts?.attestorUrl,
124
+ )
125
+ return diagnosis ? { ...shaped, diagnosis } : shaped
115
126
  },
116
127
  }
117
128
  }
@@ -121,25 +132,26 @@ function localBackend(entry: CaptureEntry): AuthoringBackend {
121
132
  * of this). */
122
133
  const RUN_PROOF_EXTRAS = {
123
134
  description:
124
- 'Run a full zkTLS proof of the drafted recipe through the Reclaim '
125
- + 'attestor SDK. Use only after replay_request looks right — attestor '
126
- + 'calls are expensive. Writes the full claim to '
127
- + '~/.reclaim/claims/<timestamp>.json and returns identifier + '
128
- + 'extractedParameters + claimPath + a prettified `claim` (request '
129
- + 'dropped, byte arrays as base64). With RECLAIM_AGENT_TRACE=1, also '
130
- + 'writes a per-call diagnostic dump (input provider, sentParams, full '
131
- + 'attestor log stream, result) and returns it as `runLogPath`. '
132
- + '`attestorUrl` overrides the default only when the dev supplies a '
133
- + 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
134
- + '(the credential to sign with) is REQUIRED — call resolve_owner_key '
135
- + 'first. errorKind="missing-owner-key" means that address has no known '
136
- + 'private key; re-run resolve_owner_key or issue/import a credential. '
137
- + 'NOTE: unlike replay_request (which re-issues from inside the live '
138
- + 'attached browser tab), the ATTESTOR calls the target from its OWN '
139
- + 'network/IP, separate from the browser\'s. A request that replayed '
140
- + 'fine can still fail here for geo/IP-bound content (country-locked '
141
- + "pages, IP-allowlisted APIs) — if that happens, it's a property of "
142
- + 'the site, not a bug in the draft.',
135
+ 'Proves the drafted recipe with a full zkTLS proof through the attestor '
136
+ + 'SDK. Call it after `replay_request` looks right; attestor calls are '
137
+ + 'expensive. Call `resolve_owner_key` first and pass its address as '
138
+ + '`ownerAddress`. Writes the full claim to '
139
+ + '`~/.reclaim/claims/<timestamp>.json`. Returns `identifier`, '
140
+ + '`extractedParameters`, `claimPath`, and a readable `claim` (request '
141
+ + 'removed, byte arrays as base64). Set `RECLAIM_AGENT_TRACE=1` to also '
142
+ + 'get `runLogPath`, a diagnostic dump with the attestor log stream. '
143
+ + 'Set `attestorUrl` only if the developer supplied one; never invent '
144
+ + 'backup URLs. If `errorKind` is `missing-owner-key`, the address has '
145
+ + 'no known private key: run `resolve_owner_key` again, or issue or '
146
+ + 'import a credential. Unlike `replay_request`, which re-sends from '
147
+ + 'inside the attached browser tab, the attestor calls the target from '
148
+ + 'its own network and IP. A request that replayed can still fail here '
149
+ + 'for geo- or IP-bound content, such as country-locked pages or '
150
+ + 'IP-allowlisted APIs. That is a property of the site, not a draft '
151
+ + 'bug. After a network-like failure ("connection closed", '
152
+ + '`ERROR_NETWORK_ERROR`, timeout), the result includes `diagnosis` '
153
+ + 'with `attestor` (a `check_attestor` result), `likelyCause`, and '
154
+ + '`next`. Follow `next`; do not retry the same proof.',
143
155
  properties: {
144
156
  ownerAddress: {
145
157
  type: 'string',
@@ -163,7 +175,10 @@ const RUN_PROOF_EXTRAS = {
163
175
  * takes `captureId`; `run_proof` also gets the credential-aware contract. The
164
176
  * tools are authored plain in `../../../authoring`; this only wraps them with
165
177
  * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
166
- export function sharedAuthoringTools(captures: CaptureStore): RegisteredTool[] {
178
+ export function sharedAuthoringTools(
179
+ captures: CaptureStore,
180
+ check: typeof checkAttestor = checkAttestor,
181
+ ): RegisteredTool[] {
167
182
  const resolveBackend = (args: Record<string, unknown>): AuthoringBackend => {
168
183
  const captureId = String(args.captureId)
169
184
  const entry = captures.sessions.get(captureId)
@@ -174,7 +189,7 @@ export function sharedAuthoringTools(captures: CaptureStore): RegisteredTool[] {
174
189
  )
175
190
  }
176
191
 
177
- return localBackend(entry)
192
+ return localBackend(entry, captures, check)
178
193
  }
179
194
 
180
195
  return authoringTools(resolveBackend).map((tool) => {
@@ -0,0 +1,84 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { defineTool, type RegisteredTool } from '../../server.ts'
4
+
5
+ interface StartIntegrationArgs {
6
+ goal: string
7
+ orgId?: string
8
+ providerIds?: string[]
9
+ }
10
+
11
+ /**
12
+ * Returns the plan a coding agent follows to add Reclaim verification to the
13
+ * developer's app, from sign-in to a tested Verify button. The plan lives in
14
+ * `skill/start-integration.md`; this tool fills in the inputs.
15
+ */
16
+ export function startIntegrationTool(): RegisteredTool {
17
+ return defineTool<StartIntegrationArgs>(
18
+ {
19
+ name: 'start_integration',
20
+ description:
21
+ 'Returns a step-by-step plan for adding Reclaim verification to the '
22
+ + 'current codebase: sign in, set up the app (session route, callback, '
23
+ + 'Verify button, local event proxy) in parallel with building or '
24
+ + 'choosing the provider, then wire the provider and test. Call it '
25
+ + 'first when a developer wants their users to prove something, and '
26
+ + 'follow the plan to the end. Changes nothing itself.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ required: ['goal'],
30
+ properties: {
31
+ goal: {
32
+ type: 'string',
33
+ minLength: 1,
34
+ maxLength: 300,
35
+ description:
36
+ 'What the developer\'s users prove, for example, "they have '
37
+ + 'a GitHub account with 100 followers".',
38
+ },
39
+ orgId: {
40
+ type: 'string',
41
+ format: 'uuid',
42
+ description: 'Organization to use. Omit to choose from the list.',
43
+ },
44
+ providerIds: {
45
+ type: 'array',
46
+ maxItems: 10,
47
+ items: { type: 'string' },
48
+ description:
49
+ 'Existing provider IDs to use. Omit to build a new provider.',
50
+ },
51
+ },
52
+ },
53
+ annotations: { readOnlyHint: true, openWorldHint: false },
54
+ },
55
+ async(args) => renderStartIntegration(args),
56
+ )
57
+ }
58
+
59
+ /** Fill the plan template. Exported for tests. */
60
+ export function renderStartIntegration(args: StartIntegrationArgs): string {
61
+ const providerIds = args.providerIds?.length
62
+ ? args.providerIds.map((id) => `\`${id}\``).join(', ')
63
+ : undefined
64
+ const values: Record<string, string | undefined> = {
65
+ goal: args.goal,
66
+ orgId: args.orgId,
67
+ providerIds,
68
+ }
69
+ const template = readFileSync(
70
+ fileURLToPath(
71
+ new URL('../../../../skill/start-integration.md', import.meta.url),
72
+ ),
73
+ 'utf8',
74
+ )
75
+ return template
76
+ // Keep a conditional block only when its input was given (or not given).
77
+ .replace(
78
+ /<!-- (if|ifnot):(\w+) -->\n([\s\S]*?)<!-- endif -->\n/g,
79
+ (_, kind: string, name: string, body: string) => {
80
+ return (kind === 'if') === Boolean(values[name]) ? body : ''
81
+ },
82
+ )
83
+ .replace(/\{\{(\w+)\}\}/g, (_, name: string) => values[name] ?? '')
84
+ }
@@ -25,18 +25,16 @@ export function authenticateTool(
25
25
  + 'get_me_providers; those need the separate old-devtools login, '
26
26
  + 'reclaim_authenticate. Check both with get_devtools_mode. '
27
27
  : '')
28
- + 'Authorize the CLI / MCP to make authenticated API calls. Opens '
29
- + 'the '
30
- + 'browser to the builder sign-in page (standard OAuth 2.0 ' +
31
- 'authorization-code + PKCE with a localhost redirect) and waits for ' +
32
- 'the user to sign in and approve. On success the access token is ' +
33
- 'cached in ~/.reclaim/config.json and activated for this session.\n\n' +
34
- 'Call with no arguments. This bearer token authenticates ' +
35
- 'dashboard/org-management calls (for example, list_orgs, or ' +
36
- 'issuing an org secret). Programmatic verification calls use a ' +
37
- 'separate org secret ' +
38
- '(rorg_…, set as RECLAIM_ORG_SECRET).',
28
+ + 'Signs the developer in to Builder. Opens the Builder sign-in page '
29
+ + 'in their browser (OAuth 2.0 authorization code with PKCE, '
30
+ + 'redirecting to a localhost port this server opens) and waits for '
31
+ + 'them to approve. Saves the access token in ~/.reclaim/config.json '
32
+ + 'with mode 0600 and uses it for this session; the token is never '
33
+ + 'returned. The token authorizes organization-management tools such '
34
+ + 'as `list_orgs`. Programmatic verification tools use a separate '
35
+ + 'organization key; see `issue_org_token`. Call with no arguments.',
39
36
  inputSchema: { type: 'object', properties: {} },
37
+ annotations: { openWorldHint: true, idempotentHint: true },
40
38
  },
41
39
  async() => runOAuthLoopback(client),
42
40
  )
@@ -3,6 +3,7 @@ import type { RegisteredTool } from '../../server.ts'
3
3
  import { authenticateTool } from './authenticate.ts'
4
4
  import { importTool } from './import.ts'
5
5
  import { issueTool } from './issue.ts'
6
+ import { issueOrgTokenTool } from './issue-org-token.ts'
6
7
  import { resolveOwnerKeyTool } from './resolve-owner-key.ts'
7
8
 
8
9
  /**
@@ -27,6 +28,7 @@ export function buildLocalCredentialTools(): RegisteredTool[] {
27
28
  export function buildCredentialTools(client: ReclaimClient): RegisteredTool[] {
28
29
  return [
29
30
  authenticateTool(client),
31
+ issueOrgTokenTool(client),
30
32
  ...buildLocalCredentialTools(),
31
33
  ]
32
34
  }
@@ -35,6 +35,7 @@ export function buildBuilderOrganizationTools(
35
35
  cursor: { type: 'string', maxLength: 500 },
36
36
  },
37
37
  },
38
+ annotations: { readOnlyHint: true, openWorldHint: true },
38
39
  },
39
40
  async({ cursor }) => {
40
41
  const { data } = await client.call('ListOrgs', {
@@ -16,8 +16,20 @@
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 {
20
+ chmodSync,
21
+ existsSync,
22
+ mkdirSync,
23
+ readFileSync,
24
+ writeFileSync,
25
+ } from 'node:fs'
26
+ import {
27
+ dirname,
28
+ isAbsolute,
29
+ join,
30
+ relative,
31
+ resolve
32
+ } from 'node:path'
21
33
  import { privateKey, privateKeyFile } from '../../../consts.ts'
22
34
  import { findProjectRoot } from '../../../paths.ts'
23
35
 
@@ -124,7 +136,7 @@ export function resolveEthKey(
124
136
  }
125
137
 
126
138
  /** Append `entry` to the project `.gitignore` if not already present. */
127
- function ensureGitignored(projectDir: string, entry: string) {
139
+ export function ensureGitignored(projectDir: string, entry: string) {
128
140
  const path = join(projectDir, '.gitignore')
129
141
  const current = existsSync(path) ? readFileSync(path, 'utf8') : ''
130
142
  if(current.split('\n').some((l) => l.trim() === entry)) {
@@ -136,29 +148,47 @@ function ensureGitignored(projectDir: string, entry: string) {
136
148
  }
137
149
 
138
150
  /**
139
- * Persist a raw eth private key to the project `.env` as
140
- * `RECLAIM_PRIVATE_KEY=0x…` (upsert; mode 0600; `.env` added to `.gitignore`).
141
- * Used by `issue_credentials` (generate) and `import_credentials` (supplied
142
- * hex). Returns the derived address and the `.env` path.
151
+ * Set `name=value` in a dotenv file, replacing an existing line or appending
152
+ * one. Writes with mode 0600 and adds the file to the project `.gitignore`.
153
+ * `envFile` is relative to `projectDir`.
143
154
  */
144
- export function writeEthKey(
145
- privateKeyHex: string,
146
- projectDir = findProjectRoot(process.cwd()),
147
- ): { path: string, address: string } {
148
- const hex = normalizeEthHex(privateKeyHex)
149
- const envPath = join(projectDir, ENV_FILE)
150
- const line = `${ENV_VAR}=${hex}`
155
+ export function upsertEnvVar(
156
+ projectDir: string,
157
+ envFile: string,
158
+ name: string,
159
+ value: string,
160
+ ): string {
161
+ const envPath = resolve(projectDir, envFile)
162
+ const line = `${name}=${value}`
151
163
  const existing = existsSync(envPath) ? readFileSync(envPath, 'utf8') : ''
152
- const re = new RegExp(`^\\s*${ENV_VAR}\\s*=.*$`, 'm')
164
+ const re = new RegExp(`^\\s*${name}\\s*=.*$`, 'm')
153
165
  let next: string
154
166
  if(re.test(existing)) {
155
- next = existing.replace(re, line)
167
+ // A function replacer keeps `$` sequences in the value literal.
168
+ next = existing.replace(re, () => line)
156
169
  } else {
157
170
  const prefix = existing && !existing.endsWith('\n') ? '\n' : ''
158
171
  next = existing + prefix + line + '\n'
159
172
  }
160
173
 
174
+ mkdirSync(dirname(envPath), { recursive: true })
161
175
  writeFileSync(envPath, next, { mode: 0o600 })
162
- ensureGitignored(projectDir, ENV_FILE)
163
- return { path: envPath, address: deriveFromPrivateKey(hex).address }
176
+ chmodSync(envPath, 0o600)
177
+ ensureGitignored(projectDir, relative(projectDir, envPath))
178
+ return envPath
179
+ }
180
+
181
+ /**
182
+ * Persist a raw eth private key to the project `.env` as
183
+ * `RECLAIM_PRIVATE_KEY=0x…` (upsert; mode 0600; `.env` added to `.gitignore`).
184
+ * Used by `issue_credentials` (generate) and `import_credentials` (supplied
185
+ * hex). Returns the derived address and the `.env` path.
186
+ */
187
+ export function writeEthKey(
188
+ privateKeyHex: string,
189
+ projectDir = findProjectRoot(process.cwd()),
190
+ ): { path: string, address: string } {
191
+ const hex = normalizeEthHex(privateKeyHex)
192
+ const path = upsertEnvVar(projectDir, ENV_FILE, ENV_VAR, hex)
193
+ return { path, address: deriveFromPrivateKey(hex).address }
164
194
  }