openmausbot 0.1.95 → 0.1.97
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-BwWpc4RK.js → DesktopViewer-CPidMCtt.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
- package/dist/assets/{arc-7kei4Hsu.js → arc-BUWGa9UU.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
- package/dist/assets/channel-DR8eD6QW.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
- package/dist/assets/{index-KygHRE3E.js → index-Cp4S4NOL.js} +1 -1
- package/dist/assets/index-CpaIXRLp.js +614 -0
- package/dist/assets/index-Z81SMsgR.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
- package/dist/assets/{linear-CcR4pc0-.js → linear-BASsfWly.js} +1 -1
- package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-BVidiPkk.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +26 -13
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/container-mcp.js +14 -27
- package/dist-server/drivers/agents-proxy.js +108 -49
- package/dist-server/harness-mcp-proxy.js +1 -2
- package/dist-server/index.js +4664 -4644
- package/dist-server/openmausbot.js +1425 -1437
- package/dist-server/pair-cli.js +1425 -1437
- package/dist-server/permission-proxy.js +0 -4
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +20 -5
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +71 -87
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +340 -167
- package/dist-server/server/chief-of-staff.js +7 -6
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +13 -16
- package/dist-server/server/cloud-home.js +3 -13
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/composio.js +10 -3
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +99 -75
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/container-computer.js +16 -5
- package/dist-server/server/contracts.js +0 -9
- 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/default-model-selection.js +20 -14
- package/dist-server/server/delegations.js +14 -7
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/acp/opencode-go.js +19 -23
- package/dist-server/server/drivers/agents-call.js +47 -64
- package/dist-server/server/drivers/agents-catalog.js +69 -3
- package/dist-server/server/drivers/agents-proxy.js +2 -1
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/claude.js +49 -30
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +7 -6
- 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 +113 -11
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/drivers/prompt-split.js +4 -4
- package/dist-server/server/harness/bus.js +49 -0
- package/dist-server/server/harness-mcp-proxy.js +1 -1
- package/dist-server/server/index.js +942 -980
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/message-file.js +4 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-delivery.js +4 -19
- package/dist-server/server/peer-roster.js +40 -6
- package/dist-server/server/profile-versions.js +0 -31
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/restart.js +40 -0
- package/dist-server/server/room-handoffs.js +12 -0
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/routes/bot-memory.js +0 -24
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routine-requests.js +2 -5
- package/dist-server/server/routines.js +6 -5
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/store.js +126 -2
- package/dist-server/server/surface.js +40 -64
- package/dist-server/server/system-prompt.js +23 -13
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/tunnel.js +4 -1
- package/dist-server/server/turn-continuation.js +162 -0
- 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/workspace.js +3 -4
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/ask-question.js +1 -71
- package/dist-server/shared/cloud-computer.js +10 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/failed-turn.js +22 -0
- package/dist-server/shared/live-approval.js +2 -2
- package/dist-server/shared/package-format.js +3 -3
- package/dist-server/shared/pairing-link.js +144 -0
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +14 -27
- package/dist-server/workspace-backup.worker.js +14 -27
- package/package.json +1 -1
- package/dist/assets/channel-DS2NbT5x.js +0 -1
- package/dist/assets/index-NF7XCHAy.css +0 -1
- package/dist/assets/index-X_5ydXIE.js +0 -610
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.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/boatagent.js +0 -443
- package/dist-server/server/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** The removed engine's driver kind ("boxAgent" was Boat's historical name). */
|
|
2
|
+
export const REMOVED_COMPUTER_DRIVER = "boxAgent";
|
|
3
|
+
/** The instance id every default fleet gave it. */
|
|
4
|
+
const REMOVED_COMPUTER_INSTANCE = "computer";
|
|
5
|
+
/** Instance ids that named the Computer engine. A saved fleet may hold the
|
|
6
|
+
* default id or one of its own; an id the person reused for another engine
|
|
7
|
+
* is theirs and stays. */
|
|
8
|
+
export function removedComputerInstanceIds(saved) {
|
|
9
|
+
const ids = new Set();
|
|
10
|
+
const reused = saved && Object.hasOwn(saved, REMOVED_COMPUTER_INSTANCE) &&
|
|
11
|
+
saved[REMOVED_COMPUTER_INSTANCE].driver !== REMOVED_COMPUTER_DRIVER;
|
|
12
|
+
if (!reused)
|
|
13
|
+
ids.add(REMOVED_COMPUTER_INSTANCE);
|
|
14
|
+
for (const [id, entry] of Object.entries(saved ?? {})) {
|
|
15
|
+
if (entry.driver === REMOVED_COMPUTER_DRIVER)
|
|
16
|
+
ids.add(id);
|
|
17
|
+
}
|
|
18
|
+
return ids;
|
|
19
|
+
}
|
|
20
|
+
/** A fleet without the removed engine: a saved entry for it is dropped, so
|
|
21
|
+
* it is never offered and the next save leaves it off disk. */
|
|
22
|
+
export function withoutComputerEngine(map) {
|
|
23
|
+
for (const [id, entry] of Object.entries(map)) {
|
|
24
|
+
if (entry.driver === REMOVED_COMPUTER_DRIVER)
|
|
25
|
+
delete map[id];
|
|
26
|
+
}
|
|
27
|
+
return map;
|
|
28
|
+
}
|
|
29
|
+
/** The line a moved conversation shows, in plain words. It names the removed
|
|
30
|
+
* choice as people saw it in the model list ("Computer", not the panel's
|
|
31
|
+
* Computer tab), claims a cloud computer only where one is still used, says
|
|
32
|
+
* so when the new engine can't use the one it had, and names the one setting
|
|
33
|
+
* to change when a turn there would be refused. */
|
|
34
|
+
export function computerEngineMoveText(move, botName, engine) {
|
|
35
|
+
const removed = "The Computer choice in the model list was removed.";
|
|
36
|
+
const bot = move.scope !== "conversation";
|
|
37
|
+
const head = move.scope === "conversation" ? `This conversation now uses ${engine}.`
|
|
38
|
+
: move.scope === "bot-only" ? `${botName} now uses ${engine}; this conversation keeps its own model.`
|
|
39
|
+
: `${botName} now uses ${engine}.`;
|
|
40
|
+
const noComputer = ` ${engine} can't use a computer, so`;
|
|
41
|
+
const place = move.cloud
|
|
42
|
+
? bot ? ` ${botName} still works on its cloud computer.` : ` It still works on ${botName}'s cloud computer.`
|
|
43
|
+
: move.noComputer === "auto"
|
|
44
|
+
? `${noComputer} ${bot ? `${botName} no longer works on its` : `it no longer works on ${botName}'s`}` +
|
|
45
|
+
" cloud computer. To use it again, choose a model that can use a computer."
|
|
46
|
+
: move.noComputer === "works-on"
|
|
47
|
+
? `${noComputer} ${botName} can't reply on ${engine} while its Works on is Cloud computer.` +
|
|
48
|
+
" Choose a model that can use a computer, or set Works on to Auto."
|
|
49
|
+
: move.noComputer === "pin"
|
|
50
|
+
? `${noComputer} ${botName} can't reply here while this conversation is pinned to its cloud computer.` +
|
|
51
|
+
" Choose a model that can use a computer, or clear this conversation's place in the composer."
|
|
52
|
+
: "";
|
|
53
|
+
const ask = !move.askNow ? ""
|
|
54
|
+
: move.scope === "bot-only" ? ` ${botName}'s permissions are now Ask, but this conversation's stay as they were.`
|
|
55
|
+
: " Its permissions are now Ask.";
|
|
56
|
+
return `${head} ${removed}${place}${ask}`;
|
|
57
|
+
}
|
|
58
|
+
/** Writes each moved conversation's line. The move is saved by then, so a
|
|
59
|
+
* later try never finds these conversations again: a line that fails is
|
|
60
|
+
* logged, and every other line is still written. */
|
|
61
|
+
export function writeComputerEngineMoveLines(moves, write) {
|
|
62
|
+
for (const move of moves) {
|
|
63
|
+
try {
|
|
64
|
+
write(move);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`[engines] the Computer engine move line for ${move.threadId} was not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The option a request picks, and whether it is the computer this turn
|
|
2
|
+
* already has. `current` is the turn's mounted place. `startsOnFirstCall`
|
|
3
|
+
* says that place's computer is the one this turn's first computer call
|
|
4
|
+
* creates or wakes (the bot's own cloud computer): it is this turn's before
|
|
5
|
+
* it is up, so asking for it, or for Auto, keeps it instead of restarting
|
|
6
|
+
* the request somewhere else. */
|
|
7
|
+
export function pickComputer(options, requested, current, startsOnFirstCall) {
|
|
8
|
+
const isCurrent = (option) => option.surface === current && (option.ready || (startsOnFirstCall && option.available));
|
|
9
|
+
const option = requested === "auto"
|
|
10
|
+
? options.find(isCurrent)
|
|
11
|
+
?? options.find((candidate) => candidate.ready && candidate.surface === "vm")
|
|
12
|
+
?? options.find((candidate) => candidate.ready)
|
|
13
|
+
?? options.find((candidate) => candidate.canStart)
|
|
14
|
+
?? options.find((candidate) => candidate.canCreate && candidate.surface === "vm")
|
|
15
|
+
?? options.find((candidate) => candidate.canCreate)
|
|
16
|
+
: options.find((candidate) => candidate.surface === requested);
|
|
17
|
+
return { option, current: option !== undefined && isCurrent(option) };
|
|
18
|
+
}
|
|
@@ -7,13 +7,13 @@ import { join } from "node:path";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { normalizeImageGenerationUrl } from "../shared/image-generation.js";
|
|
9
9
|
import { writeFileAtomic } from "./atomic.js";
|
|
10
|
+
import { withoutComputerEngine } from "./computer-engine-removal.js";
|
|
10
11
|
import { newBotDefaultsSchema } from "./new-bot-defaults.js";
|
|
11
12
|
import { EFFORT_LEVELS } from "../shared/wire.js";
|
|
12
13
|
import { isModelVariant } from "./contracts.js";
|
|
13
14
|
import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-icon.js";
|
|
14
15
|
import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
15
16
|
import { parseJson, schemaIssue } from "./schema.js";
|
|
16
|
-
import { CLOUD_SEAT_IDLE_STOP_MS } from "./cloud-overflow.js";
|
|
17
17
|
const optionalText = z.string().optional();
|
|
18
18
|
const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
19
19
|
const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -292,17 +292,6 @@ const featureConfigSchema = z.object({
|
|
|
292
292
|
* conversations that share words with the message ride into the turn.
|
|
293
293
|
* Read-only; on unless explicitly switched off — see autoRecallEnabled. */
|
|
294
294
|
autoRecall: z.boolean().optional(),
|
|
295
|
-
/** Idle release for computer claims (#1653): a desktop seat that stays
|
|
296
|
-
* screen-quiet for 90 seconds is released to waiting turns while its
|
|
297
|
-
* holder's turn still lives; the previous holder re-claims directly
|
|
298
|
-
* for 10 minutes and yields to an occupied seat. Off until baked; see
|
|
299
|
-
* computerClaimIdleReleaseEnabled for how to enable it by hand. */
|
|
300
|
-
computerClaimIdleRelease: z.boolean().optional(),
|
|
301
|
-
/** Consented cloud overflow for local computer waits (#1655): a wait
|
|
302
|
-
* may offer a per-second-billed cloud seat, but only through an explicit
|
|
303
|
-
* consent card or a configured allowlist — never a silent Auto default.
|
|
304
|
-
* Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
|
|
305
|
-
cloudOverflow: z.boolean().optional(),
|
|
306
295
|
/** Opt-in shared skills library: one store at the data dir that bots
|
|
307
296
|
* read by assignment instead of per-workspace copies. Off until
|
|
308
297
|
* explicitly enabled — see skillsLibraryEnabled. */
|
|
@@ -436,8 +425,12 @@ const appConfigSchema = z.object({
|
|
|
436
425
|
* provider is Boat now and the key is kept for compatibility. */
|
|
437
426
|
box: z.object({ token: optionalText }).optional(),
|
|
438
427
|
vps: vpsConfigSchema.optional(),
|
|
439
|
-
/** Optional OpenCode key; persisted write-only and passed only to its child.
|
|
440
|
-
|
|
428
|
+
/** Optional OpenCode key; persisted write-only and passed only to its child.
|
|
429
|
+
* `providerKeys`: keys for OpenCode's other providers (Venice, Groq…) by the
|
|
430
|
+
* environment name OpenCode reads, also write-only and only for its child.
|
|
431
|
+
* Settings changes go through mergeOpenCodeProviderKeys; the stored copy
|
|
432
|
+
* is read loosely (storedAppConfigSchema). */
|
|
433
|
+
opencodeGo: z.object({ apiKey: optionalText, providerKeys: z.record(z.string(), z.string()).optional() }).optional(),
|
|
441
434
|
/** Voice settings and the selected voice id. `provider` picks the
|
|
442
435
|
* engine: "elevenlabs" (default; needs `key`), "fish" (needs its own
|
|
443
436
|
* `fishKey`; `fishModel` picks its speech model), "system" (the Mac's
|
|
@@ -526,15 +519,6 @@ const appConfigSchema = z.object({
|
|
|
526
519
|
/** The authorization decision log (server/decision-log.ts): days of month
|
|
527
520
|
* files kept, at least; OMB_DECISION_RETENTION_DAYS wins when set. */
|
|
528
521
|
decisions: z.object({ retentionDays: z.number().int().min(1).max(3650).optional() }).strict().optional(),
|
|
529
|
-
/** #1655 cloud-overflow settings. perSecondCostUsd is the operator's own
|
|
530
|
-
* verified rate: with no price configured the feature stays inert rather
|
|
531
|
-
* than show an invented one. allowlistedThreads carries standing consent
|
|
532
|
-
* for exact thread ids. */
|
|
533
|
-
cloudOverflow: z.object({
|
|
534
|
-
perSecondCostUsd: z.number().positive().max(10).optional(),
|
|
535
|
-
idleStopMs: z.number().int().positive().max(24 * 60 * 60_000).optional(),
|
|
536
|
-
allowlistedThreads: z.array(z.string().min(1)).max(1000).optional(),
|
|
537
|
-
}).strict().optional(),
|
|
538
522
|
localVm: localVmConfigSchema.optional(),
|
|
539
523
|
features: featureConfigSchema.optional(),
|
|
540
524
|
onboarding: onboardingConfigSchema.optional(),
|
|
@@ -550,6 +534,15 @@ const appConfigSchema = z.object({
|
|
|
550
534
|
});
|
|
551
535
|
const storedAppConfigSchema = appConfigSchema.extend({
|
|
552
536
|
browserProfiles: storedBrowserProfilesSchema.optional(),
|
|
537
|
+
/** Read loosely, like mcpServers: a hand-edited provider key that isn't
|
|
538
|
+
* text is dropped here, and a bad name or key later
|
|
539
|
+
* (openCodeProviderKeys), never the whole file. */
|
|
540
|
+
opencodeGo: z.object({
|
|
541
|
+
apiKey: optionalText,
|
|
542
|
+
providerKeys: z.record(z.string(), z.unknown()).catch({})
|
|
543
|
+
.transform((keys) => Object.fromEntries(Object.entries(keys).filter((entry) => typeof entry[1] === "string")))
|
|
544
|
+
.optional(),
|
|
545
|
+
}).optional(),
|
|
553
546
|
});
|
|
554
547
|
const appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true })
|
|
555
548
|
.extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() });
|
|
@@ -749,43 +742,6 @@ export function claudeUserMcpEnabled(cfg) {
|
|
|
749
742
|
export function llmThreadTitlesEnabled(cfg) {
|
|
750
743
|
return cfg.features?.llmThreadTitles === true;
|
|
751
744
|
}
|
|
752
|
-
/** Idle release for computer claims (#1653): a whole-turn desktop hold
|
|
753
|
-
* ends after a screen-quiet window (default 90 seconds, screen-poller
|
|
754
|
-
* frames excluded) instead of at turn settle, and the previous holder
|
|
755
|
-
* re-claims directly inside a reclaim window (default 10 minutes) while
|
|
756
|
-
* yielding to a seat another turn already holds. Off unless an explicit
|
|
757
|
-
* `true` — bake it as a maintainer-only flag first, exactly like
|
|
758
|
-
* sharedComputers: enable by hand in ~/.openmausbot/config.json
|
|
759
|
-
* (`{"features": {"computerClaimIdleRelease": true}}`) and restart. */
|
|
760
|
-
export function computerClaimIdleReleaseEnabled(cfg) {
|
|
761
|
-
return cfg.features?.computerClaimIdleRelease === true;
|
|
762
|
-
}
|
|
763
|
-
/** Consented cloud overflow for local computer waits (#1655): while a turn
|
|
764
|
-
* waits for a local desktop, it may offer overflow to a per-second-billed
|
|
765
|
-
* cloud seat — never silently: the card names the cost, and only an
|
|
766
|
-
* explicit per-conversation consent or a configured allowlist thread may
|
|
767
|
-
* start the machine, which then hard-stops after an idle window. Off
|
|
768
|
-
* unless an explicit `true`, like computerClaimIdleRelease: enable by
|
|
769
|
-
* hand in ~/.openmausbot/config.json
|
|
770
|
-
* (`{"features": {"cloudOverflow": true}, "cloudOverflow": {"perSecondCostUsd": 0.0004}}`)
|
|
771
|
-
* and restart. */
|
|
772
|
-
export function cloudOverflowEnabled(cfg) {
|
|
773
|
-
return cfg.features?.cloudOverflow === true;
|
|
774
|
-
}
|
|
775
|
-
/** The per-second rate shown on the card, or null when the operator has
|
|
776
|
-
* not set one: the offer fails closed rather than guess a price. */
|
|
777
|
-
export function cloudOverflowPerSecondCostUsd(cfg) {
|
|
778
|
-
const cost = cfg.cloudOverflow?.perSecondCostUsd;
|
|
779
|
-
return typeof cost === "number" && Number.isFinite(cost) && cost > 0 ? cost : null;
|
|
780
|
-
}
|
|
781
|
-
/** How long a cloud seat may stay screen-quiet before it is stopped. */
|
|
782
|
-
export function cloudOverflowIdleStopMs(cfg) {
|
|
783
|
-
return cfg.cloudOverflow?.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
784
|
-
}
|
|
785
|
-
/** Threads with standing consent, by exact thread id. */
|
|
786
|
-
export function cloudOverflowAllowlistedThreads(cfg) {
|
|
787
|
-
return new Set(cfg.cloudOverflow?.allowlistedThreads ?? []);
|
|
788
|
-
}
|
|
789
745
|
/** Opt-in shared skills library (skills lane S1): one store at the data dir
|
|
790
746
|
* that bots reference by assignment instead of per-workspace copies. Off
|
|
791
747
|
* unless enabled by hand in ~/.openmausbot/config.json
|
|
@@ -815,7 +771,6 @@ export const FLEET_NEUTRAL_KEYS = new Set([
|
|
|
815
771
|
"memory",
|
|
816
772
|
"localVm",
|
|
817
773
|
"features",
|
|
818
|
-
"cloudOverflow",
|
|
819
774
|
"browserProfiles",
|
|
820
775
|
"onboarding",
|
|
821
776
|
]);
|
|
@@ -1151,6 +1106,75 @@ export const PROVIDER_CREDENTIAL_ENV = [
|
|
|
1151
1106
|
"CURSOR_API_KEY",
|
|
1152
1107
|
"CURSOR_AUTH_TOKEN",
|
|
1153
1108
|
];
|
|
1109
|
+
/** Provider keys OpenCode reads from its environment, as it does in a
|
|
1110
|
+
* terminal: with ANTHROPIC_API_KEY set, `opencode` lists Anthropic's models.
|
|
1111
|
+
* Keys OpenMaus saves for another engine (xAI, Mistral, the workspace
|
|
1112
|
+
* Anthropic key) are workspace credentials under other names and never
|
|
1113
|
+
* ride along. */
|
|
1114
|
+
export const OPENCODE_PROVIDER_ENV = [
|
|
1115
|
+
"ANTHROPIC_API_KEY",
|
|
1116
|
+
"OPENAI_API_KEY",
|
|
1117
|
+
"GEMINI_API_KEY",
|
|
1118
|
+
"GOOGLE_API_KEY",
|
|
1119
|
+
"KIMI_API_KEY",
|
|
1120
|
+
"MOONSHOT_API_KEY",
|
|
1121
|
+
"MINIMAX_API_KEY",
|
|
1122
|
+
];
|
|
1123
|
+
/** Keys the owner saves in Settings for OpenCode's other providers (Venice,
|
|
1124
|
+
* Groq, DeepSeek…), each under the environment name OpenCode reads. A Cloud
|
|
1125
|
+
* has no terminal to export them in. They go to OpenCode's process only,
|
|
1126
|
+
* never to this server's own environment. */
|
|
1127
|
+
export const OPENCODE_PROVIDER_KEY_LIMIT = 20;
|
|
1128
|
+
const OPENCODE_PROVIDER_KEY_NAME = /^[A-Z][A-Z0-9_]{0,59}_(?:API_KEY|KEY|TOKEN)$/;
|
|
1129
|
+
/** Printable, no spaces: no provider key has any, and a pasted sentence is not a key. */
|
|
1130
|
+
const OPENCODE_PROVIDER_KEY_VALUE = /^[\x21-\x7e]{1,4096}$/;
|
|
1131
|
+
/** Why OpenCode can't be given a key under this name, or undefined. A name
|
|
1132
|
+
* OpenMaus keeps for itself or another engine would never reach OpenCode:
|
|
1133
|
+
* every engine keeps only the credential names it reads (credentialEnv). */
|
|
1134
|
+
function openCodeProviderKeyNameRefusal(name) {
|
|
1135
|
+
if (!OPENCODE_PROVIDER_KEY_NAME.test(name)) {
|
|
1136
|
+
return "Use the name the provider reads its key from: capitals, ending in _API_KEY, _KEY or _TOKEN, such as VENICE_API_KEY.";
|
|
1137
|
+
}
|
|
1138
|
+
if (name === "OPENCODE_API_KEY")
|
|
1139
|
+
return "Save the OpenCode key in the OpenCode API key box instead.";
|
|
1140
|
+
const reserved = name.startsWith("OPENCODE_") || name.startsWith("OMB_")
|
|
1141
|
+
|| WORKSPACE_CREDENTIAL_ENV.includes(name)
|
|
1142
|
+
|| (PROVIDER_CREDENTIAL_ENV.includes(name) && !OPENCODE_PROVIDER_ENV.includes(name));
|
|
1143
|
+
return reserved ? `OpenMaus keeps ${name} for itself, so OpenCode can't be given it here. Put this key in opencode.json instead.` : undefined;
|
|
1144
|
+
}
|
|
1145
|
+
/** The saved OpenCode provider keys that can be used, by name. A hand-edited
|
|
1146
|
+
* entry OpenCode could not be given, or whose value is not a key, is
|
|
1147
|
+
* skipped; so is anything past the limit. */
|
|
1148
|
+
export function openCodeProviderKeys(cfg) {
|
|
1149
|
+
const saved = cfg.opencodeGo?.providerKeys ?? {};
|
|
1150
|
+
const usable = Object.keys(saved).sort()
|
|
1151
|
+
.filter((name) => !openCodeProviderKeyNameRefusal(name) && OPENCODE_PROVIDER_KEY_VALUE.test(saved[name] ?? ""))
|
|
1152
|
+
.slice(0, OPENCODE_PROVIDER_KEY_LIMIT);
|
|
1153
|
+
return Object.fromEntries(usable.map((name) => [name, saved[name]]));
|
|
1154
|
+
}
|
|
1155
|
+
/** A Settings change to the saved OpenCode provider keys: a key saves or
|
|
1156
|
+
* replaces its name, "" removes it, and every other saved key stays. Returns
|
|
1157
|
+
* the whole new set to store, or why it was refused. */
|
|
1158
|
+
export function mergeOpenCodeProviderKeys(cfg, change) {
|
|
1159
|
+
const keys = new Map(Object.entries(openCodeProviderKeys(cfg)));
|
|
1160
|
+
for (const [name, raw] of Object.entries(change)) {
|
|
1161
|
+
const value = raw.trim();
|
|
1162
|
+
if (!value) {
|
|
1163
|
+
keys.delete(name);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const refusal = openCodeProviderKeyNameRefusal(name);
|
|
1167
|
+
if (refusal)
|
|
1168
|
+
return { ok: false, error: refusal };
|
|
1169
|
+
if (!OPENCODE_PROVIDER_KEY_VALUE.test(value))
|
|
1170
|
+
return { ok: false, error: "That doesn't look like a key. Paste only the key, with no spaces." };
|
|
1171
|
+
keys.set(name, value);
|
|
1172
|
+
}
|
|
1173
|
+
if (keys.size > OPENCODE_PROVIDER_KEY_LIMIT) {
|
|
1174
|
+
return { ok: false, error: `OpenCode can hold up to ${OPENCODE_PROVIDER_KEY_LIMIT} provider keys. Remove one you no longer use, then add this one.` };
|
|
1175
|
+
}
|
|
1176
|
+
return { ok: true, keys: Object.fromEntries([...keys].sort(([a], [b]) => (a < b ? -1 : 1))) };
|
|
1177
|
+
}
|
|
1154
1178
|
const configSaveListeners = new Set();
|
|
1155
1179
|
/** Told, synchronously, what each saveConfig wrote: the file before and
|
|
1156
1180
|
* after. The admin activity log (server/admin-activity.ts) records the
|
|
@@ -1180,7 +1204,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1180
1204
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1181
1205
|
if (storedProfiles.success)
|
|
1182
1206
|
disk.browserProfiles = storedProfiles.data;
|
|
1183
|
-
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", "
|
|
1207
|
+
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", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1184
1208
|
const section = checkedPatch[key];
|
|
1185
1209
|
if (!section)
|
|
1186
1210
|
continue;
|
|
@@ -1420,10 +1444,10 @@ export function instanceOwnsRouting(cfg, entry, routingDefaults) {
|
|
|
1420
1444
|
}
|
|
1421
1445
|
/** The credential env instanceConfigs() injects for one driver at runtime.
|
|
1422
1446
|
* Each secret goes only to the driver that actually reads it: the API-key
|
|
1423
|
-
* Grok driver reads XAI_API_KEY
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1447
|
+
* Grok driver reads XAI_API_KEY and OpenCode reads OPENCODE_API_KEY. Every
|
|
1448
|
+
* other engine brings its own login, so handing it a key it never uses would
|
|
1449
|
+
* only put that key in the environment of an unrelated child process. The
|
|
1450
|
+
* Boat token reaches no engine at all: the harness alone talks to Boat. */
|
|
1427
1451
|
function injectedEnvironment(cfg, instanceId, driver) {
|
|
1428
1452
|
const environment = new Map();
|
|
1429
1453
|
if (driver === "mistral" && cfg.mistral?.key)
|
|
@@ -1462,13 +1486,13 @@ function injectedEnvironment(cfg, instanceId, driver) {
|
|
|
1462
1486
|
if (driver === "openai-compat" && cfg.openaiCompat?.url)
|
|
1463
1487
|
environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
|
|
1464
1488
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1489
|
+
if (driver === "opencodeGo") {
|
|
1490
|
+
// Keys for OpenCode's other providers, under the names OpenCode reads.
|
|
1491
|
+
for (const [name, key] of Object.entries(openCodeProviderKeys(cfg)))
|
|
1492
|
+
environment.set(name, key);
|
|
1493
|
+
if (cfg.opencodeGo?.apiKey)
|
|
1494
|
+
environment.set("OPENCODE_API_KEY", cfg.opencodeGo.apiKey);
|
|
1495
|
+
}
|
|
1472
1496
|
return environment;
|
|
1473
1497
|
}
|
|
1474
1498
|
/** One instance per pasted provider key, each running only on its own key.
|
|
@@ -1523,7 +1547,6 @@ export function instanceConfigs(cfg) {
|
|
|
1523
1547
|
chatgpt: { driver: "codex", displayName: "ChatGPT plan", config: { authMode: "chatgpt-plan" } },
|
|
1524
1548
|
antigravity: { driver: "antigravityAgent" },
|
|
1525
1549
|
opencodeGo: { driver: "opencodeGo" },
|
|
1526
|
-
computer: { driver: "boxAgent" },
|
|
1527
1550
|
openaiCompat: { driver: "openai-compat" },
|
|
1528
1551
|
mistral: { driver: "mistral" },
|
|
1529
1552
|
cerebras: { driver: "cerebras" },
|
|
@@ -1550,7 +1573,8 @@ export function instanceConfigs(cfg) {
|
|
|
1550
1573
|
...CUSTOM_ONLY,
|
|
1551
1574
|
};
|
|
1552
1575
|
const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;
|
|
1553
|
-
|
|
1576
|
+
// A fleet saved before the Computer engine was removed may still name it.
|
|
1577
|
+
const map = withoutComputerEngine(configured ? { ...configured } : { ...DEFAULT_FLEET });
|
|
1554
1578
|
// Product fleets pick up newly shipped engines. A one-off test/shadow map
|
|
1555
1579
|
// (no claude/grok/codex) is left exactly as written.
|
|
1556
1580
|
if (configured &&
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// A connected-app call refused for a missing OAuth permission (MOCA-273).
|
|
2
|
+
//
|
|
3
|
+
// Google answers 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT when the account's grant
|
|
4
|
+
// lacks a scope the action needs, such as gmail.settings.basic for a filter.
|
|
5
|
+
// Composio's default auth config for a toolkit never asks for that scope, so
|
|
6
|
+
// reconnecting cannot fix it and a bot that only sees the 403 retries or
|
|
7
|
+
// asks the person to reconnect. The fix is the person's own auth config in
|
|
8
|
+
// their Composio project, which OpenMausBot already prefers
|
|
9
|
+
// (listCustomAuthConfigs). This appends that explanation to the tool result,
|
|
10
|
+
// leaving the provider's own error untouched.
|
|
11
|
+
const SCOPE_ERROR = /ACCESS_TOKEN_SCOPE_INSUFFICIENT|insufficient authentication scopes|insufficientPermissions/i;
|
|
12
|
+
/** The Google scope a known tool needs, when the tool name says which. */
|
|
13
|
+
function requiredScope(slug) {
|
|
14
|
+
if (!slug.startsWith("GMAIL_"))
|
|
15
|
+
return null;
|
|
16
|
+
if (/FORWARD|SEND_AS|DELEGATE/.test(slug))
|
|
17
|
+
return "https://www.googleapis.com/auth/gmail.settings.sharing";
|
|
18
|
+
if (/FILTER|VACATION|AUTO_REPLY|IMAP|POP|LANGUAGE/.test(slug))
|
|
19
|
+
return "https://www.googleapis.com/auth/gmail.settings.basic";
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function appName(slug) {
|
|
23
|
+
const toolkit = slug.split("_")[0] ?? "";
|
|
24
|
+
if (toolkit === "GMAIL")
|
|
25
|
+
return "Gmail";
|
|
26
|
+
if (toolkit.startsWith("GOOGLE"))
|
|
27
|
+
return "Google";
|
|
28
|
+
return toolkit.charAt(0) + toolkit.slice(1).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
/** The note a bot reads after its call was refused for a missing permission. */
|
|
31
|
+
export function scopeHint(slugs) {
|
|
32
|
+
const slug = slugs.find((candidate) => requiredScope(candidate)) ?? slugs[0] ?? "";
|
|
33
|
+
const app = slug ? appName(slug) : "This app";
|
|
34
|
+
const scope = slug ? requiredScope(slug) : null;
|
|
35
|
+
const permission = scope ? `the ${scope} permission` : "the permission this action needs";
|
|
36
|
+
return `OpenMausBot note: ${app} refused this because the connected account has not granted ${permission}. ` +
|
|
37
|
+
`Reconnecting will not add it: the default Composio connection never asks for it. ` +
|
|
38
|
+
`To allow it, the person creates their own ${app} auth config in their Composio project that includes ${permission}, ` +
|
|
39
|
+
`then reconnects ${app} under Connected apps; OpenMausBot uses that config automatically. ` +
|
|
40
|
+
`Tell the person this. Do not retry this call.`;
|
|
41
|
+
}
|
|
42
|
+
/** Adds the note to one JSON-RPC frame whose tool result is a scope refusal. */
|
|
43
|
+
function annotateFrame(frame, slugs) {
|
|
44
|
+
const result = frame?.result;
|
|
45
|
+
if (!result || !Array.isArray(result.content))
|
|
46
|
+
return false;
|
|
47
|
+
const refused = result.content.some((item) => item && typeof item === "object" && typeof item.text === "string" &&
|
|
48
|
+
SCOPE_ERROR.test(item.text));
|
|
49
|
+
if (!refused)
|
|
50
|
+
return false;
|
|
51
|
+
result.content.push({ type: "text", text: scopeHint(slugs) });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The relayed MCP response, with the note added when a tool call came back
|
|
56
|
+
* refused for a missing OAuth permission. Anything else, including a body it
|
|
57
|
+
* cannot read, is returned byte for byte.
|
|
58
|
+
*/
|
|
59
|
+
export function withScopeHint(bytes, contentType, slugs) {
|
|
60
|
+
const text = new TextDecoder().decode(bytes);
|
|
61
|
+
if (!SCOPE_ERROR.test(text))
|
|
62
|
+
return bytes;
|
|
63
|
+
try {
|
|
64
|
+
if (/text\/event-stream/i.test(contentType)) {
|
|
65
|
+
let changed = false;
|
|
66
|
+
const lines = text.split("\n").map((line) => {
|
|
67
|
+
if (!line.startsWith("data:"))
|
|
68
|
+
return line;
|
|
69
|
+
const frame = JSON.parse(line.slice(5));
|
|
70
|
+
if (!annotateFrame(frame, slugs))
|
|
71
|
+
return line;
|
|
72
|
+
changed = true;
|
|
73
|
+
return `data: ${JSON.stringify(frame)}`;
|
|
74
|
+
});
|
|
75
|
+
return changed ? new TextEncoder().encode(lines.join("\n")) : bytes;
|
|
76
|
+
}
|
|
77
|
+
const frame = JSON.parse(text);
|
|
78
|
+
return annotateFrame(frame, slugs) ? new TextEncoder().encode(JSON.stringify(frame)) : bytes;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return bytes;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -372,11 +372,19 @@ function containerOwnershipLabelsMatch(labels, target) {
|
|
|
372
372
|
function normalizeImageId(id) {
|
|
373
373
|
return id?.trim().replace(/^sha256:/, "") || null;
|
|
374
374
|
}
|
|
375
|
-
function inspectedImage(stdout) {
|
|
375
|
+
function inspectedImage(stdout, runtime) {
|
|
376
376
|
const parsed = JSON.parse(stdout);
|
|
377
377
|
const image = parsed[0];
|
|
378
|
+
// Apple container runs on Apple Silicon and puts image labels inside each
|
|
379
|
+
// platform variant. Never accept another platform's labels or guess between
|
|
380
|
+
// multiple matching variants. Docker/Podman keep their existing inspect paths.
|
|
381
|
+
const variants = runtime === "container" && Array.isArray(image?.variants)
|
|
382
|
+
? image.variants.filter(variant => variant?.platform?.os === "linux" && variant.platform.architecture === "arm64")
|
|
383
|
+
: [];
|
|
378
384
|
return {
|
|
379
|
-
labels:
|
|
385
|
+
labels: runtime === "container"
|
|
386
|
+
? (variants.length === 1 ? variants[0]?.config?.config?.Labels : undefined)
|
|
387
|
+
: image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels ?? image?.configuration?.labels,
|
|
380
388
|
id: normalizeImageId(image?.Id ?? image?.id ?? image?.configuration?.descriptor?.digest),
|
|
381
389
|
};
|
|
382
390
|
}
|
|
@@ -430,7 +438,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
|
|
|
430
438
|
}
|
|
431
439
|
try {
|
|
432
440
|
const { stdout } = await runner(status.runtime, ["image", "inspect", IMAGE]);
|
|
433
|
-
const image = inspectedImage(stdout);
|
|
441
|
+
const image = inspectedImage(stdout, status.runtime);
|
|
434
442
|
status.image = imageLabelsMatch(image.labels);
|
|
435
443
|
status.image_id = image.id;
|
|
436
444
|
}
|
|
@@ -699,8 +707,11 @@ async function ensureVmWorkspace(platform, target) {
|
|
|
699
707
|
if (platform !== "win32")
|
|
700
708
|
await chmod(target.workspaceDir, 0o700);
|
|
701
709
|
}
|
|
710
|
+
function baseImagePullArgs(runtime) {
|
|
711
|
+
return runtime === "container" ? ["image", "pull", BASE_IMAGE] : ["pull", BASE_IMAGE];
|
|
712
|
+
}
|
|
702
713
|
async function prepareManagedImage(runtime, runner) {
|
|
703
|
-
await runner(runtime,
|
|
714
|
+
await runner(runtime, baseImagePullArgs(runtime), 10 * 60_000);
|
|
704
715
|
const context = await mkdtemp(join(tmpdir(), "openmausbot-cua-image-"));
|
|
705
716
|
try {
|
|
706
717
|
await writeFile(join(context, "Dockerfile"), managedImageDockerfile(), { mode: 0o600 });
|
|
@@ -938,7 +949,7 @@ export function setupCommands(runtime, platform = process.platform, target = SHA
|
|
|
938
949
|
runtimeStart,
|
|
939
950
|
// This is the inspectable base download. The normal Prepare button also
|
|
940
951
|
// builds the checksum-pinned 0.20.0 derivative automatically.
|
|
941
|
-
pull: command(
|
|
952
|
+
pull: command(baseImagePullArgs(runtime)),
|
|
942
953
|
run: runtime === "container" && target.key !== SHARED_LOCAL_VM_TARGET.key
|
|
943
954
|
? null
|
|
944
955
|
: command(containerRunArgs(runtime, "CHANGE_ME", target)),
|
|
@@ -49,15 +49,6 @@ export function qualifiedModelLabel(id, qualifier, options = {}) {
|
|
|
49
49
|
return id;
|
|
50
50
|
return `${id}${decorate(qualifier)}`;
|
|
51
51
|
}
|
|
52
|
-
/** True when this driver's turn can run against a cloud computer — natively
|
|
53
|
-
* (remoteAgent) or through its computer tools (computerMcp), where the cloud
|
|
54
|
-
* computer is one more stdio server. The one rule for Hosted desktop: every
|
|
55
|
-
* cloud attach path refuses an engine without it before anything is
|
|
56
|
-
* provisioned (cloudPlaceDriverError). Derived, so no driver can declare it
|
|
57
|
-
* out of step with the two fields it reads. */
|
|
58
|
-
export function usesCloudComputer(capabilities) {
|
|
59
|
-
return capabilities.remoteAgent === true || capabilities.computerMcp === true;
|
|
60
|
-
}
|
|
61
52
|
let eventCounter = 0;
|
|
62
53
|
export const newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
|
|
63
54
|
export const newId = () => crypto.randomUUID();
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
+
import { isLoopback } from "../provider-key-check.js";
|
|
1
2
|
export const JEV_DEFAULT_BASE_URL = "https://api.typesafe.ai";
|
|
2
3
|
export const JEV_MODEL = "jev-latest";
|
|
3
4
|
export const JEV_MAX_OPTIONS = 255;
|
|
4
5
|
export const JEV_MIN_LEVELS = 2;
|
|
5
6
|
export const JEV_MAX_LEVELS = 10;
|
|
6
|
-
function isLoopback(hostname) {
|
|
7
|
-
const host = hostname.replace(/^\[|\]$/g, "");
|
|
8
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
|
|
9
|
-
}
|
|
10
7
|
/** The endpoint for a base URL, or null when the key must not be sent there:
|
|
11
8
|
* https anywhere, plain http only to this machine (a local Jev-compatible
|
|
12
9
|
* server or a test double). */
|
|
@@ -14,6 +14,7 @@ import { createHash } from "node:crypto";
|
|
|
14
14
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { boundRetentionDays, pruneMonthFiles } from "../decision-log.js";
|
|
17
|
+
import { monthKey } from "../usage-ledger.js";
|
|
17
18
|
export const DECIDER_LOG_DIR = "decider-log";
|
|
18
19
|
const PRUNE_EVERY_MS = 60 * 60_000;
|
|
19
20
|
const writeQueues = new Map();
|
|
@@ -28,9 +29,6 @@ export function stateHash(state) {
|
|
|
28
29
|
}
|
|
29
30
|
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
30
31
|
}
|
|
31
|
-
function monthKey(at) {
|
|
32
|
-
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
33
|
-
}
|
|
34
32
|
export function deciderLogFileFor(dataDir, at) {
|
|
35
33
|
return join(dataDir, DECIDER_LOG_DIR, `${monthKey(at)}.ndjson`);
|
|
36
34
|
}
|
|
@@ -32,7 +32,7 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
32
32
|
import { appendFile, mkdir, readdir, stat, unlink } from "node:fs/promises";
|
|
33
33
|
import { basename, join } from "node:path";
|
|
34
34
|
import { redactSecrets } from "./redact.js";
|
|
35
|
-
import { csvCell } from "./usage-ledger.js";
|
|
35
|
+
import { csvCell, monthKey } from "./usage-ledger.js";
|
|
36
36
|
const DIR = "decisions";
|
|
37
37
|
const LEGACY_FILE = "decisions.ndjson";
|
|
38
38
|
const MONTH_FILE = /^(\d{4})-(\d{2})\.ndjson$/;
|
|
@@ -70,9 +70,6 @@ const actorScope = new AsyncLocalStorage();
|
|
|
70
70
|
export function withDecisionActor(actor, work, via) {
|
|
71
71
|
return actorScope.run(via ? { actor, via } : { actor }, work);
|
|
72
72
|
}
|
|
73
|
-
function monthKey(at) {
|
|
74
|
-
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
75
|
-
}
|
|
76
73
|
export function decisionFileFor(dataDir, at) {
|
|
77
74
|
return join(dataDir, DIR, `${monthKey(at)}.ndjson`);
|
|
78
75
|
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
const claudeFirst = (instances) => instances.find((instance) => instance.driverKind === "claudeAgent") ?? instances[0];
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
2
|
+
/** The ready rule: an engine can run a turn now when it is available, the
|
|
3
|
+
* organisation allows it, and it is not signed out. A custom endpoint brings
|
|
4
|
+
* its own credential, so its sign-in state does not count. */
|
|
5
|
+
export function readyToRun(instance, context = {}) {
|
|
6
|
+
return instance.snapshot.state === "available" && context.refusal?.(instance) === undefined &&
|
|
7
|
+
(instance.access === "custom" || instance.snapshot.authenticated !== false);
|
|
8
|
+
}
|
|
9
|
+
/** The engine a new bot gets, and the one a bot moved off a removed engine
|
|
10
|
+
* gets (computer-engine-removal.ts): one picker for both.
|
|
11
|
+
*
|
|
12
|
+
* A saved choice is intentional: an unavailable provider or removed model
|
|
13
|
+
* sends new bots to setup instead of silently changing their provider.
|
|
14
|
+
* Without one, an engine that can run a turn now wins (Claude first), then the
|
|
15
|
+
* first available one. On an enrolled desktop a working personal engine
|
|
16
|
+
* still beats a Company one, so billing stays the person's explicit choice. */
|
|
4
17
|
export function selectDefaultModelSelection(instances, preferred, context = {}) {
|
|
5
18
|
if (preferred) {
|
|
6
19
|
const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
|
|
@@ -22,19 +35,12 @@ export function selectDefaultModelSelection(instances, preferred, context = {})
|
|
|
22
35
|
}
|
|
23
36
|
// The organisation's policy is inert (no refusal) unless this desktop is enrolled.
|
|
24
37
|
const available = instances.filter((instance) => instance.snapshot.state === "available" && context.refusal?.(instance) === undefined);
|
|
38
|
+
const ready = available.filter((instance) => readyToRun(instance, context));
|
|
25
39
|
const company = context.company;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// can run a turn now; a working personal engine still wins, so billing
|
|
31
|
-
// stays the person's explicit choice.
|
|
32
|
-
const ready = available.filter((instance) => instance.access === "custom" || instance.snapshot.authenticated !== false);
|
|
33
|
-
pick = claudeFirst(ready.filter((instance) => !company(instance.instanceId))) ?? claudeFirst(ready) ?? claudeFirst(available);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
pick = claudeFirst(available);
|
|
37
|
-
}
|
|
40
|
+
const personal = company && instances.some((instance) => company(instance.instanceId))
|
|
41
|
+
? claudeFirst(ready.filter((instance) => !company(instance.instanceId)))
|
|
42
|
+
: undefined;
|
|
43
|
+
const pick = personal ?? claudeFirst(ready) ?? claudeFirst(available);
|
|
38
44
|
return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
|
|
39
45
|
}
|
|
40
46
|
/** Complete a new bot's selection with the workspace's new-bot effort. An
|