browserzap 0.5.0 → 0.6.0
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/CHANGELOG.md +51 -0
- package/README.md +53 -30
- package/bridge-server/background-runs.js +91 -4
- package/bridge-server/chrome-launcher.js +42 -1
- package/bridge-server/cli-run.js +2 -0
- package/bridge-server/downloads.js +173 -0
- package/bridge-server/executor.js +1 -1
- package/bridge-server/extension-id.js +25 -1
- package/bridge-server/feedback.js +255 -0
- package/bridge-server/index.js +29 -2
- package/bridge-server/limits.js +27 -1
- package/bridge-server/mcp-server.js +219 -18
- package/bridge-server/native-host.js +5 -3
- package/bridge-server/panel.js +21 -3
- package/bridge-server/probe.js +2 -1
- package/bridge-server/resolvers.js +287 -0
- package/bridge-server/server.js +2 -1
- package/bridge-server/setup.js +184 -27
- package/bridge-server/tools.js +153 -4
- package/bridge-server/workflows.js +298 -11
- package/extension/background.js +1 -1
- package/extension/engine.js +1 -1
- package/extension/manifest.json +1 -1
- package/extension/sidepanel.css +1 -1
- package/extension/sidepanel.html +23 -2
- package/extension/sidepanel.js +1 -1
- package/package.json +2 -1
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* extension id. The private half lives outside the repository at
|
|
8
8
|
* %USERPROFILE%\.browserzap-extension\key.pem and is never committed.
|
|
9
9
|
*
|
|
10
|
+
* The Chrome Web Store signs its own build and does not accept a manifest
|
|
11
|
+
* `key`, so the store build may get a different id. Fill in
|
|
12
|
+
* STORE_EXTENSION_ID from the developer dashboard after the first upload; the
|
|
13
|
+
* native host then lets both builds in (docs/RELEASE.md §3b).
|
|
14
|
+
*
|
|
10
15
|
* Everything that has to name the extension — the native messaging host
|
|
11
16
|
* manifest's allowed_origins, `browserzap doctor`, the tests — reads the id
|
|
12
17
|
* from here. `extensionIdFromKey` derives it again from the manifest key, so a
|
|
@@ -22,6 +27,12 @@ const crypto = require('crypto');
|
|
|
22
27
|
/** chrome-extension://gienkaoaimdjmcjdejjanmbkpfakmkno/ */
|
|
23
28
|
const EXTENSION_ID = 'gienkaoaimdjmcjdejjanmbkpfakmkno';
|
|
24
29
|
|
|
30
|
+
/** The Chrome Web Store build's id (assigned on the first upload, 2026-10-29). */
|
|
31
|
+
const STORE_EXTENSION_ID = 'bbnmhaonicbmljhbahimpgfafbjoijpe';
|
|
32
|
+
|
|
33
|
+
/** Every build the native host lets in: the unpacked build first, then the store build. */
|
|
34
|
+
const ALLOWED_EXTENSION_IDS = Object.freeze([EXTENSION_ID, STORE_EXTENSION_ID].filter(Boolean));
|
|
35
|
+
|
|
25
36
|
/** Name Chrome uses for the native messaging host; also the registry key's leaf. */
|
|
26
37
|
const HOST_NAME = 'com.browserzap.bridge';
|
|
27
38
|
|
|
@@ -29,6 +40,11 @@ function extensionOrigin(id = EXTENSION_ID) {
|
|
|
29
40
|
return `chrome-extension://${id}/`;
|
|
30
41
|
}
|
|
31
42
|
|
|
43
|
+
/** allowed_origins for the native host manifest, one per allowed build. */
|
|
44
|
+
function allowedOrigins(ids = ALLOWED_EXTENSION_IDS) {
|
|
45
|
+
return ids.map((id) => extensionOrigin(id));
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* Chrome's extension id for a manifest `key` value (base64 SPKI DER).
|
|
34
50
|
* Returns null when the key is missing or is not a valid public key.
|
|
@@ -51,4 +67,12 @@ function extensionIdFromKey(key) {
|
|
|
51
67
|
.join('');
|
|
52
68
|
}
|
|
53
69
|
|
|
54
|
-
module.exports = {
|
|
70
|
+
module.exports = {
|
|
71
|
+
EXTENSION_ID,
|
|
72
|
+
STORE_EXTENSION_ID,
|
|
73
|
+
ALLOWED_EXTENSION_IDS,
|
|
74
|
+
HOST_NAME,
|
|
75
|
+
extensionOrigin,
|
|
76
|
+
allowedOrigins,
|
|
77
|
+
extensionIdFromKey,
|
|
78
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserZap — Comment mode batches kept on this device (docs/COMMENT_MODE.md).
|
|
3
|
+
*
|
|
4
|
+
* The person's comments arrive from the side panel in one `feedback_submit`
|
|
5
|
+
* panel request and are written here for the agent's `feedback_get`:
|
|
6
|
+
*
|
|
7
|
+
* <config dir>/feedback/<batch id>/batch.json comments, element details, component, page
|
|
8
|
+
* <config dir>/feedback/<batch id>/c<n>.png the element screenshot (sensitive fields covered)
|
|
9
|
+
* <config dir>/feedback/<batch id>/c<n>-a<k>.* images the person attached
|
|
10
|
+
*
|
|
11
|
+
* Folders are 0700 and files 0600. Nothing is uploaded anywhere. A batch is
|
|
12
|
+
* kept BROWSERZAP_FEEDBACK_DAYS days (default 7) and at most MAX_BATCHES
|
|
13
|
+
* batches; the oldest go first. Comments never contain form values: the
|
|
14
|
+
* extension sends labels and element details only.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
const { configDir } = require('./secret');
|
|
24
|
+
|
|
25
|
+
const DEFAULT_KEEP_DAYS = 7;
|
|
26
|
+
const MAX_BATCHES = 50;
|
|
27
|
+
const MAX_COMMENTS = 30;
|
|
28
|
+
const MAX_ATTACHMENTS = 3;
|
|
29
|
+
const MAX_IMAGE_CHARS = 6 * 1024 * 1024;
|
|
30
|
+
const ID_RE = /^fb-\d{14}-[0-9a-f]{6}$/;
|
|
31
|
+
const IMAGE_TYPES = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/gif': 'gif' };
|
|
32
|
+
const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
33
|
+
|
|
34
|
+
class FeedbackError extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.code = 'INVALID_ARGUMENT';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const feedbackDir = () => path.join(configDir(), 'feedback');
|
|
42
|
+
|
|
43
|
+
function keepDays() {
|
|
44
|
+
const raw = process.env.BROWSERZAP_FEEDBACK_DAYS;
|
|
45
|
+
if (raw === undefined || raw === '') return DEFAULT_KEEP_DAYS;
|
|
46
|
+
const n = Number(raw);
|
|
47
|
+
return Number.isFinite(n) && n >= 0 && n <= 365 ? n : DEFAULT_KEEP_DAYS;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const str = (v, max) => (typeof v === 'string' ? v.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '').slice(0, max) : null);
|
|
51
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? Math.round(v) : null);
|
|
52
|
+
|
|
53
|
+
function rectOf(r) {
|
|
54
|
+
if (!r || typeof r !== 'object') return null;
|
|
55
|
+
return { x: num(r.x), y: num(r.y), width: num(r.width), height: num(r.height) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Plain JSON of bounded size: target objects are small; anything deeper or larger is dropped. */
|
|
59
|
+
function smallJson(value, maxChars = 2000) {
|
|
60
|
+
try {
|
|
61
|
+
const text = JSON.stringify(value);
|
|
62
|
+
return text && text.length <= maxChars ? JSON.parse(text) : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function elementOf(info) {
|
|
69
|
+
const i = info && typeof info === 'object' ? info : {};
|
|
70
|
+
return {
|
|
71
|
+
role: str(i.role, 60),
|
|
72
|
+
name: str(i.name, 300),
|
|
73
|
+
target: smallJson(i.target),
|
|
74
|
+
targetUnique: i.targetUnique === true,
|
|
75
|
+
tag: str(i.tag, 40),
|
|
76
|
+
id: str(i.id, 200),
|
|
77
|
+
testId: str(i.testId, 200),
|
|
78
|
+
classes: Array.isArray(i.classes) ? i.classes.map((c) => str(c, 100)).filter(Boolean).slice(0, 6) : [],
|
|
79
|
+
domPath: str(i.domPath, 600),
|
|
80
|
+
text: str(i.text, 200),
|
|
81
|
+
sensitive: i.sensitive === true ? true : undefined,
|
|
82
|
+
rect: rectOf(i.rect),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function componentOf(c) {
|
|
87
|
+
if (!c || typeof c !== 'object') return null;
|
|
88
|
+
const source = c.source && typeof c.source === 'object' && typeof c.source.file === 'string'
|
|
89
|
+
? { file: str(c.source.file, 400), line: num(c.source.line), column: num(c.source.column) }
|
|
90
|
+
: null;
|
|
91
|
+
return {
|
|
92
|
+
framework: str(c.framework, 20),
|
|
93
|
+
component: str(c.component, 100),
|
|
94
|
+
chain: Array.isArray(c.chain) ? c.chain.map((n) => str(n, 100)).filter(Boolean).slice(0, 5) : [],
|
|
95
|
+
source,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pageOf(p) {
|
|
100
|
+
const page = p && typeof p === 'object' ? p : {};
|
|
101
|
+
const url = str(page.url, 2000);
|
|
102
|
+
if (!url || !/^https?:\/\//i.test(url)) throw new FeedbackError('Every comment needs the http(s) page it was made on.');
|
|
103
|
+
return {
|
|
104
|
+
url,
|
|
105
|
+
title: str(page.title, 200),
|
|
106
|
+
viewport: page.viewport && typeof page.viewport === 'object' ? { width: num(page.viewport.width), height: num(page.viewport.height) } : null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function imageData(data, what) {
|
|
111
|
+
if (typeof data !== 'string' || !data) return null;
|
|
112
|
+
if (data.length > MAX_IMAGE_CHARS) throw new FeedbackError(`${what} is too large.`);
|
|
113
|
+
if (!BASE64_RE.test(data)) throw new FeedbackError(`${what} is not a valid image.`);
|
|
114
|
+
return Buffer.from(data, 'base64');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function newId(now = new Date()) {
|
|
118
|
+
const p = (n, w = 2) => String(n).padStart(w, '0');
|
|
119
|
+
const stamp = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
|
120
|
+
return `fb-${stamp}-${crypto.randomBytes(3).toString('hex')}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function writePrivate(file, content) {
|
|
124
|
+
fs.writeFileSync(file, content, { mode: 0o600 });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Check and store one batch. Returns { id, dir, count }. Throws FeedbackError on bad input. */
|
|
128
|
+
function saveBatch(batch, { now = new Date() } = {}) {
|
|
129
|
+
const comments = batch && Array.isArray(batch.comments) ? batch.comments : null;
|
|
130
|
+
if (!comments || comments.length === 0) throw new FeedbackError('The batch has no comments.');
|
|
131
|
+
if (comments.length > MAX_COMMENTS) throw new FeedbackError(`A batch holds at most ${MAX_COMMENTS} comments.`);
|
|
132
|
+
|
|
133
|
+
const id = newId(now);
|
|
134
|
+
const dir = path.join(feedbackDir(), id);
|
|
135
|
+
const files = [];
|
|
136
|
+
const stored = comments.map((c, index) => {
|
|
137
|
+
if (!c || typeof c !== 'object') throw new FeedbackError(`Comment ${index + 1} is not an object.`);
|
|
138
|
+
const text = str(c.text, 2000)?.trim();
|
|
139
|
+
if (!text) throw new FeedbackError(`Comment ${index + 1} has no text.`);
|
|
140
|
+
const n = Number.isInteger(c.n) && c.n > 0 && c.n < 1000 ? c.n : index + 1;
|
|
141
|
+
const shot = imageData(c.screenshot, `The screenshot of comment ${n}`);
|
|
142
|
+
const attachments = Array.isArray(c.attachments) ? c.attachments.slice(0, MAX_ATTACHMENTS) : [];
|
|
143
|
+
const entry = {
|
|
144
|
+
n,
|
|
145
|
+
text,
|
|
146
|
+
page: pageOf(c.page),
|
|
147
|
+
element: elementOf(c.info),
|
|
148
|
+
component: componentOf(c.component),
|
|
149
|
+
screenshot: null,
|
|
150
|
+
attachments: [],
|
|
151
|
+
createdAt: num(c.createdAt),
|
|
152
|
+
};
|
|
153
|
+
if (shot) {
|
|
154
|
+
entry.screenshot = `c${n}.png`;
|
|
155
|
+
files.push([entry.screenshot, shot]);
|
|
156
|
+
}
|
|
157
|
+
attachments.forEach((a, k) => {
|
|
158
|
+
const ext = a && IMAGE_TYPES[a.mimeType];
|
|
159
|
+
const data = ext ? imageData(a.data, `Image ${k + 1} of comment ${n}`) : null;
|
|
160
|
+
if (!data) return;
|
|
161
|
+
const name = `c${n}-a${k + 1}.${ext}`;
|
|
162
|
+
entry.attachments.push(name);
|
|
163
|
+
files.push([name, data]);
|
|
164
|
+
});
|
|
165
|
+
return entry;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
169
|
+
for (const [name, data] of files) writePrivate(path.join(dir, name), data);
|
|
170
|
+
const record = { id, createdAt: now.toISOString(), delivered: false, deliveredAt: null, comments: stored };
|
|
171
|
+
writePrivate(path.join(dir, 'batch.json'), JSON.stringify(record, null, 2));
|
|
172
|
+
prune({ now: now.getTime() });
|
|
173
|
+
return { id, dir, count: stored.length };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readBatch(id) {
|
|
177
|
+
if (!ID_RE.test(id)) return null;
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(fs.readFileSync(path.join(feedbackDir(), id, 'batch.json'), 'utf8'));
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function batchIds() {
|
|
186
|
+
try {
|
|
187
|
+
return fs.readdirSync(feedbackDir()).filter((d) => ID_RE.test(d)).sort();
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Newest first: { id, createdAt, count, pages, delivered }. */
|
|
194
|
+
function list({ includeDelivered = false } = {}) {
|
|
195
|
+
return batchIds().reverse()
|
|
196
|
+
.map((id) => readBatch(id))
|
|
197
|
+
.filter((b) => b && (includeDelivered || !b.delivered))
|
|
198
|
+
.map((b) => ({
|
|
199
|
+
id: b.id,
|
|
200
|
+
createdAt: b.createdAt,
|
|
201
|
+
count: b.comments.length,
|
|
202
|
+
pages: [...new Set(b.comments.map((c) => c.page.url))].slice(0, 5),
|
|
203
|
+
delivered: b.delivered,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** One batch with absolute file paths, or the newest one not yet delivered. */
|
|
208
|
+
function get(id) {
|
|
209
|
+
const chosen = id ?? list()[0]?.id;
|
|
210
|
+
const batch = chosen ? readBatch(chosen) : null;
|
|
211
|
+
if (!batch) return null;
|
|
212
|
+
const dir = path.join(feedbackDir(), batch.id);
|
|
213
|
+
return {
|
|
214
|
+
...batch,
|
|
215
|
+
dir,
|
|
216
|
+
comments: batch.comments.map((c) => ({
|
|
217
|
+
...c,
|
|
218
|
+
screenshot: c.screenshot ? path.join(dir, c.screenshot) : null,
|
|
219
|
+
attachments: c.attachments.map((a) => path.join(dir, a)),
|
|
220
|
+
})),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function markDelivered(id, { now = new Date() } = {}) {
|
|
225
|
+
const batch = readBatch(id);
|
|
226
|
+
if (!batch || batch.delivered) return false;
|
|
227
|
+
batch.delivered = true;
|
|
228
|
+
batch.deliveredAt = now.toISOString();
|
|
229
|
+
writePrivate(path.join(feedbackDir(), id, 'batch.json'), JSON.stringify(batch, null, 2));
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function pendingCount() {
|
|
234
|
+
return list().length;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Delete batches older than the retention, and the oldest beyond MAX_BATCHES. Never throws. */
|
|
238
|
+
function prune({ now = Date.now(), days = keepDays() } = {}) {
|
|
239
|
+
const cutoff = now - days * 86_400_000;
|
|
240
|
+
const ids = batchIds();
|
|
241
|
+
let removed = 0;
|
|
242
|
+
ids.forEach((id, index) => {
|
|
243
|
+
const batch = readBatch(id);
|
|
244
|
+
const created = batch ? Date.parse(batch.createdAt) : 0;
|
|
245
|
+
if (created < cutoff || index < ids.length - MAX_BATCHES) {
|
|
246
|
+
try {
|
|
247
|
+
fs.rmSync(path.join(feedbackDir(), id), { recursive: true, force: true });
|
|
248
|
+
removed++;
|
|
249
|
+
} catch { /* in use: next time */ }
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
return removed;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = { saveBatch, list, get, markDelivered, pendingCount, prune, feedbackDir, FeedbackError, MAX_COMMENTS };
|
package/bridge-server/index.js
CHANGED
|
@@ -265,7 +265,16 @@ function uninstallExtension(purge, { register = true } = {}) {
|
|
|
265
265
|
*/
|
|
266
266
|
const command = args[0] && !args[0].startsWith('-') ? args[0] : null;
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
// Inside WSL there is no Chrome to pair with and no Windows registry: BrowserZap
|
|
269
|
+
// runs on the Windows side and the agent in WSL starts it through interop.
|
|
270
|
+
if (require('./setup').insideWsl() && !process.env.BROWSERZAP_ALLOW_LINUX) {
|
|
271
|
+
process.stderr.write('BrowserZap runs on Windows, next to Chrome. From WSL, start it through Windows:\n' +
|
|
272
|
+
' claude mcp add browserzap -- cmd.exe /c browserzap\n' +
|
|
273
|
+
'(install it on Windows first: npm install -g browserzap, then browserzap setup). See docs/WSL.md.\n');
|
|
274
|
+
process.exit(2);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const SUBCOMMANDS = new Set(['setup', 'doctor', 'uninstall', 'run', 'restart-bridge']);
|
|
269
278
|
|
|
270
279
|
if (command === 'setup') {
|
|
271
280
|
// From an installed package, copy the extension to a folder that outlives an
|
|
@@ -298,12 +307,29 @@ if (command === 'setup') {
|
|
|
298
307
|
`);
|
|
299
308
|
process.exit(1);
|
|
300
309
|
});
|
|
310
|
+
} else if (command === 'restart-bridge') {
|
|
311
|
+
// A bridge that will not pair (another secret, an old version) holds the
|
|
312
|
+
// port. Stop the node process holding it (never anything that is not node);
|
|
313
|
+
// the extension's native host starts the current one with this install's secret.
|
|
314
|
+
const { stopBridgeOnPort, isListening } = require('./probe');
|
|
315
|
+
const portIdx = args.indexOf('--port');
|
|
316
|
+
const port = portIdx !== -1 && args[portIdx + 1] ? parseInt(args[portIdx + 1], 10) : DEFAULT_PORT;
|
|
317
|
+
stopBridgeOnPort(port).then(async (pids) => {
|
|
318
|
+
const still = await isListening(port);
|
|
319
|
+
process.stdout.write(pids.length
|
|
320
|
+
? `Stopped the bridge on 127.0.0.1:${port} (pid ${pids.join(', ')}). Open the BrowserZap side panel (or press Reconnect) to start the current one.\n`
|
|
321
|
+
: `No BrowserZap bridge was holding 127.0.0.1:${port}.${still ? ' Something else listens there; it was left alone.' : ''}\n`);
|
|
322
|
+
process.exit(still ? 1 : 0);
|
|
323
|
+
}).catch((err) => {
|
|
324
|
+
process.stderr.write(`[browserzap] restart-bridge failed: ${err.message}\n`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
});
|
|
301
327
|
} else if (command === 'uninstall') {
|
|
302
328
|
uninstallExtension(args.includes('--purge'), { register: !args.includes('--no-registry') });
|
|
303
329
|
} else if (command === 'help') {
|
|
304
330
|
args.push('--help');
|
|
305
331
|
} else if (command) {
|
|
306
|
-
process.stderr.write(`Unknown command "${command}". Use: setup, doctor, run, uninstall, or --help.\n`);
|
|
332
|
+
process.stderr.write(`Unknown command "${command}". Use: setup, doctor, restart-bridge, run, uninstall, or --help.\n`);
|
|
307
333
|
process.exit(2);
|
|
308
334
|
}
|
|
309
335
|
|
|
@@ -468,6 +494,7 @@ Usage:
|
|
|
468
494
|
browserzap setup One-time setup: register the native messaging host and
|
|
469
495
|
optionally add the MCP server to detected clients
|
|
470
496
|
browserzap doctor Check the host manifest, registry, extension id and bridge
|
|
497
|
+
browserzap restart-bridge Replace a bridge that will not pair (another secret or version)
|
|
471
498
|
browserzap uninstall [--purge] Remove the host registration and installed extension copy
|
|
472
499
|
browserzap run <workflow> [--params <file.csv|file.json>] [--stop-on-error]
|
|
473
500
|
Run a saved workflow with no agent; one run per row (Pro for
|
package/bridge-server/limits.js
CHANGED
|
@@ -26,6 +26,13 @@ const LIMITS = Object.freeze({
|
|
|
26
26
|
SETTLE_QUIET_MS: 500,
|
|
27
27
|
/** How long that settle waits for a busy page (it costs nothing on a quiet one). */
|
|
28
28
|
SETTLE_MAX_MS: 5000,
|
|
29
|
+
/** A wait whose condition makes no progress for this long ends early: a page that is stuck does not get better. */
|
|
30
|
+
STALL_MS: 3000,
|
|
31
|
+
/**
|
|
32
|
+
* Before "Re-run failed" resumes at the step that failed, that step's target
|
|
33
|
+
* must be actionable within this. A bounded probe, not a pause (A6-09).
|
|
34
|
+
*/
|
|
35
|
+
RESUME_PROBE_MS: 3000,
|
|
29
36
|
/** Longest per-step pacing a person may set on a saved workflow step. */
|
|
30
37
|
MAX_STEP_PACING_MS: 30000,
|
|
31
38
|
/** A tab with no agent action for this long is detached, so Chrome's "debugging" bar goes away (D-35). */
|
|
@@ -60,6 +67,16 @@ const STATUS_DEADLINE_MS = 4_000;
|
|
|
60
67
|
*/
|
|
61
68
|
const RUN_DEADLINE_MS = 600_000;
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Resolver engine bounds (`bridge-server/resolvers.js`, 0.6.0, D-37).
|
|
72
|
+
* A step may spend at most MAX_STEP_RESOLVE_MS resolving blockers in place,
|
|
73
|
+
* across at most MAX_RESOLVER_ATTEMPTS attempts, and a reversible click with
|
|
74
|
+
* no effect at all is repeated at most MAX_INPLACE_CLICK_RETRIES times (AU-08).
|
|
75
|
+
*/
|
|
76
|
+
const MAX_STEP_RESOLVE_MS = 45_000;
|
|
77
|
+
const MAX_RESOLVER_ATTEMPTS = 2;
|
|
78
|
+
const MAX_INPLACE_CLICK_RETRIES = 2;
|
|
79
|
+
|
|
63
80
|
/**
|
|
64
81
|
* How long the caller waits for the extension to answer one call: the sum of
|
|
65
82
|
* every wait the call can do, plus a margin, and never less than
|
|
@@ -97,4 +114,13 @@ function callDeadlineMs(tool, args = {}) {
|
|
|
97
114
|
return Math.max(CALL_DEADLINE_MS, waits + DEADLINE_MARGIN_MS);
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
module.exports = {
|
|
117
|
+
module.exports = {
|
|
118
|
+
LIMITS,
|
|
119
|
+
CALL_DEADLINE_MS,
|
|
120
|
+
STATUS_DEADLINE_MS,
|
|
121
|
+
RUN_DEADLINE_MS,
|
|
122
|
+
MAX_STEP_RESOLVE_MS,
|
|
123
|
+
MAX_RESOLVER_ATTEMPTS,
|
|
124
|
+
MAX_INPLACE_CLICK_RETRIES,
|
|
125
|
+
callDeadlineMs,
|
|
126
|
+
};
|