openmausbot 0.1.93 → 0.1.94
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-CKNR3SEW.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-BBdBuCgB.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-BkYwilkG.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-bq1PTDTW.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -0
- package/dist-server/companion/src/routes.js +21 -0
- package/dist-server/container-mcp.js +93 -76
- package/dist-server/drivers/agents-proxy.js +38 -1
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/index.js +16942 -13191
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +147 -19
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8233 -7651
- package/dist-server/pair-cli.js +8233 -7651
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/auto-approve.js +33 -11
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +6 -0
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +44 -10
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/drivers/acp/core.js +57 -8
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/agents-call.js +28 -0
- package/dist-server/server/drivers/agents-catalog.js +10 -1
- package/dist-server/server/drivers/boatagent.js +2 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +27 -14
- package/dist-server/server/drivers/claude.js +18 -6
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +11 -3
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1143 -254
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/skill-library.js +207 -0
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +32 -4
- package/dist-server/server/surface.js +9 -3
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/thread-events.js +1 -1
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/shared/ask-question.js +13 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +93 -76
- package/dist-server/workspace-backup.worker.js +89 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-BU94VpxE.js +0 -1
- package/dist/assets/index-Biji-bfL.css +0 -1
- package/dist/assets/index-DZlhoES0.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
|
@@ -94,7 +94,70 @@ function questionAnswersById(message, questions) {
|
|
|
94
94
|
return only && flat ? { [only.id]: flat } : {};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// shared/tool-scope.ts
|
|
98
|
+
var SERVER_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
99
|
+
var CONTROL_CHARACTER = /[\x00-\x1f\x7f]/;
|
|
100
|
+
var MAX_SELECTORS = 256;
|
|
101
|
+
var MAX_SELECTOR_LENGTH = 1024;
|
|
102
|
+
function validName(name) {
|
|
103
|
+
return typeof name === "string" && name.trim().length > 0 && name.length <= MAX_SELECTOR_LENGTH && !CONTROL_CHARACTER.test(name) && !name.includes("*");
|
|
104
|
+
}
|
|
105
|
+
function selectorIdentity(selector) {
|
|
106
|
+
if (selector.length > MAX_SELECTOR_LENGTH || CONTROL_CHARACTER.test(selector)) return;
|
|
107
|
+
if (selector.startsWith("native:")) {
|
|
108
|
+
const name = selector.slice(7);
|
|
109
|
+
if (name === "*" || validName(name)) return { kind: "native", name };
|
|
110
|
+
} else if (selector.startsWith("mcp:")) {
|
|
111
|
+
const separator = selector.indexOf(":", 4);
|
|
112
|
+
if (separator < 0) return;
|
|
113
|
+
const server = selector.slice(4, separator);
|
|
114
|
+
const name = selector.slice(separator + 1);
|
|
115
|
+
if (SERVER_NAME.test(server) && (name === "*" || validName(name))) {
|
|
116
|
+
return { kind: "mcp", server, name };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function parseToolScope(value) {
|
|
121
|
+
if (value === void 0) return { ok: true, scope: void 0 };
|
|
122
|
+
const invalid = { ok: false, error: "Tool selection must contain only valid allow and deny lists." };
|
|
123
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid;
|
|
124
|
+
const prototype = Object.getPrototypeOf(value);
|
|
125
|
+
if (prototype !== Object.prototype && prototype !== null) return invalid;
|
|
126
|
+
const scope = {};
|
|
127
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
128
|
+
if (key !== "allow" && key !== "deny") return invalid;
|
|
129
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
130
|
+
if (!descriptor || !("value" in descriptor)) return invalid;
|
|
131
|
+
const list = descriptor.value;
|
|
132
|
+
if (!Array.isArray(list) || list.length > MAX_SELECTORS) return invalid;
|
|
133
|
+
if (!Array.from(list).every((entry) => typeof entry === "string" && selectorIdentity(entry))) return invalid;
|
|
134
|
+
scope[key] = [...new Set(list)];
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, scope };
|
|
137
|
+
}
|
|
138
|
+
function matches(selector, tool) {
|
|
139
|
+
const selected = selectorIdentity(selector);
|
|
140
|
+
return selected?.kind === tool.kind && (selected.kind !== "mcp" || tool.kind === "mcp" && selected.server === tool.server) && (selected.name === "*" || selected.name === tool.name);
|
|
141
|
+
}
|
|
142
|
+
function permits(scope, tool) {
|
|
143
|
+
if (!validName(tool.name) || tool.kind === "mcp" && !SERVER_NAME.test(tool.server)) return false;
|
|
144
|
+
return (scope?.allow === void 0 || scope.allow.some((selector) => matches(selector, tool))) && !scope?.deny?.some((selector) => matches(selector, tool));
|
|
145
|
+
}
|
|
146
|
+
function allowsTool(scope, tool) {
|
|
147
|
+
const parsed = parseToolScope(scope);
|
|
148
|
+
return parsed.ok && permits(parsed.scope, tool);
|
|
149
|
+
}
|
|
150
|
+
|
|
97
151
|
// server/permission-proxy.ts
|
|
152
|
+
var selection = parseToolScope(process.env.OMB_PERMISSION_TOOL_SCOPE === void 0 ? void 0 : (() => {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(process.env.OMB_PERMISSION_TOOL_SCOPE);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
})());
|
|
159
|
+
if (!selection.ok) throw new Error(selection.error);
|
|
160
|
+
var allowQuestion = allowsTool(selection.scope, { kind: "mcp", server: "ogb", name: "ask_user" });
|
|
98
161
|
var socketPath = process.argv[2] ?? "";
|
|
99
162
|
var waiting = /* @__PURE__ */ new Map();
|
|
100
163
|
var conn = connect(socketPath);
|
|
@@ -203,11 +266,12 @@ async function handle(msg) {
|
|
|
203
266
|
}
|
|
204
267
|
});
|
|
205
268
|
}
|
|
206
|
-
if (msg.method === "tools/list") return send({ jsonrpc: "2.0", id: msg.id, result: { tools: TOOLS } });
|
|
269
|
+
if (msg.method === "tools/list") return send({ jsonrpc: "2.0", id: msg.id, result: { tools: TOOLS.filter((tool) => tool.name === "approve" || allowQuestion) } });
|
|
207
270
|
if (msg.method === "tools/call") {
|
|
208
271
|
const name = msg.params?.name;
|
|
209
272
|
const args = msg.params?.arguments ?? {};
|
|
210
273
|
const reply = (text3) => send({ jsonrpc: "2.0", id: msg.id, result: { content: [{ type: "text", text: text3 }] } });
|
|
274
|
+
if (name === "ask_user" && !allowQuestion) return reply("OpenMausBot: this question tool is excluded by the bot's tool selection.");
|
|
211
275
|
if (name === "approve" && args.tool_name === ASK_USER_QUESTION) {
|
|
212
276
|
return reply(await answerNativeQuestions(args.input));
|
|
213
277
|
}
|
|
@@ -17,6 +17,7 @@ var SPAWNED_PROXIES = {
|
|
|
17
17
|
dweb: resolveProxy("drivers/dweb-proxy"),
|
|
18
18
|
connectors: resolveProxy("connector-proxy"),
|
|
19
19
|
mcpGate: resolveProxy("mcp-gate"),
|
|
20
|
+
mcpRemote: resolveProxy("mcp-remote-proxy"),
|
|
20
21
|
phone: resolveProxy("drivers/phone-proxy"),
|
|
21
22
|
hook: resolveProxy("hooks/omb-hook"),
|
|
22
23
|
// Loaded by the external `pi` process via `-e`, not by this server — but
|
|
@@ -25,7 +25,7 @@ export async function deliverFullAccessApproval(adapter, threadId, requestId, tu
|
|
|
25
25
|
}
|
|
26
26
|
/** Full access is the person's explicit grant to this receiving bot, including
|
|
27
27
|
* delegated work. It never inherits the sender's mode or elevates another bot
|
|
28
|
-
* — with the one exception below (
|
|
28
|
+
* — with the one exception below (delegatedApprovalMode), applied
|
|
29
29
|
* where a Chief's delegated thread is created rather than here.
|
|
30
30
|
* Custom is a provider-config choice rather than an app Full-access grant, so
|
|
31
31
|
* peer-started Custom turns use Auto. Provider support and grant confirmation
|
|
@@ -35,16 +35,38 @@ export function approvalModeForOrigin(mode, origin) {
|
|
|
35
35
|
return "auto";
|
|
36
36
|
return mode;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
/** Levels by how much runs without asking. Custom is a provider config of
|
|
39
|
+
* its own and is never compared. */
|
|
40
|
+
const LEVEL_RANK = { ask: 0, edits: 1, auto: 2, full: 3 };
|
|
41
|
+
/** The level work a bot hands to a teammate starts at, or null to keep the
|
|
42
|
+
* teammate's own. A Chief of Staff's level flows down: the person set it on
|
|
43
|
+
* the Chief so the team's work runs that way, and switching every thread the
|
|
44
|
+
* Chief opens with a teammate back off "Ask for approval" was the friction
|
|
45
|
+
* people reported. Only a Chief passes its level on, only the level of the
|
|
46
|
+
* conversation it delegates from, and only upward: a teammate already on a
|
|
47
|
+
* higher level keeps it, and one on Custom keeps its own config. The level
|
|
48
|
+
* is capped at what the teammate's engine implements (Full or Auto-accept
|
|
49
|
+
* edits are not everywhere), stepping down to the next level it has. A bot
|
|
50
|
+
* never raises itself this way. */
|
|
51
|
+
export function delegatedApprovalMode(input) {
|
|
52
|
+
if (!input.senderIsChief || input.sameBot)
|
|
53
|
+
return null;
|
|
54
|
+
// A Chief's Custom config is its own; for a teammate it reads as Approve
|
|
55
|
+
// for me, as for any peer-started Custom turn (approvalModeForOrigin).
|
|
56
|
+
const sender = LEVEL_RANK[approvalModeForOrigin(input.senderMode, { peerInitiated: true })] ?? 0;
|
|
57
|
+
const own = LEVEL_RANK[input.recipientMode];
|
|
58
|
+
if (own === undefined)
|
|
59
|
+
return null;
|
|
60
|
+
for (const mode of ["full", "auto", "edits"]) {
|
|
61
|
+
const rank = LEVEL_RANK[mode];
|
|
62
|
+
if (rank > sender)
|
|
63
|
+
continue;
|
|
64
|
+
if (rank <= own)
|
|
65
|
+
return null;
|
|
66
|
+
if (supportsApprovalMode(input.recipientDriverKind, mode))
|
|
67
|
+
return mode;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
48
70
|
}
|
|
49
71
|
// Tools that ask a PERSON something. A question exists so that a human
|
|
50
72
|
// decides; any mode answering one on their behalf defeats the only reason
|
|
@@ -10,6 +10,9 @@ const HEARTBEAT_MS = 10_000;
|
|
|
10
10
|
// The native press resolver supplies the virtual key codes and Enter/Tab text
|
|
11
11
|
// that its raw input_keyboard relay omits. Keep unknown keys literal.
|
|
12
12
|
const DISCRETE_KEYS = new Set(["Backspace", "Enter", "Tab", "Escape", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
13
|
+
// The panel has no take-control button: interacting takes control, and an
|
|
14
|
+
// interrupted action is recovered from the panel's menu.
|
|
15
|
+
const RESTART_NEEDED = "A browser action was interrupted. Choose Restart browser… in the browser menu before continuing.";
|
|
13
16
|
export class BrowserLiveError extends Error {
|
|
14
17
|
status;
|
|
15
18
|
constructor(message, status = 400) { super(message); this.name = "BrowserLiveError"; this.status = status; }
|
|
@@ -569,22 +572,24 @@ export class BrowserLive {
|
|
|
569
572
|
try {
|
|
570
573
|
const taking = this.runtime.take(viewer.session, viewer.id);
|
|
571
574
|
this.control(viewer.session);
|
|
572
|
-
|
|
575
|
+
// waited: the bot's own action finished first, so input the person
|
|
576
|
+
// aimed at the page before the grant may no longer fit it.
|
|
577
|
+
const waited = await taking;
|
|
573
578
|
if (!this.current(viewer)) {
|
|
574
579
|
this.close(viewer);
|
|
575
580
|
throw new BrowserLiveError("This browser view closed.", 409);
|
|
576
581
|
}
|
|
577
|
-
return { ok: true };
|
|
582
|
+
return { ok: true, waited: waited === true };
|
|
578
583
|
}
|
|
579
584
|
catch {
|
|
580
|
-
throw
|
|
585
|
+
throw this.refusal(viewer, "Another browser view or bot action is using this browser. Try again shortly.");
|
|
581
586
|
}
|
|
582
587
|
finally {
|
|
583
588
|
this.control(viewer.session);
|
|
584
589
|
}
|
|
585
590
|
}
|
|
586
591
|
if (!this.runtime.canControl(viewer.session, viewer.id))
|
|
587
|
-
throw
|
|
592
|
+
throw this.refusal(viewer, "Browser control changed. Try again.");
|
|
588
593
|
if (viewer.pendingActions >= 32)
|
|
589
594
|
throw new BrowserLiveError("Too many browser actions are pending. Try again shortly.", 429);
|
|
590
595
|
viewer.pendingActions += 1;
|
|
@@ -600,13 +605,17 @@ export class BrowserLive {
|
|
|
600
605
|
return { ok: true };
|
|
601
606
|
}
|
|
602
607
|
catch (error) {
|
|
603
|
-
throw error instanceof BrowserLiveError ? error :
|
|
608
|
+
throw error instanceof BrowserLiveError ? error : this.refusal(viewer, "Browser control changed. Try again.");
|
|
604
609
|
}
|
|
605
610
|
finally {
|
|
606
611
|
viewer.pendingActions -= 1;
|
|
607
612
|
this.control(viewer.session);
|
|
608
613
|
}
|
|
609
614
|
}
|
|
615
|
+
/** Why this viewer cannot act now: an interrupted browser needs a restart. */
|
|
616
|
+
refusal(viewer, otherwise) {
|
|
617
|
+
return new BrowserLiveError(this.runtime.interrupted(viewer.session) ? RESTART_NEEDED : otherwise, 409);
|
|
618
|
+
}
|
|
610
619
|
closeForSession(session) { for (const viewer of this.viewers.values())
|
|
611
620
|
if (viewer.session === session)
|
|
612
621
|
this.close(viewer); }
|
|
@@ -364,6 +364,8 @@ export class BrowserRuntime {
|
|
|
364
364
|
};
|
|
365
365
|
return method === "tools/call" ? this.withAgentAction(session, invoke) : invoke();
|
|
366
366
|
}
|
|
367
|
+
/** Resolves true when the grant had to wait for the bot's own browser
|
|
368
|
+
* action to finish: the page may have changed since the person aimed. */
|
|
367
369
|
async take(session, owner) {
|
|
368
370
|
if (!owner)
|
|
369
371
|
throw new Error("Browser control requires an owner.");
|
|
@@ -374,6 +376,7 @@ export class BrowserRuntime {
|
|
|
374
376
|
throw new Error("Another person controls this browser.");
|
|
375
377
|
gate.owner = owner; // synchronous: no new agent work slips in while draining.
|
|
376
378
|
gate.ready = false;
|
|
379
|
+
const waited = gate.agents > 0;
|
|
377
380
|
await new Promise((resolve, reject) => {
|
|
378
381
|
const finish = (error) => {
|
|
379
382
|
clearTimeout(timer);
|
|
@@ -398,12 +401,15 @@ export class BrowserRuntime {
|
|
|
398
401
|
gate.changed.add(check);
|
|
399
402
|
check();
|
|
400
403
|
});
|
|
404
|
+
return waited;
|
|
401
405
|
}
|
|
402
406
|
canControl(session, owner) {
|
|
403
407
|
const gate = this.gates.get(session);
|
|
404
408
|
return Boolean(owner && gate?.owner === owner && gate.ready && !gate.releasing && !gate.closing && !gate.uncertain && gate.agents === 0);
|
|
405
409
|
}
|
|
406
410
|
heldBy(session) { return this.gates.get(session)?.owner ?? null; }
|
|
411
|
+
/** An interrupted action left the browser's state unknown; only a restart clears it. */
|
|
412
|
+
interrupted(session) { return this.gates.get(session)?.uncertain === true; }
|
|
407
413
|
/** A disconnected viewer may leave a physical key/button pressed. Never
|
|
408
414
|
* let an agent inherit that input state; explicit restart clears it. */
|
|
409
415
|
abandonHumanInput(session, owner) {
|
|
@@ -17,8 +17,11 @@ export function restoreChannelMessages() {
|
|
|
17
17
|
const entry = queues.get(row.threadId) ?? { groupId: row.ownerId, items: [] };
|
|
18
18
|
if (entry.groupId !== row.ownerId)
|
|
19
19
|
throw new Error("queued task belongs to another channel");
|
|
20
|
+
// a channel line is only ever stamped "api"; "call" belongs to 1:1 rows
|
|
21
|
+
const { via, ...payload } = row.payload;
|
|
20
22
|
entry.items.push({
|
|
21
|
-
...
|
|
23
|
+
...payload,
|
|
24
|
+
...(via === "api" ? { via } : {}),
|
|
22
25
|
id: row.id,
|
|
23
26
|
mode: row.payload.mode ?? "chat",
|
|
24
27
|
// rows queued before timestamps were kept read as queued at restore
|
|
@@ -812,7 +812,7 @@ export async function runBrowser(options, io = defaultIo()) {
|
|
|
812
812
|
io.error("on Linux, install Chrome's system libraries with `sudo openmausbot browser install --with-deps`, then retry `openmausbot browser install` as the user running serve");
|
|
813
813
|
return 1;
|
|
814
814
|
}
|
|
815
|
-
io.log("browser installed for this user and data directory; run serve as the same user, then enable it under Settings →
|
|
815
|
+
io.log("browser installed for this user and data directory; run serve as the same user, then enable it under Settings → Computers and per bot");
|
|
816
816
|
if (process.platform === "linux" && options.withDeps)
|
|
817
817
|
io.log("if serve runs as another user, run `openmausbot browser install` from that user's login shell too");
|
|
818
818
|
return 0;
|
|
@@ -834,8 +834,13 @@ export function serverEntry(here = HERE) {
|
|
|
834
834
|
}
|
|
835
835
|
/** Everything `--tunnel` needs before the server starts, or the one reason
|
|
836
836
|
* it cannot have it. Fails closed: no silent fallback to a local-only server. */
|
|
837
|
-
async function planTunnel(options, log) {
|
|
837
|
+
async function planTunnel(options, log, recovery) {
|
|
838
838
|
let access = null;
|
|
839
|
+
let account = null;
|
|
840
|
+
const fail = (error) => {
|
|
841
|
+
account?.service.dispose();
|
|
842
|
+
return { error };
|
|
843
|
+
};
|
|
839
844
|
const credential = fleetCredential();
|
|
840
845
|
if (credential) {
|
|
841
846
|
// A fleet-started container: the credential is the whole identity.
|
|
@@ -848,36 +853,41 @@ async function planTunnel(options, log) {
|
|
|
848
853
|
}
|
|
849
854
|
}
|
|
850
855
|
else {
|
|
851
|
-
|
|
856
|
+
account = createTunnelAccount({ dataDir: options.dataDir, version: serverVersion(), recovery });
|
|
852
857
|
if (account.credentials.status === "unavailable")
|
|
853
|
-
return
|
|
858
|
+
return fail(`${account.credentials.file} exists but could not be read; fix or remove it`);
|
|
854
859
|
if (!describeTunnelAccount(account.credentials.read()).email) {
|
|
855
|
-
return
|
|
860
|
+
return fail("no account on this machine yet: run `openmausbot login` first, then `openmausbot serve --tunnel`");
|
|
856
861
|
}
|
|
857
862
|
// A fresh connector token when the control plane answers; the saved one otherwise.
|
|
858
863
|
try {
|
|
859
864
|
const state = await account.service.retry();
|
|
860
|
-
|
|
861
|
-
|
|
865
|
+
const saved = tunnelAccess(account.credentials.read()) !== null;
|
|
866
|
+
// Said even when a saved address exists: an expired sign-in means this
|
|
867
|
+
// machine can no longer repair that address on its own.
|
|
868
|
+
if (state.status === "signed-out")
|
|
869
|
+
log(`tunnel: the sign-in on this machine expired; run \`openmausbot login\` to renew it${saved ? " (serving the saved address until then)" : ""}`);
|
|
870
|
+
else if (state.message)
|
|
871
|
+
log(`tunnel: ${state.message}${saved ? " Serving the saved address for now." : ""}`);
|
|
862
872
|
}
|
|
863
873
|
catch (error) {
|
|
864
874
|
log(`tunnel: control plane not reachable right now (${message(error)}); using the saved address`);
|
|
865
875
|
}
|
|
866
876
|
access = tunnelAccess(account.credentials.read());
|
|
867
877
|
if (!access)
|
|
868
|
-
return
|
|
878
|
+
return fail("this machine has no public address; run `openmausbot login` again");
|
|
869
879
|
}
|
|
870
880
|
let binary;
|
|
871
881
|
try {
|
|
872
882
|
binary = await ensureCloudflared({ dataDir: options.dataDir, log });
|
|
873
883
|
}
|
|
874
884
|
catch (error) {
|
|
875
|
-
return
|
|
885
|
+
return fail(`--tunnel: ${message(error)}`);
|
|
876
886
|
}
|
|
877
887
|
const guardian = guardianEntry();
|
|
878
888
|
if (!guardian)
|
|
879
|
-
return
|
|
880
|
-
return { access, binary, guardian, origin: createTunnelOrigin() };
|
|
889
|
+
return fail("--tunnel: the connector guardian is missing from this install");
|
|
890
|
+
return { access, binary, guardian, origin: createTunnelOrigin(), account };
|
|
881
891
|
}
|
|
882
892
|
export async function runServe(options, log = console.log) {
|
|
883
893
|
const { browserEngineStatus, describeBrowserEngine } = await import("./browser-engine.js");
|
|
@@ -896,8 +906,11 @@ export async function runServe(options, log = console.log) {
|
|
|
896
906
|
tailscale = probe.status;
|
|
897
907
|
}
|
|
898
908
|
let plan = null;
|
|
909
|
+
// The tunnel this process serves, once it runs; the account service's
|
|
910
|
+
// background recovery reads it (see TunnelRecovery).
|
|
911
|
+
const serving = { tunnel: null };
|
|
899
912
|
if (options.tunnel) {
|
|
900
|
-
const planned = await planTunnel(options, log);
|
|
913
|
+
const planned = await planTunnel(options, log, { running: () => serving.tunnel });
|
|
901
914
|
if ("error" in planned) {
|
|
902
915
|
console.error(planned.error);
|
|
903
916
|
return 1;
|
|
@@ -991,6 +1004,7 @@ export async function runServe(options, log = console.log) {
|
|
|
991
1004
|
catch (error) {
|
|
992
1005
|
if ((tailscaleServing || (startupCancelled && tailscaleAttempted)) && tailscale)
|
|
993
1006
|
await tailscaleServeOff(tailscale).catch(() => undefined);
|
|
1007
|
+
plan?.account?.service.dispose();
|
|
994
1008
|
if (plan)
|
|
995
1009
|
cleanupTunnelOrigin(plan.origin);
|
|
996
1010
|
if (error instanceof SetupCancelled) {
|
|
@@ -1015,6 +1029,8 @@ export async function runServe(options, log = console.log) {
|
|
|
1015
1029
|
let stopping = null;
|
|
1016
1030
|
const stop = () => {
|
|
1017
1031
|
stopping ??= (async () => {
|
|
1032
|
+
serving.tunnel = null;
|
|
1033
|
+
plan?.account?.service.dispose();
|
|
1018
1034
|
// The gateway and the edge stop accepting before the server they forward to goes away.
|
|
1019
1035
|
if (tunnel)
|
|
1020
1036
|
await tunnel.stop().catch(() => undefined);
|
|
@@ -1083,6 +1099,10 @@ export async function runServe(options, log = console.log) {
|
|
|
1083
1099
|
onState: (state) => log(describeTunnelState(state, plan.access.endpoint)),
|
|
1084
1100
|
});
|
|
1085
1101
|
tunnel.started.catch((error) => log(`tunnel: ${message(error)}`));
|
|
1102
|
+
// From here on the endpoint is checked every 15 minutes; one reclaimed
|
|
1103
|
+
// while this machine was offline is re-created behind the same address.
|
|
1104
|
+
serving.tunnel = tunnel;
|
|
1105
|
+
void plan.account?.service.restore().catch(() => undefined);
|
|
1086
1106
|
}
|
|
1087
1107
|
log("");
|
|
1088
1108
|
log(`OpenMausBot is running on http://127.0.0.1:${options.port}${publicUrl ? `, reachable at ${publicUrl}` : ""}`);
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { backupsBytes, beginUpload, CLOUD_MOVE_MAX_BYTES, CLOUD_MOVE_MAX_PART_BYTES, CLOUD_MOVE_PART_BYTES, completedUpload, discardNextPreviousCloud, discardUpload, forgetMoveRestore, freeVolumeBytes, isEmptyWorkspace, moveSpaceNeeded, noteMoveRestore, prepareNextPreviousCloud, previousCloud, previousCloudArchiveBytes, removeMoveFiles, stagePreviousCloud, tidyCloudMoveStorage, uploadStatus, validUploadDeclaration, workspaceContents, workspaceMoveSize, writeUploadPart, } from "./cloud-move.js";
|
|
2
|
+
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
3
|
import { commitPendingWorkspaceRestore, createWorkspaceBackup, stageWorkspaceBackup } from "./workspace-backup.js";
|
|
4
4
|
export const CLOUD_MOVE_PREFIX = "/api/cloud-move";
|
|
5
5
|
const failure = (message, status) => Object.assign(new Error(message), { status });
|
|
6
6
|
const HELD_CACHE_MS = 30_000;
|
|
7
7
|
export function createCloudMoveRoutes(options) {
|
|
8
8
|
const freeBytes = options.freeBytes ?? freeVolumeBytes;
|
|
9
|
+
const volumeBytes = (path) => { try {
|
|
10
|
+
return (options.volumeBytes ?? totalVolumeBytes)(path);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
} };
|
|
9
15
|
// A move's restore that startup has installed keeps no safety copy or
|
|
10
16
|
// staged files; an abandoned upload goes after a day.
|
|
11
17
|
if (options.cloudHome) {
|
|
@@ -113,7 +119,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
113
119
|
if (!auth.scopes.includes("admin"))
|
|
114
120
|
throw failure("Only the owner can move a workspace.", 403);
|
|
115
121
|
if (method === "GET" && path === `${CLOUD_MOVE_PREFIX}/estimate`) {
|
|
116
|
-
json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES });
|
|
122
|
+
json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), routines: enabledRoutineCount(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES });
|
|
117
123
|
return true;
|
|
118
124
|
}
|
|
119
125
|
if (!options.cloudHome)
|
|
@@ -126,7 +132,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
126
132
|
if (!held || held.at + HELD_CACHE_MS < Date.now())
|
|
127
133
|
held = { at: Date.now(), bytes: backupsBytes(options.dataDir) };
|
|
128
134
|
json(res, 200, {
|
|
129
|
-
contents, empty: isEmptyWorkspace(contents), freeBytes: freeBytes(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES,
|
|
135
|
+
contents, empty: isEmptyWorkspace(contents), freeBytes: freeBytes(options.dataDir), volumeBytes: volumeBytes(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES,
|
|
130
136
|
partBytes: CLOUD_MOVE_PART_BYTES, upload: uploadStatus(options.dataDir), previous: previousCloud(options.dataDir), job,
|
|
131
137
|
// What backups and the previous Cloud hold on the volume.
|
|
132
138
|
heldBytes: held.bytes,
|
|
@@ -96,6 +96,24 @@ export function freeVolumeBytes(path) {
|
|
|
96
96
|
const free = disk.bavail * disk.bsize;
|
|
97
97
|
return free > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(free);
|
|
98
98
|
}
|
|
99
|
+
/** The whole volume, so the app can tell how far a disk that grows may still grow. */
|
|
100
|
+
export function totalVolumeBytes(path) {
|
|
101
|
+
const disk = statfsSync(path, { bigint: true });
|
|
102
|
+
const total = disk.blocks * disk.bsize;
|
|
103
|
+
return total > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(total);
|
|
104
|
+
}
|
|
105
|
+
/** Routines switched on in this workspace. A move brings them paused, so the
|
|
106
|
+
* app says how many to turn on there. */
|
|
107
|
+
export function enabledRoutineCount(dataDir) {
|
|
108
|
+
try {
|
|
109
|
+
const value = JSON.parse(readFileSync(join(dataDir, "routines.json"), "utf8"));
|
|
110
|
+
const routines = value && typeof value === "object" ? value.routines : undefined;
|
|
111
|
+
return Array.isArray(routines) ? routines.filter((routine) => routine && typeof routine === "object" && routine.enabled !== false).length : 0;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
99
117
|
/** Peak extra space a move of `upload` bytes needs: the upload, its decrypted
|
|
100
118
|
* copy and its staged files exist together while it is checked; the restore
|
|
101
119
|
* then installs from the staged copy. Replacing a workspace first backs it up
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The chip used to read "Waiting for computer — X / Y is using it; will
|
|
4
4
|
// continue automatically", which people read as an error about a machine
|
|
5
|
-
// they could not see. It is a queue position: this turn
|
|
6
|
-
// turn on one desktop
|
|
7
|
-
// that, name the holder as a bot running a
|
|
8
|
-
// on every surface (chat chip, room chip,
|
|
9
|
-
// who has read it once recognises it everywhere.
|
|
5
|
+
// they could not see. It is a queue position: this turn sits behind a named
|
|
6
|
+
// turn on one desktop — and every turn queued ahead of it — and starts on
|
|
7
|
+
// its own when they finish. Say that, name the holder as a bot running a
|
|
8
|
+
// thread, and keep the same words on every surface (chat chip, room chip,
|
|
9
|
+
// the gate's refusal) so a person who has read it once recognises it everywhere.
|
|
10
10
|
/** Whole minutes, or seconds under one minute, singular at exactly one. */
|
|
11
11
|
const minutes = (ms) => {
|
|
12
12
|
const value = ms >= 60_000 ? Math.round(ms / 60_000) : Math.round(ms / 1000);
|
|
@@ -19,11 +19,30 @@ export function computerWaitDuration(ms) {
|
|
|
19
19
|
return ms < 1_000 ? "under a second" : minutes(ms);
|
|
20
20
|
}
|
|
21
21
|
const holderPhrase = (holder) => holder.task ? `${holder.name} is running ${holder.task}` : `${holder.name} is using it`;
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const ordinal = (position) => {
|
|
23
|
+
const mod100 = position % 100;
|
|
24
|
+
const mod10 = position % 10;
|
|
25
|
+
const suffix = mod100 >= 11 && mod100 <= 13 ? "th" : mod10 === 1 ? "st" : mod10 === 2 ? "nd" : mod10 === 3 ? "rd" : "th";
|
|
26
|
+
return `${position}${suffix}`;
|
|
27
|
+
};
|
|
28
|
+
/** The chip a turn shows while it is queued behind another turn's desktop:
|
|
29
|
+
* its stable queue position, the holder, and — only once waits here have
|
|
30
|
+
* history — about how long they have been taking. */
|
|
31
|
+
export function computerWaitingText(holder, queue) {
|
|
32
|
+
const position = queue?.position && queue.position > 0 ? queue.position : undefined;
|
|
33
|
+
const place = position ? ` — ${ordinal(position)} in queue` : "";
|
|
34
|
+
const estimate = queue?.estimateMs !== undefined ? `; recent waits here have taken ${computerWaitDuration(queue.estimateMs)}` : "";
|
|
35
|
+
// Behind more than the holder, that turn ending is not this turn's start:
|
|
36
|
+
// the released seat goes to the front waiter first, so this turn starts
|
|
37
|
+
// only once every turn ahead of it has finished (#1652).
|
|
38
|
+
const starts = position && position > 1
|
|
39
|
+
? "Starts automatically when the turns ahead finish"
|
|
40
|
+
: holder
|
|
41
|
+
? "Starts automatically when that finishes"
|
|
42
|
+
: "Starts automatically when it is free";
|
|
24
43
|
if (!holder)
|
|
25
|
-
return
|
|
26
|
-
return `Waiting for its turn on this computer — ${holderPhrase(holder)}.
|
|
44
|
+
return `Waiting for its turn on this computer${place}. ${starts}${estimate}.`;
|
|
45
|
+
return `Waiting for its turn on this computer${place} — ${holderPhrase(holder)}. ${starts}${estimate}.`;
|
|
27
46
|
}
|
|
28
47
|
/** The resolution appended once the wait landed and this turn holds the
|
|
29
48
|
* desktop: how long it waited, and who held it. The waiting chip stays as
|
|
@@ -38,9 +57,10 @@ export function computerStoppedWaitingText(holder, waitedMs) {
|
|
|
38
57
|
const who = holder ? ` — ${holderPhrase(holder)}` : "";
|
|
39
58
|
return `Stopped waiting for the computer after ${computerWaitDuration(waitedMs)}${who}.`;
|
|
40
59
|
}
|
|
41
|
-
/** The
|
|
42
|
-
*
|
|
43
|
-
|
|
60
|
+
/** The resolution appended when the wait ceiling parks the turn (#1651):
|
|
61
|
+
* still names who holds it, but the work is not lost — the turn settled and
|
|
62
|
+
* resumes on its own when the desktop frees, so nobody has to fix it. */
|
|
63
|
+
export function computerParkedText(holder, ceilingMs) {
|
|
44
64
|
const who = holder ? ` — ${holderPhrase(holder).replace(" is running ", " is still running ").replace(" is using it", " is still using it")}` : "";
|
|
45
|
-
return `Computer
|
|
65
|
+
return `Computer still busy after ${minutes(ceilingMs)}${who}. Parked — it continues automatically when the computer is free.`;
|
|
46
66
|
}
|
|
@@ -14,7 +14,6 @@ import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-ico
|
|
|
14
14
|
import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
15
15
|
import { parseJson, schemaIssue } from "./schema.js";
|
|
16
16
|
import { CLOUD_SEAT_IDLE_STOP_MS } from "./cloud-overflow.js";
|
|
17
|
-
import { cloudHomeConfigured } from "./cloud-home.js";
|
|
18
17
|
const optionalText = z.string().optional();
|
|
19
18
|
const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
20
19
|
const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -304,6 +303,10 @@ const featureConfigSchema = z.object({
|
|
|
304
303
|
* consent card or a configured allowlist — never a silent Auto default.
|
|
305
304
|
* Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
|
|
306
305
|
cloudOverflow: z.boolean().optional(),
|
|
306
|
+
/** Opt-in shared skills library: one store at the data dir that bots
|
|
307
|
+
* read by assignment instead of per-workspace copies. Off until
|
|
308
|
+
* explicitly enabled — see skillsLibraryEnabled. */
|
|
309
|
+
skillsLibrary: z.boolean().optional(),
|
|
307
310
|
});
|
|
308
311
|
/** First-run progress. Kept in the workspace config rather than a browser so
|
|
309
312
|
* it survives cleared site data and is shared by every paired client. Hint
|
|
@@ -474,6 +477,15 @@ const appConfigSchema = z.object({
|
|
|
474
477
|
.optional(),
|
|
475
478
|
jobs: z.object({ roomRouting: z.boolean().optional() }).optional(),
|
|
476
479
|
}).optional(),
|
|
480
|
+
/** Live calls: an OpenAI project key for GPT-Live, kept apart from every
|
|
481
|
+
* other OpenAI credential so a Live call never bills an image or engine key
|
|
482
|
+
* the user did not hand to it. `voice` is a GPT-Live built-in voice name. */
|
|
483
|
+
live: z.object({
|
|
484
|
+
key: optionalText,
|
|
485
|
+
voice: z.string().trim().max(40).regex(/^[a-z]*$/, "a Live voice is a lowercase built-in voice name").optional(),
|
|
486
|
+
readTypedReplies: z.boolean().optional(),
|
|
487
|
+
idleMinutes: z.number().int().min(1).max(60).optional(),
|
|
488
|
+
}).optional(),
|
|
477
489
|
/** Avatar provider credentials stay separate; choosing a router never reuses a cloud key. */
|
|
478
490
|
imageGen: z.object({
|
|
479
491
|
provider: z.enum(["openai", "xai", "custom"]).optional(),
|
|
@@ -630,6 +642,17 @@ export function browserEngineAttachCdpUrl(cfg) {
|
|
|
630
642
|
export function roomTurnTimeoutMinutes(cfg) {
|
|
631
643
|
return cfg.rooms?.turnTimeoutMinutes ?? DEFAULT_ROOM_TURN_TIMEOUT_MINUTES;
|
|
632
644
|
}
|
|
645
|
+
export const LIVE_IDLE_MINUTES_DEFAULT = 5;
|
|
646
|
+
/** Non-secret Live settings. The key only shows up as `configured`. */
|
|
647
|
+
export function liveSettingsFor(cfg) {
|
|
648
|
+
const minutes = cfg.live?.idleMinutes;
|
|
649
|
+
return {
|
|
650
|
+
configured: Boolean(cfg.live?.key?.trim()),
|
|
651
|
+
voice: cfg.live?.voice ?? "",
|
|
652
|
+
readTypedReplies: cfg.live?.readTypedReplies ?? true,
|
|
653
|
+
idleMinutes: Number.isInteger(minutes) && minutes >= 1 && minutes <= 60 ? minutes : LIVE_IDLE_MINUTES_DEFAULT,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
633
656
|
/** Room handoff tree budgets in milliseconds. The tree lifetime pauses
|
|
634
657
|
* while a node is actively executing; the hard cap is the maximum stall
|
|
635
658
|
* window, measured wall-clock from the tree's last durable progress. Read
|
|
@@ -692,14 +715,11 @@ export function showToolCallsEnabled(cfg) {
|
|
|
692
715
|
export function routinesInConversationEnabled(cfg) {
|
|
693
716
|
return cfg.features?.routinesInConversation === true;
|
|
694
717
|
}
|
|
695
|
-
/** Workspace-level gate for the
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
export function builtInBrowserEnabled(cfg, env = process.env) {
|
|
701
|
-
const chosen = cfg.features?.browser;
|
|
702
|
-
return chosen === undefined ? cloudHomeConfigured(env) : chosen === true;
|
|
718
|
+
/** Workspace-level gate for the built-in browser. A bot's own switch sits
|
|
719
|
+
* under it, so either can withhold the browser. On unless the person switched
|
|
720
|
+
* it off: an explicit `false` (Settings, or a bot's computer panel) is kept. */
|
|
721
|
+
export function builtInBrowserEnabled(cfg, _env = process.env) {
|
|
722
|
+
return cfg.features?.browser !== false;
|
|
703
723
|
}
|
|
704
724
|
/** Opt-in computer sharing: the routes, the agent tools, the advertised
|
|
705
725
|
* capability and the desktop connector. Off unless an explicit `true` turns
|
|
@@ -766,6 +786,14 @@ export function cloudOverflowIdleStopMs(cfg) {
|
|
|
766
786
|
export function cloudOverflowAllowlistedThreads(cfg) {
|
|
767
787
|
return new Set(cfg.cloudOverflow?.allowlistedThreads ?? []);
|
|
768
788
|
}
|
|
789
|
+
/** Opt-in shared skills library (skills lane S1): one store at the data dir
|
|
790
|
+
* that bots reference by assignment instead of per-workspace copies. Off
|
|
791
|
+
* unless enabled by hand in ~/.openmausbot/config.json
|
|
792
|
+
* (`{"features": {"skillsLibrary": true}}`); while off, every skills
|
|
793
|
+
* surface keeps today's byte-identical per-bot behavior. */
|
|
794
|
+
export function skillsLibraryEnabled(cfg) {
|
|
795
|
+
return cfg.features?.skillsLibrary === true;
|
|
796
|
+
}
|
|
769
797
|
/** Config sections no provider driver reads. A write that touches only
|
|
770
798
|
* these must not rebuild the fleet: rebuilding disposes every engine child
|
|
771
799
|
* and reloads it, seconds of work that would also interrupt in-flight
|
|
@@ -778,6 +806,7 @@ export const FLEET_NEUTRAL_KEYS = new Set([
|
|
|
778
806
|
// no engine reads it: the harness asks it before a turn starts
|
|
779
807
|
"decider",
|
|
780
808
|
"imageGen",
|
|
809
|
+
"live",
|
|
781
810
|
"vps",
|
|
782
811
|
"rooms",
|
|
783
812
|
"threads",
|
|
@@ -937,6 +966,9 @@ export function loadConfig() {
|
|
|
937
966
|
cfg.decider = { ...cfg.decider };
|
|
938
967
|
if (process.env.OMB_JEV_API_KEY !== undefined)
|
|
939
968
|
cfg.decider.key = process.env.OMB_JEV_API_KEY;
|
|
969
|
+
cfg.live = { ...cfg.live };
|
|
970
|
+
if (process.env.OMB_OPENAI_LIVE_KEY !== undefined)
|
|
971
|
+
cfg.live.key = process.env.OMB_OPENAI_LIVE_KEY;
|
|
940
972
|
cfg.imageGen = { ...cfg.imageGen };
|
|
941
973
|
if (process.env.OMB_OPENAI_IMAGE_KEY !== undefined)
|
|
942
974
|
cfg.imageGen.key = process.env.OMB_OPENAI_IMAGE_KEY;
|
|
@@ -978,6 +1010,7 @@ export function syncCredentialEnv(patch) {
|
|
|
978
1010
|
[patch.decider?.key, "OMB_JEV_API_KEY"],
|
|
979
1011
|
[patch.imageGen?.key, "OMB_OPENAI_IMAGE_KEY"],
|
|
980
1012
|
[patch.imageGen?.customApiKey, "OMB_CUSTOM_IMAGE_KEY"],
|
|
1013
|
+
[patch.live?.key, "OMB_OPENAI_LIVE_KEY"],
|
|
981
1014
|
];
|
|
982
1015
|
for (const [value, name] of secrets) {
|
|
983
1016
|
if (value === undefined)
|
|
@@ -1028,6 +1061,7 @@ export const WORKSPACE_CREDENTIAL_ENV = [
|
|
|
1028
1061
|
"OMB_JEV_API_KEY",
|
|
1029
1062
|
"OMB_OPENAI_IMAGE_KEY",
|
|
1030
1063
|
"OMB_CUSTOM_IMAGE_KEY",
|
|
1064
|
+
"OMB_OPENAI_LIVE_KEY",
|
|
1031
1065
|
"COMPOSIO_API_KEY",
|
|
1032
1066
|
"OMB_COMPOSIO_BROKER_TOKEN",
|
|
1033
1067
|
// Cloud Pro's included Boat, voice and decision relay tokens
|
|
@@ -1118,7 +1152,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1118
1152
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1119
1153
|
if (storedProfiles.success)
|
|
1120
1154
|
disk.browserProfiles = storedProfiles.data;
|
|
1121
|
-
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1155
|
+
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1122
1156
|
const section = checkedPatch[key];
|
|
1123
1157
|
if (!section)
|
|
1124
1158
|
continue;
|