@reclaimprotocol/agent 0.1.36 → 0.1.38

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 (104) 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 +8 -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/example-app.d.ts +2 -0
  24. package/lib/mcp/tools/agent/example-app.js +167 -0
  25. package/lib/mcp/tools/agent/pod-proof.js +5 -1
  26. package/lib/mcp/tools/agent/proof.d.ts +28 -0
  27. package/lib/mcp/tools/agent/proof.js +72 -0
  28. package/lib/mcp/tools/agent/shared.d.ts +8 -1
  29. package/lib/mcp/tools/agent/shared.js +98 -19
  30. package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
  31. package/lib/mcp/tools/agent/start-integration.js +10 -6
  32. package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
  33. package/lib/mcp/tools/authenticate/authenticate.js +212 -116
  34. package/lib/mcp/tools/authenticate/build.d.ts +2 -2
  35. package/lib/mcp/tools/authenticate/build.js +6 -3
  36. package/lib/mcp/tools/authenticate/builder-access.js +5 -2
  37. package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
  38. package/lib/mcp/tools/authenticate/eth-key.js +29 -5
  39. package/lib/mcp/tools/authenticate/index.d.ts +1 -1
  40. package/lib/mcp/tools/authenticate/index.js +1 -1
  41. package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
  42. package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
  43. package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
  44. package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
  45. package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
  46. package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
  47. package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
  48. package/lib/mcp/tools/authenticate/session-cache.js +95 -4
  49. package/lib/mcp/tools/mode/mode.js +6 -6
  50. package/lib/old/login-cdp.js +2 -3
  51. package/lib/proof/attestor.js +2 -3
  52. package/lib/proof/check-attestor.d.ts +12 -2
  53. package/lib/proof/check-attestor.js +40 -10
  54. package/lib/proof/check-matchers.d.ts +36 -0
  55. package/lib/proof/check-matchers.js +161 -0
  56. package/lib/proof/zk-files.d.ts +50 -11
  57. package/lib/proof/zk-files.js +259 -63
  58. package/lib/provider/draft.d.ts +24 -1
  59. package/lib/provider/draft.js +135 -0
  60. package/lib/provider/matcher.js +171 -4
  61. package/package.json +6 -4
  62. package/skill/SKILL.builder.md +7 -2
  63. package/skill/SKILL.common.md +90 -70
  64. package/skill/SKILL.old.md +3 -3
  65. package/skill/builder-mode.md +35 -5
  66. package/skill/old-mode.md +18 -13
  67. package/skill/start-integration.md +50 -33
  68. package/skill/verification-client-integration.md +21 -6
  69. package/src/authoring/backend.ts +12 -1
  70. package/src/authoring/tools.ts +48 -7
  71. package/src/authoring/util.ts +64 -0
  72. package/src/cdp/chrome-for-testing.ts +66 -0
  73. package/src/cdp/geo.ts +36 -21
  74. package/src/cdp/launcher.ts +328 -24
  75. package/src/logger.ts +3 -1
  76. package/src/mcp/instructions.ts +3 -3
  77. package/src/mcp/start.ts +20 -1
  78. package/src/mcp/tools/agent/attach.ts +65 -128
  79. package/src/mcp/tools/agent/build.ts +9 -0
  80. package/src/mcp/tools/agent/capture.ts +54 -11
  81. package/src/mcp/tools/agent/check-attestor.ts +15 -7
  82. package/src/mcp/tools/agent/draft-tools.ts +234 -0
  83. package/src/mcp/tools/agent/example-app.ts +227 -0
  84. package/src/mcp/tools/agent/pod-proof.ts +5 -1
  85. package/src/mcp/tools/agent/proof.ts +110 -1
  86. package/src/mcp/tools/agent/shared.ts +130 -19
  87. package/src/mcp/tools/agent/start-integration.ts +10 -6
  88. package/src/mcp/tools/authenticate/authenticate.ts +276 -131
  89. package/src/mcp/tools/authenticate/build.ts +6 -3
  90. package/src/mcp/tools/authenticate/builder-access.ts +5 -2
  91. package/src/mcp/tools/authenticate/eth-key.ts +41 -7
  92. package/src/mcp/tools/authenticate/index.ts +1 -1
  93. package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
  94. package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
  95. package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
  96. package/src/mcp/tools/authenticate/session-cache.ts +125 -5
  97. package/src/mcp/tools/mode/mode.ts +6 -6
  98. package/src/old/login-cdp.ts +2 -3
  99. package/src/proof/attestor.ts +2 -3
  100. package/src/proof/check-attestor.ts +55 -12
  101. package/src/proof/check-matchers.ts +227 -0
  102. package/src/proof/zk-files.ts +357 -65
  103. package/src/provider/draft.ts +215 -7
  104. package/src/provider/matcher.ts +237 -4
@@ -20,8 +20,15 @@ import { proposeTool } from '../mcp/tools/agent/propose.ts'
20
20
  import { replayTool } from '../mcp/tools/agent/replay.ts'
21
21
  import type { CapturedRequest } from '../provider/schema.ts'
22
22
  import { defineTool, type RegisteredTool } from '../tools/registry.ts'
23
- import type { AuthoringBackend } from './backend.ts'
24
- import { objectSchema, publicRequest, type ResolveBackend } from './util.ts'
23
+ import type { AuthoringBackend, ProveOptions } from './backend.ts'
24
+ import {
25
+ DEFAULT_MAX_CHARS,
26
+ objectSchema,
27
+ publicRequest,
28
+ type ResolveBackend,
29
+ TEXT_WINDOW_PROPERTIES,
30
+ textWindow,
31
+ } from './util.ts'
25
32
 
26
33
  const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))
27
34
 
@@ -155,13 +162,40 @@ export function authoringTools(
155
162
  + 'Use it to read a data point from the DOM, check sign-in state, '
156
163
  + 'or trigger the target request (for example, `location.reload()`). '
157
164
  + 'The page cannot reach files or processes on this machine. Return '
158
- + 'only the values you need, never cookies or tokens.',
165
+ + 'only the values you need, never cookies or tokens. A result '
166
+ + `longer than ${DEFAULT_MAX_CHARS} characters comes back `
167
+ + 'truncated (objects as JSON text) with `valueTruncation`; page '
168
+ + 'with `offset`, or pass `maxChars` or `full`.',
159
169
  inputSchema: objectSchema(
160
- { expression: { type: 'string' } }, ['expression'],
170
+ { expression: { type: 'string' }, ...TEXT_WINDOW_PROPERTIES },
171
+ ['expression'],
161
172
  ),
162
173
  annotations: { openWorldHint: true, destructiveHint: false },
163
174
  },
164
- (args) => evalInPage(resolveBackend(args), String(args.expression)),
175
+ async(args) => {
176
+ const res = await evalInPage(
177
+ resolveBackend(args), String(args.expression),
178
+ )
179
+ if(res.value === undefined) {
180
+ return res
181
+ }
182
+
183
+ const text = typeof res.value === 'string'
184
+ ? res.value
185
+ : JSON.stringify(res.value)
186
+ const slice = textWindow(text, args)
187
+ if(!slice.truncation) {
188
+ return res
189
+ }
190
+
191
+ return {
192
+ value: slice.text,
193
+ valueTruncation: {
194
+ ...slice.truncation,
195
+ ...(typeof res.value === 'string' ? {} : { format: 'json' }),
196
+ },
197
+ }
198
+ },
165
199
  ),
166
200
  defineTool<Record<string, unknown>>(
167
201
  {
@@ -484,7 +518,10 @@ export function authoringTools(
484
518
  + 'attestor calls the target from its own network and IP. A request '
485
519
  + 'that replayed can still fail here for geo- or IP-bound content '
486
520
  + '(country-locked pages, IP-allowlisted APIs); that is a property '
487
- + 'of the site, not a bug in the draft.',
521
+ + 'of the site, not a bug in the draft. HTTP 403/406/429/503 or a '
522
+ + 'challenge page from the attestor while the replay succeeded '
523
+ + 'means the site blocks non-browser clients: copying headers or '
524
+ + 'cookies will not help, so try a different endpoint or stop.',
488
525
  inputSchema: objectSchema({ draftId: { type: 'string' } }, ['draftId']),
489
526
  annotations: { openWorldHint: true, destructiveHint: false },
490
527
  },
@@ -500,7 +537,7 @@ export function authoringTools(
500
537
  throw new Error(`no draft ${String(args.draftId)}`)
501
538
  }
502
539
 
503
- const opts: { ownerAddress?: string, attestorUrl?: string } = {}
540
+ const opts: ProveOptions = {}
504
541
  if(typeof args.ownerAddress === 'string') {
505
542
  opts.ownerAddress = args.ownerAddress
506
543
  }
@@ -509,6 +546,10 @@ export function authoringTools(
509
546
  opts.attestorUrl = args.attestorUrl
510
547
  }
511
548
 
549
+ if(typeof args.geoLocation === 'string') {
550
+ opts.geoLocation = args.geoLocation
551
+ }
552
+
512
553
  return backend.prove(draft, opts)
513
554
  },
514
555
  ),
@@ -43,3 +43,67 @@ export function publicRequest(r: CapturedRequest) {
43
43
  ...(r.graphqlOp ? { graphqlOp: r.graphqlOp } : {}),
44
44
  }
45
45
  }
46
+
47
+ /** Characters a long text field returns by default (about 20 KB of ASCII),
48
+ * so one page body or eval result can't flood the caller's context. */
49
+ export const DEFAULT_MAX_CHARS = 20_000
50
+
51
+ export interface TextWindowArgs {
52
+ maxChars?: unknown
53
+ offset?: unknown
54
+ full?: unknown
55
+ }
56
+
57
+ /** Input properties that let a caller page through, or opt out of, the
58
+ * window {@link textWindow} applies. */
59
+ export const TEXT_WINDOW_PROPERTIES = {
60
+ maxChars: {
61
+ type: 'integer',
62
+ minimum: 1,
63
+ description: `Maximum characters to return. Default ${DEFAULT_MAX_CHARS}.`,
64
+ },
65
+ offset: {
66
+ type: 'integer',
67
+ minimum: 0,
68
+ description: 'Character offset to start from, such as a previous '
69
+ + '`nextOffset`. Default 0.',
70
+ },
71
+ full: {
72
+ type: 'boolean',
73
+ description: 'Return the whole text, ignoring `maxChars`. It can be '
74
+ + 'megabytes, so prefer paging with `offset`.',
75
+ },
76
+ } satisfies Record<string, JSONSchema.BaseSchema>
77
+
78
+ /**
79
+ * Returns characters `[offset, offset + maxChars)` of `text`. When that isn't
80
+ * the whole text, `truncation` reports the total size and how to get the rest.
81
+ */
82
+ export function textWindow(text: string, args: TextWindowArgs) {
83
+ const totalChars = text.length
84
+ const offset = Math.min(Math.max(Number(args.offset) || 0, 0), totalChars)
85
+ const maxChars = args.full === true
86
+ ? totalChars
87
+ : Math.max(Number(args.maxChars) || DEFAULT_MAX_CHARS, 1)
88
+ const end = Math.min(offset + maxChars, totalChars)
89
+ if(offset === 0 && end === totalChars) {
90
+ return { text }
91
+ }
92
+
93
+ const part = text.slice(offset, end)
94
+ const more = end < totalChars
95
+ return {
96
+ text: part,
97
+ truncation: {
98
+ totalChars,
99
+ offset,
100
+ returnedChars: part.length,
101
+ ...(more ? { nextOffset: end } : {}),
102
+ note: `Returned characters ${offset}-${end} of ${totalChars}.`
103
+ + (more
104
+ ? ` Call again with offset: ${end} for more, or full: true `
105
+ + 'for the whole text.'
106
+ : ''),
107
+ },
108
+ }
109
+ }
@@ -0,0 +1,66 @@
1
+ import { failedDependency } from '@hapi/boom'
2
+ import assert from 'node:assert'
3
+ import { existsSync } from 'node:fs'
4
+ import { arch, platform } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { LOGGER } from '../logger.ts'
7
+ import { reclaimHomeDirPath } from '../paths.ts'
8
+
9
+ export interface ChromeForTestingInstall {
10
+ executablePath: string
11
+ /** True when this call downloaded it; false when an earlier one had. */
12
+ downloaded: boolean
13
+ }
14
+
15
+ /** Where downloaded browsers live: `~/.reclaim/browsers`. */
16
+ export function browsersDirPath() {
17
+ return join(reclaimHomeDirPath(), 'browsers')
18
+ }
19
+
20
+ /**
21
+ * Chrome for Testing, the fallback when no local Chromium-family browser is
22
+ * installed or the installed one will not start. Downloaded once into
23
+ * `~/.reclaim/browsers` and reused after that, so only the first fallback pays
24
+ * the ~150 MB download.
25
+ */
26
+ export async function installChromeForTesting(
27
+ cacheDir = browsersDirPath(),
28
+ ): Promise<ChromeForTestingInstall> {
29
+ // Loaded on demand: only the fallback path needs it.
30
+ const {
31
+ Browser,
32
+ detectBrowserPlatform,
33
+ getInstalledBrowsers,
34
+ getVersionComparator,
35
+ install,
36
+ resolveBuildId,
37
+ } = await import('@puppeteer/browsers')
38
+ const browserPlatform = detectBrowserPlatform()
39
+ assert(browserPlatform, failedDependency(
40
+ `Chrome for Testing has no build for ${platform()}/${arch()}.`,
41
+ ))
42
+
43
+ const compare = getVersionComparator(Browser.CHROME)
44
+ const all = await getInstalledBrowsers({ cacheDir })
45
+ const installed = all
46
+ .filter((b) => b.browser === Browser.CHROME
47
+ && b.platform === browserPlatform
48
+ && existsSync(b.executablePath))
49
+ .sort((a, b) => compare(a.buildId, b.buildId))
50
+ const newest = installed.at(-1)
51
+ if(newest) {
52
+ return { executablePath: newest.executablePath, downloaded: false }
53
+ }
54
+
55
+ const buildId = await resolveBuildId(
56
+ Browser.CHROME, browserPlatform, 'stable',
57
+ )
58
+ LOGGER.info({ buildId, cacheDir }, 'downloading Chrome for Testing')
59
+ const result = await install({
60
+ browser: Browser.CHROME,
61
+ platform: browserPlatform,
62
+ buildId,
63
+ cacheDir,
64
+ })
65
+ return { executablePath: result.executablePath, downloaded: true }
66
+ }
package/src/cdp/geo.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * public-IP country, resolved the same way reclaim-portal's `ip-geolocation`
6
6
  * util does.
7
7
  */
8
+ import assert from 'node:assert'
8
9
  import { LOGGER } from '../logger.ts'
9
10
 
10
11
  export interface IpGeolocation {
@@ -24,29 +25,43 @@ export async function suggestCountryFromIp(
24
25
  fetchImpl: typeof fetch = fetch,
25
26
  ): Promise<IpGeolocation> {
26
27
  try {
27
- const res = await fetchImpl(IPINFO_URL)
28
- if(!res.ok) {
29
- throw new Error(`ipinfo.io ${res.status}`)
30
- }
31
-
32
- const data = await res.json() as {
33
- country?: string
34
- region?: string
35
- city?: string
36
- }
37
- const countryCode = data.country?.toUpperCase()
38
- if(!countryCode) {
39
- throw new Error('ipinfo.io response had no country')
40
- }
41
-
42
- return {
43
- countryCode,
44
- country: data.country,
45
- region: data.region,
46
- city: data.city,
47
- }
28
+ return await fetchIpGeolocation(fetchImpl)
48
29
  } catch(err) {
49
30
  LOGGER.warn({ err }, 'suggestCountryFromIp failed; using fallback')
50
31
  return { countryCode: FALLBACK_COUNTRY_CODE }
51
32
  }
52
33
  }
34
+
35
+ /** The calling machine's public-IP country, or undefined when the lookup
36
+ * fails: unlike {@link suggestCountryFromIp}, no guessed fallback. */
37
+ export async function lookupIpCountry(
38
+ fetchImpl: typeof fetch = fetch,
39
+ ): Promise<string | undefined> {
40
+ try {
41
+ const { countryCode } = await fetchIpGeolocation(fetchImpl)
42
+ return countryCode
43
+ } catch(err) {
44
+ LOGGER.warn({ err }, 'IP country lookup failed')
45
+ return undefined
46
+ }
47
+ }
48
+
49
+ async function fetchIpGeolocation(
50
+ fetchImpl: typeof fetch,
51
+ ): Promise<IpGeolocation> {
52
+ const res = await fetchImpl(IPINFO_URL)
53
+ assert(res.ok, new Error(`ipinfo.io ${res.status}`))
54
+ const data = await res.json() as {
55
+ country?: string
56
+ region?: string
57
+ city?: string
58
+ }
59
+ const countryCode = data.country?.toUpperCase()
60
+ assert(countryCode, new Error('ipinfo.io response had no country'))
61
+ return {
62
+ countryCode,
63
+ country: data.country,
64
+ region: data.region,
65
+ city: data.city,
66
+ }
67
+ }