openmausbot 0.1.90 → 0.1.91
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/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +18 -3
- package/dist-server/container-mcp.js +84 -32
- package/dist-server/drivers/agents-proxy.js +11 -0
- package/dist-server/index.js +25183 -23580
- package/dist-server/openmausbot.js +313 -159
- package/dist-server/pair-cli.js +313 -159
- package/dist-server/server/boat.js +66 -21
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/cloud-home-start.js +23 -5
- package/dist-server/server/cloud-home.js +25 -2
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/config.js +42 -4
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/drivers/agents-catalog.js +16 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/claude.js +1 -0
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +438 -102
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/store.js +17 -2
- package/dist-server/server/surface.js +13 -0
- package/dist-server/server/system-prompt.js +11 -0
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/vps-container-mcp.js +84 -32
- package/dist-server/workspace-backup.worker.js +113 -31
- package/package.json +1 -1
- package/dist/assets/channel-BCuyHjFV.js +0 -1
- package/dist/assets/index-CRvo-DS2.css +0 -1
- package/dist/assets/index-CWYGlA63.js +0 -588
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
|
@@ -15,7 +15,8 @@ import { selectReplay, DEFAULT_REBUILD_BYTES, MAX_SUMMARY_BYTES } from "./contex
|
|
|
15
15
|
import { draftSummary, foldPoint } from "./compaction-summary.js";
|
|
16
16
|
import { compactBudget, contextWindowFor, shouldCompact } from "./context-budget.js";
|
|
17
17
|
import { autoCompactWindow } from "./drivers/claude.js";
|
|
18
|
-
import { SharedComputers, sharedComputerOperation, sharedComputerRegistration } from "./shared-computers.js";
|
|
18
|
+
import { provenRequestPerson, SharedComputers, sharedComputerOperation, sharedComputerRegistration } from "./shared-computers.js";
|
|
19
|
+
import { cloudHomeLendingRefusal, createCloudRoutineAuthors } from "./cloud-lending.js";
|
|
19
20
|
import { SharedComputerControl } from "./shared-computer-control.js";
|
|
20
21
|
import { RoomHandoffs } from "./room-handoffs.js";
|
|
21
22
|
import { assertRequestTarget, guardedRequestPath, requestConflict, requestNeedsInput, requestSourceForCard } from "./guarded-requests.js";
|
|
@@ -60,7 +61,7 @@ import { canAccessTeam, canReachPeer, coordinatorSupervises, livePeerRoster, liv
|
|
|
60
61
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
61
62
|
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerExec, containerRuntimeStatus, localVmRecreatableOnDemand, localVmWorkspaceExists, perBotLocalVmTarget, poolLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, VM_WORKSPACE_GUEST, } from "./container-computer.js";
|
|
62
63
|
import { attachForTurn, saveBotAttachment } from "./bot-attachment.js";
|
|
63
|
-
import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, 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, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, } from "./config.js";
|
|
64
|
+
import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, 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, } from "./config.js";
|
|
64
65
|
import { sweepThreadEventLogs } from "./thread-retention.js";
|
|
65
66
|
import { ComputerControl } from "./computer-control.js";
|
|
66
67
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -103,7 +104,9 @@ import { ProviderRegistry } from "./harness/registry.js";
|
|
|
103
104
|
import { ManagedDesktopProviders } from "./managed-desktop.js";
|
|
104
105
|
import { computerKindForResource, ManagedDesktopPolicy } from "./managed-policy.js";
|
|
105
106
|
import { hostedModelPolicy, HOSTED_MODEL_POLICY_HEADER, HOSTED_PROVIDER_SETTINGS_ERROR } from "./hosted-models.js";
|
|
106
|
-
import { CLOUD_IGNORED_KEYS, CLOUD_PAIRING_PATH, cloudHomeConfiguration, createCloudPairing, readSignedBody } from "./cloud-home.js";
|
|
107
|
+
import { boatNotConfiguredMessage, CLOUD_HOME_RESTART_EXIT_CODE, CLOUD_IGNORED_KEYS, CLOUD_PAIRING_PATH, cloudHomeConfiguration, cloudHomeOffersPlace, cloudHomePlaceRefusal, createCloudPairing, readSignedBody, } from "./cloud-home.js";
|
|
108
|
+
import { createCloudMoveRoutes } from "./cloud-move-http.js";
|
|
109
|
+
import { holdIncludedServices } from "./included-services.js";
|
|
107
110
|
import { selectDefaultModelSelection, withNewBotEffort } from "./default-model-selection.js";
|
|
108
111
|
import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, peerApprovalFailure, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
|
|
109
112
|
import { peerDeliveryReceipt } from "./peer-delivery.js";
|
|
@@ -111,6 +114,8 @@ import { peerProvenanceNote, withPeerProvenance } from "./peer-provenance.js";
|
|
|
111
114
|
import { decideRoomPost, emptyRoomPostBudget } from "./room-post-budget.js";
|
|
112
115
|
import { isProjectEmoji, mentionedBots, parseConnectorTools, roomResponders, sectionKey, Store, titleFromLlm, toWireTask, } from "./store.js";
|
|
113
116
|
import * as tts from "./tts/index.js";
|
|
117
|
+
import { createDecider, deciderIncludedHere, deciderReady, deciderSavePatch, describeDecider } from "./decider/index.js";
|
|
118
|
+
import { decideRoomResponder } from "./decider/room-routing.js";
|
|
114
119
|
import { narrateTool, toUtterances } from "./tts/speech-text.js";
|
|
115
120
|
import { buildRecoveryText, buildTurnContext, engineIsFresh, NATIVELY_REPLAYING_DRIVER_KINDS, peerMessageText } from "./turn-context.js";
|
|
116
121
|
import { Handoffs, handedStateUsable, recordHanded, renderUnseen, sessionStart, unseenMessages, withUnseenMessages } from "./delta-context.js";
|
|
@@ -129,7 +134,7 @@ import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefault
|
|
|
129
134
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
130
135
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
131
136
|
import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-folder.js";
|
|
132
|
-
import { buildSystemPrompt, userProfileSystemPrompt, computerPrompt, composioSystemPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, resolveComputerPromptKind, } from "./system-prompt.js";
|
|
137
|
+
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";
|
|
133
138
|
import { readCuaConnection, readCuaUnavailableReason, gatedLocalComputer } from "./local-computer.js";
|
|
134
139
|
import { discoverExistingPerBotLocalVms, discoverExistingPoolLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
135
140
|
import { LocalVmIdleTimer } from "./local-vm-idle.js";
|
|
@@ -208,6 +213,7 @@ import { ROUTES, dispatchRoutes } from "./routes/table.js";
|
|
|
208
213
|
import { createHostedSlackRoutes } from "./routes/hosted-slack.js";
|
|
209
214
|
import { createBotPresetRoutes } from "./routes/bot-presets.js";
|
|
210
215
|
import { createBotMemoryRoutes } from "./routes/bot-memory.js";
|
|
216
|
+
import { createDeciderRoutes } from "./routes/decider.js";
|
|
211
217
|
const PORT = Number(process.env.OMB_PORT || process.env.OGB_PORT || 8799);
|
|
212
218
|
const WEBHOOK_PORT = Number(process.env.OMB_WEBHOOK_PORT || PORT + 1);
|
|
213
219
|
// Behind a proxy or tunnel, the base URL senders should use (docs/self-hosting.md).
|
|
@@ -274,6 +280,17 @@ const sharedComputers = new SharedComputers(id => sessions.isLive(id));
|
|
|
274
280
|
// OMB Cloud Pro home machine (server/cloud-home.ts, docs/cloud-pro.md). A
|
|
275
281
|
// partial or invalid boot contract stops the server here, before it serves.
|
|
276
282
|
const CLOUD_HOME = cloudHomeConfiguration();
|
|
283
|
+
/** Lending a computer to this server (the shared-computer routes, the two
|
|
284
|
+
* agent tools, the advertised capability): the maintainer flag anywhere, and
|
|
285
|
+
* always on an OMB Cloud home, where it is the person's own Mac lent to their
|
|
286
|
+
* own Cloud (docs/cloud-pro.md, "Let my Cloud use this Mac"). */
|
|
287
|
+
const lendingEnabled = () => sharedComputersEnabled(cfg) || CLOUD_HOME !== null;
|
|
288
|
+
/** On a Cloud home every lent computer and every turn belong to one person. */
|
|
289
|
+
const CLOUD_HOME_LENDER = "cloud-home";
|
|
290
|
+
/** Routines whose instructions the Cloud home's owner wrote, and manual runs
|
|
291
|
+
* the owner started: the only routine turns that may use a lent Mac. */
|
|
292
|
+
const cloudRoutineAuthors = CLOUD_HOME ? createCloudRoutineAuthors(join(DATA_DIR, "lending-routines.json")) : null;
|
|
293
|
+
const ownerStartedRoutineRuns = new Set();
|
|
277
294
|
const cloudPairing = CLOUD_HOME ? createCloudPairing({ secret: CLOUD_HOME.bootstrapSecret, sessions }) : null;
|
|
278
295
|
if (CLOUD_HOME) {
|
|
279
296
|
// The signing secret is held in memory from here on, and a platform
|
|
@@ -286,6 +303,9 @@ if (CLOUD_HOME) {
|
|
|
286
303
|
for (const warning of CLOUD_HOME.warnings)
|
|
287
304
|
console.warn(`cloud home: ${warning}`);
|
|
288
305
|
}
|
|
306
|
+
// Cloud Pro's included Boat, voice and decision relay tokens, when the Admin
|
|
307
|
+
// set them: held in memory from here on, like the signing secret.
|
|
308
|
+
holdIncludedServices();
|
|
289
309
|
// Who each thread is for, when a signed-in person can be named (server-private).
|
|
290
310
|
const threadStarters = new ThreadStarters(join(DATA_DIR, "thread-starters.json"));
|
|
291
311
|
const commandAllowlist = new CommandAllowlistStore(join(DATA_DIR, "command-allowlist.json"));
|
|
@@ -341,6 +361,9 @@ bindThreadLogCapProvider(() => threadEventLogMaxBytes(cfg));
|
|
|
341
361
|
// The decision log keeps month files for at least this many days. Read per
|
|
342
362
|
// prune, so a Settings change applies at the next one without a restart.
|
|
343
363
|
bindDecisionRetention(() => decisionRetentionDays(cfg.decisions?.retentionDays));
|
|
364
|
+
// The decision model (server/decider): reads cfg per call, so a Settings
|
|
365
|
+
// save applies to the next decision. It never throws into a turn.
|
|
366
|
+
const decider = createDecider({ config: () => cfg, dataDir: DATA_DIR });
|
|
344
367
|
const customDomainVerifier = createCustomDomainVerifier({ environmentId: ENVIRONMENT_ID });
|
|
345
368
|
// "Sign in with your email" on /pair: the allow-list is read per call so a
|
|
346
369
|
// Settings change or an env bootstrap applies without a restart.
|
|
@@ -438,6 +461,74 @@ function threadPersonKey(threadId) {
|
|
|
438
461
|
: thread.findLast((message) => message.role === "user");
|
|
439
462
|
return linePersonKey(request) ?? threadStarters.get(threadId);
|
|
440
463
|
}
|
|
464
|
+
/** The person whose lent computers this bot turn may use, or null.
|
|
465
|
+
* Fail closed: only the person whose own message the harness proved started
|
|
466
|
+
* this exact turn generation (continuations of that same request keep it).
|
|
467
|
+
* A routine, a webhook, a room, a bot's delegated or peer turn, the owner on
|
|
468
|
+
* this machine (who lends nothing to itself) and anything unprovable act for
|
|
469
|
+
* nobody here. Deliberately not the usage ledger's trigger, which defaults to
|
|
470
|
+
* the owner. */
|
|
471
|
+
function sharedComputerPrincipal(capability) {
|
|
472
|
+
// A Cloud home lends the owner's Mac only to turns that provably act for
|
|
473
|
+
// the owner (server/cloud-lending.ts): their own conversation from one of
|
|
474
|
+
// their devices, or a routine they wrote. Never a guest, a webhook, a room
|
|
475
|
+
// or a line someone else slipped in.
|
|
476
|
+
if (CLOUD_HOME)
|
|
477
|
+
return cloudHomeLendingRefusal(cloudLendingTurn(capability)) === null ? CLOUD_HOME_LENDER : null;
|
|
478
|
+
return provenRequestPerson(directRequestOwners.get(capability.threadId), capability.generation, (messageId) => linePersonKey(store.messagesFor(capability.threadId).find((message) => message.id === messageId)));
|
|
479
|
+
}
|
|
480
|
+
/** What the lending rule needs to know about one bot turn on a Cloud home. */
|
|
481
|
+
function cloudLendingTurn(capability) {
|
|
482
|
+
return {
|
|
483
|
+
request: directRequestOwners.get(capability.threadId),
|
|
484
|
+
generation: capability.generation,
|
|
485
|
+
thread: store.messagesFor(capability.threadId),
|
|
486
|
+
ownerPerson: cloudOwnerPerson,
|
|
487
|
+
cardAnswerer: cloudCardAnswerer,
|
|
488
|
+
routineRun: () => {
|
|
489
|
+
const run = activeRoutineRunForThread(capability.threadId);
|
|
490
|
+
if (!run || run.threadId !== capability.threadId || store.taskByThread(capability.botId, capability.threadId)?.routineRunId !== run.id)
|
|
491
|
+
return null;
|
|
492
|
+
return {
|
|
493
|
+
triggerSource: run.triggerSource ?? (run.manual ? "manual" : "schedule"),
|
|
494
|
+
ownerStarted: ownerStartedRoutineRuns.has(run.id),
|
|
495
|
+
// What the run snapshotted, on the schedule its routine has now.
|
|
496
|
+
ownerAuthored: ((routine) => Boolean(routine) && cloudRoutineAuthors?.authored(run.routineId, {
|
|
497
|
+
...routine, prompt: run.prompt, target: run.target, botId: run.botId, groupId: run.groupId, attachments: run.attachments, runOn: run.runOn,
|
|
498
|
+
}) === true)(routines?.listRoutines().find((candidate) => candidate.id === run.routineId)),
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/** What a bot on a Cloud home is told when its conversation cannot use the
|
|
504
|
+
* owner's lent Mac because someone else wrote in it. The bot relays it. */
|
|
505
|
+
const LENDING_SOMEONE_ELSE = "Someone else wrote in this conversation, so it can't use your Mac. Start a new conversation to use it.";
|
|
506
|
+
/** Whose lent computers a status reader may see: on a Cloud home, only the
|
|
507
|
+
* owner's own devices (admin sessions) see the lent Mac, never a guest or a
|
|
508
|
+
* local process; elsewhere, the person behind the asking session. */
|
|
509
|
+
function sessionLendingPrincipal(auth) {
|
|
510
|
+
if (CLOUD_HOME)
|
|
511
|
+
return cloudOwnerSession(auth) ? CLOUD_HOME_LENDER : null;
|
|
512
|
+
return auth.kind === "session" ? personKey(auth.session) : null;
|
|
513
|
+
}
|
|
514
|
+
/** On a Cloud home: a request from one of the owner's own devices. The
|
|
515
|
+
* Admin's signed pairing gives the desktop an admin session; a guest the
|
|
516
|
+
* owner pairs with chat-only access has client scope. Loopback is not the
|
|
517
|
+
* owner here: nothing the person does reaches a Cloud home that way, only
|
|
518
|
+
* processes on the machine (a bot's shell) do. */
|
|
519
|
+
function cloudOwnerSession(auth) {
|
|
520
|
+
return auth.kind === "session" && auth.scopes.includes("admin");
|
|
521
|
+
}
|
|
522
|
+
/** On a Cloud home: whether a message's person key is one of the owner's own
|
|
523
|
+
* devices right now (a live admin session with that key). */
|
|
524
|
+
function cloudOwnerPerson(person) {
|
|
525
|
+
return Boolean(person) && sessions.list().some((session) => session.scopes.includes("admin") && personKey(session) === person);
|
|
526
|
+
}
|
|
527
|
+
/** On a Cloud home, a session that is not one of the owner's own devices may
|
|
528
|
+
* not answer a card of any kind. */
|
|
529
|
+
function cloudCardRefusal(auth) {
|
|
530
|
+
return auth.scopes.includes("admin") ? null : "Only the owner of this Cloud can answer this card.";
|
|
531
|
+
}
|
|
441
532
|
/** Whose session may answer a card. The provider CLI's own approval modes and
|
|
442
533
|
* the harness's proposals stay exactly as they are; this adds no card, gate
|
|
443
534
|
* or prompt, it only decides whose answer to an existing card counts.
|
|
@@ -454,6 +545,11 @@ function cardAnswerRefusal(auth, threadId, requestId, behavior) {
|
|
|
454
545
|
? "A local service can only decline this request. Approve or answer it in OpenMausBot while signed in."
|
|
455
546
|
: null;
|
|
456
547
|
}
|
|
548
|
+
// A Cloud home is one person's: a device paired with chat-only access (a
|
|
549
|
+
// guest) may read along but never answer, or its words would reach the
|
|
550
|
+
// owner's turn, and through it a lent Mac (docs/cloud-pro.md).
|
|
551
|
+
if (CLOUD_HOME)
|
|
552
|
+
return cloudCardRefusal(auth);
|
|
457
553
|
if (auth.scopes.includes("admin") || !sharedMembership())
|
|
458
554
|
return null;
|
|
459
555
|
const known = [threadStarters.get(threadId), cardRequesterKey(threadId, requestId)].filter((person) => Boolean(person));
|
|
@@ -497,7 +593,19 @@ function decisionActorFor(auth) {
|
|
|
497
593
|
function cardAnswererFor(auth) {
|
|
498
594
|
if (auth.kind === "loopback")
|
|
499
595
|
return auth.trust === "service" ? { kind: "worker" } : { kind: "loopback" };
|
|
500
|
-
return { kind: "session", name: (auth.session.email ?? auth.session.label ?? "").trim() || "Signed-in user"
|
|
596
|
+
return { kind: "session", name: (auth.session.email ?? auth.session.label ?? "").trim() || "Signed-in user",
|
|
597
|
+
...(CLOUD_HOME ? { person: personKey(auth.session) } : {}) };
|
|
598
|
+
}
|
|
599
|
+
/** On a Cloud home, who is answering a card right now, by request id: the
|
|
600
|
+
* answer reaches the turn before `answeredBy` is written, and a lending turn
|
|
601
|
+
* must know whose words it just received (server/cloud-lending.ts). */
|
|
602
|
+
const cloudCardAnswersInFlight = new Map();
|
|
603
|
+
/** The person who answered a card, as far as lending on a Cloud home is
|
|
604
|
+
* concerned: the recorded answerer, else whoever is answering it now. */
|
|
605
|
+
function cloudCardAnswerer(card) {
|
|
606
|
+
if (card.answeredBy)
|
|
607
|
+
return card.answeredBy.kind === "session" ? card.answeredBy.person : undefined;
|
|
608
|
+
return card.requestId ? cloudCardAnswersInFlight.get(card.requestId) : undefined;
|
|
501
609
|
}
|
|
502
610
|
/** Answer a card as `auth`: the decision rows written meanwhile name the
|
|
503
611
|
* answerer, and a card this answer settled records who settled it. A card
|
|
@@ -507,6 +615,8 @@ async function answeringCardAs(auth, threadId, requestId, work) {
|
|
|
507
615
|
const card = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId)?.card;
|
|
508
616
|
return Boolean(card && !card.answered && !card.dismissed && !card.expired);
|
|
509
617
|
})();
|
|
618
|
+
if (CLOUD_HOME && open)
|
|
619
|
+
cloudCardAnswersInFlight.set(requestId, auth.kind === "session" ? personKey(auth.session) : undefined);
|
|
510
620
|
try {
|
|
511
621
|
await withDecisionActor(decisionActorFor(auth), work);
|
|
512
622
|
}
|
|
@@ -516,6 +626,8 @@ async function answeringCardAs(auth, threadId, requestId, work) {
|
|
|
516
626
|
if (message && card && !card.answeredBy && card.answered !== "unavailable" && (card.answered || card.dismissed)) {
|
|
517
627
|
store.patchMessage(threadId, message.id, { card: { ...card, answeredBy: cardAnswererFor(auth) } });
|
|
518
628
|
}
|
|
629
|
+
if (CLOUD_HOME && open)
|
|
630
|
+
cloudCardAnswersInFlight.delete(requestId);
|
|
519
631
|
}
|
|
520
632
|
}
|
|
521
633
|
// ── bot visibility (server/bot-visibility.ts) ──────────────────────────
|
|
@@ -771,7 +883,7 @@ function adminAuditPlan(method, path) {
|
|
|
771
883
|
if (path.startsWith("/api/internal/") || path.startsWith("/api/testing/"))
|
|
772
884
|
return null;
|
|
773
885
|
const plan = {
|
|
774
|
-
config: requiredScope(method, path, { sharedComputers:
|
|
886
|
+
config: requiredScope(method, path, { sharedComputers: lendingEnabled() }) === "admin",
|
|
775
887
|
createsBots: method === "POST" && (path === "/api/bots" || path === "/api/teams/import" || path === "/api/org-library/add"),
|
|
776
888
|
createsWebhook: method === "POST" && path === "/api/webhooks",
|
|
777
889
|
pairing: method === "POST" && path === "/api/auth/pairing",
|
|
@@ -1068,6 +1180,18 @@ const companyRuntimeStarted = new Promise(resolve => { companyRuntimeReady = res
|
|
|
1068
1180
|
/** The enrolled organisation's desktop policy: in memory only, read-only, and
|
|
1069
1181
|
* inert (null) unless Electron's enrolled parent sends one. */
|
|
1070
1182
|
const managedPolicy = new ManagedDesktopPolicy({ onChange: () => broadcast({ kind: "config", ...configStatus() }) });
|
|
1183
|
+
/** A computer kind this server will not use, refused before anything is
|
|
1184
|
+
* prepared: a Cloud home never offers this computer or a Local VM
|
|
1185
|
+
* (cloud-home.ts), and an enrolled organisation may disallow any kind. */
|
|
1186
|
+
function computerPlaceRefusal(kind) {
|
|
1187
|
+
const cloudHome = CLOUD_HOME ? cloudHomePlaceRefusal(kind === "thisComputer" ? "local" : kind === "localVm" ? "vm" : "cloud") : undefined;
|
|
1188
|
+
if (cloudHome)
|
|
1189
|
+
return { message: cloudHome, code: "cloud_home" };
|
|
1190
|
+
const managed = managedPolicy.computerRefusal(kind);
|
|
1191
|
+
return managed ? { message: managed, code: "managed_policy" } : undefined;
|
|
1192
|
+
}
|
|
1193
|
+
/** Cloud chosen with no Boat account: a Cloud home suggests the browser, not a Local VM. */
|
|
1194
|
+
const BOAT_NOT_CONFIGURED = boatNotConfiguredMessage(Boolean(CLOUD_HOME));
|
|
1071
1195
|
/** Why the organisation refuses this instance for bots, or undefined. */
|
|
1072
1196
|
function policyModelRefusal(instance) {
|
|
1073
1197
|
const engine = BUILT_IN_DRIVERS.find(driver => driver.driverKind === instance.driverKind)?.metadata.displayName;
|
|
@@ -1352,7 +1476,10 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation, r
|
|
|
1352
1476
|
OMB_SKILL_AUTHORING_ENABLED: skillAuthoring ? "1" : "0",
|
|
1353
1477
|
// The shared-computer tools are advertised only while the workspace
|
|
1354
1478
|
// gate is on; the routes behind them refuse regardless.
|
|
1355
|
-
OMB_SHARED_COMPUTERS_ENABLED:
|
|
1479
|
+
OMB_SHARED_COMPUTERS_ENABLED: lendingEnabled() ? "1" : "0",
|
|
1480
|
+
// A Cloud home offers no this computer and no Local VM (cloud-home.ts),
|
|
1481
|
+
// so select_computer lists neither and vm_exec is not shown.
|
|
1482
|
+
OMB_CLOUD_HOME: CLOUD_HOME ? "1" : "0",
|
|
1356
1483
|
// Same capability rule for voice: the tool is offered only when this
|
|
1357
1484
|
// bot can actually speak, and the route re-checks on every call.
|
|
1358
1485
|
OMB_VOICE_NOTES: (() => {
|
|
@@ -1603,11 +1730,11 @@ function releaseTurnResources(owner) {
|
|
|
1603
1730
|
}
|
|
1604
1731
|
}
|
|
1605
1732
|
async function bindTurnComputer(owner, resource, exclusive = false) {
|
|
1606
|
-
// Every computer claim passes here: a kind the organisation disallows
|
|
1607
|
-
// refused at claim time, whichever path selected it.
|
|
1608
|
-
const kind = computerKindForResource(resource), refusal = kind &&
|
|
1733
|
+
// Every computer claim passes here: a kind the organisation disallows, or a
|
|
1734
|
+
// Cloud home never offers, is refused at claim time, whichever path selected it.
|
|
1735
|
+
const kind = computerKindForResource(resource), refusal = kind && computerPlaceRefusal(kind);
|
|
1609
1736
|
if (refusal)
|
|
1610
|
-
throw Object.assign(new Error(refusal), { code:
|
|
1737
|
+
throw Object.assign(new Error(refusal.message), { code: refusal.code });
|
|
1611
1738
|
const active = () => activeInternalGenerationByThread.get(owner.threadId) === owner.generation &&
|
|
1612
1739
|
turnResourceOwners.get(owner.threadId)?.generation === owner.generation;
|
|
1613
1740
|
let waitingMessage;
|
|
@@ -2464,6 +2591,13 @@ function checkedGroupResponder(value, memberIds) {
|
|
|
2464
2591
|
return { kind: "everyone" };
|
|
2465
2592
|
if (responder.kind === "mentions")
|
|
2466
2593
|
return { kind: "mentions" };
|
|
2594
|
+
if (responder.kind === "auto") {
|
|
2595
|
+
if (responder.fallbackBotId === undefined || responder.fallbackBotId === null)
|
|
2596
|
+
return { kind: "auto" };
|
|
2597
|
+
return typeof responder.fallbackBotId === "string" && memberIds.includes(responder.fallbackBotId)
|
|
2598
|
+
? { kind: "auto", fallbackBotId: responder.fallbackBotId }
|
|
2599
|
+
: null;
|
|
2600
|
+
}
|
|
2467
2601
|
if (responder.kind === "member" &&
|
|
2468
2602
|
typeof responder.botId === "string" &&
|
|
2469
2603
|
memberIds.includes(responder.botId)) {
|
|
@@ -2619,7 +2753,8 @@ function previewSystemPrompt(bot) {
|
|
|
2619
2753
|
{ id: "plan", label: "Surface", text: previewPlan.computer === undefined ? "" : surfacePrompt({
|
|
2620
2754
|
computer: previewPlan.computer && previewPlan.computer !== "off" && computerPromptKind ? previewPlan.computer : null,
|
|
2621
2755
|
browser: previewPlan.computer === undefined ? false : previewPlan.browser,
|
|
2622
|
-
}, { note: previewPlan.note }) },
|
|
2756
|
+
}, { note: previewPlan.note, cloudHome: Boolean(CLOUD_HOME) }) },
|
|
2757
|
+
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(agentsMounted && lendingEnabled()) : "" },
|
|
2623
2758
|
{ id: "composio", label: "Connected apps", text: caps?.composioMcp && bot.composio !== false && composio.configured(cfg) ? composioSystemPrompt(bot.connectorTools) : "" },
|
|
2624
2759
|
{ id: "mcp", label: "MCP servers", text: caps?.customMcp ? customMcpPrompt(Object.keys(engineMcpServers(bot))) : "" },
|
|
2625
2760
|
{ id: "browser", label: "Browser", text: previewPlan.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
@@ -2701,6 +2836,7 @@ async function botOverview(bot) {
|
|
|
2701
2836
|
})),
|
|
2702
2837
|
engine,
|
|
2703
2838
|
browserEnabled: builtInBrowserEnabled(cfg),
|
|
2839
|
+
cloudHome: Boolean(CLOUD_HOME),
|
|
2704
2840
|
connectedApps,
|
|
2705
2841
|
sectionPeers,
|
|
2706
2842
|
timeZone,
|
|
@@ -3275,7 +3411,9 @@ function createChannel(value) {
|
|
|
3275
3411
|
throw Object.assign(new Error("invalid setup.defaultResponder"), { status: 400 });
|
|
3276
3412
|
setup = { bulletin: requested.bulletin, defaultResponder: responder, completed: true };
|
|
3277
3413
|
}
|
|
3278
|
-
|
|
3414
|
+
// A new room with no routing of its own starts on Auto while the decision
|
|
3415
|
+
// model is on for rooms; otherwise it gets today's default (first member).
|
|
3416
|
+
return store.createGroup(name, memberIds, false, section, setup ?? (deciderReady(cfg, "roomRouting") ? { defaultResponder: { kind: "auto" } } : undefined));
|
|
3279
3417
|
}
|
|
3280
3418
|
function updateChannel(groupId, value) {
|
|
3281
3419
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -4465,6 +4603,10 @@ function notify(notification) {
|
|
|
4465
4603
|
// Group threads: the fold needs to know WHO is talking — the turn engine
|
|
4466
4604
|
// records the active member here before dispatching its turn.
|
|
4467
4605
|
const groupSpeakers = new Map();
|
|
4606
|
+
/** Auto-room rounds the decision model routed, by thread: the named
|
|
4607
|
+
* speaker's next reply line carries `routedBy` (stamped in the runtime fold
|
|
4608
|
+
* below), so the room can say "Picked by Jev". Only a routed round sets it. */
|
|
4609
|
+
const routedRoomReplies = new Map();
|
|
4468
4610
|
// The latest running token totals for the turn in flight on each thread.
|
|
4469
4611
|
// Providers report cumulative-within-turn numbers; the final value is folded
|
|
4470
4612
|
// into the task's tally when the turn settles.
|
|
@@ -5199,7 +5341,7 @@ async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
|
5199
5341
|
!turnResources.owns(`computer:box:${machine.id}`, owner))
|
|
5200
5342
|
throw new Error("This computer turn ended while its machine was starting");
|
|
5201
5343
|
return {
|
|
5202
|
-
integration: { kind: "box", boxId: machine.id, token: cfg
|
|
5344
|
+
integration: { kind: "box", boxId: machine.id, token: boat.boatAccount(cfg)?.token ?? "", control: controlIntegration(botId, owner.threadId, owner.generation) },
|
|
5203
5345
|
capture: () => boat.screenshotBoat(cfg, ownerId, machine.id),
|
|
5204
5346
|
};
|
|
5205
5347
|
}
|
|
@@ -5305,7 +5447,7 @@ async function attachBotBoat(bot, owner, opts) {
|
|
|
5305
5447
|
return {
|
|
5306
5448
|
capture: () => boat.screenshotBoat(cfg, bot.id, machine.id),
|
|
5307
5449
|
integration: opts.canMount
|
|
5308
|
-
? { kind: "box", boxId: machine.id, token: cfg
|
|
5450
|
+
? { kind: "box", boxId: machine.id, token: boat.boatAccount(cfg)?.token ?? "", control: controlIntegration(bot.id, owner.threadId, owner.generation) }
|
|
5309
5451
|
: null,
|
|
5310
5452
|
};
|
|
5311
5453
|
}
|
|
@@ -5418,12 +5560,13 @@ async function computerPreviewSurface(bot, threadId) {
|
|
|
5418
5560
|
return "cloud";
|
|
5419
5561
|
}
|
|
5420
5562
|
const target = localVmTargetForStatus(bot.id, threadId);
|
|
5421
|
-
|
|
5563
|
+
// A Cloud home has neither place (cloud-home.ts), so Auto never shows one.
|
|
5564
|
+
if (!CLOUD_HOME && instance?.adapter.capabilities.computerMcp && localVmSeen.has(target.key)) {
|
|
5422
5565
|
const vm = await containerComputerStatus(undefined, undefined, target).catch(() => null);
|
|
5423
5566
|
if (vm && autoLocalVmAttachable(vm))
|
|
5424
5567
|
return "vm";
|
|
5425
5568
|
}
|
|
5426
|
-
if (shouldMountLocalComputer({ requested: undefined, hostPlatform: process.platform,
|
|
5569
|
+
if (!CLOUD_HOME && shouldMountLocalComputer({ requested: undefined, hostPlatform: process.platform,
|
|
5427
5570
|
providerSupportsLocal: instance?.adapter.capabilities.localComputerMcp === true }) && readCuaConnection())
|
|
5428
5571
|
return "local";
|
|
5429
5572
|
if (bot.cloudBackend === "vps")
|
|
@@ -5431,16 +5574,23 @@ async function computerPreviewSurface(bot, threadId) {
|
|
|
5431
5574
|
return plan.browser ? "browser" : "off";
|
|
5432
5575
|
}
|
|
5433
5576
|
/** Discovery is read-only. Starting or creating a configured computer is
|
|
5434
|
-
* deferred until a chat tool selects it and the old turn releases its tools.
|
|
5577
|
+
* deferred until a chat tool selects it and the old turn releases its tools.
|
|
5578
|
+
* A Cloud home lists only the places it has (cloud-home.ts), so a bot never
|
|
5579
|
+
* tells the person to set up this computer or a Local VM there. */
|
|
5435
5580
|
async function selectableComputers(bot) {
|
|
5436
5581
|
const caps = registry.get(bot.modelSelection.instanceId)?.adapter.capabilities;
|
|
5437
5582
|
const off = bot.computer === "off";
|
|
5438
5583
|
const localEngine = caps?.remoteAgent !== true;
|
|
5439
|
-
|
|
5584
|
+
const surfaces = ["cloud", "vm", "local", "browser"].filter(surface => !CLOUD_HOME || cloudHomeOffersPlace(surface));
|
|
5585
|
+
return Promise.all(surfaces.map(async (surface) => {
|
|
5440
5586
|
let ready = false;
|
|
5441
5587
|
let canStart = false;
|
|
5442
5588
|
let canCreate = false;
|
|
5443
|
-
|
|
5589
|
+
// On a Cloud home "this computer" reads as the person's own; name the
|
|
5590
|
+
// place the reason is about.
|
|
5591
|
+
let reason = CLOUD_HOME && surface === "cloud"
|
|
5592
|
+
? "The cloud computer is not set up or running. Open the Computer panel to set it up."
|
|
5593
|
+
: "This computer is not configured or running. Open the Computer panel to set it up.";
|
|
5444
5594
|
try {
|
|
5445
5595
|
if (off)
|
|
5446
5596
|
reason = "Computer access is Off in this bot's settings.";
|
|
@@ -5826,7 +5976,12 @@ bus.subscribe((event) => {
|
|
|
5826
5976
|
const owner = bot ? directRequestOwners.get(event.threadId) : undefined;
|
|
5827
5977
|
const proven = owner?.messageId && !owner.stopped && owner.generation === directTurnGenerationByThread.get(event.threadId) &&
|
|
5828
5978
|
owner.turnId && (!event.turnId || event.turnId === owner.turnId);
|
|
5829
|
-
|
|
5979
|
+
// One line per routed reply: the routed speaker's first text of its turn.
|
|
5980
|
+
const routed = group && m.role === "bot" && m.kind === "text" ? routedRoomReplies.get(event.threadId) : undefined;
|
|
5981
|
+
const routedBy = routed && speaker && routed.botId === speaker.botId ? routed.routedBy : undefined;
|
|
5982
|
+
if (routedBy)
|
|
5983
|
+
routedRoomReplies.delete(event.threadId);
|
|
5984
|
+
const message = store.appendMessage(event.threadId, group && m.role === "bot" ? { ...m, from: speaker, ...(routedBy ? { routedBy } : {}) }
|
|
5830
5985
|
: proven ? { ...m, requestMessageId: owner.messageId } : m);
|
|
5831
5986
|
return message;
|
|
5832
5987
|
};
|
|
@@ -7572,7 +7727,7 @@ async function startTurn(botId, text, opts) {
|
|
|
7572
7727
|
// an edit hands us its already-branched user message; a plain send appends
|
|
7573
7728
|
let userMessage = opts?.userMessage;
|
|
7574
7729
|
if (opts?.editedMessageId) {
|
|
7575
|
-
const edited = store.branchMessage(threadId, opts.editedMessageId, text, opts.sendId);
|
|
7730
|
+
const edited = store.branchMessage(threadId, opts.editedMessageId, text, opts.sendId, opts.sender);
|
|
7576
7731
|
if (!edited)
|
|
7577
7732
|
throw Object.assign(new Error("only a user text message can be edited"), { status: 400 });
|
|
7578
7733
|
store.patchTask(bot.id, threadId, { rewound: true });
|
|
@@ -7630,7 +7785,7 @@ async function startTurn(botId, text, opts) {
|
|
|
7630
7785
|
}
|
|
7631
7786
|
requestGenerations.add(dispatchClaimId);
|
|
7632
7787
|
directRequestOwners.set(threadId, { generation: dispatchClaimId, messageId: requestMessageId,
|
|
7633
|
-
generations: requestGenerations, turnId: null });
|
|
7788
|
+
generations: requestGenerations, turnId: null, ...(opts?.automationSource ? { automation: opts.automationSource } : {}) });
|
|
7634
7789
|
// An unknown control-plane wake may invalidate a prior final, but it must
|
|
7635
7790
|
// never acquire authority by guessing the latest user as its origin.
|
|
7636
7791
|
const pendingSource = requestMessageId ?? store.activePath(threadId).findLast(message => message.role === "user")?.id;
|
|
@@ -7926,13 +8081,13 @@ async function startTurn(botId, text, opts) {
|
|
|
7926
8081
|
throw new Error("the Computer engine works on the cloud computer — set Works on to Cloud, or choose another engine");
|
|
7927
8082
|
}
|
|
7928
8083
|
const wants = plan.computer;
|
|
7929
|
-
// A place the organisation disallows
|
|
7930
|
-
// prepared; Auto below simply skips
|
|
8084
|
+
// A place the organisation disallows, or a Cloud home never offers, is
|
|
8085
|
+
// refused before anything is prepared; Auto below simply skips them.
|
|
7931
8086
|
const wantedKind = teamComputer ? "box" : wants === "local" ? "thisComputer" : wants === "vm" ? "localVm"
|
|
7932
8087
|
: wants === "cloud" ? (cloudBackend === "vps" ? "vps" : "box") : undefined;
|
|
7933
|
-
const placeRefusal = wantedKind &&
|
|
8088
|
+
const placeRefusal = wantedKind && computerPlaceRefusal(wantedKind);
|
|
7934
8089
|
if (placeRefusal)
|
|
7935
|
-
throw Object.assign(new Error(placeRefusal), { status: 409, code:
|
|
8090
|
+
throw Object.assign(new Error(placeRefusal.message), { status: 409, code: placeRefusal.code });
|
|
7936
8091
|
let previewCapture = null;
|
|
7937
8092
|
let browserCapture = null;
|
|
7938
8093
|
let computerKind = null;
|
|
@@ -8243,7 +8398,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8243
8398
|
}
|
|
8244
8399
|
}
|
|
8245
8400
|
if (wants === "cloud" && cloudBackend === "box" && !boat.boatConfigured(cfg)) {
|
|
8246
|
-
throw new Error(
|
|
8401
|
+
throw new Error(BOAT_NOT_CONFIGURED);
|
|
8247
8402
|
}
|
|
8248
8403
|
if (wants === "cloud" && cloudBackend === "box" && !integrations.computer) {
|
|
8249
8404
|
throw new Error("the cloud computer could not be created or reached");
|
|
@@ -8253,7 +8408,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8253
8408
|
// Auto reaches a Local VM this bot already has before it ever touches the
|
|
8254
8409
|
// host's own desktop: on a headless server that VM is the only desktop
|
|
8255
8410
|
// there is, and a person who prepared one meant it to be used.
|
|
8256
|
-
if (wants === undefined && !integrations.computer && !integrations.localComputer &&
|
|
8411
|
+
if (wants === undefined && !integrations.computer && !integrations.localComputer && !computerPlaceRefusal("localVm") && await attachLocalVm(false))
|
|
8257
8412
|
computerKind = "vm";
|
|
8258
8413
|
// An unattended run on a bot with a VPS configured never lands on the
|
|
8259
8414
|
// host's own desktop instead: a scheduled job clicking on someone's
|
|
@@ -8263,7 +8418,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8263
8418
|
!integrations.localComputer &&
|
|
8264
8419
|
wants === undefined &&
|
|
8265
8420
|
!unattendedVps &&
|
|
8266
|
-
|
|
8421
|
+
!computerPlaceRefusal("thisComputer") &&
|
|
8267
8422
|
shouldMountLocalComputer({
|
|
8268
8423
|
requested: undefined,
|
|
8269
8424
|
hostPlatform: process.platform,
|
|
@@ -8462,7 +8617,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8462
8617
|
{ id: "files", label: "File locations", text: worksInWorkspace && opts?.runOn !== "cloud" ? workspaceLocationsPrompt(bot.id, cwd, liveBot?.cwd ?? bot.cwd) : "" },
|
|
8463
8618
|
{ id: "computer", label: "Computer", text: computerPrompt(computerPromptKind) },
|
|
8464
8619
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(teamComputer) },
|
|
8465
|
-
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: mountedComputer, browser: Boolean(integrations.browser) }, { pinned: plan.pinned, note: plan.note, canSelect: computerSelectionTurns.has(threadId) }) },
|
|
8620
|
+
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: mountedComputer, browser: Boolean(integrations.browser) }, { pinned: plan.pinned, note: plan.note, canSelect: computerSelectionTurns.has(threadId), cloudHome: Boolean(CLOUD_HOME) }) },
|
|
8621
|
+
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
8466
8622
|
// gated on the integration, not the key: the hint only goes to a
|
|
8467
8623
|
// bot whose driver actually mounted the tools
|
|
8468
8624
|
{ id: "composio", label: "Connected apps", text: integrations.composio ? composioSystemPrompt(liveBot?.connectorTools ?? bot.connectorTools) : "" },
|
|
@@ -9903,6 +10059,33 @@ function serializeRoomContext(threadId, userName, textOverrides, readerBotId) {
|
|
|
9903
10059
|
})
|
|
9904
10060
|
.join("\n");
|
|
9905
10061
|
}
|
|
10062
|
+
/** The decision model's view of a room message: the room, its people, the
|
|
10063
|
+
* last room lines before this one, and the message. Text is scrubbed of
|
|
10064
|
+
* secrets and attachment markup before it leaves this machine. */
|
|
10065
|
+
function roomRoutingInput(roomName, text, members, history, sender) {
|
|
10066
|
+
const userName = cfg.profile?.name?.trim() || "User";
|
|
10067
|
+
const clean = (value) => redactSecretsInText(extractTurnImages(value).text);
|
|
10068
|
+
const humans = new Set([userName]);
|
|
10069
|
+
const recent = history.flatMap((m) => {
|
|
10070
|
+
if (m.kind !== "text" || !m.text)
|
|
10071
|
+
return [];
|
|
10072
|
+
if (m.role === "user") {
|
|
10073
|
+
const person = m.sender?.name ?? userName;
|
|
10074
|
+
humans.add(person);
|
|
10075
|
+
return [{ from: person, text: clean(m.text) }];
|
|
10076
|
+
}
|
|
10077
|
+
return [{ from: `${m.from?.name ?? "A bot"} (bot)`, text: clean(m.text) }];
|
|
10078
|
+
});
|
|
10079
|
+
const from = sender?.name ?? userName;
|
|
10080
|
+
humans.add(from);
|
|
10081
|
+
return {
|
|
10082
|
+
room: roomName,
|
|
10083
|
+
humans: [...humans],
|
|
10084
|
+
members: members.map((member) => ({ id: member.id, name: member.name, title: member.title, description: member.description })),
|
|
10085
|
+
recent,
|
|
10086
|
+
message: { from, text: clean(text) },
|
|
10087
|
+
};
|
|
10088
|
+
}
|
|
9906
10089
|
// What each room has already taken from its bots. Keyed by room because the
|
|
9907
10090
|
// loop post_to_room can start is a property of the room, not of any one
|
|
9908
10091
|
// caller — three bots posting twice each is the same runaway as one bot
|
|
@@ -10244,13 +10427,14 @@ setupRetry = 0) {
|
|
|
10244
10427
|
readyBot.browser !== false &&
|
|
10245
10428
|
instance.adapter.capabilities.browserMcp === true,
|
|
10246
10429
|
});
|
|
10247
|
-
// A place the organisation disallows
|
|
10248
|
-
// provisioned or started, exactly as a bot
|
|
10430
|
+
// A place the organisation disallows, or a Cloud home never offers, is
|
|
10431
|
+
// refused before anything is provisioned or started, exactly as a bot
|
|
10432
|
+
// thread refuses it.
|
|
10249
10433
|
const roomKind = roomTeamComputer ? "box" : roomPlan.computer === "local" ? "thisComputer" : readyBot.computer === "vm" ? "localVm"
|
|
10250
10434
|
: roomPlan.computer === "cloud" ? (turnProvider(readyBot) === "vps" ? "vps" : "box") : undefined;
|
|
10251
|
-
const roomPlaceRefusal = roomKind &&
|
|
10435
|
+
const roomPlaceRefusal = roomKind && computerPlaceRefusal(roomKind);
|
|
10252
10436
|
if (roomPlaceRefusal)
|
|
10253
|
-
throw Object.assign(new Error(roomPlaceRefusal), { code:
|
|
10437
|
+
throw Object.assign(new Error(roomPlaceRefusal.message), { code: roomPlaceRefusal.code });
|
|
10254
10438
|
// The Computer engine runs on the Boat; this computer has no tools it can
|
|
10255
10439
|
// reach, exactly as a bot thread refuses it.
|
|
10256
10440
|
if (roomPlan.computer === "local" && instance.adapter.capabilities.remoteAgent === true) {
|
|
@@ -10318,7 +10502,7 @@ setupRetry = 0) {
|
|
|
10318
10502
|
}
|
|
10319
10503
|
else {
|
|
10320
10504
|
if (!boat.boatConfigured(cfg))
|
|
10321
|
-
throw new Error(
|
|
10505
|
+
throw new Error(BOAT_NOT_CONFIGURED);
|
|
10322
10506
|
const remoteAgent = instance.adapter.capabilities.remoteAgent === true;
|
|
10323
10507
|
const attached = await attachBotBoat(readyBot, resourceOwner, { explicitCloud: true,
|
|
10324
10508
|
canMount: instance.adapter.capabilities.usesCloudComputer === true, remoteAgent });
|
|
@@ -10472,7 +10656,8 @@ setupRetry = 0) {
|
|
|
10472
10656
|
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
10473
10657
|
{ id: "computer", label: "Computer", text: computerPrompt(roomComputerPromptKind) },
|
|
10474
10658
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(roomTeamComputer) },
|
|
10475
|
-
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: roomTeamComputer ? "cloud" : roomVmTarget ? "vm" : surfaceOfComputerKind(roomComputerKind), browser: Boolean(integrations.browser) }, { note: roomPlan.note }) },
|
|
10659
|
+
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: roomTeamComputer ? "cloud" : roomVmTarget ? "vm" : surfaceOfComputerKind(roomComputerKind), browser: Boolean(integrations.browser) }, { note: roomPlan.note, cloudHome: Boolean(CLOUD_HOME) }) },
|
|
10660
|
+
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
10476
10661
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
10477
10662
|
{ id: "recall", label: "Recall", text: integrations.agents ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
|
|
10478
10663
|
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentLines) },
|
|
@@ -11131,6 +11316,13 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
11131
11316
|
if (options.goalCoordinatorBotId && (channelMode !== "goal" || !requestedGoalCoordinator)) {
|
|
11132
11317
|
throw Object.assign(new Error("the selected goal coordinator is not an active room member"), { status: 409 });
|
|
11133
11318
|
}
|
|
11319
|
+
// An Auto room may ask the decision model who answers. It reads the room
|
|
11320
|
+
// as it stood before this message, which is appended just below.
|
|
11321
|
+
const autoCandidate = !group.dm && channelMode === "chat" && group.defaultResponder.kind === "auto" &&
|
|
11322
|
+
deciderReady(cfg, "roomRouting");
|
|
11323
|
+
const historyBeforeSend = autoCandidate
|
|
11324
|
+
? store.messagesFor(threadId).filter((m) => m.kind === "text" && m.text).slice(-GROUP_CONTEXT_MESSAGES)
|
|
11325
|
+
: [];
|
|
11134
11326
|
// A coalesced drain appends every queued line under its own queueId (the
|
|
11135
11327
|
// chips and sendId receipts stay per-item); the LAST line is the turn's
|
|
11136
11328
|
// user message, exactly like the 1:1 drain's userMessage.
|
|
@@ -11223,25 +11415,42 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
11223
11415
|
}
|
|
11224
11416
|
return message;
|
|
11225
11417
|
}
|
|
11418
|
+
// Auto with nobody addressed: the responders above are only the fallback
|
|
11419
|
+
// until the decision model answers, in the async round below. The append
|
|
11420
|
+
// and this function's return stay synchronous either way.
|
|
11421
|
+
const autoRoute = autoCandidate && !goalCoordinator && availableMembers.length > 1 &&
|
|
11422
|
+
roomResponders(text, availableMembers, { kind: "mentions" }).length === 0;
|
|
11226
11423
|
// The snippet is only the fallback name here too. The member about to
|
|
11227
11424
|
// answer supplies the same cheap one-shot the bot path uses, and the
|
|
11228
11425
|
// swap lands only while the row still carries the snippet — a rename by
|
|
11229
11426
|
// the person always wins. Channel tasks carry no peer provenance (no
|
|
11230
11427
|
// assignment opens them), and a member whose engine offers no text
|
|
11231
11428
|
// one-shot simply keeps the snippet.
|
|
11232
|
-
const titleBot = goalCoordinator ?? responders[0];
|
|
11233
|
-
const titleInstance = registry.get(titleBot.modelSelection.instanceId);
|
|
11234
11429
|
const titleText = extractTurnImages(text).text;
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11430
|
+
const startTitle = (titleBot) => {
|
|
11431
|
+
const titleInstance = registry.get(titleBot.modelSelection.instanceId);
|
|
11432
|
+
// An engine the organisation disallows never receives the text, even for a title.
|
|
11433
|
+
if (titled && snippet && titleText.trim() && llmThreadTitlesEnabled(cfg) && titleInstance?.generateText && !policyModelRefusal(titleInstance)) {
|
|
11434
|
+
void generateThreadTitle(titleInstance, titleText)
|
|
11435
|
+
.then((title) => {
|
|
11436
|
+
if (title)
|
|
11437
|
+
store.retitleGroupTask(group.id, threadId, snippet, title);
|
|
11438
|
+
})
|
|
11439
|
+
.catch(() => undefined);
|
|
11440
|
+
}
|
|
11441
|
+
};
|
|
11442
|
+
// An Auto round titles with whoever the decision picks, once it has.
|
|
11443
|
+
if (!autoRoute)
|
|
11444
|
+
startTitle(goalCoordinator ?? responders[0]);
|
|
11445
|
+
const operation = beginGroupTurnOperation(groupId, threadId, goalCoordinator || autoRoute ? [] : responders.map((responder) => responder.id));
|
|
11446
|
+
// Asked now, in parallel with any earlier room work still queued: the
|
|
11447
|
+
// answer is waited for only when this round's turn comes up. It never
|
|
11448
|
+
// rejects; a Stop aborts it through the operation's signal.
|
|
11449
|
+
const routing = autoRoute
|
|
11450
|
+
? decideRoomResponder(decider, roomRoutingInput(group.name, text, availableMembers, historyBeforeSend, options.sender), {
|
|
11451
|
+
signal: operation.cancellation.signal,
|
|
11241
11452
|
})
|
|
11242
|
-
|
|
11243
|
-
}
|
|
11244
|
-
const operation = beginGroupTurnOperation(groupId, threadId, goalCoordinator ? [] : responders.map((responder) => responder.id));
|
|
11453
|
+
: null;
|
|
11245
11454
|
if (queuedGroup && queuedGroup.length > 0) {
|
|
11246
11455
|
// member turns use these stable ids to carry every burst line's words
|
|
11247
11456
|
// and attachments through the turn that swallowed the burst
|
|
@@ -11298,9 +11507,27 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
11298
11507
|
await runGroupGoalOperation({ groupId, threadId, coordinator: goalCoordinator, members, operation });
|
|
11299
11508
|
}
|
|
11300
11509
|
else {
|
|
11510
|
+
let round = responders;
|
|
11511
|
+
let routedBy;
|
|
11512
|
+
if (routing) {
|
|
11513
|
+
const route = await routing;
|
|
11514
|
+
if (operation.cancelled)
|
|
11515
|
+
return;
|
|
11516
|
+
const picked = route.kind === "member" ? availableMembers.filter((member) => member.id === route.botId)
|
|
11517
|
+
: route.kind === "everyone" ? availableMembers
|
|
11518
|
+
: [];
|
|
11519
|
+
if (picked.length && route.kind !== "fallback") {
|
|
11520
|
+
round = picked;
|
|
11521
|
+
routedBy = { provider: "jev", probability: Math.round(route.probability * 100) / 100 };
|
|
11522
|
+
}
|
|
11523
|
+
// What a routed round holds from the start, like any other round.
|
|
11524
|
+
for (const responder of round)
|
|
11525
|
+
operation.botIds.add(responder.id);
|
|
11526
|
+
startTitle(round[0]);
|
|
11527
|
+
}
|
|
11301
11528
|
const spoken = new Set();
|
|
11302
11529
|
const skillAuthoringClaim = { claimed: false };
|
|
11303
|
-
for (const responder of
|
|
11530
|
+
for (const responder of round) {
|
|
11304
11531
|
if (operation.cancelled)
|
|
11305
11532
|
break;
|
|
11306
11533
|
if (spoken.has(responder.id))
|
|
@@ -11316,7 +11543,19 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
11316
11543
|
spoken.add(responder.id);
|
|
11317
11544
|
continue;
|
|
11318
11545
|
}
|
|
11319
|
-
|
|
11546
|
+
// This speaker's first reply line says the decision model chose it.
|
|
11547
|
+
const stamp = routedBy ? { botId: responder.id, routedBy } : undefined;
|
|
11548
|
+
if (stamp)
|
|
11549
|
+
routedRoomReplies.set(threadId, stamp);
|
|
11550
|
+
let proceed;
|
|
11551
|
+
try {
|
|
11552
|
+
proceed = await runGroupMemberTurn(groupId, threadId, responder.id, 0, spoken, undefined, undefined, () => operation.cancelled, () => groupProviderHandshakeStarted(operation), () => groupProviderHandshakeSettled(operation), skillAuthoringClaim, undefined, operation);
|
|
11553
|
+
}
|
|
11554
|
+
finally {
|
|
11555
|
+
if (stamp && routedRoomReplies.get(threadId) === stamp)
|
|
11556
|
+
routedRoomReplies.delete(threadId);
|
|
11557
|
+
}
|
|
11558
|
+
if (!proceed)
|
|
11320
11559
|
break;
|
|
11321
11560
|
}
|
|
11322
11561
|
}
|
|
@@ -12188,24 +12427,15 @@ async function testCliBinary(cli, driver) {
|
|
|
12188
12427
|
});
|
|
12189
12428
|
}
|
|
12190
12429
|
/** A pre-save probe only needs PATH. Never hand credentials inherited by the
|
|
12191
|
-
* desktop/server process to an arbitrary wrapper selected through Settings.
|
|
12430
|
+
* desktop/server process to an arbitrary wrapper selected through Settings.
|
|
12431
|
+
* The shared lists (config.ts) cover every workspace, control-plane and
|
|
12432
|
+
* provider credential, including ones added later; the probe and `claude
|
|
12433
|
+
* update` keep everything else (HOME, proxy settings, CA bundles). */
|
|
12192
12434
|
function cliProbeEnvironment() {
|
|
12193
12435
|
const env = { ...process.env, PATH: augmentedPath() };
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
"BOX_TOKEN",
|
|
12197
|
-
"OPENCODE_API_KEY",
|
|
12198
|
-
"COMPOSIO_API_KEY",
|
|
12199
|
-
"OMB_COMPOSIO_BROKER_TOKEN",
|
|
12200
|
-
"OMB_TTS_KEY",
|
|
12201
|
-
"OMB_FISH_AUDIO_API_KEY",
|
|
12202
|
-
"OMB_OPENAI_IMAGE_KEY",
|
|
12203
|
-
"OMB_CUSTOM_IMAGE_KEY",
|
|
12204
|
-
"ANTHROPIC_API_KEY",
|
|
12205
|
-
"OPENAI_API_KEY",
|
|
12206
|
-
]) {
|
|
12436
|
+
stripWorkspaceCredentialEnv(env);
|
|
12437
|
+
for (const key of PROVIDER_CREDENTIAL_ENV)
|
|
12207
12438
|
delete env[key];
|
|
12208
|
-
}
|
|
12209
12439
|
return env;
|
|
12210
12440
|
}
|
|
12211
12441
|
/** execFile's error carries the child's stderr in .stderr. */
|
|
@@ -12365,17 +12595,24 @@ function configStatus() {
|
|
|
12365
12595
|
configured: composio.configured(cfg),
|
|
12366
12596
|
mode: composio.connectionMode(cfg),
|
|
12367
12597
|
},
|
|
12368
|
-
|
|
12598
|
+
// configured = cloud computers work here; included = through Cloud Pro,
|
|
12599
|
+
// not a saved key
|
|
12600
|
+
box: boat.describeBoatAccount(cfg),
|
|
12369
12601
|
vps: { configured: Boolean(vpsSshAlias(cfg)), sshAlias: vpsSshAlias(cfg) ?? "" },
|
|
12370
12602
|
opencodeGo: { configured: Boolean(cfg.opencodeGo?.apiKey) },
|
|
12371
12603
|
// the chosen voice is a setting, not a secret; the key is reported the
|
|
12372
12604
|
// same configured-or-not way as every other credential
|
|
12373
12605
|
tts: tts.describeVoice(cfg),
|
|
12606
|
+
// the decision model: switches and configured-or-not, never the key
|
|
12607
|
+
decider: describeDecider(cfg),
|
|
12374
12608
|
imageGen: avatarImageStatus(cfg),
|
|
12375
12609
|
// not a secret — the sidebar shows it
|
|
12376
12610
|
profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "", aboutMe: cfg.profile?.aboutMe ?? "" },
|
|
12377
12611
|
// the enrolled organisation's read-only desktop policy; null when not enrolled
|
|
12378
12612
|
managedPolicy: managedPolicy.summary(),
|
|
12613
|
+
// an OMB Cloud home (cloud-home.ts): the app offers no this computer and
|
|
12614
|
+
// no Local VM here
|
|
12615
|
+
...(CLOUD_HOME ? { cloudHome: true } : {}),
|
|
12379
12616
|
// not a secret — the settings picker shows it; "" = follow the system
|
|
12380
12617
|
language: cfg.language ?? "",
|
|
12381
12618
|
rooms: { turnTimeoutMinutes: roomTurnTimeoutMinutes(cfg) },
|
|
@@ -12718,12 +12955,7 @@ function serveStatic(res, path) {
|
|
|
12718
12955
|
// requests from any loopback connection and any web page that DNS-rebinds
|
|
12719
12956
|
// onto it. Reject non-loopback Hosts outright (defeats rebinding) and
|
|
12720
12957
|
// origins outside loopback (blocks remote-web CSRF).
|
|
12721
|
-
const
|
|
12722
|
-
dataDir: DATA_DIR,
|
|
12723
|
-
appVersion: serverVersion(),
|
|
12724
|
-
readBody,
|
|
12725
|
-
restored: workspaceRestore,
|
|
12726
|
-
status: () => ({ busy: workspaceMaintenance.active, pendingRestore: workspaceMaintenance.pendingRestore }),
|
|
12958
|
+
const workspaceBackupAccess = {
|
|
12727
12959
|
authorized: (req, original) => {
|
|
12728
12960
|
const current = resolveRequestAuth(req, {
|
|
12729
12961
|
sessions, cookieName: SESSION_COOKIE, streamPath: "/api/events",
|
|
@@ -12759,6 +12991,27 @@ const workspaceBackupRoutes = createWorkspaceBackupRoutes({
|
|
|
12759
12991
|
closeMessageDb();
|
|
12760
12992
|
},
|
|
12761
12993
|
}, keepLocked),
|
|
12994
|
+
status: () => ({ busy: workspaceMaintenance.active, pendingRestore: workspaceMaintenance.pendingRestore }),
|
|
12995
|
+
};
|
|
12996
|
+
const workspaceBackupRoutes = createWorkspaceBackupRoutes({
|
|
12997
|
+
dataDir: DATA_DIR,
|
|
12998
|
+
appVersion: serverVersion(),
|
|
12999
|
+
readBody,
|
|
13000
|
+
restored: workspaceRestore,
|
|
13001
|
+
...workspaceBackupAccess,
|
|
13002
|
+
});
|
|
13003
|
+
// Move to Cloud (server/cloud-move-http.ts): every server sizes its own
|
|
13004
|
+
// workspace for the desktop; only a Cloud home receives one. After its
|
|
13005
|
+
// restore commits, the Cloud home restarts so startup installs it.
|
|
13006
|
+
let cloudHomeRestartRequested = false;
|
|
13007
|
+
const cloudMoveRoutes = createCloudMoveRoutes({
|
|
13008
|
+
dataDir: DATA_DIR,
|
|
13009
|
+
appVersion: serverVersion(),
|
|
13010
|
+
cloudHome: Boolean(CLOUD_HOME),
|
|
13011
|
+
readBody,
|
|
13012
|
+
restored: workspaceRestore,
|
|
13013
|
+
...workspaceBackupAccess,
|
|
13014
|
+
restart: () => { cloudHomeRestartRequested = true; gracefulShutdown(); },
|
|
12762
13015
|
});
|
|
12763
13016
|
// Route modules (server/routes/README.md). `workspaceAccess` is assigned at
|
|
12764
13017
|
// boot, after this line, so the dependency reads it per request.
|
|
@@ -12780,6 +13033,7 @@ ROUTES.push(createBotMemoryRoutes({
|
|
|
12780
13033
|
broadcast({ kind: "config", ...configStatus() });
|
|
12781
13034
|
},
|
|
12782
13035
|
}));
|
|
13036
|
+
ROUTES.push(createDeciderRoutes({ decider }));
|
|
12783
13037
|
const toolResults = new ToolResults();
|
|
12784
13038
|
const handleRequest = async (req, res) => {
|
|
12785
13039
|
let url;
|
|
@@ -12836,7 +13090,7 @@ const handleRequest = async (req, res) => {
|
|
|
12836
13090
|
if (method === "GET" && !path.startsWith("/api/") && !path.startsWith("/.well-known/") && serveStatic(res, path))
|
|
12837
13091
|
return;
|
|
12838
13092
|
if (method === "GET" && path === "/.well-known/openmausbot/environment") {
|
|
12839
|
-
return json(res, 200, environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: !HOSTED_WORKSPACE && emailSignIn.enabled(), sharedComputers:
|
|
13093
|
+
return json(res, 200, environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: !HOSTED_WORKSPACE && emailSignIn.enabled(), sharedComputers: lendingEnabled() }));
|
|
12840
13094
|
}
|
|
12841
13095
|
const domainCheck = /^\/\.well-known\/openmausbot\/domain-check\/([a-f0-9]{64})$/.exec(path);
|
|
12842
13096
|
if (method === "GET" && domainCheck) {
|
|
@@ -12879,7 +13133,7 @@ const handleRequest = async (req, res) => {
|
|
|
12879
13133
|
}
|
|
12880
13134
|
sessions.clearFailures(source);
|
|
12881
13135
|
const issued = sessions.issue({ label: label.trim() || labelFromUserAgent(req.headers["user-agent"]), scopes: verified.scopes, userId: verified.userId, email: verified.email });
|
|
12882
|
-
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: true, sharedComputers:
|
|
13136
|
+
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: true, sharedComputers: lendingEnabled() });
|
|
12883
13137
|
const secure = requestOrigin(req)?.startsWith("https://") === true;
|
|
12884
13138
|
res.setHeader("set-cookie", serializeSessionCookie(SESSION_COOKIE, issued.token, { secure, maxAgeSeconds: cookieMaxAgeSeconds(issued.session) }));
|
|
12885
13139
|
return json(res, 200, { session: issued.session, environment });
|
|
@@ -12921,7 +13175,7 @@ const handleRequest = async (req, res) => {
|
|
|
12921
13175
|
console.warn(`pairing refused from ${requestSource(req)}: ${result.error}`);
|
|
12922
13176
|
return json(res, result.status, { error: result.error });
|
|
12923
13177
|
}
|
|
12924
|
-
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: emailSignIn.enabled(), sharedComputers:
|
|
13178
|
+
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: emailSignIn.enabled(), sharedComputers: lendingEnabled() });
|
|
12925
13179
|
if (wantsCookie) {
|
|
12926
13180
|
const secure = requestOrigin(req)?.startsWith("https://") === true;
|
|
12927
13181
|
res.setHeader("set-cookie", serializeSessionCookie(SESSION_COOKIE, result.token, { secure, maxAgeSeconds: cookieMaxAgeSeconds(result.session) }));
|
|
@@ -12973,7 +13227,7 @@ const handleRequest = async (req, res) => {
|
|
|
12973
13227
|
url,
|
|
12974
13228
|
loopbackMutationToken: desktopMutationToken,
|
|
12975
13229
|
companionMutationToken,
|
|
12976
|
-
features: { sharedComputers:
|
|
13230
|
+
features: { sharedComputers: lendingEnabled() },
|
|
12977
13231
|
loopbackTrust: LOOPBACK.trust,
|
|
12978
13232
|
cliOwnerToken,
|
|
12979
13233
|
});
|
|
@@ -13031,6 +13285,8 @@ const handleRequest = async (req, res) => {
|
|
|
13031
13285
|
}
|
|
13032
13286
|
if (await workspaceBackupRoutes(req, res, path, auth))
|
|
13033
13287
|
return;
|
|
13288
|
+
if (await cloudMoveRoutes(req, res, path, auth))
|
|
13289
|
+
return;
|
|
13034
13290
|
// Count ordinary requests until their asynchronous handler returns, not
|
|
13035
13291
|
// merely until the browser disconnects. A cancelled upload can still write.
|
|
13036
13292
|
if (path.startsWith("/api/") && path !== "/api/events" && path !== "/api/health" && !path.startsWith("/api/shared-computers/") && !isWorkspaceBackupSessionControl(method, path)) {
|
|
@@ -13150,11 +13406,15 @@ const handleRequest = async (req, res) => {
|
|
|
13150
13406
|
const cancelled = sessions.cancelPairing(m[1]);
|
|
13151
13407
|
return json(res, cancelled ? 200 : 404, cancelled ? { ok: true } : { error: "no such pairing code" });
|
|
13152
13408
|
}
|
|
13153
|
-
|
|
13409
|
+
// The desktop's own server always answers its desktop here: lending to the
|
|
13410
|
+
// person's Cloud (docs/cloud-pro.md) needs the lease without the
|
|
13411
|
+
// maintainer flag. It grants nothing by itself; it only keeps local bot
|
|
13412
|
+
// turns and a remote screen action from using the screen at once.
|
|
13413
|
+
if (method === "POST" && path === "/api/desktop/shared-computer-control" && (sharedComputersEnabled(cfg) || DESKTOP_MANAGED)) {
|
|
13154
13414
|
if (auth.kind !== "loopback")
|
|
13155
13415
|
return json(res, 403, { error: "Local desktop only" });
|
|
13156
13416
|
const body = await readBody(req, 1024);
|
|
13157
|
-
if (!sharedComputersEnabled(cfg))
|
|
13417
|
+
if (!sharedComputersEnabled(cfg) && !DESKTOP_MANAGED)
|
|
13158
13418
|
return json(res, 404, { error: `no route: ${method} ${path}` });
|
|
13159
13419
|
if (!z.string().uuid().safeParse(body?.id).success || !["acquire", "release", "renew"].includes(body?.action))
|
|
13160
13420
|
return json(res, 400, { error: "Invalid computer lease" });
|
|
@@ -13177,13 +13437,19 @@ const handleRequest = async (req, res) => {
|
|
|
13177
13437
|
// With features.sharedComputers off the whole family falls through to the
|
|
13178
13438
|
// generic "no route" 404, so a probe cannot tell a disabled feature from
|
|
13179
13439
|
// a build that never had one.
|
|
13180
|
-
|
|
13440
|
+
// What the asking person has lent to this server, online or not, with its
|
|
13441
|
+
// scopes (docs/cloud-pro.md, "What the Cloud can see"). Owner-scoped like
|
|
13442
|
+
// the bot routes; no id a session could not already use, and no secret.
|
|
13443
|
+
if (method === "GET" && path === "/api/shared-computers" && lendingEnabled()) {
|
|
13444
|
+
return json(res, 200, { computers: sharedComputers.status(sessionLendingPrincipal(auth)) });
|
|
13445
|
+
}
|
|
13446
|
+
if (method === "POST" && path.startsWith("/api/shared-computers/") && lendingEnabled()) {
|
|
13181
13447
|
if (auth.kind !== "session")
|
|
13182
13448
|
return json(res, 403, { error: "Pair this desktop first" });
|
|
13183
13449
|
if (!/^application\/json\b/i.test(String(req.headers["content-type"] ?? "")))
|
|
13184
13450
|
return json(res, 415, { error: "JSON required" });
|
|
13185
13451
|
const body = await readBody(req, 4_000_000);
|
|
13186
|
-
if (!
|
|
13452
|
+
if (!lendingEnabled())
|
|
13187
13453
|
return json(res, 404, { error: `no route: ${method} ${path}` });
|
|
13188
13454
|
if (!sessions.isLive(auth.session.id))
|
|
13189
13455
|
return json(res, 401, { error: "Session ended" });
|
|
@@ -13195,7 +13461,11 @@ const handleRequest = async (req, res) => {
|
|
|
13195
13461
|
const registration = parsed.data;
|
|
13196
13462
|
if (registration.environmentId !== ENVIRONMENT_ID)
|
|
13197
13463
|
return json(res, 409, { error: "Workspace identity changed. Pair again before sharing this computer." });
|
|
13198
|
-
|
|
13464
|
+
// A Cloud home is one person's: only their own devices, which the
|
|
13465
|
+
// Admin's pairing signs in with admin scope, may lend to it.
|
|
13466
|
+
if (CLOUD_HOME && !auth.scopes.includes("admin"))
|
|
13467
|
+
return json(res, 403, { error: "Only your own computers can lend to your Cloud. Connect this computer to your Cloud from its OMB Cloud settings." });
|
|
13468
|
+
sharedComputers.register(registration, { session: auth.session.id, person: CLOUD_HOME ? CLOUD_HOME_LENDER : personKey(auth.session) }, secret);
|
|
13199
13469
|
return json(res, 200, { ok: true });
|
|
13200
13470
|
}
|
|
13201
13471
|
const route = /^\/api\/shared-computers\/([\w-]+)\/(poll|lease|result|disconnect)$/.exec(path);
|
|
@@ -13364,6 +13634,7 @@ const handleRequest = async (req, res) => {
|
|
|
13364
13634
|
if (method === "POST" && requested !== "auto" && !parseSurface(requested)) {
|
|
13365
13635
|
return json(res, 400, { error: "surface must be auto, cloud, vm, local, or browser" });
|
|
13366
13636
|
}
|
|
13637
|
+
const unoffered = CLOUD_HOME && method === "POST" && requested !== "auto" ? cloudHomePlaceRefusal(parseSurface(requested)) : undefined;
|
|
13367
13638
|
const options = await selectableComputers(bot);
|
|
13368
13639
|
const current = source ? source.mounted ?? "off" : await computerPreviewSurface(bot, bot.threadId);
|
|
13369
13640
|
requireActiveInternalCapability();
|
|
@@ -13371,6 +13642,8 @@ const handleRequest = async (req, res) => {
|
|
|
13371
13642
|
return json(res, 200, { current, canSelect, options });
|
|
13372
13643
|
if (computerSelectionTurns.get(internalCapability.threadId) !== source)
|
|
13373
13644
|
return json(res, 409, { error: "The user request ended before its computer was selected." });
|
|
13645
|
+
if (unoffered)
|
|
13646
|
+
return json(res, 409, { error: unoffered, options });
|
|
13374
13647
|
const option = requested === "auto"
|
|
13375
13648
|
? options.find(option => option.ready && option.surface === current) ?? options.find(option => option.ready && option.surface === "vm") ?? options.find(option => option.ready) ?? options.find(option => option.canStart) ?? options.find(option => option.canCreate && option.surface === "vm") ?? options.find(option => option.canCreate)
|
|
13376
13649
|
: options.find(option => option.surface === requested);
|
|
@@ -13484,15 +13757,30 @@ const handleRequest = async (req, res) => {
|
|
|
13484
13757
|
}
|
|
13485
13758
|
// Off by default: both fall through to the same "unknown internal
|
|
13486
13759
|
// endpoint" 404 a never-implemented route returns.
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
if (method === "
|
|
13760
|
+
// Owner-scoped: a turn sees and uses only the computers of the person it
|
|
13761
|
+
// provably acts for (sharedComputerPrincipal), never anyone else's.
|
|
13762
|
+
if (method === "GET" && path === "/api/internal/shared-computers" && lendingEnabled()) {
|
|
13763
|
+
const principal = sharedComputerPrincipal(internalCapability);
|
|
13764
|
+
// Say why an owner's own conversation sees no Mac, so the bot can tell them.
|
|
13765
|
+
const notice = CLOUD_HOME && !principal && sharedComputers.status(CLOUD_HOME_LENDER).length > 0 &&
|
|
13766
|
+
cloudHomeLendingRefusal(cloudLendingTurn(internalCapability)) === "someone-else" ? { unavailable: LENDING_SOMEONE_ELSE } : {};
|
|
13767
|
+
return json(res, 200, { computers: sharedComputers.list(principal), ...notice });
|
|
13768
|
+
}
|
|
13769
|
+
if (method === "POST" && path === "/api/internal/shared-computers" && lendingEnabled()) {
|
|
13490
13770
|
const parsed = sharedComputerOperation.safeParse(await readInternalBody());
|
|
13491
|
-
if (!
|
|
13771
|
+
if (!lendingEnabled())
|
|
13492
13772
|
return json(res, 404, { error: "unknown internal endpoint" });
|
|
13493
13773
|
if (!parsed.success)
|
|
13494
13774
|
return json(res, 400, { error: "Invalid shared computer operation" });
|
|
13495
|
-
|
|
13775
|
+
// The bot's own "no computer" setting holds on a lent screen too.
|
|
13776
|
+
if (["computer_tools", "computer_call"].includes(parsed.data.action) && store.bot(internalCapability.botId)?.computer === "off") {
|
|
13777
|
+
return json(res, 403, { error: "This bot has no computer. Change its Computer setting to use a shared computer's apps and screen." });
|
|
13778
|
+
}
|
|
13779
|
+
const principal = sharedComputerPrincipal(internalCapability);
|
|
13780
|
+
if (CLOUD_HOME && !principal && cloudHomeLendingRefusal(cloudLendingTurn(internalCapability)) === "someone-else") {
|
|
13781
|
+
return json(res, 403, { error: LENDING_SOMEONE_ELSE });
|
|
13782
|
+
}
|
|
13783
|
+
return json(res, 200, { result: await sharedComputers.request(parsed.data, principal, () => lendingEnabled() && internalCapabilityIsActive(internalCapability)) });
|
|
13496
13784
|
}
|
|
13497
13785
|
if (method === "GET" && path === "/api/internal/agents") {
|
|
13498
13786
|
const sender = internalSender;
|
|
@@ -15266,7 +15554,11 @@ const handleRequest = async (req, res) => {
|
|
|
15266
15554
|
}
|
|
15267
15555
|
const group = createChannel({
|
|
15268
15556
|
name: redactSecretsInText(parsed.data.name), memberIds, section: chief.section,
|
|
15269
|
-
setup: {
|
|
15557
|
+
setup: {
|
|
15558
|
+
bulletin: redactSecretsInText(parsed.data.bulletin ?? ""),
|
|
15559
|
+
// The Chief leads; on Auto it is the fallback when the decision model is unsure.
|
|
15560
|
+
defaultResponder: deciderReady(cfg, "roomRouting") ? { kind: "auto", fallbackBotId: chief.id } : { kind: "member", botId: chief.id },
|
|
15561
|
+
},
|
|
15270
15562
|
});
|
|
15271
15563
|
internalCapability.createdRooms = (internalCapability.createdRooms ?? 0) + 1;
|
|
15272
15564
|
return json(res, 201, { id: group.id, name: group.name, section: group.section || "General", memberIds: group.memberIds, memberCount: group.memberIds.length });
|
|
@@ -15645,7 +15937,12 @@ const handleRequest = async (req, res) => {
|
|
|
15645
15937
|
const hidden = hiddenRoutineTarget(body, visible);
|
|
15646
15938
|
if (hidden)
|
|
15647
15939
|
return json(res, 404, { error: hidden });
|
|
15648
|
-
|
|
15940
|
+
const routine = routines.create(body);
|
|
15941
|
+
// On a Cloud home, a routine the owner writes from their own device may
|
|
15942
|
+
// use their lent Mac when it runs (server/cloud-lending.ts).
|
|
15943
|
+
if (cloudRoutineAuthors && cloudOwnerSession(auth))
|
|
15944
|
+
cloudRoutineAuthors.record(routine.id, routine);
|
|
15945
|
+
return json(res, 201, { routine });
|
|
15649
15946
|
}
|
|
15650
15947
|
// The desktop shell polls this to decide whether to hold the computer
|
|
15651
15948
|
// awake: a run in flight, or a routine due within the hour.
|
|
@@ -15655,6 +15952,8 @@ const handleRequest = async (req, res) => {
|
|
|
15655
15952
|
let routineMatch = path.match(/^\/api\/routines\/([\w-]+)\/run$/);
|
|
15656
15953
|
if (routineMatch && method === "POST") {
|
|
15657
15954
|
const run = routines.runNow(routineMatch[1]);
|
|
15955
|
+
if (run && CLOUD_HOME && cloudOwnerSession(auth))
|
|
15956
|
+
ownerStartedRoutineRuns.add(run.id);
|
|
15658
15957
|
return run ? json(res, 201, { run }) : json(res, 404, { error: "no such routine" });
|
|
15659
15958
|
}
|
|
15660
15959
|
routineMatch = path.match(/^\/api\/routines\/([\w-]+)$/);
|
|
@@ -15663,11 +15962,26 @@ const handleRequest = async (req, res) => {
|
|
|
15663
15962
|
const hidden = hiddenRoutineTarget(body, visible);
|
|
15664
15963
|
if (hidden)
|
|
15665
15964
|
return json(res, 404, { error: hidden });
|
|
15965
|
+
const before = cloudRoutineAuthors ? routines.listRoutines().find((candidate) => candidate.id === routineMatch[1]) : undefined;
|
|
15966
|
+
const wasOwners = Boolean(before && cloudRoutineAuthors?.authored(before.id, before));
|
|
15666
15967
|
const routine = routines.update(routineMatch[1], body);
|
|
15968
|
+
// The owner's own edit keeps (or, when it rewrites the instructions,
|
|
15969
|
+
// makes) the routine theirs. Anyone else's edit, of any field (its
|
|
15970
|
+
// schedule, where its results go, whether it is on), makes it no longer
|
|
15971
|
+
// the owner's: it stops lending until the owner rewrites it.
|
|
15972
|
+
if (routine && cloudRoutineAuthors) {
|
|
15973
|
+
if (!cloudOwnerSession(auth))
|
|
15974
|
+
cloudRoutineAuthors.forget(routine.id);
|
|
15975
|
+
else if (wasOwners || (body && typeof body === "object" && "prompt" in body))
|
|
15976
|
+
cloudRoutineAuthors.record(routine.id, routine);
|
|
15977
|
+
}
|
|
15667
15978
|
return routine ? json(res, 200, { routine }) : json(res, 404, { error: "no such routine" });
|
|
15668
15979
|
}
|
|
15669
15980
|
if (routineMatch && method === "DELETE") {
|
|
15670
|
-
|
|
15981
|
+
const removed = routines.remove(routineMatch[1]);
|
|
15982
|
+
if (removed)
|
|
15983
|
+
cloudRoutineAuthors?.forget(routineMatch[1]);
|
|
15984
|
+
return removed
|
|
15671
15985
|
? json(res, 200, { ok: true })
|
|
15672
15986
|
: json(res, 404, { error: "no such routine" });
|
|
15673
15987
|
}
|
|
@@ -16340,7 +16654,7 @@ const handleRequest = async (req, res) => {
|
|
|
16340
16654
|
const current = resolveRequestAuth(req, {
|
|
16341
16655
|
sessions, cookieName: SESSION_COOKIE, streamPath: "/api/events", url,
|
|
16342
16656
|
loopbackMutationToken: desktopMutationToken, companionMutationToken,
|
|
16343
|
-
features: { sharedComputers:
|
|
16657
|
+
features: { sharedComputers: lendingEnabled() }, loopbackTrust: LOOPBACK.trust, cliOwnerToken,
|
|
16344
16658
|
});
|
|
16345
16659
|
if (!current.auth)
|
|
16346
16660
|
return json(res, current.status, { error: current.error });
|
|
@@ -16838,15 +17152,7 @@ const handleRequest = async (req, res) => {
|
|
|
16838
17152
|
return json(res, 400, { error: "bulletin must be a string" });
|
|
16839
17153
|
if (body.bulletin.length > 12_000)
|
|
16840
17154
|
return json(res, 400, { error: "bulletin must be at most 12000 characters" });
|
|
16841
|
-
const
|
|
16842
|
-
let responder = null;
|
|
16843
|
-
if (value?.kind === "everyone")
|
|
16844
|
-
responder = { kind: "everyone" };
|
|
16845
|
-
else if (value?.kind === "mentions")
|
|
16846
|
-
responder = { kind: "mentions" };
|
|
16847
|
-
else if (value?.kind === "member" && typeof value.botId === "string" && group.memberIds.includes(value.botId)) {
|
|
16848
|
-
responder = { kind: "member", botId: value.botId };
|
|
16849
|
-
}
|
|
17155
|
+
const responder = checkedGroupResponder(body.defaultResponder, group.memberIds);
|
|
16850
17156
|
if (!responder)
|
|
16851
17157
|
return json(res, 400, { error: "invalid default responder" });
|
|
16852
17158
|
patch.cwd = checked.cwd ?? undefined;
|
|
@@ -18775,6 +19081,12 @@ const handleRequest = async (req, res) => {
|
|
|
18775
19081
|
if (existing.card.requestId) {
|
|
18776
19082
|
return json(res, 409, { error: "request cards must be answered through the approval endpoint" });
|
|
18777
19083
|
}
|
|
19084
|
+
// On a Cloud home only the owner answers cards (cardAnswerRefusal).
|
|
19085
|
+
// This route only keeps how an options card looks; the answer itself
|
|
19086
|
+
// reaches the bot as a chat line, which lending already checks.
|
|
19087
|
+
const cloudRefusal = CLOUD_HOME && body.answered !== undefined && auth.kind === "session" ? cloudCardRefusal(auth) : null;
|
|
19088
|
+
if (cloudRefusal)
|
|
19089
|
+
return json(res, 403, { error: cloudRefusal });
|
|
18778
19090
|
if (Object.keys(body).some((key) => key !== "answered" && key !== "dismissed" && key !== "threadId")) {
|
|
18779
19091
|
return json(res, 400, { error: "only answered and dismissed may be changed" });
|
|
18780
19092
|
}
|
|
@@ -18789,6 +19101,7 @@ const handleRequest = async (req, res) => {
|
|
|
18789
19101
|
...existing.card,
|
|
18790
19102
|
...(body.answered !== undefined ? { answered: body.answered } : {}),
|
|
18791
19103
|
...(body.dismissed !== undefined ? { dismissed: body.dismissed } : {}),
|
|
19104
|
+
...(CLOUD_HOME && body.answered !== undefined ? { answeredBy: cardAnswererFor(auth) } : {}),
|
|
18792
19105
|
},
|
|
18793
19106
|
});
|
|
18794
19107
|
return json(res, 200, { message: patched });
|
|
@@ -19167,7 +19480,10 @@ const handleRequest = async (req, res) => {
|
|
|
19167
19480
|
// startTurn admits the rerun before branching. A shared-resource or
|
|
19168
19481
|
// concurrency-limit refusal must leave the original transcript intact.
|
|
19169
19482
|
const replyTo = source.replyToId ? resolveReplyTarget(bot.threadId, source.replyToId) : undefined;
|
|
19170
|
-
|
|
19483
|
+
// On a Cloud home an edit is its author's line, so the owner's own edit
|
|
19484
|
+
// keeps their conversation theirs for lending (server/cloud-lending.ts).
|
|
19485
|
+
const message = await startTurn(bot.id, text, { threadId: bot.threadId, editedMessageId: messageId, replyTo, sendId, trigger: usageTriggerFor(auth),
|
|
19486
|
+
...(CLOUD_HOME ? { sender: messageSender(auth) } : {}) });
|
|
19171
19487
|
return json(res, 202, { ok: true, message });
|
|
19172
19488
|
}
|
|
19173
19489
|
// switch which fork of the conversation is visible (no new turn)
|
|
@@ -20783,6 +21099,14 @@ const handleRequest = async (req, res) => {
|
|
|
20783
21099
|
}
|
|
20784
21100
|
if (!Object.keys(patch).length)
|
|
20785
21101
|
return json(res, 400, { error: "nothing to save" });
|
|
21102
|
+
if (patch.decider) {
|
|
21103
|
+
// Saving a key is the "turn it on"; clearing it turns it off, or
|
|
21104
|
+
// falls back to Cloud Pro's included decisions where there are some.
|
|
21105
|
+
const planned = deciderSavePatch(patch.decider, cfg.decider, deciderIncludedHere());
|
|
21106
|
+
if (!planned.ok)
|
|
21107
|
+
return json(res, 400, { error: planned.error });
|
|
21108
|
+
patch.decider = planned.patch;
|
|
21109
|
+
}
|
|
20786
21110
|
const changingVoiceProvider = patch.tts?.provider !== undefined
|
|
20787
21111
|
&& patch.tts.provider !== tts.voiceProvider(cfg);
|
|
20788
21112
|
if (changingVoiceProvider && patch.tts?.voice === undefined) {
|
|
@@ -21072,6 +21396,15 @@ const handleRequest = async (req, res) => {
|
|
|
21072
21396
|
if (!check.ok)
|
|
21073
21397
|
return json(res, 400, { error: check.message });
|
|
21074
21398
|
}
|
|
21399
|
+
// A pasted decision-model key is tried once (one tiny yes/no call).
|
|
21400
|
+
// Only a refusal blocks the save: an offline or overloaded service must
|
|
21401
|
+
// not stop a person saving a key that may well be fine.
|
|
21402
|
+
if (patch.decider?.key) {
|
|
21403
|
+
const check = await decider.testKey({ key: patch.decider.key });
|
|
21404
|
+
if (!check.ok && check.reason === "rejected") {
|
|
21405
|
+
return json(res, 400, { error: "Jev rejected this key. Check it at typesafe.ai and paste it again.", code: "decider_rejected" });
|
|
21406
|
+
}
|
|
21407
|
+
}
|
|
21075
21408
|
if (patch.browserProfiles !== undefined) {
|
|
21076
21409
|
// Provider/credential validation above may await the network. A turn
|
|
21077
21410
|
// can start during that window and claim a profile which looked idle
|
|
@@ -21139,6 +21472,8 @@ const handleRequest = async (req, res) => {
|
|
|
21139
21472
|
persisted.tts.key = "";
|
|
21140
21473
|
if (persisted.tts?.fishKey !== undefined)
|
|
21141
21474
|
persisted.tts.fishKey = "";
|
|
21475
|
+
if (persisted.decider?.key !== undefined)
|
|
21476
|
+
persisted.decider.key = "";
|
|
21142
21477
|
if (persisted.imageGen?.key !== undefined)
|
|
21143
21478
|
persisted.imageGen.key = "";
|
|
21144
21479
|
if (persisted.imageGen?.customApiKey !== undefined)
|
|
@@ -21174,7 +21509,7 @@ const handleRequest = async (req, res) => {
|
|
|
21174
21509
|
let browserReferenceCleanupError = null;
|
|
21175
21510
|
if (patch.signIn !== undefined)
|
|
21176
21511
|
sessions.revalidateEmailSessions();
|
|
21177
|
-
if (!
|
|
21512
|
+
if (!lendingEnabled()) {
|
|
21178
21513
|
sharedComputers.close();
|
|
21179
21514
|
sharedComputerControl.close();
|
|
21180
21515
|
}
|
|
@@ -21998,7 +22333,8 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
21998
22333
|
}
|
|
21999
22334
|
closeMessageDb();
|
|
22000
22335
|
releaseDataDirLeaseAtExit();
|
|
22001
|
-
|
|
22336
|
+
// A Cloud home's launcher starts the server again on this code only.
|
|
22337
|
+
process.exit(cloudHomeRestartRequested && code === 0 ? CLOUD_HOME_RESTART_EXIT_CODE : code);
|
|
22002
22338
|
},
|
|
22003
22339
|
});
|
|
22004
22340
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|