@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.
Files changed (152) hide show
  1. package/README.md +55 -20
  2. package/lib/authoring/backend.d.ts +5 -4
  3. package/lib/authoring/tools.js +30 -30
  4. package/lib/cdp/browser-proxy.d.ts +78 -0
  5. package/lib/cdp/browser-proxy.js +186 -0
  6. package/lib/cdp/capture-engine.js +8 -7
  7. package/lib/cdp/client.d.ts +10 -0
  8. package/lib/cdp/geo.d.ts +10 -0
  9. package/lib/cdp/geo.js +36 -0
  10. package/lib/cdp/index.d.ts +1 -0
  11. package/lib/cdp/index.js +1 -0
  12. package/lib/live/cloudflared.d.ts +15 -0
  13. package/lib/live/cloudflared.js +268 -0
  14. package/lib/live/page.d.ts +20 -0
  15. package/lib/live/page.js +134 -0
  16. package/lib/live/tunnel.d.ts +23 -0
  17. package/lib/live/tunnel.js +120 -0
  18. package/lib/live/viewer.d.ts +28 -0
  19. package/lib/live/viewer.js +232 -0
  20. package/lib/mcp/api/operations.gen.js +34 -26
  21. package/lib/mcp/api/to-zod.d.ts +1 -1
  22. package/lib/mcp/api/to-zod.js +1 -1
  23. package/lib/mcp/api/tools.js +1 -1
  24. package/lib/mcp/instructions.d.ts +15 -2
  25. package/lib/mcp/instructions.js +101 -38
  26. package/lib/mcp/start.js +4 -3
  27. package/lib/mcp/tools/agent/analyze.js +5 -1
  28. package/lib/mcp/tools/agent/attach.d.ts +8 -1
  29. package/lib/mcp/tools/agent/attach.js +166 -22
  30. package/lib/mcp/tools/agent/build.d.ts +13 -8
  31. package/lib/mcp/tools/agent/build.js +22 -11
  32. package/lib/mcp/tools/agent/capture.js +5 -1
  33. package/lib/mcp/tools/agent/eval.js +3 -1
  34. package/lib/mcp/tools/agent/find.js +6 -5
  35. package/lib/mcp/tools/agent/how-it-works.d.ts +15 -7
  36. package/lib/mcp/tools/agent/how-it-works.js +57 -17
  37. package/lib/mcp/tools/agent/live-view.d.ts +21 -0
  38. package/lib/mcp/tools/agent/live-view.js +153 -0
  39. package/lib/mcp/tools/agent/pod-proof.d.ts +8 -0
  40. package/lib/mcp/tools/agent/pod-proof.js +64 -0
  41. package/lib/mcp/tools/agent/propose.js +4 -2
  42. package/lib/mcp/tools/agent/proxy.d.ts +3 -0
  43. package/lib/mcp/tools/agent/proxy.js +199 -0
  44. package/lib/mcp/tools/agent/publish.js +47 -77
  45. package/lib/mcp/tools/agent/session.d.ts +2 -1
  46. package/lib/mcp/tools/agent/session.js +26 -7
  47. package/lib/mcp/tools/authenticate/authenticate.js +26 -13
  48. package/lib/mcp/tools/mode/mode.d.ts +1 -1
  49. package/lib/mcp/tools/mode/mode.js +10 -22
  50. package/lib/old/client.d.ts +1 -1
  51. package/lib/old/client.js +1 -1
  52. package/lib/old/instructions.d.ts +9 -1
  53. package/lib/old/instructions.js +4 -6
  54. package/lib/old/login-cdp.d.ts +2 -2
  55. package/lib/old/login-cdp.js +46 -6
  56. package/lib/old/to-register.d.ts +3 -2
  57. package/lib/old/to-register.js +1 -1
  58. package/lib/old/tools/authenticate.js +9 -1
  59. package/lib/old/tools/get-provider-info.js +6 -5
  60. package/lib/old/tools/issue-app-credentials.js +3 -2
  61. package/lib/old/tools/publish.d.ts +1 -1
  62. package/lib/old/tools/publish.js +90 -131
  63. package/lib/old/tools/update-provider-metadata.d.ts +1 -1
  64. package/lib/old/tools/update-provider-metadata.js +1 -1
  65. package/lib/old/tools/verify-sdk-setup.js +1 -1
  66. package/lib/proof/attestor.d.ts +3 -3
  67. package/lib/proof/debug-response-dump.js +4 -4
  68. package/lib/proof/index.d.ts +1 -0
  69. package/lib/proof/index.js +1 -0
  70. package/lib/proof/page-replay.d.ts +1 -1
  71. package/lib/proof/page-replay.js +1 -1
  72. package/lib/proof/pod-prover.d.ts +47 -0
  73. package/lib/proof/pod-prover.js +155 -0
  74. package/lib/provider/dom-xpath.js +11 -9
  75. package/lib/provider/draft.d.ts +10 -7
  76. package/lib/provider/draft.js +11 -9
  77. package/lib/provider/injection-doc.d.ts +23 -1
  78. package/lib/provider/injection-doc.js +34 -21
  79. package/lib/provider/match-redaction-warning.d.ts +2 -2
  80. package/lib/provider/match-redaction-warning.js +3 -3
  81. package/lib/provider/matcher.js +13 -12
  82. package/lib/provider/secrets.d.ts +3 -3
  83. package/lib/provider/secrets.js +3 -3
  84. package/lib/provider/to-version.d.ts +7 -6
  85. package/lib/provider/to-version.js +3 -3
  86. package/lib/tools/registry.d.ts +5 -5
  87. package/lib/tools/registry.js +5 -5
  88. package/package.json +3 -3
  89. package/samples/demo-server/README.md +7 -6
  90. package/skill/SKILL.builder.md +356 -189
  91. package/skill/SKILL.common.md +323 -97
  92. package/skill/SKILL.old.md +340 -155
  93. package/skill/builder-mode.md +58 -17
  94. package/skill/hash-validation.md +170 -0
  95. package/skill/interception.md +235 -0
  96. package/skill/js-sdk-integration.md +13 -7
  97. package/skill/old-mode.md +60 -21
  98. package/skill/user-script.md +61 -0
  99. package/src/authoring/backend.ts +5 -4
  100. package/src/authoring/tools.ts +31 -30
  101. package/src/cdp/capture-engine.ts +8 -7
  102. package/src/cdp/client.ts +8 -0
  103. package/src/cdp/geo.ts +52 -0
  104. package/src/cdp/index.ts +1 -0
  105. package/src/consts.ts +1 -0
  106. package/src/live/cloudflared.ts +348 -0
  107. package/src/live/page.ts +145 -0
  108. package/src/live/tunnel.ts +157 -0
  109. package/src/live/viewer.ts +293 -0
  110. package/src/mcp/api/operations.gen.ts +34 -26
  111. package/src/mcp/api/to-zod.ts +1 -1
  112. package/src/mcp/api/tools.ts +1 -1
  113. package/src/mcp/instructions.ts +121 -48
  114. package/src/mcp/start.ts +4 -3
  115. package/src/mcp/tools/agent/analyze.ts +5 -1
  116. package/src/mcp/tools/agent/attach.ts +194 -22
  117. package/src/mcp/tools/agent/build.ts +26 -15
  118. package/src/mcp/tools/agent/capture.ts +5 -1
  119. package/src/mcp/tools/agent/eval.ts +4 -1
  120. package/src/mcp/tools/agent/find.ts +6 -5
  121. package/src/mcp/tools/agent/how-it-works.ts +62 -21
  122. package/src/mcp/tools/agent/live-view.ts +198 -0
  123. package/src/mcp/tools/agent/pod-proof.ts +90 -0
  124. package/src/mcp/tools/agent/propose.ts +4 -2
  125. package/src/mcp/tools/agent/publish.ts +52 -77
  126. package/src/mcp/tools/agent/session.ts +26 -5
  127. package/src/mcp/tools/authenticate/authenticate.ts +28 -13
  128. package/src/mcp/tools/mode/mode.ts +10 -22
  129. package/src/old/client.ts +2 -2
  130. package/src/old/instructions.ts +4 -10
  131. package/src/old/login-cdp.ts +50 -6
  132. package/src/old/to-register.ts +3 -2
  133. package/src/old/tools/authenticate.ts +9 -1
  134. package/src/old/tools/get-provider-info.ts +6 -5
  135. package/src/old/tools/issue-app-credentials.ts +3 -2
  136. package/src/old/tools/publish.ts +95 -131
  137. package/src/old/tools/update-provider-metadata.ts +1 -1
  138. package/src/old/tools/verify-sdk-setup.ts +1 -1
  139. package/src/proof/attestor.ts +3 -3
  140. package/src/proof/debug-response-dump.ts +4 -4
  141. package/src/proof/index.ts +8 -0
  142. package/src/proof/page-replay.ts +1 -1
  143. package/src/proof/pod-prover.ts +231 -0
  144. package/src/provider/dom-xpath.ts +11 -9
  145. package/src/provider/draft.ts +13 -10
  146. package/src/provider/injection-doc.ts +37 -24
  147. package/src/provider/match-redaction-warning.ts +3 -3
  148. package/src/provider/matcher.ts +13 -12
  149. package/src/provider/secrets.ts +3 -3
  150. package/src/provider/to-version.ts +11 -9
  151. package/src/tools/registry.ts +5 -5
  152. package/skill/injection-api.md +0 -364
@@ -1,51 +1,114 @@
1
1
  /**
2
- * MCP server instructions shown to the LLM when running in BUILDER mode
3
- * (`USE_OLD_DEVTOOLS=false`). The prose lives in `skill/builder-mode.md`
4
- * (published via the `files` array) so it can be edited as markdown without
5
- * touching code. The path resolves identically from `src/mcp` (dev) and
6
- * `lib/mcp` (published) — both are two levels below the package root.
7
- * Old-devtools mode has its own reduced set in `../old/instructions.ts`.
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
- export const BUILDER_MODE_INSTRUCTIONS = readFileSync(fileURLToPath(new URL('../../skill/builder-mode.md', import.meta.url)), 'utf8').trim();
13
- /**
14
- * `SKILL.builder.md` and `SKILL.old.md` share a lot of prose (steps that are
15
- * identical regardless of backend, "never do" rules, common troubleshooting).
16
- * That shared text lives once in `skill/SKILL.common.md`, split into named
17
- * `<!-- section: name -->`-delimited blocks; each mode file splices a block
18
- * back in at `<!-- common: name -->`. Parsed once at module load.
19
- */
20
- const COMMON_SECTIONS = parseCommonSections(readFileSync(fileURLToPath(new URL('../../skill/SKILL.common.md', import.meta.url)), 'utf8'));
21
- function parseCommonSections(text) {
22
- const sections = {};
23
- const re = /<!-- section: (\S+) -->\n([\s\S]*?)(?=\n<!-- section: |$)/g;
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 sections are
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
- sections[match[1]] = match[2].replace(/^\n+/, '').replace(/\n+$/, '');
47
+ blocks.set(match[1], match[2].replace(/^\n+/, '').replace(/\n+$/, ''));
28
48
  }
29
- return sections;
49
+ return blocks;
30
50
  }
31
- /**
32
- * The full provider-authoring guide — the deep capture→prove→publish workflow,
33
- * decision frameworks, and troubleshooting. Served on demand by the
34
- * `how_it_works` MCP tool so every client (not just Claude Code, which
35
- * auto-loads skills) can pull it. There's a guide PER MODE so an agent is never
36
- * shown auth/publish tools that don't exist in its backend: the server picks
37
- * one by `oldMode` (see `mcp/start.ts`). Same `../../skill/` path resolution as
38
- * the instructions above; any leading frontmatter is stripped defensively.
39
- */
40
- function readGuide(file) {
41
- const url = new URL(file, import.meta.url);
42
- const raw = readFileSync(fileURLToPath(url), 'utf8');
43
- const stripped = raw.replace(/^---\n[\s\S]*?\n---\n/, '').trim();
44
- return stripped.replace(/<!-- common: (\S+) -->/g, (whole, name) => {
45
- const section = COMMON_SECTIONS[name];
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 (e.g. to flag a still-open browser session).
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 (e.g. fetch + decrypt result).
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 potential provider-authoring concerns for a captured request.',
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, name?: 'attach_browser' | 'attach_chrome', beforeReplace?: () => Promise<void>, dependencies?: AttachDependencies): RegisteredTool;
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, name = 'attach_browser', beforeReplace, dependencies = DEFAULT_DEPENDENCIES) {
90
+ export function attachTool(state, builderClient, beforeReplace, dependencies = DEFAULT_DEPENDENCIES) {
12
91
  return defineTool({
13
- name,
14
- description: name === 'attach_chrome'
15
- ? 'Backward-compatible alias for attach_browser.'
16
- : 'Connect the provider-authoring tools to a browser. Choose '
17
- + 'dedicated (launch local Chrome), attach (existing local CDP '
18
- + 'port), builder (quota-accounted Popcorn browser with a '
19
- + 'shareable live view), or custom (caller-supplied CDP '
20
- + 'websocket). '
21
- + 'For builder mode, orgId may be omitted: it auto-resolves to '
22
- + 'the account\'s only organization, or — if there is more '
23
- + 'than one — returns needsChoice with the full list so the '
24
- + 'caller can ask the developer which to use and re-call with '
25
- + 'that orgId. '
26
- + 'Builder and custom CDP credentials are held in memory and never '
27
- + 'returned.',
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
- const mode = parsed.mode ?? parsed.attachMode ?? persisted?.attachMode;
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: 'Choose a browser mode and call attach_browser again: '
53
- + '"dedicated" launches a local dedicated Chrome profile; '
54
- + '"attach" connects to a local Chrome debugging port; '
55
- + '"builder" creates a quota-accounted remote browser and '
56
- + 'requires orgId; "custom" requires a CDP websocket URL.',
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 (e.g. old-devtools) that supplies its own publish tool
25
- * can build the attach/capture state up front and share the SAME instance
26
- * with `buildAgentTools` — letting that publish tool see the live attach
27
- * state (e.g. to remind the caller about a still-open browser session). */
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 (e.g. OLD-devtools mode) to register only the backend-independent
33
- * capture/proof tools — old mode supplies its own publish tool.
34
- * Pass `externalState` to share attach/capture state with tools registered
35
- * elsewhere (e.g. old mode's publish tool) instead of building a fresh one. */
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 (e.g. old-devtools) that supplies its own publish tool
23
- * can build the attach/capture state up front and share the SAME instance
24
- * with `buildAgentTools` — letting that publish tool see the live attach
25
- * state (e.g. to remind the caller about a still-open browser session). */
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 (e.g. OLD-devtools mode) to register only the backend-independent
36
- * capture/proof tools — old mode supplies its own publish tool.
37
- * Pass `externalState` to share attach/capture state with tools registered
38
- * elsewhere (e.g. old mode's publish tool) instead of building a fresh one. */
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, 'attach_browser', beforeReplace));
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 + bodies for a specific captured request.',
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 Chrome tabs.',
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 (e.g. a full name), a response that carries '
19
- + 'every word SEPARATELY (a JSON profile API with first/last name in '
20
- + 'distinct fields) is returned with `splitMatch:true` — prove those '
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 (e.g. first/last name
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),