@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.
Files changed (163) hide show
  1. package/README.md +55 -20
  2. package/lib/authoring/backend.d.ts +13 -10
  3. package/lib/authoring/tools.js +106 -9
  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 +29 -3
  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 +49 -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/agent/shared.js +17 -8
  48. package/lib/mcp/tools/authenticate/authenticate.d.ts +11 -0
  49. package/lib/mcp/tools/authenticate/authenticate.js +143 -121
  50. package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -3
  51. package/lib/mcp/tools/authenticate/builder-access.js +8 -3
  52. package/lib/mcp/tools/mode/mode.d.ts +1 -1
  53. package/lib/mcp/tools/mode/mode.js +26 -7
  54. package/lib/old/client.d.ts +9 -0
  55. package/lib/old/client.js +19 -1
  56. package/lib/old/instructions.d.ts +9 -1
  57. package/lib/old/instructions.js +4 -6
  58. package/lib/old/login-cdp.d.ts +2 -2
  59. package/lib/old/login-cdp.js +46 -6
  60. package/lib/old/to-register.d.ts +20 -8
  61. package/lib/old/to-register.js +7 -1
  62. package/lib/old/tools/authenticate.js +9 -1
  63. package/lib/old/tools/get-provider-info.js +6 -5
  64. package/lib/old/tools/issue-app-credentials.js +3 -2
  65. package/lib/old/tools/me-providers.js +3 -1
  66. package/lib/old/tools/publish.d.ts +1 -1
  67. package/lib/old/tools/publish.js +132 -129
  68. package/lib/old/tools/update-provider-metadata.d.ts +1 -1
  69. package/lib/old/tools/update-provider-metadata.js +1 -1
  70. package/lib/old/tools/verify-sdk-setup.js +1 -1
  71. package/lib/proof/attestor.d.ts +3 -3
  72. package/lib/proof/debug-response-dump.js +4 -4
  73. package/lib/proof/index.d.ts +1 -0
  74. package/lib/proof/index.js +1 -0
  75. package/lib/proof/page-replay.d.ts +36 -0
  76. package/lib/proof/page-replay.js +76 -0
  77. package/lib/proof/pod-prover.d.ts +47 -0
  78. package/lib/proof/pod-prover.js +155 -0
  79. package/lib/proof/replay.d.ts +28 -0
  80. package/lib/proof/replay.js +39 -18
  81. package/lib/provider/dom-xpath.js +11 -9
  82. package/lib/provider/draft.d.ts +13 -8
  83. package/lib/provider/draft.js +47 -13
  84. package/lib/provider/injection-doc.d.ts +23 -1
  85. package/lib/provider/injection-doc.js +34 -21
  86. package/lib/provider/match-redaction-warning.d.ts +2 -2
  87. package/lib/provider/match-redaction-warning.js +3 -3
  88. package/lib/provider/matcher.js +122 -12
  89. package/lib/provider/secrets.d.ts +3 -3
  90. package/lib/provider/secrets.js +3 -3
  91. package/lib/provider/to-version.d.ts +12 -9
  92. package/lib/provider/to-version.js +16 -4
  93. package/lib/tools/registry.d.ts +5 -5
  94. package/lib/tools/registry.js +5 -5
  95. package/package.json +4 -3
  96. package/samples/demo-server/README.md +7 -6
  97. package/skill/SKILL.builder.md +356 -189
  98. package/skill/SKILL.common.md +314 -79
  99. package/skill/SKILL.old.md +340 -155
  100. package/skill/builder-mode.md +58 -17
  101. package/skill/hash-validation.md +170 -0
  102. package/skill/interception.md +235 -0
  103. package/skill/js-sdk-integration.md +13 -7
  104. package/skill/old-mode.md +53 -12
  105. package/skill/user-script.md +61 -0
  106. package/src/authoring/backend.ts +13 -10
  107. package/src/authoring/tools.ts +128 -9
  108. package/src/cdp/capture-engine.ts +8 -7
  109. package/src/cdp/client.ts +8 -0
  110. package/src/cdp/geo.ts +52 -0
  111. package/src/cdp/index.ts +1 -0
  112. package/src/consts.ts +1 -0
  113. package/src/live/cloudflared.ts +348 -0
  114. package/src/live/page.ts +145 -0
  115. package/src/live/tunnel.ts +157 -0
  116. package/src/live/viewer.ts +293 -0
  117. package/src/mcp/api/operations.gen.ts +34 -26
  118. package/src/mcp/api/to-zod.ts +1 -1
  119. package/src/mcp/api/tools.ts +1 -1
  120. package/src/mcp/instructions.ts +121 -48
  121. package/src/mcp/start.ts +4 -3
  122. package/src/mcp/tools/agent/analyze.ts +5 -1
  123. package/src/mcp/tools/agent/attach.ts +194 -22
  124. package/src/mcp/tools/agent/build.ts +26 -15
  125. package/src/mcp/tools/agent/capture.ts +5 -1
  126. package/src/mcp/tools/agent/eval.ts +4 -1
  127. package/src/mcp/tools/agent/find.ts +6 -5
  128. package/src/mcp/tools/agent/how-it-works.ts +62 -21
  129. package/src/mcp/tools/agent/live-view.ts +198 -0
  130. package/src/mcp/tools/agent/pod-proof.ts +90 -0
  131. package/src/mcp/tools/agent/propose.ts +31 -3
  132. package/src/mcp/tools/agent/publish.ts +54 -78
  133. package/src/mcp/tools/agent/session.ts +26 -5
  134. package/src/mcp/tools/agent/shared.ts +19 -8
  135. package/src/mcp/tools/authenticate/authenticate.ts +167 -165
  136. package/src/mcp/tools/authenticate/builder-access.ts +8 -3
  137. package/src/mcp/tools/mode/mode.ts +28 -7
  138. package/src/old/client.ts +30 -1
  139. package/src/old/instructions.ts +4 -10
  140. package/src/old/login-cdp.ts +50 -6
  141. package/src/old/to-register.ts +28 -8
  142. package/src/old/tools/authenticate.ts +9 -1
  143. package/src/old/tools/get-provider-info.ts +6 -5
  144. package/src/old/tools/issue-app-credentials.ts +3 -2
  145. package/src/old/tools/me-providers.ts +3 -1
  146. package/src/old/tools/publish.ts +140 -130
  147. package/src/old/tools/update-provider-metadata.ts +1 -1
  148. package/src/old/tools/verify-sdk-setup.ts +1 -1
  149. package/src/proof/attestor.ts +3 -3
  150. package/src/proof/debug-response-dump.ts +4 -4
  151. package/src/proof/index.ts +8 -0
  152. package/src/proof/page-replay.ts +131 -0
  153. package/src/proof/pod-prover.ts +231 -0
  154. package/src/proof/replay.ts +60 -19
  155. package/src/provider/dom-xpath.ts +11 -9
  156. package/src/provider/draft.ts +56 -13
  157. package/src/provider/injection-doc.ts +37 -24
  158. package/src/provider/match-redaction-warning.ts +3 -3
  159. package/src/provider/matcher.ts +151 -10
  160. package/src/provider/secrets.ts +3 -3
  161. package/src/provider/to-version.ts +30 -13
  162. package/src/tools/registry.ts +5 -5
  163. package/skill/injection-api.md +0 -335
@@ -1,66 +1,139 @@
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
31
 
13
- export const BUILDER_MODE_INSTRUCTIONS = readFileSync(
14
- fileURLToPath(new URL('../../skill/builder-mode.md', import.meta.url)),
15
- 'utf8',
16
- ).trim()
32
+ function readSkill(file: string): string {
33
+ const url = new URL(`../../skill/${file}`, import.meta.url)
34
+ return readFileSync(fileURLToPath(url), 'utf8')
35
+ // Strip any leading frontmatter defensively.
36
+ .replace(/^---\n[\s\S]*?\n---\n/, '')
37
+ .trim()
38
+ }
17
39
 
18
- /**
19
- * `SKILL.builder.md` and `SKILL.old.md` share a lot of prose (steps that are
20
- * identical regardless of backend, "never do" rules, common troubleshooting).
21
- * That shared text lives once in `skill/SKILL.common.md`, split into named
22
- * `<!-- section: name -->`-delimited blocks; each mode file splices a block
23
- * back in at `<!-- common: name -->`. Parsed once at module load.
24
- */
25
- const COMMON_SECTIONS = parseCommonSections(
26
- readFileSync(
27
- fileURLToPath(new URL('../../skill/SKILL.common.md', import.meta.url)),
28
- 'utf8',
29
- ),
30
- )
40
+ export const BUILDER_MODE_INSTRUCTIONS = readSkill('builder-mode.md')
41
+ export const OLD_MODE_INSTRUCTIONS = readSkill('old-mode.md')
31
42
 
32
- function parseCommonSections(text: string): Record<string, string> {
33
- const sections: Record<string, string> = {}
34
- const re = /<!-- section: (\S+) -->\n([\s\S]*?)(?=\n<!-- section: |$)/g
43
+ /** Split `<!-- marker: name -->`-delimited blocks, keeping document order. */
44
+ function splitBlocks(text: string, marker: string): Map<string, string> {
45
+ const blocks = new Map<string, string>()
46
+ const re = new RegExp(
47
+ `<!-- ${marker}: (\\S+) -->\\n([\\s\\S]*?)(?=\\n<!-- ${marker}: |$)`,
48
+ 'g',
49
+ )
35
50
  for(const match of text.matchAll(re)) {
36
- // Trim blank LINES only, not inline indentation — some sections are
51
+ // Trim blank LINES only, not inline indentation — some blocks are
37
52
  // list-continuation text that's intentionally indented on its first line.
38
- sections[match[1]] = match[2].replace(/^\n+/, '').replace(/\n+$/, '')
53
+ blocks.set(match[1], match[2].replace(/^\n+/, '').replace(/\n+$/, ''))
39
54
  }
40
55
 
41
- return sections
56
+ return blocks
42
57
  }
43
58
 
44
- /**
45
- * The full provider-authoring guide — the deep capture→prove→publish workflow,
46
- * decision frameworks, and troubleshooting. Served on demand by the
47
- * `how_it_works` MCP tool so every client (not just Claude Code, which
48
- * auto-loads skills) can pull it. There's a guide PER MODE so an agent is never
49
- * shown auth/publish tools that don't exist in its backend: the server picks
50
- * one by `oldMode` (see `mcp/start.ts`). Same `../../skill/` path resolution as
51
- * the instructions above; any leading frontmatter is stripped defensively.
52
- */
53
- function readGuide(file: string): string {
54
- const url = new URL(file, import.meta.url)
55
- const raw = readFileSync(fileURLToPath(url), 'utf8')
56
- const stripped = raw.replace(/^---\n[\s\S]*?\n---\n/, '').trim()
59
+ const COMMON_SECTIONS = splitBlocks(readSkill('SKILL.common.md'), 'section')
60
+
61
+ /** Topics that read the same on both backends, each its own file. */
62
+ const SHARED_TOPIC_FILES: Record<string, string> = {
63
+ 'user-script': 'user-script.md',
64
+ 'interception': 'interception.md',
65
+ 'hash-validation': 'hash-validation.md',
66
+ }
67
+
68
+ /** One line per topic, shown in the `how_it_works` index so a caller can pick
69
+ * the right one without fetching several. Keyed across both modes; only the
70
+ * topics a mode actually has are listed back to it. */
71
+ const TOPIC_SUMMARIES: Record<string, string> = {
72
+ 'overview': 'Terminology, your stance, and the whole loop at a glance.',
73
+ 'auth': 'Signing in to the backend, and which token authorizes what.',
74
+ 'browser': 'Choosing a browser source (local / remote / custom CDP), '
75
+ + 'geolocation, live view, and launch troubleshooting.',
76
+ 'capture': 'Tabs, login detection, driving the page, and picking an '
77
+ + 'authenticated request to prove.',
78
+ 'draft': 'The OPRF/privacy decision, propose_provider, and the canonical '
79
+ + 'drafted shape.',
80
+ 'params': 'Extracted vs consumer-supplied vs secret params, the URL '
81
+ + 'character budget, and runtime resolution.',
82
+ 'prove': 'replay_request, the mandatory auth-bound check, run_proof, and '
83
+ + 'diagnosing failures.',
84
+ 'publish': 'Creating/reusing a provider, versions, multi-request providers, '
85
+ + 'and going live.',
86
+ 'discover': 'Finding the right providers for a dev\'s app and creating a '
87
+ + 'verification session.',
88
+ 'user-script': 'Authoring jsUserScripts / customInjection to drive '
89
+ + 'login-gated pages.',
90
+ 'interception': 'injectionType / interceptorType, live interception, the '
91
+ + 'document-replay flag, and what to do when a site won\'t load or '
92
+ + 'won\'t let the user sign in.',
93
+ 'hash-validation': 'bodySniff, and how verifyProof({ providerId }) gates on '
94
+ + 'the declared spec hash.',
95
+ 'credentials': 'The eth proof-owner key run_proof signs with.',
96
+ 'never-do': 'The short list of things that always break a provider.',
97
+ 'troubleshooting': 'Expired drafts, browser problems, and where to look '
98
+ + 'next.',
99
+ }
57
100
 
58
- return stripped.replace(/<!-- common: (\S+) -->/g, (whole, name: string) => {
59
- const section = COMMON_SECTIONS[name]
60
- assert(section, `${file}: no SKILL.common.md section named "${name}"`)
61
- return section
101
+ export interface Guide {
102
+ /** Topic name → its markdown, in document order. */
103
+ topics: Map<string, string>
104
+ /** `{ topic, summary }` for every topic this mode has. */
105
+ index: Array<{ topic: string, summary: string }>
106
+ /** Every topic concatenated — the whole guide, as it used to be served. */
107
+ full: string
108
+ }
109
+
110
+ function buildGuide(file: string, ctxPrefix: string): Guide {
111
+ const raw = readSkill(file)
112
+ .replace(/<!-- common: (\S+) -->/g, (_whole, name: string) => {
113
+ const section = COMMON_SECTIONS.get(name)
114
+ assert(section, `${file}: no SKILL.common.md section named "${name}"`)
115
+ return section
116
+ })
117
+ .replaceAll('%CTX%', ctxPrefix)
118
+
119
+ const topics = splitBlocks(raw, 'topic')
120
+ assert(topics.size, `${file}: no <!-- topic: … --> sections found`)
121
+ for(const [name, path] of Object.entries(SHARED_TOPIC_FILES)) {
122
+ topics.set(name, readSkill(path))
123
+ }
124
+
125
+ const index = [...topics.keys()].map((topic) => {
126
+ const summary = TOPIC_SUMMARIES[topic]
127
+ assert(summary, `${file}: no TOPIC_SUMMARIES entry for "${topic}"`)
128
+ return { topic, summary }
62
129
  })
130
+
131
+ return { topics, index, full: [...topics.values()].join('\n\n') }
63
132
  }
64
133
 
65
- export const PROVIDER_GUIDE_BUILDER = readGuide('../../skill/SKILL.builder.md')
66
- export const PROVIDER_GUIDE_OLD = readGuide('../../skill/SKILL.old.md')
134
+ export const GUIDE_BUILDER = buildGuide('SKILL.builder.md', 'context.')
135
+ export const GUIDE_OLD = buildGuide('SKILL.old.md', 'context_')
136
+
137
+ export function guideFor(oldMode: boolean): Guide {
138
+ return oldMode ? GUIDE_OLD : GUIDE_BUILDER
139
+ }
package/src/mcp/start.ts CHANGED
@@ -75,7 +75,8 @@ export async function startMcpServer() {
75
75
  })
76
76
  const sdkClient = new ReclaimSdkClient({ baseUrl: sdkApiUrl() })
77
77
  // Shared with buildAgentTools below so the old-mode publish tool can see
78
- // the live attach state (e.g. to flag a still-open browser session).
78
+ // the live attach state (for example, to flag a still-open browser
79
+ // session).
79
80
  const agentState = buildAgentState()
80
81
 
81
82
  registerToolList(server, buildBuilderAuthenticationTools(builderClient))
@@ -105,8 +106,8 @@ export async function startMcpServer() {
105
106
  // Register credential tools for ETH credential management.
106
107
  registerToolList(server, buildCredentialTools(client))
107
108
 
108
- // Higher-level verification orchestrations (e.g. fetch + decrypt result).
109
- // Authenticates with RECLAIM_ORG_SECRET, not the session client.
109
+ // Higher-level verification orchestrations (for example, fetch + decrypt
110
+ // result). Authenticates with RECLAIM_ORG_SECRET, not the session client.
110
111
  registerToolList(server, buildVerificationTools())
111
112
 
112
113
  // Add auto-generated tools with snake_case wrappers
@@ -7,7 +7,11 @@ export function analyzeTool(captures: CaptureStore): RegisteredTool {
7
7
  {
8
8
  name: 'analyze_request_constraints',
9
9
  description:
10
- 'Flag potential provider-authoring concerns for a captured request.',
10
+ 'Flag provider-authoring concerns for one captured request — signed '
11
+ + 'request, CSRF dependency, bot challenge, short-lived token, '
12
+ + 'GraphQL. Call it on each top candidate from '
13
+ + 'find_requests_containing before drafting, and relay the returned '
14
+ + '`concerns` to the developer verbatim.',
11
15
  inputSchema: {
12
16
  type: 'object',
13
17
  properties: {
@@ -6,6 +6,7 @@ import {
6
6
  connectCdp,
7
7
  connectCdpUrl,
8
8
  } from '../../../cdp/client.ts'
9
+ import { suggestCountryFromIp } from '../../../cdp/geo.ts'
9
10
  import {
10
11
  defaultProfileDir,
11
12
  launchChrome,
@@ -24,6 +25,13 @@ export interface AttachState {
24
25
  liveViewUrl?: string
25
26
  browserSessionId?: string
26
27
  orgId?: string
28
+ /** The Popcorn pod's own TEE api base (builder mode only) — lets
29
+ * `run_proof_via_pod` prove through the SAME enclave that holds the
30
+ * browser's TLS session (and its country-routed egress), instead of the
31
+ * local in-process attestor SDK. */
32
+ apiUrl?: string
33
+ /** The exit country this remote browser was allocated with, if any. */
34
+ countryCode?: string
27
35
  disposeRemote?: () => Promise<void>
28
36
  }
29
37
 
@@ -40,6 +48,99 @@ interface AttachArgs {
40
48
  orgId?: string
41
49
  cdpUrl?: string
42
50
  liveViewUrl?: string
51
+ /** Builder mode: the country the remote browser should browse from. */
52
+ countryCode?: string
53
+ /** Builder mode: allocate without geo-routing, skipping the country
54
+ * prompt. Geolocation is optional — this is how the caller says so. */
55
+ skipCountry?: boolean
56
+ }
57
+
58
+ /**
59
+ * The browser sources, written once and used twice: rendered into this tool's
60
+ * DESCRIPTION (so an agent can put the choice to the developer straight from
61
+ * the tool list, without a discovery round-trip) and returned verbatim as
62
+ * structured `options` when the tool is called without a `mode` anyway.
63
+ *
64
+ * Being the single source for both is the point — an option list that a model
65
+ * reads in the description but that disagrees with what the tool returns is
66
+ * worse than either alone.
67
+ *
68
+ * `liveView` is here for the same reason. How a developer watches the browser
69
+ * differs per mode — Popcorn hands back a hosted URL, a local Chrome needs
70
+ * `share_browser_view` to tunnel one — and nobody discovers that by guessing.
71
+ * Advertising it alongside the mode is the only place it lands BEFORE the
72
+ * choice is made.
73
+ */
74
+ const BROWSER_OPTIONS = [
75
+ {
76
+ mode: 'dedicated',
77
+ label: 'Local Chrome, launched for you',
78
+ detail:
79
+ 'Launches Chrome with a dedicated profile under '
80
+ + '~/.reclaim/chrome-profile. Easiest local option; nothing to set up.',
81
+ requires: [],
82
+ liveView:
83
+ 'On this machine only, until you share it: share_browser_view puts the '
84
+ + 'tab behind a public URL (needs cloudflared) so someone elsewhere can '
85
+ + 'watch, and by default click and type.',
86
+ },
87
+ {
88
+ mode: 'attach',
89
+ label: 'Local Chrome the developer is already running',
90
+ detail:
91
+ 'Connects to an existing Chrome debugging port. That Chrome must have '
92
+ + 'been started with BOTH --remote-debugging-port AND a non-default '
93
+ + '--user-data-dir: since Chrome 136 the debug flag is silently ignored '
94
+ + 'on the default profile.',
95
+ requires: ['port (default 9222)'],
96
+ liveView:
97
+ 'On this machine only, until you share it: share_browser_view puts the '
98
+ + 'tab behind a public URL (needs cloudflared) so someone elsewhere can '
99
+ + 'watch, and by default click and type.',
100
+ },
101
+ {
102
+ mode: 'builder',
103
+ label: 'Remote Builder browser (Popcorn), with a shareable live view',
104
+ detail:
105
+ 'Quota-accounted remote browser; returns a liveViewUrl the developer '
106
+ + 'can open or share. The only way to browse a site from a SPECIFIC '
107
+ + 'COUNTRY — its whole egress is routed there. Geolocation is '
108
+ + 'optional: '
109
+ + 'omit countryCode to be offered a suggestion, or pass skipCountry '
110
+ + 'to allocate without geo-routing.',
111
+ requires: [
112
+ 'Builder authentication',
113
+ 'orgId (auto-resolved when you have only one)',
114
+ ],
115
+ liveView:
116
+ 'Included — this call returns a hosted liveViewUrl straight from '
117
+ + 'Popcorn. Nothing to start, and share_browser_view does not apply.',
118
+ },
119
+ {
120
+ mode: 'custom',
121
+ label: 'A CDP endpoint the developer supplies',
122
+ detail:
123
+ 'Connects to any ws:// or wss:// CDP websocket. The URL is treated as '
124
+ + 'a credential — it is never logged, persisted, or returned.',
125
+ requires: ['cdpUrl'],
126
+ liveView:
127
+ 'Shareable on request: share_browser_view puts the tab behind a public '
128
+ + 'URL (needs cloudflared). The developer\'s own cdpUrl is never '
129
+ + 'exposed by it.',
130
+ },
131
+ ] as const
132
+
133
+ /** Flatten {@link BROWSER_OPTIONS} into the tool description. */
134
+ function renderBrowserOptions(): string {
135
+ return BROWSER_OPTIONS
136
+ .map((option) => {
137
+ const needs = option.requires.length
138
+ ? ` Needs: ${option.requires.join('; ')}.`
139
+ : ''
140
+ return `(${option.mode}) ${option.label}. ${option.detail}${needs}`
141
+ + ` Live view: ${option.liveView}`
142
+ })
143
+ .join(' ')
43
144
  }
44
145
 
45
146
  interface AttachDependencies {
@@ -55,28 +156,27 @@ const DEFAULT_DEPENDENCIES: AttachDependencies = {
55
156
  export function attachTool(
56
157
  state: AttachRef,
57
158
  builderClient?: ReclaimClient,
58
- name: 'attach_browser' | 'attach_chrome' = 'attach_browser',
59
159
  beforeReplace?: () => Promise<void>,
60
160
  dependencies: AttachDependencies = DEFAULT_DEPENDENCIES,
61
161
  ): RegisteredTool {
62
162
  return defineTool<AttachArgs>(
63
163
  {
64
- name,
164
+ name: 'attach_browser',
65
165
  description:
66
- name === 'attach_chrome'
67
- ? 'Backward-compatible alias for attach_browser.'
68
- : 'Connect the provider-authoring tools to a browser. Choose '
69
- + 'dedicated (launch local Chrome), attach (existing local CDP '
70
- + 'port), builder (quota-accounted Popcorn browser with a '
71
- + 'shareable live view), or custom (caller-supplied CDP '
72
- + 'websocket). '
73
- + 'For builder mode, orgId may be omitted: it auto-resolves to '
74
- + 'the account\'s only organization, or — if there is more '
75
- + 'than one — returns needsChoice with the full list so the '
76
- + 'caller can ask the developer which to use and re-call with '
77
- + 'that orgId. '
78
- + 'Builder and custom CDP credentials are held in memory and never '
79
- + 'returned.',
166
+ 'Connect the provider-authoring tools to a browser. ASK THE '
167
+ + 'DEVELOPER which of the four options below they want, then call '
168
+ + 'this ONCE with their chosen `mode` — never choose for them, and '
169
+ + 'never assume a previous session\'s choice still applies. You do '
170
+ + 'not need a discovery call: the options are listed here. '
171
+ + '(Calling with no `mode` returns the same list as structured '
172
+ + '`options`, if you\'d rather relay it verbatim.) '
173
+ + 'When you put the choice to them, INCLUDE each option\'s live-view '
174
+ + 'line: how they watch the browser differs per mode, and nobody '
175
+ + 'discovers share_browser_view on their own. '
176
+ + 'OPTIONS — ' + renderBrowserOptions() + ' '
177
+ + 'Builder and custom CDP credentials are held in memory and are '
178
+ + 'never returned. '
179
+ + 'More: how_it_works({ topic: "browser" }).',
80
180
  inputSchema: {
81
181
  type: 'object',
82
182
  properties: {
@@ -93,21 +193,45 @@ export function attachTool(
93
193
  orgId: { type: 'string', format: 'uuid' },
94
194
  cdpUrl: { type: 'string', maxLength: 4000 },
95
195
  liveViewUrl: { type: 'string', format: 'uri', maxLength: 4000 },
196
+ countryCode: {
197
+ type: 'string',
198
+ pattern: '^[A-Za-z]{2}$',
199
+ description:
200
+ 'Builder mode only. ISO 3166-1 alpha-2 country the remote '
201
+ + 'browser browses from, fixed for the whole session. Omit to '
202
+ + 'get a suggested country back (needsChoice) instead of '
203
+ + 'allocating a browser.',
204
+ },
205
+ skipCountry: {
206
+ type: 'boolean',
207
+ description:
208
+ 'Builder mode only. Allocate without geo-routing, skipping '
209
+ + 'the country prompt. Pass this once the developer has said '
210
+ + 'they don\'t need a specific country.',
211
+ },
96
212
  },
97
213
  },
98
214
  },
99
215
  async(parsed) => {
100
216
  const persisted = state.config.read()?.browser_agent
101
- const mode = parsed.mode ?? parsed.attachMode ?? persisted?.attachMode
217
+ // Deliberately NOT falling back to the persisted mode: which browser
218
+ // to drive is the developer's call every time (a remote one costs
219
+ // quota, a local one exposes their own profile), and silently reusing
220
+ // last session's answer takes that decision away from them. The
221
+ // persisted choice is offered back as a default to suggest, not
222
+ // applied.
223
+ const mode = parsed.mode ?? parsed.attachMode
102
224
  if(!mode) {
103
225
  return {
104
226
  needsChoice: true,
105
227
  message:
106
- 'Choose a browser mode and call attach_browser again: '
107
- + '"dedicated" launches a local dedicated Chrome profile; '
108
- + '"attach" connects to a local Chrome debugging port; '
109
- + '"builder" creates a quota-accounted remote browser and '
110
- + 'requires orgId; "custom" requires a CDP websocket URL.',
228
+ 'Ask the developer which browser to use, then call '
229
+ + 'attach_browser again with their chosen `mode`. Present all '
230
+ + 'the options below — do not pick one for them.',
231
+ options: BROWSER_OPTIONS,
232
+ ...(persisted?.attachMode
233
+ ? { previouslyUsed: persisted.attachMode }
234
+ : {}),
111
235
  configPath: defaultConfigPath(),
112
236
  }
113
237
  }
@@ -146,6 +270,49 @@ export function attachTool(
146
270
  orgId = orgs[0].id
147
271
  }
148
272
 
273
+ // A remote browser's exit country is fixed for the whole session —
274
+ // Popcorn applies it at allocation and it can't be changed later. So
275
+ // confirm it with the developer BEFORE spending a quota-accounted
276
+ // browser on the wrong one, the same way orgId is confirmed above.
277
+ // Geo-routing is OPTIONAL though, so this asks rather than demands:
278
+ // `skipCountry` is the "no particular country" answer, and it must be
279
+ // as reachable as naming one.
280
+ if(mode === 'builder' && !parsed.countryCode && !parsed.skipCountry) {
281
+ const suggestion = await suggestCountryFromIp()
282
+ return {
283
+ needsChoice: true,
284
+ message:
285
+ 'Ask the developer which country this browser should browse '
286
+ + 'from — its egress is routed through that country for the '
287
+ + 'whole session and cannot be changed afterwards. Geolocation '
288
+ + 'is optional; offer all three answers below. Nothing has been '
289
+ + 'allocated yet, so this costs nothing.',
290
+ choices: [
291
+ {
292
+ answer: 'The suggested country',
293
+ howToCall: `attach_browser({ mode: "builder", countryCode: "${
294
+ suggestion.countryCode}" })`,
295
+ },
296
+ {
297
+ answer: 'A specific country the site must be seen from',
298
+ howToCall:
299
+ 'attach_browser({ mode: "builder", countryCode: "<ISO '
300
+ + '3166-1 alpha-2>" })',
301
+ },
302
+ {
303
+ answer: 'No particular country',
304
+ howToCall:
305
+ 'attach_browser({ mode: "builder", skipCountry: true })',
306
+ },
307
+ ],
308
+ suggestedCountryCode: suggestion.countryCode,
309
+ suggestionSource: 'caller-ip',
310
+ ...(suggestion.country ? { country: suggestion.country } : {}),
311
+ ...(suggestion.region ? { region: suggestion.region } : {}),
312
+ ...(suggestion.city ? { city: suggestion.city } : {}),
313
+ }
314
+ }
315
+
149
316
  const next = await connect(
150
317
  mode, { ...parsed, orgId }, persisted, builderClient, dependencies,
151
318
  )
@@ -172,6 +339,7 @@ export function attachTool(
172
339
  ? { browserSessionId: next.browserSessionId }
173
340
  : {}),
174
341
  ...(next.liveViewUrl ? { liveViewUrl: next.liveViewUrl } : {}),
342
+ ...(next.countryCode ? { countryCode: next.countryCode } : {}),
175
343
  }
176
344
  },
177
345
  )
@@ -197,8 +365,10 @@ async function connect(
197
365
  if(mode === 'builder') {
198
366
  assert(builderClient, badRequest('Builder browser service is unavailable'))
199
367
  assert(parsed.orgId, badRequest('orgId is required for builder mode'))
368
+ const countryCode = parsed.countryCode?.trim().toUpperCase()
200
369
  const { data } = await builderClient.call('CreateAgentBrowserSession', {
201
370
  params: { orgId: parsed.orgId },
371
+ body: countryCode ? { countryCode } : {},
202
372
  })
203
373
  let handles: CdpHandles
204
374
  try {
@@ -219,6 +389,8 @@ async function connect(
219
389
  liveViewUrl: data.liveViewUrl,
220
390
  browserSessionId: data.id,
221
391
  orgId: parsed.orgId,
392
+ ...(data.apiUrl ? { apiUrl: data.apiUrl } : {}),
393
+ ...(countryCode ? { countryCode } : {}),
222
394
  disposeRemote: async() => {
223
395
  await builderClient.call('DisposeAgentBrowserSession', {
224
396
  params: {
@@ -19,6 +19,8 @@ import { type AttachState, attachTool } from './attach.ts'
19
19
  import { type CaptureStore, captureTools, resetCaptures } from './capture.ts'
20
20
  import { listTabsTool } from './eval.ts'
21
21
  import { howItWorksTool } from './how-it-works.ts'
22
+ import { type LiveShareRef, liveViewTools } from './live-view.ts'
23
+ import { podProofTools } from './pod-proof.ts'
22
24
  import { createVersionFromCaptureTool } from './publish.ts'
23
25
  import { sessionTools } from './session.ts'
24
26
  import { sharedAuthoringTools } from './shared.ts'
@@ -26,26 +28,31 @@ import { sharedAuthoringTools } from './shared.ts'
26
28
  export interface AgentState {
27
29
  attach: { current?: AttachState, config: ConfigStore }
28
30
  captures: CaptureStore
31
+ /** The one public live view, when `share_browser_view` has opened it. */
32
+ liveShare: LiveShareRef
29
33
  }
30
34
 
31
- /** Exported so a mode (e.g. old-devtools) that supplies its own publish tool
32
- * can build the attach/capture state up front and share the SAME instance
33
- * with `buildAgentTools` — letting that publish tool see the live attach
34
- * state (e.g. to remind the caller about a still-open browser session). */
35
+ /** Exported so a mode (for example, old-devtools) that supplies its own
36
+ * publish tool can build the attach/capture state up front and share the SAME
37
+ * instance with `buildAgentTools` — letting that publish tool see the live
38
+ * attach state (for example, to remind the caller about a still-open browser
39
+ * session). */
35
40
  export function buildAgentState(): AgentState {
36
41
  return {
37
42
  attach: { config: new ConfigStore() },
38
43
  captures: { sessions: new Map() },
44
+ liveShare: {},
39
45
  }
40
46
  }
41
47
 
42
48
  /** Build the flat list of `RegisteredTool` entries, ready for
43
49
  * `registerToolList`.
44
50
  * Pass `client` to include the builder `create_provider_version_from_capture`;
45
- * omit it (e.g. OLD-devtools mode) to register only the backend-independent
46
- * capture/proof tools — old mode supplies its own publish tool.
47
- * Pass `externalState` to share attach/capture state with tools registered
48
- * elsewhere (e.g. old mode's publish tool) instead of building a fresh one. */
51
+ * omit it (for example, OLD-devtools mode) to register only the
52
+ * backend-independent capture/proof tools — old mode supplies its own publish
53
+ * tool. Pass `externalState` to share attach/capture state with tools
54
+ * registered elsewhere (for example, old mode's publish tool) instead of
55
+ * building a fresh one. */
49
56
  export function buildAgentTools(
50
57
  client?: ReclaimClient,
51
58
  builderClient: ReclaimClient | undefined = client,
@@ -58,18 +65,22 @@ export function buildAgentTools(
58
65
  // client. No `client` ⇒ old-devtools mode (see `mcp/start.ts`).
59
66
  tools.push(howItWorksTool(!client))
60
67
  const beforeReplace = () => resetCaptures(state.captures)
61
- tools.push(attachTool(
62
- state.attach, builderClient, 'attach_browser', beforeReplace,
63
- ))
64
- tools.push(attachTool(
65
- state.attach, builderClient, 'attach_chrome', beforeReplace,
66
- ))
68
+ tools.push(attachTool(state.attach, builderClient, beforeReplace))
67
69
  tools.push(listTabsTool(state.attach))
68
70
  for(const t of captureTools(state.attach, state.captures)) {
69
71
  tools.push(t)
70
72
  }
71
73
 
72
- for(const t of sessionTools(state.attach, state.captures)) {
74
+ for(const t of sessionTools(state.attach, state.captures, state.liveShare)) {
75
+ tools.push(t)
76
+ }
77
+
78
+ // Local-CDP only: a builder-mode browser already has a hosted live view.
79
+ for(const t of liveViewTools(state.attach, state.captures, state.liveShare)) {
80
+ tools.push(t)
81
+ }
82
+
83
+ for(const t of podProofTools(state.attach, state.captures)) {
73
84
  tools.push(t)
74
85
  }
75
86
 
@@ -93,7 +93,11 @@ export function captureTools(
93
93
  {
94
94
  name: 'get_request',
95
95
  description:
96
- 'Return full headers + bodies for a specific captured request.',
96
+ 'Return the full headers and bodies of one captured request. Use it '
97
+ + 'to inspect a candidate that find_requests_containing ranked. '
98
+ + 'Unlike every other tool here, the headers are RAW — cookie and '
99
+ + 'authorization values are visible. Read them; never repeat them to '
100
+ + 'the developer or write them to a file.',
97
101
  inputSchema: {
98
102
  type: 'object',
99
103
  properties: {
@@ -10,7 +10,10 @@ export function listTabsTool(attachRef: {
10
10
  return defineTool<Record<string, never>>(
11
11
  {
12
12
  name: 'list_tabs',
13
- description: 'List open Chrome tabs.',
13
+ description:
14
+ 'List the open tabs in the attached browser, with their tabId, title '
15
+ + 'and URL. Call it after attach_browser to pick the tabId to pass to '
16
+ + 'start_capture.',
14
17
  inputSchema: { type: 'object', properties: {} },
15
18
  },
16
19
  async() => {
@@ -22,9 +22,10 @@ export function findTool(resolveBackend: ResolveBackend): RegisteredTool {
22
22
  + 'URL/JSON/HTML-escaped, …). Returns a ranked candidate list with '
23
23
  + 'the matched variant, score, and a short snippet — used to locate '
24
24
  + 'the request that returns a target. Also finds SPLIT matches: when '
25
- + '`value` is multi-word (e.g. a full name), a response that carries '
26
- + 'every word SEPARATELY (a JSON profile API with first/last name in '
27
- + 'distinct fields) is returned with `splitMatch:true` — prove those '
25
+ + '`value` is multi-word (for example, a full name), a response '
26
+ + 'that carries every word SEPARATELY (a JSON profile API with '
27
+ + 'first and last name in distinct fields) is returned with '
28
+ + '`splitMatch:true` — prove those '
28
29
  + 'per-field (a redaction per part) rather than as one string. '
29
30
  + 'Server-side; bodies stay here.',
30
31
  inputSchema: objectSchema(
@@ -77,8 +78,8 @@ export function findTool(resolveBackend: ResolveBackend): RegisteredTool {
77
78
  continue
78
79
  }
79
80
 
80
- // Split match: every word present separately (e.g. first/last name
81
- // in distinct JSON fields). Requires ≥2 tokens so it's specific.
81
+ // Split match: every word present separately (for example, first/last
82
+ // name in distinct JSON fields). Requires ≥2 tokens so it's specific.
82
83
  if(tokens.length >= 2 && tokens.every((t) => body.includes(t))) {
83
84
  ranked.push({
84
85
  ...publicRequest(r),