@reclaimprotocol/agent 0.1.36 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +8 -8
  2. package/lib/authoring/backend.d.ts +12 -1
  3. package/lib/authoring/backend.js +3 -0
  4. package/lib/authoring/tools.js +33 -5
  5. package/lib/authoring/util.d.ts +43 -0
  6. package/lib/authoring/util.js +54 -0
  7. package/lib/cdp/chrome-for-testing.d.ts +14 -0
  8. package/lib/cdp/chrome-for-testing.js +43 -0
  9. package/lib/cdp/geo.d.ts +3 -0
  10. package/lib/cdp/geo.js +27 -15
  11. package/lib/cdp/launcher.d.ts +79 -1
  12. package/lib/cdp/launcher.js +244 -24
  13. package/lib/logger.js +3 -1
  14. package/lib/mcp/instructions.js +3 -3
  15. package/lib/mcp/start.js +17 -2
  16. package/lib/mcp/tools/agent/attach.d.ts +6 -0
  17. package/lib/mcp/tools/agent/attach.js +58 -118
  18. package/lib/mcp/tools/agent/build.js +6 -0
  19. package/lib/mcp/tools/agent/capture.js +33 -10
  20. package/lib/mcp/tools/agent/check-attestor.js +15 -7
  21. package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
  22. package/lib/mcp/tools/agent/draft-tools.js +170 -0
  23. package/lib/mcp/tools/agent/pod-proof.js +5 -1
  24. package/lib/mcp/tools/agent/proof.d.ts +28 -0
  25. package/lib/mcp/tools/agent/proof.js +72 -0
  26. package/lib/mcp/tools/agent/shared.d.ts +8 -1
  27. package/lib/mcp/tools/agent/shared.js +98 -19
  28. package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
  29. package/lib/mcp/tools/agent/start-integration.js +10 -6
  30. package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
  31. package/lib/mcp/tools/authenticate/authenticate.js +212 -116
  32. package/lib/mcp/tools/authenticate/build.d.ts +2 -2
  33. package/lib/mcp/tools/authenticate/build.js +6 -3
  34. package/lib/mcp/tools/authenticate/builder-access.js +5 -2
  35. package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
  36. package/lib/mcp/tools/authenticate/eth-key.js +29 -5
  37. package/lib/mcp/tools/authenticate/index.d.ts +1 -1
  38. package/lib/mcp/tools/authenticate/index.js +1 -1
  39. package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
  40. package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
  41. package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
  42. package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
  43. package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
  44. package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
  45. package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
  46. package/lib/mcp/tools/authenticate/session-cache.js +95 -4
  47. package/lib/mcp/tools/mode/mode.js +6 -6
  48. package/lib/old/login-cdp.js +2 -3
  49. package/lib/proof/attestor.js +2 -3
  50. package/lib/proof/check-attestor.d.ts +12 -2
  51. package/lib/proof/check-attestor.js +40 -10
  52. package/lib/proof/check-matchers.d.ts +36 -0
  53. package/lib/proof/check-matchers.js +161 -0
  54. package/lib/proof/zk-files.d.ts +50 -11
  55. package/lib/proof/zk-files.js +259 -63
  56. package/lib/provider/draft.d.ts +24 -1
  57. package/lib/provider/draft.js +135 -0
  58. package/lib/provider/matcher.js +171 -4
  59. package/package.json +6 -4
  60. package/skill/SKILL.builder.md +7 -2
  61. package/skill/SKILL.common.md +90 -70
  62. package/skill/SKILL.old.md +3 -3
  63. package/skill/builder-mode.md +35 -5
  64. package/skill/old-mode.md +18 -13
  65. package/skill/start-integration.md +48 -33
  66. package/skill/verification-client-integration.md +21 -6
  67. package/src/authoring/backend.ts +12 -1
  68. package/src/authoring/tools.ts +48 -7
  69. package/src/authoring/util.ts +64 -0
  70. package/src/cdp/chrome-for-testing.ts +66 -0
  71. package/src/cdp/geo.ts +36 -21
  72. package/src/cdp/launcher.ts +328 -24
  73. package/src/logger.ts +3 -1
  74. package/src/mcp/instructions.ts +3 -3
  75. package/src/mcp/start.ts +20 -1
  76. package/src/mcp/tools/agent/attach.ts +65 -128
  77. package/src/mcp/tools/agent/build.ts +7 -0
  78. package/src/mcp/tools/agent/capture.ts +54 -11
  79. package/src/mcp/tools/agent/check-attestor.ts +15 -7
  80. package/src/mcp/tools/agent/draft-tools.ts +234 -0
  81. package/src/mcp/tools/agent/pod-proof.ts +5 -1
  82. package/src/mcp/tools/agent/proof.ts +110 -1
  83. package/src/mcp/tools/agent/shared.ts +130 -19
  84. package/src/mcp/tools/agent/start-integration.ts +10 -6
  85. package/src/mcp/tools/authenticate/authenticate.ts +276 -131
  86. package/src/mcp/tools/authenticate/build.ts +6 -3
  87. package/src/mcp/tools/authenticate/builder-access.ts +5 -2
  88. package/src/mcp/tools/authenticate/eth-key.ts +41 -7
  89. package/src/mcp/tools/authenticate/index.ts +1 -1
  90. package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
  91. package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
  92. package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
  93. package/src/mcp/tools/authenticate/session-cache.ts +125 -5
  94. package/src/mcp/tools/mode/mode.ts +6 -6
  95. package/src/old/login-cdp.ts +2 -3
  96. package/src/proof/attestor.ts +2 -3
  97. package/src/proof/check-attestor.ts +55 -12
  98. package/src/proof/check-matchers.ts +227 -0
  99. package/src/proof/zk-files.ts +357 -65
  100. package/src/provider/draft.ts +215 -7
  101. package/src/provider/matcher.ts +237 -4
@@ -36,7 +36,10 @@ export function podProofTools(
36
36
  + 'instead of `run_proof` when the attach mode is "builder": the '
37
37
  + 'proof then reaches the target from the browser\'s own exit IP and '
38
38
  + 'country. Needs no `ownerAddress`; the pod signs with its own TEE '
39
- + 'key.',
39
+ + 'key. It matches the browser\'s exit IP and country, not '
40
+ + 'necessarily its TLS fingerprint, so it is not a way past bot '
41
+ + 'protection: if `run_proof` reported '
42
+ + '`blocked-non-browser-client`, expect the same here.',
40
43
  inputSchema: {
41
44
  type: 'object',
42
45
  properties: {
@@ -78,6 +81,7 @@ export function podProofTools(
78
81
  return shapeRichProofResult(result, {
79
82
  provider: draft.provider,
80
83
  hasSecrets: Object.keys(draft.secrets).length > 0,
84
+ ...(draft.lastReplay ? { lastReplay: draft.lastReplay } : {}),
81
85
  })
82
86
  },
83
87
  )
@@ -109,7 +109,15 @@ function isLikelyByteArray(v: unknown[]): boolean {
109
109
  */
110
110
  export function shapeRichProofResult(
111
111
  result: ProofResult,
112
- opts: { provider: unknown, hasSecrets: boolean, attestorUrl?: string },
112
+ opts: {
113
+ provider: unknown
114
+ hasSecrets: boolean
115
+ attestorUrl?: string
116
+ /** The draft's latest authenticated in-browser replay, when tracked. */
117
+ lastReplay?: { status: number, ok: boolean }
118
+ /** Where the attestor's request went out from. */
119
+ egress?: { geoLocation?: string }
120
+ },
113
121
  ): Record<string, unknown> {
114
122
  // Persist the full claim to disk for the dev to inspect/share.
115
123
  let claimPath: string | undefined
@@ -153,6 +161,13 @@ export function shapeRichProofResult(
153
161
  out.errorKind = result.errorKind
154
162
  }
155
163
 
164
+ const blocked = diagnoseBlockedClient(
165
+ result, opts.lastReplay, opts.egress?.geoLocation,
166
+ )
167
+ if(blocked !== undefined) {
168
+ out.diagnosis = blocked
169
+ }
170
+
156
171
  // Trim the LLM-visible logs — full stream goes to runLogPath.
157
172
  if(result.attestorLogs !== undefined && result.attestorLogs.length > 0) {
158
173
  const logs = result.verified
@@ -198,6 +213,100 @@ export function shapeRichProofResult(
198
213
  return out
199
214
  }
200
215
 
216
+ /** HTTP statuses a site's bot protection answers non-browser clients with. */
217
+ const BLOCKING_STATUSES = new Set([403, 406, 429, 503])
218
+
219
+ /** Markers of a bot-protection / challenge page rather than real content. */
220
+ const CHALLENGE_BODY = new RegExp([
221
+ 'captcha', 'challenge', 'cf-chl', 'cf-ray', 'just a moment',
222
+ 'access denied', 'request unsuccessful', '_incapsula_', 'px-captcha',
223
+ 'perimeterx', 'datadome', 'akamai', 'bot detection', 'enable javascript',
224
+ 'are you a robot', 'unusual traffic',
225
+ ].join('|'), 'i')
226
+
227
+ /** Below this, a response the browser rendered with the value in it is
228
+ * likely a shell or challenge page served to a non-browser client. */
229
+ const TINY_BODY_BYTES = 4096
230
+
231
+ export interface BlockedClientDiagnosis {
232
+ kind: 'blocked-non-browser-client'
233
+ /** HTTP status the attestor's request got, when the error carries one. */
234
+ status?: number
235
+ /** Set when the same draft's in-browser replay succeeded. */
236
+ replayedInBrowser?: true
237
+ likelyCause: string
238
+ next: string
239
+ }
240
+
241
+ /**
242
+ * Says when a failed proof looks like the site blocking non-browser clients:
243
+ * the attestor's request got 403/406/429/503, or a tiny or challenge-looking
244
+ * page where the browser got the real one. Returns undefined for verified
245
+ * proofs and other failures. The raw `error` stays on the result.
246
+ */
247
+ export function diagnoseBlockedClient(
248
+ result: Pick<ProofResult, 'verified' | 'error'>,
249
+ lastReplay?: { status: number, ok: boolean },
250
+ geoLocation?: string,
251
+ ): BlockedClientDiagnosis | undefined {
252
+ if(result.verified || !result.error) {
253
+ return undefined
254
+ }
255
+
256
+ const replayedInBrowser = lastReplay?.ok === true
257
+ const statusMatch = new RegExp(
258
+ 'Expected status 2xx, got (\\d{3})|Provider returned error (\\d{3})',
259
+ ).exec(result.error)
260
+ const status = Number(statusMatch?.[1] ?? statusMatch?.[2]) || undefined
261
+ const blockedStatus = status !== undefined && BLOCKING_STATUSES.has(status)
262
+ if(!blockedStatus && !(replayedInBrowser && challengeBody(result.error))) {
263
+ return undefined
264
+ }
265
+
266
+ return {
267
+ kind: 'blocked-non-browser-client',
268
+ ...(status !== undefined ? { status } : {}),
269
+ ...(replayedInBrowser ? { replayedInBrowser: true as const } : {}),
270
+ likelyCause:
271
+ (replayedInBrowser
272
+ ? 'The same request succeeded from inside the browser, but '
273
+ : '')
274
+ + 'the site answered the attestor'
275
+ + (geoLocation
276
+ ? `, whose request went out from ${geoLocation}, with `
277
+ : ', whose request went out from its own network, with ')
278
+ + (blockedStatus ? `HTTP ${status}` : 'a challenge or shell page')
279
+ + '. The site blocks traffic from that country, or non-browser '
280
+ + 'clients (bot protection or TLS fingerprinting). Copying headers or '
281
+ + 'cookies will not help.',
282
+ next:
283
+ 'If the site serves one country (for example, a US airline), retry '
284
+ + 'run_proof once with `geoLocation` set to that country. If that '
285
+ + 'verifies, pin it with update_draft({ geoLocation }) so the '
286
+ + 'published version goes out from there too. Otherwise, try a '
287
+ + 'different endpoint that returns the value, such as a JSON API the '
288
+ + 'page calls (find_requests_containing), then propose and prove '
289
+ + 'that. If none works, stop and report to the developer that this '
290
+ + 'site blocks the attestor. Do not tweak headers.',
291
+ }
292
+ }
293
+
294
+ /** True when an attestor error embeds a response element (base64, from a
295
+ * regex redaction that didn't match) that is tiny or a challenge page. */
296
+ function challengeBody(error: string): boolean {
297
+ const b64 = /does not match found element '([A-Za-z0-9+/=]*)/.exec(error)?.[1]
298
+ if(b64 === undefined) {
299
+ return false
300
+ }
301
+
302
+ // "Tiny" only means something for a whole document; an xPath-selected
303
+ // element is small by design.
304
+ const text = Buffer.from(b64, 'base64').toString('utf8')
305
+ const tinyDocument = Buffer.byteLength(text) < TINY_BODY_BYTES
306
+ && /<!doctype|<html/i.test(text)
307
+ return tinyDocument || CHALLENGE_BODY.test(text)
308
+ }
309
+
201
310
  export interface ProofDiagnosis {
202
311
  attestor: AttestorCheckResult
203
312
  likelyCause: string
@@ -14,11 +14,21 @@ import {
14
14
  extendTool,
15
15
  type KeptScriptRef,
16
16
  } from '../../../authoring/index.ts'
17
+ import { lookupIpCountry } from '../../../cdp/geo.ts'
17
18
  import { runAttestorProof } from '../../../proof/attestor.ts'
18
19
  import { checkAttestor } from '../../../proof/check-attestor.ts'
19
20
  import { replayProviderInPage } from '../../../proof/page-replay.ts'
20
- import type { OprfMode, ReclaimProvider } from '../../../provider/schema.ts'
21
+ import {
22
+ DYNAMIC_GEO,
23
+ normalizeGeoLocation,
24
+ type OprfMode,
25
+ type ReclaimProvider,
26
+ } from '../../../provider/schema.ts'
21
27
  import type { RegisteredTool } from '../../server.ts'
28
+ import {
29
+ GENERATED_KEY_NOTE,
30
+ resolveOrCreateEthKey,
31
+ } from '../authenticate/eth-key.ts'
22
32
  import type { CaptureEntry, CaptureStore } from './capture.ts'
23
33
  import { diagnoseNetworkFailure, shapeRichProofResult } from './proof.ts'
24
34
 
@@ -53,6 +63,42 @@ function withLocalOprfMode(provider: ReclaimProvider): ReclaimProvider {
53
63
  }
54
64
  }
55
65
 
66
+ /** Templates a verification client replaces with the end user's country. */
67
+ const DYNAMIC_GEO_TEMPLATES = new Set([DYNAMIC_GEO, '{{DYNAMIC_GEO_SECRET}}'])
68
+
69
+ export interface ProofEgress {
70
+ /** ISO country (or param template) the attestor's request goes out from;
71
+ * absent when it goes out from the attestor's own network. */
72
+ geoLocation?: string
73
+ source: 'argument' | 'draft' | 'developer-ip' | 'none'
74
+ }
75
+
76
+ /**
77
+ * Where a local proof's request goes out from, resolved the way a
78
+ * verification client does it: the `geoLocation` argument, else the draft's
79
+ * fixed country, else (unset or `{{DYNAMIC_GEO}}`) the developer's own IP
80
+ * country, which is what `{{DYNAMIC_GEO}}` becomes when they verify.
81
+ */
82
+ async function resolveProofEgress(
83
+ provider: ReclaimProvider,
84
+ argument: string | undefined,
85
+ lookupCountry: () => Promise<string | undefined>,
86
+ ): Promise<ProofEgress> {
87
+ if(argument) {
88
+ return { geoLocation: argument.toUpperCase(), source: 'argument' }
89
+ }
90
+
91
+ const fixed = normalizeGeoLocation(provider.geoLocation)
92
+ if(fixed && !DYNAMIC_GEO_TEMPLATES.has(fixed)) {
93
+ return { geoLocation: fixed, source: 'draft' }
94
+ }
95
+
96
+ const country = await lookupCountry()
97
+ return country
98
+ ? { geoLocation: country, source: 'developer-ip' }
99
+ : { source: 'none' }
100
+ }
101
+
56
102
  /** The local backend for one capture: drive its tab over CDP, read its
57
103
  * captured requests, prove via the in-process attestor, replay by re-fetching
58
104
  * FROM INSIDE the tab (real cookies/session — see proof/page-replay.ts). */
@@ -60,6 +106,8 @@ function localBackend(
60
106
  entry: CaptureEntry,
61
107
  kept: KeptScriptRef,
62
108
  check: typeof checkAttestor,
109
+ runProof: typeof runAttestorProof,
110
+ lookupCountry: () => Promise<string | undefined>,
63
111
  ): AuthoringBackend {
64
112
  const { tabClient, session, drafts } = entry
65
113
  const sendCdp = (method: string, params?: Record<string, unknown>) => {
@@ -96,6 +144,16 @@ function localBackend(
96
144
  const result = await replayProviderInPage(
97
145
  { sendCdp }, draft.provider, draft.secrets, opts,
98
146
  )
147
+ // Remember the authenticated replay so a failed proof can tell "the
148
+ // site blocks non-browser clients" from a broken recipe.
149
+ if(!opts?.withoutSecrets) {
150
+ draft.lastReplay = {
151
+ status: result.status,
152
+ ok: result.status >= 200 && result.status < 300
153
+ && result.matchesOriginal,
154
+ }
155
+ }
156
+
99
157
  return {
100
158
  status: result.status,
101
159
  matchesOriginal: result.matchesOriginal,
@@ -104,13 +162,29 @@ function localBackend(
104
162
  }
105
163
  },
106
164
  prove: async(draft, opts) => {
107
- // Prove with the locally-runnable OPRF mode; the draft (→ published
108
- // recipe) keeps oprf-mpc.
109
- const result = await runAttestorProof(
110
- withLocalOprfMode(draft.provider),
165
+ // No ownerAddress: resolve the configured key, or create a local test
166
+ // key, so a proof never stops to ask the developer for one.
167
+ const ownerKey = opts?.ownerAddress
168
+ ? undefined
169
+ : resolveOrCreateEthKey()
170
+ const ownerAddress = opts?.ownerAddress ?? ownerKey?.address
171
+ const egress = await resolveProofEgress(
172
+ draft.provider, opts?.geoLocation, lookupCountry,
173
+ )
174
+ // Prove with the locally-runnable OPRF mode and the resolved egress;
175
+ // the draft (→ published recipe) keeps oprf-mpc and its template.
176
+ const { geoLocation: _template, ...provider } = withLocalOprfMode(
177
+ draft.provider,
178
+ )
179
+ void _template
180
+ const result = await runProof(
181
+ {
182
+ ...provider,
183
+ ...(egress.geoLocation ? { geoLocation: egress.geoLocation } : {}),
184
+ },
111
185
  draft.secrets,
112
186
  {
113
- ...(opts?.ownerAddress ? { ownerAddress: opts.ownerAddress } : {}),
187
+ ...(ownerAddress ? { ownerAddress } : {}),
114
188
  ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
115
189
  },
116
190
  )
@@ -118,11 +192,27 @@ function localBackend(
118
192
  provider: draft.provider,
119
193
  hasSecrets: Object.keys(draft.secrets).length > 0,
120
194
  ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
195
+ ...(draft.lastReplay ? { lastReplay: draft.lastReplay } : {}),
196
+ egress,
121
197
  })
122
198
  const diagnosis = await diagnoseNetworkFailure(
123
199
  result, check, opts?.attestorUrl,
124
200
  )
125
- return diagnosis ? { ...shaped, diagnosis } : shaped
201
+ return {
202
+ ...shaped,
203
+ egress,
204
+ ...(diagnosis ? { diagnosis } : {}),
205
+ ...(ownerKey?.source === 'generated'
206
+ ? {
207
+ ownerKey: {
208
+ address: ownerKey.address,
209
+ source: ownerKey.source,
210
+ path: ownerKey.path,
211
+ note: GENERATED_KEY_NOTE,
212
+ },
213
+ }
214
+ : {}),
215
+ }
126
216
  },
127
217
  }
128
218
  }
@@ -134,36 +224,55 @@ const RUN_PROOF_EXTRAS = {
134
224
  description:
135
225
  'Proves the drafted recipe with a full zkTLS proof through the attestor '
136
226
  + '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 '
227
+ + 'expensive. It signs with the configured proof-owner key and creates '
228
+ + 'a local test key in the project `.env` when none exists; then the '
229
+ + 'result has `ownerKey` with a `note`: tell the developer once, after '
230
+ + 'the fact. Do not ask for a key first. Writes the full claim to '
139
231
  + '`~/.reclaim/claims/<timestamp>.json`. Returns `identifier`, '
140
232
  + '`extractedParameters`, `claimPath`, and a readable `claim` (request '
141
233
  + 'removed, byte arrays as base64). Set `RECLAIM_AGENT_TRACE=1` to also '
142
234
  + 'get `runLogPath`, a diagnostic dump with the attestor log stream. '
143
235
  + '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 '
236
+ + 'backup URLs. If `errorKind` is `missing-owner-key`, the '
237
+ + '`ownerAddress` you passed has no known private key: retry without '
238
+ + '`ownerAddress`. Unlike `replay_request`, which re-sends from '
147
239
  + 'inside the attached browser tab, the attestor calls the target from '
148
240
  + 'its own network and IP. A request that replayed can still fail here '
149
241
  + 'for geo- or IP-bound content, such as country-locked pages or '
150
242
  + 'IP-allowlisted APIs. That is a property of the site, not a draft '
151
- + 'bug. After a network-like failure ("connection closed", '
243
+ + 'bug. The request goes out from `egress.geoLocation` in the result: '
244
+ + 'the `geoLocation` argument, else the draft\'s fixed country, else '
245
+ + 'your own IP country. After a network-like failure ("connection closed", '
152
246
  + '`ERROR_NETWORK_ERROR`, timeout), the result includes `diagnosis` '
153
247
  + 'with `attestor` (a `check_attestor` result), `likelyCause`, and '
154
- + '`next`. Follow `next`; do not retry the same proof.',
248
+ + '`next`. Follow `next`; do not retry the same proof. When the '
249
+ + 'attestor gets HTTP 403/406/429/503 or a challenge page, `diagnosis` '
250
+ + 'has `kind: "blocked-non-browser-client"`: the site blocks '
251
+ + 'non-browser clients or traffic from that country, and copying '
252
+ + 'headers or cookies will not help. Follow its `next`. Run '
253
+ + '`check_matchers` first to rule out the recipe offline.',
155
254
  properties: {
156
255
  ownerAddress: {
157
256
  type: 'string',
158
257
  pattern: '^0x[0-9a-fA-F]{40}$',
159
258
  description:
160
- 'ETH address of the credential to sign the claim with. Get it '
161
- + 'from resolve_owner_key first. The private key is resolved '
162
- + 'locally (cache / env / .env) — never passed here.',
259
+ 'Optional ETH address the resolved signing key must match. Omit '
260
+ + 'it: run_proof resolves (or creates) the key itself. The '
261
+ + 'private key is resolved locally (env / .env / key file), '
262
+ + 'never passed here.',
163
263
  },
164
264
  attestorUrl: { type: 'string' },
265
+ geoLocation: {
266
+ type: 'string',
267
+ pattern: '^[A-Za-z]{2}$',
268
+ description:
269
+ 'ISO country the attestor\'s request goes out from, for example '
270
+ + '`US`. Omit it to use the draft\'s fixed country, or else your '
271
+ + 'own IP country, as `{{DYNAMIC_GEO}}` resolves for you when you '
272
+ + 'verify.',
273
+ },
165
274
  },
166
- required: ['ownerAddress'],
275
+ required: [],
167
276
  } satisfies {
168
277
  description: string
169
278
  properties: JSONSchema.ObjectSchema['properties']
@@ -178,6 +287,8 @@ const RUN_PROOF_EXTRAS = {
178
287
  export function sharedAuthoringTools(
179
288
  captures: CaptureStore,
180
289
  check: typeof checkAttestor = checkAttestor,
290
+ runProof: typeof runAttestorProof = runAttestorProof,
291
+ lookupCountry: () => Promise<string | undefined> = lookupIpCountry,
181
292
  ): RegisteredTool[] {
182
293
  const resolveBackend = (args: Record<string, unknown>): AuthoringBackend => {
183
294
  const captureId = String(args.captureId)
@@ -189,7 +300,7 @@ export function sharedAuthoringTools(
189
300
  )
190
301
  }
191
302
 
192
- return localBackend(entry, captures, check)
303
+ return localBackend(entry, captures, check, runProof, lookupCountry)
193
304
  }
194
305
 
195
306
  return authoringTools(resolveBackend).map((tool) => {
@@ -10,7 +10,8 @@ interface StartIntegrationArgs {
10
10
 
11
11
  /**
12
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
13
+ * developer's app, from sign-in and a working proof to a tested Verify
14
+ * button. The plan lives in
14
15
  * `skill/start-integration.md`; this tool fills in the inputs.
15
16
  */
16
17
  export function startIntegrationTool(): RegisteredTool {
@@ -19,11 +20,14 @@ export function startIntegrationTool(): RegisteredTool {
19
20
  name: 'start_integration',
20
21
  description:
21
22
  '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.',
23
+ + 'current codebase: sign in to Builder, attach the browser, capture, '
24
+ + 'and prove the provider first, then set up the app (session route, '
25
+ + 'callback, Verify button, local event proxy) after the proof or '
26
+ + 'while waiting on the developer, bootstrapping from the '
27
+ + 'builder-example repository when there is no app yet, then wire '
28
+ + 'the provider and test. '
29
+ + 'Call it first when a developer wants their users to prove '
30
+ + 'something, and follow the plan to the end. Changes nothing itself.',
27
31
  inputSchema: {
28
32
  type: 'object',
29
33
  required: ['goal'],