@jobshimo/browser-link 0.27.1 → 0.28.1
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/dist/activity/db.d.ts +27 -0
- package/dist/activity/db.js +98 -0
- package/dist/activity/db.js.map +1 -0
- package/dist/activity/extract.d.ts +32 -0
- package/dist/activity/extract.js +126 -0
- package/dist/activity/extract.js.map +1 -0
- package/dist/activity/paths.d.ts +16 -0
- package/dist/activity/paths.js +24 -0
- package/dist/activity/paths.js.map +1 -0
- package/dist/activity/queries.d.ts +100 -0
- package/dist/activity/queries.js +286 -0
- package/dist/activity/queries.js.map +1 -0
- package/dist/activity/types.d.ts +119 -0
- package/dist/activity/types.js +41 -0
- package/dist/activity/types.js.map +1 -0
- package/dist/bridge/ws-bridge.d.ts +18 -1
- package/dist/bridge/ws-bridge.js +107 -0
- package/dist/bridge/ws-bridge.js.map +1 -1
- package/dist/cdp/inpage/deep-query.d.ts +1 -1
- package/dist/cdp/inpage/deep-query.js +1 -1
- package/dist/cdp/inpage/dom-helpers.d.ts +1 -1
- package/dist/cdp/inpage/dom-helpers.js +1 -1
- package/dist/cli.js +24 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/activity.d.ts +11 -0
- package/dist/commands/activity.js +347 -0
- package/dist/commands/activity.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js.map +1 -1
- package/dist/extension/activity.d.ts +201 -0
- package/dist/extension/activity.html +662 -0
- package/dist/extension/activity.js +555 -0
- package/dist/extension/activity.js.map +1 -0
- package/dist/extension/background.js +146 -0
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/inpage/deep-query.d.ts +1 -1
- package/dist/extension/inpage/deep-query.js +1 -1
- package/dist/extension/inpage/dom-helpers.d.ts +1 -1
- package/dist/extension/inpage/dom-helpers.js +1 -1
- package/dist/extension/manifest.json +3 -2
- package/dist/extension/popup.html +58 -0
- package/dist/extension/popup.js +11 -0
- package/dist/extension/popup.js.map +1 -1
- package/dist/messages.d.ts +80 -2
- package/dist/server.js +30 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +50 -2
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +34 -7
- package/dist/tools/browser-dispatch.js +107 -1
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { getActivityDb, MAX_ACTIVITY_ROWS, pruneIfDue } from './db.js';
|
|
3
|
+
import { getActivityDbPath } from './paths.js';
|
|
4
|
+
/** Hard ceiling on rows a single read can return, whatever `limit` asks for.
|
|
5
|
+
* The panel pages; nothing needs ten thousand rows in one response, and an
|
|
6
|
+
* unbounded read is how a renderer freezes a browser. */
|
|
7
|
+
export const MAX_QUERY_LIMIT = 500;
|
|
8
|
+
const DEFAULT_QUERY_LIMIT = 200;
|
|
9
|
+
/** Longest stored `payload` / `error`. A `browser.evaluate` expression can be
|
|
10
|
+
* arbitrarily long and an error can carry a page's worth of text; neither is
|
|
11
|
+
* more legible at 40 KB than at 4 KB, and the trail is meant to stay
|
|
12
|
+
* greppable. Truncation is marked so a reader never mistakes a cut string for
|
|
13
|
+
* the whole thing. */
|
|
14
|
+
export const MAX_TEXT_LEN = 4_000;
|
|
15
|
+
const TRUNCATION_MARK = '…[truncated]';
|
|
16
|
+
export function truncate(value, max = MAX_TEXT_LEN) {
|
|
17
|
+
if (value === null)
|
|
18
|
+
return null;
|
|
19
|
+
if (value.length <= max)
|
|
20
|
+
return value;
|
|
21
|
+
return value.slice(0, max - TRUNCATION_MARK.length) + TRUNCATION_MARK;
|
|
22
|
+
}
|
|
23
|
+
function toRecord(row) {
|
|
24
|
+
return {
|
|
25
|
+
id: row.id,
|
|
26
|
+
at: row.at,
|
|
27
|
+
tool: row.tool,
|
|
28
|
+
tabId: row.tab_id,
|
|
29
|
+
transport: row.transport ?? null,
|
|
30
|
+
url: row.url,
|
|
31
|
+
title: row.title,
|
|
32
|
+
agent: row.agent,
|
|
33
|
+
agentPid: row.agent_pid,
|
|
34
|
+
selector: row.selector,
|
|
35
|
+
payload: row.payload,
|
|
36
|
+
outcome: row.outcome === 'error' ? 'error' : 'ok',
|
|
37
|
+
error: row.error,
|
|
38
|
+
durationMs: row.duration_ms,
|
|
39
|
+
flowId: row.flow_id,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Append one dispatched call to the trail.
|
|
44
|
+
*
|
|
45
|
+
* NEVER THROWS. Recording is observation, not the work: an agent's click must
|
|
46
|
+
* not fail because the disk is full or the DB is locked. The caller
|
|
47
|
+
* (`browser-dispatch.ts`) treats this as fire-and-forget, and a lost row is
|
|
48
|
+
* strictly better than a lost action.
|
|
49
|
+
*/
|
|
50
|
+
export function record(input, db) {
|
|
51
|
+
try {
|
|
52
|
+
const handle = db ?? getActivityDb();
|
|
53
|
+
const at = new Date().toISOString();
|
|
54
|
+
const result = handle
|
|
55
|
+
.prepare(`INSERT INTO activity
|
|
56
|
+
(at, tool, tab_id, transport, url, title, agent, agent_pid,
|
|
57
|
+
selector, payload, outcome, error, duration_ms, flow_id)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
59
|
+
.run(at, input.tool, input.tabId, input.transport, truncate(input.url), truncate(input.title, 300), input.agent, input.agentPid, truncate(input.selector), truncate(input.payload), input.outcome, truncate(input.error), Math.max(0, Math.round(input.durationMs)), input.flowId);
|
|
60
|
+
pruneIfDue(handle);
|
|
61
|
+
return { ...input, id: Number(result.lastInsertRowid), at };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Build the shared WHERE clause + bindings for a filter. Kept in one place
|
|
68
|
+
* so `query` and its COUNT can never drift apart and report a total that does
|
|
69
|
+
* not match the rows beside it. */
|
|
70
|
+
function buildFilter(q) {
|
|
71
|
+
const clauses = [];
|
|
72
|
+
const params = [];
|
|
73
|
+
if (q.sinceId !== undefined) {
|
|
74
|
+
clauses.push('id > ?');
|
|
75
|
+
params.push(q.sinceId);
|
|
76
|
+
}
|
|
77
|
+
if (q.beforeId !== undefined) {
|
|
78
|
+
clauses.push('id < ?');
|
|
79
|
+
params.push(q.beforeId);
|
|
80
|
+
}
|
|
81
|
+
if (q.tabId !== undefined) {
|
|
82
|
+
clauses.push('tab_id = ?');
|
|
83
|
+
params.push(q.tabId);
|
|
84
|
+
}
|
|
85
|
+
if (q.agent !== undefined) {
|
|
86
|
+
clauses.push('agent = ?');
|
|
87
|
+
params.push(q.agent);
|
|
88
|
+
}
|
|
89
|
+
if (q.tool !== undefined) {
|
|
90
|
+
clauses.push('tool = ?');
|
|
91
|
+
params.push(q.tool);
|
|
92
|
+
}
|
|
93
|
+
if (q.flowId !== undefined) {
|
|
94
|
+
clauses.push('flow_id = ?');
|
|
95
|
+
params.push(q.flowId);
|
|
96
|
+
}
|
|
97
|
+
if (q.outcome !== undefined) {
|
|
98
|
+
clauses.push('outcome = ?');
|
|
99
|
+
params.push(q.outcome);
|
|
100
|
+
}
|
|
101
|
+
if (q.since !== undefined) {
|
|
102
|
+
clauses.push('at >= ?');
|
|
103
|
+
params.push(q.since);
|
|
104
|
+
}
|
|
105
|
+
return { where: clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '', params };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Read a page of the trail, NEWEST FIRST.
|
|
109
|
+
*
|
|
110
|
+
* Newest-first is what both readers want and it is the only ordering that
|
|
111
|
+
* makes `limit` honest: with oldest-first, "the last 200" would require
|
|
112
|
+
* counting the whole table before choosing an offset. The window reverses the
|
|
113
|
+
* page for display; the CLI prints it as-is.
|
|
114
|
+
*/
|
|
115
|
+
export function query(q = {}, db) {
|
|
116
|
+
const handle = db ?? getActivityDb();
|
|
117
|
+
const limit = Math.min(Math.max(1, q.limit ?? DEFAULT_QUERY_LIMIT), MAX_QUERY_LIMIT);
|
|
118
|
+
const { where, params } = buildFilter(q);
|
|
119
|
+
const rows = handle
|
|
120
|
+
.prepare(`SELECT * FROM activity ${where} ORDER BY id DESC LIMIT ?`)
|
|
121
|
+
.all(...params, limit);
|
|
122
|
+
const { total } = handle
|
|
123
|
+
.prepare(`SELECT COUNT(*) AS total FROM activity ${where}`)
|
|
124
|
+
.get(...params);
|
|
125
|
+
const records = rows.map(toRecord);
|
|
126
|
+
// `latestId` is the highest id in the page — rows come back DESC, so that is
|
|
127
|
+
// the first one. Falls back to the request's cursor (not 0) when the page is
|
|
128
|
+
// empty, so a tailing caller that polls an idle bridge does not rewind and
|
|
129
|
+
// re-read everything on the next tick.
|
|
130
|
+
const latestId = records.length > 0 ? records[0].id : (q.sinceId ?? 0);
|
|
131
|
+
return { records, latestId, total };
|
|
132
|
+
}
|
|
133
|
+
/** Every distinct agent that appears in the trail, most recently seen first.
|
|
134
|
+
* Feeds the window's filter chips — a hardcoded list would go stale the
|
|
135
|
+
* moment someone wires up a new MCP client. */
|
|
136
|
+
export function listAgents(db) {
|
|
137
|
+
const handle = db ?? getActivityDb();
|
|
138
|
+
return handle
|
|
139
|
+
.prepare(`SELECT agent, COUNT(*) AS count FROM activity
|
|
140
|
+
WHERE agent IS NOT NULL
|
|
141
|
+
GROUP BY agent ORDER BY MAX(id) DESC`)
|
|
142
|
+
.all();
|
|
143
|
+
}
|
|
144
|
+
/** Delete the trail, or the part of it older than `before`. Returns rows
|
|
145
|
+
* removed. VACUUM is the caller's call (the CLI offers it) — it rewrites the
|
|
146
|
+
* whole file and has no business running inside a tool dispatch. */
|
|
147
|
+
export function clear(before, db) {
|
|
148
|
+
const handle = db ?? getActivityDb();
|
|
149
|
+
if (before === undefined) {
|
|
150
|
+
return handle.prepare('DELETE FROM activity').run().changes;
|
|
151
|
+
}
|
|
152
|
+
return handle.prepare('DELETE FROM activity WHERE at < ?').run(before).changes;
|
|
153
|
+
}
|
|
154
|
+
function rangeFilter(range) {
|
|
155
|
+
const clauses = [];
|
|
156
|
+
const params = [];
|
|
157
|
+
if (range.from !== undefined) {
|
|
158
|
+
clauses.push('at >= ?');
|
|
159
|
+
params.push(range.from);
|
|
160
|
+
}
|
|
161
|
+
if (range.to !== undefined) {
|
|
162
|
+
clauses.push('at <= ?');
|
|
163
|
+
params.push(range.to);
|
|
164
|
+
}
|
|
165
|
+
return { where: clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '', params };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* How many rows a purge of this range WOULD remove.
|
|
169
|
+
*
|
|
170
|
+
* Exists so the UI can tell the user the number before they confirm, not
|
|
171
|
+
* after. "Delete 4,812 of 12,483 actions" is a decision; "Delete?" is a
|
|
172
|
+
* gamble, and this delete is permanent.
|
|
173
|
+
*/
|
|
174
|
+
export function countInRange(range, db) {
|
|
175
|
+
const handle = db ?? getActivityDb();
|
|
176
|
+
const { where, params } = rangeFilter(range);
|
|
177
|
+
const row = handle.prepare(`SELECT COUNT(*) AS n FROM activity ${where}`).get(...params);
|
|
178
|
+
return row.n;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Delete every row inside an inclusive date range. Returns rows removed.
|
|
182
|
+
*
|
|
183
|
+
* `vacuum` defaults ON here, unlike `clear`. The whole point of purging from
|
|
184
|
+
* the UI is that the file got big, and a DELETE alone leaves the pages
|
|
185
|
+
* allocated — the user would purge half the trail, watch the reported size not
|
|
186
|
+
* move, and reasonably conclude it did nothing. VACUUM rewrites the file so
|
|
187
|
+
* the number they are watching actually drops.
|
|
188
|
+
*/
|
|
189
|
+
export function purgeRange(range, options = {}, db) {
|
|
190
|
+
const handle = db ?? getActivityDb();
|
|
191
|
+
const { where, params } = rangeFilter(range);
|
|
192
|
+
const removed = handle.prepare(`DELETE FROM activity ${where}`).run(...params).changes;
|
|
193
|
+
if (removed > 0 && options.vacuum !== false) {
|
|
194
|
+
try {
|
|
195
|
+
// VACUUM ALONE IS NOT ENOUGH IN WAL MODE, and getting this wrong is
|
|
196
|
+
// invisible in the worst way: the rows go, the reported size does not
|
|
197
|
+
// move, and the user reasonably concludes the purge did nothing.
|
|
198
|
+
//
|
|
199
|
+
// In WAL mode the freed pages land in `activity.db-wal` rather than
|
|
200
|
+
// being handed back to the filesystem, so the file set stays exactly as
|
|
201
|
+
// large as before. TRUNCATE checkpoints fold the WAL back into the main
|
|
202
|
+
// DB and then truncate the sidecar to zero — once before, so VACUUM
|
|
203
|
+
// rewrites a DB that already contains everything, and once after, so the
|
|
204
|
+
// pages VACUUM itself freed are actually released.
|
|
205
|
+
handle.pragma('wal_checkpoint(TRUNCATE)');
|
|
206
|
+
handle.exec('VACUUM');
|
|
207
|
+
handle.pragma('wal_checkpoint(TRUNCATE)');
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// A checkpoint or VACUUM can fail while another connection holds the
|
|
211
|
+
// file. The rows are already gone, which is what was asked for; the
|
|
212
|
+
// space comes back on the next successful purge.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return removed;
|
|
216
|
+
}
|
|
217
|
+
export function stats(db) {
|
|
218
|
+
const handle = db ?? getActivityDb();
|
|
219
|
+
const row = handle
|
|
220
|
+
.prepare('SELECT COUNT(*) AS rows, MIN(at) AS oldest, MAX(at) AS newest FROM activity')
|
|
221
|
+
.get();
|
|
222
|
+
return {
|
|
223
|
+
rows: row.rows,
|
|
224
|
+
bytes: activityBytesOnDisk(),
|
|
225
|
+
oldestAt: row.oldest,
|
|
226
|
+
newestAt: row.newest,
|
|
227
|
+
maxRows: MAX_ACTIVITY_ROWS,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** Sum the SQLite file set. Missing sidecars are simply 0 — a freshly
|
|
231
|
+
* checkpointed DB has no -wal, and that is not an error. */
|
|
232
|
+
function activityBytesOnDisk() {
|
|
233
|
+
const base = getActivityDbPath();
|
|
234
|
+
let total = 0;
|
|
235
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
236
|
+
try {
|
|
237
|
+
total += statSync(`${base}${suffix}`).size;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Not present — nothing to add.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return total;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Stream the whole trail for export, oldest first, in batches.
|
|
247
|
+
*
|
|
248
|
+
* A generator rather than an array on purpose: an export of 50k rows must not
|
|
249
|
+
* materialise 50k objects to hand to a JSON serializer. Oldest-first because
|
|
250
|
+
* an exported file is read top-to-bottom as a story, which is the opposite of
|
|
251
|
+
* how a live panel is read.
|
|
252
|
+
*/
|
|
253
|
+
export function* exportAll(filter = {}, db) {
|
|
254
|
+
const handle = db ?? getActivityDb();
|
|
255
|
+
const { where, params } = buildFilter(filter);
|
|
256
|
+
const stmt = handle.prepare(`SELECT * FROM activity ${where} ORDER BY id ASC`);
|
|
257
|
+
for (const row of stmt.iterate(...params)) {
|
|
258
|
+
yield toRecord(row);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Strip the free-text columns from a record on its way out of the machine.
|
|
262
|
+
* Applied at EXPORT time, never at record time — see the PRIVACY CONTRACT in
|
|
263
|
+
* `types.ts`. The row keeps its shape so a redacted export is still a valid,
|
|
264
|
+
* diffable trail; only what was typed or evaluated goes. */
|
|
265
|
+
export function redact(record) {
|
|
266
|
+
return {
|
|
267
|
+
...record,
|
|
268
|
+
payload: record.payload === null ? null : '[redacted]',
|
|
269
|
+
title: record.title === null ? null : '[redacted]',
|
|
270
|
+
url: record.url === null ? null : redactUrl(record.url),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Keep origin + pathname, drop query string and fragment — that is where
|
|
274
|
+
* tokens, search terms and session ids live. A trail that says
|
|
275
|
+
* `https://app.example.com/orders` is still useful; one that says
|
|
276
|
+
* `?token=abc123` is a leak. */
|
|
277
|
+
function redactUrl(url) {
|
|
278
|
+
try {
|
|
279
|
+
const parsed = new URL(url);
|
|
280
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return '[redacted]';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=queries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/activity/queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activity trail: one row per browser tool an agent dispatched.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS LIVES ON THE SERVER, NOT IN THE EXTENSION
|
|
5
|
+
* --------------------------------------------------
|
|
6
|
+
* The extension's flow history (`flow-registry.ts`) is a "what just happened
|
|
7
|
+
* on this tab" panel: `chrome.storage.session`, 20 entries per tab, gone when
|
|
8
|
+
* the browser closes. That is the right shape for the 320px popup and the
|
|
9
|
+
* wrong shape for an audit trail.
|
|
10
|
+
*
|
|
11
|
+
* The server already sees EVERY action — `handleBrowserTool` is the single
|
|
12
|
+
* funnel both transports pass through, extension and cdp-direct alike — and
|
|
13
|
+
* it has a real filesystem. Recording here means one hook instead of one per
|
|
14
|
+
* transport, no `chrome.storage` quota, and a trail that outlives the
|
|
15
|
+
* browser AND the server process.
|
|
16
|
+
*
|
|
17
|
+
* PRIVACY CONTRACT
|
|
18
|
+
* ----------------
|
|
19
|
+
* This is deliberately a DIFFERENT contract from the popup's.
|
|
20
|
+
*
|
|
21
|
+
* `flow-registry.ts` keeps counts and fixed enums only, because it renders
|
|
22
|
+
* into a UI the user did not ask for and cannot clear. This trail is the
|
|
23
|
+
* opposite: the user asked for it explicitly, it is theirs, it lives in
|
|
24
|
+
* their own data dir, and it is worthless for auditing an agent if it omits
|
|
25
|
+
* what the agent actually typed or evaluated.
|
|
26
|
+
*
|
|
27
|
+
* So `payload` DOES carry page-derived and agent-supplied text — the string
|
|
28
|
+
* that was typed, the expression that was evaluated, the URL navigated to.
|
|
29
|
+
* Three properties keep that honest:
|
|
30
|
+
*
|
|
31
|
+
* 1. It never leaves the machine on its own. Nothing uploads it.
|
|
32
|
+
* 2. Recording it is a config switch (`activity.record-payloads`), so a
|
|
33
|
+
* user who wants the shape of the trail without its contents can have
|
|
34
|
+
* exactly that.
|
|
35
|
+
* 3. Redaction happens on the way OUT (`activity export --redact`), not on
|
|
36
|
+
* the way in. The owner keeps the full trail; what they hand to someone
|
|
37
|
+
* else is a decision made at export time, once, deliberately — rather
|
|
38
|
+
* than a decision made silently at record time and impossible to undo.
|
|
39
|
+
*/
|
|
40
|
+
/** Outcome of a dispatched tool. `error` is the failure branch of the same
|
|
41
|
+
* call, not a separate kind of event: an agent's failed click is exactly as
|
|
42
|
+
* interesting as a successful one when you are reconstructing what happened. */
|
|
43
|
+
export type ActivityOutcome = 'ok' | 'error';
|
|
44
|
+
/** Which wire the call travelled over. `null` for tools that never touch a
|
|
45
|
+
* tab (`browser.list_tabs`, `browser.my_tabs`), so the column can be read as
|
|
46
|
+
* "how this reached the page" rather than being forced to lie. */
|
|
47
|
+
export type ActivityTransport = 'extension' | 'cdp';
|
|
48
|
+
/** One dispatched tool call, as stored. Field order mirrors the table. */
|
|
49
|
+
export interface ActivityRecord {
|
|
50
|
+
/** Monotonic rowid. Also the pagination cursor — see `queries.ts`. */
|
|
51
|
+
id: number;
|
|
52
|
+
/** ISO-8601, UTC. */
|
|
53
|
+
at: string;
|
|
54
|
+
/** Full MCP tool name, e.g. `browser.click`. Never abbreviated: the trail
|
|
55
|
+
* is read by humans and by other agents, and `click` alone is ambiguous
|
|
56
|
+
* once flows and map tools are in the same list. */
|
|
57
|
+
tool: string;
|
|
58
|
+
/** browser-link tab id (`tab_1`, `cdp:TARGET-1`), or null for tabless tools. */
|
|
59
|
+
tabId: string | null;
|
|
60
|
+
transport: ActivityTransport | null;
|
|
61
|
+
/** Tab URL at dispatch time. The single most useful column when reading
|
|
62
|
+
* the trail back weeks later — a selector without a page is noise. */
|
|
63
|
+
url: string | null;
|
|
64
|
+
title: string | null;
|
|
65
|
+
/** Self-declared agent label from the claim layer ("claude-code",
|
|
66
|
+
* "opencode"), falling back to the caller's binary. What makes an exported
|
|
67
|
+
* trail useful across agents: it answers "which of them did this". */
|
|
68
|
+
agent: string | null;
|
|
69
|
+
/** The agent's process id. Disambiguates two runs of the same binary. */
|
|
70
|
+
agentPid: number | null;
|
|
71
|
+
/** CSS selector the call targeted, when the tool takes one. */
|
|
72
|
+
selector: string | null;
|
|
73
|
+
/** Agent-supplied or page-derived text: typed string, evaluate expression,
|
|
74
|
+
* navigate URL. Null when the tool has none, or when payload recording is
|
|
75
|
+
* off. See the PRIVACY CONTRACT above. */
|
|
76
|
+
payload: string | null;
|
|
77
|
+
outcome: ActivityOutcome;
|
|
78
|
+
/** Error message when `outcome` is `error`. Truncated, never a stack. */
|
|
79
|
+
error: string | null;
|
|
80
|
+
/** Wall-clock duration of the dispatch, milliseconds. */
|
|
81
|
+
durationMs: number;
|
|
82
|
+
/** Set when this call was a step inside a `browser.flow`, so the window can
|
|
83
|
+
* group a 40-step flow under one heading instead of flooding the timeline. */
|
|
84
|
+
flowId: string | null;
|
|
85
|
+
}
|
|
86
|
+
/** What the recorder is handed. `id` and `at` are assigned on write — a
|
|
87
|
+
* caller that could choose its own id could also rewrite history. */
|
|
88
|
+
export type ActivityInput = Omit<ActivityRecord, 'id' | 'at'>;
|
|
89
|
+
/** Filters for reading the trail back. Every field is optional and ANDed;
|
|
90
|
+
* the default is "the most recent page of everything". */
|
|
91
|
+
export interface ActivityQuery {
|
|
92
|
+
/** Return rows with `id` strictly greater than this. The cursor for the
|
|
93
|
+
* live-tailing panel: it keeps the last id it rendered and asks for more,
|
|
94
|
+
* exactly like `browser.events`' `since_id`. */
|
|
95
|
+
sinceId?: number;
|
|
96
|
+
/** Return rows with `id` strictly less than this — paging BACKWARDS through
|
|
97
|
+
* history, which is what the window's "load older" does. */
|
|
98
|
+
beforeId?: number;
|
|
99
|
+
tabId?: string;
|
|
100
|
+
agent?: string;
|
|
101
|
+
tool?: string;
|
|
102
|
+
flowId?: string;
|
|
103
|
+
outcome?: ActivityOutcome;
|
|
104
|
+
/** Rows at or after this ISO timestamp. */
|
|
105
|
+
since?: string;
|
|
106
|
+
/** Hard cap on rows returned. Clamped by the query layer — an unbounded
|
|
107
|
+
* read of a table with no ceiling is how a panel freezes a browser. */
|
|
108
|
+
limit?: number;
|
|
109
|
+
}
|
|
110
|
+
/** A page of the trail plus the cursor needed to ask for the next one. */
|
|
111
|
+
export interface ActivityPage {
|
|
112
|
+
records: ActivityRecord[];
|
|
113
|
+
/** Highest id in this page, or the request's `sinceId` when empty. Feed it
|
|
114
|
+
* back as `sinceId` to tail without re-reading what you already have. */
|
|
115
|
+
latestId: number;
|
|
116
|
+
/** Total rows matching the filter, ignoring `limit`. Lets the window say
|
|
117
|
+
* "showing 200 of 4813" instead of silently truncating. */
|
|
118
|
+
total: number;
|
|
119
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activity trail: one row per browser tool an agent dispatched.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS LIVES ON THE SERVER, NOT IN THE EXTENSION
|
|
5
|
+
* --------------------------------------------------
|
|
6
|
+
* The extension's flow history (`flow-registry.ts`) is a "what just happened
|
|
7
|
+
* on this tab" panel: `chrome.storage.session`, 20 entries per tab, gone when
|
|
8
|
+
* the browser closes. That is the right shape for the 320px popup and the
|
|
9
|
+
* wrong shape for an audit trail.
|
|
10
|
+
*
|
|
11
|
+
* The server already sees EVERY action — `handleBrowserTool` is the single
|
|
12
|
+
* funnel both transports pass through, extension and cdp-direct alike — and
|
|
13
|
+
* it has a real filesystem. Recording here means one hook instead of one per
|
|
14
|
+
* transport, no `chrome.storage` quota, and a trail that outlives the
|
|
15
|
+
* browser AND the server process.
|
|
16
|
+
*
|
|
17
|
+
* PRIVACY CONTRACT
|
|
18
|
+
* ----------------
|
|
19
|
+
* This is deliberately a DIFFERENT contract from the popup's.
|
|
20
|
+
*
|
|
21
|
+
* `flow-registry.ts` keeps counts and fixed enums only, because it renders
|
|
22
|
+
* into a UI the user did not ask for and cannot clear. This trail is the
|
|
23
|
+
* opposite: the user asked for it explicitly, it is theirs, it lives in
|
|
24
|
+
* their own data dir, and it is worthless for auditing an agent if it omits
|
|
25
|
+
* what the agent actually typed or evaluated.
|
|
26
|
+
*
|
|
27
|
+
* So `payload` DOES carry page-derived and agent-supplied text — the string
|
|
28
|
+
* that was typed, the expression that was evaluated, the URL navigated to.
|
|
29
|
+
* Three properties keep that honest:
|
|
30
|
+
*
|
|
31
|
+
* 1. It never leaves the machine on its own. Nothing uploads it.
|
|
32
|
+
* 2. Recording it is a config switch (`activity.record-payloads`), so a
|
|
33
|
+
* user who wants the shape of the trail without its contents can have
|
|
34
|
+
* exactly that.
|
|
35
|
+
* 3. Redaction happens on the way OUT (`activity export --redact`), not on
|
|
36
|
+
* the way in. The owner keeps the full trail; what they hand to someone
|
|
37
|
+
* else is a decision made at export time, once, deliberately — rather
|
|
38
|
+
* than a decision made silently at record time and impossible to undo.
|
|
39
|
+
*/
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/activity/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
-
import type { FlowRecordedPayload, ServerToExtension, SettingsUpdateMessage } from '../messages.js';
|
|
2
|
+
import type { ActivityPurgeMessage, ActivityQueryMessage, FlowRecordedPayload, ServerToExtension, SettingsUpdateMessage } from '../messages.js';
|
|
3
3
|
import { type BridgeEventLog } from './events.js';
|
|
4
4
|
export declare const WS_HOST = "127.0.0.1";
|
|
5
5
|
export declare const WS_PORT = 17529;
|
|
@@ -103,6 +103,23 @@ export declare function handleFlowRecordedMessage(payload: FlowRecordedPayload,
|
|
|
103
103
|
* connected — the caller surfaces the standard not-connected error from
|
|
104
104
|
* the status call that follows, rather than two competing diagnoses.
|
|
105
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* Answer one `activity.query` frame.
|
|
108
|
+
*
|
|
109
|
+
* Exported so it is unit-testable directly, same as
|
|
110
|
+
* `handleFlowRecordedMessage`. Never throws: a trail read that fails comes
|
|
111
|
+
* back as `ok: false` with a message the window can render, because a broken
|
|
112
|
+
* audit panel must not take the bridge connection down with it.
|
|
113
|
+
*/
|
|
114
|
+
export declare function handleActivityQueryMessage(msg: ActivityQueryMessage): ServerToExtension;
|
|
115
|
+
/**
|
|
116
|
+
* Answer one `activity.purge` frame.
|
|
117
|
+
*
|
|
118
|
+
* `dry_run` is not a courtesy — it is how the window can promise the user a
|
|
119
|
+
* real number before an irreversible delete. Both paths return fresh stats so
|
|
120
|
+
* the panel's size readout updates from the same round trip that changed it.
|
|
121
|
+
*/
|
|
122
|
+
export declare function handleActivityPurgeMessage(msg: ActivityPurgeMessage): ServerToExtension;
|
|
106
123
|
export declare function sendToolCancel(tabs: Map<string, TabSession>, tabId: string, flowId: string): boolean;
|
|
107
124
|
/** Build the callback that sends a tool.request frame to a specific tab and
|
|
108
125
|
* resolves with the matching tool.response (or rejects on timeout). Kept here
|
package/dist/bridge/ws-bridge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
+
import { countInRange as countActivityInRange, listAgents as listActivityAgents, purgeRange as purgeActivityRange, query as queryActivity, stats as activityStats, } from '../activity/queries.js';
|
|
2
3
|
import { isAllowedBrowser } from '../auth/allowlist.js';
|
|
3
4
|
import { lookupPeerProcess } from '../auth/process-identity.js';
|
|
4
5
|
import { VERSION } from '../version.js';
|
|
@@ -221,6 +222,27 @@ export function startWsBridge(tabs, pendingRequests, events) {
|
|
|
221
222
|
send(ws, result);
|
|
222
223
|
return;
|
|
223
224
|
}
|
|
225
|
+
if (msg.kind === 'activity.purge') {
|
|
226
|
+
// The one DESTRUCTIVE frame the extension can send, and irreversible.
|
|
227
|
+
// The window resolves a count with `dry_run` first and puts that
|
|
228
|
+
// number in front of the user before this ever arrives for real.
|
|
229
|
+
send(ws, handleActivityPurgeMessage(msg));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (msg.kind === 'activity.query') {
|
|
233
|
+
// Read-only and deliberately NOT scoped to `assignedTabId`, unlike
|
|
234
|
+
// `flow.recorded` above. The Activity window's whole job is showing
|
|
235
|
+
// what happened across every tab and every agent, and it reaches the
|
|
236
|
+
// bridge through whichever tab's socket happens to be open. A
|
|
237
|
+
// per-connection scope here would silently hide most of the trail.
|
|
238
|
+
//
|
|
239
|
+
// The connection is already authenticated (browser allowlist + peer
|
|
240
|
+
// process identity at handshake), and the trail never leaves the
|
|
241
|
+
// machine, so "any authenticated extension connection may read the
|
|
242
|
+
// local trail" is the intended boundary.
|
|
243
|
+
send(ws, handleActivityQueryMessage(msg));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
224
246
|
});
|
|
225
247
|
ws.on('close', () => {
|
|
226
248
|
if (assignedTabId) {
|
|
@@ -397,6 +419,91 @@ export function handleFlowRecordedMessage(payload, events) {
|
|
|
397
419
|
* connected — the caller surfaces the standard not-connected error from
|
|
398
420
|
* the status call that follows, rather than two competing diagnoses.
|
|
399
421
|
*/
|
|
422
|
+
/**
|
|
423
|
+
* Answer one `activity.query` frame.
|
|
424
|
+
*
|
|
425
|
+
* Exported so it is unit-testable directly, same as
|
|
426
|
+
* `handleFlowRecordedMessage`. Never throws: a trail read that fails comes
|
|
427
|
+
* back as `ok: false` with a message the window can render, because a broken
|
|
428
|
+
* audit panel must not take the bridge connection down with it.
|
|
429
|
+
*/
|
|
430
|
+
export function handleActivityQueryMessage(msg) {
|
|
431
|
+
try {
|
|
432
|
+
const q = msg.query ?? {};
|
|
433
|
+
const page = queryActivity({
|
|
434
|
+
...(typeof q.since_id === 'number' ? { sinceId: q.since_id } : {}),
|
|
435
|
+
...(typeof q.before_id === 'number' ? { beforeId: q.before_id } : {}),
|
|
436
|
+
...(typeof q.tab_id === 'string' ? { tabId: q.tab_id } : {}),
|
|
437
|
+
...(typeof q.agent === 'string' ? { agent: q.agent } : {}),
|
|
438
|
+
...(typeof q.tool === 'string' ? { tool: q.tool } : {}),
|
|
439
|
+
...(typeof q.flow_id === 'string' ? { flowId: q.flow_id } : {}),
|
|
440
|
+
...(q.outcome === 'ok' || q.outcome === 'error' ? { outcome: q.outcome } : {}),
|
|
441
|
+
...(typeof q.limit === 'number' ? { limit: q.limit } : {}),
|
|
442
|
+
});
|
|
443
|
+
return {
|
|
444
|
+
kind: 'activity.result',
|
|
445
|
+
request_id: msg.request_id,
|
|
446
|
+
ok: true,
|
|
447
|
+
records: page.records,
|
|
448
|
+
latest_id: page.latestId,
|
|
449
|
+
total: page.total,
|
|
450
|
+
agents: listActivityAgents(),
|
|
451
|
+
stats: toStatsPayload(activityStats()),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
return {
|
|
456
|
+
kind: 'activity.result',
|
|
457
|
+
request_id: msg.request_id,
|
|
458
|
+
ok: false,
|
|
459
|
+
error: error instanceof Error ? error.message : String(error),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** camelCase store shape → snake_case wire shape. One mapper so the two can
|
|
464
|
+
* never drift into reporting different numbers under the same name. */
|
|
465
|
+
function toStatsPayload(s) {
|
|
466
|
+
return {
|
|
467
|
+
rows: s.rows,
|
|
468
|
+
bytes: s.bytes,
|
|
469
|
+
oldest_at: s.oldestAt,
|
|
470
|
+
newest_at: s.newestAt,
|
|
471
|
+
max_rows: s.maxRows,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Answer one `activity.purge` frame.
|
|
476
|
+
*
|
|
477
|
+
* `dry_run` is not a courtesy — it is how the window can promise the user a
|
|
478
|
+
* real number before an irreversible delete. Both paths return fresh stats so
|
|
479
|
+
* the panel's size readout updates from the same round trip that changed it.
|
|
480
|
+
*/
|
|
481
|
+
export function handleActivityPurgeMessage(msg) {
|
|
482
|
+
try {
|
|
483
|
+
const range = {
|
|
484
|
+
...(typeof msg.from === 'string' ? { from: msg.from } : {}),
|
|
485
|
+
...(typeof msg.to === 'string' ? { to: msg.to } : {}),
|
|
486
|
+
};
|
|
487
|
+
const dryRun = msg.dry_run === true;
|
|
488
|
+
const removed = dryRun ? countActivityInRange(range) : purgeActivityRange(range);
|
|
489
|
+
return {
|
|
490
|
+
kind: 'activity.purge.result',
|
|
491
|
+
request_id: msg.request_id,
|
|
492
|
+
ok: true,
|
|
493
|
+
removed,
|
|
494
|
+
dry_run: dryRun,
|
|
495
|
+
stats: toStatsPayload(activityStats()),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
return {
|
|
500
|
+
kind: 'activity.purge.result',
|
|
501
|
+
request_id: msg.request_id,
|
|
502
|
+
ok: false,
|
|
503
|
+
error: error instanceof Error ? error.message : String(error),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
400
507
|
export function sendToolCancel(tabs, tabId, flowId) {
|
|
401
508
|
const session = tabs.get(tabId);
|
|
402
509
|
if (!session || session.ws.readyState !== WebSocket.OPEN)
|