@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,3 +1,16 @@
|
|
|
1
1
|
export declare const BUILDER_MODE_INSTRUCTIONS: string;
|
|
2
|
-
export declare const
|
|
3
|
-
export
|
|
2
|
+
export declare const OLD_MODE_INSTRUCTIONS: string;
|
|
3
|
+
export interface Guide {
|
|
4
|
+
/** Topic name → its markdown, in document order. */
|
|
5
|
+
topics: Map<string, string>;
|
|
6
|
+
/** `{ topic, summary }` for every topic this mode has. */
|
|
7
|
+
index: Array<{
|
|
8
|
+
topic: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** Every topic concatenated — the whole guide, as it used to be served. */
|
|
12
|
+
full: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const GUIDE_BUILDER: Guide;
|
|
15
|
+
export declare const GUIDE_OLD: Guide;
|
|
16
|
+
export declare function guideFor(oldMode: boolean): Guide;
|
package/lib/mcp/instructions.js
CHANGED
|
@@ -1,51 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The authoring documentation, assembled from `skill/*.md` at module load.
|
|
3
|
+
*
|
|
4
|
+
* Two things are served from here:
|
|
5
|
+
* - the always-on MCP server `instructions` (one file per backend mode) —
|
|
6
|
+
* deliberately small, since every client pays for them on every turn;
|
|
7
|
+
* - the deep authoring guide, split into TOPICS and served on demand by the
|
|
8
|
+
* `how_it_works` tool. Nothing here is inlined into a tool schema: a doc
|
|
9
|
+
* embedded in an input-schema description is paid for on every request
|
|
10
|
+
* whether or not it's ever relevant.
|
|
11
|
+
*
|
|
12
|
+
* The prose lives in markdown (published via package.json's `files`) so it
|
|
13
|
+
* can be edited without touching code. Paths resolve identically from
|
|
14
|
+
* `src/mcp` (dev) and `lib/mcp` (published) — both are two levels below the
|
|
15
|
+
* package root.
|
|
16
|
+
*
|
|
17
|
+
* Assembly has three steps:
|
|
18
|
+
* 1. `<!-- common: name -->` splices a shared block out of `SKILL.common.md`
|
|
19
|
+
* (delimited there by `<!-- section: name -->`), so the two mode guides
|
|
20
|
+
* don't drift on the ~60% of prose that is identical.
|
|
21
|
+
* 2. `%CTX%` expands to the mode's consumer-param namespace — `context.` on
|
|
22
|
+
* the builder, `context_` on old-devtools. That is the only textual
|
|
23
|
+
* difference across most shared blocks, so tokenizing it keeps them
|
|
24
|
+
* shared.
|
|
25
|
+
* 3. `<!-- topic: name -->` splits the result into the units `how_it_works`
|
|
26
|
+
* serves. Backend-independent topics come from their own files instead.
|
|
8
27
|
*/
|
|
9
28
|
import assert from 'node:assert';
|
|
10
29
|
import { readFileSync } from 'node:fs';
|
|
11
30
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
31
|
+
function readSkill(file) {
|
|
32
|
+
const url = new URL(`../../skill/${file}`, import.meta.url);
|
|
33
|
+
return readFileSync(fileURLToPath(url), 'utf8')
|
|
34
|
+
// Strip any leading frontmatter defensively.
|
|
35
|
+
.replace(/^---\n[\s\S]*?\n---\n/, '')
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
export const BUILDER_MODE_INSTRUCTIONS = readSkill('builder-mode.md');
|
|
39
|
+
export const OLD_MODE_INSTRUCTIONS = readSkill('old-mode.md');
|
|
40
|
+
/** Split `<!-- marker: name -->`-delimited blocks, keeping document order. */
|
|
41
|
+
function splitBlocks(text, marker) {
|
|
42
|
+
const blocks = new Map();
|
|
43
|
+
const re = new RegExp(`<!-- ${marker}: (\\S+) -->\\n([\\s\\S]*?)(?=\\n<!-- ${marker}: |$)`, 'g');
|
|
24
44
|
for (const match of text.matchAll(re)) {
|
|
25
|
-
// Trim blank LINES only, not inline indentation — some
|
|
45
|
+
// Trim blank LINES only, not inline indentation — some blocks are
|
|
26
46
|
// list-continuation text that's intentionally indented on its first line.
|
|
27
|
-
|
|
47
|
+
blocks.set(match[1], match[2].replace(/^\n+/, '').replace(/\n+$/, ''));
|
|
28
48
|
}
|
|
29
|
-
return
|
|
49
|
+
return blocks;
|
|
30
50
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
const COMMON_SECTIONS = splitBlocks(readSkill('SKILL.common.md'), 'section');
|
|
52
|
+
/** Topics that read the same on both backends, each its own file. */
|
|
53
|
+
const SHARED_TOPIC_FILES = {
|
|
54
|
+
'user-script': 'user-script.md',
|
|
55
|
+
'interception': 'interception.md',
|
|
56
|
+
'hash-validation': 'hash-validation.md',
|
|
57
|
+
};
|
|
58
|
+
/** One line per topic, shown in the `how_it_works` index so a caller can pick
|
|
59
|
+
* the right one without fetching several. Keyed across both modes; only the
|
|
60
|
+
* topics a mode actually has are listed back to it. */
|
|
61
|
+
const TOPIC_SUMMARIES = {
|
|
62
|
+
'overview': 'Terminology, your stance, and the whole loop at a glance.',
|
|
63
|
+
'auth': 'Signing in to the backend, and which token authorizes what.',
|
|
64
|
+
'browser': 'Choosing a browser source (local / remote / custom CDP), '
|
|
65
|
+
+ 'geolocation, live view, and launch troubleshooting.',
|
|
66
|
+
'capture': 'Tabs, login detection, driving the page, and picking an '
|
|
67
|
+
+ 'authenticated request to prove.',
|
|
68
|
+
'draft': 'The OPRF/privacy decision, propose_provider, and the canonical '
|
|
69
|
+
+ 'drafted shape.',
|
|
70
|
+
'params': 'Extracted vs consumer-supplied vs secret params, the URL '
|
|
71
|
+
+ 'character budget, and runtime resolution.',
|
|
72
|
+
'prove': 'replay_request, the mandatory auth-bound check, run_proof, and '
|
|
73
|
+
+ 'diagnosing failures.',
|
|
74
|
+
'publish': 'Creating/reusing a provider, versions, multi-request providers, '
|
|
75
|
+
+ 'and going live.',
|
|
76
|
+
'discover': 'Finding the right providers for a dev\'s app and creating a '
|
|
77
|
+
+ 'verification session.',
|
|
78
|
+
'user-script': 'Authoring jsUserScripts / customInjection to drive '
|
|
79
|
+
+ 'login-gated pages.',
|
|
80
|
+
'interception': 'injectionType / interceptorType, live interception, the '
|
|
81
|
+
+ 'document-replay flag, and what to do when a site won\'t load or '
|
|
82
|
+
+ 'won\'t let the user sign in.',
|
|
83
|
+
'hash-validation': 'bodySniff, and how verifyProof({ providerId }) gates on '
|
|
84
|
+
+ 'the declared spec hash.',
|
|
85
|
+
'credentials': 'The eth proof-owner key run_proof signs with.',
|
|
86
|
+
'never-do': 'The short list of things that always break a provider.',
|
|
87
|
+
'troubleshooting': 'Expired drafts, browser problems, and where to look '
|
|
88
|
+
+ 'next.',
|
|
89
|
+
};
|
|
90
|
+
function buildGuide(file, ctxPrefix) {
|
|
91
|
+
const raw = readSkill(file)
|
|
92
|
+
.replace(/<!-- common: (\S+) -->/g, (_whole, name) => {
|
|
93
|
+
const section = COMMON_SECTIONS.get(name);
|
|
46
94
|
assert(section, `${file}: no SKILL.common.md section named "${name}"`);
|
|
47
95
|
return section;
|
|
96
|
+
})
|
|
97
|
+
.replaceAll('%CTX%', ctxPrefix);
|
|
98
|
+
const topics = splitBlocks(raw, 'topic');
|
|
99
|
+
assert(topics.size, `${file}: no <!-- topic: … --> sections found`);
|
|
100
|
+
for (const [name, path] of Object.entries(SHARED_TOPIC_FILES)) {
|
|
101
|
+
topics.set(name, readSkill(path));
|
|
102
|
+
}
|
|
103
|
+
const index = [...topics.keys()].map((topic) => {
|
|
104
|
+
const summary = TOPIC_SUMMARIES[topic];
|
|
105
|
+
assert(summary, `${file}: no TOPIC_SUMMARIES entry for "${topic}"`);
|
|
106
|
+
return { topic, summary };
|
|
48
107
|
});
|
|
108
|
+
return { topics, index, full: [...topics.values()].join('\n\n') };
|
|
109
|
+
}
|
|
110
|
+
export const GUIDE_BUILDER = buildGuide('SKILL.builder.md', 'context.');
|
|
111
|
+
export const GUIDE_OLD = buildGuide('SKILL.old.md', 'context_');
|
|
112
|
+
export function guideFor(oldMode) {
|
|
113
|
+
return oldMode ? GUIDE_OLD : GUIDE_BUILDER;
|
|
49
114
|
}
|
|
50
|
-
export const PROVIDER_GUIDE_BUILDER = readGuide('../../skill/SKILL.builder.md');
|
|
51
|
-
export const PROVIDER_GUIDE_OLD = readGuide('../../skill/SKILL.old.md');
|
package/lib/mcp/start.js
CHANGED
|
@@ -50,7 +50,8 @@ export async function startMcpServer() {
|
|
|
50
50
|
});
|
|
51
51
|
const sdkClient = new ReclaimSdkClient({ baseUrl: sdkApiUrl() });
|
|
52
52
|
// Shared with buildAgentTools below so the old-mode publish tool can see
|
|
53
|
-
// the live attach state (
|
|
53
|
+
// the live attach state (for example, to flag a still-open browser
|
|
54
|
+
// session).
|
|
54
55
|
const agentState = buildAgentState();
|
|
55
56
|
registerToolList(server, buildBuilderAuthenticationTools(builderClient));
|
|
56
57
|
registerToolList(server, buildBuilderOrganizationTools(builderClient));
|
|
@@ -71,8 +72,8 @@ export async function startMcpServer() {
|
|
|
71
72
|
// collision instead of one tool silently shadowing the other.
|
|
72
73
|
// Register credential tools for ETH credential management.
|
|
73
74
|
registerToolList(server, buildCredentialTools(client));
|
|
74
|
-
// Higher-level verification orchestrations (
|
|
75
|
-
// Authenticates with RECLAIM_ORG_SECRET, not the session client.
|
|
75
|
+
// Higher-level verification orchestrations (for example, fetch + decrypt
|
|
76
|
+
// result). Authenticates with RECLAIM_ORG_SECRET, not the session client.
|
|
76
77
|
registerToolList(server, buildVerificationTools());
|
|
77
78
|
// Add auto-generated tools with snake_case wrappers
|
|
78
79
|
// for every MCP-tagged operation.
|
|
@@ -3,7 +3,11 @@ import { defineTool } from "../../server.js";
|
|
|
3
3
|
export function analyzeTool(captures) {
|
|
4
4
|
return defineTool({
|
|
5
5
|
name: 'analyze_request_constraints',
|
|
6
|
-
description: 'Flag
|
|
6
|
+
description: 'Flag provider-authoring concerns for one captured request — signed '
|
|
7
|
+
+ 'request, CSRF dependency, bot challenge, short-lived token, '
|
|
8
|
+
+ 'GraphQL. Call it on each top candidate from '
|
|
9
|
+
+ 'find_requests_containing before drafting, and relay the returned '
|
|
10
|
+
+ '`concerns` to the developer verbatim.',
|
|
7
11
|
inputSchema: {
|
|
8
12
|
type: 'object',
|
|
9
13
|
properties: {
|
|
@@ -10,6 +10,13 @@ export interface AttachState {
|
|
|
10
10
|
liveViewUrl?: string;
|
|
11
11
|
browserSessionId?: string;
|
|
12
12
|
orgId?: string;
|
|
13
|
+
/** The Popcorn pod's own TEE api base (builder mode only) — lets
|
|
14
|
+
* `run_proof_via_pod` prove through the SAME enclave that holds the
|
|
15
|
+
* browser's TLS session (and its country-routed egress), instead of the
|
|
16
|
+
* local in-process attestor SDK. */
|
|
17
|
+
apiUrl?: string;
|
|
18
|
+
/** The exit country this remote browser was allocated with, if any. */
|
|
19
|
+
countryCode?: string;
|
|
13
20
|
disposeRemote?: () => Promise<void>;
|
|
14
21
|
}
|
|
15
22
|
export interface AttachRef {
|
|
@@ -20,6 +27,6 @@ interface AttachDependencies {
|
|
|
20
27
|
connectLocal: typeof connectCdp;
|
|
21
28
|
connectRemote: typeof connectCdpUrl;
|
|
22
29
|
}
|
|
23
|
-
export declare function attachTool(state: AttachRef, builderClient?: ReclaimClient,
|
|
30
|
+
export declare function attachTool(state: AttachRef, builderClient?: ReclaimClient, beforeReplace?: () => Promise<void>, dependencies?: AttachDependencies): RegisteredTool;
|
|
24
31
|
export declare function disposeAttached(state: AttachRef): Promise<void>;
|
|
25
32
|
export {};
|
|
@@ -1,30 +1,109 @@
|
|
|
1
1
|
import { badRequest } from '@hapi/boom';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import { connectCdp, connectCdpUrl, } from "../../../cdp/client.js";
|
|
4
|
+
import { suggestCountryFromIp } from "../../../cdp/geo.js";
|
|
4
5
|
import { defaultProfileDir, launchChrome, waitForCdpReady, } from "../../../cdp/launcher.js";
|
|
5
6
|
import { defaultConfigPath } from "../../../config/store.js";
|
|
6
7
|
import { defineTool } from "../../server.js";
|
|
8
|
+
/**
|
|
9
|
+
* The browser sources, written once and used twice: rendered into this tool's
|
|
10
|
+
* DESCRIPTION (so an agent can put the choice to the developer straight from
|
|
11
|
+
* the tool list, without a discovery round-trip) and returned verbatim as
|
|
12
|
+
* structured `options` when the tool is called without a `mode` anyway.
|
|
13
|
+
*
|
|
14
|
+
* Being the single source for both is the point — an option list that a model
|
|
15
|
+
* reads in the description but that disagrees with what the tool returns is
|
|
16
|
+
* worse than either alone.
|
|
17
|
+
*
|
|
18
|
+
* `liveView` is here for the same reason. How a developer watches the browser
|
|
19
|
+
* differs per mode — Popcorn hands back a hosted URL, a local Chrome needs
|
|
20
|
+
* `share_browser_view` to tunnel one — and nobody discovers that by guessing.
|
|
21
|
+
* Advertising it alongside the mode is the only place it lands BEFORE the
|
|
22
|
+
* choice is made.
|
|
23
|
+
*/
|
|
24
|
+
const BROWSER_OPTIONS = [
|
|
25
|
+
{
|
|
26
|
+
mode: 'dedicated',
|
|
27
|
+
label: 'Local Chrome, launched for you',
|
|
28
|
+
detail: 'Launches Chrome with a dedicated profile under '
|
|
29
|
+
+ '~/.reclaim/chrome-profile. Easiest local option; nothing to set up.',
|
|
30
|
+
requires: [],
|
|
31
|
+
liveView: 'On this machine only, until you share it: share_browser_view puts the '
|
|
32
|
+
+ 'tab behind a public URL (needs cloudflared) so someone elsewhere can '
|
|
33
|
+
+ 'watch, and by default click and type.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
mode: 'attach',
|
|
37
|
+
label: 'Local Chrome the developer is already running',
|
|
38
|
+
detail: 'Connects to an existing Chrome debugging port. That Chrome must have '
|
|
39
|
+
+ 'been started with BOTH --remote-debugging-port AND a non-default '
|
|
40
|
+
+ '--user-data-dir: since Chrome 136 the debug flag is silently ignored '
|
|
41
|
+
+ 'on the default profile.',
|
|
42
|
+
requires: ['port (default 9222)'],
|
|
43
|
+
liveView: 'On this machine only, until you share it: share_browser_view puts the '
|
|
44
|
+
+ 'tab behind a public URL (needs cloudflared) so someone elsewhere can '
|
|
45
|
+
+ 'watch, and by default click and type.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
mode: 'builder',
|
|
49
|
+
label: 'Remote Builder browser (Popcorn), with a shareable live view',
|
|
50
|
+
detail: 'Quota-accounted remote browser; returns a liveViewUrl the developer '
|
|
51
|
+
+ 'can open or share. The only way to browse a site from a SPECIFIC '
|
|
52
|
+
+ 'COUNTRY — its whole egress is routed there. Geolocation is '
|
|
53
|
+
+ 'optional: '
|
|
54
|
+
+ 'omit countryCode to be offered a suggestion, or pass skipCountry '
|
|
55
|
+
+ 'to allocate without geo-routing.',
|
|
56
|
+
requires: [
|
|
57
|
+
'Builder authentication',
|
|
58
|
+
'orgId (auto-resolved when you have only one)',
|
|
59
|
+
],
|
|
60
|
+
liveView: 'Included — this call returns a hosted liveViewUrl straight from '
|
|
61
|
+
+ 'Popcorn. Nothing to start, and share_browser_view does not apply.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
mode: 'custom',
|
|
65
|
+
label: 'A CDP endpoint the developer supplies',
|
|
66
|
+
detail: 'Connects to any ws:// or wss:// CDP websocket. The URL is treated as '
|
|
67
|
+
+ 'a credential — it is never logged, persisted, or returned.',
|
|
68
|
+
requires: ['cdpUrl'],
|
|
69
|
+
liveView: 'Shareable on request: share_browser_view puts the tab behind a public '
|
|
70
|
+
+ 'URL (needs cloudflared). The developer\'s own cdpUrl is never '
|
|
71
|
+
+ 'exposed by it.',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
/** Flatten {@link BROWSER_OPTIONS} into the tool description. */
|
|
75
|
+
function renderBrowserOptions() {
|
|
76
|
+
return BROWSER_OPTIONS
|
|
77
|
+
.map((option) => {
|
|
78
|
+
const needs = option.requires.length
|
|
79
|
+
? ` Needs: ${option.requires.join('; ')}.`
|
|
80
|
+
: '';
|
|
81
|
+
return `(${option.mode}) ${option.label}. ${option.detail}${needs}`
|
|
82
|
+
+ ` Live view: ${option.liveView}`;
|
|
83
|
+
})
|
|
84
|
+
.join(' ');
|
|
85
|
+
}
|
|
7
86
|
const DEFAULT_DEPENDENCIES = {
|
|
8
87
|
connectLocal: connectCdp,
|
|
9
88
|
connectRemote: connectCdpUrl,
|
|
10
89
|
};
|
|
11
|
-
export function attachTool(state, builderClient,
|
|
90
|
+
export function attachTool(state, builderClient, beforeReplace, dependencies = DEFAULT_DEPENDENCIES) {
|
|
12
91
|
return defineTool({
|
|
13
|
-
name,
|
|
14
|
-
description:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
92
|
+
name: 'attach_browser',
|
|
93
|
+
description: 'Connect the provider-authoring tools to a browser. ASK THE '
|
|
94
|
+
+ 'DEVELOPER which of the four options below they want, then call '
|
|
95
|
+
+ 'this ONCE with their chosen `mode` — never choose for them, and '
|
|
96
|
+
+ 'never assume a previous session\'s choice still applies. You do '
|
|
97
|
+
+ 'not need a discovery call: the options are listed here. '
|
|
98
|
+
+ '(Calling with no `mode` returns the same list as structured '
|
|
99
|
+
+ '`options`, if you\'d rather relay it verbatim.) '
|
|
100
|
+
+ 'When you put the choice to them, INCLUDE each option\'s live-view '
|
|
101
|
+
+ 'line: how they watch the browser differs per mode, and nobody '
|
|
102
|
+
+ 'discovers share_browser_view on their own. '
|
|
103
|
+
+ 'OPTIONS — ' + renderBrowserOptions() + ' '
|
|
104
|
+
+ 'Builder and custom CDP credentials are held in memory and are '
|
|
105
|
+
+ 'never returned. '
|
|
106
|
+
+ 'More: how_it_works({ topic: "browser" }).',
|
|
28
107
|
inputSchema: {
|
|
29
108
|
type: 'object',
|
|
30
109
|
properties: {
|
|
@@ -41,19 +120,41 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
|
|
|
41
120
|
orgId: { type: 'string', format: 'uuid' },
|
|
42
121
|
cdpUrl: { type: 'string', maxLength: 4000 },
|
|
43
122
|
liveViewUrl: { type: 'string', format: 'uri', maxLength: 4000 },
|
|
123
|
+
countryCode: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
pattern: '^[A-Za-z]{2}$',
|
|
126
|
+
description: 'Builder mode only. ISO 3166-1 alpha-2 country the remote '
|
|
127
|
+
+ 'browser browses from, fixed for the whole session. Omit to '
|
|
128
|
+
+ 'get a suggested country back (needsChoice) instead of '
|
|
129
|
+
+ 'allocating a browser.',
|
|
130
|
+
},
|
|
131
|
+
skipCountry: {
|
|
132
|
+
type: 'boolean',
|
|
133
|
+
description: 'Builder mode only. Allocate without geo-routing, skipping '
|
|
134
|
+
+ 'the country prompt. Pass this once the developer has said '
|
|
135
|
+
+ 'they don\'t need a specific country.',
|
|
136
|
+
},
|
|
44
137
|
},
|
|
45
138
|
},
|
|
46
139
|
}, async (parsed) => {
|
|
47
140
|
const persisted = state.config.read()?.browser_agent;
|
|
48
|
-
|
|
141
|
+
// Deliberately NOT falling back to the persisted mode: which browser
|
|
142
|
+
// to drive is the developer's call every time (a remote one costs
|
|
143
|
+
// quota, a local one exposes their own profile), and silently reusing
|
|
144
|
+
// last session's answer takes that decision away from them. The
|
|
145
|
+
// persisted choice is offered back as a default to suggest, not
|
|
146
|
+
// applied.
|
|
147
|
+
const mode = parsed.mode ?? parsed.attachMode;
|
|
49
148
|
if (!mode) {
|
|
50
149
|
return {
|
|
51
150
|
needsChoice: true,
|
|
52
|
-
message: '
|
|
53
|
-
+ '
|
|
54
|
-
+ '
|
|
55
|
-
|
|
56
|
-
|
|
151
|
+
message: 'Ask the developer which browser to use, then call '
|
|
152
|
+
+ 'attach_browser again with their chosen `mode`. Present all '
|
|
153
|
+
+ 'the options below — do not pick one for them.',
|
|
154
|
+
options: BROWSER_OPTIONS,
|
|
155
|
+
...(persisted?.attachMode
|
|
156
|
+
? { previouslyUsed: persisted.attachMode }
|
|
157
|
+
: {}),
|
|
57
158
|
configPath: defaultConfigPath(),
|
|
58
159
|
};
|
|
59
160
|
}
|
|
@@ -80,6 +181,44 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
|
|
|
80
181
|
}
|
|
81
182
|
orgId = orgs[0].id;
|
|
82
183
|
}
|
|
184
|
+
// A remote browser's exit country is fixed for the whole session —
|
|
185
|
+
// Popcorn applies it at allocation and it can't be changed later. So
|
|
186
|
+
// confirm it with the developer BEFORE spending a quota-accounted
|
|
187
|
+
// browser on the wrong one, the same way orgId is confirmed above.
|
|
188
|
+
// Geo-routing is OPTIONAL though, so this asks rather than demands:
|
|
189
|
+
// `skipCountry` is the "no particular country" answer, and it must be
|
|
190
|
+
// as reachable as naming one.
|
|
191
|
+
if (mode === 'builder' && !parsed.countryCode && !parsed.skipCountry) {
|
|
192
|
+
const suggestion = await suggestCountryFromIp();
|
|
193
|
+
return {
|
|
194
|
+
needsChoice: true,
|
|
195
|
+
message: 'Ask the developer which country this browser should browse '
|
|
196
|
+
+ 'from — its egress is routed through that country for the '
|
|
197
|
+
+ 'whole session and cannot be changed afterwards. Geolocation '
|
|
198
|
+
+ 'is optional; offer all three answers below. Nothing has been '
|
|
199
|
+
+ 'allocated yet, so this costs nothing.',
|
|
200
|
+
choices: [
|
|
201
|
+
{
|
|
202
|
+
answer: 'The suggested country',
|
|
203
|
+
howToCall: `attach_browser({ mode: "builder", countryCode: "${suggestion.countryCode}" })`,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
answer: 'A specific country the site must be seen from',
|
|
207
|
+
howToCall: 'attach_browser({ mode: "builder", countryCode: "<ISO '
|
|
208
|
+
+ '3166-1 alpha-2>" })',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
answer: 'No particular country',
|
|
212
|
+
howToCall: 'attach_browser({ mode: "builder", skipCountry: true })',
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
suggestedCountryCode: suggestion.countryCode,
|
|
216
|
+
suggestionSource: 'caller-ip',
|
|
217
|
+
...(suggestion.country ? { country: suggestion.country } : {}),
|
|
218
|
+
...(suggestion.region ? { region: suggestion.region } : {}),
|
|
219
|
+
...(suggestion.city ? { city: suggestion.city } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
83
222
|
const next = await connect(mode, { ...parsed, orgId }, persisted, builderClient, dependencies);
|
|
84
223
|
try {
|
|
85
224
|
await beforeReplace?.();
|
|
@@ -103,6 +242,7 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
|
|
|
103
242
|
? { browserSessionId: next.browserSessionId }
|
|
104
243
|
: {}),
|
|
105
244
|
...(next.liveViewUrl ? { liveViewUrl: next.liveViewUrl } : {}),
|
|
245
|
+
...(next.countryCode ? { countryCode: next.countryCode } : {}),
|
|
106
246
|
};
|
|
107
247
|
});
|
|
108
248
|
}
|
|
@@ -118,8 +258,10 @@ async function connect(mode, parsed, persisted, builderClient, dependencies) {
|
|
|
118
258
|
if (mode === 'builder') {
|
|
119
259
|
assert(builderClient, badRequest('Builder browser service is unavailable'));
|
|
120
260
|
assert(parsed.orgId, badRequest('orgId is required for builder mode'));
|
|
261
|
+
const countryCode = parsed.countryCode?.trim().toUpperCase();
|
|
121
262
|
const { data } = await builderClient.call('CreateAgentBrowserSession', {
|
|
122
263
|
params: { orgId: parsed.orgId },
|
|
264
|
+
body: countryCode ? { countryCode } : {},
|
|
123
265
|
});
|
|
124
266
|
let handles;
|
|
125
267
|
try {
|
|
@@ -140,6 +282,8 @@ async function connect(mode, parsed, persisted, builderClient, dependencies) {
|
|
|
140
282
|
liveViewUrl: data.liveViewUrl,
|
|
141
283
|
browserSessionId: data.id,
|
|
142
284
|
orgId: parsed.orgId,
|
|
285
|
+
...(data.apiUrl ? { apiUrl: data.apiUrl } : {}),
|
|
286
|
+
...(countryCode ? { countryCode } : {}),
|
|
143
287
|
disposeRemote: async () => {
|
|
144
288
|
await builderClient.call('DisposeAgentBrowserSession', {
|
|
145
289
|
params: {
|
|
@@ -14,23 +14,28 @@ import { ConfigStore } from '../../../config/store.ts';
|
|
|
14
14
|
import { type RegisteredTool } from '../../server.ts';
|
|
15
15
|
import { type AttachState } from './attach.ts';
|
|
16
16
|
import { type CaptureStore } from './capture.ts';
|
|
17
|
+
import { type LiveShareRef } from './live-view.ts';
|
|
17
18
|
export interface AgentState {
|
|
18
19
|
attach: {
|
|
19
20
|
current?: AttachState;
|
|
20
21
|
config: ConfigStore;
|
|
21
22
|
};
|
|
22
23
|
captures: CaptureStore;
|
|
24
|
+
/** The one public live view, when `share_browser_view` has opened it. */
|
|
25
|
+
liveShare: LiveShareRef;
|
|
23
26
|
}
|
|
24
|
-
/** Exported so a mode (
|
|
25
|
-
* can build the attach/capture state up front and share the SAME
|
|
26
|
-
* with `buildAgentTools` — letting that publish tool see the live
|
|
27
|
-
* state (
|
|
27
|
+
/** Exported so a mode (for example, old-devtools) that supplies its own
|
|
28
|
+
* publish tool can build the attach/capture state up front and share the SAME
|
|
29
|
+
* instance with `buildAgentTools` — letting that publish tool see the live
|
|
30
|
+
* attach state (for example, to remind the caller about a still-open browser
|
|
31
|
+
* session). */
|
|
28
32
|
export declare function buildAgentState(): AgentState;
|
|
29
33
|
/** Build the flat list of `RegisteredTool` entries, ready for
|
|
30
34
|
* `registerToolList`.
|
|
31
35
|
* Pass `client` to include the builder `create_provider_version_from_capture`;
|
|
32
|
-
* omit it (
|
|
33
|
-
* capture/proof tools — old mode supplies its own publish
|
|
34
|
-
* Pass `externalState` to share attach/capture state with tools
|
|
35
|
-
* elsewhere (
|
|
36
|
+
* omit it (for example, OLD-devtools mode) to register only the
|
|
37
|
+
* backend-independent capture/proof tools — old mode supplies its own publish
|
|
38
|
+
* tool. Pass `externalState` to share attach/capture state with tools
|
|
39
|
+
* registered elsewhere (for example, old mode's publish tool) instead of
|
|
40
|
+
* building a fresh one. */
|
|
36
41
|
export declare function buildAgentTools(client?: ReclaimClient, builderClient?: ReclaimClient | undefined, externalState?: AgentState): RegisteredTool[];
|
|
@@ -16,26 +16,31 @@ import { attachTool } from "./attach.js";
|
|
|
16
16
|
import { captureTools, resetCaptures } from "./capture.js";
|
|
17
17
|
import { listTabsTool } from "./eval.js";
|
|
18
18
|
import { howItWorksTool } from "./how-it-works.js";
|
|
19
|
+
import { liveViewTools } from "./live-view.js";
|
|
20
|
+
import { podProofTools } from "./pod-proof.js";
|
|
19
21
|
import { createVersionFromCaptureTool } from "./publish.js";
|
|
20
22
|
import { sessionTools } from "./session.js";
|
|
21
23
|
import { sharedAuthoringTools } from "./shared.js";
|
|
22
|
-
/** Exported so a mode (
|
|
23
|
-
* can build the attach/capture state up front and share the SAME
|
|
24
|
-
* with `buildAgentTools` — letting that publish tool see the live
|
|
25
|
-
* state (
|
|
24
|
+
/** Exported so a mode (for example, old-devtools) that supplies its own
|
|
25
|
+
* publish tool can build the attach/capture state up front and share the SAME
|
|
26
|
+
* instance with `buildAgentTools` — letting that publish tool see the live
|
|
27
|
+
* attach state (for example, to remind the caller about a still-open browser
|
|
28
|
+
* session). */
|
|
26
29
|
export function buildAgentState() {
|
|
27
30
|
return {
|
|
28
31
|
attach: { config: new ConfigStore() },
|
|
29
32
|
captures: { sessions: new Map() },
|
|
33
|
+
liveShare: {},
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
/** Build the flat list of `RegisteredTool` entries, ready for
|
|
33
37
|
* `registerToolList`.
|
|
34
38
|
* Pass `client` to include the builder `create_provider_version_from_capture`;
|
|
35
|
-
* omit it (
|
|
36
|
-
* capture/proof tools — old mode supplies its own publish
|
|
37
|
-
* Pass `externalState` to share attach/capture state with tools
|
|
38
|
-
* elsewhere (
|
|
39
|
+
* omit it (for example, OLD-devtools mode) to register only the
|
|
40
|
+
* backend-independent capture/proof tools — old mode supplies its own publish
|
|
41
|
+
* tool. Pass `externalState` to share attach/capture state with tools
|
|
42
|
+
* registered elsewhere (for example, old mode's publish tool) instead of
|
|
43
|
+
* building a fresh one. */
|
|
39
44
|
export function buildAgentTools(client, builderClient = client, externalState) {
|
|
40
45
|
const state = externalState ?? buildAgentState();
|
|
41
46
|
ensureAttestorCrypto();
|
|
@@ -44,13 +49,19 @@ export function buildAgentTools(client, builderClient = client, externalState) {
|
|
|
44
49
|
// client. No `client` ⇒ old-devtools mode (see `mcp/start.ts`).
|
|
45
50
|
tools.push(howItWorksTool(!client));
|
|
46
51
|
const beforeReplace = () => resetCaptures(state.captures);
|
|
47
|
-
tools.push(attachTool(state.attach, builderClient,
|
|
48
|
-
tools.push(attachTool(state.attach, builderClient, 'attach_chrome', beforeReplace));
|
|
52
|
+
tools.push(attachTool(state.attach, builderClient, beforeReplace));
|
|
49
53
|
tools.push(listTabsTool(state.attach));
|
|
50
54
|
for (const t of captureTools(state.attach, state.captures)) {
|
|
51
55
|
tools.push(t);
|
|
52
56
|
}
|
|
53
|
-
for (const t of sessionTools(state.attach, state.captures)) {
|
|
57
|
+
for (const t of sessionTools(state.attach, state.captures, state.liveShare)) {
|
|
58
|
+
tools.push(t);
|
|
59
|
+
}
|
|
60
|
+
// Local-CDP only: a builder-mode browser already has a hosted live view.
|
|
61
|
+
for (const t of liveViewTools(state.attach, state.captures, state.liveShare)) {
|
|
62
|
+
tools.push(t);
|
|
63
|
+
}
|
|
64
|
+
for (const t of podProofTools(state.attach, state.captures)) {
|
|
54
65
|
tools.push(t);
|
|
55
66
|
}
|
|
56
67
|
tools.push(analyzeTool(state.captures));
|
|
@@ -54,7 +54,11 @@ export function captureTools(attachRef, captures) {
|
|
|
54
54
|
});
|
|
55
55
|
const getTool = defineTool({
|
|
56
56
|
name: 'get_request',
|
|
57
|
-
description: 'Return full headers
|
|
57
|
+
description: 'Return the full headers and bodies of one captured request. Use it '
|
|
58
|
+
+ 'to inspect a candidate that find_requests_containing ranked. '
|
|
59
|
+
+ 'Unlike every other tool here, the headers are RAW — cookie and '
|
|
60
|
+
+ 'authorization values are visible. Read them; never repeat them to '
|
|
61
|
+
+ 'the developer or write them to a file.',
|
|
58
62
|
inputSchema: {
|
|
59
63
|
type: 'object',
|
|
60
64
|
properties: {
|
|
@@ -5,7 +5,9 @@ import { defineTool } from "../../server.js";
|
|
|
5
5
|
export function listTabsTool(attachRef) {
|
|
6
6
|
return defineTool({
|
|
7
7
|
name: 'list_tabs',
|
|
8
|
-
description: 'List open
|
|
8
|
+
description: 'List the open tabs in the attached browser, with their tabId, title '
|
|
9
|
+
+ 'and URL. Call it after attach_browser to pick the tabId to pass to '
|
|
10
|
+
+ 'start_capture.',
|
|
9
11
|
inputSchema: { type: 'object', properties: {} },
|
|
10
12
|
}, async () => {
|
|
11
13
|
if (!attachRef.current) {
|
|
@@ -15,9 +15,10 @@ export function findTool(resolveBackend) {
|
|
|
15
15
|
+ 'URL/JSON/HTML-escaped, …). Returns a ranked candidate list with '
|
|
16
16
|
+ 'the matched variant, score, and a short snippet — used to locate '
|
|
17
17
|
+ 'the request that returns a target. Also finds SPLIT matches: when '
|
|
18
|
-
+ '`value` is multi-word (
|
|
19
|
-
+ 'every word SEPARATELY (a JSON profile API with
|
|
20
|
-
+ 'distinct fields) is returned with
|
|
18
|
+
+ '`value` is multi-word (for example, a full name), a response '
|
|
19
|
+
+ 'that carries every word SEPARATELY (a JSON profile API with '
|
|
20
|
+
+ 'first and last name in distinct fields) is returned with '
|
|
21
|
+
+ '`splitMatch:true` — prove those '
|
|
21
22
|
+ 'per-field (a redaction per part) rather than as one string. '
|
|
22
23
|
+ 'Server-side; bodies stay here.',
|
|
23
24
|
inputSchema: objectSchema({ value: { type: 'string' }, limit: { type: 'number' } }, ['value']),
|
|
@@ -60,8 +61,8 @@ export function findTool(resolveBackend) {
|
|
|
60
61
|
});
|
|
61
62
|
continue;
|
|
62
63
|
}
|
|
63
|
-
// Split match: every word present separately (
|
|
64
|
-
// in distinct JSON fields). Requires ≥2 tokens so it's specific.
|
|
64
|
+
// Split match: every word present separately (for example, first/last
|
|
65
|
+
// name in distinct JSON fields). Requires ≥2 tokens so it's specific.
|
|
65
66
|
if (tokens.length >= 2 && tokens.every((t) => body.includes(t))) {
|
|
66
67
|
ranked.push({
|
|
67
68
|
...publicRequest(r),
|