openmausbot 0.1.94 → 0.1.96
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/{DesktopViewer-bxM7NV3N.js → DesktopViewer-DsnA23KM.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
- package/dist/assets/{arc-B5itKDcx.js → arc-Duj5Coy2.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
- package/dist/assets/channel-BZxwC-Gw.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
- package/dist/assets/{index-CPiqc_DN.js → index-DreDnjOu.js} +1 -1
- package/dist/assets/index-Du7o6Dsf.js +610 -0
- package/dist/assets/index-KxG4bWSr.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
- package/dist/assets/{linear-BpKhuyLm.js → linear-Bl669148.js} +1 -1
- package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-86Xqz5Mr.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +27 -13
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/companion/src/routes.js +39 -0
- package/dist-server/container-mcp.js +16 -29
- package/dist-server/drivers/agents-proxy.js +161 -82
- package/dist-server/enterprise/server/index.js +2 -1
- package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +34 -19
- package/dist-server/index.js +4991 -4908
- package/dist-server/local-computer.js +2 -1
- package/dist-server/openmausbot.js +906 -1785
- package/dist-server/pair-cli.js +906 -1785
- package/dist-server/proxy-paths.js +2 -1
- package/dist-server/server/account-signin.js +0 -2
- package/dist-server/server/activity.js +201 -0
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +25 -4
- package/dist-server/server/automatic-recovery.js +1 -1
- package/dist-server/server/boat-create-idempotency.js +0 -7
- package/dist-server/server/boat-delete-journal.js +0 -4
- package/dist-server/server/boat.js +4 -4
- package/dist-server/server/bot-package.js +1 -19
- package/dist-server/server/browser-runtime.js +1 -2
- package/dist-server/server/checkpoints.js +90 -148
- package/dist-server/server/chief-of-staff.js +9 -9
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-backend.js +0 -4
- package/dist-server/server/cloud-computer-tools.js +110 -0
- package/dist-server/server/cloud-home-start.js +12 -15
- package/dist-server/server/cloud-home.js +0 -5
- package/dist-server/server/cloud-lending.js +0 -3
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/composio.js +41 -5
- package/dist-server/server/computer-control.js +0 -15
- package/dist-server/server/computer-observation.js +0 -74
- package/dist-server/server/config.js +121 -65
- package/dist-server/server/container-computer.js +23 -24
- package/dist-server/server/control-client.js +4 -10
- package/dist-server/server/decider/jev.js +1 -4
- package/dist-server/server/decider/log.js +1 -3
- package/dist-server/server/decision-log.js +1 -4
- package/dist-server/server/delegations.js +3 -2
- package/dist-server/server/drivers/acp/core.js +11 -8
- package/dist-server/server/drivers/acp/opencode-go.js +17 -16
- package/dist-server/server/drivers/agents-call.js +88 -92
- package/dist-server/server/drivers/agents-catalog.js +90 -11
- package/dist-server/server/drivers/agents-proxy.js +10 -31
- package/dist-server/server/drivers/antigravity.js +1 -4
- package/dist-server/server/drivers/boatagent.js +23 -4
- package/dist-server/server/drivers/cerebras.js +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/claude.js +16 -14
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +5 -4
- package/dist-server/server/drivers/grok.js +1 -1
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
- package/dist-server/server/drivers/openai-chat.js +18 -11
- package/dist-server/server/drivers/prompt-split.js +4 -4
- package/dist-server/server/drivers/retry.js +8 -4
- package/dist-server/server/engine-install.js +1 -5
- package/dist-server/server/harness/bus.js +49 -0
- package/dist-server/server/harness/registry.js +0 -1
- package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
- package/dist-server/server/index.js +1190 -971
- package/dist-server/server/lending-memory.js +12 -8
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/memory-entries.js +38 -8
- package/dist-server/server/memory-organize.js +1 -8
- package/dist-server/server/memory-topics.js +4 -4
- package/dist-server/server/memory-upkeep.js +13 -15
- package/dist-server/server/message-file.js +9 -13
- package/dist-server/server/outbound-counts.js +79 -0
- package/dist-server/server/outbound-requests.js +60 -0
- package/dist-server/server/package-import.js +1 -1
- package/dist-server/server/peer-delivery.js +5 -19
- package/dist-server/server/peer-roster.js +30 -4
- package/dist-server/server/profile-versions.js +0 -31
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/proxy-paths.js +2 -1
- package/dist-server/server/restart.js +40 -0
- package/dist-server/server/room-handoffs.js +17 -13
- package/dist-server/server/routes/bot-memory.js +0 -24
- package/dist-server/server/routine-requests.js +2 -5
- package/dist-server/server/routines.js +10 -5
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/setup-mode.js +0 -2
- package/dist-server/server/skill-learn.js +0 -1
- package/dist-server/server/skill-library.js +0 -15
- package/dist-server/server/store.js +10 -2
- package/dist-server/server/surface.js +69 -28
- package/dist-server/server/system-prompt.js +17 -7
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/team-memory.js +209 -0
- package/dist-server/server/thread-events.js +2 -2
- package/dist-server/server/tunnel.js +4 -1
- package/dist-server/server/turn-images.js +1 -21
- package/dist-server/server/turn-resources.js +19 -150
- package/dist-server/server/usage-ledger.js +3 -1
- package/dist-server/server/vps-computer.js +4 -14
- package/dist-server/server/workspace.js +87 -50
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/activity.js +3 -0
- package/dist-server/shared/ask-question.js +1 -1
- package/dist-server/shared/bot-avatar.js +8 -0
- package/dist-server/shared/cloud-computer.js +12 -0
- package/dist-server/shared/connector-scopes.js +78 -0
- package/dist-server/shared/failed-turn.js +25 -0
- package/dist-server/shared/live-approval.js +2 -2
- package/dist-server/shared/outbound.js +128 -0
- package/dist-server/shared/package-format.js +3 -3
- package/dist-server/shared/pairing-link.js +144 -0
- package/dist-server/shared/tool-surface.js +3 -2
- package/dist-server/vps-container-mcp.js +16 -29
- package/dist-server/workspace-backup.worker.js +14 -27
- package/enterprise/server/index.js +2 -1
- package/package.json +1 -1
- package/dist/assets/channel-CdJX6i8k.js +0 -1
- package/dist/assets/index-CPYrGsv1.js +0 -610
- package/dist/assets/index-CzT42GR8.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
- package/dist-server/server/claim-idle.js +0 -33
- package/dist-server/server/cloud-overflow.js +0 -167
- package/dist-server/server/drivers/chat-boat-tools.js +0 -90
- package/dist-server/server/drivers/grokagent.js +0 -4
- package/dist-server/server/openmaus-status-capsule.js +0 -329
- package/dist-server/server/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isolatedRemoteCommand, runCommand, screenshotBoat } from "./boat.js";
|
|
2
|
+
import { CONTROL_REFUSAL_PLAIN } from "./control-client.js";
|
|
3
|
+
import { compileToolSchema } from "./drivers/chat-mcp-tools.js";
|
|
4
|
+
const coordinate = { type: "integer", minimum: 0, maximum: 32767 };
|
|
5
|
+
const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
|
|
6
|
+
export const CLOUD_COMPUTER_TOOLS = [
|
|
7
|
+
tool("screenshot", "See the assigned cloud desktop at native pixel resolution."),
|
|
8
|
+
tool("get_screen_size", "Get the cloud desktop width and height."),
|
|
9
|
+
tool("click", "Click a point on the cloud desktop.", { x: coordinate, y: coordinate, button: { type: "string", enum: ["left", "middle", "right"] }, count: { type: "integer", minimum: 1, maximum: 3 } }, ["x", "y"]),
|
|
10
|
+
tool("move", "Move the cloud mouse pointer.", { x: coordinate, y: coordinate }, ["x", "y"]),
|
|
11
|
+
tool("drag", "Drag on the cloud desktop.", { x: coordinate, y: coordinate, to_x: coordinate, to_y: coordinate }, ["x", "y", "to_x", "to_y"]),
|
|
12
|
+
tool("type_text", "Type Unicode text into the focused cloud application.", { text: { type: "string", maxLength: 4000 } }, ["text"]),
|
|
13
|
+
tool("key_press", "Press an X11 key or shortcut.", { key: { type: "string", pattern: "^[A-Za-z0-9_+]+$", minLength: 1, maxLength: 100 } }, ["key"]),
|
|
14
|
+
tool("scroll", "Scroll at a point on the cloud desktop.", { x: coordinate, y: coordinate, direction: { type: "string", enum: ["up", "down", "left", "right"] }, amount: { type: "integer", minimum: 1, maximum: 30 } }, ["x", "y", "direction"]),
|
|
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
|
+
tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
|
|
17
|
+
];
|
|
18
|
+
const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
19
|
+
const buttons = { left: 1, middle: 2, right: 3 };
|
|
20
|
+
const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
|
|
21
|
+
let validators;
|
|
22
|
+
/** Why a call cannot run, or null. Checked before anything else touches the
|
|
23
|
+
* call: x and y reach the remote shell unquoted, so only the advertised
|
|
24
|
+
* integer schema stands between a model's arguments and a command line. */
|
|
25
|
+
export function cloudComputerCallProblem(name, args) {
|
|
26
|
+
validators ??= new Map(CLOUD_COMPUTER_TOOLS.map(entry => [entry.name, compileToolSchema(entry.inputSchema)]));
|
|
27
|
+
const validate = typeof name === "string" ? validators.get(name) : undefined;
|
|
28
|
+
if (!validate)
|
|
29
|
+
return "Unknown cloud computer tool. Use one of the advertised computer tools.";
|
|
30
|
+
if (!args || typeof args !== "object" || Array.isArray(args) || !validate(args)) {
|
|
31
|
+
return "Tool arguments do not match the advertised input schema; use its required fields and types.";
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** One tool call on the given Boat with the server's own Boat account. The
|
|
36
|
+
* caller has already checked the arguments and the person's control. */
|
|
37
|
+
export async function runCloudComputerTool(cfg, boxId, name, args, signal) {
|
|
38
|
+
const problem = cloudComputerCallProblem(name, args);
|
|
39
|
+
if (problem)
|
|
40
|
+
return { isError: true, content: [{ type: "text", text: problem }] };
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
if (name === "screenshot") {
|
|
43
|
+
const shot = await screenshotBoat(cfg, "", boxId, { signal, nativeSize: true });
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
return { content: [{ type: "image", mimeType: "image/jpeg", data: shot.png }] };
|
|
46
|
+
}
|
|
47
|
+
const a = args;
|
|
48
|
+
const mouse = `xdotool mousemove --sync ${a.x} ${a.y}`;
|
|
49
|
+
let command;
|
|
50
|
+
switch (name) {
|
|
51
|
+
case "get_screen_size":
|
|
52
|
+
command = "xdotool getdisplaygeometry";
|
|
53
|
+
break;
|
|
54
|
+
case "click":
|
|
55
|
+
command = `${mouse} click --repeat ${a.count ?? 1} --delay 100 ${buttons[String(a.button ?? "left")]}`;
|
|
56
|
+
break;
|
|
57
|
+
case "move":
|
|
58
|
+
command = mouse;
|
|
59
|
+
break;
|
|
60
|
+
case "drag":
|
|
61
|
+
command = `${mouse} mousedown 1 mousemove --sync ${a.to_x} ${a.to_y} mouseup 1`;
|
|
62
|
+
break;
|
|
63
|
+
case "type_text":
|
|
64
|
+
command = `printf %s ${quote(Buffer.from(String(a.text)).toString("base64"))} | base64 -d | xclip -selection clipboard && xdotool key --clearmodifiers ctrl+v`;
|
|
65
|
+
break;
|
|
66
|
+
case "key_press":
|
|
67
|
+
command = `xdotool key --clearmodifiers ${quote(String(a.key))}`;
|
|
68
|
+
break;
|
|
69
|
+
case "scroll":
|
|
70
|
+
command = `${mouse} click --repeat ${a.amount ?? 3} --delay 80 ${scrollButtons[String(a.direction)]}`;
|
|
71
|
+
break;
|
|
72
|
+
case "open_url":
|
|
73
|
+
command = `nohup xdg-open ${quote(String(a.url))} >/dev/null 2>&1 </dev/null &`;
|
|
74
|
+
break;
|
|
75
|
+
case "exec":
|
|
76
|
+
command = String(a.command);
|
|
77
|
+
break;
|
|
78
|
+
default: return { isError: true, content: [{ type: "text", text: "Unknown cloud computer tool." }] };
|
|
79
|
+
}
|
|
80
|
+
const result = await runCommand(cfg, boxId, isolatedRemoteCommand(command), { signal });
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
if (name === "open_url" && result.ok)
|
|
83
|
+
return { content: [{ type: "text", text: "Browser launch requested. Page loading is not confirmed; inspect the screen before continuing." }] };
|
|
84
|
+
return { isError: !result.ok, content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
85
|
+
}
|
|
86
|
+
/** One JSON-RPC request from harness-mcp-proxy, answered with its MCP
|
|
87
|
+
* `result`. The order is the safety argument: arguments are checked before
|
|
88
|
+
* the control gate (which may claim the seat), a person's control refuses
|
|
89
|
+
* the call before anything reaches the Boat, and the turn's capability must
|
|
90
|
+
* still be live before and after the Boat acts. */
|
|
91
|
+
export async function cloudComputerRpc(request, deps) {
|
|
92
|
+
if (request?.method === "tools/list") {
|
|
93
|
+
deps.assertActive();
|
|
94
|
+
return { tools: CLOUD_COMPUTER_TOOLS };
|
|
95
|
+
}
|
|
96
|
+
if (request?.method !== "tools/call")
|
|
97
|
+
throw Object.assign(new Error("unsupported computer method"), { status: 400 });
|
|
98
|
+
const params = (request.params && typeof request.params === "object" ? request.params : {});
|
|
99
|
+
const args = params.arguments ?? {};
|
|
100
|
+
const problem = cloudComputerCallProblem(params.name, args);
|
|
101
|
+
if (problem)
|
|
102
|
+
return { isError: true, content: [{ type: "text", text: problem }] };
|
|
103
|
+
const control = await deps.gate();
|
|
104
|
+
deps.assertActive();
|
|
105
|
+
if (control.held)
|
|
106
|
+
return { isError: true, content: [{ type: "text", text: control.blockedReason || CONTROL_REFUSAL_PLAIN }] };
|
|
107
|
+
const result = await runCloudComputerTool(deps.cfg, deps.boxId, params.name, args, deps.signal);
|
|
108
|
+
deps.assertActive();
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// forwards with X-Forwarded-*, so request-auth.ts never grants a remote
|
|
9
9
|
// request loopback trust. If either child exits, both stop and the machine
|
|
10
10
|
// restarts; the one exception is the server asking to be started again
|
|
11
|
-
// after a restore.
|
|
11
|
+
// after a restore (server/restart.ts).
|
|
12
12
|
//
|
|
13
13
|
// The machine's secrets (the signing secret, the relay tokens) arrive as
|
|
14
14
|
// this process's environment. No child's environment ever carries them:
|
|
@@ -24,7 +24,8 @@ import { spawn } from "node:child_process";
|
|
|
24
24
|
import { chownSync, readFileSync, statSync } from "node:fs";
|
|
25
25
|
import { dirname, join, posix, resolve } from "node:path";
|
|
26
26
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
|
-
import {
|
|
27
|
+
import { CLOUD_SECRETS_FD_ENV, cloudHomeConfiguration, cloudHomeHost, cloudHomeSecrets, prepareCloudHomeVolume, withoutCloudSecrets, withoutIgnoredCloudKeys, } from "./cloud-home.js";
|
|
28
|
+
import { restartPolicy } from "./restart.js";
|
|
28
29
|
const SERVICE_USER = "maus";
|
|
29
30
|
/** The descriptor the server reads its secrets from. */
|
|
30
31
|
const SECRETS_FD = 3;
|
|
@@ -112,11 +113,6 @@ export function spawnWithSecrets(command, args, env, secrets, ids) {
|
|
|
112
113
|
pipe?.end(JSON.stringify(secrets));
|
|
113
114
|
return child;
|
|
114
115
|
}
|
|
115
|
-
/** What the launcher does when the server child exits: start it again only
|
|
116
|
-
* when it asked to (a committed restore), and only a few times in a row. */
|
|
117
|
-
export function serverExitAction(code, stopping, restarts) {
|
|
118
|
-
return !stopping && code === CLOUD_HOME_RESTART_EXIT_CODE && restarts < 5 ? "restart" : "stop";
|
|
119
|
-
}
|
|
120
116
|
export function startCloudHome(env = process.env) {
|
|
121
117
|
process.umask(0o077);
|
|
122
118
|
const config = cloudHomeConfiguration(env);
|
|
@@ -189,14 +185,15 @@ export function startCloudHome(env = process.env) {
|
|
|
189
185
|
stop(true);
|
|
190
186
|
});
|
|
191
187
|
};
|
|
192
|
-
|
|
193
|
-
const runServer = () =>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
188
|
+
const policy = restartPolicy();
|
|
189
|
+
const runServer = () => {
|
|
190
|
+
watch(spawnWithSecrets(process.execPath, [join(here, "index.js")], server, secrets, ids), (code) => {
|
|
191
|
+
if (!policy.again(code, stopping))
|
|
192
|
+
return false;
|
|
193
|
+
runServer();
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
196
|
+
};
|
|
200
197
|
runServer();
|
|
201
198
|
watch(spawn(edgeBin, ["run", "--config", edgeConfig, "--adapter", "caddyfile"], { env: edge, stdio: "inherit", ...(ids ? { uid: ids.uid, gid: ids.gid } : {}) }));
|
|
202
199
|
process.once("SIGTERM", () => stop(false));
|
|
@@ -26,7 +26,6 @@ import { join } from "node:path";
|
|
|
26
26
|
import { writeFileAtomic } from "./atomic.js";
|
|
27
27
|
import { hostedWorkspaceConfigured } from "./enterprise.js";
|
|
28
28
|
import { formatPairingCode } from "./sessions.js";
|
|
29
|
-
export const CLOUD_HOME_CONTRACT_VERSION = 1;
|
|
30
29
|
/** Any of these switches the server into Cloud home mode; then all are required. */
|
|
31
30
|
export const CLOUD_HOME_KEYS = ["OMB_CLOUD_ROLE", "OMB_CLOUD_MACHINE_ID", "OMB_CLOUD_ADMIN_URL", "OMB_CLOUD_BOOTSTRAP_SECRET"];
|
|
32
31
|
/** A platform model gateway's settings. A Cloud home never uses them: given
|
|
@@ -46,10 +45,6 @@ export const CLOUD_PAIRING_SKEW_S = 300;
|
|
|
46
45
|
export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
|
|
47
46
|
const MAX_NONCES = 10_000;
|
|
48
47
|
export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
|
|
49
|
-
/** The server exits with this after a restore commits (Move to Cloud): the
|
|
50
|
-
* launcher then starts it again, and startup installs the restore. Any other
|
|
51
|
-
* exit stops the machine for Fly to restart. */
|
|
52
|
-
export const CLOUD_HOME_RESTART_EXIT_CODE = 75;
|
|
53
48
|
export function cloudHomeConfigured(env = process.env) {
|
|
54
49
|
return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
|
|
55
50
|
}
|
|
@@ -21,9 +21,6 @@ import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { writeFileAtomic } from "./atomic.js";
|
|
23
23
|
const personOf = (line) => line?.role === "user" && !line.peerAsk ? line.sender?.id : undefined;
|
|
24
|
-
export function cloudHomeTurnMayLend(turn) {
|
|
25
|
-
return cloudHomeLendingRefusal(turn) === null;
|
|
26
|
-
}
|
|
27
24
|
export function cloudHomeLendingRefusal(turn) {
|
|
28
25
|
const { request } = turn;
|
|
29
26
|
if (!request?.messageId || request.stopped || !request.generations.has(turn.generation))
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { sharedSignIn } from "./admin-activity.js";
|
|
2
3
|
import { backupsBytes, beginUpload, CLOUD_MOVE_MAX_BYTES, CLOUD_MOVE_MAX_PART_BYTES, CLOUD_MOVE_PART_BYTES, completedUpload, discardNextPreviousCloud, discardUpload, enabledRoutineCount, forgetMoveRestore, freeVolumeBytes, isEmptyWorkspace, moveSpaceNeeded, noteMoveRestore, prepareNextPreviousCloud, previousCloud, previousCloudArchiveBytes, removeMoveFiles, stagePreviousCloud, tidyCloudMoveStorage, totalVolumeBytes, uploadStatus, validUploadDeclaration, workspaceContents, workspaceMoveSize, writeUploadPart, } from "./cloud-move.js";
|
|
3
4
|
import { commitPendingWorkspaceRestore, createWorkspaceBackup, stageWorkspaceBackup } from "./workspace-backup.js";
|
|
4
5
|
export const CLOUD_MOVE_PREFIX = "/api/cloud-move";
|
|
5
6
|
const failure = (message, status) => Object.assign(new Error(message), { status });
|
|
6
7
|
const HELD_CACHE_MS = 30_000;
|
|
8
|
+
/** Shared with other people, so one person's copy must never replace it: a
|
|
9
|
+
* hosted organisation workspace, or (not on a Cloud home, whose sign-in is its
|
|
10
|
+
* owner's OMB Cloud account) an email sign-in list that lets someone else in
|
|
11
|
+
* (sharedSignIn: a member, a second admin, or a whole @domain). The owner's
|
|
12
|
+
* own address alone (`openmausbot access add you@example.com`) is not. */
|
|
13
|
+
export function workspaceShared(input) {
|
|
14
|
+
return input.hosted || (!input.cloudHome && sharedSignIn(input.signIn));
|
|
15
|
+
}
|
|
7
16
|
export function createCloudMoveRoutes(options) {
|
|
8
17
|
const freeBytes = options.freeBytes ?? freeVolumeBytes;
|
|
9
18
|
const volumeBytes = (path) => { try {
|
|
@@ -12,16 +21,17 @@ export function createCloudMoveRoutes(options) {
|
|
|
12
21
|
catch {
|
|
13
22
|
return null;
|
|
14
23
|
} };
|
|
15
|
-
// A
|
|
16
|
-
// staged files
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
+
// A copy's restore that startup has installed keeps no safety copy or
|
|
25
|
+
// staged files, and the workspace it replaced becomes the one to swap back
|
|
26
|
+
// to; an abandoned upload goes after a day. Only what a copy recorded is
|
|
27
|
+
// touched, so on a server that never received one this does nothing.
|
|
28
|
+
try {
|
|
29
|
+
tidyCloudMoveStorage(options.dataDir, options.restored);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.warn(`workspace copy: could not tidy backup storage (${error instanceof Error ? error.message : "unknown"})`);
|
|
24
33
|
}
|
|
34
|
+
const identity = { appVersion: options.appVersion, environmentId: options.environmentId };
|
|
25
35
|
// Staged here by a preview; restore accepts nothing else.
|
|
26
36
|
const staged = new Set();
|
|
27
37
|
let job = null;
|
|
@@ -33,16 +43,16 @@ export function createCloudMoveRoutes(options) {
|
|
|
33
43
|
};
|
|
34
44
|
const check = (req, auth) => {
|
|
35
45
|
if (!options.authorized(req, auth))
|
|
36
|
-
throw failure("Your session changed. Start the
|
|
46
|
+
throw failure("Your session changed. Start the copy again.", 403);
|
|
37
47
|
};
|
|
38
48
|
const ready = () => {
|
|
39
49
|
if (job?.state === "running" || writing)
|
|
40
|
-
throw failure("Another
|
|
50
|
+
throw failure("Another copy step is running on this server. Wait for it to finish.", 409);
|
|
41
51
|
const status = options.status();
|
|
42
52
|
if (status.pendingRestore)
|
|
43
|
-
throw failure("
|
|
53
|
+
throw failure("This server is restarting to finish a restore. Try again in a minute.", 409);
|
|
44
54
|
if (status.busy)
|
|
45
|
-
throw failure("
|
|
55
|
+
throw failure("This server is busy with a backup. Try again when it finishes.", 409);
|
|
46
56
|
};
|
|
47
57
|
/** Delete what earlier previews staged and no restore took. */
|
|
48
58
|
const dropStaged = () => {
|
|
@@ -63,7 +73,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
63
73
|
after?.(result);
|
|
64
74
|
}, (error) => {
|
|
65
75
|
if (job === current)
|
|
66
|
-
job = { kind, state: "failed", error: error instanceof Error ? error.message : "The
|
|
76
|
+
job = { kind, state: "failed", error: error instanceof Error ? error.message : "The copy could not finish." };
|
|
67
77
|
});
|
|
68
78
|
}
|
|
69
79
|
// A person's own page can hold the workspace gate for a moment (an ordinary
|
|
@@ -106,7 +116,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
106
116
|
const roomFor = (archive, already = 0) => {
|
|
107
117
|
const needed = moveSpaceNeeded(archive, workspaceMoveSize(options.dataDir).bytes, true) - already;
|
|
108
118
|
const free = freeBytes(options.dataDir);
|
|
109
|
-
return free >= needed ? null : { error: "
|
|
119
|
+
return free >= needed ? null : { error: "This server does not have enough free space for this.", freeBytes: free, neededBytes: needed };
|
|
110
120
|
};
|
|
111
121
|
// The answer that the restore is ready goes out first; then the launcher
|
|
112
122
|
// starts the server again, and startup installs the restore before anything else loads.
|
|
@@ -117,15 +127,16 @@ export function createCloudMoveRoutes(options) {
|
|
|
117
127
|
const method = req.method ?? "GET";
|
|
118
128
|
try {
|
|
119
129
|
if (!auth.scopes.includes("admin"))
|
|
120
|
-
throw failure("Only the owner can
|
|
130
|
+
throw failure("Only the owner can copy a workspace.", 403);
|
|
121
131
|
if (method === "GET" && path === `${CLOUD_MOVE_PREFIX}/estimate`) {
|
|
122
|
-
json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), routines: enabledRoutineCount(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES });
|
|
132
|
+
json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), routines: enabledRoutineCount(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES, ...identity });
|
|
123
133
|
return true;
|
|
124
134
|
}
|
|
125
|
-
if (!options.cloudHome)
|
|
126
|
-
throw failure("Only an OMB Cloud home receives a moved workspace.", 404);
|
|
127
135
|
if (auth.kind !== "session")
|
|
128
|
-
throw failure("
|
|
136
|
+
throw failure("Copying a workspace needs the owner's signed-in app.", 403);
|
|
137
|
+
if (options.sharedWorkspace()) {
|
|
138
|
+
throw Object.assign(failure("This server is shared with other people, so it can't receive one computer's bots and chats. Copy to a server only you use.", 403), { code: "shared_workspace" });
|
|
139
|
+
}
|
|
129
140
|
check(req, auth);
|
|
130
141
|
if (method === "GET" && path === CLOUD_MOVE_PREFIX) {
|
|
131
142
|
const contents = workspaceContents(options.dataDir);
|
|
@@ -134,8 +145,10 @@ export function createCloudMoveRoutes(options) {
|
|
|
134
145
|
json(res, 200, {
|
|
135
146
|
contents, empty: isEmptyWorkspace(contents), freeBytes: freeBytes(options.dataDir), volumeBytes: volumeBytes(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES,
|
|
136
147
|
partBytes: CLOUD_MOVE_PART_BYTES, upload: uploadStatus(options.dataDir), previous: previousCloud(options.dataDir), job,
|
|
137
|
-
// What backups and the previous
|
|
148
|
+
// What backups and the previous workspace hold on the volume.
|
|
138
149
|
heldBytes: held.bytes,
|
|
150
|
+
// The app refuses a server older than itself, or its own, before exporting.
|
|
151
|
+
...identity,
|
|
139
152
|
...options.status(),
|
|
140
153
|
lastRestoreId: options.restored.restored ? options.restored.id ?? null : null,
|
|
141
154
|
rolledBackId: options.restored.rolledBack ? options.restored.id ?? null : null,
|
|
@@ -191,7 +204,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
191
204
|
}
|
|
192
205
|
if (discardPreview) {
|
|
193
206
|
removeMoveFiles(options.dataDir, result.id);
|
|
194
|
-
throw failure("The
|
|
207
|
+
throw failure("The copy was stopped; what it staged was removed.", 409);
|
|
195
208
|
}
|
|
196
209
|
try {
|
|
197
210
|
check(req, auth);
|
|
@@ -213,7 +226,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
213
226
|
if (method === "POST" && path === `${CLOUD_MOVE_PREFIX}/discard`) {
|
|
214
227
|
await options.readBody(req, 4096);
|
|
215
228
|
if (job?.state === "running" && job.kind !== "preview")
|
|
216
|
-
throw failure("
|
|
229
|
+
throw failure("This server is already replacing its workspace.", 409);
|
|
217
230
|
// A preview still running removes what it stages when it ends.
|
|
218
231
|
if (job?.state === "running")
|
|
219
232
|
discardPreview = true;
|
|
@@ -226,7 +239,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
226
239
|
const body = z.object({ id: z.string().uuid() }).parse(await options.readBody(req, 4096));
|
|
227
240
|
ready();
|
|
228
241
|
if (!staged.has(body.id))
|
|
229
|
-
throw failure("This
|
|
242
|
+
throw failure("This copy is no longer ready on this server. Start it again.", 404);
|
|
230
243
|
start("restore", async () => {
|
|
231
244
|
const { committed, previous } = await replaceWith(body.id, req, auth);
|
|
232
245
|
return { kind: "restore", state: "done", id: committed.id, previous };
|
|
@@ -238,15 +251,15 @@ export function createCloudMoveRoutes(options) {
|
|
|
238
251
|
await options.readBody(req, 4096);
|
|
239
252
|
ready();
|
|
240
253
|
if (!previousCloud(options.dataDir))
|
|
241
|
-
throw failure("There is
|
|
254
|
+
throw failure("There is nothing on this server to swap back to.", 404);
|
|
242
255
|
const refused = roomFor(previousCloudArchiveBytes(options.dataDir));
|
|
243
256
|
if (refused) {
|
|
244
257
|
json(res, 507, refused);
|
|
245
258
|
return true;
|
|
246
259
|
}
|
|
247
260
|
dropStaged();
|
|
248
|
-
// A swap: what
|
|
249
|
-
// can be undone the same way.
|
|
261
|
+
// A swap: what this server has now becomes the previous workspace, so
|
|
262
|
+
// this can be undone the same way.
|
|
250
263
|
start("undo", async () => {
|
|
251
264
|
const prepared = await stagePreviousCloud(options.dataDir, options.appVersion);
|
|
252
265
|
const { committed, previous } = await replaceWith(prepared.id, req, auth);
|
|
@@ -255,7 +268,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
255
268
|
json(res, 202, { job });
|
|
256
269
|
return true;
|
|
257
270
|
}
|
|
258
|
-
json(res, 404, { error: "Unknown
|
|
271
|
+
json(res, 404, { error: "Unknown copy operation." });
|
|
259
272
|
}
|
|
260
273
|
catch (error) {
|
|
261
274
|
if (res.headersSent) {
|
|
@@ -263,10 +276,11 @@ export function createCloudMoveRoutes(options) {
|
|
|
263
276
|
return true;
|
|
264
277
|
}
|
|
265
278
|
const status = error instanceof z.ZodError ? 400 : error.status ?? 400;
|
|
266
|
-
const received = error.received;
|
|
279
|
+
const received = error.received, code = error.code;
|
|
267
280
|
json(res, status, {
|
|
268
|
-
error: error instanceof z.ZodError ? "Invalid
|
|
281
|
+
error: error instanceof z.ZodError ? "Invalid copy request." : error instanceof Error ? error.message : "The copy failed.",
|
|
269
282
|
...(Number.isSafeInteger(received) ? { received } : {}),
|
|
283
|
+
...(code === "shared_workspace" ? { code } : {}),
|
|
270
284
|
});
|
|
271
285
|
}
|
|
272
286
|
return true;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
//
|
|
2
|
-
// workspace onto
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// Copy this computer here (docs/copy-workspace.md): a person's copy of their
|
|
2
|
+
// desktop's workspace onto a server they own and added in the desktop app, an
|
|
3
|
+
// OMB Cloud home included. The desktop exports the ordinary encrypted
|
|
4
|
+
// workspace backup (workspace-backup.ts) and uploads it here in parts; this
|
|
5
|
+
// server then previews and restores it like any other backup, so everything
|
|
6
|
+
// that backup policy keeps out (credentials, sign-ins, pairing and sessions,
|
|
7
|
+
// runtime state) stays where it is on both sides.
|
|
7
8
|
//
|
|
8
9
|
// What this file adds: the workspace's size and contents, a resumable upload
|
|
9
|
-
// slot, the space check for the
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// slot, the space check for the server's volume, a backup of this server's
|
|
11
|
+
// own workspace taken before it is replaced (Swap back puts it back), and the
|
|
12
|
+
// tidying that keeps that backup the only copy left behind. Everything lives
|
|
13
|
+
// under `.backups`, which no snapshot includes and no restore replaces. The
|
|
14
|
+
// file and folder names still say "cloud" (`cloud-move`, `cloud-previous`):
|
|
15
|
+
// renaming them would orphan the swap back on servers already running.
|
|
14
16
|
import { createHash, randomBytes } from "node:crypto";
|
|
15
17
|
import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statfsSync, writeFileSync } from "node:fs";
|
|
16
18
|
import { open } from "node:fs/promises";
|
|
@@ -167,9 +169,9 @@ export function validUploadDeclaration(input) {
|
|
|
167
169
|
if (!Number.isSafeInteger(input.bytes) || input.bytes < MIN_BYTES)
|
|
168
170
|
throw fail("This is not a workspace backup.", 400);
|
|
169
171
|
if (input.bytes > CLOUD_MOVE_MAX_BYTES)
|
|
170
|
-
throw fail("This workspace is larger than the 10 GB a
|
|
172
|
+
throw fail("This workspace is larger than the 10 GB a copy can carry.", 413);
|
|
171
173
|
if (input.files !== undefined && (!Number.isSafeInteger(input.files) || input.files < 0 || input.files > CLOUD_MOVE_MAX_FILES)) {
|
|
172
|
-
throw fail("This workspace has more than the 100,000 files a
|
|
174
|
+
throw fail("This workspace has more than the 100,000 files a copy can carry.", 413);
|
|
173
175
|
}
|
|
174
176
|
return { sha256: input.sha256, bytes: input.bytes };
|
|
175
177
|
}
|
|
@@ -193,7 +195,7 @@ export function beginUpload(dataDir, declared, now = Date.now()) {
|
|
|
193
195
|
export async function writeUploadPart(dataDir, sha256, offset, length, body, now = Date.now()) {
|
|
194
196
|
const meta = readUploadMeta(dataDir);
|
|
195
197
|
if (!meta || meta.sha256 !== sha256)
|
|
196
|
-
throw fail("This upload is not in progress on
|
|
198
|
+
throw fail("This upload is not in progress on this server. Start the copy again.", 404);
|
|
197
199
|
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length <= 0 || length > CLOUD_MOVE_MAX_PART_BYTES) {
|
|
198
200
|
throw fail("Invalid upload part.", 400);
|
|
199
201
|
}
|
|
@@ -238,7 +240,7 @@ export async function writeUploadPart(dataDir, sha256, offset, length, body, now
|
|
|
238
240
|
export async function completedUpload(dataDir, sha256) {
|
|
239
241
|
const status = uploadStatus(dataDir);
|
|
240
242
|
if (!status || status.sha256 !== sha256)
|
|
241
|
-
throw fail("This upload is not in progress on
|
|
243
|
+
throw fail("This upload is not in progress on this server. Start the copy again.", 404);
|
|
242
244
|
if (status.received !== status.bytes)
|
|
243
245
|
throw fail("The upload is not complete yet.", 409, { received: status.received });
|
|
244
246
|
const hash = createHash("sha256");
|
|
@@ -246,7 +248,7 @@ export async function completedUpload(dataDir, sha256) {
|
|
|
246
248
|
hash.update(chunk);
|
|
247
249
|
if (hash.digest("hex") !== sha256) {
|
|
248
250
|
discardUpload(dataDir);
|
|
249
|
-
throw fail("The uploaded file was damaged on the way. Start the
|
|
251
|
+
throw fail("The uploaded file was damaged on the way. Start the copy again.", 400);
|
|
250
252
|
}
|
|
251
253
|
return uploadPartPath(dataDir);
|
|
252
254
|
}
|
|
@@ -312,7 +314,7 @@ export function discardNextPreviousCloud(dataDir) {
|
|
|
312
314
|
export async function stagePreviousCloud(dataDir, appVersion) {
|
|
313
315
|
const previous = readPrevious(previousFolder(dataDir));
|
|
314
316
|
if (!previous || isEmptyWorkspace(previous.contents))
|
|
315
|
-
throw fail("There is
|
|
317
|
+
throw fail("There is nothing on this server to swap back to.", 404);
|
|
316
318
|
const archive = join(previousFolder(dataDir), "workspace.ombbackup");
|
|
317
319
|
const staged = await stageWorkspaceBackup(dataDir, archive, { password: previous.password, currentAppVersion: appVersion });
|
|
318
320
|
return { ...staged, bytes: lstatSync(archive).size };
|
|
@@ -373,7 +375,7 @@ function pendingRestoreId(dataDir) {
|
|
|
373
375
|
return null;
|
|
374
376
|
}
|
|
375
377
|
}
|
|
376
|
-
/** At
|
|
378
|
+
/** At every server's startup, after any pending restore was applied:
|
|
377
379
|
* - a next previous Cloud becomes the previous Cloud once its restore is
|
|
378
380
|
* installed, and is dropped otherwise;
|
|
379
381
|
* - moves' and swaps' restores, once settled, keep no safety copy or staged
|
|
@@ -244,9 +244,35 @@ function trustedAuthUrl(value, slug) {
|
|
|
244
244
|
}
|
|
245
245
|
return url.toString();
|
|
246
246
|
}
|
|
247
|
+
/** Composio's own hosts are always trusted. A backend the operator pointed
|
|
248
|
+
* the app at explicitly (OMB_COMPOSIO_API — a dev or test stub) may hand back
|
|
249
|
+
* a Session on its own origin, since the API itself was already trusted that
|
|
250
|
+
* far; any other host is refused. */
|
|
251
|
+
export function trustedSessionMcpUrl(value) {
|
|
252
|
+
let mcp;
|
|
253
|
+
try {
|
|
254
|
+
mcp = new URL(value);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (mcp.protocol === "https:" && (mcp.hostname === "composio.dev" || mcp.hostname.endsWith(".composio.dev")))
|
|
260
|
+
return true;
|
|
261
|
+
if (mcp.protocol !== "https:" && !(mcp.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(mcp.hostname)))
|
|
262
|
+
return false;
|
|
263
|
+
const override = process.env.OMB_COMPOSIO_API;
|
|
264
|
+
if (!override)
|
|
265
|
+
return false;
|
|
266
|
+
try {
|
|
267
|
+
return new URL(override).origin === mcp.origin;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
247
273
|
function parseSessionResponse(session) {
|
|
248
274
|
const mcp = new URL(session.mcp.url);
|
|
249
|
-
if (
|
|
275
|
+
if (!trustedSessionMcpUrl(session.mcp.url)) {
|
|
250
276
|
throw new Error("Composio returned an untrusted Session MCP URL");
|
|
251
277
|
}
|
|
252
278
|
return { ...session, mcp: { ...session.mcp, url: mcp.toString() } };
|
|
@@ -497,7 +523,7 @@ export async function mcpIntegration(cfg, context) {
|
|
|
497
523
|
},
|
|
498
524
|
};
|
|
499
525
|
}
|
|
500
|
-
export async function relayMcp(cfg, payload, transportSessionId) {
|
|
526
|
+
export async function relayMcp(cfg, payload, transportSessionId, beforeSend) {
|
|
501
527
|
const apiKey = projectApiKey(cfg);
|
|
502
528
|
let url;
|
|
503
529
|
let identity;
|
|
@@ -529,6 +555,9 @@ export async function relayMcp(cfg, payload, transportSessionId) {
|
|
|
529
555
|
if (forwardedTransportSessionId) {
|
|
530
556
|
headers.set("mcp-session-id", forwardedTransportSessionId);
|
|
531
557
|
}
|
|
558
|
+
// Session discovery can await network I/O. Turn authority and mutable
|
|
559
|
+
// permissions must be checked after it, immediately before dispatch.
|
|
560
|
+
beforeSend?.();
|
|
532
561
|
const response = await fetch(url, {
|
|
533
562
|
method: "POST",
|
|
534
563
|
headers,
|
|
@@ -1077,7 +1106,7 @@ export async function removeAccount(cfg, slug, accountId) {
|
|
|
1077
1106
|
}
|
|
1078
1107
|
/** Mint a browser auth link for one service. Returns { url } or throws. */
|
|
1079
1108
|
export async function authorizeService(cfg, slug, requestedAlias) {
|
|
1080
|
-
|
|
1109
|
+
let alias = normalizeAccountAlias(requestedAlias);
|
|
1081
1110
|
const toolkit = canonicalToolkitSlug(slug);
|
|
1082
1111
|
const mode = connectionMode(cfg);
|
|
1083
1112
|
const unavailable = managedConnectorUnavailableReason(mode, toolkit);
|
|
@@ -1107,8 +1136,15 @@ export async function authorizeService(cfg, slug, requestedAlias) {
|
|
|
1107
1136
|
if (usableAccounts.length >= MULTI_ACCOUNT_CONFIG.max_accounts_per_toolkit) {
|
|
1108
1137
|
throw inputError(`${toolkit} already has the maximum of ${MULTI_ACCOUNT_CONFIG.max_accounts_per_toolkit} accounts`, 409);
|
|
1109
1138
|
}
|
|
1110
|
-
if (
|
|
1111
|
-
|
|
1139
|
+
if (serviceAccounts.length > 0 && !alias) {
|
|
1140
|
+
if (serviceAccounts.every((account) => /^(initializing|initiated|expired)$/i.test(account.status ?? ""))) {
|
|
1141
|
+
// Retry without replacing a named account or revoking a former grant:
|
|
1142
|
+
// EXPIRED can describe either an abandoned flow or an expired credential.
|
|
1143
|
+
alias = `omb-retry-${randomUUID()}`;
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
throw inputError("Add an account alias so the existing connection is not replaced");
|
|
1147
|
+
}
|
|
1112
1148
|
}
|
|
1113
1149
|
if (alias && serviceAccounts.some((account) => account.alias?.trim().toLowerCase() === alias.toLowerCase())) {
|
|
1114
1150
|
throw inputError(`Account alias "${alias}" is already in use for ${toolkit}`, 409);
|
|
@@ -153,18 +153,3 @@ export class ComputerControl {
|
|
|
153
153
|
return snapshot;
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
/** Safely executes an async tool or desktop control call, catching runtime exceptions gracefully. */
|
|
157
|
-
export async function executeToolSafely(toolName, fn) {
|
|
158
|
-
try {
|
|
159
|
-
const data = await fn();
|
|
160
|
-
return { success: true, data };
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
const errorMessage = err?.message || String(err) || 'Unknown execution error';
|
|
164
|
-
console.error(`[Tool Execution Error] Name: ${toolName} | Details: ${errorMessage}`);
|
|
165
|
-
return {
|
|
166
|
-
success: false,
|
|
167
|
-
error: `Tool '${toolName}' failed to execute: ${errorMessage}`
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|