machine-bridge-mcp 0.8.2 → 0.10.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 +35 -0
- package/README.md +58 -1
- package/SECURITY.md +22 -0
- package/browser-extension/manifest.json +33 -0
- package/browser-extension/page-automation.js +246 -0
- package/browser-extension/pairing.js +19 -0
- package/browser-extension/service-worker.js +406 -0
- package/docs/AGENT_CONTEXT.md +140 -0
- package/docs/ARCHITECTURE.md +33 -4
- package/docs/CLIENTS.md +8 -0
- package/docs/ENGINEERING.md +5 -1
- package/docs/LOCAL_AUTOMATION.md +111 -0
- package/docs/LOGGING.md +9 -1
- package/docs/OPERATIONS.md +30 -0
- package/docs/PRIVACY.md +2 -1
- package/docs/RELEASING.md +2 -2
- package/docs/TESTING.md +6 -4
- package/package.json +14 -7
- package/scripts/sync-worker-version.mjs +27 -12
- package/src/local/agent-context.mjs +862 -0
- package/src/local/app-automation.mjs +400 -0
- package/src/local/browser-bridge.mjs +757 -0
- package/src/local/cli.mjs +85 -0
- package/src/local/runtime.mjs +146 -4
- package/src/local/state.mjs +1 -1
- package/src/local/stdio.mjs +12 -7
- package/src/shared/server-metadata.json +4 -0
- package/src/shared/tool-catalog.json +1025 -0
- package/src/worker/index.ts +24 -4
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
let socket = null;
|
|
2
|
+
let reconnectTimer = null;
|
|
3
|
+
let reconnectAttempt = 0;
|
|
4
|
+
let keepaliveTimer = null;
|
|
5
|
+
const MAX_RESULT_BYTES = 7 * 1024 * 1024;
|
|
6
|
+
const activeRequests = new Map();
|
|
7
|
+
|
|
8
|
+
chrome.runtime.onInstalled.addListener(() => { ensureReconnectAlarm(); void connectFromStorage(); });
|
|
9
|
+
chrome.runtime.onStartup.addListener(() => { ensureReconnectAlarm(); void connectFromStorage(); });
|
|
10
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
11
|
+
if (alarm.name === "machine-bridge-reconnect") void connectFromStorage();
|
|
12
|
+
});
|
|
13
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
14
|
+
if (message?.type !== "pair") return false;
|
|
15
|
+
pairConfiguration(message.endpoint, message.token, { replace: false, senderUrl: sender.url || "" })
|
|
16
|
+
.then(sendResponse)
|
|
17
|
+
.catch(() => sendResponse({ ok: false }));
|
|
18
|
+
return true;
|
|
19
|
+
});
|
|
20
|
+
chrome.action.onClicked.addListener((tab) => void handleActionClick(tab));
|
|
21
|
+
|
|
22
|
+
async function handleActionClick(tab) {
|
|
23
|
+
if (tab?.id && /^http:\/\/127\.0\.0\.1:\d+\/pair(?:$|[?#])/.test(String(tab.url || ""))) {
|
|
24
|
+
await confirmRepairFromTab(tab);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const current = await chrome.storage.local.get(["endpoint"]);
|
|
28
|
+
const pairUrl = pairingUrlFromEndpoint(current.endpoint) || "http://127.0.0.1:39393/pair";
|
|
29
|
+
await chrome.tabs.create({ url: pairUrl });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pairingUrlFromEndpoint(endpoint) {
|
|
33
|
+
const match = /^ws:\/\/127\.0\.0\.1:(\d+)\/extension$/.exec(String(endpoint || ""));
|
|
34
|
+
return match ? `http://127.0.0.1:${match[1]}/pair` : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setConnectionState(state) {
|
|
38
|
+
const badge = state === "connected" ? "ON" : state === "connecting" ? "..." : state === "unconfigured" ? "?" : "!";
|
|
39
|
+
const title = state === "connected"
|
|
40
|
+
? "Machine Bridge Browser: connected"
|
|
41
|
+
: state === "connecting"
|
|
42
|
+
? "Machine Bridge Browser: connecting"
|
|
43
|
+
: state === "unconfigured"
|
|
44
|
+
? "Machine Bridge Browser: click to open pairing"
|
|
45
|
+
: "Machine Bridge Browser: disconnected; click to open pairing";
|
|
46
|
+
try { ignoreOptionalPromise(chrome.action.setBadgeText({ text: badge })); } catch {}
|
|
47
|
+
try { ignoreOptionalPromise(chrome.action.setTitle({ title })); } catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ignoreOptionalPromise(value) {
|
|
51
|
+
if (value && typeof value.catch === "function") value.catch(() => {});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function pairConfiguration(rawEndpoint, rawToken, { replace, senderUrl }) {
|
|
55
|
+
const endpoint = String(rawEndpoint || "");
|
|
56
|
+
const token = String(rawToken || "");
|
|
57
|
+
if (!/^http:\/\/127\.0\.0\.1:\d+\/pair(?:$|[?#])/.test(String(senderUrl || ""))) return { ok: false, error: "invalid_pairing_page" };
|
|
58
|
+
if (!/^ws:\/\/127\.0\.0\.1:\d+\/extension$/.test(endpoint) || !/^[A-Za-z0-9_-]{32,100}$/.test(token)) return { ok: false, error: "invalid_pairing_material" };
|
|
59
|
+
const current = await chrome.storage.local.get(["endpoint", "token"]);
|
|
60
|
+
const alreadyPaired = Boolean(current.endpoint && current.token);
|
|
61
|
+
if (alreadyPaired && !replace && (current.endpoint !== endpoint || current.token !== token)) {
|
|
62
|
+
return { ok: false, requires_manual_repair: true };
|
|
63
|
+
}
|
|
64
|
+
await chrome.storage.local.set({ endpoint, token });
|
|
65
|
+
setConnectionState("connecting");
|
|
66
|
+
connect(endpoint, token);
|
|
67
|
+
return { ok: true, replaced: alreadyPaired && (current.endpoint !== endpoint || current.token !== token) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function confirmRepairFromTab(tab) {
|
|
71
|
+
if (!tab?.id || !/^http:\/\/127\.0\.0\.1:\d+\/pair(?:$|[?#])/.test(String(tab.url || ""))) return;
|
|
72
|
+
try {
|
|
73
|
+
const [result] = await chrome.scripting.executeScript({
|
|
74
|
+
target: { tabId: tab.id },
|
|
75
|
+
func: () => ({
|
|
76
|
+
endpoint: `ws://127.0.0.1:${document.querySelector('meta[name="machine-bridge-browser-port"]')?.content || ""}/extension`,
|
|
77
|
+
token: document.querySelector('meta[name="machine-bridge-browser-token"]')?.content || "",
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
const paired = await pairConfiguration(result?.result?.endpoint, result?.result?.token, { replace: true, senderUrl: tab.url });
|
|
81
|
+
await chrome.scripting.executeScript({
|
|
82
|
+
target: { tabId: tab.id },
|
|
83
|
+
func: (ok) => {
|
|
84
|
+
const status = document.getElementById("status");
|
|
85
|
+
if (status) status.textContent = ok ? "Paired. You may close this tab." : "Pairing failed.";
|
|
86
|
+
},
|
|
87
|
+
args: [paired.ok === true],
|
|
88
|
+
});
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ensureReconnectAlarm();
|
|
93
|
+
void connectFromStorage();
|
|
94
|
+
|
|
95
|
+
function ensureReconnectAlarm() {
|
|
96
|
+
chrome.alarms.create("machine-bridge-reconnect", { periodInMinutes: 1 });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function connectFromStorage() {
|
|
100
|
+
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
|
|
101
|
+
const value = await chrome.storage.local.get(["endpoint", "token"]);
|
|
102
|
+
if (value.endpoint && value.token) {
|
|
103
|
+
setConnectionState("connecting");
|
|
104
|
+
connect(value.endpoint, value.token);
|
|
105
|
+
} else {
|
|
106
|
+
setConnectionState("unconfigured");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function connect(endpoint, token) {
|
|
111
|
+
clearTimeout(reconnectTimer);
|
|
112
|
+
reconnectTimer = null;
|
|
113
|
+
if (socket) {
|
|
114
|
+
try { socket.close(); } catch {}
|
|
115
|
+
}
|
|
116
|
+
const ws = new WebSocket(endpoint, [`mbm.${token}`]);
|
|
117
|
+
socket = ws;
|
|
118
|
+
ws.onopen = () => {
|
|
119
|
+
if (socket !== ws) {
|
|
120
|
+
try { ws.close(); } catch {}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
reconnectAttempt = 0;
|
|
124
|
+
setConnectionState("connected");
|
|
125
|
+
clearInterval(keepaliveTimer);
|
|
126
|
+
keepaliveTimer = setInterval(() => { if (socket === ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" })); }, 20000);
|
|
127
|
+
};
|
|
128
|
+
ws.onmessage = (event) => void handleMessage(ws, event.data);
|
|
129
|
+
ws.onerror = () => {};
|
|
130
|
+
ws.onclose = () => {
|
|
131
|
+
if (socket !== ws) return;
|
|
132
|
+
clearInterval(keepaliveTimer);
|
|
133
|
+
keepaliveTimer = null;
|
|
134
|
+
socket = null;
|
|
135
|
+
setConnectionState("disconnected");
|
|
136
|
+
scheduleReconnect(endpoint, token);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function scheduleReconnect(endpoint, token) {
|
|
141
|
+
clearTimeout(reconnectTimer);
|
|
142
|
+
const delay = Math.min(30000, 1000 * 2 ** Math.min(reconnectAttempt++, 5));
|
|
143
|
+
reconnectTimer = setTimeout(() => connect(endpoint, token), delay);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function handleMessage(ws, raw) {
|
|
147
|
+
let message;
|
|
148
|
+
try { message = JSON.parse(String(raw)); } catch { return; }
|
|
149
|
+
if (message?.type === "cancel" && typeof message.id === "string") {
|
|
150
|
+
const state = activeRequests.get(message.id);
|
|
151
|
+
if (state) state.cancelled = true;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (message?.type !== "request" || typeof message.id !== "string" || typeof message.method !== "string") return;
|
|
155
|
+
const state = { cancelled: false };
|
|
156
|
+
activeRequests.set(message.id, state);
|
|
157
|
+
try {
|
|
158
|
+
throwIfCancelled(state);
|
|
159
|
+
const result = await dispatch(message.method, message.params || {});
|
|
160
|
+
throwIfCancelled(state);
|
|
161
|
+
sendResponse(ws, message.id, true, result);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!state.cancelled) sendResponse(ws, message.id, false, null, String(error?.message || error).slice(0, 2000));
|
|
164
|
+
} finally {
|
|
165
|
+
activeRequests.delete(message.id);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function throwIfCancelled(state) {
|
|
170
|
+
if (state.cancelled) throw new Error("browser request cancelled");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sendResponse(ws, id, ok, result, error = "") {
|
|
174
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
175
|
+
let payload = JSON.stringify({ type: "response", id, ok, ...(ok ? { result } : { error }) });
|
|
176
|
+
if (new TextEncoder().encode(payload).byteLength > MAX_RESULT_BYTES) {
|
|
177
|
+
payload = JSON.stringify({ type: "response", id, ok: false, error: "browser result exceeds maximum size" });
|
|
178
|
+
}
|
|
179
|
+
try { ws.send(payload); } catch {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function dispatch(method, params) {
|
|
183
|
+
if (method === "list_tabs") return listTabs(params);
|
|
184
|
+
if (method === "get_source") return getSource(params);
|
|
185
|
+
if (method === "inspect_page") return inspectPage(params);
|
|
186
|
+
if (method === "action") return browserAction(params);
|
|
187
|
+
if (method === "fill_form") return fillForm(params);
|
|
188
|
+
if (method === "upload_files") return uploadFiles(params);
|
|
189
|
+
if (method === "screenshot") return screenshot(params);
|
|
190
|
+
throw new Error(`unknown browser method: ${method}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function listTabs(params) {
|
|
194
|
+
const query = params.currentWindow ? { currentWindow: true } : {};
|
|
195
|
+
const tabs = await chrome.tabs.query(query);
|
|
196
|
+
return {
|
|
197
|
+
tabs: tabs
|
|
198
|
+
.filter((tab) => params.includePinned !== false || !tab.pinned)
|
|
199
|
+
.map((tab) => ({
|
|
200
|
+
id: tab.id,
|
|
201
|
+
window_id: tab.windowId,
|
|
202
|
+
active: tab.active,
|
|
203
|
+
pinned: tab.pinned,
|
|
204
|
+
audible: tab.audible,
|
|
205
|
+
discarded: tab.discarded,
|
|
206
|
+
status: tab.status,
|
|
207
|
+
title: tab.title || "",
|
|
208
|
+
url: tab.url || "",
|
|
209
|
+
})),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function scriptTarget(tabId, frameId, allFrames) {
|
|
214
|
+
if (Number.isInteger(frameId)) return { tabId, frameIds: [frameId] };
|
|
215
|
+
if (allFrames) return { tabId, allFrames: true };
|
|
216
|
+
return { tabId };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function resolveTab(tabId) {
|
|
220
|
+
if (Number.isInteger(tabId)) return chrome.tabs.get(tabId);
|
|
221
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
222
|
+
if (!tab?.id) throw new Error("no active browser tab");
|
|
223
|
+
return tab;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function getSource(params) {
|
|
227
|
+
const tab = await resolveTab(params.tabId);
|
|
228
|
+
assertPageTab(tab);
|
|
229
|
+
const maxBytes = Number(params.maxBytes) || 1024 * 1024;
|
|
230
|
+
const executions = await chrome.scripting.executeScript({
|
|
231
|
+
target: scriptTarget(tab.id, params.frameId, params.allFrames === true),
|
|
232
|
+
func: (limit) => {
|
|
233
|
+
const doctype = document.doctype ? `<!DOCTYPE ${document.doctype.name}>\n` : "";
|
|
234
|
+
const source = `${doctype}${document.documentElement?.outerHTML || ""}`;
|
|
235
|
+
const encoder = new TextEncoder();
|
|
236
|
+
const bytes = encoder.encode(source);
|
|
237
|
+
if (bytes.byteLength <= limit) return { source, bytes: bytes.byteLength, truncated: false, url: location.href };
|
|
238
|
+
const decoder = new TextDecoder();
|
|
239
|
+
return { source: decoder.decode(bytes.slice(0, limit)), bytes: bytes.byteLength, truncated: true, url: location.href };
|
|
240
|
+
},
|
|
241
|
+
args: [maxBytes],
|
|
242
|
+
});
|
|
243
|
+
return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", frames: executions.map((item) => ({ frame_id: item.frameId, ...item.result })) };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function inspectPage(params) {
|
|
247
|
+
const tab = await resolveTab(params.tabId);
|
|
248
|
+
assertPageTab(tab);
|
|
249
|
+
const executions = await executePageAutomation(
|
|
250
|
+
scriptTarget(tab.id, params.frameId, params.allFrames === true),
|
|
251
|
+
"inspect",
|
|
252
|
+
{ maxElements: Number(params.maxElements) || 300, includeValues: params.includeValues === true },
|
|
253
|
+
);
|
|
254
|
+
return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", frames: executions.map((item) => ({ frame_id: item.frameId, ...item.result })) };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function browserAction(params) {
|
|
258
|
+
const tab = await resolveTab(params.tabId);
|
|
259
|
+
if (params.action === "navigate") {
|
|
260
|
+
if (!params.url) throw new Error("navigate requires url");
|
|
261
|
+
const waiter = beginTabWait(tab.id, params.waitFor);
|
|
262
|
+
try {
|
|
263
|
+
const updated = await chrome.tabs.update(tab.id, { url: params.url });
|
|
264
|
+
await waiter.promise;
|
|
265
|
+
return publicTab(await chrome.tabs.get(updated.id));
|
|
266
|
+
} catch (error) {
|
|
267
|
+
waiter.cancel();
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (params.action === "reload") {
|
|
272
|
+
const waiter = beginTabWait(tab.id, params.waitFor);
|
|
273
|
+
try {
|
|
274
|
+
await chrome.tabs.reload(tab.id);
|
|
275
|
+
await waiter.promise;
|
|
276
|
+
return publicTab(await chrome.tabs.get(tab.id));
|
|
277
|
+
} catch (error) {
|
|
278
|
+
waiter.cancel();
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (params.action === "back" || params.action === "forward") {
|
|
283
|
+
const waiter = beginTabWait(tab.id, params.waitFor);
|
|
284
|
+
try {
|
|
285
|
+
if (params.action === "back") await chrome.tabs.goBack(tab.id);
|
|
286
|
+
else await chrome.tabs.goForward(tab.id);
|
|
287
|
+
await waiter.promise;
|
|
288
|
+
return publicTab(await chrome.tabs.get(tab.id));
|
|
289
|
+
} catch (error) {
|
|
290
|
+
waiter.cancel();
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
assertPageTab(tab);
|
|
295
|
+
const waiter = beginTabWait(tab.id, params.waitFor);
|
|
296
|
+
let execution;
|
|
297
|
+
try {
|
|
298
|
+
[execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "action", params);
|
|
299
|
+
await waiter.promise;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
waiter.cancel();
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", ...execution.result };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function fillForm(params) {
|
|
308
|
+
const tab = await resolveTab(params.tabId);
|
|
309
|
+
assertPageTab(tab);
|
|
310
|
+
const waiter = beginTabWait(tab.id, params.waitFor);
|
|
311
|
+
let execution;
|
|
312
|
+
try {
|
|
313
|
+
[execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "fillForm", params);
|
|
314
|
+
await waiter.promise;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
waiter.cancel();
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", ...execution.result };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function uploadFiles(params) {
|
|
323
|
+
const tab = await resolveTab(params.tabId);
|
|
324
|
+
assertPageTab(tab);
|
|
325
|
+
const [execution] = await executePageAutomation(scriptTarget(tab.id, params.frameId, false), "uploadFiles", params);
|
|
326
|
+
return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", ...execution.result };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function screenshot(params) {
|
|
330
|
+
const tab = await resolveTab(params.tabId);
|
|
331
|
+
await chrome.tabs.update(tab.id, { active: true });
|
|
332
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
333
|
+
const data = await chrome.tabs.captureVisibleTab(tab.windowId, {
|
|
334
|
+
format: params.format === "jpeg" ? "jpeg" : "png",
|
|
335
|
+
quality: Number(params.quality) || 90,
|
|
336
|
+
});
|
|
337
|
+
return { tab_id: tab.id, title: tab.title || "", url: tab.url || "", data };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function publicTab(tab) {
|
|
341
|
+
return { tab_id: tab.id, window_id: tab.windowId, title: tab.title || "", url: tab.url || "", status: tab.status || "" };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function assertPageTab(tab) {
|
|
345
|
+
const url = String(tab.url || "");
|
|
346
|
+
if (!/^(https?|file):/i.test(url)) throw new Error("this page cannot be scripted by a browser extension");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function beginTabWait(tabId, mode) {
|
|
350
|
+
if (!mode || mode === "none") return { promise: Promise.resolve(), cancel() {} };
|
|
351
|
+
let settled = false;
|
|
352
|
+
let navigationStarted = false;
|
|
353
|
+
let pollTimer = null;
|
|
354
|
+
let timeout = null;
|
|
355
|
+
let resolveWait;
|
|
356
|
+
let rejectWait;
|
|
357
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
358
|
+
resolveWait = resolvePromise;
|
|
359
|
+
rejectWait = rejectPromise;
|
|
360
|
+
});
|
|
361
|
+
const cleanup = () => {
|
|
362
|
+
clearTimeout(timeout);
|
|
363
|
+
clearTimeout(pollTimer);
|
|
364
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
365
|
+
};
|
|
366
|
+
const settle = (error = null) => {
|
|
367
|
+
if (settled) return;
|
|
368
|
+
settled = true;
|
|
369
|
+
cleanup();
|
|
370
|
+
if (error) rejectWait(error); else resolveWait();
|
|
371
|
+
};
|
|
372
|
+
const pollReadyState = async () => {
|
|
373
|
+
if (settled || !navigationStarted || mode !== "domcontentloaded") return;
|
|
374
|
+
try {
|
|
375
|
+
const [execution] = await chrome.scripting.executeScript({ target: { tabId }, func: () => document.readyState });
|
|
376
|
+
if (["interactive", "complete"].includes(execution?.result)) {
|
|
377
|
+
settle();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
} catch {}
|
|
381
|
+
pollTimer = setTimeout(pollReadyState, 100);
|
|
382
|
+
};
|
|
383
|
+
const listener = (updatedId, changeInfo, tab) => {
|
|
384
|
+
if (updatedId !== tabId || settled) return;
|
|
385
|
+
if (changeInfo.status === "loading" || typeof changeInfo.url === "string") navigationStarted = true;
|
|
386
|
+
if (!navigationStarted) return;
|
|
387
|
+
if (mode === "complete" && (changeInfo.status === "complete" || (typeof changeInfo.url === "string" && tab.status === "complete"))) settle();
|
|
388
|
+
if (mode === "domcontentloaded") void pollReadyState();
|
|
389
|
+
};
|
|
390
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
391
|
+
timeout = setTimeout(() => settle(new Error("browser navigation wait timed out")), 30000);
|
|
392
|
+
return { promise, cancel: () => settle() };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function executePageAutomation(target, method, params) {
|
|
396
|
+
return chrome.scripting.executeScript({ target, files: ["page-automation.js"] })
|
|
397
|
+
.then(() => chrome.scripting.executeScript({
|
|
398
|
+
target,
|
|
399
|
+
func: (operation, payload) => {
|
|
400
|
+
const api = globalThis.__machineBridgePageAutomation;
|
|
401
|
+
if (!api || typeof api[operation] !== "function") throw new Error("page automation module is unavailable");
|
|
402
|
+
return api[operation](payload);
|
|
403
|
+
},
|
|
404
|
+
args: [method, params],
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Session instructions, skills, commands, and capability discovery
|
|
2
|
+
|
|
3
|
+
Machine Bridge provides a static MCP bootstrap surface for Codex-style instructions and filesystem skills. The catalog remains stable while local instructions, skills, commands, applications, and browser state are discovered at call time.
|
|
4
|
+
|
|
5
|
+
This approximates a local coding agent without pretending that the MCP server owns the model loop. The host can filter tools, require confirmation, truncate context, or decline calls before they reach Machine Bridge.
|
|
6
|
+
|
|
7
|
+
## MCP tools
|
|
8
|
+
|
|
9
|
+
- `session_bootstrap` returns the current global/session instruction text and refresh fingerprint.
|
|
10
|
+
- `agent_context` returns the complete target-specific instruction chain, skill summaries, and command registry.
|
|
11
|
+
- `resolve_task_capabilities` rescans and ranks skills/commands for the current task, optionally loading the best skill; the runtime also adds application and browser capability metadata.
|
|
12
|
+
- `list_local_skills` searches discovered `SKILL.md` bundles.
|
|
13
|
+
- `load_local_skill` returns one skill entrypoint and bounded file inventory without execution.
|
|
14
|
+
- `list_local_commands` returns effective registered commands.
|
|
15
|
+
- `run_local_command` executes a registered direct-argv command when policy permits.
|
|
16
|
+
|
|
17
|
+
Both stdio and remote Worker connection initialization attempt `session_bootstrap`. Its instruction text is appended to the MCP `initialize` result. Because a host may reuse one MCP connection across conversations, the explicit tool and per-task `resolve_task_capabilities` call remain necessary to refresh and reapply instructions reliably.
|
|
18
|
+
|
|
19
|
+
## Global `model_instructions_file`
|
|
20
|
+
|
|
21
|
+
The user-level configuration is:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
~/.config/machine-bridge-mcp/agent.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"version": 1,
|
|
32
|
+
"model_instructions_file": "~/.config/machine-bridge-mcp/MODEL.md"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The file is loaded before repository guidance for every session and every target path. It must be a non-empty, regular, non-symbolic-link UTF-8 file and is bounded by the hard instruction-file limit. Relative values are resolved from the user's home directory.
|
|
37
|
+
|
|
38
|
+
`model_instructions_file` is global-only. A project `.machine-bridge/agent.json` cannot set or override it. It is read even under workspace-confined profiles because the user explicitly designated it as session configuration. Under those profiles, other user-manifest fields that would widen filesystem/skill/command scope are ignored; project instruction and command policy remains confined.
|
|
39
|
+
|
|
40
|
+
In remote mode, the selected instruction text necessarily traverses the user's Cloudflare Worker and the authorized MCP host as part of initialization. Do not put credentials or private data in an instruction file.
|
|
41
|
+
|
|
42
|
+
## Repository instruction precedence
|
|
43
|
+
|
|
44
|
+
For a target path, Machine Bridge chooses the nearest Git ancestor as scope root, falling back to the configured workspace or target directory. The default candidate priority in each directory is:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
[
|
|
48
|
+
"AGENTS.override.md",
|
|
49
|
+
"AGENTS.md"
|
|
50
|
+
]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Order:
|
|
54
|
+
|
|
55
|
+
1. `model_instructions_file`, when configured;
|
|
56
|
+
2. under unrestricted policy, the first non-empty candidate in `CODEX_HOME` or `~/.codex`;
|
|
57
|
+
3. project scope root through the target directory;
|
|
58
|
+
4. in each directory, apply `.machine-bridge/agent.json`, then select its first non-empty candidate.
|
|
59
|
+
|
|
60
|
+
Only one candidate contributes per directory. Deeper files have higher precedence. Empty files are skipped. The repository/global candidate budget defaults to 32 KiB and can be configured up to the hard 2 MiB ceiling. The separately designated model-instructions file retains its own file-size ceiling.
|
|
61
|
+
|
|
62
|
+
## Project manifest
|
|
63
|
+
|
|
64
|
+
A project manifest lives at `.machine-bridge/agent.json`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"version": 1,
|
|
69
|
+
"instruction_files": [
|
|
70
|
+
"PROJECT.override.md",
|
|
71
|
+
"AGENTS.override.md",
|
|
72
|
+
"AGENTS.md"
|
|
73
|
+
],
|
|
74
|
+
"instruction_max_bytes": 65536,
|
|
75
|
+
"skill_roots": [
|
|
76
|
+
".agents/skills",
|
|
77
|
+
".codex/skills"
|
|
78
|
+
],
|
|
79
|
+
"commands": {
|
|
80
|
+
"check": {
|
|
81
|
+
"description": "Run repository validation.",
|
|
82
|
+
"argv": ["npm", "run", "check"],
|
|
83
|
+
"cwd": ".",
|
|
84
|
+
"timeout_seconds": 600,
|
|
85
|
+
"allow_extra_args": false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Supported project fields are `version`, `instruction_files`, `instruction_max_bytes`, `skill_roots`, and `commands`. Unknown fields fail closed. Deeper manifests replace inherited instruction/skill settings, override commands by name, and can delete a command with `null`.
|
|
92
|
+
|
|
93
|
+
## Skill discovery and live refresh
|
|
94
|
+
|
|
95
|
+
Without explicit `skill_roots`, discovery scans:
|
|
96
|
+
|
|
97
|
+
- target-to-root `.agents/skills` directories;
|
|
98
|
+
- under unrestricted policy, `~/.agents/skills`;
|
|
99
|
+
- under unrestricted policy on Unix-like systems, `/etc/codex/skills`.
|
|
100
|
+
|
|
101
|
+
A skill directory contains `SKILL.md` or `skill.md` with simple front matter:
|
|
102
|
+
|
|
103
|
+
```markdown
|
|
104
|
+
---
|
|
105
|
+
name: release-review
|
|
106
|
+
description: Review a release without publishing it.
|
|
107
|
+
---
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The entrypoint requires non-empty `name` and `description`. Invalid bundles are skipped with bounded warnings. Symlinked skill directories are followed after canonical policy validation; symbolic-link entrypoint files are rejected. Traversal, depth, entries, summaries, content, and inventory are bounded.
|
|
111
|
+
|
|
112
|
+
No persistent skill index is trusted as authoritative. `agent_context`, `list_local_skills`, and `resolve_task_capabilities` rescan the effective roots. A refresh fingerprint changes when instruction hashes, skill hashes, command definitions, or relevant configuration changes. Newly created or edited skills are therefore visible without restarting the daemon or changing the MCP tool catalog.
|
|
113
|
+
|
|
114
|
+
## Progressive disclosure and task selection
|
|
115
|
+
|
|
116
|
+
`agent_context` returns bounded skill metadata. `load_local_skill` returns full instructions only for one selected bundle. `resolve_task_capabilities` tokenizes the current task, ranks skill names/descriptions and command names/descriptions/argv, returns matches with scores, and loads the leading skill only when its relevance threshold is met.
|
|
117
|
+
|
|
118
|
+
This ranking is deterministic local assistance, not semantic certainty. The model must still evaluate whether the selected skill applies. Machine Bridge does not execute skill scripts implicitly and does not fabricate a dynamically named MCP tool per skill.
|
|
119
|
+
|
|
120
|
+
## Registered commands
|
|
121
|
+
|
|
122
|
+
`run_local_command` uses direct argv spawning rather than a shell. The manifest controls working directory, timeout ceiling, and whether caller arguments are accepted. A caller may reduce but not increase the timeout.
|
|
123
|
+
|
|
124
|
+
Registered commands are workflow aliases, not an approval boundary or sandbox. Package scripts, interpreters, compilers, and executables retain local-user authority. Use `review` or `edit`, or external VM/container isolation, for untrusted content.
|
|
125
|
+
|
|
126
|
+
## Recommended host workflow
|
|
127
|
+
|
|
128
|
+
1. consume MCP initialization instructions;
|
|
129
|
+
2. call `resolve_task_capabilities` with the complete user task and target path;
|
|
130
|
+
3. apply returned global/project instructions;
|
|
131
|
+
4. follow the selected skill only after checking relevance;
|
|
132
|
+
5. prefer registered commands for stable workflows;
|
|
133
|
+
6. use structured application/browser tools where applicable;
|
|
134
|
+
7. inspect before mutation or submission and report operations performed.
|
|
135
|
+
|
|
136
|
+
Machine Bridge can automatically discover, refresh, rank, and load capabilities. Actual invocation remains a host/model decision. This boundary cannot be removed by server architecture alone.
|
|
137
|
+
|
|
138
|
+
## Security and failure behavior
|
|
139
|
+
|
|
140
|
+
Instruction and skill text is untrusted operational content and may contain prompt injection or destructive guidance. The bridge exposes it but does not certify it. Paths, files, roots, counts, byte budgets, argv, timeouts, and result sizes are bounded. Escaping paths, ambiguous skill names, malformed metadata, invalid configuration, and missing commands fail explicitly or produce bounded warnings.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -32,12 +32,36 @@ A canonical workspace receives an independent profile, Worker name, secret set,
|
|
|
32
32
|
- process-session buffers and stdin lifecycle;
|
|
33
33
|
- layered fixed runtime diagnostics;
|
|
34
34
|
- local resource aliases and detached managed-job coordination;
|
|
35
|
+
- session/bootstrap instruction discovery, live capability ranking, and registered-command execution coordination;
|
|
36
|
+
- structured local application and existing-profile browser automation coordination;
|
|
35
37
|
- mutation serialization;
|
|
36
38
|
- child-process tracking and cancellation;
|
|
37
39
|
- output, traversal, concurrency, and time limits.
|
|
38
40
|
|
|
39
41
|
`RelayConnection` owns remote WebSocket transport, authenticated `hello_ack` readiness, heartbeat liveness, reconnect backoff, and outage logging. Stdio mode invokes `LocalRuntime` directly without that adapter.
|
|
40
42
|
|
|
43
|
+
### Agent context and capability resolver
|
|
44
|
+
|
|
45
|
+
`AgentContextManager` discovers the nearest Git/workspace scope, applies the user configuration and hierarchical `.machine-bridge/agent.json` files, selects global/root-to-target instructions, discovers bounded filesystem skills, and resolves registered commands. A global `model_instructions_file` is a separate user-designated session source and cannot be overridden by a project.
|
|
46
|
+
|
|
47
|
+
`session_bootstrap` is requested during both stdio and remote MCP initialization. The Worker delegates this read to the connected daemon with a short bounded timeout; failure falls back to static server instructions rather than blocking initialization indefinitely. `resolve_task_capabilities` performs a fresh deterministic scan and ranks skill/command metadata for the current task. Application and browser capability metadata is added by `LocalRuntime`.
|
|
48
|
+
|
|
49
|
+
The MCP catalog remains static: local skills and commands do not become dynamically named tools. This avoids stale host catalog caches and keeps Worker/stdio schema parity. Progressive disclosure separates discovery, instruction loading, and execution authority. A refresh fingerprint is descriptive rather than a cache-validity guarantee.
|
|
50
|
+
|
|
51
|
+
See [Session instructions, skills, commands, and capability discovery](AGENT_CONTEXT.md).
|
|
52
|
+
|
|
53
|
+
### Application automation manager
|
|
54
|
+
|
|
55
|
+
`AppAutomationManager` owns installed-application discovery, OS launching, and structured UI automation. macOS inspection/actions execute fixed JXA implementation code through `osascript` and expose only typed selectors/actions. The caller cannot provide script source. OS Accessibility/TCC remains an independent boundary.
|
|
56
|
+
|
|
57
|
+
### Browser extension and machine broker
|
|
58
|
+
|
|
59
|
+
`BrowserBridgeManager` owns a loopback HTTP/WebSocket broker. The first runtime for the machine-level state root becomes broker owner; additional workspaces and stdio runtimes authenticate to `/runtime` and proxy through the same extension socket. This preserves one extension pairing while allowing multiple local MCP runtimes.
|
|
60
|
+
|
|
61
|
+
The packaged Manifest V3 extension runs in the user's existing Chromium profile. Its service worker is transport/orchestration only; a fixed packaged `page-automation.js` module is injected into selected frames to expose DOM operations in the correct execution world. It supports accessible frames, open Shadow DOM roots, structured actions, multi-field forms, resource-backed file inputs, and screenshots. The extension never receives arbitrary caller-provided code. The broker validates loopback hostnames, extension origin, bearer subprotocols, message sizes, concurrency, and timeouts. Pairing material is owner-only and omitted from MCP/log output.
|
|
62
|
+
|
|
63
|
+
See [Local application and browser automation](LOCAL_AUTOMATION.md).
|
|
64
|
+
|
|
41
65
|
### Managed job runner
|
|
42
66
|
|
|
43
67
|
`ManagedJobManager` persists bounded per-workspace job envelopes below the owner-only profile directory. `start_job` validates the complete plan, snapshots referenced resource metadata/hashes, writes an owner-only plan/status, and launches `job-runner.mjs` as a detached process with runner-level logs redirected to owner-only files. `stage_job` performs the same acceptance validation but writes a non-running `staged` envelope; only local `job approve` transitions it to queued and launches the runner.
|
|
@@ -96,6 +120,9 @@ flowchart LR
|
|
|
96
120
|
L[Local MCP client] -->|stdio JSON-RPC| R
|
|
97
121
|
R -->|canonical workspace tools| F[Selected workspace]
|
|
98
122
|
R -->|optional direct/shell processes| P[Local user / OS / network]
|
|
123
|
+
R -->|structured Accessibility actions| A[Local applications]
|
|
124
|
+
R -->|authenticated loopback broker| B[Existing-profile browser extension]
|
|
125
|
+
B -->|DOM and visible UI authority| WB[Web pages and browser tabs]
|
|
99
126
|
R -->|durable accepted plan| J[Detached managed-job runner]
|
|
100
127
|
J -->|private copies| LR[Local resource files]
|
|
101
128
|
J -->|argv/stdin/env| P
|
|
@@ -115,7 +142,7 @@ Remote OAuth determines which client may call tools. Local stdio access relies o
|
|
|
115
142
|
4. The user verifies client name and redirect URI and enters the connection password.
|
|
116
143
|
5. The Worker creates a five-minute code bound to client, redirect, resource, scope, and PKCE challenge.
|
|
117
144
|
6. A valid verifier exchanges the one-time code for an expiring bearer token; only its hash is stored.
|
|
118
|
-
7. The MCP client initializes and negotiates a supported protocol version.
|
|
145
|
+
7. The MCP client initializes and negotiates a supported protocol version. When the daemon advertises `session_bootstrap`, the Worker requests bounded local instructions and appends them to the initialization result; failure degrades to static instructions.
|
|
119
146
|
8. `tools/list` is derived from the active daemon handshake; without a daemon, only `server_info` is advertised.
|
|
120
147
|
9. `tools/call` receives a random relay call ID and is bound to the current socket and authenticated client request key.
|
|
121
148
|
10. The runtime validates policy and arguments, executes the tool, and returns a bounded result.
|
|
@@ -128,7 +155,7 @@ Duplicate in-flight JSON-RPC IDs for the same access token are rejected so cance
|
|
|
128
155
|
## Stdio request lifecycle
|
|
129
156
|
|
|
130
157
|
1. The local client launches `machine-mcp stdio` with a workspace and profile.
|
|
131
|
-
2. The server negotiates one of the supported MCP versions.
|
|
158
|
+
2. The server negotiates one of the supported MCP versions and appends bounded local `session_bootstrap` instructions when available.
|
|
132
159
|
3. Tool discovery is generated from the same catalog and policy used by remote mode.
|
|
133
160
|
4. Each call receives an internal random call ID used only for cancellation and process tracking.
|
|
134
161
|
5. Input is parsed as incrementally bounded newline-delimited JSON-RPC, so an oversized line is discarded before unbounded buffering and the next line can still be processed. Results are emitted as JSON-RPC on stdout; logs remain on stderr.
|
|
@@ -185,7 +212,7 @@ The Worker also treats a new connection as a bounded candidate until it authenti
|
|
|
185
212
|
|
|
186
213
|
## Persistence
|
|
187
214
|
|
|
188
|
-
Local state and global config are owner-only, versioned, size-bounded, and written through temporary files, flushes, and atomic rename. State, managed-job manager, and detached runner commits share a bounded retrying atomic-replace primitive for transient Windows `EPERM`, `EACCES`, `EBUSY`, and `ENOTEMPTY` sharing failures. Managed-job manager and detached runner use one shared no-follow, size-bounded regular-file reader; transition and recovery locks use one PID-lock primitive with bounded stale-lock reclamation and optional runner handoff. Malformed or oversized state becomes a bounded-count `.corrupt-*` backup. Resource paths are omitted from redacted status output. Custom roots are adopted only when empty or recognizable as legacy Machine Bridge state.
|
|
215
|
+
Local state and global config are owner-only, versioned, size-bounded, and written through temporary files, flushes, and atomic rename. Machine-level browser pairing state is owner-only and shared across workspace runtimes through the local broker; its bearer token is not part of workspace state responses. State, managed-job manager, and detached runner commits share a bounded retrying atomic-replace primitive for transient Windows `EPERM`, `EACCES`, `EBUSY`, and `ENOTEMPTY` sharing failures. Managed-job manager and detached runner use one shared no-follow, size-bounded regular-file reader; transition and recovery locks use one PID-lock primitive with bounded stale-lock reclamation and optional runner handoff. Malformed or oversized state becomes a bounded-count `.corrupt-*` backup. Resource paths are omitted from redacted status output. Custom roots are adopted only when empty or recognizable as legacy Machine Bridge state.
|
|
189
216
|
|
|
190
217
|
Active managed jobs persist an owner-only plan, status, runner PID, and bounded runner diagnostics. Terminal jobs delete the full plan and retain only bounded status/redacted results for up to seven days. This balances crash cleanup with minimization of scripts, stdin, argv, environment overrides, and resource source paths.
|
|
191
218
|
|
|
@@ -210,5 +237,7 @@ Cloudflare sampling is size control rather than an audit log. The project intent
|
|
|
210
237
|
- guaranteed finally cleanup across permanent power, disk, credential, network, or endpoint-security failure;
|
|
211
238
|
- bypassing MCP-host, connector, operating-system, or endpoint-security policy;
|
|
212
239
|
- PTY/terminal emulation;
|
|
213
|
-
- model-level prompt-injection prevention;
|
|
240
|
+
- model-level prompt-injection prevention or semantic validation of browser/application actions;
|
|
241
|
+
- universal desktop UI automation beyond the implemented OS Accessibility backend;
|
|
242
|
+
- scripting browser-internal/enterprise-blocked pages or inaccessible cross-origin frames;
|
|
214
243
|
- multi-user tenancy in one Worker deployment.
|
package/docs/CLIENTS.md
CHANGED
|
@@ -52,6 +52,14 @@ It is therefore an optional compatibility and reuse surface, not a replacement f
|
|
|
52
52
|
|
|
53
53
|
The MCP specification defines stdio and Streamable HTTP as standard transports. In stdio, the host launches the server as a subprocess and exchanges newline-delimited JSON-RPC through stdin/stdout. Streamable HTTP runs the server independently behind an HTTP endpoint.
|
|
54
54
|
|
|
55
|
+
## Automatic capability selection
|
|
56
|
+
|
|
57
|
+
MCP server instructions and `resolve_task_capabilities` give the host a current view of global/project instructions, skills, registered commands, applications, and browser capability. The resolver rescans rather than relying on a stale dynamic tool list and can return the best matching skill instructions in one call.
|
|
58
|
+
|
|
59
|
+
The host still owns the agent loop. ChatGPT web may use the recommendation automatically, ask for confirmation, expose only part of the catalog, or ignore server instructions. No MCP implementation can guarantee automatic invocation from the server side. Machine Bridge models that limitation explicitly instead of treating a recommendation as execution.
|
|
60
|
+
|
|
61
|
+
For browser tasks, the remote host reaches the local extension through the Worker and daemon. The extension controls the existing Chromium profile; it is not a separate Playwright profile.
|
|
62
|
+
|
|
55
63
|
## Generate local configuration
|
|
56
64
|
|
|
57
65
|
The default profile is `full`:
|
package/docs/ENGINEERING.md
CHANGED
|
@@ -9,7 +9,10 @@ This document records project-wide decisions that must survive individual fixes,
|
|
|
9
9
|
3. **Machine Bridge authority and host authority are separate.** `full` removes Machine Bridge's own policy, path, shell, and environment restrictions. It cannot override an MCP host, connector gateway, operating system, endpoint-security product, cloud IAM, remote authentication, or `sudo`.
|
|
10
10
|
4. **Publication surfaces contain no real environment metadata.** Source, tests, fixtures, examples, release notes, filenames, package contents, tags, and release assets use synthetic identifiers and reserved example domains.
|
|
11
11
|
5. **Secrets are never operational log data.** Tool arguments, command text, stdin, stdout, stderr, file content, OAuth bodies, credentials, and local resource values are not logged.
|
|
12
|
-
6. **A release is one version across all surfaces.** Package metadata, Worker version, Git tag, GitHub Release, npm version, documentation, and deployed health version must agree before a release is considered complete.
|
|
12
|
+
6. **A release is one version across all surfaces.** Package metadata, Worker version, browser-extension version/name, Git tag, GitHub Release, npm version, documentation, and deployed health version must agree before a release is considered complete.
|
|
13
|
+
7. **Generic local automation is structured, not arbitrary evaluation.** Browser/application features may expose broad user authority under canonical `full`, but must not accept caller-provided JavaScript, AppleScript, JXA, or extension code.
|
|
14
|
+
8. **Daily-browser integration uses the existing profile.** The supported primary browser path is the packaged authenticated extension and machine-level loopback broker, preserving current tabs/login state; a separate automation profile is not an equivalent replacement.
|
|
15
|
+
9. **Pairing and resource secrets are not conversation or log data.** Tokens and injected local-resource values must not be returned, embedded in URLs, or written to operational logs.
|
|
13
16
|
|
|
14
17
|
A proposed change that conflicts with an invariant requires an explicit owner decision and corresponding documentation update. It must not be hidden inside an unrelated refactor.
|
|
15
18
|
|
|
@@ -158,6 +161,7 @@ A thorough review asks:
|
|
|
158
161
|
- Are all success, failure, cancellation, timeout, disconnect, restart, and recovery branches bounded and tested?
|
|
159
162
|
- Are logs actionable, rate-limited, and privacy-preserving?
|
|
160
163
|
- Are persistent files atomic, owner-only, size-bounded, and symlink-aware?
|
|
164
|
+
- Can browser/app automation be expressed without arbitrary evaluation, and are pairing/resource values absent from results and logs?
|
|
161
165
|
- Can a stale PID, stale socket, duplicate request, partial write, or ambiguous remote response violate integrity?
|
|
162
166
|
- Are package, CI, Worker, service, and release behavior tested on every supported platform?
|
|
163
167
|
- Does the complete diff contain any real identifier, path, host, account, or credential-shaped value?
|