@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.
- package/README.md +8 -8
- package/lib/authoring/backend.d.ts +12 -1
- package/lib/authoring/backend.js +3 -0
- package/lib/authoring/tools.js +33 -5
- package/lib/authoring/util.d.ts +43 -0
- package/lib/authoring/util.js +54 -0
- package/lib/cdp/chrome-for-testing.d.ts +14 -0
- package/lib/cdp/chrome-for-testing.js +43 -0
- package/lib/cdp/geo.d.ts +3 -0
- package/lib/cdp/geo.js +27 -15
- package/lib/cdp/launcher.d.ts +79 -1
- package/lib/cdp/launcher.js +244 -24
- package/lib/logger.js +3 -1
- package/lib/mcp/instructions.js +3 -3
- package/lib/mcp/start.js +17 -2
- package/lib/mcp/tools/agent/attach.d.ts +6 -0
- package/lib/mcp/tools/agent/attach.js +58 -118
- package/lib/mcp/tools/agent/build.js +6 -0
- package/lib/mcp/tools/agent/capture.js +33 -10
- package/lib/mcp/tools/agent/check-attestor.js +15 -7
- package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
- package/lib/mcp/tools/agent/draft-tools.js +170 -0
- package/lib/mcp/tools/agent/pod-proof.js +5 -1
- package/lib/mcp/tools/agent/proof.d.ts +28 -0
- package/lib/mcp/tools/agent/proof.js +72 -0
- package/lib/mcp/tools/agent/shared.d.ts +8 -1
- package/lib/mcp/tools/agent/shared.js +98 -19
- package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
- package/lib/mcp/tools/agent/start-integration.js +10 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
- package/lib/mcp/tools/authenticate/authenticate.js +212 -116
- package/lib/mcp/tools/authenticate/build.d.ts +2 -2
- package/lib/mcp/tools/authenticate/build.js +6 -3
- package/lib/mcp/tools/authenticate/builder-access.js +5 -2
- package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
- package/lib/mcp/tools/authenticate/eth-key.js +29 -5
- package/lib/mcp/tools/authenticate/index.d.ts +1 -1
- package/lib/mcp/tools/authenticate/index.js +1 -1
- package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
- package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
- package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
- package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
- package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
- package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
- package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
- package/lib/mcp/tools/authenticate/session-cache.js +95 -4
- package/lib/mcp/tools/mode/mode.js +6 -6
- package/lib/old/login-cdp.js +2 -3
- package/lib/proof/attestor.js +2 -3
- package/lib/proof/check-attestor.d.ts +12 -2
- package/lib/proof/check-attestor.js +40 -10
- package/lib/proof/check-matchers.d.ts +36 -0
- package/lib/proof/check-matchers.js +161 -0
- package/lib/proof/zk-files.d.ts +50 -11
- package/lib/proof/zk-files.js +259 -63
- package/lib/provider/draft.d.ts +24 -1
- package/lib/provider/draft.js +135 -0
- package/lib/provider/matcher.js +171 -4
- package/package.json +6 -4
- package/skill/SKILL.builder.md +7 -2
- package/skill/SKILL.common.md +90 -70
- package/skill/SKILL.old.md +3 -3
- package/skill/builder-mode.md +35 -5
- package/skill/old-mode.md +18 -13
- package/skill/start-integration.md +48 -33
- package/skill/verification-client-integration.md +21 -6
- package/src/authoring/backend.ts +12 -1
- package/src/authoring/tools.ts +48 -7
- package/src/authoring/util.ts +64 -0
- package/src/cdp/chrome-for-testing.ts +66 -0
- package/src/cdp/geo.ts +36 -21
- package/src/cdp/launcher.ts +328 -24
- package/src/logger.ts +3 -1
- package/src/mcp/instructions.ts +3 -3
- package/src/mcp/start.ts +20 -1
- package/src/mcp/tools/agent/attach.ts +65 -128
- package/src/mcp/tools/agent/build.ts +7 -0
- package/src/mcp/tools/agent/capture.ts +54 -11
- package/src/mcp/tools/agent/check-attestor.ts +15 -7
- package/src/mcp/tools/agent/draft-tools.ts +234 -0
- package/src/mcp/tools/agent/pod-proof.ts +5 -1
- package/src/mcp/tools/agent/proof.ts +110 -1
- package/src/mcp/tools/agent/shared.ts +130 -19
- package/src/mcp/tools/agent/start-integration.ts +10 -6
- package/src/mcp/tools/authenticate/authenticate.ts +276 -131
- package/src/mcp/tools/authenticate/build.ts +6 -3
- package/src/mcp/tools/authenticate/builder-access.ts +5 -2
- package/src/mcp/tools/authenticate/eth-key.ts +41 -7
- package/src/mcp/tools/authenticate/index.ts +1 -1
- package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
- package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
- package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
- package/src/mcp/tools/authenticate/session-cache.ts +125 -5
- package/src/mcp/tools/mode/mode.ts +6 -6
- package/src/old/login-cdp.ts +2 -3
- package/src/proof/attestor.ts +2 -3
- package/src/proof/check-attestor.ts +55 -12
- package/src/proof/check-matchers.ts +227 -0
- package/src/proof/zk-files.ts +357 -65
- package/src/provider/draft.ts +215 -7
- package/src/provider/matcher.ts +237 -4
package/src/authoring/tools.ts
CHANGED
|
@@ -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 {
|
|
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' } },
|
|
170
|
+
{ expression: { type: 'string' }, ...TEXT_WINDOW_PROPERTIES },
|
|
171
|
+
['expression'],
|
|
161
172
|
),
|
|
162
173
|
annotations: { openWorldHint: true, destructiveHint: false },
|
|
163
174
|
},
|
|
164
|
-
(args) =>
|
|
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:
|
|
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
|
),
|
package/src/authoring/util.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|