openmausbot 0.1.84 → 0.1.86
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/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/abnfDiagram-O67JEVCF-DpUZNnFD.js +1 -0
- package/dist/assets/arc-DW7mrI4p.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
- package/dist/assets/channel-BXFkgus9.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
- package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
- package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
- package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
- package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BGgqlWPJ.css +1 -0
- package/dist/assets/index-BtHo8ZKl.js +577 -0
- package/dist/assets/{index-DNa2umw-.js → index-h9FFNwTu.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
- package/dist/assets/linear-3su2TG93.js +1 -0
- package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +1 -0
- package/dist-server/container-mcp.js +231 -17
- package/dist-server/drivers/agents-proxy.js +16097 -1411
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +6 -5
- package/dist-server/hooks/omb-hook.js +74 -0
- package/dist-server/index.js +12495 -7584
- package/dist-server/local-computer-proxy.js +83 -1
- package/dist-server/local-computer.js +14 -2
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +5458 -4587
- package/dist-server/pair-cli.js +5458 -4587
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/agent-tool-policy.js +1 -0
- package/dist-server/server/bot-package.js +2 -0
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box.js +25 -5
- package/dist-server/server/browser-engine.js +87 -10
- package/dist-server/server/browser-live.js +7 -5
- package/dist-server/server/browser-runtime.js +66 -11
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/checkpoints.js +62 -18
- package/dist-server/server/cli-prompts.js +26 -2
- package/dist-server/server/cli.js +79 -10
- package/dist-server/server/commands.js +27 -0
- package/dist-server/server/compaction-summary.js +78 -0
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/config.js +137 -14
- package/dist-server/server/context-budget.js +29 -0
- package/dist-server/server/context-rebuild.js +72 -0
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +20 -3
- package/dist-server/server/delegations.js +11 -9
- package/dist-server/server/digest.js +130 -0
- package/dist-server/server/drivers/acp/core.js +637 -238
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1123 -0
- package/dist-server/server/drivers/agents-catalog.js +659 -0
- package/dist-server/server/drivers/agents-client.js +28 -0
- package/dist-server/server/drivers/agents-proxy.js +18 -1683
- package/dist-server/server/drivers/agents-result.js +23 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
- package/dist-server/server/drivers/claude.js +83 -16
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex.js +32 -4
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/openai-chat.js +66 -45
- package/dist-server/server/drivers/openai-compat.js +16 -8
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -0
- package/dist-server/server/hooks/omb-hook.js +103 -0
- package/dist-server/server/hosted-models.js +151 -0
- package/dist-server/server/hosted-slack.js +11 -0
- package/dist-server/server/index.js +2810 -678
- package/dist-server/server/local-computer.js +21 -2
- package/dist-server/server/managed-desktop.js +79 -14
- package/dist-server/server/managed-policy.js +153 -0
- package/dist-server/server/mcp-bridge.js +10 -2
- package/dist-server/server/mcp-tool-images.js +37 -0
- package/dist-server/server/mcp-tool-schema.js +108 -0
- package/dist-server/server/message-db.js +108 -21
- package/dist-server/server/message-file.js +3 -2
- package/dist-server/server/model-context-window.js +7 -0
- package/dist-server/server/model-prices.js +121 -0
- package/dist-server/server/notify.js +10 -1
- package/dist-server/server/package-export.js +1 -0
- package/dist-server/server/peer-roster.js +17 -4
- package/dist-server/server/provider-icon.js +20 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/recent-work.js +15 -0
- package/dist-server/server/request-auth.js +97 -0
- package/dist-server/server/room-handoffs.js +41 -13
- package/dist-server/server/routes/hosted-slack.js +20 -0
- package/dist-server/server/routes/table.js +14 -0
- package/dist-server/server/routine-requests.js +39 -1
- package/dist-server/server/routines.js +65 -9
- package/dist-server/server/shared-computer-control.js +31 -4
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +12 -1
- package/dist-server/server/store.js +384 -33
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-backup.js +19 -2
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/tool-results.js +72 -0
- package/dist-server/server/tts/grok.js +74 -0
- package/dist-server/server/tts/index.js +19 -1
- package/dist-server/server/usage-ledger.js +31 -8
- package/dist-server/server/webhooks.js +28 -0
- package/dist-server/server/workspace-backup-policy.js +9 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/shared/approval-mode.js +7 -1
- package/dist-server/shared/digest.js +1 -0
- package/dist-server/shared/markdown-windows-paths.js +36 -0
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/provider-icon.js +110 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/team-backup.js +5 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +236 -22
- package/enterprise/README.md +33 -12
- package/enterprise/server/index.js +6 -5
- package/package.json +1 -1
- package/dist/assets/index-BbU5REzd.js +0 -310
- package/dist/assets/index-CLGfYlx_.css +0 -1
|
@@ -25,7 +25,7 @@ const DOWNLOAD_TIMEOUT_MS = 10 * 60_000;
|
|
|
25
25
|
/** Native restore looks up a filename prefix. Fixed-length keys prevent a
|
|
26
26
|
* legacy profile named work from accidentally restoring work-client. */
|
|
27
27
|
export function browserRestoreKey(session) {
|
|
28
|
-
if (!/^[A-Za-z0-9_
|
|
28
|
+
if (!/^[A-Za-z0-9_.-]{1,96}$/.test(session))
|
|
29
29
|
throw new Error("Invalid browser session.");
|
|
30
30
|
return `omb-${createHash("sha256").update(session).digest("hex")}`;
|
|
31
31
|
}
|
|
@@ -56,7 +56,7 @@ function ensureManagedBrowserConfig(env) {
|
|
|
56
56
|
}
|
|
57
57
|
/** Close exactly one daemon without the CLI's implicit launch envelope. */
|
|
58
58
|
export async function closeBrowserSession(binaryPath, env, timeoutMs = 15_000) {
|
|
59
|
-
if (!env.AGENT_BROWSER_SESSION || !/^[A-Za-z0-9_
|
|
59
|
+
if (!env.AGENT_BROWSER_SESSION || !/^[A-Za-z0-9_.-]{1,96}$/.test(env.AGENT_BROWSER_SESSION))
|
|
60
60
|
return false;
|
|
61
61
|
// Native CLI prepends a launch even to `close` when launch flags are set.
|
|
62
62
|
// Remove them, and bypass external project/user config, so closing a
|
|
@@ -85,7 +85,16 @@ export async function closeBrowserSession(binaryPath, env, timeoutMs = 15_000) {
|
|
|
85
85
|
catch {
|
|
86
86
|
return finish(false);
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
let exited = false;
|
|
89
|
+
let killTimer;
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
// TERM lets the daemon flush; KILL after a grace period reaps a stuck one.
|
|
92
|
+
child.kill("SIGTERM");
|
|
93
|
+
killTimer = setTimeout(() => { if (!exited)
|
|
94
|
+
child.kill("SIGKILL"); }, 5_000);
|
|
95
|
+
killTimer.unref?.();
|
|
96
|
+
finish(false);
|
|
97
|
+
}, Math.max(1, deadline - Date.now()));
|
|
89
98
|
timer.unref?.();
|
|
90
99
|
child.stdout?.on("data", (chunk) => {
|
|
91
100
|
output += String(chunk);
|
|
@@ -95,9 +104,9 @@ export async function closeBrowserSession(binaryPath, env, timeoutMs = 15_000) {
|
|
|
95
104
|
finish(false);
|
|
96
105
|
}
|
|
97
106
|
});
|
|
98
|
-
child.on("error", () => { clearTimeout(timer); finish(false); });
|
|
107
|
+
child.on("error", () => { exited = true; clearTimeout(timer); clearTimeout(killTimer); finish(false); });
|
|
99
108
|
// exit can precede the last piped stdout chunk; close follows stdio.
|
|
100
|
-
child.on("close", (code) => { clearTimeout(timer); finish(code === 0); });
|
|
109
|
+
child.on("close", (code) => { exited = true; clearTimeout(timer); clearTimeout(killTimer); finish(code === 0); });
|
|
101
110
|
});
|
|
102
111
|
if (!(await run(["close"])).ok)
|
|
103
112
|
return false;
|
|
@@ -241,6 +250,8 @@ export function resolveAgentBrowserBinary(options = {}) {
|
|
|
241
250
|
const pinned = pinnedBinaryPath(options.dataDir, platform, options.arch);
|
|
242
251
|
if (exists(pinned))
|
|
243
252
|
return pinned;
|
|
253
|
+
if (options.managedOnly)
|
|
254
|
+
return null;
|
|
244
255
|
return onPath(env, platform, exists);
|
|
245
256
|
}
|
|
246
257
|
/** Download the pinned release asset for this machine into the data dir,
|
|
@@ -263,7 +274,35 @@ export async function installAgentBrowserBinary(options = {}) {
|
|
|
263
274
|
const response = await (options.fetchImpl ?? fetch)(url, { redirect: "follow", signal: controller.signal });
|
|
264
275
|
if (!response.ok)
|
|
265
276
|
throw new Error(`the agent-browser download failed (HTTP ${response.status})`);
|
|
266
|
-
|
|
277
|
+
const declared = Number(response.headers.get("content-length") ?? NaN);
|
|
278
|
+
if (Number.isFinite(declared) && declared !== asset.bytes)
|
|
279
|
+
throw new Error("the agent-browser download did not match its pinned size; nothing was installed");
|
|
280
|
+
// Stream, never buffer whole: a body past the pin is cut off mid-flight.
|
|
281
|
+
const chunks = [];
|
|
282
|
+
let received = 0;
|
|
283
|
+
if (response.body) {
|
|
284
|
+
try {
|
|
285
|
+
for await (const chunk of response.body) {
|
|
286
|
+
chunks.push(chunk);
|
|
287
|
+
received += chunk.byteLength;
|
|
288
|
+
if (received > asset.bytes) {
|
|
289
|
+
controller.abort();
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
// Only the abort we asked for is swallowed; the size check reports it.
|
|
296
|
+
if (received <= asset.bytes)
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
body = Buffer.concat(chunks);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
body = Buffer.from(await response.arrayBuffer());
|
|
303
|
+
if (body.length > asset.bytes)
|
|
304
|
+
controller.abort();
|
|
305
|
+
}
|
|
267
306
|
}
|
|
268
307
|
finally {
|
|
269
308
|
clearTimeout(timer);
|
|
@@ -273,6 +312,18 @@ export async function installAgentBrowserBinary(options = {}) {
|
|
|
273
312
|
const digest = createHash("sha256").update(body).digest("hex");
|
|
274
313
|
if (digest !== asset.sha256)
|
|
275
314
|
throw new Error("the agent-browser download failed its SHA-256 check; nothing was installed");
|
|
315
|
+
// Sweep staging files abandoned by earlier runs before writing a new one.
|
|
316
|
+
try {
|
|
317
|
+
for (const name of readdirSync(directory)) {
|
|
318
|
+
if (!name.endsWith(".part"))
|
|
319
|
+
continue;
|
|
320
|
+
try {
|
|
321
|
+
unlinkSync(join(directory, name));
|
|
322
|
+
}
|
|
323
|
+
catch { /* already gone */ }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch { /* best effort */ }
|
|
276
327
|
const staging = `${destination}.${randomBytes(6).toString("hex")}.part`;
|
|
277
328
|
writeFileSync(staging, body, { mode: 0o755 });
|
|
278
329
|
if (platform !== "win32")
|
|
@@ -293,10 +344,34 @@ export function ensureChrome(binaryPath, options = {}) {
|
|
|
293
344
|
return new Promise((done, fail) => {
|
|
294
345
|
const child = spawn(binaryPath, args, { env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
295
346
|
let output = "";
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
347
|
+
// Keep only the tail: the failure path quotes the last lines, so a chatty
|
|
348
|
+
// installer cannot grow the buffer without bound while it hangs.
|
|
349
|
+
const record = (chunk) => {
|
|
350
|
+
output += String(chunk);
|
|
351
|
+
if (output.length > 262_144)
|
|
352
|
+
output = output.slice(-262_144);
|
|
353
|
+
};
|
|
354
|
+
child.stdout?.on("data", record);
|
|
355
|
+
child.stderr?.on("data", record);
|
|
356
|
+
let timedOut = false;
|
|
357
|
+
let exited = false;
|
|
358
|
+
// A hung installer gets the same wall clock as the download: TERM, then
|
|
359
|
+
// KILL after a grace period, with the promise failing at the deadline.
|
|
360
|
+
const timer = setTimeout(() => {
|
|
361
|
+
timedOut = true;
|
|
362
|
+
child.kill("SIGTERM");
|
|
363
|
+
const killTimer = setTimeout(() => { if (!exited)
|
|
364
|
+
child.kill("SIGKILL"); }, 5_000);
|
|
365
|
+
killTimer.unref?.();
|
|
366
|
+
fail(new Error("agent-browser install timed out"));
|
|
367
|
+
}, DOWNLOAD_TIMEOUT_MS);
|
|
368
|
+
timer.unref?.();
|
|
369
|
+
child.on("error", (error) => { exited = true; clearTimeout(timer); fail(error); });
|
|
299
370
|
child.on("exit", (code) => {
|
|
371
|
+
exited = true;
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
if (timedOut)
|
|
374
|
+
return; // already failed with the timeout error
|
|
300
375
|
if (code === 0) {
|
|
301
376
|
options.log?.("agent-browser: Chrome is ready");
|
|
302
377
|
done();
|
|
@@ -369,6 +444,8 @@ export function agentBrowserIntegration(input) {
|
|
|
369
444
|
};
|
|
370
445
|
if (input.headless !== false)
|
|
371
446
|
env.AGENT_BROWSER_HEADLESS = "1";
|
|
447
|
+
if (input.attachCdpUrl)
|
|
448
|
+
env.AGENT_BROWSER_CDP = input.attachCdpUrl;
|
|
372
449
|
// MCP clients may filter the parent environment. Carry the configured
|
|
373
450
|
// Chrome path explicitly without forwarding unrelated secrets or flags.
|
|
374
451
|
for (const name of ["PATH", "AGENT_BROWSER_EXECUTABLE_PATH"]) {
|
|
@@ -464,7 +541,7 @@ export const BUILT_IN_BROWSER_SYSTEM_PROMPT = " You have your own web browser th
|
|
|
464
541
|
export async function clearBrowserSessionState(binaryPath, session, options = {}) {
|
|
465
542
|
// The native state-clear CLI ignores the daemon session, and even its named
|
|
466
543
|
// form currently drops that name before dispatch. Never invoke it here.
|
|
467
|
-
if (!/^[A-Za-z0-9_
|
|
544
|
+
if (!/^[A-Za-z0-9_.-]{1,96}$/.test(session))
|
|
468
545
|
return false;
|
|
469
546
|
const env = browserRuntimeEnv({ ...options.env, AGENT_BROWSER_SESSION: session, AGENT_BROWSER_HEADLESS: "1" });
|
|
470
547
|
let directory;
|
|
@@ -45,7 +45,7 @@ export function normalizeBrowserLiveMessage(value) {
|
|
|
45
45
|
if (!message)
|
|
46
46
|
return null;
|
|
47
47
|
if (message.type === "error")
|
|
48
|
-
return { type: "error", message: "The browser stream was interrupted.
|
|
48
|
+
return { type: "error", retryable: true, message: "The browser stream was interrupted." };
|
|
49
49
|
if (message.type === "url")
|
|
50
50
|
return { type: "url", url: displayUrl(message.url) };
|
|
51
51
|
if (message.type === "tabs" && Array.isArray(message.tabs) && message.tabs.length <= 100) {
|
|
@@ -292,7 +292,7 @@ export class BrowserLive {
|
|
|
292
292
|
const env = browserRuntimeEnv({ ...viewer.spec.env, AGENT_BROWSER_SESSION: viewer.session });
|
|
293
293
|
try {
|
|
294
294
|
const { stdout } = await execute(viewer.spec.command, [...args, "--json", "--no-webmcp"], {
|
|
295
|
-
env, timeout: 30_000, maxBuffer: 1024 * 1024, encoding: "utf8", windowsHide: true,
|
|
295
|
+
env, timeout: 30_000, killSignal: "SIGKILL", maxBuffer: 1024 * 1024, encoding: "utf8", windowsHide: true,
|
|
296
296
|
});
|
|
297
297
|
if (!this.current(viewer))
|
|
298
298
|
throw new Error("stale viewer");
|
|
@@ -302,7 +302,8 @@ export class BrowserLive {
|
|
|
302
302
|
throw new Error("browser command failed");
|
|
303
303
|
return data;
|
|
304
304
|
}
|
|
305
|
-
catch {
|
|
305
|
+
catch (error) {
|
|
306
|
+
console.warn("browser-live:", error);
|
|
306
307
|
throw new BrowserLiveError("The browser could not complete this action. Check that the browser engine is installed, then reconnect.", 503);
|
|
307
308
|
}
|
|
308
309
|
}
|
|
@@ -432,7 +433,7 @@ export class BrowserLive {
|
|
|
432
433
|
this.send(viewer, message);
|
|
433
434
|
});
|
|
434
435
|
socket.addEventListener("error", () => { if (!viewer.restarting) {
|
|
435
|
-
this.send(viewer, { type: "error", message: "The browser stream disconnected.
|
|
436
|
+
this.send(viewer, { type: "error", retryable: true, message: "The browser stream disconnected." });
|
|
436
437
|
this.close(viewer);
|
|
437
438
|
} });
|
|
438
439
|
socket.addEventListener("close", () => { if (!viewer.restarting)
|
|
@@ -461,8 +462,9 @@ export class BrowserLive {
|
|
|
461
462
|
viewer.heartbeat.unref();
|
|
462
463
|
}
|
|
463
464
|
catch (error) {
|
|
465
|
+
console.warn("browser-live:", error);
|
|
464
466
|
if (options.res.headersSent) {
|
|
465
|
-
this.send(viewer, { type: "error", message: "The browser stream could not start.
|
|
467
|
+
this.send(viewer, { type: "error", retryable: true, message: "The browser stream could not start." });
|
|
466
468
|
this.close(viewer);
|
|
467
469
|
return;
|
|
468
470
|
}
|
|
@@ -3,6 +3,9 @@ import { DEFAULT_BROWSER_RESULT_BUDGET, shapeBrowserToolResult, slimBrowserToolL
|
|
|
3
3
|
export const BROWSER_CONTROL_REFUSAL = "Browser tools are paused while a person controls this browser. Wait for them to hand control back; do not try another browser or execution tool.";
|
|
4
4
|
const MAX_REQUEST_BYTES = 1_048_576;
|
|
5
5
|
const MAX_RESPONSE_BYTES = 16_777_216;
|
|
6
|
+
/** Startup, not per-request work: a cold engine spawn can exceed a tight
|
|
7
|
+
* per-request budget before anything has been accepted to guard. */
|
|
8
|
+
const HANDSHAKE_TIMEOUT_MS = 1_000;
|
|
6
9
|
const HOST_ENV = ["HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "PATH", "Path", "TMPDIR", "TMP", "TEMP", "SystemRoot", "WINDIR", "SYSTEMDRIVE", "COMSPEC", "PATHEXT", "LANG", "LC_ALL", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_RUNTIME_DIR"];
|
|
7
10
|
/** MCP, viewer commands, and cleanup must resolve the same HOME/socket paths.
|
|
8
11
|
* Inherit OS plumbing, never the harness's model-provider credentials. */
|
|
@@ -10,7 +13,7 @@ export function browserRuntimeEnv(overrides) {
|
|
|
10
13
|
const env = Object.fromEntries(HOST_ENV.flatMap((key) => process.env[key] === undefined ? [] : [[key, process.env[key]]]));
|
|
11
14
|
return { ...env, ...overrides };
|
|
12
15
|
}
|
|
13
|
-
class TransportError extends Error {
|
|
16
|
+
export class TransportError extends Error {
|
|
14
17
|
}
|
|
15
18
|
/** A server-owned JSONL client. Neither child stderr nor its environment is
|
|
16
19
|
* returned to the agent. Reuse the existing cross-platform spawn/kill rules. */
|
|
@@ -27,11 +30,13 @@ class BrowserClient {
|
|
|
27
30
|
idleMs;
|
|
28
31
|
maxPending;
|
|
29
32
|
onClose;
|
|
30
|
-
|
|
33
|
+
onRequestTimeout;
|
|
34
|
+
constructor(spec, requestTimeoutMs, idleMs, maxPending, onClose, onRequestTimeout) {
|
|
31
35
|
this.requestTimeoutMs = requestTimeoutMs;
|
|
32
36
|
this.idleMs = idleMs;
|
|
33
37
|
this.maxPending = maxPending;
|
|
34
38
|
this.onClose = onClose;
|
|
39
|
+
this.onRequestTimeout = onRequestTimeout;
|
|
35
40
|
this.child = spawnCli(spec.command, spec.args, {
|
|
36
41
|
env: browserRuntimeEnv(spec.env), stdio: ["pipe", "pipe", "pipe"], shell: false,
|
|
37
42
|
});
|
|
@@ -43,7 +48,7 @@ class BrowserClient {
|
|
|
43
48
|
this.ready = this.rpc("initialize", {
|
|
44
49
|
protocolVersion: "2024-11-05", capabilities: {},
|
|
45
50
|
clientInfo: { name: "openmausbot-browser", version: "1" },
|
|
46
|
-
}).then((result) => {
|
|
51
|
+
}, Math.max(this.requestTimeoutMs, HANDSHAKE_TIMEOUT_MS)).then((result) => {
|
|
47
52
|
if (!result || typeof result !== "object" || !("protocolVersion" in result)) {
|
|
48
53
|
throw new TransportError("Browser engine returned an invalid handshake.");
|
|
49
54
|
}
|
|
@@ -102,7 +107,7 @@ class BrowserClient {
|
|
|
102
107
|
void this.stop(new TransportError("Browser connection closed."));
|
|
103
108
|
});
|
|
104
109
|
}
|
|
105
|
-
rpc(method, params) {
|
|
110
|
+
rpc(method, params, timeoutMs = this.requestTimeoutMs) {
|
|
106
111
|
if (this.stopped)
|
|
107
112
|
return Promise.reject(new TransportError("Browser connection closed."));
|
|
108
113
|
if (this.pending.size >= this.maxPending)
|
|
@@ -114,7 +119,13 @@ class BrowserClient {
|
|
|
114
119
|
if (this.idleTimer)
|
|
115
120
|
clearTimeout(this.idleTimer);
|
|
116
121
|
return new Promise((resolve, reject) => {
|
|
117
|
-
const timer = setTimeout(() => {
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
// Seal the gate synchronously with the timer, before stop() teardown:
|
|
124
|
+
// the rejection can surface through the ready handshake, which sits
|
|
125
|
+
// outside agentRpc's uncertainty classifier.
|
|
126
|
+
this.onRequestTimeout();
|
|
127
|
+
void this.stop(new TransportError("Browser request timed out; restart the browser before taking control."));
|
|
128
|
+
}, timeoutMs);
|
|
118
129
|
timer.unref();
|
|
119
130
|
this.pending.set(id, { resolve, reject, timer });
|
|
120
131
|
try {
|
|
@@ -131,11 +142,35 @@ class BrowserClient {
|
|
|
131
142
|
if (!this.stopped && this.pending.size === 0) {
|
|
132
143
|
// Only the stateless MCP transport expires; saved browser state and the
|
|
133
144
|
// browser daemon itself belong to the profile, not this client.
|
|
134
|
-
this.idleTimer = setTimeout(() => { void this.stop(); }, this.idleMs);
|
|
145
|
+
this.idleTimer = setTimeout(() => { void this.stop(undefined, "transport"); }, this.idleMs);
|
|
135
146
|
this.idleTimer.unref();
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
|
-
|
|
149
|
+
/** Upstream's MCP loop exits on stdin EOF without closing the daemon.
|
|
150
|
+
* In particular, Windows taskkill /T would also kill that profile's Chrome,
|
|
151
|
+
* even when the daemon created a new process group. Idle is not shutdown. */
|
|
152
|
+
retireTransport() {
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
const finish = () => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
this.child.off("exit", finish);
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
try {
|
|
161
|
+
this.child.kill("SIGKILL");
|
|
162
|
+
}
|
|
163
|
+
catch { /* transport already exited */ }
|
|
164
|
+
finish();
|
|
165
|
+
}, 1_000);
|
|
166
|
+
this.child.once("exit", finish);
|
|
167
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null)
|
|
168
|
+
finish();
|
|
169
|
+
else
|
|
170
|
+
this.child.stdin.end();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
stop(error = new TransportError("Browser connection closed."), scope = "tree") {
|
|
139
174
|
if (this.stoppedPromise)
|
|
140
175
|
return this.stoppedPromise;
|
|
141
176
|
this.stopped = true;
|
|
@@ -148,7 +183,7 @@ class BrowserClient {
|
|
|
148
183
|
}
|
|
149
184
|
this.pending.clear();
|
|
150
185
|
this.onClose();
|
|
151
|
-
this.stoppedPromise = killCliTree(this.child, 1_000).then((stopped) => {
|
|
186
|
+
this.stoppedPromise = scope === "transport" ? this.retireTransport() : killCliTree(this.child, 1_000).then((stopped) => {
|
|
152
187
|
if (stopped)
|
|
153
188
|
return;
|
|
154
189
|
try {
|
|
@@ -211,6 +246,14 @@ export class BrowserRuntime {
|
|
|
211
246
|
async agentRpc(session, spec, method, params, beforeDispatch) {
|
|
212
247
|
if (method !== "tools/list" && method !== "tools/call")
|
|
213
248
|
throw new Error("Unsupported browser method.");
|
|
249
|
+
// tools/list bypasses withAgentAction (a human may hold control), so it
|
|
250
|
+
// must refuse the closing window itself or its client outlives restart().
|
|
251
|
+
if (method !== "tools/call" && this.gate(session).closing)
|
|
252
|
+
throw new Error("The browser is closing. Try again shortly.");
|
|
253
|
+
// Uncertainty survives client replacement and never self-resolves: refuse
|
|
254
|
+
// every new browser request until an explicit restart clears it.
|
|
255
|
+
if (this.gate(session).uncertain)
|
|
256
|
+
throw new Error("A browser action was interrupted. Restart this browser before continuing.");
|
|
214
257
|
const invoke = async () => {
|
|
215
258
|
const key = JSON.stringify([spec.command, spec.args, Object.entries(spec.env).sort(([a], [b]) => a.localeCompare(b))]);
|
|
216
259
|
let entry = this.clients.get(session);
|
|
@@ -220,6 +263,10 @@ export class BrowserRuntime {
|
|
|
220
263
|
const client = new BrowserClient(spec, this.options.requestTimeoutMs, this.options.idleMs, this.options.maxPending, () => {
|
|
221
264
|
if (this.clients.get(session)?.client === client)
|
|
222
265
|
this.clients.delete(session);
|
|
266
|
+
}, () => {
|
|
267
|
+
const gate = this.gate(session);
|
|
268
|
+
if (!gate.closing)
|
|
269
|
+
gate.uncertain = true;
|
|
223
270
|
});
|
|
224
271
|
entry = { key, client };
|
|
225
272
|
this.clients.set(session, entry);
|
|
@@ -242,8 +289,11 @@ export class BrowserRuntime {
|
|
|
242
289
|
catch (error) {
|
|
243
290
|
// An MCP timeout cannot prove the independent daemon stopped an
|
|
244
291
|
// accepted action. Recovery must close the browser, not just its pipe.
|
|
245
|
-
|
|
246
|
-
|
|
292
|
+
// A stop from close()/restart() is intentional, not uncertainty.
|
|
293
|
+
if (method === "tools/call" && error instanceof TransportError && !this.gate(session).closing) {
|
|
294
|
+
const gate = this.gate(session);
|
|
295
|
+
gate.uncertain = true;
|
|
296
|
+
}
|
|
247
297
|
throw error;
|
|
248
298
|
}
|
|
249
299
|
};
|
|
@@ -344,6 +394,10 @@ export class BrowserRuntime {
|
|
|
344
394
|
try {
|
|
345
395
|
await closeBrowser();
|
|
346
396
|
await this.clients.get(session)?.client.stop();
|
|
397
|
+
// A tools/list admitted before closing set in can register a client
|
|
398
|
+
// while that stop awaits; registration is synchronous, so one re-check
|
|
399
|
+
// is deterministic and no stray transport survives to idle expiry.
|
|
400
|
+
await this.clients.get(session)?.client.stop();
|
|
347
401
|
gate.uncertain = false;
|
|
348
402
|
gate.owner = null;
|
|
349
403
|
gate.releasing = false;
|
|
@@ -364,10 +418,11 @@ export class BrowserRuntime {
|
|
|
364
418
|
const gate = this.gate(session);
|
|
365
419
|
gate.closing = true;
|
|
366
420
|
gate.ready = false;
|
|
421
|
+
// Clear before the awaited stop: any uncertain latch after this point must win.
|
|
422
|
+
gate.uncertain = false;
|
|
367
423
|
this.changed(gate);
|
|
368
424
|
await this.clients.get(session)?.client.stop();
|
|
369
425
|
gate.closing = false;
|
|
370
|
-
gate.uncertain = false;
|
|
371
426
|
this.changed(gate);
|
|
372
427
|
if (!gate.owner && !gate.agents && !gate.humans)
|
|
373
428
|
this.gates.delete(session);
|
|
@@ -31,6 +31,8 @@ export function queueChannelMessage(groupId, threadId, text, options = {}) {
|
|
|
31
31
|
sendId: options.sendId,
|
|
32
32
|
mode: options.mode ?? "chat",
|
|
33
33
|
via: options.via,
|
|
34
|
+
sender: options.sender,
|
|
35
|
+
trigger: options.trigger,
|
|
34
36
|
};
|
|
35
37
|
saveChatFollowup({ id: item.id, kind: "channel", ownerId: groupId, threadId, payload: item });
|
|
36
38
|
entry.items.push(item);
|
|
@@ -133,7 +133,7 @@ function disable(botId, message) {
|
|
|
133
133
|
let gitProbe = null;
|
|
134
134
|
function gitAvailable() {
|
|
135
135
|
gitProbe ??= new Promise((resolveProbe) => {
|
|
136
|
-
execFile("git", ["--version"], { windowsHide: true }, (err) => resolveProbe(!err));
|
|
136
|
+
execFile("git", ["--version"], { windowsHide: true, timeout: 5_000 }, (err) => resolveProbe(!err));
|
|
137
137
|
});
|
|
138
138
|
return gitProbe;
|
|
139
139
|
}
|
|
@@ -205,9 +205,10 @@ function gitEnv(shadow, cwd) {
|
|
|
205
205
|
* "." pathspec in add/restore resolves relative to it. A hung git (index
|
|
206
206
|
* lock, dead network filesystem) would otherwise jam the per-repo queue for
|
|
207
207
|
* the whole session, so every call carries a hard timeout. */
|
|
208
|
-
function runGit(args, cwd, env) {
|
|
208
|
+
function runGit(args, cwd, env, signal) {
|
|
209
209
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
210
|
-
|
|
210
|
+
signal?.throwIfAborted();
|
|
211
|
+
execFile("git", args, { cwd, env, signal, windowsHide: true, encoding: "utf8", timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
211
212
|
if (err)
|
|
212
213
|
rejectPromise(new Error(`git ${args[0]}: ${(stderr || err.message).trim().slice(0, 400)}`));
|
|
213
214
|
else
|
|
@@ -219,9 +220,10 @@ function runGit(args, cwd, env) {
|
|
|
219
220
|
* instead of `status --porcelain` emptiness on purpose — a nested repo with
|
|
220
221
|
* a dirty work tree shows up in status forever while staging nothing, which
|
|
221
222
|
* would either commit empty churn every turn or fail the commit outright. */
|
|
222
|
-
function hasStagedChanges(cwd, env) {
|
|
223
|
+
function hasStagedChanges(cwd, env, signal) {
|
|
223
224
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
224
|
-
|
|
225
|
+
signal?.throwIfAborted();
|
|
226
|
+
execFile("git", ["diff", "--cached", "--quiet", "--ignore-submodules=dirty"], { cwd, env, signal, windowsHide: true, encoding: "utf8", timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }, (err, _stdout, stderr) => {
|
|
225
227
|
if (err === null)
|
|
226
228
|
resolvePromise(false);
|
|
227
229
|
else if (err.code === 1)
|
|
@@ -248,46 +250,49 @@ function serialize(key, fn) {
|
|
|
248
250
|
* always resolves — it is filtered from listings and refused as a restore
|
|
249
251
|
* target, because "restore to empty" on a user's own project folder would
|
|
250
252
|
* delete their files. */
|
|
251
|
-
async function ensureShadow(cwd, env, shadow) {
|
|
253
|
+
async function ensureShadow(cwd, env, shadow, signal) {
|
|
254
|
+
signal?.throwIfAborted();
|
|
252
255
|
mkdirSync(shadow, { recursive: true, mode: 0o700 });
|
|
253
256
|
writeFileSync(join(shadow, "gitconfig"), GITCONFIG, { mode: 0o600 });
|
|
254
257
|
writeFileSync(join(shadow, "gitconfig_empty"), "", { mode: 0o600 });
|
|
255
258
|
if (!existsSync(join(shadow, ".git", "HEAD"))) {
|
|
256
259
|
// --template= keeps the user's init.templateDir hooks/config out
|
|
257
|
-
await runGit(["init", "--initial-branch=main", "--template="], cwd, env);
|
|
260
|
+
await runGit(["init", "--initial-branch=main", "--template="], cwd, env, signal);
|
|
258
261
|
}
|
|
259
262
|
mkdirSync(join(shadow, ".git", "info"), { recursive: true });
|
|
260
263
|
writeFileSync(join(shadow, ".git", "info", "exclude"), EXCLUDES);
|
|
261
264
|
try {
|
|
262
|
-
await runGit(["rev-parse", "--verify", "HEAD"], cwd, env);
|
|
265
|
+
await runGit(["rev-parse", "--verify", "HEAD"], cwd, env, signal);
|
|
263
266
|
}
|
|
264
267
|
catch {
|
|
265
268
|
// brand-new repo (or a crash between init and first commit)
|
|
266
|
-
await runGit(["commit", "--no-verify", "--allow-empty", "-m", "checkpoint base"], cwd, env);
|
|
269
|
+
await runGit(["commit", "--no-verify", "--allow-empty", "-m", "checkpoint base"], cwd, env, signal);
|
|
267
270
|
}
|
|
268
271
|
}
|
|
269
272
|
/** Stage everything and commit if anything actually changed. `complete`
|
|
270
273
|
* records whether every path was indexable: ordinary snapshots may keep the
|
|
271
274
|
* useful subset, but restore must not delete files its safety point missed. */
|
|
272
|
-
async function commitAll(cwd, env, label) {
|
|
275
|
+
async function commitAll(cwd, env, label, signal) {
|
|
273
276
|
// --ignore-errors skips files git cannot index (unreadable, FIFOs) instead
|
|
274
277
|
// of aborting — but still exits 1 when it skipped an unreadable file, so
|
|
275
278
|
// the exit code is retained even though a partial snapshot remains useful.
|
|
276
279
|
// Restore treats an incomplete safety point as a hard stop before touching
|
|
277
280
|
// the work tree.
|
|
278
281
|
let complete = true;
|
|
279
|
-
await runGit(["add", "-A", "--ignore-errors", "."], cwd, env).catch(() => {
|
|
282
|
+
await runGit(["add", "-A", "--ignore-errors", "."], cwd, env, signal).catch(() => {
|
|
280
283
|
complete = false;
|
|
281
284
|
});
|
|
282
|
-
if (await hasStagedChanges(cwd, env)) {
|
|
283
|
-
await runGit(["commit", "--no-verify", "-m", label], cwd, env);
|
|
285
|
+
if (await hasStagedChanges(cwd, env, signal)) {
|
|
286
|
+
await runGit(["commit", "--no-verify", "-m", label], cwd, env, signal);
|
|
284
287
|
}
|
|
285
|
-
return { hash: (await runGit(["rev-parse", "HEAD"], cwd, env)).trim(), complete };
|
|
288
|
+
return { hash: (await runGit(["rev-parse", "HEAD"], cwd, env, signal)).trim(), complete };
|
|
286
289
|
}
|
|
287
290
|
/** Snapshot the folder. Returns the checkpoint hash, or null when the
|
|
288
291
|
* feature is off for this bot, git is missing, or the folder is refused.
|
|
289
292
|
* Never throws — this is called fire-and-forget on the turn path. */
|
|
290
|
-
export async function snapshot(botId, cwd, label) {
|
|
293
|
+
export async function snapshot(botId, cwd, label, signal) {
|
|
294
|
+
if (signal?.aborted)
|
|
295
|
+
return null;
|
|
291
296
|
if (disabledBots.has(botId))
|
|
292
297
|
return null;
|
|
293
298
|
if (!(await gitAvailable()))
|
|
@@ -299,12 +304,51 @@ export async function snapshot(botId, cwd, label) {
|
|
|
299
304
|
const shadow = shadowDir(botId, worktree);
|
|
300
305
|
return await serialize(shadow, async () => {
|
|
301
306
|
const env = gitEnv(shadow, worktree);
|
|
302
|
-
await ensureShadow(worktree, env, shadow);
|
|
303
|
-
return (await commitAll(worktree, env, label)).hash;
|
|
307
|
+
await ensureShadow(worktree, env, shadow, signal);
|
|
308
|
+
return (await commitAll(worktree, env, label, signal)).hash;
|
|
304
309
|
});
|
|
305
310
|
}
|
|
306
311
|
catch (e) {
|
|
307
|
-
|
|
312
|
+
if (!signal?.aborted)
|
|
313
|
+
disable(botId, e instanceof Error ? e.message : String(e));
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** Paths that differ between two checkpoints of the same bot+folder, from
|
|
318
|
+
* `git diff --name-status` against the shadow repo. Null when the hashes
|
|
319
|
+
* are equal, the folder is refused or checkpoints are off, or git fails —
|
|
320
|
+
* the digest then simply omits its file list. Never throws (turn path). */
|
|
321
|
+
export async function diffStat(botId, cwd, fromHash, toHash, signal) {
|
|
322
|
+
if (signal?.aborted)
|
|
323
|
+
return null;
|
|
324
|
+
if (fromHash === toHash)
|
|
325
|
+
return null;
|
|
326
|
+
if (disabledBots.has(botId))
|
|
327
|
+
return null;
|
|
328
|
+
if (!(await gitAvailable()))
|
|
329
|
+
return null;
|
|
330
|
+
if (refusalReason(cwd) !== null)
|
|
331
|
+
return null;
|
|
332
|
+
try {
|
|
333
|
+
const worktree = realpathSync(resolve(cwd));
|
|
334
|
+
const shadow = shadowDir(botId, worktree);
|
|
335
|
+
const env = gitEnv(shadow, worktree);
|
|
336
|
+
const out = await serialize(shadow, () => runGit(["diff", "--name-status", "--no-renames", "-z", fromHash, toHash], worktree, env, signal));
|
|
337
|
+
const diff = { changed: [], added: [], deleted: [] };
|
|
338
|
+
const fields = out.split("\0").filter((field) => field.length > 0);
|
|
339
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
340
|
+
const status = fields[i];
|
|
341
|
+
const path = fields[i + 1];
|
|
342
|
+
if (status.startsWith("A"))
|
|
343
|
+
diff.added.push(path);
|
|
344
|
+
else if (status.startsWith("D"))
|
|
345
|
+
diff.deleted.push(path);
|
|
346
|
+
else
|
|
347
|
+
diff.changed.push(path);
|
|
348
|
+
}
|
|
349
|
+
return diff;
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
308
352
|
return null;
|
|
309
353
|
}
|
|
310
354
|
}
|
|
@@ -9,10 +9,34 @@ export class SetupCancelled extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
function displayText(value, multiline = false) {
|
|
12
|
-
|
|
12
|
+
// OSC 8 hyperlinks slip past stripVTControlCharacters on supported Node
|
|
13
|
+
// versions whenever the URI carries a character RFC 3986 allows unencoded
|
|
14
|
+
// ((, ), +, and friends): the failed match strands most of the URI and its
|
|
15
|
+
// BEL as printable text (nodejs/node#64313). Strip whole OSC sequences
|
|
16
|
+
// here, from the raw value while their openers are still intact; the
|
|
17
|
+
// label between a link's opening and closing sequences survives, and an
|
|
18
|
+
// unterminated sequence is removed outright. Both introducer forms
|
|
19
|
+
// match: ESC ] and the lone C1 byte, which carries no closing bracket.
|
|
20
|
+
// eslint-disable-next-line no-control-regex
|
|
21
|
+
const withoutOsc = value.replace(/(?:\u001b\]|\u009d)[^\u0007\u001b\u009c]*(?:\u0007|\u001b\\|\u009c)?/g, "");
|
|
22
|
+
const plain = stripVTControlCharacters(withoutOsc);
|
|
13
23
|
// Provider-supplied labels must not issue terminal control commands.
|
|
24
|
+
// stripVTControlCharacters glues a trailing BEL onto the escape sequence
|
|
25
|
+
// before it on some Node versions and leaves it bare on others, so the
|
|
26
|
+
// fallback below must be version-independent: controls that render no
|
|
27
|
+
// glyph (BEL, DEL, C1, …) are removed outright — padding them would
|
|
28
|
+
// counterfeit whitespace — while tab and carriage return collapse to one
|
|
29
|
+
// space and a newline survives only in multiline mode.
|
|
14
30
|
// eslint-disable-next-line no-control-regex
|
|
15
|
-
|
|
31
|
+
const withoutControls = plain.replace(/[\u0000-\u001f\u007f-\u009f]/g, (character) => {
|
|
32
|
+
if (character === "\n")
|
|
33
|
+
return multiline ? "\n" : " ";
|
|
34
|
+
if (character === "\t" || character === "\r")
|
|
35
|
+
return " ";
|
|
36
|
+
return "";
|
|
37
|
+
});
|
|
38
|
+
// Log lines must not end in trailing spaces; question prompts keep spacing like "Key: ".
|
|
39
|
+
return multiline ? withoutControls.replace(/[ \t]+(?=\n|$)/g, "") : withoutControls;
|
|
16
40
|
}
|
|
17
41
|
/** A line-based fallback with no cursor/color output. Readline has no output
|
|
18
42
|
* stream for secrets, so even pasted input cannot be echoed. */
|