openmausbot 0.1.86 → 0.1.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +19 -2
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -6,7 +6,8 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { createConnection, createServer } from "node:net";
|
|
7
7
|
import { BASE_IMAGE, CUA_DRIVER_VERSION, CUA_SOCKET, IMAGE as CUA_IMAGE, cuaExecArgs, dockerSecurityIsHardened, imageLabelsMatch, managedImageDockerfile, wholeScreenshot, BASE_IMAGE_DIGEST, BASE_IMAGE_LABEL, DRIVER_LABEL, IMAGE_LAYER_LABEL, IMAGE_LAYER_VERSION, MANAGED_LABEL, } from "./container-computer.js";
|
|
8
8
|
import { DATA_DIR, isValidSshAlias, vpsSshAlias } from "./config.js";
|
|
9
|
-
import { augmentedPath
|
|
9
|
+
import { augmentedPath } from "./env-path.js";
|
|
10
|
+
import { killCliTree, spawnCli } from "./procs.js";
|
|
10
11
|
import { prepareVpsSsh } from "./vps-ssh.js";
|
|
11
12
|
import { loadEnvironmentId } from "./environment.js";
|
|
12
13
|
import { SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
@@ -28,7 +29,9 @@ function vpsEnvironmentId() {
|
|
|
28
29
|
// SIGTERM must give ssh + docker time to tear down the remote exec before the
|
|
29
30
|
// SIGKILL escalation; 1s was routinely too short over a WAN round-trip, and an
|
|
30
31
|
// orphaned remote exec keeps the driver socket busy for the next command.
|
|
31
|
-
const
|
|
32
|
+
const COMMAND_TIMEOUT_TERM_GRACE_MS = 5_000;
|
|
33
|
+
// killCliTree also waits up to one second for the forced group exit.
|
|
34
|
+
const COMMAND_TIMEOUT_KILL_GRACE_MS = COMMAND_TIMEOUT_TERM_GRACE_MS + 1_000;
|
|
32
35
|
const CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/;
|
|
33
36
|
const CONTAINER_ID = /^[a-f0-9]{12,64}$/i;
|
|
34
37
|
const FULL_CONTAINER_ID = /^[a-f0-9]{64}$/i;
|
|
@@ -63,20 +66,32 @@ function snapshotVpsConfig(cfg) {
|
|
|
63
66
|
export function vpsLifecycleBusy() {
|
|
64
67
|
return lifecycleLocks.size > 0;
|
|
65
68
|
}
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
+
// Sleep, remove and preview requests must not queue behind a potentially
|
|
70
|
+
// 10-minute image build. Turn preparation may instead wait through a normal
|
|
71
|
+
// preview, whose bounded duration is accounted for below.
|
|
69
72
|
const LOCK_ACQUIRE_TIMEOUT_MS = 5_000;
|
|
70
73
|
// The panel polls status every 4-6s and the screen poller re-checks it before
|
|
71
74
|
// every frame; each full status is several docker-over-SSH processes. Same
|
|
72
75
|
// pattern as container-computer's screenshotStatusCache, and the same TTL.
|
|
73
76
|
const STATUS_CACHE_TTL_MS = 10_000;
|
|
74
77
|
const statusCache = new Map();
|
|
78
|
+
const pendingStatuses = new Map();
|
|
79
|
+
const pendingProvisions = new Map();
|
|
75
80
|
const SCREENSHOT_BUDGET_MS = 45_000;
|
|
76
81
|
const SCREENSHOT_CLEANUP_BUDGET_MS = 10_000;
|
|
82
|
+
// Turn preparation may wait for a normal preview to finish; destructive
|
|
83
|
+
// actions retain the short acquisition limit. Overlapping provisions share
|
|
84
|
+
// one operation below, so they never queue behind their own image build.
|
|
85
|
+
const PREPARATION_LOCK_TIMEOUT_MS = SCREENSHOT_BUDGET_MS + LOCK_ACQUIRE_TIMEOUT_MS;
|
|
77
86
|
const pendingScreenshots = new Map();
|
|
78
87
|
const viewerConnections = new Map();
|
|
79
88
|
const desktopTunnels = new Map();
|
|
89
|
+
function invalidateVpsStatus(key) {
|
|
90
|
+
statusCache.delete(key);
|
|
91
|
+
// Detach old polls as well: an inspection begun before a mutation must
|
|
92
|
+
// neither be joined nor republish its old result after that mutation.
|
|
93
|
+
pendingStatuses.delete(key);
|
|
94
|
+
}
|
|
80
95
|
/** Whether a turn may prepare or start the VPS container rather than only
|
|
81
96
|
* reuse a running one. Explicit Cloud always may; Auto only when the person
|
|
82
97
|
* switched on Start VPS automatically — except for unattended runs. A
|
|
@@ -216,11 +231,10 @@ function tailCollector() {
|
|
|
216
231
|
}
|
|
217
232
|
export function defaultRunner(args, options = {}) {
|
|
218
233
|
return new Promise((resolve, reject) => {
|
|
219
|
-
const command = resolveCliSpawn("docker", args);
|
|
220
234
|
// docker's SSH transport runs the first `ssh` on PATH: the app's shim,
|
|
221
235
|
// which shares one connection across every command of this VPS.
|
|
222
236
|
const ssh = prepareVpsSsh(DATA_DIR, augmentedPath());
|
|
223
|
-
const child =
|
|
237
|
+
const child = spawnCli("docker", args, {
|
|
224
238
|
shell: false,
|
|
225
239
|
env: { ...process.env, PATH: ssh.path },
|
|
226
240
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -228,31 +242,33 @@ export function defaultRunner(args, options = {}) {
|
|
|
228
242
|
const stdout = tailCollector();
|
|
229
243
|
const stderr = tailCollector();
|
|
230
244
|
let settled = false;
|
|
231
|
-
let
|
|
232
|
-
let killTimer;
|
|
245
|
+
let failure = null;
|
|
233
246
|
let timeout;
|
|
234
247
|
const settle = (finish) => {
|
|
235
248
|
if (settled)
|
|
236
249
|
return;
|
|
237
250
|
settled = true;
|
|
238
251
|
clearTimeout(timeout);
|
|
239
|
-
if (killTimer)
|
|
240
|
-
clearTimeout(killTimer);
|
|
241
252
|
finish();
|
|
242
253
|
};
|
|
243
|
-
|
|
244
|
-
if (settled)
|
|
254
|
+
const fail = (error) => {
|
|
255
|
+
if (settled || failure)
|
|
245
256
|
return;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
failure = error;
|
|
258
|
+
clearTimeout(timeout);
|
|
259
|
+
// Docker launches ssh, which can retain these pipes after Docker exits.
|
|
260
|
+
// Only this command's private process group is ours to stop; persistent
|
|
261
|
+
// SSH masters detach from it and may serve other commands. Hold callers'
|
|
262
|
+
// lifecycle ownership until the bounded tree cleanup has completed.
|
|
263
|
+
const cleaned = () => {
|
|
264
|
+
child.stdin.destroy();
|
|
265
|
+
child.stdout.destroy();
|
|
266
|
+
child.stderr.destroy();
|
|
267
|
+
settle(() => reject(error));
|
|
268
|
+
};
|
|
269
|
+
void killCliTree(child, COMMAND_TIMEOUT_TERM_GRACE_MS).then(cleaned, cleaned);
|
|
270
|
+
};
|
|
271
|
+
timeout = setTimeout(() => fail(new Error("Docker-over-SSH command timed out")), options.timeoutMs ?? 120_000);
|
|
256
272
|
timeout.unref?.();
|
|
257
273
|
child.stdout.setEncoding("utf8");
|
|
258
274
|
child.stderr.setEncoding("utf8");
|
|
@@ -263,18 +279,14 @@ export function defaultRunner(args, options = {}) {
|
|
|
263
279
|
stderr.push(chunk);
|
|
264
280
|
});
|
|
265
281
|
child.stdin.on("error", (error) => {
|
|
266
|
-
|
|
267
|
-
return;
|
|
268
|
-
settle(() => reject(new Error(`Docker-over-SSH stdin failed: ${error.message}`)));
|
|
282
|
+
fail(new Error(`Docker-over-SSH stdin failed: ${error.message}`));
|
|
269
283
|
});
|
|
270
284
|
child.on("error", (error) => {
|
|
271
|
-
|
|
285
|
+
fail(new Error(`Docker-over-SSH could not start: ${error.message}`));
|
|
272
286
|
});
|
|
273
287
|
child.on("close", (code, signal) => {
|
|
274
|
-
if (
|
|
275
|
-
settle(() => reject(new Error("Docker-over-SSH command timed out")));
|
|
288
|
+
if (failure)
|
|
276
289
|
return;
|
|
277
|
-
}
|
|
278
290
|
settle(() => {
|
|
279
291
|
if (code === 0)
|
|
280
292
|
return resolve({ stdout: stdout.text(), stderr: stderr.text() });
|
|
@@ -286,7 +298,7 @@ export function defaultRunner(args, options = {}) {
|
|
|
286
298
|
child.stdin.end(options.input);
|
|
287
299
|
}
|
|
288
300
|
catch (error) {
|
|
289
|
-
|
|
301
|
+
fail(new Error(`Docker-over-SSH stdin failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
290
302
|
}
|
|
291
303
|
});
|
|
292
304
|
}
|
|
@@ -531,15 +543,30 @@ export async function vpsComputerStatus(cfg, botId, runner = defaultRunner) {
|
|
|
531
543
|
if (cacheable) {
|
|
532
544
|
// A capture already checks this exact target. Let it finish instead of
|
|
533
545
|
// opening another six SSH connections while its readiness check is cold.
|
|
534
|
-
|
|
546
|
+
const capture = pendingScreenshots.get(key);
|
|
547
|
+
if (capture)
|
|
548
|
+
await capture.catch(() => { });
|
|
535
549
|
const cached = statusCache.get(key);
|
|
536
550
|
if (cached && cached.expiresAt > Date.now())
|
|
537
551
|
return cached.status;
|
|
552
|
+
const pending = pendingStatuses.get(key);
|
|
553
|
+
if (pending)
|
|
554
|
+
return pending;
|
|
538
555
|
}
|
|
539
|
-
const
|
|
556
|
+
const inspection = computeVpsComputerStatus(cfg, botId, runner);
|
|
540
557
|
if (cacheable)
|
|
541
|
-
|
|
542
|
-
|
|
558
|
+
pendingStatuses.set(key, inspection);
|
|
559
|
+
try {
|
|
560
|
+
const status = await inspection;
|
|
561
|
+
if (cacheable && pendingStatuses.get(key) === inspection && !lifecycleLocks.has(key)) {
|
|
562
|
+
statusCache.set(key, { status, expiresAt: Date.now() + STATUS_CACHE_TTL_MS });
|
|
563
|
+
}
|
|
564
|
+
return status;
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
if (cacheable && pendingStatuses.get(key) === inspection)
|
|
568
|
+
pendingStatuses.delete(key);
|
|
569
|
+
}
|
|
543
570
|
}
|
|
544
571
|
const VPS_INVENTORY_LIMIT = 256;
|
|
545
572
|
const VPS_INVENTORY_STATES = new Set([
|
|
@@ -761,14 +788,32 @@ async function prepareVpsImage(alias, runner) {
|
|
|
761
788
|
* Backoff doubles 0.5s→4s because each poll is a real SSH connection, and
|
|
762
789
|
* between polls only ONE cheap exec (`cua-driver status`) asks whether the
|
|
763
790
|
* driver answers yet — the full multi-invocation status runs again only when
|
|
764
|
-
* that predicate flips
|
|
765
|
-
*
|
|
791
|
+
* that predicate flips. Every command, including the diagnostic tail, is
|
|
792
|
+
* clamped to the readiness phase's deadline and transport cleanup allowance. */
|
|
766
793
|
async function waitForVpsReady(cfg, botId, runner, budgetMs = 60_000) {
|
|
767
794
|
const alias = vpsSshAlias(cfg);
|
|
768
795
|
const deadline = Date.now() + budgetMs;
|
|
769
|
-
let
|
|
796
|
+
let budgetExpired = false;
|
|
797
|
+
const boundedRunner = async (args, options = {}) => {
|
|
798
|
+
// Include transport termination in the readiness phase's budget; a
|
|
799
|
+
// final failed probe or diagnostic tail must not keep its lock past it.
|
|
800
|
+
const remaining = deadline - Date.now() - COMMAND_TIMEOUT_KILL_GRACE_MS;
|
|
801
|
+
if (remaining <= 0) {
|
|
802
|
+
budgetExpired = true;
|
|
803
|
+
throw new Error("VPS desktop readiness timed out");
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
return await runner(args, { ...options, timeoutMs: Math.min(options.timeoutMs ?? 120_000, remaining) });
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
if (Date.now() >= deadline - COMMAND_TIMEOUT_KILL_GRACE_MS)
|
|
810
|
+
budgetExpired = true;
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
let status = await computeVpsComputerStatus(cfg, botId, boundedRunner);
|
|
770
815
|
let delayMs = 500;
|
|
771
|
-
while (!status.ready && alias && Date.now() < deadline) {
|
|
816
|
+
while (!status.ready && alias && !budgetExpired && Date.now() < deadline) {
|
|
772
817
|
if (!status.daemonUp ||
|
|
773
818
|
!status.image ||
|
|
774
819
|
!status.imageMatches ||
|
|
@@ -785,14 +830,24 @@ async function waitForVpsReady(cfg, botId, runner, budgetMs = 60_000) {
|
|
|
785
830
|
});
|
|
786
831
|
delayMs = Math.min(delayMs * 2, 4_000);
|
|
787
832
|
const container = status.container_id ?? status.container_name;
|
|
788
|
-
const driverAnswers = await
|
|
833
|
+
const driverAnswers = await boundedRunner(vpsDockerArgs(alias, cuaExecArgs(["status", "--socket", CUA_SOCKET], { container })), { timeoutMs: 10_000 }).then(() => true, () => false);
|
|
834
|
+
if (budgetExpired)
|
|
835
|
+
break;
|
|
789
836
|
if (!driverAnswers && Date.now() < deadline)
|
|
790
837
|
continue;
|
|
791
|
-
status = await computeVpsComputerStatus(cfg, botId,
|
|
838
|
+
status = await computeVpsComputerStatus(cfg, botId, boundedRunner);
|
|
792
839
|
}
|
|
840
|
+
if (budgetExpired)
|
|
841
|
+
return {
|
|
842
|
+
...status,
|
|
843
|
+
ready: false,
|
|
844
|
+
desktopReady: false,
|
|
845
|
+
desktop_error: "VPS desktop readiness timed out",
|
|
846
|
+
problem: "The VPS desktop did not become ready in time — retry when the connection recovers",
|
|
847
|
+
};
|
|
793
848
|
return status;
|
|
794
849
|
}
|
|
795
|
-
async function withVpsLifecycleLock(key, operation) {
|
|
850
|
+
async function withVpsLifecycleLock(key, operation, acquireTimeoutMs = LOCK_ACQUIRE_TIMEOUT_MS) {
|
|
796
851
|
const previous = lifecycleLocks.get(key);
|
|
797
852
|
let release;
|
|
798
853
|
const current = new Promise((resolve) => {
|
|
@@ -804,7 +859,7 @@ async function withVpsLifecycleLock(key, operation) {
|
|
|
804
859
|
const acquired = await Promise.race([
|
|
805
860
|
previous.then(() => true),
|
|
806
861
|
new Promise((resolve) => {
|
|
807
|
-
acquireTimer = setTimeout(() => resolve(false),
|
|
862
|
+
acquireTimer = setTimeout(() => resolve(false), acquireTimeoutMs);
|
|
808
863
|
acquireTimer.unref?.();
|
|
809
864
|
}),
|
|
810
865
|
]);
|
|
@@ -849,6 +904,7 @@ export async function removeManagedVpsComputer(cfg, owners, containerName, confi
|
|
|
849
904
|
}
|
|
850
905
|
const key = `${alias}:${containerName}`;
|
|
851
906
|
return withVpsLifecycleLock(key, async () => {
|
|
907
|
+
invalidateVpsStatus(key);
|
|
852
908
|
const scan = await scanManagedVpsComputers(cfg, owners, runner);
|
|
853
909
|
const inventory = scan.inventory;
|
|
854
910
|
if (!inventory.available) {
|
|
@@ -874,13 +930,14 @@ export async function removeManagedVpsComputer(cfg, owners, containerName, confi
|
|
|
874
930
|
await runner(vpsDockerArgs(alias, ["rm", "-f", containerId]), { timeoutMs: 2 * 60_000 });
|
|
875
931
|
}
|
|
876
932
|
catch (error) {
|
|
933
|
+
invalidateVpsStatus(key);
|
|
877
934
|
const detail = (error instanceof Error ? error.message : String(error))
|
|
878
935
|
.replace(/[\r\n\t]+/g, " ")
|
|
879
936
|
.trim()
|
|
880
937
|
.slice(0, 200);
|
|
881
938
|
throw Object.assign(new Error(`The VPS refused to remove this computer${detail ? `: ${detail}` : ""}`), { status: 502 });
|
|
882
939
|
}
|
|
883
|
-
|
|
940
|
+
invalidateVpsStatus(key);
|
|
884
941
|
viewerConnections.delete(key);
|
|
885
942
|
if (instance.ownerBotId)
|
|
886
943
|
stopDesktopTunnel(instance.ownerBotId);
|
|
@@ -893,14 +950,19 @@ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunn
|
|
|
893
950
|
if (!alias)
|
|
894
951
|
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in App Settings → Connections"), { status: 409 });
|
|
895
952
|
const key = `${alias}:${vpsContainerName(botId)}`;
|
|
953
|
+
const pending = action === "provision" ? pendingProvisions.get(key) : undefined;
|
|
954
|
+
if (pending)
|
|
955
|
+
return pending;
|
|
896
956
|
const operation = async () => {
|
|
897
957
|
// A mutation invalidates every cached poll answer, before and after: the
|
|
898
958
|
// panel must never keep showing the pre-action world for a TTL.
|
|
899
|
-
|
|
959
|
+
invalidateVpsStatus(key);
|
|
900
960
|
try {
|
|
901
961
|
const before = await computeVpsComputerStatus(cfg, botId, runner);
|
|
902
962
|
if (!before.daemonUp)
|
|
903
963
|
throw Object.assign(new Error(before.problem ?? "Docker over SSH is not reachable"), { status: 409 });
|
|
964
|
+
if (action === "provision" && before.ready)
|
|
965
|
+
return before;
|
|
904
966
|
// A real lifecycle change invalidates the remote endpoint. Provision
|
|
905
967
|
// is also the turn-start idempotency path, so leave an already-running
|
|
906
968
|
// viewer alone when no start/replacement will occur.
|
|
@@ -946,7 +1008,7 @@ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunn
|
|
|
946
1008
|
throw Object.assign(new Error("The VPS container name is occupied by a container OpenMausBot did not create — remove it on the VPS yourself"), { status: 409 });
|
|
947
1009
|
}
|
|
948
1010
|
await run(["rm", "-f", containerRef]);
|
|
949
|
-
return computeVpsComputerStatus(cfg, botId, runner);
|
|
1011
|
+
return await computeVpsComputerStatus(cfg, botId, runner);
|
|
950
1012
|
}
|
|
951
1013
|
else {
|
|
952
1014
|
if (before.container !== "running")
|
|
@@ -954,13 +1016,22 @@ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunn
|
|
|
954
1016
|
assertUsableContainer(before);
|
|
955
1017
|
await run(["stop", containerRef]);
|
|
956
1018
|
}
|
|
957
|
-
return action === "stop" ? computeVpsComputerStatus(cfg, botId, runner) : waitForVpsReady(cfg, botId, runner);
|
|
1019
|
+
return await (action === "stop" ? computeVpsComputerStatus(cfg, botId, runner) : waitForVpsReady(cfg, botId, runner));
|
|
958
1020
|
}
|
|
959
1021
|
finally {
|
|
960
|
-
|
|
1022
|
+
invalidateVpsStatus(key);
|
|
961
1023
|
}
|
|
962
1024
|
};
|
|
963
|
-
|
|
1025
|
+
const preparation = withVpsLifecycleLock(key, operation, action === "provision" ? PREPARATION_LOCK_TIMEOUT_MS : LOCK_ACQUIRE_TIMEOUT_MS);
|
|
1026
|
+
if (action === "provision")
|
|
1027
|
+
pendingProvisions.set(key, preparation);
|
|
1028
|
+
try {
|
|
1029
|
+
return await preparation;
|
|
1030
|
+
}
|
|
1031
|
+
finally {
|
|
1032
|
+
if (pendingProvisions.get(key) === preparation)
|
|
1033
|
+
pendingProvisions.delete(key);
|
|
1034
|
+
}
|
|
964
1035
|
}
|
|
965
1036
|
/** Auto is intentionally read-only: it can attach only to an existing ready
|
|
966
1037
|
* container. It recomputes rather than reading the poll cache — routing a
|
|
@@ -976,7 +1047,7 @@ export async function inspectVpsForAuto(cfg, botId, runner = defaultRunner) {
|
|
|
976
1047
|
cfg = snapshotVpsConfig(cfg);
|
|
977
1048
|
const key = vpsLockKey(cfg, botId);
|
|
978
1049
|
return key
|
|
979
|
-
? withVpsLifecycleLock(key, () => computeVpsComputerStatus(cfg, botId, runner))
|
|
1050
|
+
? withVpsLifecycleLock(key, () => computeVpsComputerStatus(cfg, botId, runner), PREPARATION_LOCK_TIMEOUT_MS)
|
|
980
1051
|
: computeVpsComputerStatus(cfg, botId, runner);
|
|
981
1052
|
}
|
|
982
1053
|
/** Open a temporary noVNC connection through the configured SSH alias. The
|
|
@@ -1116,9 +1187,26 @@ export async function vpsComputerScreenshot(cfg, botId, runner = defaultRunner)
|
|
|
1116
1187
|
// poller runs every few seconds, and re-verifying the whole container
|
|
1117
1188
|
// between frames multiplied every frame's SSH cost.
|
|
1118
1189
|
const cached = cacheable ? statusCache.get(key) : undefined;
|
|
1190
|
+
const pendingStatus = cacheable ? pendingStatuses.get(key) : undefined;
|
|
1191
|
+
// A poll which started first already owns the inspection. Sharing its
|
|
1192
|
+
// result must not extend this preview's own deadline; the poll remains
|
|
1193
|
+
// independently owned if it is still checking when this preview expires.
|
|
1194
|
+
let statusTimer;
|
|
1119
1195
|
const status = cached && cached.expiresAt > Date.now()
|
|
1120
1196
|
? cached.status
|
|
1121
|
-
:
|
|
1197
|
+
: pendingStatus
|
|
1198
|
+
? await Promise.race([
|
|
1199
|
+
pendingStatus,
|
|
1200
|
+
new Promise((_resolve, reject) => {
|
|
1201
|
+
statusTimer = setTimeout(() => {
|
|
1202
|
+
invalidateVpsStatus(key);
|
|
1203
|
+
reject(timeoutError());
|
|
1204
|
+
}, Math.max(0, workDeadline - Date.now()));
|
|
1205
|
+
statusTimer.unref?.();
|
|
1206
|
+
}),
|
|
1207
|
+
]).finally(() => { if (statusTimer)
|
|
1208
|
+
clearTimeout(statusTimer); })
|
|
1209
|
+
: await computeVpsComputerStatus(cfg, botId, boundedRunner);
|
|
1122
1210
|
// Status converts transport errors into displayable state. A deadline is
|
|
1123
1211
|
// still a timeout, not evidence that this container became incompatible.
|
|
1124
1212
|
if (budgetExpired) {
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// mcp-bridge.ts, shared with the Local VM entry point.
|
|
6
6
|
import { runMcpBridge } from "./mcp-bridge.js";
|
|
7
7
|
import { vpsContainerMcpArgs, vpsDockerArgs } from "./vps-computer.js";
|
|
8
|
+
import { DATA_DIR } from "./config.js";
|
|
9
|
+
import { augmentedPath } from "./env-path.js";
|
|
10
|
+
import { prepareVpsSsh } from "./vps-ssh.js";
|
|
8
11
|
const [alias, containerName] = process.argv.slice(2);
|
|
9
12
|
const sshAlias = alias ?? "";
|
|
10
13
|
let args;
|
|
@@ -19,9 +22,13 @@ catch {
|
|
|
19
22
|
// through `ps`, and the token guards a loopback endpoint.
|
|
20
23
|
const controlUrl = process.env.OMB_CONTROL_URL ?? "";
|
|
21
24
|
const controlToken = process.env.OMB_CONTROL_TOKEN ?? "";
|
|
25
|
+
const ssh = prepareVpsSsh(DATA_DIR, augmentedPath());
|
|
22
26
|
runMcpBridge({
|
|
23
27
|
command: "docker",
|
|
24
28
|
args,
|
|
29
|
+
// Keep tool calls and the watchdog on the same bounded, shared transport
|
|
30
|
+
// as startup and previews; otherwise only the panel gets the SSH defaults.
|
|
31
|
+
env: { ...process.env, PATH: ssh.path },
|
|
25
32
|
label: "VPS Cua Driver",
|
|
26
33
|
// The probe checks the TRANSPORT (SSH + daemon), deliberately not the
|
|
27
34
|
// driver: a busy desktop mid-tool-call must never look dead, while an
|
|
@@ -10,10 +10,39 @@
|
|
|
10
10
|
// that includes theirs first (so anything they set still wins) and fills in
|
|
11
11
|
// connection sharing and fail-fast timeouts, plus an `ssh` shim docker finds
|
|
12
12
|
// ahead of the real one that points at that config.
|
|
13
|
-
import {
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { homedir } from "node:os";
|
|
15
|
-
import { delimiter, join } from "node:path";
|
|
16
|
+
import { delimiter, join, resolve } from "node:path";
|
|
16
17
|
export const VPS_SSH_DIR = "ssh";
|
|
18
|
+
function shellQuote(value) {
|
|
19
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
20
|
+
}
|
|
21
|
+
/** macOS limits Unix socket paths to 104 bytes, including the terminator.
|
|
22
|
+
* OpenSSH also appends a temporary suffix while creating a master socket.
|
|
23
|
+
* A long application/test data path must not disable every SSH command. */
|
|
24
|
+
function controlDirectory(sshDir) {
|
|
25
|
+
if (Buffer.byteLength(join(sshDir, `cm-${"0".repeat(40)}`)) <= 80)
|
|
26
|
+
return sshDir;
|
|
27
|
+
const uid = process.getuid();
|
|
28
|
+
const id = createHash("sha256").update(resolve(sshDir)).digest("hex").slice(0, 12);
|
|
29
|
+
// /tmp is intentionally used instead of tmpdir(): macOS TMPDIR is itself
|
|
30
|
+
// often too long for a control socket. Validate before chmod or use so an
|
|
31
|
+
// existing symlink/other user's directory cannot redirect the socket.
|
|
32
|
+
const dir = `/tmp/omb-ssh-${uid}-${id}`;
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code !== "EEXIST")
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
const stat = lstatSync(dir);
|
|
41
|
+
if (!stat.isDirectory() || stat.uid !== uid)
|
|
42
|
+
throw new Error("The VPS SSH control directory is not private to this user");
|
|
43
|
+
chmodSync(dir, 0o700);
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
17
46
|
function findExecutable(name, pathValue, exclude) {
|
|
18
47
|
for (const dir of pathValue.split(delimiter)) {
|
|
19
48
|
if (!dir || dir === exclude)
|
|
@@ -49,15 +78,15 @@ export function vpsSshConfigText(sshDir, userConfig = join(homedir(), ".ssh", "c
|
|
|
49
78
|
return [
|
|
50
79
|
"# Written by OpenMausBot for its VPS computer connections. Do not edit;",
|
|
51
80
|
"# it is regenerated. Your own ~/.ssh/config is included first and wins.",
|
|
52
|
-
...(existsSync(userConfig) ? [`Include ${userConfig}`] : []),
|
|
81
|
+
...(existsSync(userConfig) ? [`Include ${JSON.stringify(userConfig)}`] : []),
|
|
53
82
|
"Host *",
|
|
54
83
|
" ControlMaster auto",
|
|
55
|
-
` ControlPath ${join(sshDir, "cm-%C")}`,
|
|
84
|
+
` ControlPath ${JSON.stringify(join(sshDir.replace(/%/g, "%%"), "cm-%C"))}`,
|
|
56
85
|
" ControlPersist 10m",
|
|
57
86
|
" ServerAliveInterval 15",
|
|
58
87
|
" ServerAliveCountMax 3",
|
|
59
88
|
" ConnectTimeout 10",
|
|
60
|
-
...(existsSync(systemConfig) ? [`Include ${systemConfig}`] : []),
|
|
89
|
+
...(existsSync(systemConfig) ? [`Include ${JSON.stringify(systemConfig)}`] : []),
|
|
61
90
|
"",
|
|
62
91
|
].join("\n");
|
|
63
92
|
}
|
|
@@ -72,7 +101,7 @@ export function prepareVpsSsh(dataDir, pathValue, platform = process.platform) {
|
|
|
72
101
|
mkdirSync(binDir, { recursive: true, mode: 0o700 });
|
|
73
102
|
chmodSync(sshDir, 0o700);
|
|
74
103
|
const configPath = join(sshDir, "config");
|
|
75
|
-
writeIfChanged(configPath, vpsSshConfigText(sshDir), 0o600);
|
|
104
|
+
writeIfChanged(configPath, vpsSshConfigText(controlDirectory(sshDir)), 0o600);
|
|
76
105
|
const realSsh = findExecutable("ssh", pathValue, binDir);
|
|
77
106
|
if (!realSsh)
|
|
78
107
|
return { configPath, path: pathValue };
|
|
@@ -80,7 +109,7 @@ export function prepareVpsSsh(dataDir, pathValue, platform = process.platform) {
|
|
|
80
109
|
"#!/bin/sh",
|
|
81
110
|
"# Written by OpenMausBot. docker's SSH transport finds this ssh first, so",
|
|
82
111
|
"# every VPS command shares one connection whether or not the alias says so.",
|
|
83
|
-
`exec ${
|
|
112
|
+
`exec ${shellQuote(realSsh)} -F ${shellQuote(configPath)} "$@"`,
|
|
84
113
|
"",
|
|
85
114
|
].join("\n");
|
|
86
115
|
writeIfChanged(join(binDir, "ssh"), shim, 0o700);
|
|
@@ -22,10 +22,22 @@ export function restoredWorkspaceConfig(portable, destination) {
|
|
|
22
22
|
}
|
|
23
23
|
/** Exact app-owned authentication paths, not a scan of user document text. */
|
|
24
24
|
export function excludedWorkspaceAuthPath(path) {
|
|
25
|
+
// Machine/provider-specific execution grants are not portable template data.
|
|
26
|
+
if (/^command-allowlist\.json(?:$|\.\d+\.[0-9a-f-]+\.tmp$)/.test(path))
|
|
27
|
+
return true;
|
|
25
28
|
return /^(?:(?:providers|caddy|chrome-profile|\.agent-browser)(?:\/|$)|workspace-credentials\.json$|external-runtimes\.json$|browser-engine-key$)/.test(path) ||
|
|
26
29
|
/^(?:config\.json|webhooks\.json|workspace-credentials\.json|external-runtimes\.json|browser-engine-key|sessions\.json|tunnel-account\.json)\.\d+(?:\.[0-9a-f-]+)?\.tmp$/.test(path) ||
|
|
27
30
|
/^(?:vm-home|vm-homes\/[^/]+)\/\.browser-profiles(?:\/|$)/.test(path);
|
|
28
31
|
}
|
|
32
|
+
/** The Organization library files Electron main downloads and verifies
|
|
33
|
+
* (docs/desktop-library.md): the catalog, a catalog write in progress, and the
|
|
34
|
+
* cached release files. Main fetches them again from Admin, and sign-out
|
|
35
|
+
* deletes them, so a backup never keeps an Organization's package bytes. The
|
|
36
|
+
* runtime's own `org-library/state.json` and `presets.json` are backed up. Like
|
|
37
|
+
* the hook tokens below, an archive that holds them still restores. */
|
|
38
|
+
export function redownloadedOrgLibraryPath(path) {
|
|
39
|
+
return /^org-library\/(?:catalog\.json(?:\.[0-9a-f-]{36}\.tmp)?$|blobs(?:\/|$))/.test(path);
|
|
40
|
+
}
|
|
29
41
|
/** Per-turn engine hook bearers (`hook-tokens/<digest>.token`, written by the
|
|
30
42
|
* Claude driver). They are dead once their turn settles, so they are never
|
|
31
43
|
* exported. Unlike the saved auth paths above they are not refused on import:
|
|
@@ -6,12 +6,13 @@ import { isAbsolute, join, parse, posix, relative, resolve, win32 } from "node:p
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { backup, DatabaseSync } from "node:sqlite";
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { Worker } from "node:worker_threads";
|
|
9
10
|
import * as tar from "tar";
|
|
10
11
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
11
12
|
import { writeFileAtomic } from "./atomic.js";
|
|
12
13
|
import { escapeAttribute, splitTranscriptAttachments } from "../src/lib/composer-attachments.js";
|
|
13
14
|
import { WORKSPACE_BACKUP_CLIENT_KEYS } from "../shared/workspace-backup-client.js";
|
|
14
|
-
import { ephemeralWorkspaceTokenPath, excludedWorkspaceAuthPath, portableWorkspaceConfig, restoredWorkspaceConfig } from "./workspace-backup-policy.js";
|
|
15
|
+
import { ephemeralWorkspaceTokenPath, excludedWorkspaceAuthPath, portableWorkspaceConfig, redownloadedOrgLibraryPath, restoredWorkspaceConfig } from "./workspace-backup-policy.js";
|
|
15
16
|
export const MAX_WORKSPACE_BACKUP_BYTES = 10 * 1024 ** 3;
|
|
16
17
|
export const MAX_WORKSPACE_BACKUP_FILES = 100_000;
|
|
17
18
|
export const MAX_WORKSPACE_BACKUP_UPLOAD_BYTES = MAX_WORKSPACE_BACKUP_BYTES + 256 * 1024 ** 2;
|
|
@@ -77,8 +78,7 @@ function jobPath(dataDir, id) {
|
|
|
77
78
|
}
|
|
78
79
|
return path;
|
|
79
80
|
}
|
|
80
|
-
function newJob(dataDir) {
|
|
81
|
-
const id = randomUUID();
|
|
81
|
+
function newJob(dataDir, id = randomUUID()) {
|
|
82
82
|
const directory = jobPath(dataDir, id);
|
|
83
83
|
mkdirSync(directory, { mode: 0o700 });
|
|
84
84
|
return { id, directory };
|
|
@@ -265,13 +265,50 @@ function databaseCounts(path) {
|
|
|
265
265
|
db.close();
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
|
+
/** The existing maintenance gate owns exclusivity until this worker has exited.
|
|
269
|
+
* Keep synchronous file validation/fsync intact, but off the request thread. */
|
|
268
270
|
export async function createWorkspaceBackup(dataDir, options) {
|
|
271
|
+
const source = new URL("./workspace-backup.worker.ts", import.meta.url);
|
|
272
|
+
// Assign ownership before the worker can create staging, including crashes
|
|
273
|
+
// too early to send a job-created message back to this process.
|
|
274
|
+
const jobId = randomUUID();
|
|
275
|
+
let worker;
|
|
276
|
+
let completed = false;
|
|
277
|
+
try {
|
|
278
|
+
worker = new Worker(existsSync(source) ? source : new URL("./workspace-backup.worker.js", import.meta.url), {
|
|
279
|
+
workerData: { dataDir, options, jobId }, execArgv: [],
|
|
280
|
+
});
|
|
281
|
+
const result = await new Promise((resolveBackup, reject) => {
|
|
282
|
+
const failed = () => reject(new Error("The backup worker stopped before completing. Try again."));
|
|
283
|
+
worker.once("message", (reply) => {
|
|
284
|
+
if (reply.result)
|
|
285
|
+
resolveBackup(reply.result);
|
|
286
|
+
else if (reply.error)
|
|
287
|
+
reject(new Error(reply.error));
|
|
288
|
+
else
|
|
289
|
+
failed();
|
|
290
|
+
});
|
|
291
|
+
worker.once("error", failed);
|
|
292
|
+
worker.once("exit", failed);
|
|
293
|
+
});
|
|
294
|
+
completed = true;
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
await worker?.terminate();
|
|
299
|
+
// Reuse the guarded deletion path: never sweep other jobs or recovery.
|
|
300
|
+
if (!completed && entryExists(join(dataDir, ".backups", jobId)))
|
|
301
|
+
removeWorkspaceBackupJob(dataDir, jobId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** Worker implementation; application callers use createWorkspaceBackup. */
|
|
305
|
+
export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
|
|
269
306
|
if (Object.hasOwn(options, "credentials"))
|
|
270
307
|
throw new Error("Workspace backups do not transfer credentials.");
|
|
271
308
|
assertLocalAuthOutsideSnapshot(dataDir);
|
|
272
309
|
const salt = randomBytes(16);
|
|
273
310
|
const key = await passwordKey(options.password, salt);
|
|
274
|
-
const job = newJob(dataDir);
|
|
311
|
+
const job = newJob(dataDir, jobId);
|
|
275
312
|
const snapshot = join(job.directory, "snapshot");
|
|
276
313
|
folder(join(snapshot, "data"));
|
|
277
314
|
const entries = [];
|
|
@@ -311,7 +348,7 @@ export async function createWorkspaceBackup(dataDir, options) {
|
|
|
311
348
|
if (!prefix && excluded(name))
|
|
312
349
|
continue;
|
|
313
350
|
const path = prefix ? `${prefix}/${name}` : name;
|
|
314
|
-
if (excludedWorkspaceAuthPath(path) || ephemeralWorkspaceTokenPath(path))
|
|
351
|
+
if (excludedWorkspaceAuthPath(path) || ephemeralWorkspaceTokenPath(path) || redownloadedOrgLibraryPath(path))
|
|
315
352
|
continue;
|
|
316
353
|
// Do not silently skip noncanonical source spellings: reject them so
|
|
317
354
|
// a case-sensitive host cannot export auth paths active on Windows/Mac.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { createWorkspaceBackupSnapshot } from "./workspace-backup.js";
|
|
3
|
+
const { dataDir, options, jobId } = workerData;
|
|
4
|
+
try {
|
|
5
|
+
// Same snapshot and cleanup implementation; never send password/options back.
|
|
6
|
+
parentPort.postMessage({ result: await createWorkspaceBackupSnapshot(dataDir, options, jobId) });
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
parentPort.postMessage({ error: error instanceof Error ? error.message : "The workspace backup could not finish." });
|
|
10
|
+
}
|
|
@@ -18,7 +18,7 @@ export const WORKSPACES_DIR = join(DATA_DIR, "workspaces");
|
|
|
18
18
|
export const TASK_WORKSPACES_DIR = join(DATA_DIR, "task-workspaces");
|
|
19
19
|
/** MCP support does not imply native filesystem tools or a local working directory. */
|
|
20
20
|
export function supportsWorkspaceFiles(driverKind) {
|
|
21
|
-
return !["grok", "openai-compat", "minimax", "boxAgent"].includes(driverKind);
|
|
21
|
+
return !["grok", "openai-compat", "minimax", "mistral", "boxAgent"].includes(driverKind);
|
|
22
22
|
}
|
|
23
23
|
/** Default task files are private to the thread, outside the bot's shared
|
|
24
24
|
* memory folder. This is directory organization, not a shell sandbox. */
|
|
@@ -21,7 +21,7 @@ export function supportsApprovalMode(driverKind, mode) {
|
|
|
21
21
|
// engines could never stop asking — not by its own level, and not through
|
|
22
22
|
// a Chief's delegated Full access either.
|
|
23
23
|
return ["codex", "claudeAgent", "antigravityAgent", "cursorAgent", "grokAgent", "opencodeGo", "qwenAgent", "geminiAgent",
|
|
24
|
-
"openai-compat", "grok", "minimax"].includes(driverKind ?? "");
|
|
24
|
+
"openai-compat", "grok", "minimax", "mistral"].includes(driverKind ?? "");
|
|
25
25
|
}
|
|
26
26
|
/** A Full/Custom grant belongs to one provider's tool semantics. Other
|
|
27
27
|
* modes carry across only when the destination actually implements them.
|