@reclaimprotocol/agent 0.1.18 → 0.1.21

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 (168) hide show
  1. package/README.md +6 -2
  2. package/lib/authoring/backend.d.ts +4 -0
  3. package/lib/authoring/tools.js +4 -1
  4. package/lib/cdp/capture.d.ts +2 -2
  5. package/lib/cdp/capture.js +25 -21
  6. package/lib/cdp/client.d.ts +39 -1
  7. package/lib/cdp/client.js +125 -1
  8. package/lib/mcp/api/operations.gen.js +33 -9
  9. package/lib/mcp/instructions.js +27 -3
  10. package/lib/mcp/remap_errors.js +2 -1
  11. package/lib/mcp/start.js +16 -9
  12. package/lib/mcp/tools/agent/attach.d.ts +18 -4
  13. package/lib/mcp/tools/agent/attach.js +142 -52
  14. package/lib/mcp/tools/agent/build.d.ts +9 -2
  15. package/lib/mcp/tools/agent/build.js +15 -7
  16. package/lib/mcp/tools/agent/capture.d.ts +1 -0
  17. package/lib/mcp/tools/agent/capture.js +8 -1
  18. package/lib/mcp/tools/agent/eval.js +2 -3
  19. package/lib/mcp/tools/agent/how-it-works.d.ts +1 -1
  20. package/lib/mcp/tools/agent/how-it-works.js +1 -1
  21. package/lib/mcp/tools/agent/index.d.ts +1 -1
  22. package/lib/mcp/tools/agent/index.js +1 -1
  23. package/lib/mcp/tools/agent/publish.d.ts +4 -1
  24. package/lib/mcp/tools/agent/publish.js +42 -3
  25. package/lib/mcp/tools/agent/session.d.ts +2 -2
  26. package/lib/mcp/tools/agent/session.js +21 -5
  27. package/lib/mcp/tools/agent/shared.js +4 -6
  28. package/lib/mcp/tools/authenticate/authenticate.d.ts +1 -1
  29. package/lib/mcp/tools/authenticate/authenticate.js +13 -6
  30. package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -0
  31. package/lib/mcp/tools/authenticate/builder-access.js +31 -0
  32. package/lib/mcp/tools/authenticate/index.d.ts +1 -0
  33. package/lib/mcp/tools/authenticate/index.js +1 -0
  34. package/lib/old/login-cdp.js +11 -3
  35. package/lib/old/tools/build.d.ts +8 -1
  36. package/lib/old/tools/build.js +6 -2
  37. package/lib/old/tools/publish.d.ts +10 -1
  38. package/lib/old/tools/publish.js +49 -3
  39. package/lib/provider/draft.d.ts +20 -0
  40. package/lib/provider/draft.js +75 -51
  41. package/lib/provider/index.d.ts +1 -1
  42. package/lib/provider/index.js +1 -1
  43. package/lib/provider/secrets.d.ts +37 -0
  44. package/lib/provider/secrets.js +147 -6
  45. package/lib/provider/to-version.d.ts +7 -0
  46. package/lib/provider/to-version.js +11 -1
  47. package/package.json +2 -2
  48. package/skill/SKILL.builder.md +44 -94
  49. package/skill/SKILL.common.md +102 -0
  50. package/skill/SKILL.old.md +21 -73
  51. package/skill/builder-mode.md +2 -2
  52. package/skill/injection-api.md +95 -10
  53. package/skill/old-mode.md +2 -2
  54. package/src/authoring/backend.ts +4 -0
  55. package/src/authoring/tools.ts +4 -1
  56. package/src/cdp/capture.ts +24 -24
  57. package/src/cdp/client.ts +197 -2
  58. package/src/mcp/api/operations.gen.ts +33 -9
  59. package/src/mcp/instructions.ts +36 -3
  60. package/src/mcp/remap_errors.ts +2 -1
  61. package/src/mcp/start.ts +24 -9
  62. package/src/mcp/tools/agent/attach.ts +198 -57
  63. package/src/mcp/tools/agent/build.ts +20 -6
  64. package/src/mcp/tools/agent/capture.ts +10 -1
  65. package/src/mcp/tools/agent/eval.ts +2 -3
  66. package/src/mcp/tools/agent/how-it-works.ts +1 -1
  67. package/src/mcp/tools/agent/index.ts +1 -1
  68. package/src/mcp/tools/agent/publish.ts +46 -2
  69. package/src/mcp/tools/agent/session.ts +29 -8
  70. package/src/mcp/tools/agent/shared.ts +5 -10
  71. package/src/mcp/tools/authenticate/authenticate.ts +17 -9
  72. package/src/mcp/tools/authenticate/builder-access.ts +42 -0
  73. package/src/mcp/tools/authenticate/index.ts +4 -0
  74. package/src/old/login-cdp.ts +13 -3
  75. package/src/old/tools/build.ts +7 -1
  76. package/src/old/tools/publish.ts +55 -4
  77. package/src/provider/draft.ts +98 -56
  78. package/src/provider/index.ts +9 -1
  79. package/src/provider/secrets.ts +171 -6
  80. package/src/provider/to-version.ts +12 -1
  81. package/lib/authoring/find.d.ts +0 -3
  82. package/lib/authoring/find.js +0 -54
  83. package/lib/authoring/propose.d.ts +0 -3
  84. package/lib/authoring/propose.js +0 -109
  85. package/lib/authoring/replay.d.ts +0 -10
  86. package/lib/authoring/replay.js +0 -34
  87. package/lib/cli/args.d.ts +0 -14
  88. package/lib/cli/args.js +0 -19
  89. package/lib/cli/commands.d.ts +0 -1
  90. package/lib/cli/commands.js +0 -19
  91. package/lib/cli.d.ts +0 -1
  92. package/lib/cli.js +0 -7
  93. package/lib/mcp/credentials/authenticate.d.ts +0 -3
  94. package/lib/mcp/credentials/authenticate.js +0 -108
  95. package/lib/mcp/credentials/build.d.ts +0 -5
  96. package/lib/mcp/credentials/build.js +0 -18
  97. package/lib/mcp/credentials/cache.d.ts +0 -70
  98. package/lib/mcp/credentials/cache.js +0 -131
  99. package/lib/mcp/credentials/env-file.d.ts +0 -25
  100. package/lib/mcp/credentials/env-file.js +0 -71
  101. package/lib/mcp/credentials/import.d.ts +0 -2
  102. package/lib/mcp/credentials/import.js +0 -52
  103. package/lib/mcp/credentials/issue.d.ts +0 -2
  104. package/lib/mcp/credentials/issue.js +0 -84
  105. package/lib/mcp/credentials/keypair.d.ts +0 -17
  106. package/lib/mcp/credentials/keypair.js +0 -48
  107. package/lib/mcp/credentials/link.d.ts +0 -3
  108. package/lib/mcp/credentials/link.js +0 -107
  109. package/lib/mcp/credentials/list-cached.d.ts +0 -2
  110. package/lib/mcp/credentials/list-cached.js +0 -55
  111. package/lib/mcp/credentials/owner-key.d.ts +0 -23
  112. package/lib/mcp/credentials/owner-key.js +0 -58
  113. package/lib/mcp/credentials/paths.d.ts +0 -12
  114. package/lib/mcp/credentials/paths.js +0 -35
  115. package/lib/mcp/credentials/poll.d.ts +0 -27
  116. package/lib/mcp/credentials/poll.js +0 -61
  117. package/lib/mcp/credentials/register.d.ts +0 -18
  118. package/lib/mcp/credentials/register.js +0 -30
  119. package/lib/mcp/credentials/resolve-owner-key.d.ts +0 -12
  120. package/lib/mcp/credentials/resolve-owner-key.js +0 -68
  121. package/lib/mcp/credentials/session-cache.d.ts +0 -14
  122. package/lib/mcp/credentials/session-cache.js +0 -42
  123. package/lib/mcp/credentials/status.d.ts +0 -3
  124. package/lib/mcp/credentials/status.js +0 -76
  125. package/lib/mcp/register.d.ts +0 -38
  126. package/lib/mcp/register.js +0 -80
  127. package/lib/mcp/tools/analyze.d.ts +0 -3
  128. package/lib/mcp/tools/analyze.js +0 -26
  129. package/lib/mcp/tools/attach.d.ts +0 -11
  130. package/lib/mcp/tools/attach.js +0 -72
  131. package/lib/mcp/tools/capture.d.ts +0 -10
  132. package/lib/mcp/tools/capture.js +0 -142
  133. package/lib/mcp/tools/eval.d.ts +0 -5
  134. package/lib/mcp/tools/eval.js +0 -127
  135. package/lib/mcp/tools/find.d.ts +0 -3
  136. package/lib/mcp/tools/find.js +0 -60
  137. package/lib/mcp/tools/owner-key.d.ts +0 -2
  138. package/lib/mcp/tools/owner-key.js +0 -63
  139. package/lib/mcp/tools/proof.d.ts +0 -3
  140. package/lib/mcp/tools/proof.js +0 -214
  141. package/lib/mcp/tools/propose.d.ts +0 -9
  142. package/lib/mcp/tools/propose.js +0 -75
  143. package/lib/mcp/tools/publish.d.ts +0 -3
  144. package/lib/mcp/tools/publish.js +0 -59
  145. package/lib/mcp/tools/replay.d.ts +0 -3
  146. package/lib/mcp/tools/replay.js +0 -46
  147. package/lib/mcp/tools/session.d.ts +0 -10
  148. package/lib/mcp/tools/session.js +0 -29
  149. package/lib/mcp/tools/verification/diagnose.d.ts +0 -11
  150. package/lib/mcp/tools/verification/diagnose.js +0 -132
  151. package/lib/mcp/verifications/build.d.ts +0 -5
  152. package/lib/mcp/verifications/build.js +0 -14
  153. package/lib/mcp/verifications/create.d.ts +0 -3
  154. package/lib/mcp/verifications/create.js +0 -57
  155. package/lib/mcp/verifications/find.d.ts +0 -3
  156. package/lib/mcp/verifications/find.js +0 -37
  157. package/lib/mcp/verifications/get.d.ts +0 -3
  158. package/lib/mcp/verifications/get.js +0 -26
  159. package/lib/mcp/verifications/update.d.ts +0 -3
  160. package/lib/mcp/verifications/update.js +0 -78
  161. package/lib/old/tools/get-version-config.d.ts +0 -17
  162. package/lib/old/tools/get-version-config.js +0 -47
  163. package/lib/routes.gen.d.ts +0 -271
  164. package/lib/routes.gen.js +0 -73
  165. package/lib/types/openapi.d.ts +0 -4
  166. package/lib/types/openapi.gen.d.ts +0 -3967
  167. package/lib/types/openapi.gen.js +0 -5
  168. package/lib/types/openapi.js +0 -1
package/src/mcp/start.ts CHANGED
@@ -19,7 +19,10 @@ import { buildOldBackendTools } from '../old/tools/build.ts'
19
19
  import { NAME, VERSION } from '../pkg.ts'
20
20
  import { registerTools } from './api/tools.ts'
21
21
  import {
22
+ buildAgentState,
22
23
  buildAgentTools,
24
+ buildBuilderAuthenticationTools,
25
+ buildBuilderOrganizationTools,
23
26
  buildCredentialTools,
24
27
  buildLocalCredentialTools,
25
28
  buildModeTools,
@@ -49,7 +52,13 @@ export async function startMcpServer() {
49
52
  // deployments — set RECLAIM_AGENT_DISABLED=1 to register only the API
50
53
  // surface.
51
54
  const agentToolsEnabled = !agentDisabled()
52
-
55
+ const builderClient = new ReclaimClient({
56
+ baseUrl: apiUrl(),
57
+ // Builder access is orthogonal to publishing mode. A legacy-devtools
58
+ // session can authenticate here solely to allocate a remote browser.
59
+ token: apiToken() || readSession()?.token,
60
+ headers: { 'User-Agent': `${NAME}/${VERSION}` },
61
+ })
53
62
  // Backend-mode toggle — available in both modes and regardless of whether
54
63
  // the agent tools are enabled, so a user can always switch either direction.
55
64
  registerToolList(server, buildModeTools(config, oldMode))
@@ -65,21 +74,27 @@ export async function startMcpServer() {
65
74
  identity: resolveInitialIdentity(),
66
75
  })
67
76
  const sdkClient = new ReclaimSdkClient({ baseUrl: sdkApiUrl() })
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).
79
+ const agentState = buildAgentState()
68
80
 
81
+ registerToolList(server, buildBuilderAuthenticationTools(builderClient))
82
+ registerToolList(server, buildBuilderOrganizationTools(builderClient))
69
83
  registerToolList(server, buildLocalCredentialTools())
70
- registerToolList(server, buildOldBackendTools(oldClient, sdkClient))
84
+ registerToolList(
85
+ server,
86
+ buildOldBackendTools(oldClient, sdkClient, agentState.attach),
87
+ )
71
88
  if(agentToolsEnabled) {
72
89
  // No builder client → no builder publish tool (old mode has its own).
73
- registerToolList(server, buildAgentTools())
90
+ registerToolList(
91
+ server,
92
+ buildAgentTools(undefined, builderClient, agentState),
93
+ )
74
94
  }
75
95
  // NOTE: registerTools() is intentionally NOT called — generated tools off.
76
96
  } else {
77
- const client = new ReclaimClient({
78
- baseUrl: apiUrl(),
79
- // Priority: explicit env var → cached session token → unauthenticated
80
- token: apiToken() || readSession()?.token,
81
- headers: { 'User-Agent': `${NAME}/${VERSION}` },
82
- })
97
+ const client = builderClient
83
98
 
84
99
  // Tool names must stay disjoint across these three groups: hand-written
85
100
  // tools are higher-level orchestrations (multi-call flows, ReclaimProvider
@@ -1,4 +1,11 @@
1
- import { type CdpHandles, connectCdp } from '../../../cdp/client.ts'
1
+ import { badRequest } from '@hapi/boom'
2
+ import type { ReclaimClient } from '@reclaimprotocol/client'
3
+ import assert from 'node:assert'
4
+ import {
5
+ type CdpHandles,
6
+ connectCdp,
7
+ connectCdpUrl,
8
+ } from '../../../cdp/client.ts'
2
9
  import {
3
10
  defaultProfileDir,
4
11
  launchChrome,
@@ -8,88 +15,222 @@ import type { ConfigStore } from '../../../config/store.ts'
8
15
  import { defaultConfigPath } from '../../../config/store.ts'
9
16
  import { defineTool, type RegisteredTool } from '../../server.ts'
10
17
 
18
+ export type BrowserMode = 'dedicated' | 'attach' | 'builder' | 'custom'
19
+
11
20
  export interface AttachState {
12
21
  handles: CdpHandles
13
- port: number
22
+ mode: BrowserMode
23
+ port?: number
24
+ liveViewUrl?: string
25
+ browserSessionId?: string
26
+ orgId?: string
27
+ disposeRemote?: () => Promise<void>
14
28
  }
15
29
 
16
- export function attachTool(state: {
30
+ export interface AttachRef {
17
31
  current?: AttachState
18
32
  config: ConfigStore
19
- }): RegisteredTool {
20
- return defineTool<{ attachMode?: 'dedicated' | 'attach', port?: number }>(
33
+ }
34
+
35
+ interface AttachArgs {
36
+ mode?: BrowserMode
37
+ /** Backward-compatible local-mode spelling. */
38
+ attachMode?: 'dedicated' | 'attach'
39
+ port?: number
40
+ orgId?: string
41
+ cdpUrl?: string
42
+ liveViewUrl?: string
43
+ }
44
+
45
+ interface AttachDependencies {
46
+ connectLocal: typeof connectCdp
47
+ connectRemote: typeof connectCdpUrl
48
+ }
49
+
50
+ const DEFAULT_DEPENDENCIES: AttachDependencies = {
51
+ connectLocal: connectCdp,
52
+ connectRemote: connectCdpUrl,
53
+ }
54
+
55
+ export function attachTool(
56
+ state: AttachRef,
57
+ builderClient?: ReclaimClient,
58
+ name: 'attach_browser' | 'attach_chrome' = 'attach_browser',
59
+ beforeReplace?: () => Promise<void>,
60
+ dependencies: AttachDependencies = DEFAULT_DEPENDENCIES,
61
+ ): RegisteredTool {
62
+ return defineTool<AttachArgs>(
21
63
  {
22
- name: 'attach_chrome',
64
+ name,
23
65
  description:
24
- 'Attach to a Chrome instance via CDP. ' +
25
- 'Auto-launches a dedicated-profile Chrome ' +
26
- 'on first call if no attachMode is persisted. ' +
27
- 'Persists the chosen mode in ~/.reclaim/config.json.',
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
+ + 'Builder and custom CDP credentials are held in memory and never '
74
+ + 'returned.',
28
75
  inputSchema: {
29
76
  type: 'object',
30
77
  properties: {
31
- attachMode: { type: 'string', enum: ['dedicated', 'attach'] },
32
- port: { type: 'number' },
78
+ mode: {
79
+ type: 'string',
80
+ enum: ['dedicated', 'attach', 'builder', 'custom'],
81
+ },
82
+ attachMode: {
83
+ type: 'string',
84
+ enum: ['dedicated', 'attach'],
85
+ description: 'Deprecated local-mode alias for mode.',
86
+ },
87
+ port: { type: 'number', minimum: 1, maximum: 65535 },
88
+ orgId: { type: 'string', format: 'uuid' },
89
+ cdpUrl: { type: 'string', maxLength: 4000 },
90
+ liveViewUrl: { type: 'string', format: 'uri', maxLength: 4000 },
33
91
  },
34
92
  },
35
93
  },
36
94
  async(parsed) => {
37
- let cfg = state.config.read()?.browser_agent
38
- if(parsed.attachMode || parsed.port) {
39
- cfg = {
40
- attachMode: parsed.attachMode ?? cfg?.attachMode ?? 'dedicated',
41
- port: parsed.port ?? cfg?.port ?? 9222,
42
- }
43
- state.config.write({ browser_agent: cfg })
44
- }
45
-
46
- if(!cfg) {
95
+ const persisted = state.config.read()?.browser_agent
96
+ const mode = parsed.mode ?? parsed.attachMode ?? persisted?.attachMode
97
+ if(!mode) {
47
98
  return {
48
99
  needsChoice: true,
49
100
  message:
50
- 'First-run: choose attach mode. ' +
51
- 'Call attach_chrome again with attachMode="dedicated" ' +
52
- '(auto-launch a Chrome profile under ~/.reclaim/chrome-profile) ' +
53
- 'or attachMode="attach" ' +
54
- '(connect to a Chrome you launched yourself with ' +
55
- 'BOTH --remote-debugging-port=9222 AND ' +
56
- '--user-data-dir=<some-non-default-path>; ' +
57
- 'since Chrome 136 the debug-port flag is silently ' +
58
- 'ignored if the default profile is in use).',
101
+ 'Choose a browser mode and call attach_browser again: '
102
+ + '"dedicated" launches a local dedicated Chrome profile; '
103
+ + '"attach" connects to a local Chrome debugging port; '
104
+ + '"builder" creates a quota-accounted remote browser and '
105
+ + 'requires orgId; "custom" requires a CDP websocket URL.',
59
106
  configPath: defaultConfigPath(),
60
107
  }
61
108
  }
62
109
 
63
- if(cfg.attachMode === 'dedicated') {
64
- try {
65
- await waitForCdpReady(cfg.port, 500)
66
- } catch{
67
- launchChrome({ port: cfg.port, profileDir: defaultProfileDir() })
68
- await waitForCdpReady(cfg.port, 10_000)
69
- }
70
- } else {
71
- await waitForCdpReady(cfg.port, 2_000).catch(() => {
72
- throw new Error(
73
- `attach mode: nothing answering CDP on port ${cfg.port}. ` +
74
- 'Chrome must be launched with BOTH ' +
75
- `--remote-debugging-port=${cfg.port} AND ` +
76
- '--user-data-dir=<some-non-default-path>. ' +
77
- 'Since Chrome 136 (May 2025), the debug-port flag is ' +
78
- 'silently ignored when the default ' +
79
- "user profile is in use — that's why nothing opens the port. " +
80
- 'Either re-launch Chrome with ' +
81
- 'a custom --user-data-dir, or switch to attachMode="dedicated".',
82
- )
83
- })
110
+ const next = await connect(
111
+ mode, parsed, persisted, builderClient, dependencies,
112
+ )
113
+ try {
114
+ await beforeReplace?.()
115
+ await disposeAttached(state)
116
+ } catch(err) {
117
+ await disposeState(next).catch(() => {})
118
+ throw err
84
119
  }
85
120
 
86
- if(state.current) {
87
- await state.current.handles.close().catch(() => {})
121
+ state.current = next
122
+ if(mode === 'dedicated' || mode === 'attach') {
123
+ state.config.write({
124
+ browser_agent: { attachMode: mode, port: next.port! },
125
+ })
88
126
  }
89
127
 
90
- const handles = await connectCdp(cfg.port)
91
- state.current = { handles, port: cfg.port }
92
- return { attached: true, port: cfg.port, mode: cfg.attachMode }
128
+ return {
129
+ attached: true,
130
+ mode,
131
+ ...(next.port ? { port: next.port } : {}),
132
+ ...(next.browserSessionId
133
+ ? { browserSessionId: next.browserSessionId }
134
+ : {}),
135
+ ...(next.liveViewUrl ? { liveViewUrl: next.liveViewUrl } : {}),
136
+ }
93
137
  },
94
138
  )
95
139
  }
140
+
141
+ export async function disposeAttached(state: AttachRef) {
142
+ const current = state.current
143
+ if(!current) {
144
+ return
145
+ }
146
+
147
+ delete state.current
148
+ await disposeState(current)
149
+ }
150
+
151
+ async function connect(
152
+ mode: BrowserMode,
153
+ parsed: AttachArgs,
154
+ persisted: { attachMode: 'dedicated' | 'attach', port: number } | undefined,
155
+ builderClient: ReclaimClient | undefined,
156
+ dependencies: AttachDependencies,
157
+ ): Promise<AttachState> {
158
+ if(mode === 'builder') {
159
+ assert(builderClient, badRequest('Builder browser service is unavailable'))
160
+ assert(parsed.orgId, badRequest('orgId is required for builder mode'))
161
+ const { data } = await builderClient.call('CreateAgentBrowserSession', {
162
+ params: { orgId: parsed.orgId },
163
+ })
164
+ let handles: CdpHandles
165
+ try {
166
+ handles = await dependencies.connectRemote(data.cdpUrl)
167
+ } catch(err) {
168
+ await builderClient.call('DisposeAgentBrowserSession', {
169
+ params: {
170
+ orgId: parsed.orgId,
171
+ browserSessionId: data.id,
172
+ },
173
+ }).catch(() => {})
174
+ throw err
175
+ }
176
+
177
+ return {
178
+ handles,
179
+ mode,
180
+ liveViewUrl: data.liveViewUrl,
181
+ browserSessionId: data.id,
182
+ orgId: parsed.orgId,
183
+ disposeRemote: async() => {
184
+ await builderClient.call('DisposeAgentBrowserSession', {
185
+ params: {
186
+ orgId: parsed.orgId!,
187
+ browserSessionId: data.id,
188
+ },
189
+ })
190
+ },
191
+ }
192
+ }
193
+
194
+ if(mode === 'custom') {
195
+ assert(parsed.cdpUrl, badRequest('cdpUrl is required for custom mode'))
196
+ const url = new URL(parsed.cdpUrl)
197
+ assert(
198
+ url.protocol === 'ws:' || url.protocol === 'wss:',
199
+ badRequest('cdpUrl must use ws:// or wss://'),
200
+ )
201
+ return {
202
+ handles: await dependencies.connectRemote(parsed.cdpUrl),
203
+ mode,
204
+ ...(parsed.liveViewUrl ? { liveViewUrl: parsed.liveViewUrl } : {}),
205
+ }
206
+ }
207
+
208
+ const port = parsed.port ?? persisted?.port ?? 9222
209
+ if(mode === 'dedicated') {
210
+ try {
211
+ await waitForCdpReady(port, 500)
212
+ } catch{
213
+ launchChrome({ port, profileDir: defaultProfileDir() })
214
+ await waitForCdpReady(port, 10_000)
215
+ }
216
+ } else {
217
+ await waitForCdpReady(port, 2_000).catch(() => {
218
+ throw new Error(
219
+ `attach mode: nothing answering CDP on port ${port}. Chrome must `
220
+ + `be launched with --remote-debugging-port=${port} AND `
221
+ + '--user-data-dir=<some-non-default-path>. Chrome 136+ ignores '
222
+ + 'the debugging flag for the default profile.',
223
+ )
224
+ })
225
+ }
226
+
227
+ return { handles: await dependencies.connectLocal(port), mode, port }
228
+ }
229
+
230
+ async function disposeState(state: AttachState) {
231
+ try {
232
+ await state.handles.close()
233
+ } finally {
234
+ await state.disposeRemote?.()
235
+ }
236
+ }
@@ -16,7 +16,7 @@ import { ensureAttestorCrypto } from '../../../proof/proof-core.ts'
16
16
  import { type RegisteredTool } from '../../server.ts'
17
17
  import { analyzeTool } from './analyze.ts'
18
18
  import { type AttachState, attachTool } from './attach.ts'
19
- import { type CaptureStore, captureTools } from './capture.ts'
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
22
  import { createVersionFromCaptureTool } from './publish.ts'
@@ -28,7 +28,11 @@ export interface AgentState {
28
28
  captures: CaptureStore
29
29
  }
30
30
 
31
- function buildAgentState(): AgentState {
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
+ export function buildAgentState(): AgentState {
32
36
  return {
33
37
  attach: { config: new ConfigStore() },
34
38
  captures: { sessions: new Map() },
@@ -39,17 +43,27 @@ function buildAgentState(): AgentState {
39
43
  * `registerToolList`.
40
44
  * Pass `client` to include the builder `create_provider_version_from_capture`;
41
45
  * omit it (e.g. OLD-devtools mode) to register only the backend-independent
42
- * capture/proof tools — old mode supplies its own publish tool. */
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. */
43
49
  export function buildAgentTools(
44
50
  client?: ReclaimClient,
51
+ builderClient: ReclaimClient | undefined = client,
52
+ externalState?: AgentState,
45
53
  ): RegisteredTool[] {
46
- const state = buildAgentState()
54
+ const state = externalState ?? buildAgentState()
47
55
  ensureAttestorCrypto()
48
56
  const tools: RegisteredTool[] = []
49
57
  // Stateless docs tool — serve the mode-specific authoring guide to any
50
58
  // client. No `client` ⇒ old-devtools mode (see `mcp/start.ts`).
51
59
  tools.push(howItWorksTool(!client))
52
- tools.push(attachTool(state.attach))
60
+ 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
+ ))
53
67
  tools.push(listTabsTool(state.attach))
54
68
  for(const t of captureTools(state.attach, state.captures)) {
55
69
  tools.push(t)
@@ -68,7 +82,7 @@ export function buildAgentTools(
68
82
  }
69
83
 
70
84
  if(client) {
71
- tools.push(createVersionFromCaptureTool(client))
85
+ tools.push(createVersionFromCaptureTool(client, state.attach))
72
86
  }
73
87
 
74
88
  return tools
@@ -20,13 +20,22 @@ export interface CaptureStore {
20
20
  activeTab?: string
21
21
  }
22
22
 
23
+ export async function resetCaptures(captures: CaptureStore) {
24
+ for(const entry of captures.sessions.values()) {
25
+ await entry.session.stop().catch(() => {})
26
+ }
27
+
28
+ captures.sessions.clear()
29
+ delete captures.activeTab
30
+ }
31
+
23
32
  export function captureTools(
24
33
  attachRef: { current?: AttachState },
25
34
  captures: CaptureStore,
26
35
  ): RegisteredTool[] {
27
36
  const requireAttached = (): AttachState => {
28
37
  if(!attachRef.current) {
29
- throw new Error('Not attached. Call attach_chrome first.')
38
+ throw new Error('Not attached. Call attach_browser first.')
30
39
  }
31
40
 
32
41
  return attachRef.current
@@ -1,4 +1,3 @@
1
- import { listTabs } from '../../../cdp/tabs.ts'
2
1
  import { defineTool, type RegisteredTool } from '../../server.ts'
3
2
  import type { AttachState } from './attach.ts'
4
3
 
@@ -16,10 +15,10 @@ export function listTabsTool(attachRef: {
16
15
  },
17
16
  async() => {
18
17
  if(!attachRef.current) {
19
- throw new Error('Not attached. Call attach_chrome first.')
18
+ throw new Error('Not attached. Call attach_browser first.')
20
19
  }
21
20
 
22
- return { tabs: await listTabs(attachRef.current.port) }
21
+ return { tabs: await attachRef.current.handles.listTabs() }
23
22
  },
24
23
  )
25
24
  }
@@ -11,7 +11,7 @@ import { defineTool, type RegisteredTool } from '../../server.ts'
11
11
  * so any MCP client — not just Claude Code, which alone auto-loads skills — can
12
12
  * pull it. `oldMode` selects the backend-specific guide so the agent is never
13
13
  * told to call auth/publish tools that aren't registered in its mode.
14
- * Stateless: callable before `attach_chrome`.
14
+ * Stateless: callable before `attach_browser`.
15
15
  */
16
16
  export function howItWorksTool(oldMode: boolean): RegisteredTool {
17
17
  const guide = oldMode ? PROVIDER_GUIDE_OLD : PROVIDER_GUIDE_BUILDER
@@ -1,4 +1,4 @@
1
1
  // Barrel for the agent tool group (Chrome CDP capture, provider synthesis,
2
2
  // replay, zkTLS proof). Import from here, not the individual files.
3
- export { buildAgentTools } from './build.ts'
3
+ export { buildAgentState, buildAgentTools } from './build.ts'
4
4
  export type { AgentState } from './build.ts'
@@ -12,6 +12,7 @@ import {
12
12
  providerToCreateVersionRequest,
13
13
  } from '../../../provider/to-version.ts'
14
14
  import { defineTool, type RegisteredTool } from '../../server.ts'
15
+ import type { AttachState } from './attach.ts'
15
16
 
16
17
  /** Schemas shared by the top-level `provider`/`providers` and the lighter
17
18
  * `allowedJsRequests` drafts — keeps the request shape from drifting between
@@ -142,6 +143,7 @@ interface PublishArgs {
142
143
 
143
144
  export function createVersionFromCaptureTool(
144
145
  client: ReclaimClient,
146
+ attachRef?: { current?: AttachState },
145
147
  ): RegisteredTool {
146
148
  return defineTool<PublishArgs>(
147
149
  {
@@ -172,7 +174,11 @@ export function createVersionFromCaptureTool(
172
174
  'sessions (verification matches those values exactly; a missing ' +
173
175
  'field is a 400 at session create). ' +
174
176
  'Refine types/descriptions via `update_provider_version` while ' +
175
- 'still on draft.',
177
+ 'still on draft. ' +
178
+ 'On success, if a browser is still attached, the response\'s ' +
179
+ '`_notes` reminds you to confirm with the developer before ' +
180
+ 'calling `dispose_browser` — never close it unprompted, they ' +
181
+ 'may want it for further edits.',
176
182
  inputSchema: {
177
183
  type: 'object',
178
184
  properties: {
@@ -234,7 +240,28 @@ export function createVersionFromCaptureTool(
234
240
  description:
235
241
  'Extra requests the injection script is allowed to fire ' +
236
242
  '(e.g. paginated API calls), as RequestSelectionTemplates. ' +
237
- '`multiple`/`required` default to true.',
243
+ '`multiple`/`required` default to true. ' +
244
+ '⚠️ BEFORE populating this to gate ' +
245
+ '`verifyProof(proof, { providerId })` (old-devtools calls the ' +
246
+ 'same concept `allowedInjectedRequestData` — same mechanics, ' +
247
+ 'different field name): this whole entry (url, method, body, ' +
248
+ 'responseMatches, responseRedactions) feeds a client-side ' +
249
+ 'hash-equality check, and `url` is hashed VERBATIM — no regex/ ' +
250
+ 'template resolution, even with `urlType: REGEX` or ' +
251
+ '`TEMPLATE`. If the real captured URL contains ANY per-session ' +
252
+ 'part (a tracking nonce, CSRF token, cache-busting timestamp), ' +
253
+ 'no declared spec can EVER hash-match a real proof — populating' +
254
+ ' this for such a provider makes verifyProof reject every real ' +
255
+ 'proof, which is worse than leaving it empty. Diff the same ' +
256
+ 'endpoint\'s captured URL across two separate sessions before ' +
257
+ 'wiring this up. If provider-hash validation keeps causing ' +
258
+ 'problems (unstable url, or any other persistent mismatch), ' +
259
+ 'the fallback on the verifying side is `verifyProof(proof, { ' +
260
+ 'dangerouslyDisableContentValidation: true })` — trusts only ' +
261
+ 'the attestor\'s signature, skips the hash-equality check ' +
262
+ 'entirely; see the "Provider-hash validation" section of the ' +
263
+ 'injection script\'s guide for the full mechanics and when to ' +
264
+ 'reach for it.',
238
265
  items: ALLOWED_JS_REQUEST_SCHEMA,
239
266
  },
240
267
  interceptorOptions: {
@@ -307,6 +334,23 @@ export function createVersionFromCaptureTool(
307
334
  ]
308
335
  notes.push(...matchRedactionWarnings(requestCounts))
309
336
 
337
+ // A browser session left attached after a successful publish is easy
338
+ // to forget about — surface it so the caller asks the developer
339
+ // before closing it (a builder-mode session is quota-accounted and
340
+ // otherwise just sits until its TTL expires).
341
+ const attached = attachRef?.current
342
+ if(attached) {
343
+ notes.push(
344
+ `A "${attached.mode}" browser session is still attached` +
345
+ (attached.browserSessionId
346
+ ? ` (browserSessionId: ${attached.browserSessionId})`
347
+ : '') +
348
+ '. Ask the developer whether they want to keep it open for ' +
349
+ 'further edits — only call dispose_browser once they confirm ' +
350
+ 'they\'re done with it.',
351
+ )
352
+ }
353
+
310
354
  // Let the dev know how/when the script runs (and the window.Reclaim gap).
311
355
  if(args.jsUserScripts?.trim()) {
312
356
  notes.push(
@@ -1,7 +1,7 @@
1
1
  import type { ConfigStore } from '../../../config/store.ts'
2
2
  import { defineTool, type RegisteredTool } from '../../server.ts'
3
- import type { AttachState } from './attach.ts'
4
- import type { CaptureStore } from './capture.ts'
3
+ import { type AttachState, disposeAttached } from './attach.ts'
4
+ import { type CaptureStore, resetCaptures } from './capture.ts'
5
5
 
6
6
  export function sessionTools(
7
7
  attachRef: { current?: AttachState, config: ConfigStore },
@@ -18,11 +18,29 @@ export function sessionTools(
18
18
  async() => ({
19
19
  config: attachRef.config.read()?.browser_agent,
20
20
  attached: Boolean(attachRef.current),
21
+ mode: attachRef.current?.mode,
21
22
  port: attachRef.current?.port,
23
+ liveViewUrl: attachRef.current?.liveViewUrl,
24
+ browserSessionId: attachRef.current?.browserSessionId,
25
+ orgId: attachRef.current?.orgId,
22
26
  captures: [...captures.sessions.keys()],
23
27
  activeTab: captures.activeTab,
24
28
  }),
25
29
  ),
30
+ defineTool<Record<string, never>>(
31
+ {
32
+ name: 'dispose_browser',
33
+ description:
34
+ 'Stop captures, disconnect the current browser, and immediately '
35
+ + 'delete a Builder-hosted Popcorn session. Custom CDP sessions '
36
+ + 'are disconnected but never deleted by Reclaim.',
37
+ inputSchema: { type: 'object', properties: {} },
38
+ },
39
+ async() => {
40
+ await resetAll(attachRef, captures)
41
+ return { disposed: true }
42
+ },
43
+ ),
26
44
  defineTool<Record<string, never>>(
27
45
  {
28
46
  name: 'reset_session',
@@ -32,14 +50,17 @@ export function sessionTools(
32
50
  inputSchema: { type: 'object', properties: {} },
33
51
  },
34
52
  async() => {
35
- for(const entry of captures.sessions.values()) {
36
- await entry.session.stop().catch(() => {})
37
- }
38
-
39
- captures.sessions.clear()
40
- delete captures.activeTab
53
+ await resetAll(attachRef, captures)
41
54
  return { reset: true }
42
55
  },
43
56
  ),
44
57
  ]
45
58
  }
59
+
60
+ async function resetAll(
61
+ attachRef: { current?: AttachState, config: ConfigStore },
62
+ captures: CaptureStore,
63
+ ) {
64
+ await resetCaptures(captures)
65
+ await disposeAttached(attachRef)
66
+ }
@@ -54,19 +54,14 @@ function withLocalOprfMode(provider: ReclaimProvider): ReclaimProvider {
54
54
  * captured requests, prove via the in-process attestor, replay by re-fetch. */
55
55
  function localBackend(entry: CaptureEntry): AuthoringBackend {
56
56
  const { tabClient, session, drafts } = entry
57
- // The per-tab CDP client is strongly typed to `keyof Commands`; the shared
58
- // tools drive it with dynamic method/event names, so view it as string-keyed.
59
- const client = tabClient as unknown as {
60
- send: (method: string, params: object) => Promise<unknown>
61
- once: (e: string, cb: () => void) => void
62
- removeListener: (e: string, cb: () => void) => void
63
- }
64
57
 
65
58
  return {
66
59
  capture: session,
67
60
  drafts,
68
- sendCdp: (method, params) => client.send(method, params ?? {}),
61
+ sendCdp: (method, params) => tabClient.send(method, params ?? {}),
69
62
  waitForEvent: (method, timeoutMs) => new Promise((resolve) => {
63
+ let stopListening = () => {}
64
+
70
65
  const onEvent = () => {
71
66
  cleanup()
72
67
  resolve('fired')
@@ -78,10 +73,10 @@ function localBackend(entry: CaptureEntry): AuthoringBackend {
78
73
  }, timeoutMs)
79
74
  const cleanup = () => {
80
75
  clearTimeout(timer)
81
- client.removeListener(method, onEvent)
76
+ stopListening()
82
77
  }
83
78
 
84
- client.once(method, onEvent)
79
+ stopListening = tabClient.subscribeOnce(method, onEvent)
85
80
  }),
86
81
  replay: async(draft, opts) => {
87
82
  // Auth-bound check: re-fetch with the secrets dropped. replayProvider