@reclaimprotocol/agent 0.1.24 → 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 (46) hide show
  1. package/README.md +3 -2
  2. package/lib/cdp/claims.d.ts +21 -0
  3. package/lib/cdp/claims.js +129 -0
  4. package/lib/cdp/client.d.ts +0 -10
  5. package/lib/cdp/container.d.ts +85 -0
  6. package/lib/cdp/container.js +481 -0
  7. package/lib/live/tunnel.d.ts +6 -3
  8. package/lib/live/tunnel.js +57 -17
  9. package/lib/mcp/instructions.js +2 -1
  10. package/lib/mcp/start.js +58 -3
  11. package/lib/mcp/tools/agent/attach.d.ts +22 -2
  12. package/lib/mcp/tools/agent/attach.js +197 -55
  13. package/lib/mcp/tools/agent/build.d.ts +5 -0
  14. package/lib/mcp/tools/agent/build.js +9 -2
  15. package/lib/mcp/tools/agent/index.d.ts +1 -2
  16. package/lib/mcp/tools/agent/index.js +1 -1
  17. package/lib/mcp/tools/agent/live-view.d.ts +3 -7
  18. package/lib/mcp/tools/agent/live-view.js +74 -81
  19. package/lib/mcp/tools/agent/session.d.ts +14 -0
  20. package/lib/mcp/tools/agent/session.js +14 -8
  21. package/package.json +2 -2
  22. package/skill/SKILL.common.md +115 -38
  23. package/skill/builder-mode.md +9 -7
  24. package/skill/interception.md +3 -2
  25. package/skill/old-mode.md +10 -8
  26. package/src/cdp/claims.ts +153 -0
  27. package/src/cdp/client.ts +0 -8
  28. package/src/cdp/container.ts +593 -0
  29. package/src/live/tunnel.ts +71 -20
  30. package/src/mcp/instructions.ts +2 -1
  31. package/src/mcp/start.ts +65 -2
  32. package/src/mcp/tools/agent/attach.ts +229 -60
  33. package/src/mcp/tools/agent/build.ts +10 -2
  34. package/src/mcp/tools/agent/index.ts +6 -2
  35. package/src/mcp/tools/agent/live-view.ts +77 -93
  36. package/src/mcp/tools/agent/session.ts +14 -8
  37. package/lib/cdp/browser-proxy.d.ts +0 -78
  38. package/lib/cdp/browser-proxy.js +0 -186
  39. package/lib/live/page.d.ts +0 -20
  40. package/lib/live/page.js +0 -134
  41. package/lib/live/viewer.d.ts +0 -28
  42. package/lib/live/viewer.js +0 -232
  43. package/lib/mcp/tools/agent/proxy.d.ts +0 -3
  44. package/lib/mcp/tools/agent/proxy.js +0 -199
  45. package/src/live/page.ts +0 -145
  46. package/src/live/viewer.ts +0 -293
@@ -16,12 +16,9 @@
16
16
  import { badRequest, conflict } from '@hapi/boom';
17
17
  import assert from 'node:assert';
18
18
  import { startTunnel } from "../../../live/tunnel.js";
19
- import { startLiveView } from "../../../live/viewer.js";
20
19
  import { LOGGER } from "../../../logger.js";
21
20
  import { defineTool } from "../../server.js";
22
- /** Modes whose browser we reach over our own CDP connection, and which
23
- * therefore have no hosted live view of their own. */
24
- const SHAREABLE_MODES = new Set(['dedicated', 'attach', 'custom']);
21
+ import { SHAREABLE_MODES } from "./attach.js";
25
22
  /** Tear down whatever share is running. Safe when none is. Exported so the
26
23
  * session tools can call it from `dispose_browser` / `reset_session`. */
27
24
  export async function stopLiveShare(ref) {
@@ -30,98 +27,87 @@ export async function stopLiveShare(ref) {
30
27
  return false;
31
28
  }
32
29
  ref.current = undefined;
33
- await share.view.stop().catch((err) => {
34
- LOGGER.warn({ err }, 'live view: server stop failed');
35
- });
30
+ // Only the tunnel is ours to close. The browser and its viewer belong to the
31
+ // container, which keeps running until the browser is disposed.
36
32
  await share.tunnel.stop().catch((err) => {
37
33
  LOGGER.warn({ err }, 'live view: tunnel stop failed');
38
34
  });
39
35
  return true;
40
36
  }
41
- export function liveViewTools(attachRef, captures, shareRef) {
37
+ export function liveViewTools(attachRef, shareRef) {
42
38
  return [
43
39
  defineTool({
44
40
  name: 'share_browser_view',
45
- description: 'Put the attached tab behind a PUBLIC HTTPS URL (a cloudflared '
46
- + 'quick tunnel) so someone elsewhere can watch it live, and by '
47
- + 'default click and type in it. Use it to hand a login or a 2FA '
48
- + 'step to the person who holds the credentials, or to let a '
49
- + 'colleague watch a capture. Returns the URL — relay it to the '
50
- + 'developer VERBATIM, on its own line. '
51
- + 'ONLY for a locally attached browser (attach_browser mode '
52
- + '`dedicated`, `attach`, or `custom`). In `builder` mode the '
53
- + 'Popcorn browser already has a hosted live view: use the '
54
- + '`liveViewUrl` that attach_browser returned instead. '
55
- + 'THE URL IS A CREDENTIAL: anyone holding it can see this tab, '
56
- + 'and with the default `interactive: true` can act in it as the '
57
- + 'signed-in user. Share it only with the person who needs it, and '
58
- + 'call stop_browser_view the moment they are done. Pass '
59
- + '`interactive: false` for a watch-only link. '
41
+ description: 'Put the attached browser\'s live view behind a PUBLIC HTTPS URL (a '
42
+ + 'cloudflared quick tunnel) so someone elsewhere can watch it and '
43
+ + 'act in it. Use it to hand a login or a 2FA step to the person '
44
+ + 'who holds the credentials, or to let a colleague watch a '
45
+ + 'capture. Returns the URL — relay it to the developer VERBATIM, '
46
+ + 'on its own line. '
47
+ + 'ONLY for `container` mode, whose browser runs its own viewer. '
48
+ + 'A `builder` browser already has a hosted live view: use the '
49
+ + '`liveViewUrl` attach_browser returned. A local browser '
50
+ + '(`dedicated`, `attach`, `custom`) cannot be shared at all — no '
51
+ + 'tunnel can fix that, because there is no viewer behind it. Tell '
52
+ + 'them so, and offer to re-attach: `container` is free and needs '
53
+ + 'Docker, `builder` needs no install but is chargeable. '
54
+ + 'THE URL IS THE ONLY CREDENTIAL: the random hostname is the whole '
55
+ + 'secret, with no token or login behind it, so anyone who learns '
56
+ + 'it can see and drive this browser as the signed-in user. Share '
57
+ + 'it only with the person who needs it, and call stop_browser_view '
58
+ + 'the moment they are done. '
60
59
  + 'Needs cloudflared: an existing install on PATH is used as-is, '
61
60
  + 'otherwise it is downloaded once to ~/.reclaim/bin on first '
62
61
  + 'share (never at install time). Set RECLAIM_CLOUDFLARED_PATH to '
63
62
  + 'point at your own copy instead.',
64
- inputSchema: {
65
- type: 'object',
66
- properties: {
67
- tabId: {
68
- type: 'string',
69
- description: 'Tab to stream, from list_tabs. Defaults to the tab the '
70
- + 'active capture is on, else the first tab.',
71
- },
72
- interactive: {
73
- type: 'boolean',
74
- description: 'Let the viewer click and type in the page. Default true. '
75
- + 'Set false to stream a read-only view.',
76
- },
77
- },
78
- },
79
- }, async ({ tabId, interactive }) => {
63
+ inputSchema: { type: 'object', properties: {} },
64
+ }, async () => {
80
65
  const attached = attachRef.current;
81
66
  assert(attached, badRequest('No browser attached. Call attach_browser first.'));
82
67
  assert(SHAREABLE_MODES.has(attached.mode), badRequest(`share_browser_view does not apply to \`${attached.mode}\` mode. `
83
- + 'That browser already has a hosted live view — use the '
84
- + '`liveViewUrl` from attach_browser'
85
- + (attached.liveViewUrl ? `: ${attached.liveViewUrl}` : '')
68
+ + (attached.liveViewUrl
69
+ ? 'That browser already has a hosted live view — use the '
70
+ + `\`liveViewUrl\` from attach_browser: ${attached.liveViewUrl}`
71
+ : 'That browser has no live view to put behind a URL. To let '
72
+ + 'someone else in, re-attach with mode "container" (the '
73
+ + 'Reclaim runtime in Docker — free, needs Docker) or '
74
+ + '"builder" (hosted, no install, chargeable)')
86
75
  + '.'));
87
76
  assert(!shareRef.current, conflict('A live view is already running at '
88
77
  + `${shareRef.current?.url}. Call stop_browser_view first, or `
89
78
  + 'reuse that URL.'));
90
- const tabs = tabId || captures.activeTab
91
- ? []
92
- : await attached.handles.listTabs();
93
- const target = tabId ?? captures.activeTab ?? tabs[0]?.tabId;
94
- assert(target, badRequest('No tab to stream. Open a tab, or call list_tabs to pick one.'));
95
- const tab = await attached.handles.attachToTab(target);
96
- const wantsInput = interactive !== false;
97
- const view = await startLiveView({
98
- tab,
99
- interactive: wantsInput,
100
- title: `Reclaim — ${attached.mode} browser`,
101
- });
102
- let tunnel;
103
- try {
104
- tunnel = await startTunnel(view.port);
105
- }
106
- catch (err) {
107
- // Don't leave the loopback server behind if the tunnel failed.
108
- await view.stop().catch(() => { });
109
- throw err;
110
- }
111
- const url = `${tunnel.url}${view.path}`;
79
+ // The container publishes its viewer on loopback and its full CDP
80
+ // port beside it. Only the VIEWER is ever tunnelled: the CDP port is
81
+ // unauthenticated, total control of the browser, and putting it on a
82
+ // public URL would hand that to anyone with the link.
83
+ const viewerPort = attached.viewerPort;
84
+ assert(viewerPort, badRequest('This browser reports no live-view port. Re-attach with mode '
85
+ + '"container".'));
86
+ const tunnel = await startTunnel(viewerPort);
87
+ // The path is the runtime's own viewer page; `magnify=1` is what makes
88
+ // it lay out for the device that opens it.
89
+ const url = `${tunnel.url}/liveview.html?magnify=1`;
112
90
  const share = {
113
- view, tunnel, url, interactive: wantsInput, startedAt: Date.now(),
91
+ tunnel, url,
114
92
  };
115
93
  shareRef.current = share;
116
- LOGGER.info({ mode: attached.mode, interactive: wantsInput }, 'live view shared');
94
+ LOGGER.info({ mode: attached.mode }, 'live view shared');
117
95
  return {
118
96
  url,
119
- interactive: wantsInput,
120
- tabId: target,
121
97
  ...(share.tunnel.binary.source === 'downloaded'
122
98
  ? { cloudflaredInstalled: true }
123
99
  : {}),
124
- ...(share.tunnel.ready ? {} : { propagating: true }),
100
+ ...(share.tunnel.ready
101
+ ? {}
102
+ : {
103
+ propagating: true,
104
+ // cloudflared's own last words. Without them, "the URL does
105
+ // not connect" is unactionable: the tunnel, the local server
106
+ // and this machine's egress all look identical from outside.
107
+ ...(share.tunnel.diagnostics
108
+ ? { cloudflaredLog: share.tunnel.diagnostics }
109
+ : {}),
110
+ }),
125
111
  _notes: [
126
112
  ...(share.tunnel.binary.source === 'downloaded'
127
113
  ? ['cloudflared was not installed, so it was downloaded once '
@@ -129,24 +115,31 @@ export function liveViewTools(attachRef, captures, shareRef) {
129
115
  : []),
130
116
  ...(share.tunnel.ready
131
117
  ? []
132
- : ['The tunnel had not started routing yet — if the URL '
133
- + 'errors, wait a few seconds and reload.']),
118
+ : ['The tunnel never confirmed a connection to Cloudflare, so '
119
+ + 'the URL may not resolve yet. Give it ~10s and reload '
120
+ + 'first. If it still fails, say so and offer to retry '
121
+ + '(stop_browser_view then share_browser_view) — a quick '
122
+ + 'tunnel does sometimes fail to register. '
123
+ + '`cloudflaredLog` has its output if it keeps happening.']),
134
124
  'Relay `url` to the developer verbatim, on its own line.',
135
- 'The URL is a credential: it needs no login, and anyone with '
136
- + 'it can see this tab'
137
- + (wantsInput ? ' and act in it as the signed-in user.' : '.'),
138
- 'Call stop_browser_view when they are done — the tunnel stays '
139
- + 'open until you do.',
125
+ 'The URL is a credential: it needs no login, and anyone with it '
126
+ + 'can see this browser and act in it as the signed-in user.',
127
+ 'It shows the whole browser, so a sign-in that opens a separate '
128
+ + 'window is visible too — unlike a single-tab stream.',
129
+ 'Opening it on a phone lays the page out for that phone, with '
130
+ + 'touch, pinch-zoom and the soft keyboard.',
131
+ 'Call stop_browser_view when they are done. That closes the '
132
+ + 'tunnel only — the browser keeps running.',
140
133
  ],
141
134
  };
142
135
  }),
143
136
  defineTool({
144
137
  name: 'stop_browser_view',
145
- description: 'Close the public live view opened by share_browser_view: stops '
146
- + 'the screen stream and shuts the tunnel down, so the URL stops '
147
- + 'resolving. Call it as soon as the person you shared with is '
148
- + 'finished. The browser itself stays attached and the capture '
149
- + 'keeps running.',
138
+ description: 'Close the public URL opened by share_browser_view: shuts the '
139
+ + 'tunnel down, so the URL stops resolving for whoever still has '
140
+ + 'it. Call it as soon as the person you shared with is finished. '
141
+ + 'Nothing else changes — the browser stays attached, its own live '
142
+ + 'view keeps serving locally, and any capture keeps running.',
150
143
  inputSchema: { type: 'object', properties: {} },
151
144
  }, async () => ({ stopped: await stopLiveShare(shareRef) })),
152
145
  ];
@@ -7,3 +7,17 @@ export declare function sessionTools(attachRef: {
7
7
  current?: AttachState;
8
8
  config: ConfigStore;
9
9
  }, captures: CaptureStore, liveShare: LiveShareRef): RegisteredTool[];
10
+ /**
11
+ * Release everything a session holds: the public tunnel, the captures, and the
12
+ * browser itself.
13
+ *
14
+ * Exported because a session can end WITHOUT anyone calling `dispose_browser` —
15
+ * the developer closes their agent, the client kills the server, the transport
16
+ * drops. Nothing runs on its own then, so `start.ts` runs this. Without it a
17
+ * container keeps running and, worse, a public URL to a live browser outlives
18
+ * the session that created it.
19
+ */
20
+ export declare function resetAll(attachRef: {
21
+ current?: AttachState;
22
+ config: ConfigStore;
23
+ }, captures: CaptureStore, liveShare: LiveShareRef): Promise<void>;
@@ -24,11 +24,7 @@ export function sessionTools(attachRef, captures, liveShare) {
24
24
  // URL can recover it instead of opening a second tunnel.
25
25
  ...(liveShare.current
26
26
  ? {
27
- sharedView: {
28
- url: liveShare.current.url,
29
- interactive: liveShare.current.interactive,
30
- viewers: liveShare.current.view.viewers(),
31
- },
27
+ sharedView: { url: liveShare.current.url },
32
28
  }
33
29
  : {}),
34
30
  })),
@@ -55,9 +51,19 @@ export function sessionTools(attachRef, captures, liveShare) {
55
51
  }),
56
52
  ];
57
53
  }
58
- async function resetAll(attachRef, captures, liveShare) {
59
- // Before the captures: the live view holds a tab session of its own, and a
60
- // public URL must never outlive the browser it points at.
54
+ /**
55
+ * Release everything a session holds: the public tunnel, the captures, and the
56
+ * browser itself.
57
+ *
58
+ * Exported because a session can end WITHOUT anyone calling `dispose_browser` —
59
+ * the developer closes their agent, the client kills the server, the transport
60
+ * drops. Nothing runs on its own then, so `start.ts` runs this. Without it a
61
+ * container keeps running and, worse, a public URL to a live browser outlives
62
+ * the session that created it.
63
+ */
64
+ export async function resetAll(attachRef, captures, liveShare) {
65
+ // Before the captures: a public URL must never outlive the browser it points
66
+ // at.
61
67
  await stopLiveShare(liveShare);
62
68
  await resetCaptures(captures);
63
69
  await disposeAttached(attachRef);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reclaimprotocol/agent",
3
3
  "title": "Reclaim Agent",
4
- "version": "0.1.24",
4
+ "version": "0.1.25",
5
5
  "description": "Reclaim Protocol tools for AI coding agents. Reclaim lets your app verify data points about its users, such as a follower count, an account balance, or an order history. This package is the Reclaim MCP server: it gives your agent the tools to find, build, test, and fix data verification providers by capturing and replaying browser requests, drafting provider recipes, running zkTLS proofs, and publishing provider versions.",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -83,7 +83,7 @@
83
83
  "@modelcontextprotocol/sdk": "^1.29.0",
84
84
  "@noble/hashes": "^2.2.0",
85
85
  "@reclaimprotocol/attestor-core": "^5.0.8",
86
- "@reclaimprotocol/client": "0.1.24",
86
+ "@reclaimprotocol/client": "0.1.25",
87
87
  "@reclaimprotocol/tls": "^0.1.3",
88
88
  "chrome-remote-interface": "^0.33.0",
89
89
  "domhandler": "^6.0.0",
@@ -22,24 +22,45 @@ already names it.
22
22
  <!-- section: browser -->
23
23
  ## Choosing a browser
24
24
 
25
- **Ask the developer which browser they want, then call `attach_browser` ONCE
26
- with their chosen `mode`.** Never pick for them, and never assume last session's
27
- choice still applies. You don't need a discovery call — the four options are
28
- spelled out in the tool's own description (and summarized below). Calling with
29
- no `mode` returns the same list as `needsChoice: true` + `options`, if you'd
30
- rather relay it verbatim.
31
-
32
- | mode | what it is | needs | live view |
25
+ - Choosing one — ask who signs in, then offer the matching pair
26
+ - Sharing the container's live view with someone remote
27
+ - What happens when the session ends
28
+ - When the container will not start
29
+
30
+ **Ask one question first, before you list anything:**
31
+
32
+ > Will you be doing this yourself, or does someone else need to sign in — the
33
+ > person whose account it is, say?
34
+
35
+ That answer decides half the list, and a developer can answer it without knowing
36
+ what CDP is. Only then offer the matching pair, call `attach_browser` ONCE with
37
+ the `mode` they pick, and never pick for them or assume last session's choice
38
+ still applies.
39
+
40
+ **They're doing it themselves** — no link, no Docker, nothing to install:
41
+
42
+ | mode | what it is | needs |
43
+ | --- | --- | --- |
44
+ | `dedicated` | Chrome the agent launches with its own profile (`~/.reclaim/chrome-profile`), which persists, so a site stays signed in. The default choice. | nothing |
45
+ | `attach` | The Chrome they already have open, with its tabs, extensions and logins. | `port` (default 9222) |
46
+
47
+ Neither can be shared, and neither needs to be: the browser is on their screen.
48
+
49
+ **Someone else has to sign in, or the site only works on a phone** — these two
50
+ are the only modes that produce a link another person can open:
51
+
52
+ | mode | what it is | needs | costs |
33
53
  | --- | --- | --- | --- |
34
- | `dedicated` | Local Chrome the agent launches with its own profile (`~/.reclaim/chrome-profile`). Easiest local path. | nothing | `share_browser_view` on request |
35
- | `attach` | A Chrome the developer is already running. | `port` (default 9222) | `share_browser_view` on request |
36
- | `builder` | Quota-accounted remote Popcorn browser. Only way to browse from a **specific country**. | Builder auth, `orgId` | hosted `liveViewUrl`, returned by `attach_browser` |
37
- | `custom` | Any CDP websocket the developer supplies. | `cdpUrl` | `share_browser_view` on request |
54
+ | `container` | The Reclaim runtime in Docker, on their machine. `share_browser_view` turns its viewer into a public link. | Docker running | free — their Docker, their bandwidth. 432 MB the first time, about 2 GB of RAM, 7 to 17 seconds to start |
55
+ | `builder` | A browser in Reclaim's cloud. The link comes back immediately, with nothing to install. Also the only way to browse from a **specific country**. | Builder auth, `orgId` | **chargeable** — billed against the org's quota |
38
56
 
39
- **Say the live-view column out loud when you offer the choice.** How the
40
- developer watches the browser is part of picking one, and a local browser looks
41
- like it can't be shared at all unless you tell them `share_browser_view`
42
- exists.
57
+ **Say which one costs money.** They are otherwise interchangeable from the
58
+ developer's point of view, so price and "do you have Docker" are the whole
59
+ decision. Do not quietly pick the billed one.
60
+
61
+ `custom` (any `ws://`/`wss://` CDP endpoint they supply) exists for someone who
62
+ was handed an endpoint and knows why. Don't offer it unprompted; it cannot be
63
+ shared either.
43
64
 
44
65
  **`attach` gotcha (Chrome 136+, May 2025):** the developer's Chrome must be
45
66
  started with BOTH `--remote-debugging-port=9222` AND
@@ -72,31 +93,87 @@ default profile. If `attach` fails, say this verbatim and offer `dedicated`.
72
93
  **`custom` specifics:** the `cdpUrl` is a credential — never repeat, log, or
73
94
  persist it. If a `liveViewUrl` comes back, share it full and verbatim.
74
95
 
75
- ### Sharing a local browser (`share_browser_view`)
76
-
77
- A `dedicated`, `attach`, or `custom` browser runs on the developer's machine,
78
- so only they can see it. `share_browser_view` puts the tab behind a public
79
- HTTPS URL through a cloudflared quick tunnel — use it when the person who has
80
- to act is not at that machine: a login or 2FA only they can complete, or a
81
- colleague watching a capture.
82
-
83
- - **`builder` mode already has one.** The Popcorn browser returns a hosted
84
- `liveViewUrl` from `attach_browser`; share that instead. The tool refuses
85
- here rather than opening a second tunnel.
86
- - **The URL is a credential.** It carries an unguessable token and needs no
87
- login, so whoever holds it can see the tab — and, by default, click and type
88
- in it as the signed-in user. Give it to one person, and say plainly what it
89
- grants.
90
- - **Return it verbatim, on its own line**, exactly as with `liveViewUrl`.
91
- - `interactive: false` streams a watch-only view. Prefer it when the developer
92
- only needs to see progress.
93
- - **Call `stop_browser_view` as soon as they're done.** The tunnel stays open
94
- until you do. `dispose_browser` and `reset_session` also close it.
96
+ ### Sharing the container's live view (`share_browser_view`)
97
+
98
+ A `container` browser serves its own viewer on loopback, so the developer can
99
+ open it the moment `attach_browser` returns. `share_browser_view` puts that same
100
+ viewer behind a public HTTPS URL through a cloudflared quick tunnel — for when
101
+ the person who has to act is not at that machine: a login or 2FA only they can
102
+ complete, or a colleague watching a capture.
103
+
104
+ **Offer it whenever a login is coming and the developer is not the one holding
105
+ the credentials.** Nobody asks for a feature they have no way of knowing exists.
106
+ Ask plainly:
107
+
108
+ > Do you want a link to this browser? You can open it yourself, or send it to
109
+ > whoever holds the login — they can watch and click in it, including from a
110
+ > phone.
111
+
112
+ If they say yes, call `share_browser_view` and **relay the full URL verbatim, on
113
+ its own line.**
114
+
115
+ - **`container` is the only mode this applies to.** `builder` returns a hosted
116
+ `liveViewUrl` from `attach_browser` — relay that instead, full and verbatim.
117
+ `dedicated`, `attach`, and `custom` have no view to share at all; if someone
118
+ else needs to see the browser, say so and offer `container`.
119
+ - **The URL is the ONLY credential.** The hostname Cloudflare hands out is the
120
+ whole secret: there is no token and no login behind it, so whoever learns the
121
+ URL has the browser, as the signed-in user. Give it to one person, say plainly
122
+ what it grants, and stop the share when they are done rather than leaving it
123
+ open. Tell them the watcher sees everything typed into that browser, including
124
+ a password — so if the point is for the DEVELOPER to log in, they should do it
125
+ before you share, not after.
126
+ - **It shows the whole browser, not one tab.** A sign-in that opens a separate
127
+ window is visible too.
128
+ - **Opening it on a phone lays the page out for that phone**, with touch,
129
+ pinch-zoom and the soft keyboard. That is the reason to reach for `container`
130
+ when a site only works on mobile.
131
+ - **Call `stop_browser_view` as soon as they're done.** That closes the tunnel
132
+ only — the browser keeps running. `dispose_browser` and `reset_session` stop
133
+ the container itself.
134
+ - **If the URL will not load**, the tunnel may not have registered with
135
+ Cloudflare — the hostname exists before the route does. Have them reload after
136
+ 10 seconds; if it still fails, `stop_browser_view` then `share_browser_view`
137
+ again, since a quick tunnel does occasionally fail to come up. A
138
+ `propagating: true` in the result is that case, and `cloudflaredLog` carries
139
+ the reason.
95
140
  - **cloudflared is handled for you.** An existing install on PATH wins;
96
- otherwise it's fetched once into `~/.reclaim/bin` on the first share, never
97
- at install time. Say so when it happens — it's a ~37 MB download onto their
141
+ otherwise it's fetched once into `~/.reclaim/bin` on the first share, never at
142
+ install time. Say so when it happens — it's a 37 MB download onto their
98
143
  machine. `RECLAIM_CLOUDFLARED_PATH` points at their own copy instead.
99
144
 
145
+ ### How containers are shared, and when they close
146
+
147
+ - **Closing the agent releases the browser.** The server stops the container and
148
+ the tunnel when its connection drops, so a public URL never outlives the
149
+ session that made it. `dispose_browser` does the same on demand.
150
+ - **Re-attaching in one session reuses the same browser** — under a second,
151
+ rather than the twenty a fresh one costs — so whatever it is signed into is
152
+ still there.
153
+ - **A second session working at the same time gets its own container.** They
154
+ cannot share one: the live view streams the whole screen, so two sessions in
155
+ one container would see each other's browsing, and two browsers on one profile
156
+ corrupt the cookie jar they share. Each concurrent session therefore has its
157
+ own profile, and starts out signed in to nothing.
158
+ - **Four at once is the ceiling**, because each is about 2 GB of RAM. Past that,
159
+ `attach_browser` says so and points at `dedicated`.
160
+ - **A crash cannot strand one for long.** A killed session runs no cleanup, so
161
+ the next `container` attach removes any container whose owning process is gone.
162
+ One belonging to a session that is still alive is never touched.
163
+
164
+ ### When the container will not start
165
+
166
+ - **Docker is not running.** The error says so and names the other browser
167
+ modes. Docker is not something you can install for them.
168
+ - **Apple Silicon.** The image is amd64 and cannot be built for arm64, so it
169
+ runs translated. OrbStack does this itself; Docker Desktop needs "Use Rosetta
170
+ for x86/amd64 emulation" turned on. Under QEMU instead of Rosetta, Chromium
171
+ crashes on launch.
172
+ - **First run looks stuck.** It is downloading 432 MB. Say that rather than
173
+ letting it look hung.
174
+ - A container left over from a crashed run is **reused if it is still serving**,
175
+ and replaced if it is not — a second attach does not need a different port.
176
+
100
177
  **When a local attach errors or `waitForCdpReady` reports a dead port**, walk
101
178
  these before retrying:
102
179
 
@@ -18,15 +18,17 @@ device-pairing `code` + `attachUrl` — give the user the URL, then poll
18
18
  `authenticate({ code })` until LINKED. Tokens cache ~2 days.
19
19
  (→ `how_it_works({ topic: "auth" })`)
20
20
 
21
- **Browser choice is the developer's, and so is watching it.** `attach_browser`
22
- advertises all four sources in its own description — relay them, including each
23
- one's live-view line. A `builder` browser returns a hosted `liveViewUrl`; a
24
- local or custom CDP browser is private until you call `share_browser_view`,
25
- which tunnels a public URL with cloudflared. Say that option exists: nobody
26
- guesses it.
21
+ **Ask who is doing the signing in, then offer a browser.** "Yourself, or does
22
+ someone else need to log in?" decides half of `attach_browser`'s list. Doing it
23
+ themselves → `dedicated` or `attach`, both local, neither shareable. Someone
24
+ else, or a phone-only site → `container` (the Reclaim runtime in Docker, free,
25
+ shareable with `share_browser_view`) or `builder` (hosted, no install,
26
+ **chargeable**). Say which costs money, and say `container` exists at all:
27
+ nobody guesses it.
27
28
 
28
29
  **The flow:** `attach_browser` (ask the developer which browser source — local
29
- Chrome, a CDP URL they supply, or a remote Builder browser) → `start_capture`
30
+ Chrome, the Reclaim runtime in Docker, a remote Builder browser, or a CDP URL
31
+ they supply) → `start_capture`
30
32
  ⇒ **`captureId`** (the handle every browser/inspect tool takes first) → drive
31
33
  the page (`navigate`, `wait_for_page`, `eval_in_page`, `get_cookies`) → inspect
32
34
  (`list_requests`, `get_request`, `find_requests_containing`) →
@@ -96,7 +96,7 @@ to detect and nothing to disable — and `builder` never delivers intercepted
96
96
  traffic to the page at all (topic `user-script`). A verification on those that
97
97
  won't load or won't sign in is almost always a CAPTCHA or bot challenge
98
98
  instead: check `analyze_request_constraints` for a `botChallenge` concern, and
99
- treat it as a site that needs a human through the live view rather than a
99
+ treat it as a site that needs a human through a live view rather than a
100
100
  provider to fix.
101
101
 
102
102
  ### Then bisect, cheapest change first
@@ -232,4 +232,5 @@ oversight.
232
232
  `window.Reclaim` is inert on Builder's own client.
233
233
  - `hash-validation` — declaring script-fired requests without breaking
234
234
  `verifyProof({ providerId })`.
235
- - `browser` — live view, for handing a stuck sign-in to a human.
235
+ - `browser` — the modes with a live view (`container`, `builder`), for handing
236
+ a stuck sign-in to a human.
package/skill/old-mode.md CHANGED
@@ -12,16 +12,18 @@ topic at a time — call `how_it_works({})` for the topic index. Topics:
12
12
  `user-script`, `interception`, `hash-validation`, `credentials`, `never-do`,
13
13
  `troubleshooting`.
14
14
 
15
- **Browser choice is the developer's, and so is watching it.** `attach_browser`
16
- advertises all four sources in its own description — relay them, including each
17
- one's live-view line. A `builder` browser returns a hosted `liveViewUrl`; a
18
- local or custom CDP browser is private until you call `share_browser_view`,
19
- which tunnels a public URL with cloudflared. Say that option exists: nobody
20
- guesses it.
15
+ **Ask who is doing the signing in, then offer a browser.** "Yourself, or does
16
+ someone else need to log in?" decides half of `attach_browser`'s list. Doing it
17
+ themselves → `dedicated` or `attach`, both local, neither shareable. Someone
18
+ else, or a phone-only site → `container` (the Reclaim runtime in Docker, free,
19
+ shareable with `share_browser_view`) or `builder` (hosted, no install,
20
+ **chargeable**). Say which costs money, and say `container` exists at all:
21
+ nobody guesses it.
21
22
 
22
23
  **The flow:** `reclaim_authenticate` once → `attach_browser` (ask the developer
23
- which browser source — local Chrome, a CDP URL they supply, or a remote Builder
24
- browser) → `start_capture` ⇒ **`captureId`** → drive the page (`navigate`,
24
+ which browser source — local Chrome, the Reclaim runtime in Docker, a remote
25
+ Builder browser, or a CDP URL they supply) → `start_capture` ⇒ **`captureId`** →
26
+ drive the page (`navigate`,
25
27
  `wait_for_page`, `eval_in_page`) → inspect (`list_requests`, `get_request`,
26
28
  `find_requests_containing`) → `propose_provider` ⇒ **`draftId`** →
27
29
  `replay_request(captureId, draftId)` → `run_proof(captureId, draftId,