@reclaimprotocol/agent 0.1.23 → 0.1.25

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 (149) hide show
  1. package/README.md +56 -20
  2. package/lib/authoring/backend.d.ts +5 -4
  3. package/lib/authoring/tools.js +30 -30
  4. package/lib/cdp/capture-engine.js +8 -7
  5. package/lib/cdp/claims.d.ts +21 -0
  6. package/lib/cdp/claims.js +129 -0
  7. package/lib/cdp/container.d.ts +85 -0
  8. package/lib/cdp/container.js +481 -0
  9. package/lib/cdp/geo.d.ts +10 -0
  10. package/lib/cdp/geo.js +36 -0
  11. package/lib/cdp/index.d.ts +1 -0
  12. package/lib/cdp/index.js +1 -0
  13. package/lib/live/cloudflared.d.ts +15 -0
  14. package/lib/live/cloudflared.js +268 -0
  15. package/lib/live/tunnel.d.ts +26 -0
  16. package/lib/live/tunnel.js +160 -0
  17. package/lib/mcp/api/operations.gen.js +34 -26
  18. package/lib/mcp/api/to-zod.d.ts +1 -1
  19. package/lib/mcp/api/to-zod.js +1 -1
  20. package/lib/mcp/api/tools.js +1 -1
  21. package/lib/mcp/instructions.d.ts +15 -2
  22. package/lib/mcp/instructions.js +102 -38
  23. package/lib/mcp/start.js +62 -6
  24. package/lib/mcp/tools/agent/analyze.js +5 -1
  25. package/lib/mcp/tools/agent/attach.d.ts +29 -2
  26. package/lib/mcp/tools/agent/attach.js +310 -24
  27. package/lib/mcp/tools/agent/build.d.ts +18 -8
  28. package/lib/mcp/tools/agent/build.js +30 -12
  29. package/lib/mcp/tools/agent/capture.js +5 -1
  30. package/lib/mcp/tools/agent/eval.js +3 -1
  31. package/lib/mcp/tools/agent/find.js +6 -5
  32. package/lib/mcp/tools/agent/how-it-works.d.ts +15 -7
  33. package/lib/mcp/tools/agent/how-it-works.js +57 -17
  34. package/lib/mcp/tools/agent/index.d.ts +1 -2
  35. package/lib/mcp/tools/agent/index.js +1 -1
  36. package/lib/mcp/tools/agent/live-view.d.ts +17 -0
  37. package/lib/mcp/tools/agent/live-view.js +146 -0
  38. package/lib/mcp/tools/agent/pod-proof.d.ts +8 -0
  39. package/lib/mcp/tools/agent/pod-proof.js +64 -0
  40. package/lib/mcp/tools/agent/propose.js +4 -2
  41. package/lib/mcp/tools/agent/publish.js +47 -77
  42. package/lib/mcp/tools/agent/session.d.ts +16 -1
  43. package/lib/mcp/tools/agent/session.js +32 -7
  44. package/lib/mcp/tools/authenticate/authenticate.js +26 -13
  45. package/lib/mcp/tools/mode/mode.d.ts +1 -1
  46. package/lib/mcp/tools/mode/mode.js +10 -22
  47. package/lib/old/client.d.ts +1 -1
  48. package/lib/old/client.js +1 -1
  49. package/lib/old/instructions.d.ts +9 -1
  50. package/lib/old/instructions.js +4 -6
  51. package/lib/old/login-cdp.d.ts +2 -2
  52. package/lib/old/login-cdp.js +46 -6
  53. package/lib/old/to-register.d.ts +3 -2
  54. package/lib/old/to-register.js +1 -1
  55. package/lib/old/tools/authenticate.js +9 -1
  56. package/lib/old/tools/get-provider-info.js +6 -5
  57. package/lib/old/tools/issue-app-credentials.js +3 -2
  58. package/lib/old/tools/publish.d.ts +1 -1
  59. package/lib/old/tools/publish.js +90 -131
  60. package/lib/old/tools/update-provider-metadata.d.ts +1 -1
  61. package/lib/old/tools/update-provider-metadata.js +1 -1
  62. package/lib/old/tools/verify-sdk-setup.js +1 -1
  63. package/lib/proof/attestor.d.ts +3 -3
  64. package/lib/proof/debug-response-dump.js +4 -4
  65. package/lib/proof/index.d.ts +1 -0
  66. package/lib/proof/index.js +1 -0
  67. package/lib/proof/page-replay.d.ts +1 -1
  68. package/lib/proof/page-replay.js +1 -1
  69. package/lib/proof/pod-prover.d.ts +47 -0
  70. package/lib/proof/pod-prover.js +155 -0
  71. package/lib/provider/dom-xpath.js +11 -9
  72. package/lib/provider/draft.d.ts +10 -7
  73. package/lib/provider/draft.js +11 -9
  74. package/lib/provider/injection-doc.d.ts +23 -1
  75. package/lib/provider/injection-doc.js +34 -21
  76. package/lib/provider/match-redaction-warning.d.ts +2 -2
  77. package/lib/provider/match-redaction-warning.js +3 -3
  78. package/lib/provider/matcher.js +13 -12
  79. package/lib/provider/secrets.d.ts +3 -3
  80. package/lib/provider/secrets.js +3 -3
  81. package/lib/provider/to-version.d.ts +7 -6
  82. package/lib/provider/to-version.js +3 -3
  83. package/lib/tools/registry.d.ts +5 -5
  84. package/lib/tools/registry.js +5 -5
  85. package/package.json +3 -3
  86. package/samples/demo-server/README.md +7 -6
  87. package/skill/SKILL.builder.md +356 -189
  88. package/skill/SKILL.common.md +400 -97
  89. package/skill/SKILL.old.md +340 -155
  90. package/skill/builder-mode.md +60 -17
  91. package/skill/hash-validation.md +170 -0
  92. package/skill/interception.md +236 -0
  93. package/skill/js-sdk-integration.md +13 -7
  94. package/skill/old-mode.md +62 -21
  95. package/skill/user-script.md +61 -0
  96. package/src/authoring/backend.ts +5 -4
  97. package/src/authoring/tools.ts +31 -30
  98. package/src/cdp/capture-engine.ts +8 -7
  99. package/src/cdp/claims.ts +153 -0
  100. package/src/cdp/container.ts +593 -0
  101. package/src/cdp/geo.ts +52 -0
  102. package/src/cdp/index.ts +1 -0
  103. package/src/consts.ts +1 -0
  104. package/src/live/cloudflared.ts +348 -0
  105. package/src/live/tunnel.ts +208 -0
  106. package/src/mcp/api/operations.gen.ts +34 -26
  107. package/src/mcp/api/to-zod.ts +1 -1
  108. package/src/mcp/api/tools.ts +1 -1
  109. package/src/mcp/instructions.ts +122 -48
  110. package/src/mcp/start.ts +69 -5
  111. package/src/mcp/tools/agent/analyze.ts +5 -1
  112. package/src/mcp/tools/agent/attach.ts +367 -26
  113. package/src/mcp/tools/agent/build.ts +35 -16
  114. package/src/mcp/tools/agent/capture.ts +5 -1
  115. package/src/mcp/tools/agent/eval.ts +4 -1
  116. package/src/mcp/tools/agent/find.ts +6 -5
  117. package/src/mcp/tools/agent/how-it-works.ts +62 -21
  118. package/src/mcp/tools/agent/index.ts +6 -2
  119. package/src/mcp/tools/agent/live-view.ts +182 -0
  120. package/src/mcp/tools/agent/pod-proof.ts +90 -0
  121. package/src/mcp/tools/agent/propose.ts +4 -2
  122. package/src/mcp/tools/agent/publish.ts +52 -77
  123. package/src/mcp/tools/agent/session.ts +33 -6
  124. package/src/mcp/tools/authenticate/authenticate.ts +28 -13
  125. package/src/mcp/tools/mode/mode.ts +10 -22
  126. package/src/old/client.ts +2 -2
  127. package/src/old/instructions.ts +4 -10
  128. package/src/old/login-cdp.ts +50 -6
  129. package/src/old/to-register.ts +3 -2
  130. package/src/old/tools/authenticate.ts +9 -1
  131. package/src/old/tools/get-provider-info.ts +6 -5
  132. package/src/old/tools/issue-app-credentials.ts +3 -2
  133. package/src/old/tools/publish.ts +95 -131
  134. package/src/old/tools/update-provider-metadata.ts +1 -1
  135. package/src/old/tools/verify-sdk-setup.ts +1 -1
  136. package/src/proof/attestor.ts +3 -3
  137. package/src/proof/debug-response-dump.ts +4 -4
  138. package/src/proof/index.ts +8 -0
  139. package/src/proof/page-replay.ts +1 -1
  140. package/src/proof/pod-prover.ts +231 -0
  141. package/src/provider/dom-xpath.ts +11 -9
  142. package/src/provider/draft.ts +13 -10
  143. package/src/provider/injection-doc.ts +37 -24
  144. package/src/provider/match-redaction-warning.ts +3 -3
  145. package/src/provider/matcher.ts +13 -12
  146. package/src/provider/secrets.ts +3 -3
  147. package/src/provider/to-version.ts +11 -9
  148. package/src/tools/registry.ts +5 -5
  149. package/skill/injection-api.md +0 -364
@@ -1,36 +1,180 @@
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 { dockerAvailable, dockerMissingMessage, startContainer, } from "../../../cdp/container.js";
5
+ import { suggestCountryFromIp } from "../../../cdp/geo.js";
4
6
  import { defaultProfileDir, launchChrome, waitForCdpReady, } from "../../../cdp/launcher.js";
5
7
  import { defaultConfigPath } from "../../../config/store.js";
6
8
  import { defineTool } from "../../server.js";
9
+ /**
10
+ * Modes with a live view that `share_browser_view` can put behind a public URL.
11
+ *
12
+ * Only `container`: it runs the Reclaim browser runtime, whose viewer already
13
+ * serves on loopback, so sharing is a tunnel in front of it. `builder` gives a
14
+ * hosted `liveViewUrl` instead, and a plain local browser has nothing to
15
+ * show — one on this machine is watched by looking at it.
16
+ */
17
+ export const SHAREABLE_MODES = new Set(['container']);
18
+ /**
19
+ * The browser sources, written once and used twice: rendered into this tool's
20
+ * DESCRIPTION (so an agent can put the choice to the developer straight from
21
+ * the tool list, without a discovery round-trip) and returned verbatim as
22
+ * structured `options` when the tool is called without a `mode` anyway.
23
+ *
24
+ * Being the single source for both is the point — an option list that a model
25
+ * reads in the description but that disagrees with what the tool returns is
26
+ * worse than either alone.
27
+ *
28
+ * Ordered and labelled BY USE CASE, because that is the question a developer
29
+ * can actually answer. "Which CDP transport?" is not; "does anyone else need to
30
+ * get into this browser?" is, and it decides two of the four. `useCase` is the
31
+ * grouping key, so the rendered list reads as two pairs rather than four
32
+ * unrelated transports.
33
+ */
34
+ const BROWSER_OPTIONS = [
35
+ {
36
+ mode: 'dedicated',
37
+ useCase: 'Working on your own machine',
38
+ label: 'Local Chrome, launched for you',
39
+ detail: 'Launches Chrome with a dedicated profile under '
40
+ + '~/.reclaim/chrome-profile. Nothing to set up, and the profile '
41
+ + 'persists, so a site you sign in to stays signed in. The easiest '
42
+ + 'choice when you are doing the work yourself.',
43
+ requires: [],
44
+ liveView: 'None, and none needed — the browser is on your screen.',
45
+ },
46
+ {
47
+ mode: 'attach',
48
+ useCase: 'Working on your own machine',
49
+ label: 'The Chrome you already have open',
50
+ detail: 'Connects to a Chrome you started yourself, keeping the tabs, '
51
+ + 'extensions and logins already in it. That Chrome must have been '
52
+ + 'started with BOTH --remote-debugging-port AND a non-default '
53
+ + '--user-data-dir: since Chrome 136 the debug flag is silently '
54
+ + 'ignored on the default profile.',
55
+ requires: ['port (default 9222)'],
56
+ liveView: 'None, and none needed — the browser is on your screen.',
57
+ },
58
+ {
59
+ mode: 'container',
60
+ useCase: 'Someone else has to sign in, or you need a phone',
61
+ label: 'Reclaim runtime in Docker — free, runs on your machine',
62
+ detail: 'Runs the Reclaim browser runtime (the same image the remote browser '
63
+ + 'uses) as a container on this machine, and gives you a link you can '
64
+ + 'send to whoever holds the account. They open it in any browser, '
65
+ + 'including a phone, and can see and click in it. Free — it is your '
66
+ + 'Docker and your bandwidth. Costs a 432 MB first-run download and '
67
+ + 'about 2 GB of RAM. The image is amd64, so Apple Silicon runs it '
68
+ + 'translated. OrbStack handles that itself; Docker Desktop needs '
69
+ + 'Rosetta enabled.',
70
+ requires: ['Docker running'],
71
+ liveView: 'Built in. This call returns a local `viewerUrl`, and '
72
+ + 'share_browser_view turns that into a public link for the person '
73
+ + 'helping you.',
74
+ },
75
+ {
76
+ mode: 'builder',
77
+ useCase: 'Someone else has to sign in, or you need a phone',
78
+ label: 'Remote browser, hosted by Reclaim — CHARGEABLE',
79
+ detail: 'A browser allocated in Reclaim\'s cloud, billed against the '
80
+ + 'organization\'s quota. Nothing to install, and the shareable link '
81
+ + 'comes back immediately. It is also the only way to browse a site '
82
+ + 'from a SPECIFIC COUNTRY — its whole egress is routed there. '
83
+ + 'Geolocation is optional: omit countryCode to be offered a '
84
+ + 'suggestion, or pass skipCountry to allocate without geo-routing.',
85
+ requires: [
86
+ 'Builder authentication',
87
+ 'orgId (auto-resolved when you have only one)',
88
+ 'quota — this one costs money',
89
+ ],
90
+ liveView: 'Included. This call returns a hosted liveViewUrl to pass on; '
91
+ + 'share_browser_view does not apply.',
92
+ },
93
+ {
94
+ mode: 'custom',
95
+ useCase: 'Advanced',
96
+ label: 'A CDP endpoint you supply',
97
+ detail: 'Connects to any ws:// or wss:// CDP websocket — a browser you run '
98
+ + 'elsewhere, or another tool\'s. The URL is treated as a credential: '
99
+ + 'never logged, persisted, or returned. Only pick this if you were '
100
+ + 'given an endpoint and know why.',
101
+ requires: ['cdpUrl'],
102
+ liveView: 'None, unless you pass your own liveViewUrl alongside the cdpUrl.',
103
+ },
104
+ ];
105
+ /**
106
+ * The options as prose, grouped by use case.
107
+ *
108
+ * The grouping is the useful part: it turns "pick one of five transports" into
109
+ * "answer one question, then pick one of two".
110
+ */
111
+ function renderBrowserOptions() {
112
+ const seen = new Set();
113
+ const lines = [];
114
+ for (const option of BROWSER_OPTIONS) {
115
+ if (!seen.has(option.useCase)) {
116
+ seen.add(option.useCase);
117
+ lines.push(`— ${option.useCase.toUpperCase()} —`);
118
+ }
119
+ const needs = option.requires.length
120
+ ? ` Needs: ${option.requires.join('; ')}.`
121
+ : '';
122
+ lines.push(`(${option.mode}) ${option.label}. ${option.detail}${needs}`
123
+ + ` Live view: ${option.liveView}`);
124
+ }
125
+ return lines.join(' ');
126
+ }
7
127
  const DEFAULT_DEPENDENCIES = {
8
128
  connectLocal: connectCdp,
9
129
  connectRemote: connectCdpUrl,
130
+ dockerAvailable,
131
+ startContainer,
10
132
  };
11
- export function attachTool(state, builderClient, name = 'attach_browser', beforeReplace, dependencies = DEFAULT_DEPENDENCIES) {
133
+ export function attachTool(state, builderClient, beforeReplace,
134
+ // Partial so a caller can stub one thing without restating the rest — the
135
+ // tests only ever care about a couple of these at a time.
136
+ dependencies = {}) {
137
+ const deps = {
138
+ ...DEFAULT_DEPENDENCIES, ...dependencies,
139
+ };
12
140
  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.',
141
+ name: 'attach_browser',
142
+ description: 'Connect the provider-authoring tools to a browser. Never choose '
143
+ + 'for them, never assume a previous session\'s choice still '
144
+ + 'applies, and call this ONCE with the `mode` they pick. '
145
+ + 'ASK THIS FIRST, before listing anything: "Will you be doing this '
146
+ + 'yourself, or does someone else need to sign in — for example the '
147
+ + 'person whose account it is?" That answer decides half the list, '
148
+ + 'and a developer can answer it without knowing what CDP is. '
149
+ + 'THEMSELVES → offer `dedicated` or `attach`. SOMEONE ELSE, or the '
150
+ + 'site only works on a phone → offer `container` or `builder`, '
151
+ + 'which are the only two that produce a link another person can '
152
+ + 'open. Say which of those two costs money: `container` is free '
153
+ + 'but needs Docker, `builder` needs no install but is billed '
154
+ + 'against the org\'s quota. '
155
+ + 'Then relay the matching options with their live-view lines. You '
156
+ + 'do not need a discovery call — the full list is here, and calling '
157
+ + 'with no `mode` returns it as structured `options` if you would '
158
+ + 'rather read it back verbatim. '
159
+ + 'OPTIONS — ' + renderBrowserOptions() + ' '
160
+ + 'A `container` browser returns a local `viewerUrl`; relay it. When '
161
+ + 'someone remote needs in, call share_browser_view and relay the '
162
+ + 'FULL public url verbatim, on its own line. `builder` returns a '
163
+ + 'hosted `liveViewUrl` — relay that the same way. `dedicated`, '
164
+ + '`attach` and `custom` cannot be shared at all: if they ask for a '
165
+ + 'link after picking one, say so and offer to re-attach with '
166
+ + '`container`. '
167
+ + 'Builder and custom CDP credentials are held in memory and are '
168
+ + 'never returned. '
169
+ + 'More: how_it_works({ topic: "browser" }).',
28
170
  inputSchema: {
29
171
  type: 'object',
30
172
  properties: {
31
173
  mode: {
32
174
  type: 'string',
33
- enum: ['dedicated', 'attach', 'builder', 'custom'],
175
+ enum: [
176
+ 'dedicated', 'attach', 'container', 'builder', 'custom',
177
+ ],
34
178
  },
35
179
  attachMode: {
36
180
  type: 'string',
@@ -41,19 +185,54 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
41
185
  orgId: { type: 'string', format: 'uuid' },
42
186
  cdpUrl: { type: 'string', maxLength: 4000 },
43
187
  liveViewUrl: { type: 'string', format: 'uri', maxLength: 4000 },
188
+ viewerPort: {
189
+ type: 'number',
190
+ minimum: 1024,
191
+ maximum: 60000,
192
+ description: '`container` mode only. Loopback port for the built-in live '
193
+ + 'viewer. Change it only to avoid a clash; the CDP port and '
194
+ + 'the container name are derived from it.',
195
+ },
196
+ countryCode: {
197
+ type: 'string',
198
+ pattern: '^[A-Za-z]{2}$',
199
+ description: 'Builder mode only. ISO 3166-1 alpha-2 country the remote '
200
+ + 'browser browses from, fixed for the whole session. Omit to '
201
+ + 'get a suggested country back (needsChoice) instead of '
202
+ + 'allocating a browser.',
203
+ },
204
+ skipCountry: {
205
+ type: 'boolean',
206
+ description: 'Builder mode only. Allocate without geo-routing, skipping '
207
+ + 'the country prompt. Pass this once the developer has said '
208
+ + 'they don\'t need a specific country.',
209
+ },
44
210
  },
45
211
  },
46
212
  }, async (parsed) => {
47
213
  const persisted = state.config.read()?.browser_agent;
48
- const mode = parsed.mode ?? parsed.attachMode ?? persisted?.attachMode;
214
+ // Deliberately NOT falling back to the persisted mode: which browser
215
+ // to drive is the developer's call every time (a remote one costs
216
+ // quota, a local one exposes their own profile), and silently reusing
217
+ // last session's answer takes that decision away from them. The
218
+ // persisted choice is offered back as a default to suggest, not
219
+ // applied.
220
+ const mode = parsed.mode ?? parsed.attachMode;
49
221
  if (!mode) {
50
222
  return {
51
223
  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.',
224
+ message: 'Ask FIRST whether they will be doing this themselves or '
225
+ + 'someone else has to sign in — that answer decides half the '
226
+ + 'list. Themselves: `dedicated` or `attach`. Someone else, or '
227
+ + 'a phone-only site: `container` (free, needs Docker) or '
228
+ + '`builder` (no install, chargeable) — the only two that give '
229
+ + 'you a link to send. Then present the matching options and '
230
+ + 'call attach_browser again with the `mode` they pick. Do not '
231
+ + 'pick one for them.',
232
+ options: BROWSER_OPTIONS,
233
+ ...(persisted?.attachMode
234
+ ? { previouslyUsed: persisted.attachMode }
235
+ : {}),
57
236
  configPath: defaultConfigPath(),
58
237
  };
59
238
  }
@@ -80,7 +259,45 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
80
259
  }
81
260
  orgId = orgs[0].id;
82
261
  }
83
- const next = await connect(mode, { ...parsed, orgId }, persisted, builderClient, dependencies);
262
+ // A remote browser's exit country is fixed for the whole session —
263
+ // Popcorn applies it at allocation and it can't be changed later. So
264
+ // confirm it with the developer BEFORE spending a quota-accounted
265
+ // browser on the wrong one, the same way orgId is confirmed above.
266
+ // Geo-routing is OPTIONAL though, so this asks rather than demands:
267
+ // `skipCountry` is the "no particular country" answer, and it must be
268
+ // as reachable as naming one.
269
+ if (mode === 'builder' && !parsed.countryCode && !parsed.skipCountry) {
270
+ const suggestion = await suggestCountryFromIp();
271
+ return {
272
+ needsChoice: true,
273
+ message: 'Ask the developer which country this browser should browse '
274
+ + 'from — its egress is routed through that country for the '
275
+ + 'whole session and cannot be changed afterwards. Geolocation '
276
+ + 'is optional; offer all three answers below. Nothing has been '
277
+ + 'allocated yet, so this costs nothing.',
278
+ choices: [
279
+ {
280
+ answer: 'The suggested country',
281
+ howToCall: `attach_browser({ mode: "builder", countryCode: "${suggestion.countryCode}" })`,
282
+ },
283
+ {
284
+ answer: 'A specific country the site must be seen from',
285
+ howToCall: 'attach_browser({ mode: "builder", countryCode: "<ISO '
286
+ + '3166-1 alpha-2>" })',
287
+ },
288
+ {
289
+ answer: 'No particular country',
290
+ howToCall: 'attach_browser({ mode: "builder", skipCountry: true })',
291
+ },
292
+ ],
293
+ suggestedCountryCode: suggestion.countryCode,
294
+ suggestionSource: 'caller-ip',
295
+ ...(suggestion.country ? { country: suggestion.country } : {}),
296
+ ...(suggestion.region ? { region: suggestion.region } : {}),
297
+ ...(suggestion.city ? { city: suggestion.city } : {}),
298
+ };
299
+ }
300
+ const next = await connect(mode, { ...parsed, orgId }, persisted, builderClient, deps);
84
301
  try {
85
302
  await beforeReplace?.();
86
303
  await disposeAttached(state);
@@ -95,6 +312,11 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
95
312
  browser_agent: { attachMode: mode, port: next.port },
96
313
  });
97
314
  }
315
+ // How the developer watches this browser differs per mode, and none of
316
+ // it is guessable, so every mode says its own piece here.
317
+ const localView = mode === 'container';
318
+ const hostedView = Boolean(next.liveViewUrl) && !localView;
319
+ const canShare = SHAREABLE_MODES.has(mode);
98
320
  return {
99
321
  attached: true,
100
322
  mode,
@@ -103,6 +325,38 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
103
325
  ? { browserSessionId: next.browserSessionId }
104
326
  : {}),
105
327
  ...(next.liveViewUrl ? { liveViewUrl: next.liveViewUrl } : {}),
328
+ ...(next.countryCode ? { countryCode: next.countryCode } : {}),
329
+ ...(next.containerName ? { containerName: next.containerName } : {}),
330
+ ...(canShare ? { liveViewAvailable: true } : {}),
331
+ _notes: [
332
+ ...(next.imagePulled
333
+ ? ['The browser runtime image was downloaded just now (about '
334
+ + '432 MB, about 1.3 GB on disk). Mention it — later attaches '
335
+ + 'reuse it. RECLAIM_BROWSER_RUNTIME_IMAGE points at a '
336
+ + 'different or locally built image.']
337
+ : []),
338
+ ...(localView
339
+ ? ['This browser has its own live view, already running on this '
340
+ + 'machine: relay `liveViewUrl` to the developer verbatim, on '
341
+ + 'its own line, so they can watch and drive it. It is '
342
+ + 'loopback-only, so only they can reach it. If someone ELSE '
343
+ + 'needs to — a login only they can complete — call '
344
+ + 'share_browser_view to put that same viewer behind a public '
345
+ + 'URL.']
346
+ : []),
347
+ ...(hostedView
348
+ ? ['This browser has a hosted live view already. Relay '
349
+ + '`liveViewUrl` to the developer verbatim, on its own line, '
350
+ + 'so they can watch the run — share_browser_view does not '
351
+ + 'apply here.']
352
+ : []),
353
+ ...(!localView && !hostedView
354
+ ? ['This browser has no live view: it runs on the developer\'s '
355
+ + 'own machine, so they watch it by looking at it. If someone '
356
+ + 'ELSE needs to see or drive it, say that `container` mode '
357
+ + 'runs a browser with a shareable live view built in.']
358
+ : []),
359
+ ],
106
360
  };
107
361
  });
108
362
  }
@@ -118,8 +372,10 @@ async function connect(mode, parsed, persisted, builderClient, dependencies) {
118
372
  if (mode === 'builder') {
119
373
  assert(builderClient, badRequest('Builder browser service is unavailable'));
120
374
  assert(parsed.orgId, badRequest('orgId is required for builder mode'));
375
+ const countryCode = parsed.countryCode?.trim().toUpperCase();
121
376
  const { data } = await builderClient.call('CreateAgentBrowserSession', {
122
377
  params: { orgId: parsed.orgId },
378
+ body: countryCode ? { countryCode } : {},
123
379
  });
124
380
  let handles;
125
381
  try {
@@ -140,6 +396,8 @@ async function connect(mode, parsed, persisted, builderClient, dependencies) {
140
396
  liveViewUrl: data.liveViewUrl,
141
397
  browserSessionId: data.id,
142
398
  orgId: parsed.orgId,
399
+ ...(data.apiUrl ? { apiUrl: data.apiUrl } : {}),
400
+ ...(countryCode ? { countryCode } : {}),
143
401
  disposeRemote: async () => {
144
402
  await builderClient.call('DisposeAgentBrowserSession', {
145
403
  params: {
@@ -160,6 +418,34 @@ async function connect(mode, parsed, persisted, builderClient, dependencies) {
160
418
  ...(parsed.liveViewUrl ? { liveViewUrl: parsed.liveViewUrl } : {}),
161
419
  };
162
420
  }
421
+ if (mode === 'container') {
422
+ assert(await dependencies.dockerAvailable(), badRequest(dockerMissingMessage()));
423
+ // Ports are allocated per session unless the caller names one. A fixed
424
+ // default would give two concurrent sessions the SAME container — the
425
+ // name derives from the viewer port — so session two would silently
426
+ // drive session one's browser.
427
+ const container = await dependencies.startContainer({
428
+ ...(parsed.viewerPort ? { viewerPort: parsed.viewerPort } : {}),
429
+ image: process.env.RECLAIM_BROWSER_RUNTIME_IMAGE?.trim() || undefined,
430
+ });
431
+ try {
432
+ return {
433
+ handles: await dependencies.connectRemote(container.cdpUrl),
434
+ mode,
435
+ liveViewUrl: container.viewerUrl,
436
+ containerName: container.name,
437
+ viewerPort: container.viewerPort,
438
+ imagePulled: container.pulled,
439
+ disposeRemote: container.stop,
440
+ };
441
+ }
442
+ catch (err) {
443
+ // Never leave a 1.3 GB container running for a browser we could not
444
+ // talk to.
445
+ await container.stop().catch(() => { });
446
+ throw err;
447
+ }
448
+ }
163
449
  const port = parsed.port ?? persisted?.port ?? 9222;
164
450
  if (mode === 'dedicated') {
165
451
  try {
@@ -14,23 +14,33 @@ 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[];
42
+ /**
43
+ * Tear down an agent state on the way out. Safe to call more than once, and
44
+ * safe when nothing was ever attached.
45
+ */
46
+ export declare function disposeAgentState(state: AgentState): Promise<void>;
@@ -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
- import { sessionTools } from "./session.js";
22
+ import { resetAll, 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.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));
@@ -65,3 +76,10 @@ export function buildAgentTools(client, builderClient = client, externalState) {
65
76
  }
66
77
  return tools;
67
78
  }
79
+ /**
80
+ * Tear down an agent state on the way out. Safe to call more than once, and
81
+ * safe when nothing was ever attached.
82
+ */
83
+ export async function disposeAgentState(state) {
84
+ await resetAll(state.attach, state.captures, state.liveShare);
85
+ }
@@ -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),
@@ -1,11 +1,19 @@
1
1
  import { type RegisteredTool } from '../../server.ts';
2
2
  /**
3
- * Serves the full provider-authoring guide on demand. The MCP server's
4
- * always-on `instructions` are a terse tool-flow reference; this tool returns
5
- * the deep version (workflow, decision frameworks, gotchas, "never do" rules)
6
- * so any MCP client — not just Claude Code, which alone auto-loads skills — can
7
- * pull it. `oldMode` selects the backend-specific guide so the agent is never
8
- * told to call auth/publish tools that aren't registered in its mode.
9
- * Stateless: callable before `attach_browser`.
3
+ * Serves the provider-authoring guide on demand, ONE TOPIC AT A TIME. The
4
+ * server's always-on `instructions` are a terse tool-flow reference; this tool
5
+ * is the deep version, so any MCP client — not just Claude Code, which alone
6
+ * auto-loads skills — can pull it.
7
+ *
8
+ * Topic-scoped rather than all-at-once because the full guide is ~7k tokens:
9
+ * paying that to answer "how do I mark a header secret?" crowds out the actual
10
+ * task, which is exactly what makes an agent slow and prone to inventing
11
+ * details. Every response carries the topic index, so a caller that guessed
12
+ * wrong (or called with no topic at all) always learns what else is available
13
+ * without a second round-trip.
14
+ *
15
+ * `oldMode` selects the backend-specific guide, so the agent is never told to
16
+ * call auth/publish tools that aren't registered in its mode. Stateless:
17
+ * callable before `attach_browser`.
10
18
  */
11
19
  export declare function howItWorksTool(oldMode: boolean): RegisteredTool;