@reclaimprotocol/agent 0.1.24 → 0.1.26
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.
- package/README.md +5 -2
- package/lib/cdp/claims.d.ts +21 -0
- package/lib/cdp/claims.js +129 -0
- package/lib/cdp/client.d.ts +0 -10
- package/lib/cdp/container.d.ts +85 -0
- package/lib/cdp/container.js +481 -0
- package/lib/live/tunnel.d.ts +6 -3
- package/lib/live/tunnel.js +57 -17
- package/lib/mcp/instructions.js +2 -1
- package/lib/mcp/start.js +58 -3
- package/lib/mcp/tools/agent/attach.d.ts +22 -2
- package/lib/mcp/tools/agent/attach.js +197 -55
- package/lib/mcp/tools/agent/build.d.ts +5 -0
- package/lib/mcp/tools/agent/build.js +9 -2
- package/lib/mcp/tools/agent/index.d.ts +1 -2
- package/lib/mcp/tools/agent/index.js +1 -1
- package/lib/mcp/tools/agent/live-view.d.ts +3 -7
- package/lib/mcp/tools/agent/live-view.js +74 -81
- package/lib/mcp/tools/agent/session.d.ts +14 -0
- package/lib/mcp/tools/agent/session.js +14 -8
- package/lib/old/client.d.ts +83 -0
- package/lib/old/client.js +46 -0
- package/lib/old/tools/build.js +2 -0
- package/lib/old/tools/session-logs.d.ts +3 -0
- package/lib/old/tools/session-logs.js +310 -0
- package/package.json +2 -2
- package/skill/SKILL.common.md +115 -38
- package/skill/SKILL.old.md +28 -2
- package/skill/builder-mode.md +9 -7
- package/skill/interception.md +3 -2
- package/skill/old-mode.md +14 -10
- package/src/cdp/claims.ts +153 -0
- package/src/cdp/client.ts +0 -8
- package/src/cdp/container.ts +593 -0
- package/src/live/tunnel.ts +71 -20
- package/src/mcp/instructions.ts +2 -1
- package/src/mcp/start.ts +65 -2
- package/src/mcp/tools/agent/attach.ts +229 -60
- package/src/mcp/tools/agent/build.ts +10 -2
- package/src/mcp/tools/agent/index.ts +6 -2
- package/src/mcp/tools/agent/live-view.ts +77 -93
- package/src/mcp/tools/agent/session.ts +14 -8
- package/src/old/client.ts +128 -0
- package/src/old/tools/build.ts +2 -0
- package/src/old/tools/session-logs.ts +395 -0
- package/lib/cdp/browser-proxy.d.ts +0 -78
- package/lib/cdp/browser-proxy.js +0 -186
- package/lib/live/page.d.ts +0 -20
- package/lib/live/page.js +0 -134
- package/lib/live/viewer.d.ts +0 -28
- package/lib/live/viewer.js +0 -232
- package/lib/mcp/tools/agent/proxy.d.ts +0 -3
- package/lib/mcp/tools/agent/proxy.js +0 -199
- package/src/live/page.ts +0 -145
- package/src/live/viewer.ts +0 -293
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { defineTool } from "../../mcp/server.js";
|
|
4
|
+
import { findProjectRoot } from "../../paths.js";
|
|
5
|
+
/** The log-stream service caps a page at 1000 entries however large a
|
|
6
|
+
* `limit` asks for, so paging is the only way past it. */
|
|
7
|
+
const MAX_PAGE = 1000;
|
|
8
|
+
/** Inline default. Small enough to stay readable in a tool result; a bigger
|
|
9
|
+
* pull belongs in `saveTo`. */
|
|
10
|
+
const DEFAULT_LIMIT = 200;
|
|
11
|
+
/** Ceiling on a `saveTo` dump, so a chatty session can't page forever. */
|
|
12
|
+
const DEFAULT_MAX_ENTRIES = 20000;
|
|
13
|
+
export function sessionLogsTool(client) {
|
|
14
|
+
return defineTool({
|
|
15
|
+
name: 'session_logs',
|
|
16
|
+
description: 'Fetch the raw log entries the in-app SDK emitted during a '
|
|
17
|
+
+ 'verification session, each with its event type (for example '
|
|
18
|
+
+ 'REQUEST_MATCHED, CLAIM_CREATION_STARTED, or PROOF_GENERATED), '
|
|
19
|
+
+ 'log level, and logger name. This is the data behind the '
|
|
20
|
+
+ 'dashboard session-logs tab, and it is far more detailed than '
|
|
21
|
+
+ 'session_analytics_logs, which returns only the coarse milestone '
|
|
22
|
+
+ 'events. Use it to diagnose a session that stalled, matched '
|
|
23
|
+
+ 'nothing, or produced no claim. Narrow the result with eventType, '
|
|
24
|
+
+ 'logLevel, or contains; page through it with limit and offset; or '
|
|
25
|
+
+ 'set saveTo to write every matching entry to an NDJSON file and '
|
|
26
|
+
+ 'read that file with your own tools. Old-devtools mode only. You '
|
|
27
|
+
+ 'must own the app the session belongs to, so call '
|
|
28
|
+
+ 'reclaim_authenticate first.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
sessionId: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Id of the verification session to read log entries for.',
|
|
35
|
+
},
|
|
36
|
+
eventType: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Keep only entries that carry this exact event type, for '
|
|
39
|
+
+ 'example REQUEST_MATCHED or PROOF_GENERATION_FAILED_'
|
|
40
|
+
+ 'EXCEPTION. Most entries carry no event type at all, so '
|
|
41
|
+
+ 'prefer eventsOnly when you want the milestone timeline '
|
|
42
|
+
+ 'rather than guessing a name.',
|
|
43
|
+
},
|
|
44
|
+
eventsOnly: {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
description: 'Keep only entries that carry an event type, which gives you '
|
|
47
|
+
+ 'a compact milestone timeline for the session. This tool '
|
|
48
|
+
+ 'applies the filter after fetching, so it narrows what you '
|
|
49
|
+
+ 'read rather than what it fetches.',
|
|
50
|
+
},
|
|
51
|
+
logLevel: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
enum: ['fine', 'config', 'info', 'warning', 'severe', 'unknown'],
|
|
54
|
+
description: 'Lowest severity to include. Each value is hierarchical: '
|
|
55
|
+
+ '`fine` includes FINE, CONFIG, INFO, WARNING, and SEVERE; '
|
|
56
|
+
+ '`info` drops FINE and CONFIG; `severe` keeps only '
|
|
57
|
+
+ 'failures. Defaults to `fine`, which is wider than the '
|
|
58
|
+
+ 'backend default of `info`. The FINER and FINEST tiers '
|
|
59
|
+
+ 'contain PII, and this route rejects them.',
|
|
60
|
+
},
|
|
61
|
+
logType: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Exact client-side logger name, which the SDK calls `type` — '
|
|
64
|
+
+ 'for example reclaim_flutter_sdk.ClaimCreation. This is not '
|
|
65
|
+
+ 'the event type.',
|
|
66
|
+
},
|
|
67
|
+
contains: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Substring that an entry\'s log line must contain.',
|
|
70
|
+
},
|
|
71
|
+
providerId: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Keep only entries for this provider. A session can touch '
|
|
74
|
+
+ 'more than one provider, so this narrows within the '
|
|
75
|
+
+ 'session; it cannot search across sessions.',
|
|
76
|
+
},
|
|
77
|
+
deviceId: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'Keep only entries for this device id, which is the claimant '
|
|
80
|
+
+ 'id.',
|
|
81
|
+
},
|
|
82
|
+
source: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Keep only entries from this client build. The source string '
|
|
85
|
+
+ 'carries the SDK, platform, and version, so it is the field '
|
|
86
|
+
+ 'to compare a working run against a broken one.',
|
|
87
|
+
},
|
|
88
|
+
startTime: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Lower bound, as an ISO-8601 timestamp. If you set neither '
|
|
91
|
+
+ 'bound, the backend searches only the last 3 days.',
|
|
92
|
+
},
|
|
93
|
+
endTime: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Upper bound, as an ISO-8601 timestamp.',
|
|
96
|
+
},
|
|
97
|
+
limit: {
|
|
98
|
+
type: 'number',
|
|
99
|
+
description: `Log entries per page. Defaults to ${DEFAULT_LIMIT} and is `
|
|
100
|
+
+ `capped at ${MAX_PAGE}. Ignored when you set saveTo.`,
|
|
101
|
+
},
|
|
102
|
+
offset: {
|
|
103
|
+
type: 'number',
|
|
104
|
+
description: 'Entries to skip, counting back from the newest match. '
|
|
105
|
+
+ 'Ignored when you set saveTo.',
|
|
106
|
+
},
|
|
107
|
+
saveTo: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Path to write every matching entry to, as NDJSON: one raw '
|
|
110
|
+
+ 'entry per line, oldest first. When you set this, the tool '
|
|
111
|
+
+ 'returns a summary instead of the entries. Relative paths '
|
|
112
|
+
+ 'resolve against the project root. Use it when a session '
|
|
113
|
+
+ 'has more entries than you want to read inline.',
|
|
114
|
+
},
|
|
115
|
+
maxEntries: {
|
|
116
|
+
type: 'number',
|
|
117
|
+
description: 'Maximum entries a saveTo dump collects. Defaults to '
|
|
118
|
+
+ `${DEFAULT_MAX_ENTRIES}.`,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ['sessionId'],
|
|
122
|
+
},
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
// The backend's own default is `info`, which silently hides every
|
|
125
|
+
// FINE and CONFIG entry — the ones that say what the interceptor
|
|
126
|
+
// actually saw. Debugging is the whole point of this tool, so widen
|
|
127
|
+
// the default here.
|
|
128
|
+
const query = {
|
|
129
|
+
logLevel: (args.logLevel ?? 'fine'),
|
|
130
|
+
logType: args.logType,
|
|
131
|
+
eventType: args.eventType,
|
|
132
|
+
providerId: args.providerId,
|
|
133
|
+
deviceId: args.deviceId,
|
|
134
|
+
source: args.source,
|
|
135
|
+
startTime: args.startTime,
|
|
136
|
+
endTime: args.endTime,
|
|
137
|
+
logLine: args.contains,
|
|
138
|
+
};
|
|
139
|
+
return args.saveTo
|
|
140
|
+
? await dump(client, args, query, args.saveTo)
|
|
141
|
+
: await page(client, args, query);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** One page, returned inline. */
|
|
145
|
+
async function page(client, args, query) {
|
|
146
|
+
const limit = clampLimit(args.limit ?? DEFAULT_LIMIT);
|
|
147
|
+
const offset = Math.max(args.offset ?? 0, 0);
|
|
148
|
+
const res = await client.getSessionLogs(args.sessionId, {
|
|
149
|
+
...query,
|
|
150
|
+
limit,
|
|
151
|
+
offset,
|
|
152
|
+
includeCount: true,
|
|
153
|
+
});
|
|
154
|
+
// Newest-first off the wire; a debugger reads a session forwards.
|
|
155
|
+
const chronological = [...res.data].reverse();
|
|
156
|
+
const entries = args.eventsOnly
|
|
157
|
+
? chronological.filter(hasEvent)
|
|
158
|
+
: chronological;
|
|
159
|
+
const fetched = res.data.length;
|
|
160
|
+
const total = res.totalCount;
|
|
161
|
+
return {
|
|
162
|
+
...summarize(args, res.failedRegions, entries, total),
|
|
163
|
+
page: {
|
|
164
|
+
limit,
|
|
165
|
+
offset,
|
|
166
|
+
fetched,
|
|
167
|
+
...(args.eventsOnly ? { keptWithEvent: entries.length } : {}),
|
|
168
|
+
hasMore: total === undefined
|
|
169
|
+
? fetched === limit
|
|
170
|
+
: offset + fetched < total,
|
|
171
|
+
nextOffset: offset + fetched,
|
|
172
|
+
},
|
|
173
|
+
entries: compact(entries),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Every match, paged out to an NDJSON file. */
|
|
177
|
+
async function dump(client, args, query, saveTo) {
|
|
178
|
+
const maxEntries = Math.max(args.maxEntries ?? DEFAULT_MAX_ENTRIES, 1);
|
|
179
|
+
const all = [];
|
|
180
|
+
const failedRegions = new Set();
|
|
181
|
+
let total;
|
|
182
|
+
let offset = 0;
|
|
183
|
+
while (all.length < maxEntries) {
|
|
184
|
+
const res = await client.getSessionLogs(args.sessionId, {
|
|
185
|
+
...query,
|
|
186
|
+
limit: Math.min(MAX_PAGE, maxEntries - all.length),
|
|
187
|
+
offset,
|
|
188
|
+
// Only the first page pays for the count; it does not move.
|
|
189
|
+
includeCount: offset === 0,
|
|
190
|
+
});
|
|
191
|
+
for (const region of res.failedRegions ?? []) {
|
|
192
|
+
failedRegions.add(region);
|
|
193
|
+
}
|
|
194
|
+
if (offset === 0) {
|
|
195
|
+
total = res.totalCount;
|
|
196
|
+
}
|
|
197
|
+
all.push(...res.data);
|
|
198
|
+
offset += res.data.length;
|
|
199
|
+
// A short page means the match set is exhausted.
|
|
200
|
+
if (!res.data.length || res.data.length < MAX_PAGE) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
if (total !== undefined && offset >= total) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
all.reverse();
|
|
208
|
+
const entries = args.eventsOnly ? all.filter(hasEvent) : all;
|
|
209
|
+
const path = isAbsolute(saveTo)
|
|
210
|
+
? saveTo
|
|
211
|
+
: join(findProjectRoot(process.cwd()), saveTo);
|
|
212
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
213
|
+
writeFileSync(path, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
214
|
+
return {
|
|
215
|
+
...summarize(args, [...failedRegions], entries, total),
|
|
216
|
+
file: {
|
|
217
|
+
path,
|
|
218
|
+
format: 'ndjson',
|
|
219
|
+
entries: entries.length,
|
|
220
|
+
order: 'oldest first',
|
|
221
|
+
note: 'Each line holds one raw log entry with all of its columns: '
|
|
222
|
+
+ 'timestamp, log_level, event_type, log_type, log_line, '
|
|
223
|
+
+ 'session_id, app_id, provider_id, device_id, source, and '
|
|
224
|
+
+ 'metadata.',
|
|
225
|
+
truncated: entries.length >= maxEntries
|
|
226
|
+
? `Stopped at maxEntries=${maxEntries}. Narrow the filters, or `
|
|
227
|
+
+ 'raise the limit.'
|
|
228
|
+
: undefined,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** The header both modes share: what was asked for, what came back, and the
|
|
233
|
+
* event-type histogram — usually enough on its own to see where a session
|
|
234
|
+
* died. */
|
|
235
|
+
function summarize(args, failedRegions, entries, total) {
|
|
236
|
+
const events = {};
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
if (entry.event_type) {
|
|
239
|
+
events[entry.event_type] = (events[entry.event_type] ?? 0) + 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
sessionId: args.sessionId,
|
|
244
|
+
context: contextOf(entries),
|
|
245
|
+
totalMatching: total,
|
|
246
|
+
span: entries.length
|
|
247
|
+
? {
|
|
248
|
+
from: entries[0].timestamp,
|
|
249
|
+
to: entries[entries.length - 1].timestamp,
|
|
250
|
+
}
|
|
251
|
+
: undefined,
|
|
252
|
+
eventTypes: Object.keys(events).length ? events : undefined,
|
|
253
|
+
...(failedRegions?.length
|
|
254
|
+
? {
|
|
255
|
+
warning: 'The log-stream query failed in region '
|
|
256
|
+
+ `${failedRegions.join(', ')}, so this result is incomplete.`,
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
...(entries.length
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
hint: 'No entries matched. Check these causes in order: the '
|
|
263
|
+
+ 'search window defaults to the last 3 days, so pass startTime '
|
|
264
|
+
+ 'and endTime for an older session; the backend deletes entries '
|
|
265
|
+
+ 'after 30 days; the backend picks which regions to query from '
|
|
266
|
+
+ 'the analytics table, so a session that never recorded an '
|
|
267
|
+
+ 'analytics event reads as empty here even when entries exist; '
|
|
268
|
+
+ 'or the filters are too narrow. Cross-check with '
|
|
269
|
+
+ 'session_analytics_logs.',
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Fields that are identical on every entry, hoisted out so they are not
|
|
274
|
+
* repeated hundreds of times. */
|
|
275
|
+
function contextOf(entries) {
|
|
276
|
+
const uniq = (pick) => {
|
|
277
|
+
const set = new Set(entries.map(pick).filter(Boolean));
|
|
278
|
+
return [...set];
|
|
279
|
+
};
|
|
280
|
+
const one = (values) => (values.length === 1 ? values[0] : values);
|
|
281
|
+
return entries.length
|
|
282
|
+
? {
|
|
283
|
+
appId: one(uniq(e => e.app_id)),
|
|
284
|
+
providerId: one(uniq(e => e.provider_id)),
|
|
285
|
+
deviceId: one(uniq(e => e.device_id)),
|
|
286
|
+
source: one(uniq(e => e.source)),
|
|
287
|
+
}
|
|
288
|
+
: undefined;
|
|
289
|
+
}
|
|
290
|
+
function compact(entries) {
|
|
291
|
+
const devices = new Set(entries.map(e => e.device_id));
|
|
292
|
+
const sources = new Set(entries.map(e => e.source));
|
|
293
|
+
return entries.map(e => ({
|
|
294
|
+
ts: e.timestamp,
|
|
295
|
+
level: e.log_level,
|
|
296
|
+
...(e.event_type ? { event: e.event_type } : {}),
|
|
297
|
+
logger: e.log_type,
|
|
298
|
+
line: e.log_line,
|
|
299
|
+
// Only worth repeating per entry once the page actually mixes them.
|
|
300
|
+
...(devices.size > 1 ? { device: e.device_id } : {}),
|
|
301
|
+
...(sources.size > 1 ? { source: e.source } : {}),
|
|
302
|
+
...(e.metadata && e.metadata !== '{}' ? { metadata: e.metadata } : {}),
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
function hasEvent(entry) {
|
|
306
|
+
return !!entry.event_type;
|
|
307
|
+
}
|
|
308
|
+
function clampLimit(limit) {
|
|
309
|
+
return Math.min(Math.max(Math.trunc(limit), 1), MAX_PAGE);
|
|
310
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reclaimprotocol/agent",
|
|
3
3
|
"title": "Reclaim Agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.26",
|
|
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.
|
|
86
|
+
"@reclaimprotocol/client": "0.1.26",
|
|
87
87
|
"@reclaimprotocol/tls": "^0.1.3",
|
|
88
88
|
"chrome-remote-interface": "^0.33.0",
|
|
89
89
|
"domhandler": "^6.0.0",
|
package/skill/SKILL.common.md
CHANGED
|
@@ -22,24 +22,45 @@ already names it.
|
|
|
22
22
|
<!-- section: browser -->
|
|
23
23
|
## Choosing a browser
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
| `
|
|
35
|
-
| `
|
|
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
|
|
40
|
-
developer
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
A `
|
|
78
|
-
|
|
79
|
-
HTTPS URL through a cloudflared quick tunnel —
|
|
80
|
-
to act is not at that machine: a login or 2FA only they can
|
|
81
|
-
colleague watching a capture.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
package/skill/SKILL.old.md
CHANGED
|
@@ -37,8 +37,9 @@ workflow — the narrow `authenticate_builder` / `list_builder_organizations`
|
|
|
37
37
|
exception exists only to allocate Builder-hosted authoring browsers.
|
|
38
38
|
`get_me_providers` is the only provider-listing tool here.
|
|
39
39
|
|
|
40
|
-
**Diagnosing a session:** `session_analytics_logs`
|
|
41
|
-
|
|
40
|
+
**Diagnosing a session:** `session_analytics_logs` returns a session's
|
|
41
|
+
milestone events; `session_logs` returns the log entries the SDK itself
|
|
42
|
+
emitted. Read both from topic `troubleshooting`.
|
|
42
43
|
|
|
43
44
|
<!-- topic: auth -->
|
|
44
45
|
## Getting authenticated
|
|
@@ -357,6 +358,31 @@ breaks the page, and it's the smallest change. Only if that doesn't help, turn
|
|
|
357
358
|
interception off entirely (`injectionType: NONE`, or omit
|
|
358
359
|
`interceptorOptions`). Full checklist and remedies: topic `interception`.
|
|
359
360
|
|
|
361
|
+
**A session stalled, matched nothing, or produced no claim.** Read it with
|
|
362
|
+
two tools, coarse to fine. `session_analytics_logs` returns the milestone
|
|
363
|
+
events for a session id — SESSION_STARTED, PROOF_GENERATION_FAILED, and so on
|
|
364
|
+
— which tells you how far the session got. `session_logs` returns the log
|
|
365
|
+
entries the SDK emitted, each with its event type, log level, and logger name,
|
|
366
|
+
which tells you why it stopped: the request it intercepted, the match or
|
|
367
|
+
redaction that failed, and what the attestor answered. Work through it in this
|
|
368
|
+
order:
|
|
369
|
+
|
|
370
|
+
1. Call `session_analytics_logs({ sessionId })` to place the failure on the
|
|
371
|
+
timeline.
|
|
372
|
+
2. Call `session_logs({ sessionId, eventsOnly: true })` for the same timeline
|
|
373
|
+
at entry level, then drop `eventsOnly` and add `contains` or `eventType` to
|
|
374
|
+
read around the entry that failed.
|
|
375
|
+
3. To compare a working run against a broken one, filter by `source` — it
|
|
376
|
+
carries the SDK, platform, and version, which is usually the variable that
|
|
377
|
+
differs.
|
|
378
|
+
|
|
379
|
+
Two defaults decide whether you see anything: the backend searches only the
|
|
380
|
+
last 3 days unless you pass `startTime` and `endTime`, and it deletes entries
|
|
381
|
+
after 30 days. An empty result is more often one of those than a genuinely
|
|
382
|
+
silent session, so read the `hint` the tool returns before concluding
|
|
383
|
+
anything. When a session is too large to read inline, pass `saveTo` to write
|
|
384
|
+
every entry to an NDJSON file and grep that file instead.
|
|
385
|
+
|
|
360
386
|
**Proof failures / attestor errors** → topic `prove`.
|
|
361
387
|
**Browser won't launch or attach** → topic `browser`.
|
|
362
388
|
**Missing signing key** → topic `credentials`.
|
package/skill/builder-mode.md
CHANGED
|
@@ -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
|
-
**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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,
|
|
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`) →
|
package/skill/interception.md
CHANGED
|
@@ -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
|
|
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` —
|
|
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
|
-
**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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,
|
|
24
|
-
browser) → `start_capture` ⇒ **`captureId`** →
|
|
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,
|
|
@@ -50,8 +52,10 @@ and the proof binds to nobody — find an auth-bound request instead.
|
|
|
50
52
|
Values the CONSUMER supplies at verification time use `{{context_<name>}}` —
|
|
51
53
|
this backend does NOT support the builder's dotted `{{context.<name>}}` form.
|
|
52
54
|
|
|
53
|
-
**Diagnosing a session:** `session_analytics_logs`
|
|
54
|
-
|
|
55
|
+
**Diagnosing a session:** `session_analytics_logs` returns a session's
|
|
56
|
+
milestone events; `session_logs` returns the log entries the SDK emitted, each
|
|
57
|
+
with its event type — that is where a failed match, redaction, or claim shows
|
|
58
|
+
up. Both take a `sessionId`; see topic `troubleshooting` for how to read them.
|
|
55
59
|
|
|
56
60
|
**Backend mode:** the BUILDER backend (generated API tools, orgs, verification
|
|
57
61
|
sessions, public review) is one switch away. `get_devtools_mode` reports the
|