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.
Files changed (112) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-q73qM-At.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-G1PHKGyh.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-Dwc3Hrru.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +18 -3
  61. package/dist-server/container-mcp.js +84 -32
  62. package/dist-server/drivers/agents-proxy.js +11 -0
  63. package/dist-server/index.js +25183 -23580
  64. package/dist-server/openmausbot.js +313 -159
  65. package/dist-server/pair-cli.js +313 -159
  66. package/dist-server/server/boat.js +66 -21
  67. package/dist-server/server/bot-overview.js +7 -4
  68. package/dist-server/server/cloud-home-start.js +23 -5
  69. package/dist-server/server/cloud-home.js +25 -2
  70. package/dist-server/server/cloud-lending.js +103 -0
  71. package/dist-server/server/cloud-move-http.js +268 -0
  72. package/dist-server/server/cloud-move.js +418 -0
  73. package/dist-server/server/config.js +42 -4
  74. package/dist-server/server/decider/index.js +238 -0
  75. package/dist-server/server/decider/jev.js +183 -0
  76. package/dist-server/server/decider/log.js +62 -0
  77. package/dist-server/server/decider/relay.js +52 -0
  78. package/dist-server/server/decider/room-routing.js +88 -0
  79. package/dist-server/server/decider/types.js +5 -0
  80. package/dist-server/server/drivers/agents-catalog.js +16 -0
  81. package/dist-server/server/drivers/boatagent.js +20 -12
  82. package/dist-server/server/drivers/claude.js +1 -0
  83. package/dist-server/server/group-goal-run.js +6 -4
  84. package/dist-server/server/harness-capabilities.js +2 -0
  85. package/dist-server/server/included-services.js +70 -0
  86. package/dist-server/server/index.js +438 -102
  87. package/dist-server/server/model-context-window.js +4 -4
  88. package/dist-server/server/package-export.js +17 -10
  89. package/dist-server/server/package-import.js +6 -3
  90. package/dist-server/server/package-parts.js +3 -1
  91. package/dist-server/server/request-auth.js +2 -0
  92. package/dist-server/server/routes/decider.js +19 -0
  93. package/dist-server/server/shared-computers.js +116 -30
  94. package/dist-server/server/store.js +17 -2
  95. package/dist-server/server/surface.js +13 -0
  96. package/dist-server/server/system-prompt.js +11 -0
  97. package/dist-server/server/team-backup.js +6 -2
  98. package/dist-server/server/team-manifest.js +11 -3
  99. package/dist-server/server/tts/elevenlabs.js +11 -6
  100. package/dist-server/server/tts/index.js +17 -10
  101. package/dist-server/server/workspace-backup-policy.js +22 -3
  102. package/dist-server/server/workspace-backup.js +25 -8
  103. package/dist-server/shared/cloud-home.js +4 -0
  104. package/dist-server/shared/package-format.js +30 -4
  105. package/dist-server/shared/team-backup.js +20 -3
  106. package/dist-server/vps-container-mcp.js +84 -32
  107. package/dist-server/workspace-backup.worker.js +113 -31
  108. package/package.json +1 -1
  109. package/dist/assets/channel-BCuyHjFV.js +0 -1
  110. package/dist/assets/index-CRvo-DS2.css +0 -1
  111. package/dist/assets/index-CWYGlA63.js +0 -588
  112. 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: sharedComputersEnabled(cfg) }) === "admin",
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: sharedComputersEnabled(cfg) ? "1" : "0",
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 is
1607
- // refused at claim time, whichever path selected it.
1608
- const kind = computerKindForResource(resource), refusal = kind && managedPolicy.computerRefusal(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: "managed_policy" });
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
- return store.createGroup(name, memberIds, false, section, setup);
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.box.token, control: controlIntegration(botId, owner.threadId, owner.generation) },
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.box.token, control: controlIntegration(bot.id, owner.threadId, owner.generation) }
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
- if (instance?.adapter.capabilities.computerMcp && localVmSeen.has(target.key)) {
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
- return Promise.all(["cloud", "vm", "local", "browser"].map(async (surface) => {
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
- let reason = "This computer is not configured or running. Open the Computer panel to set it up.";
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
- const message = store.appendMessage(event.threadId, group && m.role === "bot" ? { ...m, from: speaker }
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 is refused before anything is
7930
- // prepared; Auto below simply skips disallowed places.
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 && managedPolicy.computerRefusal(wantedKind);
8088
+ const placeRefusal = wantedKind && computerPlaceRefusal(wantedKind);
7934
8089
  if (placeRefusal)
7935
- throw Object.assign(new Error(placeRefusal), { status: 409, code: "managed_policy" });
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("Cloud Boat is not configured — add a Boat API key or choose Local VM");
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 && managedPolicy.computerAllowed("localVm") && await attachLocalVm(false))
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
- managedPolicy.computerAllowed("thisComputer") &&
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 is refused before anything is
10248
- // provisioned or started, exactly as a bot thread refuses it.
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 && managedPolicy.computerRefusal(roomKind);
10435
+ const roomPlaceRefusal = roomKind && computerPlaceRefusal(roomKind);
10252
10436
  if (roomPlaceRefusal)
10253
- throw Object.assign(new Error(roomPlaceRefusal), { code: "managed_policy" });
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("Cloud Boat is not configured — add a Boat API key or choose Local VM");
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
- // An engine the organisation disallows never receives the text, even for a title.
11236
- if (titled && snippet && titleText.trim() && llmThreadTitlesEnabled(cfg) && titleInstance?.generateText && !policyModelRefusal(titleInstance)) {
11237
- void generateThreadTitle(titleInstance, titleText)
11238
- .then((title) => {
11239
- if (title)
11240
- store.retitleGroupTask(group.id, threadId, snippet, title);
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
- .catch(() => undefined);
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 responders) {
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
- if (!(await runGroupMemberTurn(groupId, threadId, responder.id, 0, spoken, undefined, undefined, () => operation.cancelled, () => groupProviderHandshakeStarted(operation), () => groupProviderHandshakeSettled(operation), skillAuthoringClaim, undefined, operation)))
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
- for (const key of [
12195
- "XAI_API_KEY",
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
- box: { configured: Boolean(cfg.box?.token) },
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 workspaceBackupRoutes = createWorkspaceBackupRoutes({
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: sharedComputersEnabled(cfg) }));
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: sharedComputersEnabled(cfg) });
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: sharedComputersEnabled(cfg) });
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: sharedComputersEnabled(cfg) },
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
- if (method === "POST" && path === "/api/desktop/shared-computer-control" && sharedComputersEnabled(cfg)) {
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
- if (method === "POST" && path.startsWith("/api/shared-computers/") && sharedComputersEnabled(cfg)) {
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 (!sharedComputersEnabled(cfg))
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
- sharedComputers.register(registration, auth.session.id, secret);
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
- if (method === "GET" && path === "/api/internal/shared-computers" && sharedComputersEnabled(cfg))
13488
- return json(res, 200, { computers: sharedComputers.list() });
13489
- if (method === "POST" && path === "/api/internal/shared-computers" && sharedComputersEnabled(cfg)) {
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 (!sharedComputersEnabled(cfg))
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
- return json(res, 200, { result: await sharedComputers.request(parsed.data, () => sharedComputersEnabled(cfg) && internalCapabilityIsActive(internalCapability)) });
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: { bulletin: redactSecretsInText(parsed.data.bulletin ?? ""), defaultResponder: { kind: "member", botId: chief.id } },
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
- return json(res, 201, { routine: routines.create(body) });
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
- return routines.remove(routineMatch[1])
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: sharedComputersEnabled(cfg) }, loopbackTrust: LOOPBACK.trust, cliOwnerToken,
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 value = body.defaultResponder;
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
- const message = await startTurn(bot.id, text, { threadId: bot.threadId, editedMessageId: messageId, replyTo, sendId, trigger: usageTriggerFor(auth) });
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 (!sharedComputersEnabled(cfg)) {
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
- process.exit(code);
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"]) {