@reclaimprotocol/agent 0.1.23 → 0.1.24
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 +55 -20
- package/lib/authoring/backend.d.ts +5 -4
- package/lib/authoring/tools.js +30 -30
- package/lib/cdp/browser-proxy.d.ts +78 -0
- package/lib/cdp/browser-proxy.js +186 -0
- package/lib/cdp/capture-engine.js +8 -7
- package/lib/cdp/client.d.ts +10 -0
- package/lib/cdp/geo.d.ts +10 -0
- package/lib/cdp/geo.js +36 -0
- package/lib/cdp/index.d.ts +1 -0
- package/lib/cdp/index.js +1 -0
- package/lib/live/cloudflared.d.ts +15 -0
- package/lib/live/cloudflared.js +268 -0
- package/lib/live/page.d.ts +20 -0
- package/lib/live/page.js +134 -0
- package/lib/live/tunnel.d.ts +23 -0
- package/lib/live/tunnel.js +120 -0
- package/lib/live/viewer.d.ts +28 -0
- package/lib/live/viewer.js +232 -0
- package/lib/mcp/api/operations.gen.js +34 -26
- package/lib/mcp/api/to-zod.d.ts +1 -1
- package/lib/mcp/api/to-zod.js +1 -1
- package/lib/mcp/api/tools.js +1 -1
- package/lib/mcp/instructions.d.ts +15 -2
- package/lib/mcp/instructions.js +101 -38
- package/lib/mcp/start.js +4 -3
- package/lib/mcp/tools/agent/analyze.js +5 -1
- package/lib/mcp/tools/agent/attach.d.ts +8 -1
- package/lib/mcp/tools/agent/attach.js +166 -22
- package/lib/mcp/tools/agent/build.d.ts +13 -8
- package/lib/mcp/tools/agent/build.js +22 -11
- package/lib/mcp/tools/agent/capture.js +5 -1
- package/lib/mcp/tools/agent/eval.js +3 -1
- package/lib/mcp/tools/agent/find.js +6 -5
- package/lib/mcp/tools/agent/how-it-works.d.ts +15 -7
- package/lib/mcp/tools/agent/how-it-works.js +57 -17
- package/lib/mcp/tools/agent/live-view.d.ts +21 -0
- package/lib/mcp/tools/agent/live-view.js +153 -0
- package/lib/mcp/tools/agent/pod-proof.d.ts +8 -0
- package/lib/mcp/tools/agent/pod-proof.js +64 -0
- package/lib/mcp/tools/agent/propose.js +4 -2
- package/lib/mcp/tools/agent/proxy.d.ts +3 -0
- package/lib/mcp/tools/agent/proxy.js +199 -0
- package/lib/mcp/tools/agent/publish.js +47 -77
- package/lib/mcp/tools/agent/session.d.ts +2 -1
- package/lib/mcp/tools/agent/session.js +26 -7
- package/lib/mcp/tools/authenticate/authenticate.js +26 -13
- package/lib/mcp/tools/mode/mode.d.ts +1 -1
- package/lib/mcp/tools/mode/mode.js +10 -22
- package/lib/old/client.d.ts +1 -1
- package/lib/old/client.js +1 -1
- package/lib/old/instructions.d.ts +9 -1
- package/lib/old/instructions.js +4 -6
- package/lib/old/login-cdp.d.ts +2 -2
- package/lib/old/login-cdp.js +46 -6
- package/lib/old/to-register.d.ts +3 -2
- package/lib/old/to-register.js +1 -1
- package/lib/old/tools/authenticate.js +9 -1
- package/lib/old/tools/get-provider-info.js +6 -5
- package/lib/old/tools/issue-app-credentials.js +3 -2
- package/lib/old/tools/publish.d.ts +1 -1
- package/lib/old/tools/publish.js +90 -131
- package/lib/old/tools/update-provider-metadata.d.ts +1 -1
- package/lib/old/tools/update-provider-metadata.js +1 -1
- package/lib/old/tools/verify-sdk-setup.js +1 -1
- package/lib/proof/attestor.d.ts +3 -3
- package/lib/proof/debug-response-dump.js +4 -4
- package/lib/proof/index.d.ts +1 -0
- package/lib/proof/index.js +1 -0
- package/lib/proof/page-replay.d.ts +1 -1
- package/lib/proof/page-replay.js +1 -1
- package/lib/proof/pod-prover.d.ts +47 -0
- package/lib/proof/pod-prover.js +155 -0
- package/lib/provider/dom-xpath.js +11 -9
- package/lib/provider/draft.d.ts +10 -7
- package/lib/provider/draft.js +11 -9
- package/lib/provider/injection-doc.d.ts +23 -1
- package/lib/provider/injection-doc.js +34 -21
- package/lib/provider/match-redaction-warning.d.ts +2 -2
- package/lib/provider/match-redaction-warning.js +3 -3
- package/lib/provider/matcher.js +13 -12
- package/lib/provider/secrets.d.ts +3 -3
- package/lib/provider/secrets.js +3 -3
- package/lib/provider/to-version.d.ts +7 -6
- package/lib/provider/to-version.js +3 -3
- package/lib/tools/registry.d.ts +5 -5
- package/lib/tools/registry.js +5 -5
- package/package.json +3 -3
- package/samples/demo-server/README.md +7 -6
- package/skill/SKILL.builder.md +356 -189
- package/skill/SKILL.common.md +323 -97
- package/skill/SKILL.old.md +340 -155
- package/skill/builder-mode.md +58 -17
- package/skill/hash-validation.md +170 -0
- package/skill/interception.md +235 -0
- package/skill/js-sdk-integration.md +13 -7
- package/skill/old-mode.md +60 -21
- package/skill/user-script.md +61 -0
- package/src/authoring/backend.ts +5 -4
- package/src/authoring/tools.ts +31 -30
- package/src/cdp/capture-engine.ts +8 -7
- package/src/cdp/client.ts +8 -0
- package/src/cdp/geo.ts +52 -0
- package/src/cdp/index.ts +1 -0
- package/src/consts.ts +1 -0
- package/src/live/cloudflared.ts +348 -0
- package/src/live/page.ts +145 -0
- package/src/live/tunnel.ts +157 -0
- package/src/live/viewer.ts +293 -0
- package/src/mcp/api/operations.gen.ts +34 -26
- package/src/mcp/api/to-zod.ts +1 -1
- package/src/mcp/api/tools.ts +1 -1
- package/src/mcp/instructions.ts +121 -48
- package/src/mcp/start.ts +4 -3
- package/src/mcp/tools/agent/analyze.ts +5 -1
- package/src/mcp/tools/agent/attach.ts +194 -22
- package/src/mcp/tools/agent/build.ts +26 -15
- package/src/mcp/tools/agent/capture.ts +5 -1
- package/src/mcp/tools/agent/eval.ts +4 -1
- package/src/mcp/tools/agent/find.ts +6 -5
- package/src/mcp/tools/agent/how-it-works.ts +62 -21
- package/src/mcp/tools/agent/live-view.ts +198 -0
- package/src/mcp/tools/agent/pod-proof.ts +90 -0
- package/src/mcp/tools/agent/propose.ts +4 -2
- package/src/mcp/tools/agent/publish.ts +52 -77
- package/src/mcp/tools/agent/session.ts +26 -5
- package/src/mcp/tools/authenticate/authenticate.ts +28 -13
- package/src/mcp/tools/mode/mode.ts +10 -22
- package/src/old/client.ts +2 -2
- package/src/old/instructions.ts +4 -10
- package/src/old/login-cdp.ts +50 -6
- package/src/old/to-register.ts +3 -2
- package/src/old/tools/authenticate.ts +9 -1
- package/src/old/tools/get-provider-info.ts +6 -5
- package/src/old/tools/issue-app-credentials.ts +3 -2
- package/src/old/tools/publish.ts +95 -131
- package/src/old/tools/update-provider-metadata.ts +1 -1
- package/src/old/tools/verify-sdk-setup.ts +1 -1
- package/src/proof/attestor.ts +3 -3
- package/src/proof/debug-response-dump.ts +4 -4
- package/src/proof/index.ts +8 -0
- package/src/proof/page-replay.ts +1 -1
- package/src/proof/pod-prover.ts +231 -0
- package/src/provider/dom-xpath.ts +11 -9
- package/src/provider/draft.ts +13 -10
- package/src/provider/injection-doc.ts +37 -24
- package/src/provider/match-redaction-warning.ts +3 -3
- package/src/provider/matcher.ts +13 -12
- package/src/provider/secrets.ts +3 -3
- package/src/provider/to-version.ts +11 -9
- package/src/tools/registry.ts +5 -5
- package/skill/injection-api.md +0 -364
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `share_browser_view` / `stop_browser_view` — put the LOCAL attached tab
|
|
3
|
+
* behind a public URL so someone who is not at this machine can watch it, and
|
|
4
|
+
* optionally drive it.
|
|
5
|
+
*
|
|
6
|
+
* Scoped to the CDP-attached modes (`dedicated`, `attach`, `custom`) on
|
|
7
|
+
* purpose. A `builder`-mode browser is a Popcorn pod that already ships its
|
|
8
|
+
* own hosted live view on `attach_browser`'s `liveViewUrl`; tunnelling a
|
|
9
|
+
* second one would duplicate it and burn a share URL for nothing, so this
|
|
10
|
+
* refuses and points at the one that exists.
|
|
11
|
+
*
|
|
12
|
+
* The share is deliberately explicit and single-instance: one live view per
|
|
13
|
+
* attached browser, started only when asked for, and torn down by
|
|
14
|
+
* `stop_browser_view`, `dispose_browser`, or `reset_session`.
|
|
15
|
+
*/
|
|
16
|
+
import { badRequest, conflict } from '@hapi/boom'
|
|
17
|
+
import assert from 'node:assert'
|
|
18
|
+
import { startTunnel, type Tunnel } from '../../../live/tunnel.ts'
|
|
19
|
+
import { type LiveView, startLiveView } from '../../../live/viewer.ts'
|
|
20
|
+
import { LOGGER } from '../../../logger.ts'
|
|
21
|
+
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
22
|
+
import type { AttachState } from './attach.ts'
|
|
23
|
+
import type { CaptureStore } from './capture.ts'
|
|
24
|
+
|
|
25
|
+
/** Modes whose browser we reach over our own CDP connection, and which
|
|
26
|
+
* therefore have no hosted live view of their own. */
|
|
27
|
+
const SHAREABLE_MODES = new Set(['dedicated', 'attach', 'custom'])
|
|
28
|
+
|
|
29
|
+
export interface LiveShare {
|
|
30
|
+
view: LiveView
|
|
31
|
+
tunnel: Tunnel
|
|
32
|
+
url: string
|
|
33
|
+
interactive: boolean
|
|
34
|
+
startedAt: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LiveShareRef {
|
|
38
|
+
current?: LiveShare
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Tear down whatever share is running. Safe when none is. Exported so the
|
|
42
|
+
* session tools can call it from `dispose_browser` / `reset_session`. */
|
|
43
|
+
export async function stopLiveShare(ref: LiveShareRef): Promise<boolean> {
|
|
44
|
+
const share = ref.current
|
|
45
|
+
if(!share) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ref.current = undefined
|
|
50
|
+
await share.view.stop().catch((err) => {
|
|
51
|
+
LOGGER.warn({ err }, 'live view: server stop failed')
|
|
52
|
+
})
|
|
53
|
+
await share.tunnel.stop().catch((err) => {
|
|
54
|
+
LOGGER.warn({ err }, 'live view: tunnel stop failed')
|
|
55
|
+
})
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function liveViewTools(
|
|
60
|
+
attachRef: { current?: AttachState },
|
|
61
|
+
captures: CaptureStore,
|
|
62
|
+
shareRef: LiveShareRef,
|
|
63
|
+
): RegisteredTool[] {
|
|
64
|
+
return [
|
|
65
|
+
defineTool<{ tabId?: string, interactive?: boolean }>(
|
|
66
|
+
{
|
|
67
|
+
name: 'share_browser_view',
|
|
68
|
+
description:
|
|
69
|
+
'Put the attached tab behind a PUBLIC HTTPS URL (a cloudflared '
|
|
70
|
+
+ 'quick tunnel) so someone elsewhere can watch it live, and by '
|
|
71
|
+
+ 'default click and type in it. Use it to hand a login or a 2FA '
|
|
72
|
+
+ 'step to the person who holds the credentials, or to let a '
|
|
73
|
+
+ 'colleague watch a capture. Returns the URL — relay it to the '
|
|
74
|
+
+ 'developer VERBATIM, on its own line. '
|
|
75
|
+
+ 'ONLY for a locally attached browser (attach_browser mode '
|
|
76
|
+
+ '`dedicated`, `attach`, or `custom`). In `builder` mode the '
|
|
77
|
+
+ 'Popcorn browser already has a hosted live view: use the '
|
|
78
|
+
+ '`liveViewUrl` that attach_browser returned instead. '
|
|
79
|
+
+ 'THE URL IS A CREDENTIAL: anyone holding it can see this tab, '
|
|
80
|
+
+ 'and with the default `interactive: true` can act in it as the '
|
|
81
|
+
+ 'signed-in user. Share it only with the person who needs it, and '
|
|
82
|
+
+ 'call stop_browser_view the moment they are done. Pass '
|
|
83
|
+
+ '`interactive: false` for a watch-only link. '
|
|
84
|
+
+ 'Needs cloudflared: an existing install on PATH is used as-is, '
|
|
85
|
+
+ 'otherwise it is downloaded once to ~/.reclaim/bin on first '
|
|
86
|
+
+ 'share (never at install time). Set RECLAIM_CLOUDFLARED_PATH to '
|
|
87
|
+
+ 'point at your own copy instead.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
tabId: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description:
|
|
94
|
+
'Tab to stream, from list_tabs. Defaults to the tab the '
|
|
95
|
+
+ 'active capture is on, else the first tab.',
|
|
96
|
+
},
|
|
97
|
+
interactive: {
|
|
98
|
+
type: 'boolean',
|
|
99
|
+
description:
|
|
100
|
+
'Let the viewer click and type in the page. Default true. '
|
|
101
|
+
+ 'Set false to stream a read-only view.',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
async({ tabId, interactive }) => {
|
|
107
|
+
const attached = attachRef.current
|
|
108
|
+
assert(attached, badRequest(
|
|
109
|
+
'No browser attached. Call attach_browser first.',
|
|
110
|
+
))
|
|
111
|
+
assert(SHAREABLE_MODES.has(attached.mode), badRequest(
|
|
112
|
+
`share_browser_view does not apply to \`${attached.mode}\` mode. `
|
|
113
|
+
+ 'That browser already has a hosted live view — use the '
|
|
114
|
+
+ '`liveViewUrl` from attach_browser'
|
|
115
|
+
+ (attached.liveViewUrl ? `: ${attached.liveViewUrl}` : '')
|
|
116
|
+
+ '.',
|
|
117
|
+
))
|
|
118
|
+
assert(!shareRef.current, conflict(
|
|
119
|
+
'A live view is already running at '
|
|
120
|
+
+ `${shareRef.current?.url}. Call stop_browser_view first, or `
|
|
121
|
+
+ 'reuse that URL.',
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
const tabs = tabId || captures.activeTab
|
|
125
|
+
? []
|
|
126
|
+
: await attached.handles.listTabs()
|
|
127
|
+
const target = tabId ?? captures.activeTab ?? tabs[0]?.tabId
|
|
128
|
+
assert(target, badRequest(
|
|
129
|
+
'No tab to stream. Open a tab, or call list_tabs to pick one.',
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
const tab = await attached.handles.attachToTab(target)
|
|
133
|
+
const wantsInput = interactive !== false
|
|
134
|
+
const view = await startLiveView({
|
|
135
|
+
tab,
|
|
136
|
+
interactive: wantsInput,
|
|
137
|
+
title: `Reclaim — ${attached.mode} browser`,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
let tunnel: Tunnel
|
|
141
|
+
try {
|
|
142
|
+
tunnel = await startTunnel(view.port)
|
|
143
|
+
} catch(err) {
|
|
144
|
+
// Don't leave the loopback server behind if the tunnel failed.
|
|
145
|
+
await view.stop().catch(() => {})
|
|
146
|
+
throw err
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const url = `${tunnel.url}${view.path}`
|
|
150
|
+
const share: LiveShare = {
|
|
151
|
+
view, tunnel, url, interactive: wantsInput, startedAt: Date.now(),
|
|
152
|
+
}
|
|
153
|
+
shareRef.current = share
|
|
154
|
+
LOGGER.info({ mode: attached.mode, interactive: wantsInput },
|
|
155
|
+
'live view shared')
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
url,
|
|
159
|
+
interactive: wantsInput,
|
|
160
|
+
tabId: target,
|
|
161
|
+
...(share.tunnel.binary.source === 'downloaded'
|
|
162
|
+
? { cloudflaredInstalled: true }
|
|
163
|
+
: {}),
|
|
164
|
+
...(share.tunnel.ready ? {} : { propagating: true }),
|
|
165
|
+
_notes: [
|
|
166
|
+
...(share.tunnel.binary.source === 'downloaded'
|
|
167
|
+
? ['cloudflared was not installed, so it was downloaded once '
|
|
168
|
+
+ 'to ~/.reclaim/bin. Mention that; later shares reuse it.']
|
|
169
|
+
: []),
|
|
170
|
+
...(share.tunnel.ready
|
|
171
|
+
? []
|
|
172
|
+
: ['The tunnel had not started routing yet — if the URL '
|
|
173
|
+
+ 'errors, wait a few seconds and reload.']),
|
|
174
|
+
'Relay `url` to the developer verbatim, on its own line.',
|
|
175
|
+
'The URL is a credential: it needs no login, and anyone with '
|
|
176
|
+
+ 'it can see this tab'
|
|
177
|
+
+ (wantsInput ? ' and act in it as the signed-in user.' : '.'),
|
|
178
|
+
'Call stop_browser_view when they are done — the tunnel stays '
|
|
179
|
+
+ 'open until you do.',
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
defineTool<Record<string, never>>(
|
|
185
|
+
{
|
|
186
|
+
name: 'stop_browser_view',
|
|
187
|
+
description:
|
|
188
|
+
'Close the public live view opened by share_browser_view: stops '
|
|
189
|
+
+ 'the screen stream and shuts the tunnel down, so the URL stops '
|
|
190
|
+
+ 'resolving. Call it as soon as the person you shared with is '
|
|
191
|
+
+ 'finished. The browser itself stays attached and the capture '
|
|
192
|
+
+ 'keeps running.',
|
|
193
|
+
inputSchema: { type: 'object', properties: {} },
|
|
194
|
+
},
|
|
195
|
+
async() => ({ stopped: await stopLiveShare(shareRef) }),
|
|
196
|
+
),
|
|
197
|
+
]
|
|
198
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prove a drafted recipe through the Popcorn pod's OWN `/reclaim/prove` — the
|
|
3
|
+
* remote browser session's own API for creating a proof from that same
|
|
4
|
+
* session — instead of the local in-process attestor SDK (`run_proof`).
|
|
5
|
+
* Recommended for any `builder`-mode attach: the pod is the SAME enclave that
|
|
6
|
+
* holds the browser's TLS session, so proving there reaches the target from
|
|
7
|
+
* the identical exit IP the browser used — including the country-routed egress
|
|
8
|
+
* Popcorn applies when the session was allocated with a country. A local SDK
|
|
9
|
+
* proof runs from the agent's own network instead, which won't match a
|
|
10
|
+
* country-locked / IP-allowlisted target the remote browser could reach.
|
|
11
|
+
*
|
|
12
|
+
* Builder mode only (needs the attach's pod `apiUrl`, from
|
|
13
|
+
* `CreateAgentBrowserSession`) — throws a clear error otherwise rather than
|
|
14
|
+
* silently falling back to the local path.
|
|
15
|
+
*/
|
|
16
|
+
import { badRequest } from '@hapi/boom'
|
|
17
|
+
import assert from 'node:assert'
|
|
18
|
+
import type { ConfigStore } from '../../../config/store.ts'
|
|
19
|
+
import { popcornProve } from '../../../proof/pod-prover.ts'
|
|
20
|
+
import { buildHttpParams } from '../../../proof/proof-core.ts'
|
|
21
|
+
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
22
|
+
import type { AttachState } from './attach.ts'
|
|
23
|
+
import type { CaptureStore } from './capture.ts'
|
|
24
|
+
import { shapeRichProofResult } from './proof.ts'
|
|
25
|
+
|
|
26
|
+
export function podProofTools(
|
|
27
|
+
attachRef: { current?: AttachState, config: ConfigStore },
|
|
28
|
+
captures: CaptureStore,
|
|
29
|
+
): RegisteredTool[] {
|
|
30
|
+
const tool = defineTool<{ captureId: string, draftId: string }>(
|
|
31
|
+
{
|
|
32
|
+
name: 'run_proof_via_pod',
|
|
33
|
+
description:
|
|
34
|
+
'Prove the drafted recipe through the Popcorn pod\'s own TEE '
|
|
35
|
+
+ '(/reclaim/prove) instead of the local attestor SDK — this is '
|
|
36
|
+
+ 'the remote browser session\'s own api for creating a proof '
|
|
37
|
+
+ 'from that same session. Use this instead of run_proof '
|
|
38
|
+
+ 'whenever this capture\'s '
|
|
39
|
+
+ 'attach mode is "builder": the pod is the same enclave that '
|
|
40
|
+
+ 'held the browser\'s TLS session (and its country-routed egress, '
|
|
41
|
+
+ 'if any), so '
|
|
42
|
+
+ 'the proof reaches the target from the browser\'s own exit IP. '
|
|
43
|
+
+ 'No ownerAddress needed — the pod signs with the attestor\'s '
|
|
44
|
+
+ 'own TEE key, not a local credential.',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
captureId: { type: 'string' },
|
|
49
|
+
draftId: { type: 'string' },
|
|
50
|
+
},
|
|
51
|
+
required: ['captureId', 'draftId'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
async({ captureId, draftId }) => {
|
|
55
|
+
const apiUrl = attachRef.current?.apiUrl
|
|
56
|
+
assert(
|
|
57
|
+
apiUrl,
|
|
58
|
+
badRequest(
|
|
59
|
+
'No pod api available for this attach — run_proof_via_pod needs '
|
|
60
|
+
+ 'mode "builder" (attach_browser), not local/custom CDP.',
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const entry = captures.sessions.get(captureId)
|
|
65
|
+
assert(
|
|
66
|
+
entry,
|
|
67
|
+
badRequest(
|
|
68
|
+
`no capture ${captureId} — call start_capture first `
|
|
69
|
+
+ '(it has been reset or never started).',
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const draft = entry.drafts.get(draftId)
|
|
74
|
+
assert(draft, badRequest(`no draft ${draftId} in capture ${captureId}`))
|
|
75
|
+
|
|
76
|
+
const result = await popcornProve({
|
|
77
|
+
apiUrl,
|
|
78
|
+
params: buildHttpParams(draft.provider),
|
|
79
|
+
secretParams: { headers: draft.secrets },
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return shapeRichProofResult(result, {
|
|
83
|
+
provider: draft.provider,
|
|
84
|
+
hasSecrets: Object.keys(draft.secrets).length > 0,
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return [tool]
|
|
90
|
+
}
|
|
@@ -25,13 +25,15 @@ export function proposeTool(resolveBackend: ResolveBackend): RegisteredTool {
|
|
|
25
25
|
+ 'Single-value shorthand: `target` + `name`. Per-target `hash` '
|
|
26
26
|
+ 'OPRF-hashes that value. BY DEFAULT each `name` is shortened to '
|
|
27
27
|
+ 'its last alphanumeric token, lowercased, to form the param key '
|
|
28
|
-
+ '(
|
|
28
|
+
+ '(for example, "employer_name" → "name") — pick names that stay '
|
|
29
|
+
+ 'distinct '
|
|
29
30
|
+ 'after shortening ("firstName"/"surname", not "first_name"/'
|
|
30
31
|
+ '"last_name"). Pass `shortenNames: false` to use each `name` '
|
|
31
32
|
+ 'VERBATIM as the param key instead — it becomes both a regex '
|
|
32
33
|
+ 'capture-group name and a paramValues key, so it must be a plain '
|
|
33
34
|
+ 'identifier (letters/digits/underscore, not starting with a '
|
|
34
|
-
+ 'digit); we recommend lower_snake_case (
|
|
35
|
+
+ 'digit); we recommend lower_snake_case (for example, '
|
|
36
|
+
+ '"employer_name"), '
|
|
35
37
|
+ 'but you choose the exact name.',
|
|
36
38
|
inputSchema: objectSchema(
|
|
37
39
|
{
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ReclaimClient } from '@reclaimprotocol/client'
|
|
2
2
|
import type { JSONSchema } from 'zod/v4/core'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ALLOWED_REQUESTS_HINT,
|
|
5
|
+
USER_SCRIPT_HINT,
|
|
6
|
+
} from '../../../provider/injection-doc.ts'
|
|
4
7
|
import {
|
|
5
8
|
countsFor,
|
|
6
9
|
matchRedactionWarnings,
|
|
@@ -82,7 +85,8 @@ const PROVIDER_SCHEMA: JSONSchema.Schema = {
|
|
|
82
85
|
type: 'object',
|
|
83
86
|
description:
|
|
84
87
|
'A `provider` object from propose_provider, passed through verbatim — ' +
|
|
85
|
-
'the tool builds the version body (the requests[] entry
|
|
88
|
+
'the tool builds the version body (the requests[] entry and so on) ' +
|
|
89
|
+
'for you.',
|
|
86
90
|
properties: {
|
|
87
91
|
name: { type: 'string', description: 'Verification name.' },
|
|
88
92
|
...REQUEST_DRAFT_PROPERTIES,
|
|
@@ -95,7 +99,7 @@ const PROVIDER_SCHEMA: JSONSchema.Schema = {
|
|
|
95
99
|
paramValues: {
|
|
96
100
|
type: 'object',
|
|
97
101
|
description:
|
|
98
|
-
'Real values for the {{param}} placeholders,
|
|
102
|
+
'Real values for the {{param}} placeholders, for example, ' +
|
|
99
103
|
'{ "username": "Syed" }.',
|
|
100
104
|
},
|
|
101
105
|
},
|
|
@@ -111,8 +115,8 @@ const ALLOWED_JS_REQUEST_SCHEMA: JSONSchema.Schema = {
|
|
|
111
115
|
multiple: {
|
|
112
116
|
type: 'boolean',
|
|
113
117
|
description:
|
|
114
|
-
'Allow the template to match many requests (
|
|
115
|
-
'Default true.',
|
|
118
|
+
'Allow the template to match many requests (for example, ' +
|
|
119
|
+
'pagination). Default true.',
|
|
116
120
|
},
|
|
117
121
|
required: {
|
|
118
122
|
type: 'boolean',
|
|
@@ -137,6 +141,7 @@ interface PublishArgs {
|
|
|
137
141
|
interceptorOptions?: {
|
|
138
142
|
interceptorType: 'HAWKEYE' | 'MSWJS' | 'CDP'
|
|
139
143
|
interceptorSettings?: string
|
|
144
|
+
isDocumentRequestReplayEnabled?: boolean
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|
|
@@ -148,36 +153,21 @@ export function createVersionFromCaptureTool(
|
|
|
148
153
|
{
|
|
149
154
|
name: 'create_provider_version_from_capture',
|
|
150
155
|
description:
|
|
151
|
-
'Persist drafted ReclaimProvider(s) as a provider version
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'page-injection script that drives the page to where the request ' +
|
|
162
|
-
'fires) and/or `allowedJsRequests` (extra requests the script may ' +
|
|
163
|
-
'fire, e.g. pagination). ' +
|
|
164
|
-
'Omit `version` to have the tool auto-select the next available ' +
|
|
165
|
-
'version (it lists existing versions and bumps past the highest). ' +
|
|
166
|
-
'The version lands on the draft branch — to go live, call ' +
|
|
167
|
-
'`publish_provider_version` (PRIVATE) or ' +
|
|
168
|
-
'`submit_provider_version_for_review` (PUBLIC) afterwards. ' +
|
|
156
|
+
'Persist drafted ReclaimProvider(s) as a provider version, after ' +
|
|
157
|
+
'run_proof succeeds. Pass the `provider` object(s) from ' +
|
|
158
|
+
'propose_provider VERBATIM — this tool builds the version body ' +
|
|
159
|
+
'(requests[], webSettings, …) for you; do NOT hand-construct that ' +
|
|
160
|
+
'shape or call create_provider_version directly. Exactly one of ' +
|
|
161
|
+
'`provider` (one request) or `providers` (a multi-request provider — ' +
|
|
162
|
+
'every entry is required at verification time). Omit `version` to ' +
|
|
163
|
+
'auto-bump past the highest existing one. The version lands on the ' +
|
|
164
|
+
'draft branch: call `publish_provider_version` (PRIVATE) or ' +
|
|
165
|
+
'`submit_provider_version_for_review` (PUBLIC) to go live. ' +
|
|
169
166
|
'`requiredContext` is derived from any {{context.<name>}} ' +
|
|
170
|
-
'placeholders
|
|
171
|
-
'
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
'field is a 400 at session create). ' +
|
|
175
|
-
'Refine types/descriptions via `update_provider_version` while ' +
|
|
176
|
-
'still on draft. ' +
|
|
177
|
-
'On success, if a browser is still attached, the response\'s ' +
|
|
178
|
-
'`_notes` reminds you to confirm with the developer before ' +
|
|
179
|
-
'calling `dispose_browser` — never close it unprompted, they ' +
|
|
180
|
-
'may want it for further edits.',
|
|
167
|
+
'placeholders and returned on the created version — relay it to the ' +
|
|
168
|
+
'dev verbatim, since a session missing those fields is a 400. Always ' +
|
|
169
|
+
'read the response\'s `_notes`. ' +
|
|
170
|
+
'Full walkthrough: how_it_works({ topic: "publish" }).',
|
|
181
171
|
inputSchema: {
|
|
182
172
|
type: 'object',
|
|
183
173
|
properties: {
|
|
@@ -199,13 +189,15 @@ export function createVersionFromCaptureTool(
|
|
|
199
189
|
type: 'string',
|
|
200
190
|
description:
|
|
201
191
|
'The website URL the user landed on to trigger the request ' +
|
|
202
|
-
'(
|
|
203
|
-
'flow opens for end-users — NOT the API URL
|
|
192
|
+
'(for example, https://github.com/Syed). The URL the ' +
|
|
193
|
+
'verification flow opens for end-users — NOT the API URL ' +
|
|
194
|
+
'inside the provider.',
|
|
204
195
|
},
|
|
205
196
|
notes: {
|
|
206
197
|
type: 'string',
|
|
207
198
|
description:
|
|
208
|
-
'Short change note (
|
|
199
|
+
'Short change note (for example, "Initial draft from ' +
|
|
200
|
+
'browser capture").',
|
|
209
201
|
},
|
|
210
202
|
version: {
|
|
211
203
|
type: 'object',
|
|
@@ -222,45 +214,22 @@ export function createVersionFromCaptureTool(
|
|
|
222
214
|
jsUserScripts: {
|
|
223
215
|
type: 'string',
|
|
224
216
|
description:
|
|
225
|
-
|
|
226
|
-
'
|
|
227
|
-
'
|
|
228
|
-
'
|
|
229
|
-
'
|
|
230
|
-
'
|
|
231
|
-
'verification client installs this to run before every page ' +
|
|
232
|
-
'load (before navigating to initialUrl), matching old-devtool ' +
|
|
233
|
-
'customInjection; window.Reclaim itself is not yet exposed ' +
|
|
234
|
-
'there, so prefer plain browser JS. Guide:\n\n' +
|
|
235
|
-
INJECTION_GUIDE,
|
|
217
|
+
USER_SCRIPT_HINT +
|
|
218
|
+
' Stored as webSettings.jsUserScripts; the Builder ' +
|
|
219
|
+
'verification client runs it before every page load ' +
|
|
220
|
+
'(including before navigating to initialUrl), matching ' +
|
|
221
|
+
'old-devtools customInjection. window.Reclaim is not exposed ' +
|
|
222
|
+
'there yet, so prefer plain browser JS.',
|
|
236
223
|
},
|
|
237
224
|
allowedJsRequests: {
|
|
238
225
|
type: 'array',
|
|
239
226
|
description:
|
|
240
|
-
'Extra requests the
|
|
241
|
-
'
|
|
242
|
-
'
|
|
243
|
-
'
|
|
244
|
-
'
|
|
245
|
-
|
|
246
|
-
'different field name): this whole entry (url, method, body, ' +
|
|
247
|
-
'responseMatches, responseRedactions) feeds a client-side ' +
|
|
248
|
-
'hash-equality check, and `url` is hashed VERBATIM — no regex/ ' +
|
|
249
|
-
'template resolution, even with `urlType: REGEX` or ' +
|
|
250
|
-
'`TEMPLATE`. If the real captured URL contains ANY per-session ' +
|
|
251
|
-
'part (a tracking nonce, CSRF token, cache-busting timestamp), ' +
|
|
252
|
-
'no declared spec can EVER hash-match a real proof — populating' +
|
|
253
|
-
' this for such a provider makes verifyProof reject every real ' +
|
|
254
|
-
'proof, which is worse than leaving it empty. Diff the same ' +
|
|
255
|
-
'endpoint\'s captured URL across two separate sessions before ' +
|
|
256
|
-
'wiring this up. If provider-hash validation keeps causing ' +
|
|
257
|
-
'problems (unstable url, or any other persistent mismatch), ' +
|
|
258
|
-
'the fallback on the verifying side is `verifyProof(proof, { ' +
|
|
259
|
-
'dangerouslyDisableContentValidation: true })` — trusts only ' +
|
|
260
|
-
'the attestor\'s signature, skips the hash-equality check ' +
|
|
261
|
-
'entirely; see the "Provider-hash validation" section of the ' +
|
|
262
|
-
'injection script\'s guide for the full mechanics and when to ' +
|
|
263
|
-
'reach for it.',
|
|
227
|
+
'Extra requests the user script is allowed to fire (for ' +
|
|
228
|
+
'example, paginated API calls), as ' +
|
|
229
|
+
'RequestSelectionTemplates. ' +
|
|
230
|
+
'`multiple`/`required` default to true. Old-devtools calls the ' +
|
|
231
|
+
'same concept `allowedInjectedRequestData`. ' +
|
|
232
|
+
ALLOWED_REQUESTS_HINT,
|
|
264
233
|
items: ALLOWED_JS_REQUEST_SCHEMA,
|
|
265
234
|
},
|
|
266
235
|
interceptorOptions: {
|
|
@@ -269,11 +238,7 @@ export function createVersionFromCaptureTool(
|
|
|
269
238
|
'In-app interception settings, stored as ' +
|
|
270
239
|
'webSettings.clientOptions.inapp.interceptorOptions — only ' +
|
|
271
240
|
'needed for a genuine live-interception provider (a real ' +
|
|
272
|
-
'HAWKEYE/MSWJS script, not the standard capture→replay flow).
|
|
273
|
-
'Document replay (webSettings.clientOptions.inapp' +
|
|
274
|
-
'.interceptorOptions.isDocumentRequestReplayEnabled) is ' +
|
|
275
|
-
'ALWAYS published disabled regardless of this field — there ' +
|
|
276
|
-
'is no way to enable it here.',
|
|
241
|
+
'HAWKEYE/MSWJS script, not the standard capture→replay flow).',
|
|
277
242
|
properties: {
|
|
278
243
|
interceptorType: {
|
|
279
244
|
type: 'string',
|
|
@@ -285,6 +250,16 @@ export function createVersionFromCaptureTool(
|
|
|
285
250
|
description:
|
|
286
251
|
'Free-form JSON config for the chosen interceptor.',
|
|
287
252
|
},
|
|
253
|
+
isDocumentRequestReplayEnabled: {
|
|
254
|
+
type: 'boolean',
|
|
255
|
+
description:
|
|
256
|
+
'Whether the page\'s own document/navigation request is ' +
|
|
257
|
+
'genuinely replayed through the interceptor, rather than ' +
|
|
258
|
+
'constructed locally. Defaults to `false` (disabled) when ' +
|
|
259
|
+
'omitted. Only meaningful for HAWKEYE/MSWJS — pass `true` ' +
|
|
260
|
+
'when the target value is embedded in the page\'s own HTML ' +
|
|
261
|
+
'rather than a separate API call. Inert for CDP.',
|
|
262
|
+
},
|
|
288
263
|
},
|
|
289
264
|
required: ['interceptorType'],
|
|
290
265
|
},
|
|
@@ -2,17 +2,21 @@ import type { ConfigStore } from '../../../config/store.ts'
|
|
|
2
2
|
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
3
3
|
import { type AttachState, disposeAttached } from './attach.ts'
|
|
4
4
|
import { type CaptureStore, resetCaptures } from './capture.ts'
|
|
5
|
+
import { type LiveShareRef, stopLiveShare } from './live-view.ts'
|
|
5
6
|
|
|
6
7
|
export function sessionTools(
|
|
7
8
|
attachRef: { current?: AttachState, config: ConfigStore },
|
|
8
9
|
captures: CaptureStore,
|
|
10
|
+
liveShare: LiveShareRef,
|
|
9
11
|
): RegisteredTool[] {
|
|
10
12
|
return [
|
|
11
13
|
defineTool<Record<string, never>>(
|
|
12
14
|
{
|
|
13
15
|
name: 'get_session',
|
|
14
16
|
description:
|
|
15
|
-
'
|
|
17
|
+
'Report the current attach state, the live captureIds, and the '
|
|
18
|
+
+ 'persisted browser config. Call it to check whether a browser is '
|
|
19
|
+
+ 'still attached before reattaching, or to recover a captureId.',
|
|
16
20
|
inputSchema: { type: 'object', properties: {} },
|
|
17
21
|
},
|
|
18
22
|
async() => ({
|
|
@@ -25,6 +29,17 @@ export function sessionTools(
|
|
|
25
29
|
orgId: attachRef.current?.orgId,
|
|
26
30
|
captures: [...captures.sessions.keys()],
|
|
27
31
|
activeTab: captures.activeTab,
|
|
32
|
+
// The public share, when one is open — so a caller that lost the
|
|
33
|
+
// URL can recover it instead of opening a second tunnel.
|
|
34
|
+
...(liveShare.current
|
|
35
|
+
? {
|
|
36
|
+
sharedView: {
|
|
37
|
+
url: liveShare.current.url,
|
|
38
|
+
interactive: liveShare.current.interactive,
|
|
39
|
+
viewers: liveShare.current.view.viewers(),
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
: {}),
|
|
28
43
|
}),
|
|
29
44
|
),
|
|
30
45
|
defineTool<Record<string, never>>(
|
|
@@ -33,11 +48,12 @@ export function sessionTools(
|
|
|
33
48
|
description:
|
|
34
49
|
'Stop captures, disconnect the current browser, and immediately '
|
|
35
50
|
+ 'delete a Builder-hosted Popcorn session. Custom CDP sessions '
|
|
36
|
-
+ 'are disconnected but never deleted by Reclaim.'
|
|
51
|
+
+ 'are disconnected but never deleted by Reclaim. Also closes a '
|
|
52
|
+
+ 'public live view opened by share_browser_view.',
|
|
37
53
|
inputSchema: { type: 'object', properties: {} },
|
|
38
54
|
},
|
|
39
55
|
async() => {
|
|
40
|
-
await resetAll(attachRef, captures)
|
|
56
|
+
await resetAll(attachRef, captures, liveShare)
|
|
41
57
|
return { disposed: true }
|
|
42
58
|
},
|
|
43
59
|
),
|
|
@@ -46,11 +62,12 @@ export function sessionTools(
|
|
|
46
62
|
name: 'reset_session',
|
|
47
63
|
description:
|
|
48
64
|
'Stop all captures and clear capture state — drops every '
|
|
49
|
-
+ 'session\'s drafts and their in-memory secrets
|
|
65
|
+
+ 'session\'s drafts and their in-memory secrets, and closes a '
|
|
66
|
+
+ 'public live view opened by share_browser_view.',
|
|
50
67
|
inputSchema: { type: 'object', properties: {} },
|
|
51
68
|
},
|
|
52
69
|
async() => {
|
|
53
|
-
await resetAll(attachRef, captures)
|
|
70
|
+
await resetAll(attachRef, captures, liveShare)
|
|
54
71
|
return { reset: true }
|
|
55
72
|
},
|
|
56
73
|
),
|
|
@@ -60,7 +77,11 @@ export function sessionTools(
|
|
|
60
77
|
async function resetAll(
|
|
61
78
|
attachRef: { current?: AttachState, config: ConfigStore },
|
|
62
79
|
captures: CaptureStore,
|
|
80
|
+
liveShare: LiveShareRef,
|
|
63
81
|
) {
|
|
82
|
+
// Before the captures: the live view holds a tab session of its own, and a
|
|
83
|
+
// public URL must never outlive the browser it points at.
|
|
84
|
+
await stopLiveShare(liveShare)
|
|
64
85
|
await resetCaptures(captures)
|
|
65
86
|
await disposeAttached(attachRef)
|
|
66
87
|
}
|
|
@@ -18,17 +18,12 @@ export function authenticateTool(
|
|
|
18
18
|
name,
|
|
19
19
|
description:
|
|
20
20
|
(name === 'authenticate_builder'
|
|
21
|
-
? '
|
|
22
|
-
+ '
|
|
23
|
-
+ '
|
|
24
|
-
+ '
|
|
25
|
-
+ '
|
|
26
|
-
+ '
|
|
27
|
-
+ '(list_builder_organizations — old-devtools has no generated '
|
|
28
|
-
+ 'org tool of its own). It does NOT authorize '
|
|
29
|
-
+ 'create_provider_version_from_capture or get_me_providers, '
|
|
30
|
-
+ 'which need their OWN, separate login via the old-devtools '
|
|
31
|
-
+ 'reclaim_authenticate tool. Check both with get_devtools_mode. '
|
|
21
|
+
? 'OLD-DEVTOOLS MODE ONLY, and narrow: it authorizes exactly two '
|
|
22
|
+
+ 'Builder-exclusive features — allocating a remote browser '
|
|
23
|
+
+ '(attach_browser mode="builder") and list_builder_organizations. '
|
|
24
|
+
+ 'It does NOT authorize create_provider_version_from_capture or '
|
|
25
|
+
+ 'get_me_providers; those need the separate old-devtools login, '
|
|
26
|
+
+ 'reclaim_authenticate. Check both with get_devtools_mode. '
|
|
32
27
|
: '')
|
|
33
28
|
+ 'Authorize the CLI / MCP to make authenticated API calls. Opens '
|
|
34
29
|
+ 'the '
|
|
@@ -37,8 +32,9 @@ export function authenticateTool(
|
|
|
37
32
|
'the user to sign in and approve. On success the access token is ' +
|
|
38
33
|
'cached in ~/.reclaim/config.json and activated for this session.\n\n' +
|
|
39
34
|
'Call with no arguments. This bearer token authenticates ' +
|
|
40
|
-
'dashboard/org-management calls (
|
|
41
|
-
'secret). Programmatic verification calls use a
|
|
35
|
+
'dashboard/org-management calls (for example, list_orgs, or ' +
|
|
36
|
+
'issuing an org secret). Programmatic verification calls use a ' +
|
|
37
|
+
'separate org secret ' +
|
|
42
38
|
'(rorg_…, set as RECLAIM_ORG_SECRET).',
|
|
43
39
|
inputSchema: { type: 'object', properties: {} },
|
|
44
40
|
},
|
|
@@ -85,6 +81,18 @@ export async function runOAuthLoopback(
|
|
|
85
81
|
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier)
|
|
86
82
|
const state = oauth.randomState()
|
|
87
83
|
|
|
84
|
+
// openid-client/oauth4webapi refuse any non-HTTPS issuer by default
|
|
85
|
+
// ("only requests to HTTPS are allowed"). Better Auth's own dev default
|
|
86
|
+
// (`canonicalOrigin()`, packages/app/src/consts.ts) is plain
|
|
87
|
+
// `http://localhost:4001` — the server side has no HTTPS requirement at
|
|
88
|
+
// all — so a local `RECLAIM_API_URL=http://localhost:...` would otherwise
|
|
89
|
+
// always fail here. Loosen it ONLY for loopback hosts, matching what the
|
|
90
|
+
// server already trusts (`authTrustedOrigins()` defaults to
|
|
91
|
+
// localhost:4000/4001 outside production); a non-loopback http:// origin
|
|
92
|
+
// still gets the strict check.
|
|
93
|
+
const isLoopbackAuthBase =
|
|
94
|
+
['localhost', '127.0.0.1', '[::1]'].includes(new URL(authBase).hostname)
|
|
95
|
+
|
|
88
96
|
// Bind the callback port first: the client is registered for this exact
|
|
89
97
|
// loopback redirect.
|
|
90
98
|
const callback = await listenForCallback()
|
|
@@ -119,6 +127,13 @@ export async function runOAuthLoopback(
|
|
|
119
127
|
// `authorizationCodeGrant` fails instead (confirmed by hitting
|
|
120
128
|
// exactly that when this only checked method === 'POST').
|
|
121
129
|
[oauth.customFetch]: _customFetchForOauth,
|
|
130
|
+
// Applies to the `Configuration` this call returns, so the later
|
|
131
|
+
// `buildAuthorizationUrl`/`authorizationCodeGrant` calls below (which
|
|
132
|
+
// reuse that same `config`) stop enforcing HTTPS too — no separate
|
|
133
|
+
// opt-in needed for them.
|
|
134
|
+
...(isLoopbackAuthBase
|
|
135
|
+
? { execute: [oauth.allowInsecureRequests] }
|
|
136
|
+
: {}),
|
|
122
137
|
},
|
|
123
138
|
)
|
|
124
139
|
openBrowserFn(
|