@reclaimprotocol/agent 0.1.22 → 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 +13 -10
- package/lib/authoring/tools.js +106 -9
- 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 +29 -3
- 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 +49 -77
- package/lib/mcp/tools/agent/session.d.ts +2 -1
- package/lib/mcp/tools/agent/session.js +26 -7
- package/lib/mcp/tools/agent/shared.js +17 -8
- package/lib/mcp/tools/authenticate/authenticate.d.ts +11 -0
- package/lib/mcp/tools/authenticate/authenticate.js +143 -121
- package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -3
- package/lib/mcp/tools/authenticate/builder-access.js +8 -3
- package/lib/mcp/tools/mode/mode.d.ts +1 -1
- package/lib/mcp/tools/mode/mode.js +26 -7
- package/lib/old/client.d.ts +9 -0
- package/lib/old/client.js +19 -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 +20 -8
- package/lib/old/to-register.js +7 -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/me-providers.js +3 -1
- package/lib/old/tools/publish.d.ts +1 -1
- package/lib/old/tools/publish.js +132 -129
- 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 +36 -0
- package/lib/proof/page-replay.js +76 -0
- package/lib/proof/pod-prover.d.ts +47 -0
- package/lib/proof/pod-prover.js +155 -0
- package/lib/proof/replay.d.ts +28 -0
- package/lib/proof/replay.js +39 -18
- package/lib/provider/dom-xpath.js +11 -9
- package/lib/provider/draft.d.ts +13 -8
- package/lib/provider/draft.js +47 -13
- 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 +122 -12
- package/lib/provider/secrets.d.ts +3 -3
- package/lib/provider/secrets.js +3 -3
- package/lib/provider/to-version.d.ts +12 -9
- package/lib/provider/to-version.js +16 -4
- package/lib/tools/registry.d.ts +5 -5
- package/lib/tools/registry.js +5 -5
- package/package.json +4 -3
- package/samples/demo-server/README.md +7 -6
- package/skill/SKILL.builder.md +356 -189
- package/skill/SKILL.common.md +314 -79
- 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 +53 -12
- package/skill/user-script.md +61 -0
- package/src/authoring/backend.ts +13 -10
- package/src/authoring/tools.ts +128 -9
- 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 +31 -3
- package/src/mcp/tools/agent/publish.ts +54 -78
- package/src/mcp/tools/agent/session.ts +26 -5
- package/src/mcp/tools/agent/shared.ts +19 -8
- package/src/mcp/tools/authenticate/authenticate.ts +167 -165
- package/src/mcp/tools/authenticate/builder-access.ts +8 -3
- package/src/mcp/tools/mode/mode.ts +28 -7
- package/src/old/client.ts +30 -1
- package/src/old/instructions.ts +4 -10
- package/src/old/login-cdp.ts +50 -6
- package/src/old/to-register.ts +28 -8
- 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/me-providers.ts +3 -1
- package/src/old/tools/publish.ts +140 -130
- 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 +131 -0
- package/src/proof/pod-prover.ts +231 -0
- package/src/proof/replay.ts +60 -19
- package/src/provider/dom-xpath.ts +11 -9
- package/src/provider/draft.ts +56 -13
- package/src/provider/injection-doc.ts +37 -24
- package/src/provider/match-redaction-warning.ts +3 -3
- package/src/provider/matcher.ts +151 -10
- package/src/provider/secrets.ts +3 -3
- package/src/provider/to-version.ts +30 -13
- package/src/tools/registry.ts +5 -5
- package/skill/injection-api.md +0 -335
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { type RegisteredTool } from '../../server.ts';
|
|
2
2
|
/**
|
|
3
|
-
* Serves the
|
|
4
|
-
* always-on `instructions` are a terse tool-flow reference; this tool
|
|
5
|
-
* the deep version
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
* Serves the provider-authoring guide on demand, ONE TOPIC AT A TIME. The
|
|
4
|
+
* server's always-on `instructions` are a terse tool-flow reference; this tool
|
|
5
|
+
* is the deep version, so any MCP client — not just Claude Code, which alone
|
|
6
|
+
* auto-loads skills — can pull it.
|
|
7
|
+
*
|
|
8
|
+
* Topic-scoped rather than all-at-once because the full guide is ~7k tokens:
|
|
9
|
+
* paying that to answer "how do I mark a header secret?" crowds out the actual
|
|
10
|
+
* task, which is exactly what makes an agent slow and prone to inventing
|
|
11
|
+
* details. Every response carries the topic index, so a caller that guessed
|
|
12
|
+
* wrong (or called with no topic at all) always learns what else is available
|
|
13
|
+
* without a second round-trip.
|
|
14
|
+
*
|
|
15
|
+
* `oldMode` selects the backend-specific guide, so the agent is never told to
|
|
16
|
+
* call auth/publish tools that aren't registered in its mode. Stateless:
|
|
17
|
+
* callable before `attach_browser`.
|
|
10
18
|
*/
|
|
11
19
|
export declare function howItWorksTool(oldMode: boolean): RegisteredTool;
|
|
@@ -1,24 +1,64 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { guideFor } from "../../instructions.js";
|
|
2
2
|
import { defineTool } from "../../server.js";
|
|
3
3
|
/**
|
|
4
|
-
* Serves the
|
|
5
|
-
* always-on `instructions` are a terse tool-flow reference; this tool
|
|
6
|
-
* the deep version
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Serves the provider-authoring guide on demand, ONE TOPIC AT A TIME. The
|
|
5
|
+
* server's always-on `instructions` are a terse tool-flow reference; this tool
|
|
6
|
+
* is the deep version, so any MCP client — not just Claude Code, which alone
|
|
7
|
+
* auto-loads skills — can pull it.
|
|
8
|
+
*
|
|
9
|
+
* Topic-scoped rather than all-at-once because the full guide is ~7k tokens:
|
|
10
|
+
* paying that to answer "how do I mark a header secret?" crowds out the actual
|
|
11
|
+
* task, which is exactly what makes an agent slow and prone to inventing
|
|
12
|
+
* details. Every response carries the topic index, so a caller that guessed
|
|
13
|
+
* wrong (or called with no topic at all) always learns what else is available
|
|
14
|
+
* without a second round-trip.
|
|
15
|
+
*
|
|
16
|
+
* `oldMode` selects the backend-specific guide, so the agent is never told to
|
|
17
|
+
* call auth/publish tools that aren't registered in its mode. Stateless:
|
|
18
|
+
* callable before `attach_browser`.
|
|
11
19
|
*/
|
|
12
20
|
export function howItWorksTool(oldMode) {
|
|
13
|
-
const guide = oldMode
|
|
21
|
+
const guide = guideFor(oldMode);
|
|
22
|
+
const names = [...guide.topics.keys()];
|
|
14
23
|
return defineTool({
|
|
15
24
|
name: 'how_it_works',
|
|
16
|
-
description: '
|
|
17
|
-
+ '
|
|
18
|
-
+ '
|
|
19
|
-
+ '
|
|
20
|
-
+ '
|
|
21
|
-
+ '
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
description: 'Reclaim provider-authoring guide for THIS server, by topic. Call it '
|
|
26
|
+
+ 'with no arguments for the overview plus the topic index, then again '
|
|
27
|
+
+ 'for whichever topic you need. Topics: '
|
|
28
|
+
+ names.join(', ')
|
|
29
|
+
+ '. Use `all` for the entire guide at once (large — prefer a topic). '
|
|
30
|
+
+ 'Read the relevant topic BEFORE acting unless you are already fluent '
|
|
31
|
+
+ 'in that part of the flow.',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
topic: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
// Deliberately NOT an `enum`: schema validation runs before the
|
|
38
|
+
// handler, so an enum turns a mistyped topic into a validation
|
|
39
|
+
// error instead of a response carrying the real topic list. A
|
|
40
|
+
// plain string lets the tool answer the mistake usefully.
|
|
41
|
+
description: 'Which section to return: ' + names.join(', ')
|
|
42
|
+
+ ', or `all`. Omit for the overview + topic index. An '
|
|
43
|
+
+ 'unrecognized value comes back with the list, not an error.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
}, async ({ topic }) => {
|
|
48
|
+
if (topic === 'all') {
|
|
49
|
+
return { topic: 'all', topics: guide.index, guide: guide.full };
|
|
50
|
+
}
|
|
51
|
+
const requested = topic ?? 'overview';
|
|
52
|
+
const section = guide.topics.get(requested);
|
|
53
|
+
if (!section) {
|
|
54
|
+
// Not an error: hand back the index so the caller self-corrects in
|
|
55
|
+
// the same turn rather than retrying blind.
|
|
56
|
+
return {
|
|
57
|
+
error: `Unknown topic "${requested}".`,
|
|
58
|
+
topics: guide.index,
|
|
59
|
+
guide: guide.topics.get('overview'),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { topic: requested, topics: guide.index, guide: section };
|
|
63
|
+
});
|
|
24
64
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Tunnel } from '../../../live/tunnel.ts';
|
|
2
|
+
import { type LiveView } from '../../../live/viewer.ts';
|
|
3
|
+
import { type RegisteredTool } from '../../server.ts';
|
|
4
|
+
import type { AttachState } from './attach.ts';
|
|
5
|
+
import type { CaptureStore } from './capture.ts';
|
|
6
|
+
export interface LiveShare {
|
|
7
|
+
view: LiveView;
|
|
8
|
+
tunnel: Tunnel;
|
|
9
|
+
url: string;
|
|
10
|
+
interactive: boolean;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
}
|
|
13
|
+
export interface LiveShareRef {
|
|
14
|
+
current?: LiveShare;
|
|
15
|
+
}
|
|
16
|
+
/** Tear down whatever share is running. Safe when none is. Exported so the
|
|
17
|
+
* session tools can call it from `dispose_browser` / `reset_session`. */
|
|
18
|
+
export declare function stopLiveShare(ref: LiveShareRef): Promise<boolean>;
|
|
19
|
+
export declare function liveViewTools(attachRef: {
|
|
20
|
+
current?: AttachState;
|
|
21
|
+
}, captures: CaptureStore, shareRef: LiveShareRef): RegisteredTool[];
|
|
@@ -0,0 +1,153 @@
|
|
|
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 } from "../../../live/tunnel.js";
|
|
19
|
+
import { startLiveView } from "../../../live/viewer.js";
|
|
20
|
+
import { LOGGER } from "../../../logger.js";
|
|
21
|
+
import { defineTool } from "../../server.js";
|
|
22
|
+
/** Modes whose browser we reach over our own CDP connection, and which
|
|
23
|
+
* therefore have no hosted live view of their own. */
|
|
24
|
+
const SHAREABLE_MODES = new Set(['dedicated', 'attach', 'custom']);
|
|
25
|
+
/** Tear down whatever share is running. Safe when none is. Exported so the
|
|
26
|
+
* session tools can call it from `dispose_browser` / `reset_session`. */
|
|
27
|
+
export async function stopLiveShare(ref) {
|
|
28
|
+
const share = ref.current;
|
|
29
|
+
if (!share) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
ref.current = undefined;
|
|
33
|
+
await share.view.stop().catch((err) => {
|
|
34
|
+
LOGGER.warn({ err }, 'live view: server stop failed');
|
|
35
|
+
});
|
|
36
|
+
await share.tunnel.stop().catch((err) => {
|
|
37
|
+
LOGGER.warn({ err }, 'live view: tunnel stop failed');
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
export function liveViewTools(attachRef, captures, shareRef) {
|
|
42
|
+
return [
|
|
43
|
+
defineTool({
|
|
44
|
+
name: 'share_browser_view',
|
|
45
|
+
description: 'Put the attached tab behind a PUBLIC HTTPS URL (a cloudflared '
|
|
46
|
+
+ 'quick tunnel) so someone elsewhere can watch it live, and by '
|
|
47
|
+
+ 'default click and type in it. Use it to hand a login or a 2FA '
|
|
48
|
+
+ 'step to the person who holds the credentials, or to let a '
|
|
49
|
+
+ 'colleague watch a capture. Returns the URL — relay it to the '
|
|
50
|
+
+ 'developer VERBATIM, on its own line. '
|
|
51
|
+
+ 'ONLY for a locally attached browser (attach_browser mode '
|
|
52
|
+
+ '`dedicated`, `attach`, or `custom`). In `builder` mode the '
|
|
53
|
+
+ 'Popcorn browser already has a hosted live view: use the '
|
|
54
|
+
+ '`liveViewUrl` that attach_browser returned instead. '
|
|
55
|
+
+ 'THE URL IS A CREDENTIAL: anyone holding it can see this tab, '
|
|
56
|
+
+ 'and with the default `interactive: true` can act in it as the '
|
|
57
|
+
+ 'signed-in user. Share it only with the person who needs it, and '
|
|
58
|
+
+ 'call stop_browser_view the moment they are done. Pass '
|
|
59
|
+
+ '`interactive: false` for a watch-only link. '
|
|
60
|
+
+ 'Needs cloudflared: an existing install on PATH is used as-is, '
|
|
61
|
+
+ 'otherwise it is downloaded once to ~/.reclaim/bin on first '
|
|
62
|
+
+ 'share (never at install time). Set RECLAIM_CLOUDFLARED_PATH to '
|
|
63
|
+
+ 'point at your own copy instead.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
tabId: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Tab to stream, from list_tabs. Defaults to the tab the '
|
|
70
|
+
+ 'active capture is on, else the first tab.',
|
|
71
|
+
},
|
|
72
|
+
interactive: {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description: 'Let the viewer click and type in the page. Default true. '
|
|
75
|
+
+ 'Set false to stream a read-only view.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}, async ({ tabId, interactive }) => {
|
|
80
|
+
const attached = attachRef.current;
|
|
81
|
+
assert(attached, badRequest('No browser attached. Call attach_browser first.'));
|
|
82
|
+
assert(SHAREABLE_MODES.has(attached.mode), badRequest(`share_browser_view does not apply to \`${attached.mode}\` mode. `
|
|
83
|
+
+ 'That browser already has a hosted live view — use the '
|
|
84
|
+
+ '`liveViewUrl` from attach_browser'
|
|
85
|
+
+ (attached.liveViewUrl ? `: ${attached.liveViewUrl}` : '')
|
|
86
|
+
+ '.'));
|
|
87
|
+
assert(!shareRef.current, conflict('A live view is already running at '
|
|
88
|
+
+ `${shareRef.current?.url}. Call stop_browser_view first, or `
|
|
89
|
+
+ 'reuse that URL.'));
|
|
90
|
+
const tabs = tabId || captures.activeTab
|
|
91
|
+
? []
|
|
92
|
+
: await attached.handles.listTabs();
|
|
93
|
+
const target = tabId ?? captures.activeTab ?? tabs[0]?.tabId;
|
|
94
|
+
assert(target, badRequest('No tab to stream. Open a tab, or call list_tabs to pick one.'));
|
|
95
|
+
const tab = await attached.handles.attachToTab(target);
|
|
96
|
+
const wantsInput = interactive !== false;
|
|
97
|
+
const view = await startLiveView({
|
|
98
|
+
tab,
|
|
99
|
+
interactive: wantsInput,
|
|
100
|
+
title: `Reclaim — ${attached.mode} browser`,
|
|
101
|
+
});
|
|
102
|
+
let tunnel;
|
|
103
|
+
try {
|
|
104
|
+
tunnel = await startTunnel(view.port);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// Don't leave the loopback server behind if the tunnel failed.
|
|
108
|
+
await view.stop().catch(() => { });
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
const url = `${tunnel.url}${view.path}`;
|
|
112
|
+
const share = {
|
|
113
|
+
view, tunnel, url, interactive: wantsInput, startedAt: Date.now(),
|
|
114
|
+
};
|
|
115
|
+
shareRef.current = share;
|
|
116
|
+
LOGGER.info({ mode: attached.mode, interactive: wantsInput }, 'live view shared');
|
|
117
|
+
return {
|
|
118
|
+
url,
|
|
119
|
+
interactive: wantsInput,
|
|
120
|
+
tabId: target,
|
|
121
|
+
...(share.tunnel.binary.source === 'downloaded'
|
|
122
|
+
? { cloudflaredInstalled: true }
|
|
123
|
+
: {}),
|
|
124
|
+
...(share.tunnel.ready ? {} : { propagating: true }),
|
|
125
|
+
_notes: [
|
|
126
|
+
...(share.tunnel.binary.source === 'downloaded'
|
|
127
|
+
? ['cloudflared was not installed, so it was downloaded once '
|
|
128
|
+
+ 'to ~/.reclaim/bin. Mention that; later shares reuse it.']
|
|
129
|
+
: []),
|
|
130
|
+
...(share.tunnel.ready
|
|
131
|
+
? []
|
|
132
|
+
: ['The tunnel had not started routing yet — if the URL '
|
|
133
|
+
+ 'errors, wait a few seconds and reload.']),
|
|
134
|
+
'Relay `url` to the developer verbatim, on its own line.',
|
|
135
|
+
'The URL is a credential: it needs no login, and anyone with '
|
|
136
|
+
+ 'it can see this tab'
|
|
137
|
+
+ (wantsInput ? ' and act in it as the signed-in user.' : '.'),
|
|
138
|
+
'Call stop_browser_view when they are done — the tunnel stays '
|
|
139
|
+
+ 'open until you do.',
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}),
|
|
143
|
+
defineTool({
|
|
144
|
+
name: 'stop_browser_view',
|
|
145
|
+
description: 'Close the public live view opened by share_browser_view: stops '
|
|
146
|
+
+ 'the screen stream and shuts the tunnel down, so the URL stops '
|
|
147
|
+
+ 'resolving. Call it as soon as the person you shared with is '
|
|
148
|
+
+ 'finished. The browser itself stays attached and the capture '
|
|
149
|
+
+ 'keeps running.',
|
|
150
|
+
inputSchema: { type: 'object', properties: {} },
|
|
151
|
+
}, async () => ({ stopped: await stopLiveShare(shareRef) })),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConfigStore } from '../../../config/store.ts';
|
|
2
|
+
import { type RegisteredTool } from '../../server.ts';
|
|
3
|
+
import type { AttachState } from './attach.ts';
|
|
4
|
+
import type { CaptureStore } from './capture.ts';
|
|
5
|
+
export declare function podProofTools(attachRef: {
|
|
6
|
+
current?: AttachState;
|
|
7
|
+
config: ConfigStore;
|
|
8
|
+
}, captures: CaptureStore): RegisteredTool[];
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { popcornProve } from "../../../proof/pod-prover.js";
|
|
19
|
+
import { buildHttpParams } from "../../../proof/proof-core.js";
|
|
20
|
+
import { defineTool } from "../../server.js";
|
|
21
|
+
import { shapeRichProofResult } from "./proof.js";
|
|
22
|
+
export function podProofTools(attachRef, captures) {
|
|
23
|
+
const tool = defineTool({
|
|
24
|
+
name: 'run_proof_via_pod',
|
|
25
|
+
description: 'Prove the drafted recipe through the Popcorn pod\'s own TEE '
|
|
26
|
+
+ '(/reclaim/prove) instead of the local attestor SDK — this is '
|
|
27
|
+
+ 'the remote browser session\'s own api for creating a proof '
|
|
28
|
+
+ 'from that same session. Use this instead of run_proof '
|
|
29
|
+
+ 'whenever this capture\'s '
|
|
30
|
+
+ 'attach mode is "builder": the pod is the same enclave that '
|
|
31
|
+
+ 'held the browser\'s TLS session (and its country-routed egress, '
|
|
32
|
+
+ 'if any), so '
|
|
33
|
+
+ 'the proof reaches the target from the browser\'s own exit IP. '
|
|
34
|
+
+ 'No ownerAddress needed — the pod signs with the attestor\'s '
|
|
35
|
+
+ 'own TEE key, not a local credential.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
captureId: { type: 'string' },
|
|
40
|
+
draftId: { type: 'string' },
|
|
41
|
+
},
|
|
42
|
+
required: ['captureId', 'draftId'],
|
|
43
|
+
},
|
|
44
|
+
}, async ({ captureId, draftId }) => {
|
|
45
|
+
const apiUrl = attachRef.current?.apiUrl;
|
|
46
|
+
assert(apiUrl, badRequest('No pod api available for this attach — run_proof_via_pod needs '
|
|
47
|
+
+ 'mode "builder" (attach_browser), not local/custom CDP.'));
|
|
48
|
+
const entry = captures.sessions.get(captureId);
|
|
49
|
+
assert(entry, badRequest(`no capture ${captureId} — call start_capture first `
|
|
50
|
+
+ '(it has been reset or never started).'));
|
|
51
|
+
const draft = entry.drafts.get(draftId);
|
|
52
|
+
assert(draft, badRequest(`no draft ${draftId} in capture ${captureId}`));
|
|
53
|
+
const result = await popcornProve({
|
|
54
|
+
apiUrl,
|
|
55
|
+
params: buildHttpParams(draft.provider),
|
|
56
|
+
secretParams: { headers: draft.secrets },
|
|
57
|
+
});
|
|
58
|
+
return shapeRichProofResult(result, {
|
|
59
|
+
provider: draft.provider,
|
|
60
|
+
hasSecrets: Object.keys(draft.secrets).length > 0,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
return [tool];
|
|
64
|
+
}
|
|
@@ -19,17 +19,40 @@ export function proposeTool(resolveBackend) {
|
|
|
19
19
|
+ 'one match + redaction per target. Returns a draftId + recipe '
|
|
20
20
|
+ '(never secrets). Prefer a SINGLE request holding ALL values. '
|
|
21
21
|
+ 'Single-value shorthand: `target` + `name`. Per-target `hash` '
|
|
22
|
-
+ 'OPRF-hashes that value.'
|
|
22
|
+
+ 'OPRF-hashes that value. BY DEFAULT each `name` is shortened to '
|
|
23
|
+
+ 'its last alphanumeric token, lowercased, to form the param key '
|
|
24
|
+
+ '(for example, "employer_name" → "name") — pick names that stay '
|
|
25
|
+
+ 'distinct '
|
|
26
|
+
+ 'after shortening ("firstName"/"surname", not "first_name"/'
|
|
27
|
+
+ '"last_name"). Pass `shortenNames: false` to use each `name` '
|
|
28
|
+
+ 'VERBATIM as the param key instead — it becomes both a regex '
|
|
29
|
+
+ 'capture-group name and a paramValues key, so it must be a plain '
|
|
30
|
+
+ 'identifier (letters/digits/underscore, not starting with a '
|
|
31
|
+
+ 'digit); we recommend lower_snake_case (for example, '
|
|
32
|
+
+ '"employer_name"), '
|
|
33
|
+
+ 'but you choose the exact name.',
|
|
23
34
|
inputSchema: objectSchema({
|
|
24
35
|
requestId: { type: 'string' },
|
|
25
36
|
name: { type: 'string' },
|
|
37
|
+
shortenNames: {
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
description: 'Default true: shorten each target `name` to its last '
|
|
40
|
+
+ 'alphanumeric token, lowercased, to form the param key. '
|
|
41
|
+
+ 'Set false to use `name` verbatim instead (must be a plain '
|
|
42
|
+
+ 'identifier — lower_snake_case recommended).',
|
|
43
|
+
},
|
|
26
44
|
targets: {
|
|
27
45
|
type: 'array',
|
|
28
46
|
items: {
|
|
29
47
|
type: 'object',
|
|
30
48
|
properties: {
|
|
31
49
|
value: { type: 'string' },
|
|
32
|
-
name: {
|
|
50
|
+
name: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'The param key, or the source of one — see '
|
|
53
|
+
+ '`shortenNames`. Must stay distinct from other targets\' '
|
|
54
|
+
+ 'names after whichever treatment applies.',
|
|
55
|
+
},
|
|
33
56
|
hash: {
|
|
34
57
|
type: 'string',
|
|
35
58
|
enum: ['oprf-raw', 'oprf', 'oprf-mpc'],
|
|
@@ -46,6 +69,9 @@ export function proposeTool(resolveBackend) {
|
|
|
46
69
|
const requestId = String(args.requestId);
|
|
47
70
|
const providerName = String(args.name);
|
|
48
71
|
const defaultHash = args.hash;
|
|
72
|
+
const shortenNames = typeof args.shortenNames === 'boolean'
|
|
73
|
+
? args.shortenNames
|
|
74
|
+
: true;
|
|
49
75
|
// Explicit multi-value `targets`, else the single `target` shorthand.
|
|
50
76
|
const raw = Array.isArray(args.targets) ? args.targets : [];
|
|
51
77
|
const targets = raw.length
|
|
@@ -85,7 +111,7 @@ export function proposeTool(resolveBackend) {
|
|
|
85
111
|
if (!captured.responseBody) {
|
|
86
112
|
throw new Error(`request body unavailable for ${requestId}`);
|
|
87
113
|
}
|
|
88
|
-
const { provider, secretRefs, extractions } = draftProvider(captured, targets, providerName, defaultHash);
|
|
114
|
+
const { provider, secretRefs, extractions } = draftProvider(captured, targets, providerName, defaultHash, { shortenNames });
|
|
89
115
|
// Materialize the real secret header values for later replay/prove;
|
|
90
116
|
// they stay in the server-side draft, never in this result.
|
|
91
117
|
const secrets = {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native browser-level network proxy for a capture's tab — separate from the
|
|
3
|
+
* attestor's own egress (`geoLocation`/`useProxy` on a published provider).
|
|
4
|
+
* Recommended for a Popcorn (`builder`-mode) remote browser session: applied
|
|
5
|
+
* via the Popcorn extension's `window.__pcn` page API so the browser stays
|
|
6
|
+
* in its existing kiosk-mode window instead of relaunching with a new proxy
|
|
7
|
+
* context — see `../../../cdp/browser-proxy.ts` for the mechanism.
|
|
8
|
+
*
|
|
9
|
+
* Username/password are ALWAYS explicit caller input — this module never
|
|
10
|
+
* reads `PROXY_PASSWORD` (there is no such getter) and never applies a
|
|
11
|
+
* proxy without the caller supplying credentials on the call itself. Env
|
|
12
|
+
* (`PROXY_SERVER`, `PROXY_USERNAME`) only supplies the vendor's routing
|
|
13
|
+
* endpoint and username TEMPLATE, for the "templated" convenience mode.
|
|
14
|
+
*/
|
|
15
|
+
import { badRequest } from '@hapi/boom';
|
|
16
|
+
import assert from 'node:assert';
|
|
17
|
+
import { applyBrowserProxy, buildTemplatedProxyUsername, clearBrowserProxy, generateProxySessionId, parseProxyServer, } from "../../../cdp/browser-proxy.js";
|
|
18
|
+
import { suggestCountryFromIp } from "../../../cdp/geo.js";
|
|
19
|
+
import { proxyServer, proxyUsernameTemplate } from "../../../consts.js";
|
|
20
|
+
import { defineTool } from "../../server.js";
|
|
21
|
+
/** `CdpTabClient.subscribe` is typed against a closed `CdpTabEventMap` of
|
|
22
|
+
* known capture-engine events; this proxy module deliberately listens for
|
|
23
|
+
* CDP `Fetch.*` events outside that map. Same shape of adapter as
|
|
24
|
+
* `client.ts`'s `flattenedSessionClient` sender — forwarding a dynamic
|
|
25
|
+
* method string the declared type doesn't literally cover. */
|
|
26
|
+
function tabProxySender(tabClient) {
|
|
27
|
+
return {
|
|
28
|
+
send: (method, params) => tabClient.send(method, params),
|
|
29
|
+
subscribe: (method, listener) => {
|
|
30
|
+
return tabClient.subscribe(method, listener);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const COUNTRY_CODE_PATTERN = '^[A-Za-z]{2}$';
|
|
35
|
+
export function proxyTools(captures) {
|
|
36
|
+
const requireCapture = (captureId) => {
|
|
37
|
+
const entry = captures.sessions.get(captureId);
|
|
38
|
+
assert(entry, badRequest(`no capture ${captureId} — call start_capture first `
|
|
39
|
+
+ '(it has been reset or never started).'));
|
|
40
|
+
return entry;
|
|
41
|
+
};
|
|
42
|
+
const applyTool = defineTool({
|
|
43
|
+
name: 'apply_browser_proxy',
|
|
44
|
+
description: 'Route this capture\'s browser tab through a native network proxy. '
|
|
45
|
+
+ 'Recommended for a Popcorn (builder-mode) remote browser session — '
|
|
46
|
+
+ 'applied via the Popcorn extension so the browser stays in its '
|
|
47
|
+
+ 'existing kiosk-mode window instead of relaunching. ALWAYS ask the '
|
|
48
|
+
+ 'developer for the proxy `password`, and for `username` when using '
|
|
49
|
+
+ '"full" mode — never invent or reuse a credential from elsewhere. '
|
|
50
|
+
+ 'Two username modes: "full" (caller supplies the exact string) or '
|
|
51
|
+
+ '"templated" (the server\'s PROXY_USERNAME env template combined '
|
|
52
|
+
+ 'with `countryCode` and `proxySessionId`). Missing `countryCode` '
|
|
53
|
+
+ '(templated mode only) and/or missing `proxySessionId` (always) '
|
|
54
|
+
+ 'come back bundled in ONE `needsChoice` response — a best-known/IP '
|
|
55
|
+
+ 'suggestion for country, and a random 10-character alphanumeric '
|
|
56
|
+
+ 'suggestion for the sticky-session id (deliberately NOT this '
|
|
57
|
+
+ 'capture\'s own id — vendors expect a short alphanumeric token, '
|
|
58
|
+
+ 'and the same value later becomes the attestor\'s proxySessionId '
|
|
59
|
+
+ 'too, so reusing it across calls keeps proofs on the same exit '
|
|
60
|
+
+ 'IP the browser used). Ask the developer to confirm or override '
|
|
61
|
+
+ 'each suggestion, then call again with them set. Reloads the '
|
|
62
|
+
+ 'page once applied, since proxy routing only affects new '
|
|
63
|
+
+ 'connections. Also arms proxy-auth handling over CDP `Fetch` '
|
|
64
|
+
+ '(Popcorn\'s extension only sets host/port routing, not '
|
|
65
|
+
+ 'credentials). Subsequent run_proof calls on this captureId '
|
|
66
|
+
+ 'automatically prove through the same proxy session/country.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
captureId: { type: 'string' },
|
|
71
|
+
password: { type: 'string', minLength: 1 },
|
|
72
|
+
username: { type: 'string', minLength: 1 },
|
|
73
|
+
usernameMode: { type: 'string', enum: ['full', 'templated'] },
|
|
74
|
+
countryCode: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
pattern: COUNTRY_CODE_PATTERN,
|
|
77
|
+
description: '2-letter ISO country code, e.g. "US".',
|
|
78
|
+
},
|
|
79
|
+
proxySessionId: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
minLength: 1,
|
|
82
|
+
maxLength: 64,
|
|
83
|
+
description: 'Sticky-session id — same value routes through the same '
|
|
84
|
+
+ 'exit IP, and later becomes the attestor\'s proxySessionId '
|
|
85
|
+
+ 'override too. Omit to get a random 10-character '
|
|
86
|
+
+ 'suggestion back instead of applying anything yet.',
|
|
87
|
+
},
|
|
88
|
+
host: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Override the PROXY_SERVER env default. Give both host and '
|
|
91
|
+
+ 'port, or neither.',
|
|
92
|
+
},
|
|
93
|
+
port: { type: 'number', minimum: 1, maximum: 65535 },
|
|
94
|
+
},
|
|
95
|
+
required: ['captureId', 'password'],
|
|
96
|
+
},
|
|
97
|
+
}, async (args) => {
|
|
98
|
+
const entry = requireCapture(args.captureId);
|
|
99
|
+
const mode = args.usernameMode ?? (args.username ? 'full' : 'templated');
|
|
100
|
+
let template;
|
|
101
|
+
if (mode === 'full') {
|
|
102
|
+
assert(args.username, badRequest('username is required when usernameMode is "full"'));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
template = proxyUsernameTemplate();
|
|
106
|
+
assert(template, badRequest('usernameMode "templated" needs PROXY_USERNAME configured in '
|
|
107
|
+
+ 'the agent\'s environment. Use usernameMode "full" instead, or '
|
|
108
|
+
+ 'set PROXY_USERNAME.'));
|
|
109
|
+
}
|
|
110
|
+
const countryCode = args.countryCode;
|
|
111
|
+
const needsCountrySuggestion = mode === 'templated' && !countryCode;
|
|
112
|
+
const needsSessionIdSuggestion = !args.proxySessionId;
|
|
113
|
+
if (needsCountrySuggestion || needsSessionIdSuggestion) {
|
|
114
|
+
let suggestedCountryCode;
|
|
115
|
+
if (needsCountrySuggestion) {
|
|
116
|
+
const suggestion = await suggestCountryFromIp();
|
|
117
|
+
suggestedCountryCode = suggestion.countryCode;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
needsChoice: true,
|
|
121
|
+
message: 'Confirm or override the suggestion(s) below, then call '
|
|
122
|
+
+ 'apply_browser_proxy again with them set.',
|
|
123
|
+
...(suggestedCountryCode
|
|
124
|
+
? { suggestedCountryCode, suggestionSource: 'caller-ip' }
|
|
125
|
+
: {}),
|
|
126
|
+
...(needsSessionIdSuggestion
|
|
127
|
+
? { suggestedProxySessionId: generateProxySessionId() }
|
|
128
|
+
: {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Non-null: the `needsChoice` branch above already returned for every
|
|
132
|
+
// case where `proxySessionId`/`username`/`template`/`countryCode`
|
|
133
|
+
// (for the applicable mode) could be missing — TS can't see that
|
|
134
|
+
// across the bundled `needsCountrySuggestion || needsSessionIdSuggestion`
|
|
135
|
+
// check, since it narrows through direct property tests, not through
|
|
136
|
+
// intermediate boolean variables.
|
|
137
|
+
const proxySessionId = args.proxySessionId;
|
|
138
|
+
const username = mode === 'full'
|
|
139
|
+
? args.username
|
|
140
|
+
: buildTemplatedProxyUsername({
|
|
141
|
+
template: template, countryCode: countryCode, proxySessionId,
|
|
142
|
+
});
|
|
143
|
+
let server;
|
|
144
|
+
if (args.host && args.port) {
|
|
145
|
+
server = { host: args.host, port: args.port };
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const raw = proxyServer();
|
|
149
|
+
assert(raw, badRequest('No host/port given and PROXY_SERVER is not configured in the '
|
|
150
|
+
+ 'agent\'s environment.'));
|
|
151
|
+
server = parseProxyServer(raw);
|
|
152
|
+
}
|
|
153
|
+
const { appliedViaExtension } = await applyBrowserProxy(tabProxySender(entry.tabClient), {
|
|
154
|
+
host: server.host,
|
|
155
|
+
port: server.port,
|
|
156
|
+
username,
|
|
157
|
+
password: args.password,
|
|
158
|
+
});
|
|
159
|
+
entry.browserProxy = {
|
|
160
|
+
proxySessionId,
|
|
161
|
+
...(countryCode ? { geoLocation: countryCode } : {}),
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
applied: true,
|
|
165
|
+
appliedViaExtension,
|
|
166
|
+
host: server.host,
|
|
167
|
+
port: server.port,
|
|
168
|
+
maskedUsername: maskUsername(username),
|
|
169
|
+
...(countryCode ? { countryCode } : {}),
|
|
170
|
+
proxySessionId,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
const clearTool = defineTool({
|
|
174
|
+
name: 'clear_browser_proxy',
|
|
175
|
+
description: 'Undo apply_browser_proxy for this capture: route its tab direct '
|
|
176
|
+
+ 'again, reload the page, and stop overriding run_proof\'s '
|
|
177
|
+
+ 'geoLocation/proxySessionId.',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: { captureId: { type: 'string' } },
|
|
181
|
+
required: ['captureId'],
|
|
182
|
+
},
|
|
183
|
+
}, async ({ captureId }) => {
|
|
184
|
+
const entry = requireCapture(captureId);
|
|
185
|
+
const sender = tabProxySender(entry.tabClient);
|
|
186
|
+
const { cleared } = await clearBrowserProxy(sender);
|
|
187
|
+
delete entry.browserProxy;
|
|
188
|
+
return { cleared };
|
|
189
|
+
});
|
|
190
|
+
return [applyTool, clearTool];
|
|
191
|
+
}
|
|
192
|
+
/** Never echo a full proxy username verbatim in a tool result — it may embed
|
|
193
|
+
* the vendor account id. Short strings mask fully rather than leaking most
|
|
194
|
+
* of themselves through the "keep first/last 4" rule. */
|
|
195
|
+
function maskUsername(username) {
|
|
196
|
+
return username.length <= 8
|
|
197
|
+
? '*'.repeat(username.length)
|
|
198
|
+
: `${username.slice(0, 4)}…${username.slice(-4)}`;
|
|
199
|
+
}
|