openmausbot 0.1.96 → 0.1.98
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/ActivityPanel-H6D3SD39.js +1 -0
- package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
- package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
- package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
- package/dist/assets/DesktopViewer-utONms1p.js +5 -0
- package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
- package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
- package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
- package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
- package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
- package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
- package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
- package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
- package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
- package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
- package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
- package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
- package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
- package/dist/assets/activity-BxOkayvD.js +1 -0
- package/dist/assets/{arc-Duj5Coy2.js → arc-CJNA94c6.js} +1 -1
- package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
- package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
- package/dist/assets/brain-CFZ7PEXc.js +1 -0
- package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
- package/dist/assets/channel-WhFyujhZ.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
- package/dist/assets/computer-control-NLov352p.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
- package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
- package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
- package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
- package/dist/assets/image-plus-BbVjGHin.js +1 -0
- package/dist/assets/index-BrPW5HDO.css +1 -0
- package/dist/assets/index-C3NqfdVU.js +585 -0
- package/dist/assets/{index-DreDnjOu.js → index-CJTp9JEp.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
- package/dist/assets/{linear-Bl669148.js → linear-BVVKbEMy.js} +1 -1
- package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-JJdoKyKB.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
- package/dist/assets/module-DCUsy2BQ.js +3 -0
- package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
- package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
- package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
- package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
- package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
- package/dist/assets/user-DcZBnAuv.js +1 -0
- package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +20 -9
- package/dist-server/container-mcp.js +8 -0
- package/dist-server/desktop-entry.mjs +54 -0
- package/dist-server/drivers/agents-proxy.js +26 -9
- package/dist-server/index.js +3734 -2360
- package/dist-server/openmausbot.js +1664 -1553
- package/dist-server/pair-cli.js +1664 -1553
- package/dist-server/permission-proxy.js +2 -4
- package/dist-server/server/atomic.js +70 -2
- package/dist-server/server/auto-approve.js +1 -1
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +68 -84
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +275 -44
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +4 -4
- package/dist-server/server/cloud-home.js +3 -9
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +29 -12
- package/dist-server/server/connector-card-text.js +15 -0
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/default-model-selection.js +23 -14
- package/dist-server/server/delegations.js +11 -5
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +11 -1
- package/dist-server/server/drivers/acp/grok.js +47 -5
- package/dist-server/server/drivers/acp/opencode-go.js +16 -21
- package/dist-server/server/drivers/agents-call.js +1 -1
- package/dist-server/server/drivers/agents-catalog.js +27 -7
- package/dist-server/server/drivers/agents-proxy.js +1 -0
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
- package/dist-server/server/drivers/claude-login-auth.js +1 -1
- package/dist-server/server/drivers/claude.js +84 -41
- package/dist-server/server/drivers/codex-device-auth.js +27 -319
- package/dist-server/server/drivers/codex.js +2 -2
- package/dist-server/server/drivers/device-auth.js +342 -0
- package/dist-server/server/drivers/openai-chat.js +110 -10
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/fleet-agent.js +18 -5
- package/dist-server/server/harness/http.js +5 -2
- package/dist-server/server/harness/registry.js +19 -0
- package/dist-server/server/harness-capabilities.js +7 -4
- package/dist-server/server/hosted-models.js +3 -1
- package/dist-server/server/index.js +823 -333
- package/dist-server/server/key-rejections.js +57 -0
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/message-file.js +7 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-roster.js +10 -2
- package/dist-server/server/permission-proxy.js +4 -0
- package/dist-server/server/provider-key-check.js +5 -1
- package/dist-server/server/request-auth.js +1 -1
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/room-turn-timeout.js +28 -0
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routes/thread-models.js +36 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +3 -3
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/skills.js +3 -2
- package/dist-server/server/store.js +273 -61
- package/dist-server/server/surface.js +29 -49
- package/dist-server/server/system-prompt.js +10 -10
- package/dist-server/server/team-backup.js +19 -4
- package/dist-server/server/thread-model.js +25 -0
- package/dist-server/server/turn-continuation.js +168 -0
- package/dist-server/server/webhook-ingress.js +8 -3
- package/dist-server/server/webhooks.js +40 -5
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/ask-question.js +0 -70
- package/dist-server/shared/cloud-computer.js +4 -6
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/device-sign-in.js +57 -0
- package/dist-server/shared/failed-turn.js +1 -4
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/shared/thread-model.js +13 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +8 -0
- package/dist-server/workspace-backup.worker.js +41 -22
- package/package.json +1 -1
- package/dist/assets/DesktopViewer-DsnA23KM.js +0 -5
- package/dist/assets/architectureDiagram-NJMV4G6O-uYUsDawP.js +0 -36
- package/dist/assets/channel-BZxwC-Gw.js +0 -1
- package/dist/assets/cose-bilkent-JH36ORCC-D-Uglxqq.js +0 -1
- package/dist/assets/ganttDiagram-FUAMR5RP-CKV5D2F6.js +0 -292
- package/dist/assets/index-Du7o6Dsf.js +0 -610
- package/dist/assets/index-KxG4bWSr.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
- package/dist-server/server/drivers/boatagent.js +0 -443
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Failure policy: checkpointing is best-effort convenience, never load-bearing.
|
|
16
16
|
// Any git failure disables the feature for that bot for the rest of the
|
|
17
|
-
// session and logs — nothing here ever throws into the turn path.
|
|
17
|
+
// session and logs — nothing here ever throws into the turn path. A git call
|
|
18
|
+
// that hits the timeout disables only that bot+folder, so a slow folder costs
|
|
19
|
+
// one wait and one log line, not one wait per turn.
|
|
18
20
|
//
|
|
19
21
|
// Adapted from the checkpoint designs of Cline, Roo-Code, and Gemini CLI
|
|
20
22
|
// (all Apache-2.0): the per-call GIT_DIR/GIT_WORK_TREE/GIT_CONFIG_* env
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
import { execFile } from "node:child_process";
|
|
25
27
|
import { createHash } from "node:crypto";
|
|
26
28
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { readdir } from "node:fs/promises";
|
|
27
30
|
import { homedir } from "node:os";
|
|
28
31
|
import { isAbsolute, join, parse, resolve } from "node:path";
|
|
29
32
|
import { DATA_DIR } from "./config.js";
|
|
@@ -117,9 +120,20 @@ Thumbs.db
|
|
|
117
120
|
`;
|
|
118
121
|
// gpgsign off: the user's global config may demand signing, and a shadow
|
|
119
122
|
// commit must never block on a passphrase prompt. Automatic GC stays off:
|
|
120
|
-
// explicit GC runs
|
|
121
|
-
//
|
|
123
|
+
// explicit GC runs in the same per-folder queue as snapshots and diffs, on
|
|
124
|
+
// the throttle below, after the objects it drops are no longer needed.
|
|
122
125
|
const GITCONFIG = "[commit]\n\tgpgsign = false\n[core]\n\tautocrlf = false\n[gc]\n\tauto = 0\n";
|
|
126
|
+
/** Hard ceiling on one git call. A hung git (index lock, dead network
|
|
127
|
+
* filesystem, cloud placeholders being downloaded) would otherwise jam the
|
|
128
|
+
* per-repo queue for the whole session. */
|
|
129
|
+
const GIT_TIMEOUT_MS = 120_000;
|
|
130
|
+
// The snapshot is awaited before every turn's dispatch, so GC does not run on
|
|
131
|
+
// every snapshot: collect after this many HEAD-advancing snapshots of one
|
|
132
|
+
// folder, or sooner when loose objects (git's own gc.auto default) or new
|
|
133
|
+
// object data pile up. Counters are per process and reset on restart.
|
|
134
|
+
export const GC_EVERY_COMMITS = 25;
|
|
135
|
+
const GC_LOOSE_OBJECTS = 6_700;
|
|
136
|
+
const GC_NEW_DATA_KIB = 262_144;
|
|
123
137
|
/** One failed git call disables checkpoints for that bot until restart —
|
|
124
138
|
* a broken shadow repo must cost the user one log line, not a failed turn. */
|
|
125
139
|
const disabledBots = new Set();
|
|
@@ -127,6 +141,30 @@ function disable(botId, message) {
|
|
|
127
141
|
disabledBots.add(botId);
|
|
128
142
|
console.warn(`workspace checkpoints disabled for bot ${botId} this session: ${message}`);
|
|
129
143
|
}
|
|
144
|
+
/** A git call that hit GIT_TIMEOUT_MS (not a cancellation). The folder is
|
|
145
|
+
* too slow to snapshot — waiting that long again on every turn is worse than
|
|
146
|
+
* having no checkpoint, so only that bot+folder is switched off. */
|
|
147
|
+
class GitTimeoutError extends Error {
|
|
148
|
+
}
|
|
149
|
+
/** Bot+folder pairs (keyed by shadow dir) switched off after a timeout. */
|
|
150
|
+
const disabledFolders = new Set();
|
|
151
|
+
function disableFolder(shadow, botId, worktree, message) {
|
|
152
|
+
if (disabledFolders.has(shadow))
|
|
153
|
+
return;
|
|
154
|
+
disabledFolders.add(shadow);
|
|
155
|
+
console.warn(`workspace checkpoints disabled for bot ${botId} in ${worktree} this session: ${message}`);
|
|
156
|
+
}
|
|
157
|
+
/** The shadow dir doubles as the bot+folder key. Null when the folder cannot
|
|
158
|
+
* be resolved (the refusal check reports that case). */
|
|
159
|
+
function folderShadow(botId, cwd) {
|
|
160
|
+
try {
|
|
161
|
+
const worktree = realpathSync(resolve(cwd));
|
|
162
|
+
return { worktree, shadow: shadowDir(botId, worktree) };
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
130
168
|
// probed once: either the system has a usable git or checkpoints stay off
|
|
131
169
|
let gitProbe = null;
|
|
132
170
|
function gitAvailable() {
|
|
@@ -198,16 +236,23 @@ function gitEnv(shadow, cwd) {
|
|
|
198
236
|
env.GIT_COMMITTER_EMAIL = "checkpoint@openmausbot.local";
|
|
199
237
|
return env;
|
|
200
238
|
}
|
|
239
|
+
/** execFile reports its own timeout as a killed child; a cancellation via
|
|
240
|
+
* `signal` arrives as an AbortError instead and is not a timeout. */
|
|
241
|
+
function gitFailure(command, err, stderr, signal) {
|
|
242
|
+
if (err.killed && err.name !== "AbortError" && !signal?.aborted) {
|
|
243
|
+
return new GitTimeoutError(`git ${command} timed out after ${GIT_TIMEOUT_MS / 1000} s`);
|
|
244
|
+
}
|
|
245
|
+
return new Error(`git ${command}: ${(stderr || err.message).trim().slice(0, 400)}`);
|
|
246
|
+
}
|
|
201
247
|
/** Run one git command against the shadow repo. cwd is the WORK TREE — the
|
|
202
|
-
* "." pathspec in add resolves relative to it.
|
|
203
|
-
*
|
|
204
|
-
* session, so every call carries a hard timeout. */
|
|
248
|
+
* "." pathspec in add resolves relative to it. Every call carries the
|
|
249
|
+
* GIT_TIMEOUT_MS hard timeout. */
|
|
205
250
|
function runGit(args, cwd, env, signal) {
|
|
206
251
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
207
252
|
signal?.throwIfAborted();
|
|
208
|
-
execFile("git", args, { cwd, env, signal, windowsHide: true, encoding: "utf8", timeout:
|
|
253
|
+
execFile("git", args, { cwd, env, signal, windowsHide: true, encoding: "utf8", timeout: GIT_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
209
254
|
if (err)
|
|
210
|
-
rejectPromise(
|
|
255
|
+
rejectPromise(gitFailure(args[0], err, stderr, signal));
|
|
211
256
|
else
|
|
212
257
|
resolvePromise(stdout);
|
|
213
258
|
});
|
|
@@ -220,13 +265,13 @@ function runGit(args, cwd, env, signal) {
|
|
|
220
265
|
function hasStagedChanges(cwd, env, signal) {
|
|
221
266
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
222
267
|
signal?.throwIfAborted();
|
|
223
|
-
execFile("git", ["diff", "--cached", "--quiet", "--ignore-submodules=dirty"], { cwd, env, signal, windowsHide: true, encoding: "utf8", timeout:
|
|
268
|
+
execFile("git", ["diff", "--cached", "--quiet", "--ignore-submodules=dirty"], { cwd, env, signal, windowsHide: true, encoding: "utf8", timeout: GIT_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, (err, _stdout, stderr) => {
|
|
224
269
|
if (err === null)
|
|
225
270
|
resolvePromise(false);
|
|
226
271
|
else if (err.code === 1)
|
|
227
272
|
resolvePromise(true);
|
|
228
273
|
else
|
|
229
|
-
rejectPromise(
|
|
274
|
+
rejectPromise(gitFailure("diff", err, stderr, signal));
|
|
230
275
|
});
|
|
231
276
|
});
|
|
232
277
|
}
|
|
@@ -241,26 +286,30 @@ function serialize(key, fn) {
|
|
|
241
286
|
chains.set(key, run.then(() => undefined, () => undefined));
|
|
242
287
|
return run;
|
|
243
288
|
}
|
|
244
|
-
// A ref per running turn. Every snapshot parents only the empty base and
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// the pre-turn commit a still-running turn diffs against at its
|
|
248
|
-
// keeps that commit reachable until release(). The pin's name is
|
|
249
|
-
// any token is a legal ref name.
|
|
289
|
+
// A ref per running turn. Every snapshot parents only the empty base and a
|
|
290
|
+
// collection prunes whatever HEAD no longer reaches — so when a bot's threads
|
|
291
|
+
// work side by side in one folder, a collection after a sibling's snapshot
|
|
292
|
+
// would discard the pre-turn commit a still-running turn diffs against at its
|
|
293
|
+
// end. A pin keeps that commit reachable until release(). The pin's name is
|
|
294
|
+
// hashed so any token is a legal ref name.
|
|
250
295
|
const LIVE_REFS = "refs/omb-live/";
|
|
251
296
|
function liveRef(pin) {
|
|
252
297
|
return LIVE_REFS + createHash("sha256").update(pin).digest("hex").slice(0, 16);
|
|
253
298
|
}
|
|
254
299
|
// Pins outlive the process that wrote them (a crash mid-turn), so the first
|
|
255
300
|
// use of each shadow in this process drops every pin left behind: no turn of
|
|
256
|
-
// ours can be live in a shadow this process has not touched yet.
|
|
301
|
+
// ours can be live in a shadow this process has not touched yet. Dropping one
|
|
302
|
+
// makes the next snapshot's collection due (see collectionDue).
|
|
257
303
|
const sweptShadows = new Set();
|
|
304
|
+
const droppedPins = new Set();
|
|
258
305
|
async function sweepLiveRefs(cwd, env, shadow, signal) {
|
|
259
306
|
if (sweptShadows.has(shadow))
|
|
260
307
|
return;
|
|
261
308
|
const stale = (await runGit(["for-each-ref", "--format=%(refname)", LIVE_REFS], cwd, env, signal)).split("\n").filter(Boolean);
|
|
262
309
|
for (const ref of stale)
|
|
263
310
|
await runGit(["update-ref", "-d", ref], cwd, env, signal);
|
|
311
|
+
if (stale.length > 0)
|
|
312
|
+
droppedPins.add(shadow);
|
|
264
313
|
sweptShadows.add(shadow);
|
|
265
314
|
}
|
|
266
315
|
/** Create the shadow repo on first use; self-heal its config files on every
|
|
@@ -309,32 +358,190 @@ async function ensureShadow(cwd, env, shadow, signal) {
|
|
|
309
358
|
}
|
|
310
359
|
await sweepLiveRefs(cwd, env, shadow, signal);
|
|
311
360
|
}
|
|
361
|
+
/** Folder listings in flight at once (libuv's pool has four threads). */
|
|
362
|
+
const LISTING_CONCURRENCY = 4;
|
|
363
|
+
/** Case-insensitive file systems only (APFS/HFS+ as macOS formats them,
|
|
364
|
+
* NTFS): does a staged path, or one of its folders, now exist on disk under a
|
|
365
|
+
* different case? git init sets core.ignorecase there, so an incremental
|
|
366
|
+
* `add -A` matches `R0` to the staged `r0` and keeps the old spelling
|
|
367
|
+
* forever. Reads folder listings only — each folder holding a staged path
|
|
368
|
+
* once, asynchronously, so a big tree never stalls the event loop — never
|
|
369
|
+
* file contents. Names compare in NFC: git stores precomposed paths on macOS,
|
|
370
|
+
* readdir returns names as they were created. */
|
|
371
|
+
async function caseRenamed(cwd, paths) {
|
|
372
|
+
if (paths.length === 0)
|
|
373
|
+
return false;
|
|
374
|
+
const pending = new Set([""]);
|
|
375
|
+
for (const path of paths) {
|
|
376
|
+
for (let slash = path.indexOf("/"); slash > 0; slash = path.indexOf("/", slash + 1))
|
|
377
|
+
pending.add(path.slice(0, slash));
|
|
378
|
+
}
|
|
379
|
+
const listings = new Map();
|
|
380
|
+
const queue = [...pending];
|
|
381
|
+
const read = async () => {
|
|
382
|
+
for (let dir = queue.pop(); dir !== undefined; dir = queue.pop()) {
|
|
383
|
+
try {
|
|
384
|
+
const exact = new Set((await readdir(join(cwd, dir))).map((name) => name.normalize("NFC")));
|
|
385
|
+
listings.set(dir, { exact, folded: new Set([...exact].map((name) => name.toLowerCase())) });
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
listings.set(dir, null); // gone or unreadable: add -A handles deletions itself
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
await Promise.all(Array.from({ length: Math.min(LISTING_CONCURRENCY, queue.length) }, read));
|
|
393
|
+
const folders = new Map();
|
|
394
|
+
const renamed = (path, isFolder) => {
|
|
395
|
+
const known = isFolder ? folders.get(path) : undefined;
|
|
396
|
+
if (known !== undefined)
|
|
397
|
+
return known;
|
|
398
|
+
const slash = path.lastIndexOf("/");
|
|
399
|
+
const parent = slash < 0 ? "" : path.slice(0, slash);
|
|
400
|
+
const name = path.slice(slash + 1).normalize("NFC");
|
|
401
|
+
const names = listings.get(parent) ?? null;
|
|
402
|
+
let verdict;
|
|
403
|
+
if (names === null)
|
|
404
|
+
verdict = false;
|
|
405
|
+
else if (!names.exact.has(name))
|
|
406
|
+
verdict = names.folded.has(name.toLowerCase()); // else simply deleted
|
|
407
|
+
else
|
|
408
|
+
verdict = parent !== "" && renamed(parent, true);
|
|
409
|
+
if (isFolder)
|
|
410
|
+
folders.set(path, verdict);
|
|
411
|
+
return verdict;
|
|
412
|
+
};
|
|
413
|
+
return paths.some((path) => renamed(path, false));
|
|
414
|
+
}
|
|
415
|
+
/** Would an incremental `git add -A` leave the shadow index different from
|
|
416
|
+
* one rebuilt from empty? Only in four cases, all detected here: a tracked
|
|
417
|
+
* path is now ignored or inside a cache-tagged directory (add -A never drops
|
|
418
|
+
* tracked files), a folder holding tracked files became a nested repository
|
|
419
|
+
* (add -A keeps the files instead of a gitlink), a recorded nested
|
|
420
|
+
* repository lost its .git (add -A keeps the stale gitlink), or — on a
|
|
421
|
+
* case-insensitive file system — a tracked path was renamed by case only
|
|
422
|
+
* (add -A keeps the old spelling). */
|
|
423
|
+
async function indexNeedsRebuild(cwd, env, signal) {
|
|
424
|
+
// If the index cannot be inspected (say, a listing past maxBuffer on a huge
|
|
425
|
+
// folder), rebuild: that is always correct, just slower.
|
|
426
|
+
const list = (args) => runGit(args, cwd, env, signal).catch((e) => {
|
|
427
|
+
if (e instanceof GitTimeoutError || signal?.aborted)
|
|
428
|
+
throw e;
|
|
429
|
+
return null;
|
|
430
|
+
});
|
|
431
|
+
const ignored = await list(["ls-files", "--cached", "--ignored", "--exclude-standard", "-z"]);
|
|
432
|
+
if (ignored === null || ignored.length > 0)
|
|
433
|
+
return true;
|
|
434
|
+
const staged = await list(["ls-files", "--stage", "-z"]);
|
|
435
|
+
if (staged === null)
|
|
436
|
+
return true;
|
|
437
|
+
const paths = [];
|
|
438
|
+
const parents = new Set();
|
|
439
|
+
for (const entry of staged.split("\0")) {
|
|
440
|
+
const tab = entry.indexOf("\t");
|
|
441
|
+
if (tab < 0)
|
|
442
|
+
continue;
|
|
443
|
+
const path = entry.slice(tab + 1);
|
|
444
|
+
paths.push(path);
|
|
445
|
+
if (entry.startsWith("160000 ")) {
|
|
446
|
+
if (!existsSync(join(cwd, path, ".git")))
|
|
447
|
+
return true;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
for (let slash = path.indexOf("/"); slash > 0; slash = path.indexOf("/", slash + 1))
|
|
451
|
+
parents.add(path.slice(0, slash));
|
|
452
|
+
}
|
|
453
|
+
for (const dir of parents)
|
|
454
|
+
if (existsSync(join(cwd, dir, ".git")))
|
|
455
|
+
return true;
|
|
456
|
+
return (process.platform === "darwin" || process.platform === "win32") && (await caseRenamed(cwd, paths));
|
|
457
|
+
}
|
|
458
|
+
/** Stage the work tree into the shadow index. The index persists between
|
|
459
|
+
* snapshots so `git add -A` only re-reads files whose stat data changed;
|
|
460
|
+
* rebuilding it from empty re-reads and re-hashes every file (and downloads
|
|
461
|
+
* every cloud placeholder). It is rebuilt only when indexNeedsRebuild says an
|
|
462
|
+
* incremental add would differ, so the staged content always matches a
|
|
463
|
+
* from-empty rebuild. Only the shadow index changes; no user index or
|
|
464
|
+
* work-tree file does. */
|
|
465
|
+
async function stageWorkTree(cwd, env, signal) {
|
|
466
|
+
if (await indexNeedsRebuild(cwd, env, signal))
|
|
467
|
+
await runGit(["read-tree", "--empty"], cwd, env, signal);
|
|
468
|
+
await runGit(["add", "-A", "--ignore-errors", "."], cwd, env, signal);
|
|
469
|
+
}
|
|
312
470
|
/** Capture fully before advancing HEAD: null when the add was partial, so a
|
|
313
471
|
* partial capture never replaces the last usable checkpoint or authorizes
|
|
314
472
|
* removal of its objects. */
|
|
315
473
|
async function commitAll(cwd, env, label, signal) {
|
|
316
474
|
const previous = (await runGit(["rev-parse", "HEAD"], cwd, env, signal)).trim();
|
|
317
475
|
try {
|
|
318
|
-
|
|
319
|
-
// pinning old blobs; no user index or work-tree file is changed.
|
|
320
|
-
await runGit(["read-tree", "--empty"], cwd, env, signal);
|
|
321
|
-
await runGit(["add", "-A", "--ignore-errors", "."], cwd, env, signal);
|
|
476
|
+
await stageWorkTree(cwd, env, signal);
|
|
322
477
|
}
|
|
323
|
-
catch {
|
|
478
|
+
catch (e) {
|
|
479
|
+
// A timeout is not an unreadable file: the caller switches the folder off.
|
|
480
|
+
if (e instanceof GitTimeoutError)
|
|
481
|
+
throw e;
|
|
324
482
|
return null;
|
|
325
483
|
}
|
|
326
484
|
const changed = await hasStagedChanges(cwd, env, signal);
|
|
327
485
|
const base = (await runGit(["rev-list", "--max-parents=0", "HEAD"], cwd, env, signal)).trim();
|
|
328
486
|
const count = (await runGit(["rev-list", "--count", "HEAD"], cwd, env, signal)).trim();
|
|
329
487
|
if (!changed && Number(count) <= 2)
|
|
330
|
-
return previous;
|
|
488
|
+
return { hash: previous, advanced: false, compacted: false };
|
|
331
489
|
const tree = (await runGit(["write-tree"], cwd, env, signal)).trim();
|
|
332
490
|
const message = changed ? label : (await runGit(["show", "-s", "--format=%B", "HEAD"], cwd, env, signal)).trim();
|
|
333
491
|
// Parent only the empty marker, never the previous snapshot: keeping the
|
|
334
492
|
// previous commit as an ancestor would retain every old tree indefinitely.
|
|
335
493
|
const hash = (await runGit(["commit-tree", tree, "-p", base, "-m", message], cwd, env, signal)).trim();
|
|
336
494
|
await runGit(["update-ref", "HEAD", hash, previous], cwd, env, signal);
|
|
337
|
-
return hash;
|
|
495
|
+
return { hash, advanced: true, compacted: Number(count) > 2 };
|
|
496
|
+
}
|
|
497
|
+
/** HEAD-advancing snapshots per shadow since this process last collected. */
|
|
498
|
+
const commitsSinceGc = new Map();
|
|
499
|
+
/** Pack size (KiB) first counted after the last collection — the live
|
|
500
|
+
* snapshot. Packs also grow between collections: `git add` streams files over
|
|
501
|
+
* core.bigFileThreshold straight into new packs. */
|
|
502
|
+
const packBaselineKib = new Map();
|
|
503
|
+
/** A collection the throttle asked for, held until that turn's settled diff
|
|
504
|
+
* has been queued, or — when the turn settled without one — until the next
|
|
505
|
+
* snapshot, which runs it first (see snapshot and diffWorkingTree). */
|
|
506
|
+
const pendingCollections = new Map();
|
|
507
|
+
/** Queue the folder's pending collection behind whatever was just queued for
|
|
508
|
+
* it. Nothing awaits it. */
|
|
509
|
+
function releaseCollection(shadow) {
|
|
510
|
+
const collect = pendingCollections.get(shadow);
|
|
511
|
+
if (collect === undefined)
|
|
512
|
+
return;
|
|
513
|
+
pendingCollections.delete(shadow);
|
|
514
|
+
void serialize(shadow, collect);
|
|
515
|
+
}
|
|
516
|
+
/** Throttle for collectObsolete (see GC_EVERY_COMMITS). */
|
|
517
|
+
async function collectionDue(shadow, result, cwd, env, signal) {
|
|
518
|
+
// A compaction or a swept leftover pin frees a whole tree at once: reclaim it now.
|
|
519
|
+
const freed = droppedPins.delete(shadow) || result.compacted;
|
|
520
|
+
if (!result.advanced && !freed)
|
|
521
|
+
return false;
|
|
522
|
+
const commits = (commitsSinceGc.get(shadow) ?? 0) + (result.advanced ? 1 : 0);
|
|
523
|
+
commitsSinceGc.set(shadow, commits);
|
|
524
|
+
let due = freed || commits >= GC_EVERY_COMMITS;
|
|
525
|
+
if (!due) {
|
|
526
|
+
// Advisory only: a failed count must not cost the snapshot just taken.
|
|
527
|
+
const counted = await runGit(["count-objects", "-v"], cwd, env, signal).catch(() => "");
|
|
528
|
+
if (counted !== "") {
|
|
529
|
+
const field = (name) => Number(new RegExp(`^${name}: (\\d+)$`, "m").exec(counted)?.[1] ?? 0);
|
|
530
|
+
const pack = field("size-pack");
|
|
531
|
+
if (!packBaselineKib.has(shadow))
|
|
532
|
+
packBaselineKib.set(shadow, pack);
|
|
533
|
+
// Every loose object is new since the last collection (gc packs or
|
|
534
|
+
// prunes them all). Only pack growth counts, so a folder whose live
|
|
535
|
+
// snapshot alone passes the ceiling does not collect every turn.
|
|
536
|
+
const newData = field("size") + Math.max(0, pack - packBaselineKib.get(shadow));
|
|
537
|
+
due = field("count") >= GC_LOOSE_OBJECTS || newData >= GC_NEW_DATA_KIB;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (due) {
|
|
541
|
+
commitsSinceGc.set(shadow, 0);
|
|
542
|
+
packBaselineKib.delete(shadow);
|
|
543
|
+
}
|
|
544
|
+
return due;
|
|
338
545
|
}
|
|
339
546
|
async function collectObsolete(cwd, env) {
|
|
340
547
|
// No caller may still need an unpinned previous tree: HEAD stays reachable,
|
|
@@ -349,8 +556,8 @@ async function collectObsolete(cwd, env) {
|
|
|
349
556
|
}
|
|
350
557
|
}
|
|
351
558
|
/** Snapshot the folder. Returns the checkpoint hash, or null when the
|
|
352
|
-
* feature is off for this bot, git is missing, the folder is
|
|
353
|
-
* file could not be read.
|
|
559
|
+
* feature is off for this bot or folder, git is missing, the folder is
|
|
560
|
+
* refused, or a file could not be read.
|
|
354
561
|
* With `pin`, the returned commit stays reachable through later snapshots of
|
|
355
562
|
* the same folder until release(pin) — for a turn that will diff against it
|
|
356
563
|
* when it ends. Never throws — this is called fire-and-forget on the turn path. */
|
|
@@ -363,30 +570,47 @@ export async function snapshot(botId, cwd, label, signal, opts) {
|
|
|
363
570
|
return null;
|
|
364
571
|
if (refusalReason(cwd) !== null)
|
|
365
572
|
return null;
|
|
573
|
+
const folder = folderShadow(botId, cwd);
|
|
574
|
+
if (folder === null || disabledFolders.has(folder.shadow))
|
|
575
|
+
return null;
|
|
576
|
+
const { worktree, shadow } = folder;
|
|
366
577
|
try {
|
|
367
|
-
|
|
368
|
-
|
|
578
|
+
// Still pending from the previous snapshot: that turn settled without a
|
|
579
|
+
// diff. Collect ahead of this snapshot, not behind it, so this turn's own
|
|
580
|
+
// diff never queues behind GC and the object counts below see the
|
|
581
|
+
// collected repo (a stale count would make GC due again at once).
|
|
582
|
+
releaseCollection(shadow);
|
|
369
583
|
return await serialize(shadow, async () => {
|
|
370
584
|
const env = gitEnv(shadow, worktree);
|
|
371
585
|
await ensureShadow(worktree, env, shadow, signal);
|
|
372
|
-
const
|
|
373
|
-
if (!
|
|
586
|
+
const result = await commitAll(worktree, env, label, signal);
|
|
587
|
+
if (!result)
|
|
374
588
|
return null;
|
|
375
589
|
if (opts?.pin)
|
|
376
|
-
await runGit(["update-ref", liveRef(opts.pin), hash], worktree, env, signal);
|
|
377
|
-
|
|
378
|
-
|
|
590
|
+
await runGit(["update-ref", liveRef(opts.pin), result.hash], worktree, env, signal);
|
|
591
|
+
// Never awaited by the turn waiting on this snapshot, and held for that
|
|
592
|
+
// turn's settled diff: diffWorkingTree queues it behind itself, so GC
|
|
593
|
+
// never eats into the digest's capture window.
|
|
594
|
+
if (await collectionDue(shadow, result, worktree, env, signal)) {
|
|
595
|
+
pendingCollections.set(shadow, () => collectObsolete(worktree, env));
|
|
596
|
+
}
|
|
597
|
+
return result.hash;
|
|
379
598
|
});
|
|
380
599
|
}
|
|
381
600
|
catch (e) {
|
|
382
|
-
if (
|
|
383
|
-
|
|
601
|
+
if (signal?.aborted)
|
|
602
|
+
return null;
|
|
603
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
604
|
+
if (e instanceof GitTimeoutError)
|
|
605
|
+
disableFolder(shadow, botId, worktree, message);
|
|
606
|
+
else
|
|
607
|
+
disable(botId, message);
|
|
384
608
|
return null;
|
|
385
609
|
}
|
|
386
610
|
}
|
|
387
|
-
/** Let go of a pinned pre-turn commit: the next
|
|
388
|
-
*
|
|
389
|
-
*
|
|
611
|
+
/** Let go of a pinned pre-turn commit: the next collection may reclaim it.
|
|
612
|
+
* Queued behind the shadow's pending operations, so a digest's diff already
|
|
613
|
+
* in that queue still finds its commit. Best-effort like the
|
|
390
614
|
* rest of this module — a pin that cannot be dropped costs one log line and
|
|
391
615
|
* is swept on the next start. */
|
|
392
616
|
export async function release(botId, cwd, pin) {
|
|
@@ -411,14 +635,15 @@ export async function diffWorkingTree(botId, cwd, fromHash, signal) {
|
|
|
411
635
|
return null;
|
|
412
636
|
if (disabledBots.has(botId) || !(await gitAvailable()) || refusalReason(cwd) !== null)
|
|
413
637
|
return null;
|
|
638
|
+
const folder = folderShadow(botId, cwd);
|
|
639
|
+
if (folder === null || disabledFolders.has(folder.shadow))
|
|
640
|
+
return null;
|
|
641
|
+
const { worktree, shadow } = folder;
|
|
414
642
|
try {
|
|
415
|
-
const worktree = realpathSync(resolve(cwd));
|
|
416
|
-
const shadow = shadowDir(botId, worktree);
|
|
417
643
|
const env = gitEnv(shadow, worktree);
|
|
418
|
-
|
|
644
|
+
const run = serialize(shadow, async () => {
|
|
419
645
|
await ensureShadow(worktree, env, shadow, signal);
|
|
420
|
-
await
|
|
421
|
-
await runGit(["add", "-A", "--ignore-errors", "."], worktree, env, signal);
|
|
646
|
+
await stageWorkTree(worktree, env, signal);
|
|
422
647
|
const out = await runGit(["diff", "--cached", "--name-status", "--no-renames", "-z", fromHash], worktree, env, signal);
|
|
423
648
|
const diff = { changed: [], added: [], deleted: [] };
|
|
424
649
|
const fields = out.split("\0").filter((field) => field.length > 0);
|
|
@@ -434,8 +659,14 @@ export async function diffWorkingTree(botId, cwd, fromHash, signal) {
|
|
|
434
659
|
}
|
|
435
660
|
return diff;
|
|
436
661
|
});
|
|
662
|
+
// The collection this turn's snapshot left pending runs behind this
|
|
663
|
+
// diff, never ahead of it.
|
|
664
|
+
releaseCollection(shadow);
|
|
665
|
+
return await run;
|
|
437
666
|
}
|
|
438
|
-
catch {
|
|
667
|
+
catch (e) {
|
|
668
|
+
if (e instanceof GitTimeoutError && !signal?.aborted)
|
|
669
|
+
disableFolder(shadow, botId, worktree, e.message);
|
|
439
670
|
return null;
|
|
440
671
|
}
|
|
441
672
|
}
|
|
@@ -118,7 +118,7 @@ const NATIVE = [
|
|
|
118
118
|
async function connectNative(choice, id, entry, io, deps) {
|
|
119
119
|
const cli = typeof rawObject(entry.config).cli === "string" ? rawObject(entry.config).cli : choice.cli;
|
|
120
120
|
io.log("Connecting your account…");
|
|
121
|
-
io.log("Sign-in stays with your provider. Its account limits apply;
|
|
121
|
+
io.log("Sign-in stays with your provider. Its account limits apply; OpenMausBot never asks for your password.");
|
|
122
122
|
let state = await deps.inspect(id, entry);
|
|
123
123
|
if (state.snapshot.state !== "available") {
|
|
124
124
|
if (cli !== choice.cli)
|
|
@@ -143,7 +143,7 @@ async function connectNative(choice, id, entry, io, deps) {
|
|
|
143
143
|
await deps.runCli(cli, args, entry.environment);
|
|
144
144
|
state = await deps.inspect(id, entry);
|
|
145
145
|
if (state.snapshot.state !== "available" || !state.snapshot.authenticated) {
|
|
146
|
-
throw new Error("Sign-in was not confirmed. Your
|
|
146
|
+
throw new Error("Sign-in was not confirmed. Your OpenMausBot settings are unchanged; complete provider sign-in and try again.");
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
else {
|
|
@@ -294,7 +294,7 @@ export async function runSetup(options, io = defaultSetupIo(), deps = dependenci
|
|
|
294
294
|
catch (error) {
|
|
295
295
|
if (!(error instanceof SetupCancelled))
|
|
296
296
|
throw error;
|
|
297
|
-
io.log("\nSetup cancelled. No
|
|
297
|
+
io.log("\nSetup cancelled. No OpenMausBot settings were changed. Provider sign-ins or installs already completed are kept.");
|
|
298
298
|
return false;
|
|
299
299
|
}
|
|
300
300
|
finally {
|
|
@@ -15,6 +15,8 @@ export const CLOUD_COMPUTER_TOOLS = [
|
|
|
15
15
|
tool("open_url", "Request opening an HTTP or HTTPS URL in the cloud desktop browser; inspect the screen to confirm it loaded.", { url: { type: "string", pattern: "^https?://", maxLength: 2000 } }, ["url"]),
|
|
16
16
|
tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
|
|
17
17
|
];
|
|
18
|
+
/** A call the gate let through while no Boat had landed for the turn. */
|
|
19
|
+
export const CLOUD_COMPUTER_NOT_READY = "The cloud computer is not ready. This call was not performed. Take a fresh screenshot in a moment.";
|
|
18
20
|
const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
19
21
|
const buttons = { left: 1, middle: 2, right: 3 };
|
|
20
22
|
const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
|
|
@@ -85,9 +87,11 @@ export async function runCloudComputerTool(cfg, boxId, name, args, signal) {
|
|
|
85
87
|
}
|
|
86
88
|
/** One JSON-RPC request from harness-mcp-proxy, answered with its MCP
|
|
87
89
|
* `result`. The order is the safety argument: arguments are checked before
|
|
88
|
-
* the control gate (which may claim the seat
|
|
89
|
-
*
|
|
90
|
-
*
|
|
90
|
+
* the control gate (which may claim the seat, and for a cloud computer
|
|
91
|
+
* mounted before it existed, create or wake it), a person's control refuses
|
|
92
|
+
* the call before anything reaches the Boat, the Boat is read only once the
|
|
93
|
+
* gate has let the call through, and the turn's capability must still be
|
|
94
|
+
* live before and after the Boat acts. Listing the tools touches no Boat. */
|
|
91
95
|
export async function cloudComputerRpc(request, deps) {
|
|
92
96
|
if (request?.method === "tools/list") {
|
|
93
97
|
deps.assertActive();
|
|
@@ -104,7 +108,10 @@ export async function cloudComputerRpc(request, deps) {
|
|
|
104
108
|
deps.assertActive();
|
|
105
109
|
if (control.held)
|
|
106
110
|
return { isError: true, content: [{ type: "text", text: control.blockedReason || CONTROL_REFUSAL_PLAIN }] };
|
|
107
|
-
const
|
|
111
|
+
const boxId = deps.boxId();
|
|
112
|
+
if (!boxId)
|
|
113
|
+
return { isError: true, content: [{ type: "text", text: CLOUD_COMPUTER_NOT_READY }] };
|
|
114
|
+
const result = await runCloudComputerTool(deps.cfg, boxId, params.name, args, deps.signal);
|
|
108
115
|
deps.assertActive();
|
|
109
116
|
return result;
|
|
110
117
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Entry point of the OMB Cloud Pro home image (
|
|
1
|
+
// Entry point of the OMB Cloud Pro home image (the Dockerfile's cloud-home target).
|
|
2
2
|
//
|
|
3
3
|
// Starts as root, hands a fresh Fly volume (mounted root-owned at /data) to
|
|
4
4
|
// the unprivileged `maus` user, and stays a small root supervisor of two
|
|
@@ -77,7 +77,7 @@ export function cloudHomeChildEnvironments(config, env, home) {
|
|
|
77
77
|
/** Why the root supervisor must not run or trust `files`, or null: each
|
|
78
78
|
* must be root's and not writable by anyone else (nor any folder above it),
|
|
79
79
|
* and none may live on the volume `home`, which `maus` owns. The image makes its code
|
|
80
|
-
* root's (
|
|
80
|
+
* root's (the Dockerfile's cloud-home target); a file `maus` could rewrite would run as
|
|
81
81
|
* root at the next start, or be handed the secrets. */
|
|
82
82
|
export function codeTrustProblem(files, home, stat = statSync) {
|
|
83
83
|
const volume = posix.join(home, "/");
|
|
@@ -117,7 +117,7 @@ export function startCloudHome(env = process.env) {
|
|
|
117
117
|
process.umask(0o077);
|
|
118
118
|
const config = cloudHomeConfiguration(env);
|
|
119
119
|
if (!config)
|
|
120
|
-
throw new Error("This image runs
|
|
120
|
+
throw new Error("This image runs a My Cloud machine for OpenMausBot Cloud; set its boot contract (docs/cloud-pro.md).");
|
|
121
121
|
// Logged here once: the server child never sees what they are about.
|
|
122
122
|
for (const warning of config.warnings)
|
|
123
123
|
console.warn(`cloud home: ${warning}`);
|
|
@@ -155,7 +155,7 @@ export function startCloudHome(env = process.env) {
|
|
|
155
155
|
if (ids) {
|
|
156
156
|
const problem = codeTrustProblem([process.execPath, fileURLToPath(import.meta.url), join(here, "index.js"), edgeBin, edgeConfig], home);
|
|
157
157
|
if (problem)
|
|
158
|
-
throw new Error(`This image's code is not safe to run as root: ${problem}. Rebuild it
|
|
158
|
+
throw new Error(`This image's code is not safe to run as root: ${problem}. Rebuild it with docker build --target cloud-home.`);
|
|
159
159
|
}
|
|
160
160
|
const { server, edge, secrets, dropped } = cloudHomeChildEnvironments(config, env, home);
|
|
161
161
|
// Names only, never values: what the server does not get from here.
|
|
@@ -109,17 +109,11 @@ export { cloudHomeOffersPlace } from "../shared/cloud-home.js";
|
|
|
109
109
|
* place it offers. A turn's error shows 160 characters, so each fits. */
|
|
110
110
|
export function cloudHomePlaceRefusal(place) {
|
|
111
111
|
if (place === "local")
|
|
112
|
-
return "This computer isn't a place on
|
|
112
|
+
return "This computer isn't a place on My Cloud. Set Works on to Auto, Cloud computer or Browser, or lend your Mac under Settings → OpenMausBot Cloud.";
|
|
113
113
|
if (place === "vm")
|
|
114
|
-
return "Bots on
|
|
114
|
+
return "Bots on My Cloud can't use a Local VM. Set Works on to Auto, Cloud computer or Browser.";
|
|
115
115
|
return undefined;
|
|
116
116
|
}
|
|
117
|
-
/** What a turn is told when Cloud is chosen but no Boat account is set up (no
|
|
118
|
-
* key of the person's and no included Boat). A Cloud home has no Local VM to
|
|
119
|
-
* suggest instead. */
|
|
120
|
-
export function boatNotConfiguredMessage(cloudHome) {
|
|
121
|
-
return `Cloud Boat is not configured — add a Boat API key or choose ${cloudHome ? "Browser" : "Local VM"}`;
|
|
122
|
-
}
|
|
123
117
|
/** The Cloud's setup checklist (docs/cloud-pro.md) has a "try something"
|
|
124
118
|
* step that is done once a bot's turn finishes on the machine itself. The
|
|
125
119
|
* server records when, once, in this Cloud's own onboarding record: that
|
|
@@ -198,7 +192,7 @@ export function createCloudPairing(options) {
|
|
|
198
192
|
const ttl = Math.min(ttlSeconds ?? CLOUD_PAIRING_DEFAULT_TTL_S, browser ? CLOUD_BROWSER_SIGN_IN_MAX_TTL_S : CLOUD_PAIRING_MAX_TTL_S);
|
|
199
193
|
const opened = sessions.openPairing({
|
|
200
194
|
scopes: ["admin", "client"],
|
|
201
|
-
label: typeof label === "string" && label.trim() ? label.trim() : "
|
|
195
|
+
label: typeof label === "string" && label.trim() ? label.trim() : "OpenMausBot Cloud",
|
|
202
196
|
ttlMs: ttl * 1000,
|
|
203
197
|
browser,
|
|
204
198
|
...(browser ? { owner: owner } : {}),
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { lstatSync, readFileSync } from "node:fs";
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
import { writeFileAtomic } from "./atomic.js";
|
|
32
|
-
export const CLOUD_PERSONAL_REFUSAL = "
|
|
32
|
+
export const CLOUD_PERSONAL_REFUSAL = "My Cloud is personal: only your own devices can connect.";
|
|
33
33
|
const KEY = /^p_[\w-]{22}$/;
|
|
34
34
|
const keys = z.array(z.string().regex(KEY)).max(100_000);
|
|
35
35
|
const recordFile = z.object({
|