openmausbot 0.1.92 → 0.1.93
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-Do6lBa4C.js → DesktopViewer-CKNR3SEW.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DGBaEXrM.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +6 -1
- package/dist-server/drivers/agents-proxy.js +2 -0
- package/dist-server/index.js +1722 -906
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/mcp-gate.js +5 -1
- package/dist-server/openmausbot.js +763 -93
- package/dist-server/pair-cli.js +763 -93
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +14 -1
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +172 -12
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
- package/dist-server/server/drivers/claude.js +17 -3
- package/dist-server/server/drivers/openai-chat.js +26 -2
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/index.js +210 -57
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/store.js +54 -0
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup.js +22 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/vps-container-mcp.js +6 -1
- package/dist-server/workspace-backup.worker.js +14 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +0 -1
- package/dist/assets/index-Cdmjrl28.css +0 -1
- package/dist/assets/index-DR7YPMkW.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
|
@@ -63,7 +63,7 @@ import { createMemoryUpkeep, upkeepEnabled } from "./memory-upkeep.js";
|
|
|
63
63
|
import { appendAboutMe, commitLearned, planLearned } from "./profile-learned.js";
|
|
64
64
|
import { canAccessTeam, canReachPeer, coordinatorSupervises, livePeerRoster, livePeerRosterBlock, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, resolveTeammate, roomPeerRosterSystemPrompt, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
|
|
65
65
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
66
|
-
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerExec, containerRuntimeStatus,
|
|
66
|
+
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerExec, containerRuntimeStatus, localVmWakeAction, localVmWorkspaceExists, perBotLocalVmTarget, poolLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, VM_WORKSPACE_GUEST, } from "./container-computer.js";
|
|
67
67
|
import { attachForTurn, saveBotAttachment } from "./bot-attachment.js";
|
|
68
68
|
import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmIdleTimeoutMinutes, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, routinesInConversationEnabled, claudeUserMcpEnabled, skillAuthoringEnabled, autoRecallEnabled, captureQuietMs, tidyHour, sharedComputersEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, computerClaimIdleReleaseEnabled, cloudOverflowAllowlistedThreads, cloudOverflowEnabled, cloudOverflowIdleStopMs, cloudOverflowPerSecondCostUsd, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, stripWorkspaceCredentialEnv, PROVIDER_CREDENTIAL_ENV, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, CLAUDE_API_INSTANCE, } from "./config.js";
|
|
69
69
|
import { sweepThreadEventLogs } from "./thread-retention.js";
|
|
@@ -145,6 +145,7 @@ import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-
|
|
|
145
145
|
import { buildSystemPrompt, userProfileSystemPrompt, cloudHomePrompt, computerPrompt, composioSystemPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, resolveComputerPromptKind, } from "./system-prompt.js";
|
|
146
146
|
import { readCuaConnection, readCuaUnavailableReason, gatedLocalComputer } from "./local-computer.js";
|
|
147
147
|
import { discoverExistingPerBotLocalVms, discoverExistingPoolLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
148
|
+
import { localVmStopReason, recordLocalVmIdleStop } from "./local-vm-stop-reason.js";
|
|
148
149
|
import { LocalVmIdleTimer } from "./local-vm-idle.js";
|
|
149
150
|
import { LocalVmLeasePool } from "./local-vm-lease.js";
|
|
150
151
|
import { LocalVmSeatPool } from "./local-vm-seat-pool.js";
|
|
@@ -3466,6 +3467,9 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
3466
3467
|
if (bot.approvalGrant.allThreads && mode === "full") {
|
|
3467
3468
|
store.setAllThreadApprovalMode(botId, mode);
|
|
3468
3469
|
}
|
|
3470
|
+
else if (bot.approvalGrant.refreshPermissions && bot.approvalGrant.threadId) {
|
|
3471
|
+
store.refreshTaskPermissions(botId, bot.approvalGrant.threadId);
|
|
3472
|
+
}
|
|
3469
3473
|
else if (bot.approvalGrant.threadId) {
|
|
3470
3474
|
store.patchTask(botId, bot.approvalGrant.threadId, { approvalMode: mode, autoApprove: false });
|
|
3471
3475
|
}
|
|
@@ -3671,6 +3675,39 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
3671
3675
|
respond({ ok: false, error: "Invalid thread approval scope" });
|
|
3672
3676
|
return true;
|
|
3673
3677
|
}
|
|
3678
|
+
if (message.refreshPermissions !== undefined && message.refreshPermissions !== true) {
|
|
3679
|
+
respond({ ok: false, error: "Invalid permission refresh" });
|
|
3680
|
+
return true;
|
|
3681
|
+
}
|
|
3682
|
+
if (message.refreshPermissions === true) {
|
|
3683
|
+
const target = typeof threadId === "string" ? store.projectBotForTask(botId, threadId) : null;
|
|
3684
|
+
const savedMode = approvalModeFor({ ...existing, approvalGrant: undefined });
|
|
3685
|
+
if (message.threadOnly !== true || !target || message.allThreads === true || message.modelSelection !== undefined || message.updateBotDefault !== undefined || savedMode !== mode) {
|
|
3686
|
+
respond({ ok: false, error: "Choose this bot's approval level in bot settings before refreshing a thread" });
|
|
3687
|
+
return true;
|
|
3688
|
+
}
|
|
3689
|
+
if (existing.approvalGrant || threadBusy(botId, target.threadId)) {
|
|
3690
|
+
respond({ ok: false, error: "Stop this thread and finish its pending approval change first" });
|
|
3691
|
+
return true;
|
|
3692
|
+
}
|
|
3693
|
+
if (!supportsApprovalMode(target.modelSelection, mode)) {
|
|
3694
|
+
respond({ ok: false, error: "This thread's provider does not support that approval level" });
|
|
3695
|
+
return true;
|
|
3696
|
+
}
|
|
3697
|
+
if (mode === "auto" && target.computer === "local" && approvalModeFor(target) !== "auto" && message.acknowledgeLocalAuto !== true) {
|
|
3698
|
+
respond({ ok: false, error: "Auto mode on this computer requires confirming the warning" });
|
|
3699
|
+
return true;
|
|
3700
|
+
}
|
|
3701
|
+
if (mode === "full" || mode === "custom") {
|
|
3702
|
+
store.patchBot(botId, { approvalGrant: { requestId, mode, phase: "prepared", threadId: target.threadId, threadOnly: true, refreshPermissions: true } });
|
|
3703
|
+
}
|
|
3704
|
+
else if (!store.refreshTaskPermissions(botId, target.threadId)) {
|
|
3705
|
+
respond({ ok: false, error: "That thread is no longer available" });
|
|
3706
|
+
return true;
|
|
3707
|
+
}
|
|
3708
|
+
respond({ ok: true, bot: wireTrustedApprovalBot(store.bot(botId)) });
|
|
3709
|
+
return true;
|
|
3710
|
+
}
|
|
3674
3711
|
if (message.threadOnly === true) {
|
|
3675
3712
|
const target = typeof threadId === "string" ? store.projectBotForTask(botId, threadId) : null;
|
|
3676
3713
|
if (!target || message.modelSelection !== undefined || message.updateBotDefault !== undefined) {
|
|
@@ -4864,6 +4901,18 @@ const desktopViewer = createDesktopViewer({
|
|
|
4864
4901
|
});
|
|
4865
4902
|
},
|
|
4866
4903
|
live: auth => auth.kind === "loopback" || sessions.isLive(auth.session.id),
|
|
4904
|
+
// A phone paired with this server directly drives a bot's Local VM through
|
|
4905
|
+
// this proxy under its control lease (see the local-computer join route).
|
|
4906
|
+
// The lease must hold the bot's computer, and that computer must be the
|
|
4907
|
+
// desktop being viewed: a lease on one bot opens no other bot's VM.
|
|
4908
|
+
lease: (id, botId, controlLeaseId, threadId) => {
|
|
4909
|
+
const bot = store.bot(botId);
|
|
4910
|
+
if (!bot || viewerTargetId(localVmTargetForStatus(bot.id, threadId)) !== id)
|
|
4911
|
+
return;
|
|
4912
|
+
const key = botComputerControlKey(bot);
|
|
4913
|
+
const holds = () => computerControl.ownsLease(key, controlLeaseId);
|
|
4914
|
+
return holds() ? holds : undefined;
|
|
4915
|
+
},
|
|
4867
4916
|
});
|
|
4868
4917
|
function closeSessionStreams(sessionId) {
|
|
4869
4918
|
browserLive.closeForOwner(sessionId);
|
|
@@ -5012,7 +5061,7 @@ const askMessageByRequest = new Map(); // threadId:requestId -> messageId
|
|
|
5012
5061
|
const pendingCommandRules = new Map();
|
|
5013
5062
|
function commandRememberRefusal(auth, threadId, requestId, behavior) {
|
|
5014
5063
|
if (!canManageCommandAllowlist(auth))
|
|
5015
|
-
return "Only the
|
|
5064
|
+
return "Only the owner or an admin can save bot-wide command permissions.";
|
|
5016
5065
|
if (behavior !== "allow" || !pendingCommandRules.has(`${threadId}:${requestId}`)) {
|
|
5017
5066
|
return "This live request does not contain a command that can be saved. Allow it once instead.";
|
|
5018
5067
|
}
|
|
@@ -5199,10 +5248,9 @@ const watchdog = new TurnWatchdog({
|
|
|
5199
5248
|
onStall: (turn) => {
|
|
5200
5249
|
const stalledResourceOwner = turnResourceOwners.get(turn.threadId);
|
|
5201
5250
|
const stalledGeneration = directTurnGenerationByThread.get(turn.threadId);
|
|
5251
|
+
const stalledProviderTurn = directRequestOwners.get(turn.threadId);
|
|
5202
5252
|
cancelDirectTurnDispatch(turn.botId, turn.threadId);
|
|
5203
|
-
|
|
5204
|
-
// to the room grace cleanup added here.
|
|
5205
|
-
const stalledVmTarget = groupSpeakers.has(turn.threadId) ? localVmThreadTargets.get(turn.threadId) : undefined;
|
|
5253
|
+
const stalledVmTarget = localVmThreadTargets.get(turn.threadId);
|
|
5206
5254
|
revokeInternalCapabilitiesForThread(turn.threadId);
|
|
5207
5255
|
repeats.settle(turn.threadId);
|
|
5208
5256
|
const bot = botForThread(turn.botId, turn.threadId);
|
|
@@ -5240,6 +5288,9 @@ const watchdog = new TurnWatchdog({
|
|
|
5240
5288
|
return;
|
|
5241
5289
|
if (stalledResourceOwner && turnResourceOwners.get(turn.threadId)?.generation !== stalledResourceOwner.generation)
|
|
5242
5290
|
return;
|
|
5291
|
+
if (stalledGeneration && stalledProviderTurn?.generation === stalledGeneration && stalledProviderTurn.turnId) {
|
|
5292
|
+
retireProviderTurn(stalledProviderTurn.turnId);
|
|
5293
|
+
}
|
|
5243
5294
|
// A goal coordinator can stall before sendTurn reveals its provider
|
|
5244
5295
|
// turn id. Reusing the room during that ambiguous pre-id window would
|
|
5245
5296
|
// make old and replacement events indistinguishable. Keep ownership
|
|
@@ -5254,7 +5305,8 @@ const watchdog = new TurnWatchdog({
|
|
|
5254
5305
|
}
|
|
5255
5306
|
const group = store.groupByThread(turn.threadId);
|
|
5256
5307
|
const speaker = groupSpeakers.get(turn.threadId);
|
|
5257
|
-
|
|
5308
|
+
const stalledRoomSpeaker = group && group.busyBotId === turn.botId && speaker?.botId === turn.botId;
|
|
5309
|
+
if (stalledRoomSpeaker) {
|
|
5258
5310
|
groupSpeakers.delete(turn.threadId);
|
|
5259
5311
|
store.patchGroup(group.id, { busyBotId: null, unread: true });
|
|
5260
5312
|
}
|
|
@@ -5262,7 +5314,7 @@ const watchdog = new TurnWatchdog({
|
|
|
5262
5314
|
// computer claim. The generation checks above protect replacements.
|
|
5263
5315
|
releaseTurnResources(stalledResourceOwner);
|
|
5264
5316
|
const currentBot = store.bot(turn.botId);
|
|
5265
|
-
if (currentBot?.busy) {
|
|
5317
|
+
if (currentBot?.busy && (threadBusy(currentBot.id, turn.threadId) || stalledRoomSpeaker)) {
|
|
5266
5318
|
stopScreenPoller(currentBot.id, turn.threadId);
|
|
5267
5319
|
vpsThreadEnded(currentBot.id, turn.threadId);
|
|
5268
5320
|
if (store.taskByThread(currentBot.id, turn.threadId))
|
|
@@ -6378,11 +6430,11 @@ function localVmIdleFor(target) {
|
|
|
6378
6430
|
localVmLifecycleBusy.add(target.key);
|
|
6379
6431
|
try {
|
|
6380
6432
|
const status = await containerComputerStatus(undefined, undefined, target);
|
|
6381
|
-
// The
|
|
6382
|
-
//
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6433
|
+
// The pinned VNC startup clears stale X locks. Keep the container's
|
|
6434
|
+
// home and installed software so inactivity is a reversible stop.
|
|
6435
|
+
if (status.container === "running" && status.managed) {
|
|
6436
|
+
const stopped = await containerComputerAction("stop", undefined, undefined, target);
|
|
6437
|
+
recordLocalVmIdleStop(target.key, stopped.stopped_at);
|
|
6386
6438
|
}
|
|
6387
6439
|
}
|
|
6388
6440
|
finally {
|
|
@@ -6424,7 +6476,7 @@ function releaseLocalVmThread(threadId) {
|
|
|
6424
6476
|
// Browser, This computer, Auto, or Off does not delete its old Local VM.
|
|
6425
6477
|
void (async () => {
|
|
6426
6478
|
if (localVmMode(cfg) === "pool") {
|
|
6427
|
-
// Same restore rule as per-bot: idle
|
|
6479
|
+
// Same restore rule as per-bot: idle shutdown preserves the container and
|
|
6428
6480
|
// its provisioned workspace, so every surviving seat stays Auto-eligible.
|
|
6429
6481
|
const seats = localVmMaxInstances(cfg);
|
|
6430
6482
|
for (let seat = 0; seat < seats; seat += 1) {
|
|
@@ -8759,7 +8811,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8759
8811
|
* desktop, not whatever localVmTargetForBot resolves to by the time
|
|
8760
8812
|
* the first screen call arrives. */
|
|
8761
8813
|
const claimAutoLocalVm = async (claimThreadId, pinnedTarget) => {
|
|
8762
|
-
const localVmTarget = pinnedTarget ?? localVmTargetForThread(bot.id, claimThreadId);
|
|
8814
|
+
const localVmTarget = pinnedTarget ?? localVmThreadTargets.get(claimThreadId) ?? { ...localVmTargetForThread(bot.id, claimThreadId) };
|
|
8763
8815
|
await bindTurnComputer(resourceOwner, `computer:vm:${localVmTarget.key}`, true);
|
|
8764
8816
|
if (localVmImageBusy || localVmModeChangeBusy || localVmLifecycleBusy.has(localVmTarget.key)) {
|
|
8765
8817
|
throw new Error("this Local VM is being started, stopped, or replaced — wait for setup to finish");
|
|
@@ -8783,6 +8835,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8783
8835
|
// without this the exclusive lease would sit held for the rest of a
|
|
8784
8836
|
// turn that never got the VM — the very serialisation #1361 removes.
|
|
8785
8837
|
const dropLease = () => {
|
|
8838
|
+
if (localVmThreadTargets.get(claimThreadId) !== localVmTarget)
|
|
8839
|
+
return;
|
|
8786
8840
|
localVmLeaseFor(localVmTarget).release(claimThreadId);
|
|
8787
8841
|
if (localVmActiveThreads.get(localVmTarget.key) === claimThreadId)
|
|
8788
8842
|
localVmActiveThreads.delete(localVmTarget.key);
|
|
@@ -8876,7 +8930,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8876
8930
|
if (!localVmSeen.has(poolCandidate.key) && !opts?.automationSource)
|
|
8877
8931
|
return false;
|
|
8878
8932
|
}
|
|
8879
|
-
const localVmTarget = localVmTargetForThread(bot.id, threadId);
|
|
8933
|
+
const localVmTarget = { ...localVmTargetForThread(bot.id, threadId) };
|
|
8880
8934
|
let lazyReadyVm = null;
|
|
8881
8935
|
if (!strict) {
|
|
8882
8936
|
// Nothing this process has ever seen for this target, and nobody is
|
|
@@ -8941,7 +8995,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8941
8995
|
catch (error) {
|
|
8942
8996
|
if (strict)
|
|
8943
8997
|
throw error;
|
|
8944
|
-
|
|
8998
|
+
if (directTurnGenerationByThread.get(threadId) === dispatchClaimId)
|
|
8999
|
+
releaseLocalVmThread(threadId);
|
|
8945
9000
|
return false;
|
|
8946
9001
|
}
|
|
8947
9002
|
};
|
|
@@ -10414,7 +10469,7 @@ async function resolveAndSendTeamSetup(res, args, ownerReview) {
|
|
|
10414
10469
|
if (!card)
|
|
10415
10470
|
return false;
|
|
10416
10471
|
if (args.behavior === "allow" && !ownerReview) {
|
|
10417
|
-
json(res, 403, { error: "Approve team setup or deletion from the desktop app or a paired
|
|
10472
|
+
json(res, 403, { error: "Approve team setup or deletion from the desktop app or a paired Full-access device. In a local browser, wait until every bot is idle." });
|
|
10418
10473
|
return true;
|
|
10419
10474
|
}
|
|
10420
10475
|
if (args.behavior === "allow" && !card.answered && !card.dismissed && !card.expired) {
|
|
@@ -13183,34 +13238,15 @@ async function localVmPayload(target, auth) {
|
|
|
13183
13238
|
const status = await containerComputerStatus(undefined, undefined, target);
|
|
13184
13239
|
return {
|
|
13185
13240
|
...localVmViewerStatus(status, auth),
|
|
13241
|
+
stop_reason: localVmStopReason(target.key, status),
|
|
13186
13242
|
commands: setupCommands(status.runtime, process.platform, target),
|
|
13187
13243
|
idle_timeout_ms: localVmIdleMs(),
|
|
13188
13244
|
mode: localVmMode(cfg),
|
|
13189
13245
|
max_instances: localVmMaxInstances(cfg),
|
|
13190
13246
|
};
|
|
13191
13247
|
}
|
|
13192
|
-
/**
|
|
13193
|
-
*
|
|
13194
|
-
* `LocalVmIdleTimer` REMOVES an unused Local VM rather than pausing it. The
|
|
13195
|
-
* turn then failed with "Create the Local VM (App Settings → Local VM)" —
|
|
13196
|
-
* which reads like a fault the person must repair by hand, for a container the
|
|
13197
|
-
* app itself deleted eight hours earlier. Someone who steps away overnight
|
|
13198
|
-
* comes back to an error on their first message.
|
|
13199
|
-
*
|
|
13200
|
-
* The cloud branch below already does the opposite: an absent boat is
|
|
13201
|
-
* provisioned on first use behind a `provisioning` broadcast. This gives the
|
|
13202
|
-
* Local VM the same lifecycle for the same reason.
|
|
13203
|
-
*
|
|
13204
|
-
* Only `missing` is recovered, and only when a fresh `run` is all it takes.
|
|
13205
|
-
* Every other problem still surfaces: no runtime installed, no image pulled,
|
|
13206
|
-
* `create_supported` false, or an existing container that is stale, unmanaged
|
|
13207
|
-
* or unsafe. Those need a decision — install podman, download 1.4 GB, replace
|
|
13208
|
-
* a container someone else made — and a stopped container is deliberately not
|
|
13209
|
-
* resumed here, because `localVmProblem` says this desktop image cannot safely
|
|
13210
|
-
* resume and asks for a recreate rather than a start. Per-bot mode keeps its
|
|
13211
|
-
* instance cap; creating past it would quietly do what the lifecycle route
|
|
13212
|
-
* refuses.
|
|
13213
|
-
*/
|
|
13248
|
+
/** Wake an idle desktop, or recreate a missing one from an already prepared
|
|
13249
|
+
* image. Incompatible or unsafe containers still require an explicit decision. */
|
|
13214
13250
|
async function readyLocalVmForTurn(botId, target, isCurrent = () => true) {
|
|
13215
13251
|
localVmLifecycleBusy.add(target.key);
|
|
13216
13252
|
// Fence this target, and the cross-target capacity decision for creates,
|
|
@@ -13230,21 +13266,22 @@ async function readyLocalVmForTurn(botId, target, isCurrent = () => true) {
|
|
|
13230
13266
|
noteLocalVmSeen(target, status);
|
|
13231
13267
|
if (!isCurrent())
|
|
13232
13268
|
return status;
|
|
13233
|
-
|
|
13269
|
+
const action = localVmWakeAction(status);
|
|
13270
|
+
if (status.ready || !action || !status.runtime)
|
|
13234
13271
|
return status;
|
|
13235
13272
|
// Another creation is already mid-flight and its container is not yet
|
|
13236
13273
|
// visible to a count, so the safe answer is the inspected status —
|
|
13237
13274
|
// exactly what the over-cap path below returns.
|
|
13238
|
-
if (!pooled && !ownsProvision)
|
|
13275
|
+
if (action === "run" && !pooled && !ownsProvision)
|
|
13239
13276
|
return status;
|
|
13240
|
-
if (target.key.startsWith("bot:")) {
|
|
13277
|
+
if (action === "run" && target.key.startsWith("bot:")) {
|
|
13241
13278
|
const count = await existingPerBotLocalVmCount(status.runtime);
|
|
13242
13279
|
if (!isCurrent() || count >= localVmMaxInstances(cfg))
|
|
13243
13280
|
return status;
|
|
13244
13281
|
}
|
|
13245
13282
|
broadcast({ kind: "computer", botId, state: "provisioning" });
|
|
13246
13283
|
try {
|
|
13247
|
-
status = await containerComputerAction(
|
|
13284
|
+
status = await containerComputerAction(action, undefined, undefined, target);
|
|
13248
13285
|
}
|
|
13249
13286
|
catch {
|
|
13250
13287
|
// Keep the inspected status: its `problem` names the real obstacle,
|
|
@@ -13312,6 +13349,7 @@ function configStatus() {
|
|
|
13312
13349
|
return {
|
|
13313
13350
|
xai: { configured: Boolean(cfg.xai?.key) },
|
|
13314
13351
|
mistral: { configured: Boolean(cfg.mistral?.key) },
|
|
13352
|
+
cerebras: { configured: Boolean(cfg.cerebras?.key) },
|
|
13315
13353
|
anthropic: { configured: Boolean(cfg.anthropic?.key), everyClaudeBot: cfg.anthropic?.everyClaudeBot !== false },
|
|
13316
13354
|
openai: { configured: Boolean(cfg.openai?.key) },
|
|
13317
13355
|
openrouter: { configured: Boolean(cfg.openrouter?.key) },
|
|
@@ -19045,7 +19083,7 @@ const handleRequest = async (req, res) => {
|
|
|
19045
19083
|
m = path.match(/^\/api\/bots\/([\w-]+)\/command-allowlist(?:\/([\w-]+))?$/);
|
|
19046
19084
|
if (m && ["GET", "POST", "DELETE"].includes(method)) {
|
|
19047
19085
|
if (!canManageCommandAllowlist(auth))
|
|
19048
|
-
return json(res, 403, { error: "Only the
|
|
19086
|
+
return json(res, 403, { error: "Only the owner or an admin can manage command permissions." });
|
|
19049
19087
|
const bot = requestedTaskBot(m[1], url.searchParams.get("threadId") ?? undefined);
|
|
19050
19088
|
if (method === "GET" && !m[2])
|
|
19051
19089
|
return json(res, 200, commandAllowlistResponse(bot));
|
|
@@ -19529,7 +19567,7 @@ const handleRequest = async (req, res) => {
|
|
|
19529
19567
|
// come from local scripts only when all bots are idle; paired owners
|
|
19530
19568
|
// and the packaged desktop's private capability can grant it mid-turn.
|
|
19531
19569
|
if (loosened.includes("managedSections") && auth.kind === "loopback" && !DESKTOP_MANAGED && store.bots.some(bot => bot.busy)) {
|
|
19532
|
-
return json(res, 409, { error: "Stop running bots before granting access to another team, or use the desktop app or a paired
|
|
19570
|
+
return json(res, 409, { error: "Stop running bots before granting access to another team, or use the desktop app or a paired Full-access session." });
|
|
19533
19571
|
}
|
|
19534
19572
|
if (body.peers !== undefined && Array.isArray(existingBot?.peers)) {
|
|
19535
19573
|
const nextPeers = patch.peers;
|
|
@@ -20867,7 +20905,7 @@ const handleRequest = async (req, res) => {
|
|
|
20867
20905
|
const current = store.projectBotForTask(m[1], m[2]);
|
|
20868
20906
|
if (!current)
|
|
20869
20907
|
return json(res, 404, { error: "no such task" });
|
|
20870
|
-
const allowed = new Set(["title", "projectId", "modelSelection", "updateBotDefault", "resetApprovalToAsk", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto", "archivedAt", "pinned", "snoozedUntil", "surface"]);
|
|
20908
|
+
const allowed = new Set(["title", "projectId", "modelSelection", "updateBotDefault", "resetApprovalToAsk", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto", "archivedAt", "pinned", "snoozedUntil", "surface", "refreshPermissions"]);
|
|
20871
20909
|
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
20872
20910
|
return json(res, 400, { error: "unsupported thread setting" });
|
|
20873
20911
|
const notYours = cloudThreadRefusal(auth, m[2]);
|
|
@@ -20877,7 +20915,7 @@ const handleRequest = async (req, res) => {
|
|
|
20877
20915
|
// asking them, even in a conversation someone else started, and what
|
|
20878
20916
|
// the bot itself defaults to.
|
|
20879
20917
|
if (CLOUD_HOME && !cloudOwnerSession(auth) &&
|
|
20880
|
-
(body.approvalMode !== undefined || body.autoApprove !== undefined || body.acknowledgeLocalAuto !== undefined || body.updateBotDefault === true)) {
|
|
20918
|
+
(body.approvalMode !== undefined || body.autoApprove !== undefined || body.acknowledgeLocalAuto !== undefined || body.updateBotDefault === true || body.refreshPermissions === true)) {
|
|
20881
20919
|
return json(res, 403, { error: "On this Cloud only its owner can change how a bot asks for approval, or its default model." });
|
|
20882
20920
|
}
|
|
20883
20921
|
for (const key of ["requireAvailableModel", "acknowledgeLocalAuto", "updateBotDefault", "resetApprovalToAsk"]) {
|
|
@@ -20892,6 +20930,50 @@ const handleRequest = async (req, res) => {
|
|
|
20892
20930
|
(body.approvalMode !== undefined && body.approvalMode !== "ask") || body.autoApprove === true)) {
|
|
20893
20931
|
return json(res, 400, { error: "resetApprovalToAsk requires a model selection and cannot be combined with another approval mode" });
|
|
20894
20932
|
}
|
|
20933
|
+
if (body.refreshPermissions !== undefined && body.refreshPermissions !== true) {
|
|
20934
|
+
return json(res, 400, { error: "refreshPermissions must be true" });
|
|
20935
|
+
}
|
|
20936
|
+
if (body.refreshPermissions === true) {
|
|
20937
|
+
const extra = Object.keys(body).filter((key) => key !== "refreshPermissions" && key !== "acknowledgeLocalAuto");
|
|
20938
|
+
if (extra.length)
|
|
20939
|
+
return json(res, 400, { error: "refreshPermissions cannot be combined with other thread settings" });
|
|
20940
|
+
const profile = store.bot(m[1]);
|
|
20941
|
+
if (!profile)
|
|
20942
|
+
return json(res, 404, { error: "no such bot" });
|
|
20943
|
+
if (profile.approvalGrant)
|
|
20944
|
+
return json(res, 409, { error: "the bot's approval mode is still being confirmed" });
|
|
20945
|
+
if (threadBusy(profile.id, m[2]))
|
|
20946
|
+
return json(res, 409, { error: "stop this thread before changing its approval mode" });
|
|
20947
|
+
const nextMode = approvalModeFor({ ...profile, approvalGrant: undefined });
|
|
20948
|
+
// Full and Custom never travel over the bot-reachable HTTP surface.
|
|
20949
|
+
// The desktop's private channel copies those levels, and it is also
|
|
20950
|
+
// the only way to leave Custom.
|
|
20951
|
+
if (nextMode === "full" || nextMode === "custom") {
|
|
20952
|
+
return json(res, 403, { error: "Refresh Full or Custom access from the packaged desktop app" });
|
|
20953
|
+
}
|
|
20954
|
+
if (approvalModeFor(current) === "custom") {
|
|
20955
|
+
return json(res, 403, { error: "Leaving Custom approval requires the packaged desktop app" });
|
|
20956
|
+
}
|
|
20957
|
+
if (!supportsApprovalMode(current.modelSelection, nextMode)) {
|
|
20958
|
+
return json(res, 400, { error: "This provider does not support the selected approval level" });
|
|
20959
|
+
}
|
|
20960
|
+
if (nextMode === "auto" && current.computer === "local" && approvalModeFor(current) !== "auto" && body.acknowledgeLocalAuto !== true) {
|
|
20961
|
+
return json(res, 400, { error: "Auto mode on this computer requires confirming the warning first (acknowledgeLocalAuto)" });
|
|
20962
|
+
}
|
|
20963
|
+
const approvalRank = { ask: 0, edits: 1, auto: 2, full: 3, custom: 4 };
|
|
20964
|
+
const widensMode = approvalRank[nextMode] > approvalRank[approvalModeFor(current)];
|
|
20965
|
+
const nextAllow = profile.alwaysAllow ?? [];
|
|
20966
|
+
const widensAllow = nextAllow.some((key) => !(current.alwaysAllow ?? []).includes(key));
|
|
20967
|
+
if ((widensMode || widensAllow) && auth.kind === "loopback" && !DESKTOP_MANAGED && !req.headers.origin && store.bots.some((bot) => bot.busy)) {
|
|
20968
|
+
return json(res, 409, { error: "Change approval mode from the app or a paired device while bots are working." });
|
|
20969
|
+
}
|
|
20970
|
+
const task = store.refreshTaskPermissions(profile.id, m[2]);
|
|
20971
|
+
if (!task)
|
|
20972
|
+
return json(res, 404, { error: "no such task" });
|
|
20973
|
+
const fresh = botWithThread(store.bot(profile.id));
|
|
20974
|
+
broadcast({ kind: "bot", bot: fresh });
|
|
20975
|
+
return json(res, 200, { task: wireTask(task), bot: fresh });
|
|
20976
|
+
}
|
|
20895
20977
|
const patch = {};
|
|
20896
20978
|
if (body.projectId !== undefined) {
|
|
20897
20979
|
if (body.projectId === null)
|
|
@@ -21311,8 +21393,10 @@ const handleRequest = async (req, res) => {
|
|
|
21311
21393
|
localVmLifecycleBusy.add(SHARED_LOCAL_VM_TARGET.key);
|
|
21312
21394
|
try {
|
|
21313
21395
|
const status = await containerComputerAction(action, undefined, undefined, SHARED_LOCAL_VM_TARGET);
|
|
21314
|
-
if (action === "run" || action === "start")
|
|
21396
|
+
if (action === "run" || action === "start") {
|
|
21397
|
+
localVmSeen.add(SHARED_LOCAL_VM_TARGET.key);
|
|
21315
21398
|
localVmIdleFor(SHARED_LOCAL_VM_TARGET).touch();
|
|
21399
|
+
}
|
|
21316
21400
|
if (action === "stop" || action === "remove")
|
|
21317
21401
|
localVmIdleFor(SHARED_LOCAL_VM_TARGET).cancel();
|
|
21318
21402
|
return json(res, 200, {
|
|
@@ -21344,7 +21428,7 @@ const handleRequest = async (req, res) => {
|
|
|
21344
21428
|
return json(res, 404, { error: "no such bot" });
|
|
21345
21429
|
return json(res, 200, await localVmPayload(localVmTargetForStatus(bot.id, bot.threadId), auth));
|
|
21346
21430
|
}
|
|
21347
|
-
m = path.match(/^\/api\/bots\/([\w-]+)\/local-computer\/(run|stop|remove)$/);
|
|
21431
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/local-computer\/(run|start|stop|remove)$/);
|
|
21348
21432
|
if (m && method === "POST") {
|
|
21349
21433
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
21350
21434
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
@@ -21355,7 +21439,7 @@ const handleRequest = async (req, res) => {
|
|
|
21355
21439
|
if (boatLifecycleBusyBots.has(bot.id)) {
|
|
21356
21440
|
return json(res, 409, { error: "this bot's computer is being changed or deleted — wait for it to finish" });
|
|
21357
21441
|
}
|
|
21358
|
-
const action = z.enum(["run", "stop", "remove"]).parse(m[2]);
|
|
21442
|
+
const action = z.enum(["run", "start", "stop", "remove"]).parse(m[2]);
|
|
21359
21443
|
const target = localVmTargetForBot(bot.id);
|
|
21360
21444
|
if (target.key === SHARED_LOCAL_VM_TARGET.key) {
|
|
21361
21445
|
return json(res, 409, { error: "Shared mode manages this desktop in App Settings → Computers" });
|
|
@@ -21394,8 +21478,10 @@ const handleRequest = async (req, res) => {
|
|
|
21394
21478
|
}
|
|
21395
21479
|
}
|
|
21396
21480
|
const status = await containerComputerAction(action, undefined, undefined, target);
|
|
21397
|
-
if (action === "run")
|
|
21481
|
+
if (action === "run" || action === "start") {
|
|
21482
|
+
localVmSeen.add(target.key);
|
|
21398
21483
|
localVmIdleFor(target).touch();
|
|
21484
|
+
}
|
|
21399
21485
|
if (action === "stop" || action === "remove")
|
|
21400
21486
|
localVmIdleFor(target).cancel();
|
|
21401
21487
|
return json(res, 200, {
|
|
@@ -21427,6 +21513,61 @@ const handleRequest = async (req, res) => {
|
|
|
21427
21513
|
image: await containerComputerScreenshot(undefined, undefined, target),
|
|
21428
21514
|
});
|
|
21429
21515
|
}
|
|
21516
|
+
// The Local VM's live desktop for a phone, granted only to the control
|
|
21517
|
+
// lease that holds this bot's computer right now. Two callers:
|
|
21518
|
+
//
|
|
21519
|
+
// The companion sidecar (a loopback caller) gets the VM's own noVNC
|
|
21520
|
+
// address, VNC password included, relays it to a paired phone the owner
|
|
21521
|
+
// has allowed computer access, keeps re-checking the lease with
|
|
21522
|
+
// `action: "check"`, and cuts the relay when it no longer holds.
|
|
21523
|
+
//
|
|
21524
|
+
// A phone paired with this server directly (`openmausbot serve`, no
|
|
21525
|
+
// sidecar) never sees that address. It gets this server's own
|
|
21526
|
+
// authenticated desktop proxy, bound to its lease: the proxy re-checks
|
|
21527
|
+
// the lease and the session every few seconds and closes the socket when
|
|
21528
|
+
// either lapses. Reaching here at all took the admin scope, which a
|
|
21529
|
+
// chat-only pairing (`openmausbot pair --client`) does not have, so
|
|
21530
|
+
// computer access is the same explicit choice the owner's browser makes.
|
|
21531
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/local-computer\/join$/);
|
|
21532
|
+
if (m && method === "POST") {
|
|
21533
|
+
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
21534
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
21535
|
+
}
|
|
21536
|
+
const bot = computerPreviewBot(m[1], url);
|
|
21537
|
+
if (!bot)
|
|
21538
|
+
return json(res, 404, { error: "no such bot" });
|
|
21539
|
+
const threadId = url.searchParams.has("threadId") ? bot.threadId : undefined;
|
|
21540
|
+
if (threadId && await computerPreviewSurface(bot, threadId) !== "vm") {
|
|
21541
|
+
return json(res, 409, { error: "This conversation is not using the Local VM" });
|
|
21542
|
+
}
|
|
21543
|
+
// A bot's human-control hold does not reserve a shared desktop or pool
|
|
21544
|
+
// seat. Another bot could drive it while the phone is holding this bot.
|
|
21545
|
+
if (localVmMode(cfg) !== "per-bot") {
|
|
21546
|
+
return json(res, 409, { error: "Phone control requires a per-bot Local VM. Select per-bot mode in Settings → Computers." });
|
|
21547
|
+
}
|
|
21548
|
+
const lease = controlLeaseIdSchema.safeParse(url.searchParams.get("controlLeaseId") ?? undefined);
|
|
21549
|
+
if (!lease.success)
|
|
21550
|
+
return json(res, 400, { error: "controlLeaseId is required" });
|
|
21551
|
+
const controlBot = store.bot(bot.id);
|
|
21552
|
+
if (!controlBot || !computerControl.ownsLease(botComputerControlKey(controlBot), lease.data)) {
|
|
21553
|
+
return json(res, 409, { error: "Take control of this computer first" });
|
|
21554
|
+
}
|
|
21555
|
+
const target = localVmTargetForStatus(bot.id, threadId);
|
|
21556
|
+
const status = await containerComputerStatus(undefined, undefined, target);
|
|
21557
|
+
if (!status.ready || !status.managed || status.container !== "running" || status.network !== "loopback"
|
|
21558
|
+
|| !status.viewer_url) {
|
|
21559
|
+
return json(res, 409, { error: status.problem ?? "The Local VM is not ready" });
|
|
21560
|
+
}
|
|
21561
|
+
localVmIdleFor(target).touch();
|
|
21562
|
+
res.setHeader("cache-control", "private, no-store");
|
|
21563
|
+
if (auth.kind === "loopback")
|
|
21564
|
+
return json(res, 200, { joinUrl: status.viewer_url });
|
|
21565
|
+
const socket = new URLSearchParams({ botId: bot.id, ...(threadId ? { threadId } : {}), controlLeaseId: lease.data });
|
|
21566
|
+
return json(res, 200, {
|
|
21567
|
+
socketPath: `api/desktop-viewer/${viewerTargetId(target)}/websockify?${socket}`,
|
|
21568
|
+
password: new URLSearchParams(new URL(status.viewer_url).hash.slice(1)).get("password"),
|
|
21569
|
+
});
|
|
21570
|
+
}
|
|
21430
21571
|
// identity handshake for the packaged app's port fallback: the forked
|
|
21431
21572
|
// child proves it is OURS by echoing its pid (a stray dev server has
|
|
21432
21573
|
// the same API shape but a different pid)
|
|
@@ -21566,7 +21707,7 @@ const handleRequest = async (req, res) => {
|
|
|
21566
21707
|
return json(res, 400, { error: `provider must be one of ${PROVIDER_KEY_KINDS.join(", ")}` });
|
|
21567
21708
|
}
|
|
21568
21709
|
const kind = provider;
|
|
21569
|
-
const saved = { anthropic: cfg.anthropic, openai: cfg.openai, openrouter: cfg.openrouter, openaiCompat: cfg.openaiCompat, mistral: cfg.mistral, xai: cfg.xai }[kind];
|
|
21710
|
+
const saved = { anthropic: cfg.anthropic, openai: cfg.openai, openrouter: cfg.openrouter, openaiCompat: cfg.openaiCompat, mistral: cfg.mistral, cerebras: cfg.cerebras, xai: cfg.xai }[kind];
|
|
21570
21711
|
if (body?.key !== undefined && typeof body.key !== "string") {
|
|
21571
21712
|
return json(res, 400, { error: "key must be a string" });
|
|
21572
21713
|
}
|
|
@@ -21907,8 +22048,8 @@ const handleRequest = async (req, res) => {
|
|
|
21907
22048
|
return json(res, 400, { error: "Account settings are currently available for Claude only." });
|
|
21908
22049
|
}
|
|
21909
22050
|
if (body.tools !== undefined) {
|
|
21910
|
-
if (!["openai-compat", "grok", "minimax", "mistral"].includes(entry.driver)) {
|
|
21911
|
-
return json(res, 400, { error: "The tools setting is available for OpenAI-compatible, Grok API, MiniMax API and
|
|
22051
|
+
if (!["openai-compat", "grok", "minimax", "mistral", "cerebras"].includes(entry.driver)) {
|
|
22052
|
+
return json(res, 400, { error: "The tools setting is available for OpenAI-compatible, Grok API, MiniMax API, Mistral API and Cerebras API instances only." });
|
|
21912
22053
|
}
|
|
21913
22054
|
entry.config = { ...entry.config, tools: body.tools };
|
|
21914
22055
|
}
|
|
@@ -22187,7 +22328,7 @@ const handleRequest = async (req, res) => {
|
|
|
22187
22328
|
}
|
|
22188
22329
|
if ((method === "PUT" || method === "PATCH") && path === "/api/config") {
|
|
22189
22330
|
const body = await readBody(req);
|
|
22190
|
-
if (hostedModels && ["instances", "anthropic", "openai", "openrouter", "openaiCompat", "xai", "mistral", "opencodeGo"].some(key => Object.hasOwn(body, key)))
|
|
22331
|
+
if (hostedModels && ["instances", "anthropic", "openai", "openrouter", "openaiCompat", "xai", "mistral", "cerebras", "opencodeGo"].some(key => Object.hasOwn(body, key)))
|
|
22191
22332
|
return json(res, 403, { error: HOSTED_PROVIDER_SETTINGS_ERROR });
|
|
22192
22333
|
const patch = parseConfigPatch(body);
|
|
22193
22334
|
// A Cloud home is personal: nobody is invited to sign in to it.
|
|
@@ -23065,13 +23206,19 @@ const handleRequest = async (req, res) => {
|
|
|
23065
23206
|
const result = computerControl.releaseLease(controlKey, controlLeaseId);
|
|
23066
23207
|
return json(res, 200, { ...result.snapshot, released: result.released });
|
|
23067
23208
|
}
|
|
23209
|
+
// Read-only: does this lease still hold the wheel? The companion
|
|
23210
|
+
// sidecar asks this while it relays a phone's live Local VM desktop,
|
|
23211
|
+
// and cuts the relay the moment the answer is no.
|
|
23212
|
+
if (action === "check" && controlLeaseId) {
|
|
23213
|
+
return json(res, 200, { ...computerControl.snapshot(controlKey), owned: computerControl.ownsLease(controlKey, controlLeaseId) });
|
|
23214
|
+
}
|
|
23068
23215
|
if (action === "take")
|
|
23069
23216
|
return json(res, 200, computerControl.take(controlKey));
|
|
23070
23217
|
if (action === "release")
|
|
23071
23218
|
return json(res, 200, computerControl.release(controlKey));
|
|
23072
23219
|
if (action === "dismiss-help")
|
|
23073
23220
|
return json(res, 200, computerControl.dismissHelp(controlKey));
|
|
23074
|
-
return json(res, 400, { error: "action must be take, release, or dismiss-help" });
|
|
23221
|
+
return json(res, 400, { error: "action must be take, release, check, or dismiss-help" });
|
|
23075
23222
|
}
|
|
23076
23223
|
return json(res, 405, { error: "method not allowed" });
|
|
23077
23224
|
}
|
|
@@ -23083,7 +23230,13 @@ const handleRequest = async (req, res) => {
|
|
|
23083
23230
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
23084
23231
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
23085
23232
|
}
|
|
23086
|
-
|
|
23233
|
+
// A directly paired phone's Local VM desktop runs through this server's
|
|
23234
|
+
// own proxy under its lease on this bot; hand-back closes it here, ahead
|
|
23235
|
+
// of the lease re-check. The session's other viewers are left alone.
|
|
23236
|
+
const closedViewers = auth.kind === "session" ? desktopViewer.closeForOwner(auth.session.id, bot.id) : 0;
|
|
23237
|
+
if (bot.cloudBackend === "vps")
|
|
23238
|
+
return json(res, 200, vps.closeVpsDesktopTunnel(bot.id));
|
|
23239
|
+
return json(res, 200, { closed: closedViewers > 0 });
|
|
23087
23240
|
}
|
|
23088
23241
|
m = path.match(/^\/api\/bots\/([\w-]+)\/computer\/(provision|join|sleep|exec|screenshot|remove)$/);
|
|
23089
23242
|
if (m && method === "POST") {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
5
|
+
import { DATA_DIR } from "./config.js";
|
|
6
|
+
function recordPath(key, dataDir) {
|
|
7
|
+
return join(dataDir, "local-vm-stops", `${createHash("sha256").update(key).digest("hex")}.json`);
|
|
8
|
+
}
|
|
9
|
+
/** Record the runtime's actual finish timestamp. An external stop later gets
|
|
10
|
+
* a different timestamp and must not be misreported as OMB's idle shutdown. */
|
|
11
|
+
export function recordLocalVmIdleStop(key, stoppedAt, dataDir = DATA_DIR) {
|
|
12
|
+
if (!stoppedAt)
|
|
13
|
+
return;
|
|
14
|
+
mkdirSync(join(dataDir, "local-vm-stops"), { recursive: true, mode: 0o700 });
|
|
15
|
+
writeFileAtomic(recordPath(key, dataDir), JSON.stringify({ stoppedAt }), { mode: 0o600 });
|
|
16
|
+
}
|
|
17
|
+
export function localVmStopReason(key, status, dataDir = DATA_DIR) {
|
|
18
|
+
if (status.container !== "stopped" || !status.stopped_at)
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(recordPath(key, dataDir), "utf8")).stoppedAt === status.stopped_at ? "idle" : null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PROVIDER_KEY_KINDS = ["anthropic", "openai", "openrouter", "openaiCompat", "xai", "mistral"];
|
|
1
|
+
export const PROVIDER_KEY_KINDS = ["anthropic", "openai", "openrouter", "openaiCompat", "xai", "mistral", "cerebras"];
|
|
2
2
|
const DEFAULT_URLS = {
|
|
3
3
|
anthropic: "https://api.anthropic.com",
|
|
4
4
|
openai: "https://api.openai.com/v1",
|
|
@@ -6,6 +6,7 @@ const DEFAULT_URLS = {
|
|
|
6
6
|
openaiCompat: "https://openrouter.ai/api/v1",
|
|
7
7
|
xai: "https://api.x.ai/v1",
|
|
8
8
|
mistral: "https://api.mistral.ai/v1",
|
|
9
|
+
cerebras: "https://api.cerebras.ai/v1",
|
|
9
10
|
};
|
|
10
11
|
const MAX_MODELS = 5;
|
|
11
12
|
function isLoopback(hostname) {
|