openmausbot 0.1.90 → 0.1.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
- package/dist/assets/index-Cdmjrl28.css +1 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
- package/dist/assets/index-DR7YPMkW.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +128 -15
- package/dist-server/container-mcp.js +277 -45
- package/dist-server/drivers/agents-proxy.js +16 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +46703 -39955
- package/dist-server/local-computer-proxy.js +128 -7
- package/dist-server/mcp-gate.js +11 -5
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4099 -1217
- package/dist-server/pair-cli.js +4099 -1217
- package/dist-server/server/boat.js +67 -22
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +136 -26
- package/dist-server/server/cloud-home.js +64 -5
- package/dist-server/server/cloud-lending.js +167 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/config.js +215 -17
- package/dist-server/server/container-computer.js +3 -2
- 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/desktop-viewer-targets.js +26 -0
- package/dist-server/server/drivers/acp/core.js +167 -21
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/agents-catalog.js +22 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +313 -34
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +41 -3
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +15 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1753 -263
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- 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/provider-key-check.js +4 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +25 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routes/desktop-viewer.js +194 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +79 -124
- 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/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +20 -11
- package/dist-server/server/vps-computer.js +79 -12
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +25 -3
- package/dist-server/server/workspace-backup.js +28 -8
- package/dist-server/server/workspace.js +65 -15
- 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/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +278 -45
- package/dist-server/workspace-backup.worker.js +184 -38
- package/enterprise/server/index.js +5 -1
- 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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// OpenMausBot server — the harness host. Clients hold no transports
|
|
2
2
|
// (upstream rule): the React app dispatches typed commands over HTTP and
|
|
3
3
|
// folds one SSE event stream; every provider process runs here.
|
|
4
|
+
// First, before any module that could start a process: a Cloud home's
|
|
5
|
+
// secrets off the launcher's pipe (cloud-secrets-boot.ts).
|
|
6
|
+
import { BOOT_CLOUD_SECRETS } from "./cloud-secrets-boot.js";
|
|
4
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
8
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
9
|
import { existsSync, readFileSync, realpathSync, rmSync, mkdirSync } from "node:fs";
|
|
@@ -15,7 +18,9 @@ import { selectReplay, DEFAULT_REBUILD_BYTES, MAX_SUMMARY_BYTES } from "./contex
|
|
|
15
18
|
import { draftSummary, foldPoint } from "./compaction-summary.js";
|
|
16
19
|
import { compactBudget, contextWindowFor, shouldCompact } from "./context-budget.js";
|
|
17
20
|
import { autoCompactWindow } from "./drivers/claude.js";
|
|
18
|
-
import { SharedComputers, sharedComputerOperation, sharedComputerRegistration } from "./shared-computers.js";
|
|
21
|
+
import { provenRequestPerson, SharedComputers, sharedComputerOperation, sharedComputerRegistration } from "./shared-computers.js";
|
|
22
|
+
import { cloudHomeLendingRefusal, createCloudRoutineAuthors, ownerOnlyConversation } from "./cloud-lending.js";
|
|
23
|
+
import { botMemoryFiles, createLendingMemory } from "./lending-memory.js";
|
|
19
24
|
import { SharedComputerControl } from "./shared-computer-control.js";
|
|
20
25
|
import { RoomHandoffs } from "./room-handoffs.js";
|
|
21
26
|
import { assertRequestTarget, guardedRequestPath, requestConflict, requestNeedsInput, requestSourceForCard } from "./guarded-requests.js";
|
|
@@ -60,7 +65,7 @@ import { canAccessTeam, canReachPeer, coordinatorSupervises, livePeerRoster, liv
|
|
|
60
65
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
61
66
|
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
67
|
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";
|
|
68
|
+
import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmIdleTimeoutMinutes, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, routinesInConversationEnabled, claudeUserMcpEnabled, skillAuthoringEnabled, autoRecallEnabled, captureQuietMs, tidyHour, sharedComputersEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, computerClaimIdleReleaseEnabled, cloudOverflowAllowlistedThreads, cloudOverflowEnabled, cloudOverflowIdleStopMs, cloudOverflowPerSecondCostUsd, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, stripWorkspaceCredentialEnv, PROVIDER_CREDENTIAL_ENV, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, CLAUDE_API_INSTANCE, } from "./config.js";
|
|
64
69
|
import { sweepThreadEventLogs } from "./thread-retention.js";
|
|
65
70
|
import { ComputerControl } from "./computer-control.js";
|
|
66
71
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -79,10 +84,12 @@ import { isModelVariant, TurnNotStartedError, newId, } from "./contracts.js";
|
|
|
79
84
|
import { RETRY_MAX_ATTEMPTS } from "./drivers/retry.js";
|
|
80
85
|
import { recoveryCapabilityError } from "./automatic-recovery.js";
|
|
81
86
|
import { decodeGeneratedImage } from "./generated-image.js";
|
|
82
|
-
import { MAX_MCP_SERVERS, listMcpServers, mcpServerNameError, parseMcpServerMutation, parseMcpServersImport, parseStoredMcpServer, } from "./mcp-registry.js";
|
|
87
|
+
import { MAX_MCP_SERVERS, isRemoteMcpServer, listMcpServers, mcpServerNameError, parseMcpServerMutation, parseMcpServersImport, parseStoredMcpServer, } from "./mcp-registry.js";
|
|
88
|
+
import { McpOAuthError, McpSignInError, McpOAuthManager, mcpOAuthRedirectUri, withMcpSignIn, withoutPendingSignIn } from "./mcp-oauth.js";
|
|
83
89
|
import { probeMcpServer } from "./mcp-probe.js";
|
|
84
90
|
import { GROUP_GOAL_MAX_TURNS, groupGoalAssignmentKey, groupGoalCompletionTurnId, groupGoalCoordinatorInstructions, groupGoalWorkerInstructions, parseGroupGoalDecision, resolveGroupGoalMember, selectGroupGoalCoordinator, } from "./group-goal-run.js";
|
|
85
91
|
import { BUILT_IN_DRIVERS } from "./drivers/builtIn.js";
|
|
92
|
+
import { openCodeProviderKeysAllowed, setOpenCodeProviderKeyPolicy } from "./drivers/acp/opencode-go.js";
|
|
86
93
|
import { getOrCreateChannel, mirrorActivity, mirrorExchange, mirrorReply } from "./comms-visibility.js";
|
|
87
94
|
import { readMessageText, recallMessages, recentMessages, searchMessagesAsync, closeMessageSearch, closeMessageDb, chatFollowups, cancelledChatFollowup, settleChatFollowups, threadsReferencing } from "./message-db.js";
|
|
88
95
|
import { briefCrossingLabel, claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.js";
|
|
@@ -103,7 +110,10 @@ import { ProviderRegistry } from "./harness/registry.js";
|
|
|
103
110
|
import { ManagedDesktopProviders } from "./managed-desktop.js";
|
|
104
111
|
import { computerKindForResource, ManagedDesktopPolicy } from "./managed-policy.js";
|
|
105
112
|
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";
|
|
113
|
+
import { boatNotConfiguredMessage, CLOUD_HOME_RESTART_EXIT_CODE, CLOUD_HOME_SECRET_KEYS, CLOUD_IGNORED_KEYS, CLOUD_PAIRING_PATH, cloudHomeConfiguration, cloudHomeOffersPlace, cloudHomePlaceRefusal, createCloudPairing, firstCloudTurnPatch, readSignedBody, } from "./cloud-home.js";
|
|
114
|
+
import { CLOUD_PERSONAL_REFUSAL, settleCloudOwnership } from "./cloud-owner.js";
|
|
115
|
+
import { createCloudMoveRoutes } from "./cloud-move-http.js";
|
|
116
|
+
import { holdIncludedServices } from "./included-services.js";
|
|
107
117
|
import { selectDefaultModelSelection, withNewBotEffort } from "./default-model-selection.js";
|
|
108
118
|
import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, peerApprovalFailure, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
|
|
109
119
|
import { peerDeliveryReceipt } from "./peer-delivery.js";
|
|
@@ -111,14 +121,17 @@ import { peerProvenanceNote, withPeerProvenance } from "./peer-provenance.js";
|
|
|
111
121
|
import { decideRoomPost, emptyRoomPostBudget } from "./room-post-budget.js";
|
|
112
122
|
import { isProjectEmoji, mentionedBots, parseConnectorTools, roomResponders, sectionKey, Store, titleFromLlm, toWireTask, } from "./store.js";
|
|
113
123
|
import * as tts from "./tts/index.js";
|
|
124
|
+
import { createDecider, deciderIncludedHere, deciderReady, deciderSavePatch, describeDecider } from "./decider/index.js";
|
|
125
|
+
import { decideRoomResponder } from "./decider/room-routing.js";
|
|
114
126
|
import { narrateTool, toUtterances } from "./tts/speech-text.js";
|
|
115
127
|
import { buildRecoveryText, buildTurnContext, engineIsFresh, NATIVELY_REPLAYING_DRIVER_KINDS, peerMessageText } from "./turn-context.js";
|
|
116
128
|
import { Handoffs, handedStateUsable, recordHanded, renderUnseen, sessionStart, unseenMessages, withUnseenMessages } from "./delta-context.js";
|
|
117
129
|
import { extractTurnImages } from "./turn-images.js";
|
|
130
|
+
import { threadTitlePrompt, titleConversationExcerpt } from "./thread-title.js";
|
|
118
131
|
import { TurnWatchdog } from "./turn-watchdog.js";
|
|
119
132
|
import { TurnResources, workspaceResource } from "./turn-resources.js";
|
|
120
133
|
import { IdleReleasePolicy } from "./claim-idle.js";
|
|
121
|
-
import { ensureWorkspace, ensureTaskWorkspace, workspaceLocationsPrompt, supportsWorkspaceFiles, updateMemory, appendMemoryLog, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, workspaceDir, } from "./workspace.js";
|
|
134
|
+
import { ensureWorkspace, ensureTaskWorkspace, workspaceLocationsPrompt, supportsWorkspaceFiles, updateMemory, appendMemoryLog, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, TASK_WORKSPACES_DIR, readMemoryOnlyFromRegularFiles, workspaceDir, } from "./workspace.js";
|
|
122
135
|
import { listMemoryTopics, memoryDate, readMemoryFile, readMemoryTopic, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
|
|
123
136
|
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, journalMemoryWrite, } from "./memory-journal.js";
|
|
124
137
|
import { readSectionContext, readSections, sectionContextKey, sectionContextLabel, sectionContextSystemPrompt, writeSectionContext, SECTION_CONTEXT_MAX_BYTES, } from "./section-context.js";
|
|
@@ -129,7 +142,7 @@ import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefault
|
|
|
129
142
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
130
143
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
131
144
|
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";
|
|
145
|
+
import { buildSystemPrompt, userProfileSystemPrompt, cloudHomePrompt, computerPrompt, composioSystemPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, resolveComputerPromptKind, } from "./system-prompt.js";
|
|
133
146
|
import { readCuaConnection, readCuaUnavailableReason, gatedLocalComputer } from "./local-computer.js";
|
|
134
147
|
import { discoverExistingPerBotLocalVms, discoverExistingPoolLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
135
148
|
import { LocalVmIdleTimer } from "./local-vm-idle.js";
|
|
@@ -141,12 +154,12 @@ import * as vps from "./vps-computer.js";
|
|
|
141
154
|
import { RoutineManager } from "./routines.js";
|
|
142
155
|
import { CalendarCallManager } from "./calendar-calls.js";
|
|
143
156
|
import { BUILT_IN_BROWSER_SYSTEM_PROMPT } from "./browser-engine.js";
|
|
144
|
-
import { BrowserRuntime } from "./browser-runtime.js";
|
|
157
|
+
import { BrowserRuntime, browserRuntimeEnv } from "./browser-runtime.js";
|
|
145
158
|
import { BrowserLive } from "./browser-live.js";
|
|
146
|
-
import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey, prepareBrowserSessionState, clearBrowserSessionState, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
|
|
159
|
+
import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey, prepareBrowserSessionState, clearBrowserSessionState, closeBrowserSession, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
|
|
147
160
|
import { createScreenFrameSource } from "./screen-frame-source.js";
|
|
148
161
|
import { screenFrameHash, screenSurfaceForTool, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
|
|
149
|
-
import { RoutineRequestService } from "./routine-requests.js";
|
|
162
|
+
import { asSchedule, RoutineRequestService } from "./routine-requests.js";
|
|
150
163
|
import { createOptionsCard } from "./options-card.js";
|
|
151
164
|
import { buildBotOverview, connectedAppsFacts } from "./bot-overview.js";
|
|
152
165
|
import { ProfileRequestService } from "./profile-requests.js";
|
|
@@ -208,6 +221,11 @@ import { ROUTES, dispatchRoutes } from "./routes/table.js";
|
|
|
208
221
|
import { createHostedSlackRoutes } from "./routes/hosted-slack.js";
|
|
209
222
|
import { createBotPresetRoutes } from "./routes/bot-presets.js";
|
|
210
223
|
import { createBotMemoryRoutes } from "./routes/bot-memory.js";
|
|
224
|
+
import { createDeciderRoutes } from "./routes/decider.js";
|
|
225
|
+
import { createDesktopViewer, desktopViewerUrl } from "./routes/desktop-viewer.js";
|
|
226
|
+
import { localDesktopTarget, localVmViewerStatus, viewerTargetId } from "./desktop-viewer-targets.js";
|
|
227
|
+
import { createAntigravityLeftoverRoutes } from "./routes/antigravity-leftovers.js";
|
|
228
|
+
import { findAntigravityLeftovers, removeAntigravityLeftovers } from "./drivers/antigravity-temp.js";
|
|
211
229
|
const PORT = Number(process.env.OMB_PORT || process.env.OGB_PORT || 8799);
|
|
212
230
|
const WEBHOOK_PORT = Number(process.env.OMB_WEBHOOK_PORT || PORT + 1);
|
|
213
231
|
// Behind a proxy or tunnel, the base URL senders should use (docs/self-hosting.md).
|
|
@@ -217,6 +235,7 @@ const MIME = {
|
|
|
217
235
|
".html": "text/html",
|
|
218
236
|
".js": "text/javascript",
|
|
219
237
|
".css": "text/css",
|
|
238
|
+
".txt": "text/plain; charset=utf-8",
|
|
220
239
|
".svg": "image/svg+xml",
|
|
221
240
|
".png": "image/png",
|
|
222
241
|
".ico": "image/x-icon",
|
|
@@ -246,8 +265,12 @@ process.once("exit", releaseDataDirLeaseAtExit);
|
|
|
246
265
|
// Restore before constructing any long-lived config, Store, session or provider
|
|
247
266
|
// objects. Replacing files underneath a live Store would overwrite restored data.
|
|
248
267
|
let workspaceRestore = { restored: false };
|
|
268
|
+
/** A restore applied at this very start (not the last one on record). */
|
|
269
|
+
let restoredAtStart;
|
|
249
270
|
if (existsSync(join(DATA_DIR, ".backups"))) {
|
|
250
271
|
workspaceRestore = applyPendingWorkspaceRestore(DATA_DIR);
|
|
272
|
+
if (workspaceRestore.restored)
|
|
273
|
+
restoredAtStart = workspaceRestore.id;
|
|
251
274
|
if (!workspaceRestore.restored && !workspaceRestore.rolledBack) {
|
|
252
275
|
workspaceRestore = readLastWorkspaceRestore(DATA_DIR) ?? workspaceRestore;
|
|
253
276
|
}
|
|
@@ -273,7 +296,50 @@ const sessions = new SessionRegistry({
|
|
|
273
296
|
const sharedComputers = new SharedComputers(id => sessions.isLive(id));
|
|
274
297
|
// OMB Cloud Pro home machine (server/cloud-home.ts, docs/cloud-pro.md). A
|
|
275
298
|
// partial or invalid boot contract stops the server here, before it serves.
|
|
276
|
-
|
|
299
|
+
// Its secrets come over the launcher's pipe, never this process's
|
|
300
|
+
// environment (cloud-home-start.ts); a server started another way (tests,
|
|
301
|
+
// development) still reads them from its environment.
|
|
302
|
+
const CLOUD_SECRETS = BOOT_CLOUD_SECRETS;
|
|
303
|
+
const CLOUD_ENV = { ...process.env, ...CLOUD_SECRETS };
|
|
304
|
+
const CLOUD_HOME = cloudHomeConfiguration(CLOUD_ENV);
|
|
305
|
+
// A Cloud home is personal: only the owner's own devices, each with admin
|
|
306
|
+
// scope, connect (server/cloud-owner.ts; the stored rest is revoked below).
|
|
307
|
+
if (CLOUD_HOME)
|
|
308
|
+
sessions.requireAdmin(CLOUD_PERSONAL_REFUSAL);
|
|
309
|
+
/** Lending a computer to this server (the shared-computer routes, the two
|
|
310
|
+
* agent tools, the advertised capability): the maintainer flag anywhere, and
|
|
311
|
+
* always on an OMB Cloud home, where it is the person's own Mac lent to their
|
|
312
|
+
* own Cloud (docs/cloud-pro.md, "Let my Cloud use this Mac"). */
|
|
313
|
+
const lendingEnabled = () => sharedComputersEnabled(cfg) || CLOUD_HOME !== null;
|
|
314
|
+
/** On a Cloud home every lent computer and every turn belong to one person. */
|
|
315
|
+
const CLOUD_HOME_LENDER = "cloud-home";
|
|
316
|
+
/** Routines whose instructions the Cloud home's owner wrote, and manual runs
|
|
317
|
+
* the owner started: the only routine turns that may use a lent Mac. */
|
|
318
|
+
const cloudRoutineAuthors = CLOUD_HOME ? createCloudRoutineAuthors(join(DATA_DIR, "lending-routines.json")) : null;
|
|
319
|
+
const ownerStartedRoutineRuns = new Set();
|
|
320
|
+
/** Turns running now that are not provably the owner's, by generation. */
|
|
321
|
+
const foreignTurns = new Map();
|
|
322
|
+
/** Whether each bot's memory is still the owner's (server/lending-memory.ts):
|
|
323
|
+
* its memory files, and the instruction files its engine reads in the
|
|
324
|
+
* folders its conversations work in. */
|
|
325
|
+
const lendingMemory = CLOUD_HOME ? createLendingMemory({
|
|
326
|
+
file: join(DATA_DIR, "lending-memory.json"),
|
|
327
|
+
files: (botId) => {
|
|
328
|
+
const bot = store.bot(botId);
|
|
329
|
+
// The workspace as a turn dispatch leaves it: a bot's first turn seeds
|
|
330
|
+
// MEMORY.md, and that seed is not a change anyone made. Only the folders
|
|
331
|
+
// of conversations the owner opened: what a guest's own conversation
|
|
332
|
+
// writes in its own private folder reaches no turn of the owner's.
|
|
333
|
+
// (A bot's own conversations are never a room's: its opener is enough.)
|
|
334
|
+
const ownerOpened = (threadId) => { const starter = threadStarters.get(threadId); return starter === undefined || cloudOwnerPerson(starter); };
|
|
335
|
+
return botMemoryFiles(bot ? { id: bot.id, cwd: bot.cwd, tasks: store.tasks(bot.id).filter((task) => ownerOpened(task.threadId)) } : undefined, { workspace: ensureWorkspace, taskWorkspaces: TASK_WORKSPACES_DIR });
|
|
336
|
+
},
|
|
337
|
+
knownBots: () => store.bots.map((bot) => bot.id),
|
|
338
|
+
log: (line) => console.warn(line),
|
|
339
|
+
}) : null;
|
|
340
|
+
// On a Cloud home memory is never read through a link (server/workspace.ts).
|
|
341
|
+
if (CLOUD_HOME)
|
|
342
|
+
readMemoryOnlyFromRegularFiles();
|
|
277
343
|
const cloudPairing = CLOUD_HOME ? createCloudPairing({ secret: CLOUD_HOME.bootstrapSecret, sessions }) : null;
|
|
278
344
|
if (CLOUD_HOME) {
|
|
279
345
|
// The signing secret is held in memory from here on, and a platform
|
|
@@ -286,6 +352,14 @@ if (CLOUD_HOME) {
|
|
|
286
352
|
for (const warning of CLOUD_HOME.warnings)
|
|
287
353
|
console.warn(`cloud home: ${warning}`);
|
|
288
354
|
}
|
|
355
|
+
// Cloud Pro's included Boat, voice and decision relay tokens, when the Admin
|
|
356
|
+
// set them: held in memory from here on, like the signing secret.
|
|
357
|
+
holdIncludedServices(CLOUD_ENV);
|
|
358
|
+
for (const key of CLOUD_HOME_SECRET_KEYS)
|
|
359
|
+
delete process.env[key];
|
|
360
|
+
if (CLOUD_HOME && Object.keys(CLOUD_SECRETS).length === 0) {
|
|
361
|
+
console.warn("cloud home: its secrets came in this process's environment, which anything running as the same user can read in /proc; start it with cloud-home-start");
|
|
362
|
+
}
|
|
289
363
|
// Who each thread is for, when a signed-in person can be named (server-private).
|
|
290
364
|
const threadStarters = new ThreadStarters(join(DATA_DIR, "thread-starters.json"));
|
|
291
365
|
const commandAllowlist = new CommandAllowlistStore(join(DATA_DIR, "command-allowlist.json"));
|
|
@@ -298,7 +372,7 @@ const sharedWorkspaceFullAccessEnabled = () => SHARED_WORKSPACE_FULL_ACCESS && B
|
|
|
298
372
|
// Who a loopback request without a session is (server/request-auth.ts
|
|
299
373
|
// LoopbackTrust): the owner on a desktop or a one-person server; a service on
|
|
300
374
|
// a shared workspace, where every bot's shell is a loopback caller too.
|
|
301
|
-
const LOOPBACK = resolveLoopbackTrust({ desktopManaged: DESKTOP_MANAGED, hostedWorkspace: HOSTED_WORKSPACE });
|
|
375
|
+
const LOOPBACK = resolveLoopbackTrust({ desktopManaged: DESKTOP_MANAGED, hostedWorkspace: HOSTED_WORKSPACE, cloudHome: Boolean(CLOUD_HOME) });
|
|
302
376
|
// `openmausbot serve` on a service-trust server hands the server it starts a
|
|
303
377
|
// per-launch secret on stdin, then closes it (server/cli.ts). It opens only
|
|
304
378
|
// the pairing route, for that CLI. Never an environment variable: every
|
|
@@ -341,6 +415,9 @@ bindThreadLogCapProvider(() => threadEventLogMaxBytes(cfg));
|
|
|
341
415
|
// The decision log keeps month files for at least this many days. Read per
|
|
342
416
|
// prune, so a Settings change applies at the next one without a restart.
|
|
343
417
|
bindDecisionRetention(() => decisionRetentionDays(cfg.decisions?.retentionDays));
|
|
418
|
+
// The decision model (server/decider): reads cfg per call, so a Settings
|
|
419
|
+
// save applies to the next decision. It never throws into a turn.
|
|
420
|
+
const decider = createDecider({ config: () => cfg, dataDir: DATA_DIR });
|
|
344
421
|
const customDomainVerifier = createCustomDomainVerifier({ environmentId: ENVIRONMENT_ID });
|
|
345
422
|
// "Sign in with your email" on /pair: the allow-list is read per call so a
|
|
346
423
|
// Settings change or an env bootstrap applies without a restart.
|
|
@@ -393,7 +470,7 @@ function messageSender(auth) {
|
|
|
393
470
|
if (auth.kind !== "session")
|
|
394
471
|
return undefined;
|
|
395
472
|
const name = (auth.session.email ?? auth.session.label ?? "").trim();
|
|
396
|
-
return name ? { name, id:
|
|
473
|
+
return name ? { name, id: actorKey(auth) } : undefined;
|
|
397
474
|
}
|
|
398
475
|
/** An opaque, stable key for the person behind a session: their account
|
|
399
476
|
* email when they signed in with one (a new device is still them), else the
|
|
@@ -438,6 +515,404 @@ function threadPersonKey(threadId) {
|
|
|
438
515
|
: thread.findLast((message) => message.role === "user");
|
|
439
516
|
return linePersonKey(request) ?? threadStarters.get(threadId);
|
|
440
517
|
}
|
|
518
|
+
/** The person whose lent computers this bot turn may use, or null.
|
|
519
|
+
* Fail closed: only the person whose own message the harness proved started
|
|
520
|
+
* this exact turn generation (continuations of that same request keep it).
|
|
521
|
+
* A routine, a webhook, a room, a bot's delegated or peer turn, the owner on
|
|
522
|
+
* this machine (who lends nothing to itself) and anything unprovable act for
|
|
523
|
+
* nobody here. Deliberately not the usage ledger's trigger, which defaults to
|
|
524
|
+
* the owner. */
|
|
525
|
+
function sharedComputerPrincipal(capability) {
|
|
526
|
+
// A Cloud home lends the owner's Mac only to turns that provably act for
|
|
527
|
+
// the owner (server/cloud-lending.ts): their own conversation from one of
|
|
528
|
+
// their devices, or a routine they wrote. Never a guest, a webhook, a room
|
|
529
|
+
// or a line someone else slipped in.
|
|
530
|
+
if (CLOUD_HOME)
|
|
531
|
+
return cloudLendingRefusal(capability) === null ? CLOUD_HOME_LENDER : null;
|
|
532
|
+
return provenRequestPerson(directRequestOwners.get(capability.threadId), capability.generation, (messageId) => linePersonKey(store.messagesFor(capability.threadId).find((message) => message.id === messageId)));
|
|
533
|
+
}
|
|
534
|
+
/** What the lending rule needs to know about one bot turn on a Cloud home. */
|
|
535
|
+
function cloudLendingTurn(capability) {
|
|
536
|
+
return {
|
|
537
|
+
request: directRequestOwners.get(capability.threadId),
|
|
538
|
+
generation: capability.generation,
|
|
539
|
+
thread: store.messagesFor(capability.threadId),
|
|
540
|
+
starters: cloudThreadStarters(capability.threadId),
|
|
541
|
+
reportsFromOthers: store.messagesFor(capability.threadId).some(cloudOthersRoutineReport),
|
|
542
|
+
ownerPerson: cloudProvenOwnerPerson,
|
|
543
|
+
cardAnswerer: cloudCardAnswerer,
|
|
544
|
+
routineRun: () => {
|
|
545
|
+
const run = activeRoutineRunForThread(capability.threadId);
|
|
546
|
+
if (!run || run.threadId !== capability.threadId || store.taskByThread(capability.botId, capability.threadId)?.routineRunId !== run.id)
|
|
547
|
+
return null;
|
|
548
|
+
return {
|
|
549
|
+
triggerSource: run.triggerSource ?? (run.manual ? "manual" : "schedule"),
|
|
550
|
+
ownerStarted: ownerStartedRoutineRuns.has(run.id),
|
|
551
|
+
// What the run snapshotted, on the schedule its routine has now.
|
|
552
|
+
ownerAuthored: cloudRoutineRunIsOwners(run, capability.botId, capability.threadId),
|
|
553
|
+
};
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/** What a bot on a Cloud home is told when its conversation cannot use the
|
|
558
|
+
* owner's lent Mac because someone else wrote in it. The bot relays it. */
|
|
559
|
+
const LENDING_SOMEONE_ELSE = "Someone else wrote in this conversation, so it can't use your Mac. Start a new conversation to use it.";
|
|
560
|
+
/** …or because its memory was changed where the owner did not write. */
|
|
561
|
+
const LENDING_MEMORY_CHANGED = "This bot's memory was changed in a conversation you didn't write. Review it in Memory to use your Mac again.";
|
|
562
|
+
/** Why this bot turn may not use the owner's lent Mac on a Cloud home, or
|
|
563
|
+
* null: the conversation's provenance (server/cloud-lending.ts), then the
|
|
564
|
+
* bot's memory, which every one of its turns loads (server/lending-memory.ts). */
|
|
565
|
+
function cloudLendingRefusal(capability) {
|
|
566
|
+
const refusal = cloudHomeLendingRefusal(cloudLendingTurn(capability));
|
|
567
|
+
if (refusal)
|
|
568
|
+
return refusal;
|
|
569
|
+
// Checked on every Mac action: one snapshot per tick is enough to look.
|
|
570
|
+
return lendingMemory?.reconcile(capability.botId, foreignTurnRunning(capability.botId), { cached: true }).changedBySomeoneElse ? "memory-changed" : null;
|
|
571
|
+
}
|
|
572
|
+
/** Who opened a conversation, and the room it is in: on a Cloud home a
|
|
573
|
+
* person who can name, title and steer it (see cloudThreadRefusal). */
|
|
574
|
+
function cloudThreadStarters(threadId) {
|
|
575
|
+
const room = store.groupByThread(threadId);
|
|
576
|
+
return [threadStarters.get(threadId), ...(room && room.threadId !== threadId ? [threadStarters.get(room.threadId)] : [])];
|
|
577
|
+
}
|
|
578
|
+
/** On a Cloud home: a turn that may use the owner's lent Mac, as far as its
|
|
579
|
+
* conversation goes. Such a turn reads only the owner's own conversations
|
|
580
|
+
* (session_search, session_read, list_threads). */
|
|
581
|
+
function cloudLendingView(capability) {
|
|
582
|
+
return CLOUD_HOME !== null && cloudHomeLendingRefusal(cloudLendingTurn(capability)) === null;
|
|
583
|
+
}
|
|
584
|
+
/** On a Cloud home: a routine's report (its lifecycle card, carrying what
|
|
585
|
+
* the run said) of a routine the owner did not write, as it stands now. It
|
|
586
|
+
* brings someone else's instructions' results into the conversation it
|
|
587
|
+
* reports to. */
|
|
588
|
+
function cloudOthersRoutineReport(line) {
|
|
589
|
+
if (line.kind !== "routine.run" || !line.routineRun)
|
|
590
|
+
return false;
|
|
591
|
+
const routine = routines?.listRoutines().find((candidate) => candidate.id === line.routineRun.routineId);
|
|
592
|
+
return !routine || cloudRoutineAuthors?.authored(routine.id, routine) !== true;
|
|
593
|
+
}
|
|
594
|
+
/** On a Cloud home: a conversation of this bot that only its owner opened
|
|
595
|
+
* and wrote in (server/cloud-lending.ts ownerOnlyConversation), the only kind
|
|
596
|
+
* that may feed the bot's memory, daily log, recall or recent-work brief, or
|
|
597
|
+
* be quoted there by its title. Cached per thread until it gets another
|
|
598
|
+
* message, and dropped whenever a session is revoked. */
|
|
599
|
+
const ownerOnlyCache = new Map();
|
|
600
|
+
function cloudOwnerOnlyThread(threadId) {
|
|
601
|
+
if (cloudThreadStarters(threadId).some((person) => person !== undefined && !cloudProvenOwnerPerson(person)))
|
|
602
|
+
return false;
|
|
603
|
+
const thread = store.messagesFor(threadId);
|
|
604
|
+
const last = thread.at(-1);
|
|
605
|
+
const cached = ownerOnlyCache.get(threadId);
|
|
606
|
+
// A card answered later changes the answer too: key on its answerer as well.
|
|
607
|
+
const lastKey = last ? `${last.id}:${last.card?.answeredBy ? JSON.stringify(last.card.answeredBy) : ""}` : undefined;
|
|
608
|
+
if (cached && cached.length === thread.length && cached.last === lastKey)
|
|
609
|
+
return cached.owner;
|
|
610
|
+
const owner = ownerOnlyConversation(thread, cloudProvenOwnerPerson, cloudCardAnswerer) && !thread.some(cloudOthersRoutineReport);
|
|
611
|
+
if (ownerOnlyCache.size > 5_000)
|
|
612
|
+
ownerOnlyCache.clear();
|
|
613
|
+
ownerOnlyCache.set(threadId, { length: thread.length, last: lastKey, owner });
|
|
614
|
+
return owner;
|
|
615
|
+
}
|
|
616
|
+
/** On a Cloud home: a turn that is not provably the owner's starts for this
|
|
617
|
+
* bot (its memory is "pending" until the turn's generation ends:
|
|
618
|
+
* server/lending-memory.ts; see revokeInternalCapabilityGeneration). */
|
|
619
|
+
function noteForeignTurn(botId, threadId, generation) {
|
|
620
|
+
if (!lendingMemory)
|
|
621
|
+
return;
|
|
622
|
+
foreignTurns.set(generation, { botId, threadId });
|
|
623
|
+
// The record is updated in memory before it is saved: a failed save still
|
|
624
|
+
// leaves the bot pending, and must not fail the turn that is starting
|
|
625
|
+
// (whose end is what clears this entry).
|
|
626
|
+
try {
|
|
627
|
+
lendingMemory.noteForeignTurn(botId);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
console.warn(`lending: could not record a turn someone else started: ${error.message}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function foreignTurnRunning(botId) {
|
|
634
|
+
return [...foreignTurns.values()].some((turn) => turn.botId === botId);
|
|
635
|
+
}
|
|
636
|
+
/** Whose lent computers a status reader may see: on a Cloud home, only the
|
|
637
|
+
* owner's own devices (admin sessions) see the lent Mac, never a guest or a
|
|
638
|
+
* local process; elsewhere, the person behind the asking session. */
|
|
639
|
+
function sessionLendingPrincipal(auth) {
|
|
640
|
+
if (CLOUD_HOME)
|
|
641
|
+
return cloudOwnerSession(auth) ? CLOUD_HOME_LENDER : null;
|
|
642
|
+
return auth.kind === "session" ? personKey(auth.session) : null;
|
|
643
|
+
}
|
|
644
|
+
/** Profile fields only the owner may change on a Cloud home: what rides the
|
|
645
|
+
* bot's prompt, and whether it notifies the owner. */
|
|
646
|
+
const CLOUD_OWNER_PROFILE_FIELDS = ["name", "title", "description", "soul", "notifications"];
|
|
647
|
+
/** On a Cloud home: why this request may not act on a conversation, or
|
|
648
|
+
* null. The owner's conversations feed the owner's turns (and a lent Mac),
|
|
649
|
+
* so only one of the owner's own devices may rename them, change how they
|
|
650
|
+
* run, rewrite or delete them; anyone else only a conversation they started
|
|
651
|
+
* themselves (a guest's own session). Sends are checked by
|
|
652
|
+
* cloudGuestSendRefusal instead. Elsewhere always null. */
|
|
653
|
+
function cloudThreadRefusal(auth, threadId) {
|
|
654
|
+
if (!CLOUD_HOME || cloudOwnerSession(auth))
|
|
655
|
+
return null;
|
|
656
|
+
if (auth.kind === "session" && threadStarters.get(threadId) === actorKey(auth))
|
|
657
|
+
return null;
|
|
658
|
+
return "On this Cloud you can only change conversations you started.";
|
|
659
|
+
}
|
|
660
|
+
/** On a Cloud home: why a paired guest may not send into a conversation, or
|
|
661
|
+
* null. A guest writes only in conversations it started, never in the
|
|
662
|
+
* owner's (a line steered into the owner's running turn would reach it
|
|
663
|
+
* before anything could reclassify that turn). Local services (loopback)
|
|
664
|
+
* keep their own conversations; their lines make a conversation someone
|
|
665
|
+
* else's all the same (see noteSomeoneElsesLine). */
|
|
666
|
+
function cloudGuestSendRefusal(auth, threadId) {
|
|
667
|
+
if (!CLOUD_HOME || auth.kind !== "session" || cloudOwnerSession(auth))
|
|
668
|
+
return null;
|
|
669
|
+
return threadStarters.get(threadId) === actorKey(auth)
|
|
670
|
+
? null : "On this Cloud you can only write in conversations you started. Start a new conversation with this bot.";
|
|
671
|
+
}
|
|
672
|
+
/** On a Cloud home: the one key every line, card answer, conversation and
|
|
673
|
+
* routine of the owner's own devices carries, whichever device and however
|
|
674
|
+
* often it was paired again, so re-pairing never demotes what the owner
|
|
675
|
+
* wrote. Only an admin session is given it: a guest keeps its own key. */
|
|
676
|
+
const CLOUD_OWNER_KEY = CLOUD_HOME ? `p_${createHash("sha256").update(`cloud-owner:${CLOUD_HOME.machineId}`).digest("base64url").slice(0, 22)}` : null;
|
|
677
|
+
/** …and the key of nobody at all: what a conversation made for work no
|
|
678
|
+
* person can be named for (a routine with no recorded writer) opens as. */
|
|
679
|
+
const CLOUD_NOBODY_KEY = `p_${createHash("sha256").update("cloud-nobody").digest("base64url").slice(0, 22)}`;
|
|
680
|
+
/** A Cloud home is personal (server/cloud-owner.ts): the keys its owner's
|
|
681
|
+
* devices carried before they shared one, settled at boot once the routines
|
|
682
|
+
* are loaded (settleCloudOwnership, below). `adopted` decides who opened a
|
|
683
|
+
* conversation, its level and its folder; only `proven` feeds lending and
|
|
684
|
+
* memory. Nothing is either until then. */
|
|
685
|
+
let cloudEarlierOwners = { adopted: new Set(), proven: new Set() };
|
|
686
|
+
/** Who a session acts as, for lines, openers and answers: on a Cloud home
|
|
687
|
+
* one of the owner's own devices is the owner; anyone else is themselves. */
|
|
688
|
+
function actorKey(auth) {
|
|
689
|
+
return CLOUD_OWNER_KEY && auth.scopes.includes("admin") ? CLOUD_OWNER_KEY : personKey(auth.session);
|
|
690
|
+
}
|
|
691
|
+
/** On a Cloud home: whoever is writing a routine in this request (routine
|
|
692
|
+
* creation and edits can open its results conversation). */
|
|
693
|
+
let routineWriterInFlight;
|
|
694
|
+
function withRoutineWriter(writer, work) {
|
|
695
|
+
const previous = routineWriterInFlight;
|
|
696
|
+
routineWriterInFlight = writer;
|
|
697
|
+
try {
|
|
698
|
+
return work();
|
|
699
|
+
}
|
|
700
|
+
finally {
|
|
701
|
+
routineWriterInFlight = previous;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/** On a Cloud home: a routine the owner approved (its card, or a proposal
|
|
705
|
+
* applied at once in their own Full-access conversation) is theirs as it
|
|
706
|
+
* stands: their key as its writer, their fingerprint on it. */
|
|
707
|
+
function cloudOwnersRoutine(routineId) {
|
|
708
|
+
const routine = routineId ? routines?.listRoutines().find((candidate) => candidate.id === routineId) : undefined;
|
|
709
|
+
if (!routine || !cloudRoutineAuthors || !CLOUD_OWNER_KEY)
|
|
710
|
+
return;
|
|
711
|
+
cloudRoutineAuthors.record(routine.id, routine);
|
|
712
|
+
cloudRoutineAuthors.wrote(routine.id, CLOUD_OWNER_KEY);
|
|
713
|
+
}
|
|
714
|
+
/** On a Cloud home: a routine that is the owner's as it stands: their
|
|
715
|
+
* fingerprint matches what it runs, or they are its writer and no
|
|
716
|
+
* fingerprint of theirs was ever recorded (a template, a restore). */
|
|
717
|
+
function cloudRoutineIsOwners(routineId) {
|
|
718
|
+
const routine = routineId ? routines?.listRoutines().find((candidate) => candidate.id === routineId) : undefined;
|
|
719
|
+
if (!routine || !cloudRoutineAuthors || !CLOUD_OWNER_KEY)
|
|
720
|
+
return false;
|
|
721
|
+
return cloudRoutineAuthors.authored(routine.id, routine) ||
|
|
722
|
+
(cloudRoutineAuthors.writer(routine.id) === CLOUD_OWNER_KEY && !cloudRoutineAuthors.recorded(routine.id));
|
|
723
|
+
}
|
|
724
|
+
/** On a Cloud home: a routine run that acts for the owner. Only in the run's
|
|
725
|
+
* own thread, and only while the owner's fingerprint matches what the run
|
|
726
|
+
* snapshotted, the same test as lending the Mac. A routine that is the
|
|
727
|
+
* owner's but not yet cleared for the Mac (no fingerprint: a v0.1.91
|
|
728
|
+
* approval, a template, a restore) can't clear itself or others this way. */
|
|
729
|
+
function cloudRoutineRunIsOwners(run, botId, threadId) {
|
|
730
|
+
if (!run || run.threadId !== threadId || store.taskByThread(botId, threadId)?.routineRunId !== run.id || !cloudRoutineAuthors)
|
|
731
|
+
return false;
|
|
732
|
+
const routine = routines?.listRoutines().find((candidate) => candidate.id === run.routineId);
|
|
733
|
+
return Boolean(routine) && cloudRoutineAuthors.authored(run.routineId, {
|
|
734
|
+
...routine, prompt: run.prompt, target: run.target, botId: run.botId, groupId: run.groupId, attachments: run.attachments, runOn: run.runOn,
|
|
735
|
+
}) === true;
|
|
736
|
+
}
|
|
737
|
+
/** After the owner approved a routine request (its card, or at once in their
|
|
738
|
+
* own conversation): a routine it created is theirs; one it changed, paused
|
|
739
|
+
* or resumed stays theirs only if it already was (`wasOwners`, taken before
|
|
740
|
+
* the change). Approving a harmless change never makes anyone else's routine
|
|
741
|
+
* the owner's. */
|
|
742
|
+
function cloudOwnerApplied(action, routineId, wasOwners) {
|
|
743
|
+
if (action === "create" || ((action === "update" || action === "pause" || action === "resume") && wasOwners))
|
|
744
|
+
cloudOwnersRoutine(routineId);
|
|
745
|
+
}
|
|
746
|
+
/** What the owner saw on a card that proposed a routine, and what a routine
|
|
747
|
+
* runs, in the same terms (everything its fingerprint covers except an
|
|
748
|
+
* interval's anchor, which only moves when it runs). */
|
|
749
|
+
function approvalShape(shape) {
|
|
750
|
+
const schedule = shape.schedule && typeof shape.schedule === "object" ? { ...shape.schedule } : null;
|
|
751
|
+
if (schedule)
|
|
752
|
+
delete schedule.anchorAt;
|
|
753
|
+
return JSON.stringify([shape.prompt ?? "", shape.botId ?? "", shape.runOn ?? "maus", schedule, shape.target ?? "bot", shape.groupId ?? null,
|
|
754
|
+
(shape.attachments ?? []).map((attachment) => [attachment.id, attachment.path])]);
|
|
755
|
+
}
|
|
756
|
+
/** Who opens a routine's results conversation on a Cloud home: the writer
|
|
757
|
+
* of this request, else the owner for a routine that is theirs (they wrote
|
|
758
|
+
* it as it stands, or they are its writer: cloud-owner.ts), else its last
|
|
759
|
+
* writer, else nobody. */
|
|
760
|
+
function routineOpener(routineId) {
|
|
761
|
+
if (routineWriterInFlight)
|
|
762
|
+
return routineWriterInFlight;
|
|
763
|
+
const routine = routines?.listRoutines().find((candidate) => candidate.id === routineId);
|
|
764
|
+
if (routine && CLOUD_OWNER_KEY && cloudRoutineAuthors?.authored(routineId, routine))
|
|
765
|
+
return CLOUD_OWNER_KEY;
|
|
766
|
+
const writer = cloudRoutineAuthors?.writer(routineId);
|
|
767
|
+
if (writer && CLOUD_OWNER_KEY && writer === CLOUD_OWNER_KEY)
|
|
768
|
+
return CLOUD_OWNER_KEY;
|
|
769
|
+
return writer && !cloudOwnerPerson(writer) ? writer : CLOUD_NOBODY_KEY;
|
|
770
|
+
}
|
|
771
|
+
/** On a Cloud home: a conversation someone other than the owner opened (a
|
|
772
|
+
* guest, a guest's routine, or its room; on a personal Cloud, one a revoked
|
|
773
|
+
* session opened: nobody's). Its turns run in Ask, confined, in a private
|
|
774
|
+
* folder of their own (pinned at its first turn, or after it was unpinned at
|
|
775
|
+
* boot: see pinTaskCwd), and nothing in it reaches the owner's turns. */
|
|
776
|
+
function cloudGuestOpened(threadId) {
|
|
777
|
+
return Boolean(CLOUD_HOME) && cloudThreadStarters(threadId).some((person) => person !== undefined && !cloudOwnerPerson(person));
|
|
778
|
+
}
|
|
779
|
+
/** On a Cloud home: turns running now that a guest drives through another
|
|
780
|
+
* conversation (a teammate's handoff started from a guest's turn), by
|
|
781
|
+
* thread, with their generation. They are guest-driven like the guest's
|
|
782
|
+
* own conversation while they run (cloudGuestDriven). */
|
|
783
|
+
const guestDrivenTurns = new Map();
|
|
784
|
+
/** On a Cloud home: whether a handoff's chain started in (or passed
|
|
785
|
+
* through) a guest-driven conversation: work a guest's turn hands a
|
|
786
|
+
* teammate is the guest's too, however deep. */
|
|
787
|
+
function cloudGuestDrivenHandoff(nodeId) {
|
|
788
|
+
if (!CLOUD_HOME || !nodeId)
|
|
789
|
+
return false;
|
|
790
|
+
let node = roomHandoffs.nodes.get(nodeId);
|
|
791
|
+
for (let steps = 0; node && steps < 32; steps++) {
|
|
792
|
+
// Every conversation the work came through, up to the one it started in.
|
|
793
|
+
if (node.id !== nodeId && cloudGuestDriven(node.threadId))
|
|
794
|
+
return true;
|
|
795
|
+
if (node.id === node.rootId)
|
|
796
|
+
break;
|
|
797
|
+
node = roomHandoffs.nodes.get(node.parentId ?? node.rootId);
|
|
798
|
+
}
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
/** Who a conversation a bot opens from another is for: on a Cloud home,
|
|
802
|
+
* never the owner when a guest drives the source (it opens as the guest's,
|
|
803
|
+
* or nobody's), so it is confined like the source. */
|
|
804
|
+
function openerFrom(fromThreadId) {
|
|
805
|
+
const person = threadPersonKey(fromThreadId);
|
|
806
|
+
if (!CLOUD_HOME || !cloudGuestDriven(fromThreadId))
|
|
807
|
+
return person;
|
|
808
|
+
return person && !cloudOwnerPerson(person) ? person : CLOUD_NOBODY_KEY;
|
|
809
|
+
}
|
|
810
|
+
/** The model a thread a bot opens from one of its own threads starts on:
|
|
811
|
+
* that thread's, not the bot default the person may have moved away from
|
|
812
|
+
* (#1950). Only the bot's own thread counts — a room or a teammate's thread
|
|
813
|
+
* is not one of its tasks, and another bot's model belongs to another engine.
|
|
814
|
+
* Approval stays the bot's, except that a level the inherited engine would
|
|
815
|
+
* have to confirm on a model switch starts the new thread in Ask. */
|
|
816
|
+
function parentThreadModel(botId, parentThreadId) {
|
|
817
|
+
// The stored profile, never a per-thread projection: the comparison is
|
|
818
|
+
// against the defaults the new thread would otherwise get.
|
|
819
|
+
const bot = store.bot(botId);
|
|
820
|
+
const modelSelection = bot && store.taskByThread(bot.id, parentThreadId)?.modelSelection;
|
|
821
|
+
if (!bot || !modelSelection)
|
|
822
|
+
return {};
|
|
823
|
+
const needsAsk = modelSwitchNeedsAsk(approvalModeFor(bot), registry.cliTarget(bot.modelSelection.instanceId)?.driverKind, registry.cliTarget(modelSelection.instanceId)?.driverKind);
|
|
824
|
+
return { modelSelection, ...(needsAsk ? { approvalMode: "ask" } : {}) };
|
|
825
|
+
}
|
|
826
|
+
/** On a Cloud home: the conversation that work a guest-driven turn hands a
|
|
827
|
+
* teammate (delegate_bot, ask_bot) runs in. It is a fresh one of the guest's
|
|
828
|
+
* own on the teammate, never the owner's conversation with it, so the work,
|
|
829
|
+
* and anything that later continues there, stays confined. Undefined when
|
|
830
|
+
* the source is not guest-driven, null when it could not be made. */
|
|
831
|
+
function guestWorkThread(from, target, fromThreadId, brief) {
|
|
832
|
+
if (!CLOUD_HOME || !cloudGuestDriven(fromThreadId))
|
|
833
|
+
return undefined;
|
|
834
|
+
const title = brief.replace(/\s+/g, " ").trim().slice(0, 60) || `From ${from.name}`;
|
|
835
|
+
const task = store.createTask(target.id, title, false, undefined, { botId: from.id, name: from.name, at: Date.now() });
|
|
836
|
+
if (!task)
|
|
837
|
+
return null;
|
|
838
|
+
threadStarters.set(task.threadId, openerFrom(fromThreadId));
|
|
839
|
+
return task.threadId;
|
|
840
|
+
}
|
|
841
|
+
/** On a personal Cloud home the only turns confined like a guest's
|
|
842
|
+
* (cloudGuestDriven) are in what came before it: a routine nobody is proven
|
|
843
|
+
* to have written, or a conversation or room a revoked session opened. What
|
|
844
|
+
* the owner is told about one, and when its engine cannot be confined. */
|
|
845
|
+
const ROUTINE_FROM_BEFORE = "This routine was made before this update. Open it and save it once to run it with full access.";
|
|
846
|
+
const CONVERSATION_FROM_BEFORE = "This conversation is from before your Cloud was only yours, so this bot works in it without a shell. Start a new conversation to use it fully.";
|
|
847
|
+
function confinedWhy(threadId) {
|
|
848
|
+
const run = activeRoutineRunForThread(threadId);
|
|
849
|
+
const writer = run ? cloudRoutineAuthors?.writer(run.routineId) : undefined;
|
|
850
|
+
return run && writer && !cloudOwnerPerson(writer) ? ROUTINE_FROM_BEFORE : CONVERSATION_FROM_BEFORE;
|
|
851
|
+
}
|
|
852
|
+
function guestEngineRefusal(threadId) {
|
|
853
|
+
const why = confinedWhy(threadId);
|
|
854
|
+
return why === ROUTINE_FROM_BEFORE ? `This bot's engine can't run it that way. ${why}`
|
|
855
|
+
: "This conversation is from before your Cloud was only yours, and this bot's engine can't work in it. Start a new conversation.";
|
|
856
|
+
}
|
|
857
|
+
/** On a Cloud home: a turn a guest drives, which runs in Ask whatever the
|
|
858
|
+
* bot's own level (no Full access, no Auto reviewer, no saved commands), with
|
|
859
|
+
* no shell (guestEngineRefusal): a conversation a guest opened, a run of a
|
|
860
|
+
* routine a guest wrote, work a guest's turn handed on (guestDrivenTurns), or
|
|
861
|
+
* a room whose latest line from a person is a guest's. */
|
|
862
|
+
function cloudGuestDriven(threadId) {
|
|
863
|
+
if (!CLOUD_HOME)
|
|
864
|
+
return false;
|
|
865
|
+
if (guestDrivenTurns.has(threadId) || cloudGuestOpened(threadId))
|
|
866
|
+
return true;
|
|
867
|
+
// A run of a routine a guest wrote (or last edited) is the guest's.
|
|
868
|
+
const run = activeRoutineRunForThread(threadId);
|
|
869
|
+
const writer = run ? cloudRoutineAuthors?.writer(run.routineId) : undefined;
|
|
870
|
+
if (writer && !cloudOwnerPerson(writer))
|
|
871
|
+
return true;
|
|
872
|
+
if (!store.groupByThread(threadId))
|
|
873
|
+
return false;
|
|
874
|
+
const person = store.messagesFor(threadId).findLast((line) => line.role === "user" && !line.peerAsk && line.sender?.id)?.sender?.id;
|
|
875
|
+
return person !== undefined && !cloudOwnerPerson(person);
|
|
876
|
+
}
|
|
877
|
+
/** On a Cloud home: a request from one of the owner's own devices. The
|
|
878
|
+
* Admin's signed pairing gives the desktop an admin session; a guest the
|
|
879
|
+
* owner pairs with chat-only access has client scope. Loopback is not the
|
|
880
|
+
* owner here: nothing the person does reaches a Cloud home that way, only
|
|
881
|
+
* processes on the machine (a bot's shell) do. */
|
|
882
|
+
function cloudOwnerSession(auth) {
|
|
883
|
+
return auth.kind === "session" && auth.scopes.includes("admin");
|
|
884
|
+
}
|
|
885
|
+
/** On a Cloud home: whether a person key is the owner's, for who opened a
|
|
886
|
+
* conversation, who drives a turn, its approval level and its folder: the
|
|
887
|
+
* owner's one key, a device of theirs still paired, or a key from before
|
|
888
|
+
* they shared one (cloud-owner.ts, adopted). */
|
|
889
|
+
function cloudOwnerPerson(person) {
|
|
890
|
+
if (!person)
|
|
891
|
+
return false;
|
|
892
|
+
if (person === CLOUD_OWNER_KEY || cloudEarlierOwners.adopted.has(person))
|
|
893
|
+
return true;
|
|
894
|
+
return cloudOwnerDevice(person);
|
|
895
|
+
}
|
|
896
|
+
/** …and whether it is proven the owner's, the only kind whose words may
|
|
897
|
+
* reach the lent Mac, memory, recall and the recent-work brief: a key from
|
|
898
|
+
* before counts only with proof it was the owner's (cloud-owner.ts, proven),
|
|
899
|
+
* never just because nobody revoked it. */
|
|
900
|
+
function cloudProvenOwnerPerson(person) {
|
|
901
|
+
if (!person)
|
|
902
|
+
return false;
|
|
903
|
+
if (person === CLOUD_OWNER_KEY || cloudEarlierOwners.proven.has(person))
|
|
904
|
+
return true;
|
|
905
|
+
return cloudOwnerDevice(person);
|
|
906
|
+
}
|
|
907
|
+
/** Written before the owner had one key: a device of theirs still paired. */
|
|
908
|
+
function cloudOwnerDevice(person) {
|
|
909
|
+
return sessions.list().some((session) => session.scopes.includes("admin") && personKey(session) === person);
|
|
910
|
+
}
|
|
911
|
+
/** On a Cloud home, a session that is not one of the owner's own devices may
|
|
912
|
+
* not answer a card of any kind. */
|
|
913
|
+
function cloudCardRefusal(auth) {
|
|
914
|
+
return auth.scopes.includes("admin") ? null : "Only the owner of this Cloud can answer this card.";
|
|
915
|
+
}
|
|
441
916
|
/** Whose session may answer a card. The provider CLI's own approval modes and
|
|
442
917
|
* the harness's proposals stay exactly as they are; this adds no card, gate
|
|
443
918
|
* or prompt, it only decides whose answer to an existing card counts.
|
|
@@ -454,6 +929,11 @@ function cardAnswerRefusal(auth, threadId, requestId, behavior) {
|
|
|
454
929
|
? "A local service can only decline this request. Approve or answer it in OpenMausBot while signed in."
|
|
455
930
|
: null;
|
|
456
931
|
}
|
|
932
|
+
// A Cloud home is one person's: a device paired with chat-only access (a
|
|
933
|
+
// guest) may read along but never answer, or its words would reach the
|
|
934
|
+
// owner's turn, and through it a lent Mac (docs/cloud-pro.md).
|
|
935
|
+
if (CLOUD_HOME)
|
|
936
|
+
return cloudCardRefusal(auth);
|
|
457
937
|
if (auth.scopes.includes("admin") || !sharedMembership())
|
|
458
938
|
return null;
|
|
459
939
|
const known = [threadStarters.get(threadId), cardRequesterKey(threadId, requestId)].filter((person) => Boolean(person));
|
|
@@ -497,7 +977,19 @@ function decisionActorFor(auth) {
|
|
|
497
977
|
function cardAnswererFor(auth) {
|
|
498
978
|
if (auth.kind === "loopback")
|
|
499
979
|
return auth.trust === "service" ? { kind: "worker" } : { kind: "loopback" };
|
|
500
|
-
return { kind: "session", name: (auth.session.email ?? auth.session.label ?? "").trim() || "Signed-in user"
|
|
980
|
+
return { kind: "session", name: (auth.session.email ?? auth.session.label ?? "").trim() || "Signed-in user",
|
|
981
|
+
...(CLOUD_HOME ? { person: actorKey(auth) } : {}) };
|
|
982
|
+
}
|
|
983
|
+
/** On a Cloud home, who is answering a card right now, by request id: the
|
|
984
|
+
* answer reaches the turn before `answeredBy` is written, and a lending turn
|
|
985
|
+
* must know whose words it just received (server/cloud-lending.ts). */
|
|
986
|
+
const cloudCardAnswersInFlight = new Map();
|
|
987
|
+
/** The person who answered a card, as far as lending on a Cloud home is
|
|
988
|
+
* concerned: the recorded answerer, else whoever is answering it now. */
|
|
989
|
+
function cloudCardAnswerer(card) {
|
|
990
|
+
if (card.answeredBy)
|
|
991
|
+
return card.answeredBy.kind === "session" ? card.answeredBy.person : undefined;
|
|
992
|
+
return card.requestId ? cloudCardAnswersInFlight.get(card.requestId) : undefined;
|
|
501
993
|
}
|
|
502
994
|
/** Answer a card as `auth`: the decision rows written meanwhile name the
|
|
503
995
|
* answerer, and a card this answer settled records who settled it. A card
|
|
@@ -507,6 +999,8 @@ async function answeringCardAs(auth, threadId, requestId, work) {
|
|
|
507
999
|
const card = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId)?.card;
|
|
508
1000
|
return Boolean(card && !card.answered && !card.dismissed && !card.expired);
|
|
509
1001
|
})();
|
|
1002
|
+
if (CLOUD_HOME && open)
|
|
1003
|
+
cloudCardAnswersInFlight.set(requestId, auth.kind === "session" ? actorKey(auth) : undefined);
|
|
510
1004
|
try {
|
|
511
1005
|
await withDecisionActor(decisionActorFor(auth), work);
|
|
512
1006
|
}
|
|
@@ -516,6 +1010,8 @@ async function answeringCardAs(auth, threadId, requestId, work) {
|
|
|
516
1010
|
if (message && card && !card.answeredBy && card.answered !== "unavailable" && (card.answered || card.dismissed)) {
|
|
517
1011
|
store.patchMessage(threadId, message.id, { card: { ...card, answeredBy: cardAnswererFor(auth) } });
|
|
518
1012
|
}
|
|
1013
|
+
if (CLOUD_HOME && open)
|
|
1014
|
+
cloudCardAnswersInFlight.delete(requestId);
|
|
519
1015
|
}
|
|
520
1016
|
}
|
|
521
1017
|
// ── bot visibility (server/bot-visibility.ts) ──────────────────────────
|
|
@@ -631,6 +1127,11 @@ function settleRoomFloor(group) {
|
|
|
631
1127
|
return;
|
|
632
1128
|
store.patchGroup(group.id, { audienceFloor: next === "everyone" ? undefined : next });
|
|
633
1129
|
}
|
|
1130
|
+
/** On a Cloud home the recent-work brief draws only on conversations the
|
|
1131
|
+
* owner alone wrote in (see cloudOwnerOnlyThread). */
|
|
1132
|
+
function recentWorkFilter() {
|
|
1133
|
+
return CLOUD_HOME ? { include: cloudOwnerOnlyThread } : {};
|
|
1134
|
+
}
|
|
634
1135
|
/** The conversations a bot's recent-work brief may draw on: its own, and the
|
|
635
1136
|
* rooms roomFeedsBot allows. */
|
|
636
1137
|
function recentWorkSources(bot) {
|
|
@@ -645,7 +1146,7 @@ function recentWorkSources(bot) {
|
|
|
645
1146
|
* or a room must not be able to pull a private chat into its reply. The
|
|
646
1147
|
* conversations are the ones session_search would search, minus this one. */
|
|
647
1148
|
function autoRecallPrompt(bot, threadId, message, opts) {
|
|
648
|
-
if (!autoRecallEnabled(cfg))
|
|
1149
|
+
if (bot.memoryEnabled === false || !autoRecallEnabled(cfg))
|
|
649
1150
|
return "";
|
|
650
1151
|
const roomByThread = new Map();
|
|
651
1152
|
if (opts.conversations) {
|
|
@@ -658,7 +1159,10 @@ function autoRecallPrompt(bot, threadId, message, opts) {
|
|
|
658
1159
|
}
|
|
659
1160
|
}
|
|
660
1161
|
const threadIds = opts.conversations
|
|
661
|
-
? [...new Set([bot.threadId, ...(bot.tasks ?? []).map((task) => task.threadId), ...roomByThread.keys()])]
|
|
1162
|
+
? [...new Set([bot.threadId, ...(bot.tasks ?? []).map((task) => task.threadId), ...roomByThread.keys()])]
|
|
1163
|
+
// On a Cloud home a conversation anyone else wrote in never feeds
|
|
1164
|
+
// another: its words would reach the owner's turns (and a lent Mac).
|
|
1165
|
+
.filter((id) => id !== threadId && (!CLOUD_HOME || cloudOwnerOnlyThread(id)))
|
|
662
1166
|
: [];
|
|
663
1167
|
try {
|
|
664
1168
|
const recalled = buildRecall({
|
|
@@ -771,7 +1275,7 @@ function adminAuditPlan(method, path) {
|
|
|
771
1275
|
if (path.startsWith("/api/internal/") || path.startsWith("/api/testing/"))
|
|
772
1276
|
return null;
|
|
773
1277
|
const plan = {
|
|
774
|
-
config: requiredScope(method, path, { sharedComputers:
|
|
1278
|
+
config: requiredScope(method, path, { sharedComputers: lendingEnabled() }) === "admin",
|
|
775
1279
|
createsBots: method === "POST" && (path === "/api/bots" || path === "/api/teams/import" || path === "/api/org-library/add"),
|
|
776
1280
|
createsWebhook: method === "POST" && path === "/api/webhooks",
|
|
777
1281
|
pairing: method === "POST" && path === "/api/auth/pairing",
|
|
@@ -790,11 +1294,13 @@ function adminAuditPlan(method, path) {
|
|
|
790
1294
|
m = /^\/api\/groups\/([\w-]+)$/.exec(path);
|
|
791
1295
|
if (m && method === "PATCH")
|
|
792
1296
|
plan.roomId = m[1];
|
|
793
|
-
m = /^\/api\/instances\/([\w.-]+)\/(install|auth\/complete|auth\/sign-out|claude-update)$/.exec(path);
|
|
1297
|
+
m = /^\/api\/instances\/([\w.-]+)\/(install|auth\/complete|auth\/sign-out|claude-update|leftover-files\/remove)$/.exec(path);
|
|
794
1298
|
if (m && method === "POST")
|
|
795
1299
|
plan.engineAction = { id: m[1], action: m[2].replace("/", "-") };
|
|
796
1300
|
if (method === "POST" && path === "/api/instances/claude-accounts")
|
|
797
1301
|
plan.engineAction = { id: "claude", action: "account-add" };
|
|
1302
|
+
if (method === "POST" && path === "/api/instances/chatgpt-accounts")
|
|
1303
|
+
plan.engineAction = { id: "chatgpt", action: "account-add" };
|
|
798
1304
|
return plan.config || plan.botId || plan.createsBots || plan.deletesBot || plan.createsWebhook || plan.webhookId || plan.sessionId || plan.pairing || plan.engineAction || plan.roomId ? plan : null;
|
|
799
1305
|
}
|
|
800
1306
|
const WEBHOOK_AUDIT_FIELDS = ["name", "prompt", "botId", "runOn", "enabled", "eventTypes"];
|
|
@@ -969,6 +1475,46 @@ function queuedTurnTrigger(item) {
|
|
|
969
1475
|
return item.sender ? { kind: "user", label: item.sender.name } : { kind: "owner" };
|
|
970
1476
|
}
|
|
971
1477
|
const providerAuthSessions = new ProviderAuthSessions();
|
|
1478
|
+
/** OAuth sign-in for URL servers; tokens live in their own owner-only file. */
|
|
1479
|
+
const mcpOAuth = new McpOAuthManager({
|
|
1480
|
+
file: join(DATA_DIR, "mcp-oauth.json"),
|
|
1481
|
+
isOwnerLive: (owner) => owner === "loopback" || sessions.isLive(owner),
|
|
1482
|
+
// Read the registered app when needed; never copy its secret into the token store.
|
|
1483
|
+
clients: (name, url) => {
|
|
1484
|
+
const raw = cfg.mcpServers?.[name];
|
|
1485
|
+
const parsed = raw === undefined ? null : parseStoredMcpServer(name, raw);
|
|
1486
|
+
return parsed?.ok && isRemoteMcpServer(parsed.server) && parsed.server.url === url ? parsed.server.oauth : undefined;
|
|
1487
|
+
},
|
|
1488
|
+
});
|
|
1489
|
+
// OpenCode reads provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, …) from
|
|
1490
|
+
// its environment, as it does in a terminal, but only where that environment
|
|
1491
|
+
// is the person's own shell (openCodeProviderKeysAllowed). Wired before the
|
|
1492
|
+
// first catalog probe below. Company enrollment arrives later from the
|
|
1493
|
+
// desktop parent and is wired in once it can (openCodeOrganisationManaged).
|
|
1494
|
+
let openCodeOrganisationManaged = () => false;
|
|
1495
|
+
const openCodeKeysAllowed = () => openCodeProviderKeysAllowed({
|
|
1496
|
+
cloudHome: Boolean(CLOUD_HOME),
|
|
1497
|
+
hostedWorkspace: HOSTED_WORKSPACE,
|
|
1498
|
+
organisationManaged: openCodeOrganisationManaged(),
|
|
1499
|
+
sharedSignIn: sharedSignIn(signInAllowList()),
|
|
1500
|
+
});
|
|
1501
|
+
setOpenCodeProviderKeyPolicy(openCodeKeysAllowed);
|
|
1502
|
+
let openCodeKeysLastAllowed = openCodeKeysAllowed();
|
|
1503
|
+
/** OpenCode lists a provider's models only while it may read that
|
|
1504
|
+
* provider's key. When enrollment or the sign-in list changes the answer,
|
|
1505
|
+
* list them again, so the picker never offers models its turns lack. */
|
|
1506
|
+
function resyncOpenCodeProviderKeys() {
|
|
1507
|
+
const allowed = openCodeKeysAllowed();
|
|
1508
|
+
if (allowed === openCodeKeysLastAllowed)
|
|
1509
|
+
return;
|
|
1510
|
+
openCodeKeysLastAllowed = allowed;
|
|
1511
|
+
for (const instance of registry.instances()) {
|
|
1512
|
+
if (instance.driverKind !== "opencodeGo")
|
|
1513
|
+
continue;
|
|
1514
|
+
void registry.refreshModels(instance.instanceId)
|
|
1515
|
+
.then(() => broadcast({ kind: "config", ...configStatus() }), () => { });
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
972
1518
|
await registry.load(providerConfigs(), decorateHostedProvider);
|
|
973
1519
|
const bundledSkills = loadBundledSkills();
|
|
974
1520
|
const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
|
|
@@ -1067,7 +1613,22 @@ let companyRuntimeReady;
|
|
|
1067
1613
|
const companyRuntimeStarted = new Promise(resolve => { companyRuntimeReady = resolve; });
|
|
1068
1614
|
/** The enrolled organisation's desktop policy: in memory only, read-only, and
|
|
1069
1615
|
* inert (null) unless Electron's enrolled parent sends one. */
|
|
1070
|
-
const managedPolicy = new ManagedDesktopPolicy({ onChange: () =>
|
|
1616
|
+
const managedPolicy = new ManagedDesktopPolicy({ onChange: () => {
|
|
1617
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
1618
|
+
resyncOpenCodeProviderKeys();
|
|
1619
|
+
} });
|
|
1620
|
+
/** A computer kind this server will not use, refused before anything is
|
|
1621
|
+
* prepared: a Cloud home never offers this computer or a Local VM
|
|
1622
|
+
* (cloud-home.ts), and an enrolled organisation may disallow any kind. */
|
|
1623
|
+
function computerPlaceRefusal(kind) {
|
|
1624
|
+
const cloudHome = CLOUD_HOME ? cloudHomePlaceRefusal(kind === "thisComputer" ? "local" : kind === "localVm" ? "vm" : "cloud") : undefined;
|
|
1625
|
+
if (cloudHome)
|
|
1626
|
+
return { message: cloudHome, code: "cloud_home" };
|
|
1627
|
+
const managed = managedPolicy.computerRefusal(kind);
|
|
1628
|
+
return managed ? { message: managed, code: "managed_policy" } : undefined;
|
|
1629
|
+
}
|
|
1630
|
+
/** Cloud chosen with no Boat account: a Cloud home suggests the browser, not a Local VM. */
|
|
1631
|
+
const BOAT_NOT_CONFIGURED = boatNotConfiguredMessage(Boolean(CLOUD_HOME));
|
|
1071
1632
|
/** Why the organisation refuses this instance for bots, or undefined. */
|
|
1072
1633
|
function policyModelRefusal(instance) {
|
|
1073
1634
|
const engine = BUILT_IN_DRIVERS.find(driver => driver.driverKind === instance.driverKind)?.metadata.displayName;
|
|
@@ -1079,7 +1640,10 @@ const managedDesktop = new ManagedDesktopProviders({
|
|
|
1079
1640
|
beforeReplace: stopCompanyInstances,
|
|
1080
1641
|
// Ids became stable across re-enrolment; carry old references forward once.
|
|
1081
1642
|
migrate: aliases => { store.renameInstances(new Map(aliases.map(({ from, to }) => [from, to]))); },
|
|
1082
|
-
onAvailability: () =>
|
|
1643
|
+
onAvailability: () => {
|
|
1644
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
1645
|
+
resyncOpenCodeProviderKeys();
|
|
1646
|
+
},
|
|
1083
1647
|
afterReplace: ids => {
|
|
1084
1648
|
for (const id of providerInstancesChanging)
|
|
1085
1649
|
if (managedDesktop.owns(id))
|
|
@@ -1088,8 +1652,11 @@ const managedDesktop = new ManagedDesktopProviders({
|
|
|
1088
1652
|
// Existing renderer config events refresh /api/instances as well, so
|
|
1089
1653
|
// Company grants and revocations appear without reloading the window.
|
|
1090
1654
|
broadcast({ kind: "config", ...configStatus() });
|
|
1655
|
+
resyncOpenCodeProviderKeys();
|
|
1091
1656
|
},
|
|
1092
1657
|
});
|
|
1658
|
+
// An organisation-managed desktop's environment is not the person's shell.
|
|
1659
|
+
openCodeOrganisationManaged = () => managedPolicy.current() !== null || managedDesktop.enrolled();
|
|
1093
1660
|
utilityParentPort?.on("message", event => {
|
|
1094
1661
|
const message = event.data;
|
|
1095
1662
|
if (message?.type !== "openmausbot:managed-desktop-identity")
|
|
@@ -1212,7 +1779,27 @@ function mintInternalCapability(capability) {
|
|
|
1212
1779
|
});
|
|
1213
1780
|
return token;
|
|
1214
1781
|
}
|
|
1782
|
+
/** A turn that was not provably the owner's has ended: judge its bot's
|
|
1783
|
+
* memory now, so a change it made is caught and an unchanged one is cleared
|
|
1784
|
+
* (server/lending-memory.ts). `generation` undefined: every such turn on the
|
|
1785
|
+
* thread (a teardown that settles threads, not generations). */
|
|
1786
|
+
function endForeignTurns(threadId, generation) {
|
|
1787
|
+
for (const [id, foreign] of foreignTurns) {
|
|
1788
|
+
if (foreign.threadId !== threadId || (generation !== undefined && id !== generation))
|
|
1789
|
+
continue;
|
|
1790
|
+
foreignTurns.delete(id);
|
|
1791
|
+
try {
|
|
1792
|
+
lendingMemory?.reconcile(foreign.botId, foreignTurnRunning(foreign.botId));
|
|
1793
|
+
}
|
|
1794
|
+
catch (error) {
|
|
1795
|
+
console.warn(`lending: could not check a bot's memory: ${error.message}`);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1215
1799
|
function revokeInternalCapabilityGeneration(threadId, generation) {
|
|
1800
|
+
endForeignTurns(threadId, generation);
|
|
1801
|
+
if (guestDrivenTurns.get(threadId) === generation)
|
|
1802
|
+
guestDrivenTurns.delete(threadId);
|
|
1216
1803
|
for (const [token, capability] of internalCapabilities) {
|
|
1217
1804
|
if (capability.threadId === threadId && capability.generation === generation) {
|
|
1218
1805
|
internalCapabilities.delete(token);
|
|
@@ -1239,6 +1826,9 @@ function revokeInternalCapabilitiesForThread(threadId) {
|
|
|
1239
1826
|
function revokeAllInternalCapabilities() {
|
|
1240
1827
|
computerSelectionTurns.clear();
|
|
1241
1828
|
internalCapabilities.clear();
|
|
1829
|
+
// foreignTurns stays: a turn that is not provably the owner's can still
|
|
1830
|
+
// write while its engine is torn down, and its own end (which revokes its
|
|
1831
|
+
// generation by id) is what judges it (server/lending-memory.ts).
|
|
1242
1832
|
activeInternalGenerationByThread.clear();
|
|
1243
1833
|
internalGenerationByProviderTurn.clear();
|
|
1244
1834
|
}
|
|
@@ -1350,9 +1940,13 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation, r
|
|
|
1350
1940
|
OMB_ROOM_TURN: roomCoordination ? "1" : "0",
|
|
1351
1941
|
OMB_OWN_THREAD_CREATION: ownThreadCreation ? "1" : "0",
|
|
1352
1942
|
OMB_SKILL_AUTHORING_ENABLED: skillAuthoring ? "1" : "0",
|
|
1943
|
+
OMB_MEMORY_ENABLED: store.bot(botId)?.memoryEnabled === false ? "0" : "1",
|
|
1353
1944
|
// The shared-computer tools are advertised only while the workspace
|
|
1354
1945
|
// gate is on; the routes behind them refuse regardless.
|
|
1355
|
-
OMB_SHARED_COMPUTERS_ENABLED:
|
|
1946
|
+
OMB_SHARED_COMPUTERS_ENABLED: lendingEnabled() ? "1" : "0",
|
|
1947
|
+
// A Cloud home offers no this computer and no Local VM (cloud-home.ts),
|
|
1948
|
+
// so select_computer lists neither and vm_exec is not shown.
|
|
1949
|
+
OMB_CLOUD_HOME: CLOUD_HOME ? "1" : "0",
|
|
1356
1950
|
// Same capability rule for voice: the tool is offered only when this
|
|
1357
1951
|
// bot can actually speak, and the route re-checks on every call.
|
|
1358
1952
|
OMB_VOICE_NOTES: (() => {
|
|
@@ -1603,11 +2197,11 @@ function releaseTurnResources(owner) {
|
|
|
1603
2197
|
}
|
|
1604
2198
|
}
|
|
1605
2199
|
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 &&
|
|
2200
|
+
// Every computer claim passes here: a kind the organisation disallows, or a
|
|
2201
|
+
// Cloud home never offers, is refused at claim time, whichever path selected it.
|
|
2202
|
+
const kind = computerKindForResource(resource), refusal = kind && computerPlaceRefusal(kind);
|
|
1609
2203
|
if (refusal)
|
|
1610
|
-
throw Object.assign(new Error(refusal), { code:
|
|
2204
|
+
throw Object.assign(new Error(refusal.message), { code: refusal.code });
|
|
1611
2205
|
const active = () => activeInternalGenerationByThread.get(owner.threadId) === owner.generation &&
|
|
1612
2206
|
turnResourceOwners.get(owner.threadId)?.generation === owner.generation;
|
|
1613
2207
|
let waitingMessage;
|
|
@@ -1775,7 +2369,15 @@ function canAdmitDirectTurn(botId, threadId) {
|
|
|
1775
2369
|
/** Routine and webhook dispatch shares startTurn's admission preconditions
|
|
1776
2370
|
* instead of waiting for whole-bot idleness: a free thread slot and no
|
|
1777
2371
|
* active group turn. A group turn blocks scheduled starts the same way it
|
|
1778
|
-
* blocks every other turn kind; it does not consume a capacity slot.
|
|
2372
|
+
* blocks every other turn kind; it does not consume a capacity slot.
|
|
2373
|
+
*
|
|
2374
|
+
* A fresh dispatch also pins to the bot's own project folder, exactly like
|
|
2375
|
+
* startTurn's cwd resolution: a free thread slot alone can miss a second
|
|
2376
|
+
* task of this bot landing in that one folder (#F-collide). Predicting that
|
|
2377
|
+
* collision here lets it defer through the same busy-target machinery as
|
|
2378
|
+
* capacity and group-turn contention, instead of reaching startTurn's own
|
|
2379
|
+
* workspace claim — which throws deep inside a detached dispatch that no
|
|
2380
|
+
* caller here awaits, so the run would otherwise fail permanently. */
|
|
1779
2381
|
function unattendedDispatchState(botId) {
|
|
1780
2382
|
const bot = store.bot(botId);
|
|
1781
2383
|
const decision = admit("unattended", {}, {
|
|
@@ -1783,7 +2385,19 @@ function unattendedDispatchState(botId) {
|
|
|
1783
2385
|
atCapacity: Boolean(bot) && botAtThreadCapacity(botId),
|
|
1784
2386
|
groupTurn: Boolean(bot) && Boolean(activeGroupTurnForBot(botId)),
|
|
1785
2387
|
});
|
|
1786
|
-
|
|
2388
|
+
if (decision.action === "refuse")
|
|
2389
|
+
return decision.code === "missing" ? "missing" : "busy";
|
|
2390
|
+
if (bot?.cwd) {
|
|
2391
|
+
try {
|
|
2392
|
+
if (turnResources.blocker(workspaceResource(bot.cwd), { threadId: "", generation: "" }))
|
|
2393
|
+
return "busy";
|
|
2394
|
+
}
|
|
2395
|
+
catch {
|
|
2396
|
+
// An unresolvable folder is startTurn's own admission check to
|
|
2397
|
+
// report; this prediction only ever adds a defer, never a refusal.
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return "ready";
|
|
1787
2401
|
}
|
|
1788
2402
|
function requestedTaskBot(botId, rawThreadId) {
|
|
1789
2403
|
const profile = store.bot(botId);
|
|
@@ -1949,7 +2563,16 @@ function cancelDirectTurnDispatch(botId, expectedThreadId) {
|
|
|
1949
2563
|
/** The bot's browser for this turn: agent-browser, one isolated session per
|
|
1950
2564
|
* browser profile or per bot (docs/plans/browser-engine.md). Null, with the
|
|
1951
2565
|
* reason logged once, when the engine is not on this machine. */
|
|
1952
|
-
const browserRuntime = new BrowserRuntime(
|
|
2566
|
+
const browserRuntime = new BrowserRuntime({
|
|
2567
|
+
// The bot's restart_browser tool. An open Browser panel watched the old
|
|
2568
|
+
// browser; drop its stream the way the panel's own Restart button does.
|
|
2569
|
+
closeBrowser: async (session, spec) => {
|
|
2570
|
+
const closed = await closeBrowserSession(spec.command, browserRuntimeEnv({ ...spec.env, AGENT_BROWSER_SESSION: session }));
|
|
2571
|
+
if (closed)
|
|
2572
|
+
browserLive.closeForSession(session);
|
|
2573
|
+
return closed;
|
|
2574
|
+
},
|
|
2575
|
+
});
|
|
1953
2576
|
const browserLive = new BrowserLive({ runtime: browserRuntime });
|
|
1954
2577
|
// Temporary profiles last for this server run, but are never saved to disk.
|
|
1955
2578
|
// The viewer and the agent must address the SAME temporary browser.
|
|
@@ -2464,6 +3087,13 @@ function checkedGroupResponder(value, memberIds) {
|
|
|
2464
3087
|
return { kind: "everyone" };
|
|
2465
3088
|
if (responder.kind === "mentions")
|
|
2466
3089
|
return { kind: "mentions" };
|
|
3090
|
+
if (responder.kind === "auto") {
|
|
3091
|
+
if (responder.fallbackBotId === undefined || responder.fallbackBotId === null)
|
|
3092
|
+
return { kind: "auto" };
|
|
3093
|
+
return typeof responder.fallbackBotId === "string" && memberIds.includes(responder.fallbackBotId)
|
|
3094
|
+
? { kind: "auto", fallbackBotId: responder.fallbackBotId }
|
|
3095
|
+
: null;
|
|
3096
|
+
}
|
|
2467
3097
|
if (responder.kind === "member" &&
|
|
2468
3098
|
typeof responder.botId === "string" &&
|
|
2469
3099
|
memberIds.includes(responder.botId)) {
|
|
@@ -2619,7 +3249,8 @@ function previewSystemPrompt(bot) {
|
|
|
2619
3249
|
{ id: "plan", label: "Surface", text: previewPlan.computer === undefined ? "" : surfacePrompt({
|
|
2620
3250
|
computer: previewPlan.computer && previewPlan.computer !== "off" && computerPromptKind ? previewPlan.computer : null,
|
|
2621
3251
|
browser: previewPlan.computer === undefined ? false : previewPlan.browser,
|
|
2622
|
-
}, { note: previewPlan.note }) },
|
|
3252
|
+
}, { note: previewPlan.note, cloudHome: Boolean(CLOUD_HOME) }) },
|
|
3253
|
+
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(agentsMounted && lendingEnabled()) : "" },
|
|
2623
3254
|
{ id: "composio", label: "Connected apps", text: caps?.composioMcp && bot.composio !== false && composio.configured(cfg) ? composioSystemPrompt(bot.connectorTools) : "" },
|
|
2624
3255
|
{ id: "mcp", label: "MCP servers", text: caps?.customMcp ? customMcpPrompt(Object.keys(engineMcpServers(bot))) : "" },
|
|
2625
3256
|
{ id: "browser", label: "Browser", text: previewPlan.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
@@ -2628,7 +3259,7 @@ function previewSystemPrompt(bot) {
|
|
|
2628
3259
|
{ id: "routine", label: "Routines", text: agentsMounted ? ROUTINE_PROMPT : "" },
|
|
2629
3260
|
{ id: "profile", label: "Profile changes", text: agentsMounted ? PROFILE_PROMPT : "" },
|
|
2630
3261
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
2631
|
-
{ id: "memory", label: "Memory", text: memorySystemPrompt(bot.id, { managedWrites: agentsMounted, fileTools: Boolean(privateWorkspace) }) },
|
|
3262
|
+
{ id: "memory", label: "Memory", text: memorySystemPrompt(bot.id, { managedWrites: agentsMounted, fileTools: Boolean(privateWorkspace), enabled: bot.memoryEnabled !== false }) },
|
|
2632
3263
|
{ id: "skills", label: "Skills index", text: privateWorkspace ? skillsSystemPrompt(bot.id) : "" },
|
|
2633
3264
|
]);
|
|
2634
3265
|
const totalBytes = built.sections.reduce((n, s) => n + s.bytes, 0);
|
|
@@ -2701,6 +3332,7 @@ async function botOverview(bot) {
|
|
|
2701
3332
|
})),
|
|
2702
3333
|
engine,
|
|
2703
3334
|
browserEnabled: builtInBrowserEnabled(cfg),
|
|
3335
|
+
cloudHome: Boolean(CLOUD_HOME),
|
|
2704
3336
|
connectedApps,
|
|
2705
3337
|
sectionPeers,
|
|
2706
3338
|
timeZone,
|
|
@@ -2715,7 +3347,12 @@ async function botOverview(bot) {
|
|
|
2715
3347
|
* with one deliberate exception: a Chief of Staff with Full access makes the
|
|
2716
3348
|
* threads it delegates Full too (delegatedFullAccess), so the grant the
|
|
2717
3349
|
* person gave the Chief covers the work the Chief hands out. */
|
|
2718
|
-
const approvalModeForTurn = (bot, peerInitiated = false) => {
|
|
3350
|
+
const approvalModeForTurn = (bot, peerInitiated = false, threadId = bot.threadId) => {
|
|
3351
|
+
// On a Cloud home a turn a guest drives runs in Ask, whatever the bot's
|
|
3352
|
+
// own level. Judged by the conversation the turn runs in (a room's, for a
|
|
3353
|
+
// room turn), never by whichever of the bot's conversations is active.
|
|
3354
|
+
if (cloudGuestDriven(threadId))
|
|
3355
|
+
return "ask";
|
|
2719
3356
|
const mode = approvalModeForOrigin(approvalModeFor(bot), { peerInitiated });
|
|
2720
3357
|
if (!supportsApprovalMode(bot.modelSelection, mode)) {
|
|
2721
3358
|
return "ask";
|
|
@@ -2730,7 +3367,7 @@ function fullAccessForSource(botId, threadId) {
|
|
|
2730
3367
|
return false;
|
|
2731
3368
|
const bot = store.projectBotForTask(botId, threadId) ?? owner.bot;
|
|
2732
3369
|
// Origin changes Custom to Auto, never Full; no live-turn state is needed.
|
|
2733
|
-
return approvalModeForTurn(bot) === "full";
|
|
3370
|
+
return approvalModeForTurn(bot, false, threadId) === "full";
|
|
2734
3371
|
}
|
|
2735
3372
|
function peerReviewRequired(bot, threadId) {
|
|
2736
3373
|
return Boolean(bot.approvePeerComms && !fullAccessForSource(bot.id, threadId));
|
|
@@ -2754,8 +3391,7 @@ function delegatedFullAccess(from, fromThreadId, target) {
|
|
|
2754
3391
|
}
|
|
2755
3392
|
/** Make a delegated thread Full and say so in it once, so the level the
|
|
2756
3393
|
* chip shows and the level the turns run at agree, and the person can see
|
|
2757
|
-
* where the access came from.
|
|
2758
|
-
* across delegations and must not collect a chip per request. */
|
|
3394
|
+
* where the access came from. */
|
|
2759
3395
|
function grantDelegatedFullAccess(from, target, threadId) {
|
|
2760
3396
|
if (store.taskByThread(target.id, threadId)?.approvalMode === "full")
|
|
2761
3397
|
return;
|
|
@@ -2769,13 +3405,16 @@ function grantDelegatedFullAccess(from, target, threadId) {
|
|
|
2769
3405
|
/** A room member's level for one turn. Work a Full-access Chief hands out
|
|
2770
3406
|
* in a room runs Full for that turn: the room thread is shared, so the
|
|
2771
3407
|
* level is not stored on it — it rides the handoff. */
|
|
2772
|
-
function roomTurnApprovalMode(bot, orchestration) {
|
|
3408
|
+
function roomTurnApprovalMode(bot, threadId, orchestration) {
|
|
3409
|
+
// On a Cloud home a room turn a guest drives runs in Ask (cloudGuestDriven).
|
|
3410
|
+
if (cloudGuestDriven(threadId))
|
|
3411
|
+
return "ask";
|
|
2773
3412
|
const handoff = orchestration?.roomHandoffId ? roomHandoffs.nodes.get(orchestration.roomHandoffId) : undefined;
|
|
2774
3413
|
const source = handoff?.parentId ? roomHandoffs.nodes.get(handoff.parentId) : undefined;
|
|
2775
3414
|
const from = source ? store.bot(source.botId) : undefined;
|
|
2776
3415
|
if (from && source && delegatedFullAccess(from, source.threadId, bot))
|
|
2777
3416
|
return "full";
|
|
2778
|
-
return approvalModeForTurn(bot, Boolean(orchestration?.roomHandoffId));
|
|
3417
|
+
return approvalModeForTurn(bot, Boolean(orchestration?.roomHandoffId), threadId);
|
|
2779
3418
|
}
|
|
2780
3419
|
/** Privileged approval-mode transitions are deliberately absent from the
|
|
2781
3420
|
* loopback HTTP authority model: a bot with shell access can curl that
|
|
@@ -3275,7 +3914,9 @@ function createChannel(value) {
|
|
|
3275
3914
|
throw Object.assign(new Error("invalid setup.defaultResponder"), { status: 400 });
|
|
3276
3915
|
setup = { bulletin: requested.bulletin, defaultResponder: responder, completed: true };
|
|
3277
3916
|
}
|
|
3278
|
-
|
|
3917
|
+
// A new room with no routing of its own starts on Auto while the decision
|
|
3918
|
+
// model is on for rooms; otherwise it gets today's default (first member).
|
|
3919
|
+
return store.createGroup(name, memberIds, false, section, setup ?? (deciderReady(cfg, "roomRouting") ? { defaultResponder: { kind: "auto" } } : undefined));
|
|
3279
3920
|
}
|
|
3280
3921
|
function updateChannel(groupId, value) {
|
|
3281
3922
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -3530,6 +4171,10 @@ const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
|
|
|
3530
4171
|
: child.status === "completed" ? `${bot?.name ?? "Teammate"} replied${child.groupId ? ` · ${store.group(child.groupId)?.name ?? "Room"}` : ""}`
|
|
3531
4172
|
: `${bot?.name ?? "Teammate"} — ${child.status}: ${child.result.slice(0, 180)}`,
|
|
3532
4173
|
ok: child.status === "completed" && !problem,
|
|
4174
|
+
// The chip label stays short; the report itself rides along so a
|
|
4175
|
+
// phone can show what the teammate said without opening its thread.
|
|
4176
|
+
...(child.status === "completed" && !problem && child.result.trim()
|
|
4177
|
+
? { output: redactSecretsInText(child.result.trim()).slice(0, 2_000) } : {}),
|
|
3533
4178
|
},
|
|
3534
4179
|
...(child.groupId ? { comm: { groupId: child.groupId, threadId: child.threadId, withBotId: child.botId,
|
|
3535
4180
|
withName: bot?.name ?? "Teammate", withColor: bot?.color ?? "blue" } }
|
|
@@ -3543,14 +4188,9 @@ const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
|
|
|
3543
4188
|
if (source)
|
|
3544
4189
|
markTaskContextExternallyUpdated(source, parent.threadId);
|
|
3545
4190
|
}
|
|
3546
|
-
// A work thread
|
|
3547
|
-
//
|
|
3548
|
-
//
|
|
3549
|
-
// sidebar, never deleted, and open again the moment anyone speaks
|
|
3550
|
-
// there. A finished job tidies up after itself; a failed or withheld
|
|
3551
|
-
// one stays in the sidebar where the person can see it. The pair
|
|
3552
|
-
// conversation is the standing line between two bots and never
|
|
3553
|
-
// auto-closes.
|
|
4191
|
+
// A successful direct work thread closes after its result is reported:
|
|
4192
|
+
// folded out of the sidebar, never deleted, and reopened if addressed.
|
|
4193
|
+
// Failed or withheld work stays visible.
|
|
3554
4194
|
const childTask = store.taskByThread(child.botId, child.threadId);
|
|
3555
4195
|
if (!child.groupId && child.status === "completed" && !problem && !childTask?.closedBy
|
|
3556
4196
|
&& childTask?.openedBy?.kind === "work" && childTask.openedBy.botId === parent.botId) {
|
|
@@ -4199,8 +4839,35 @@ function audienceChanged() {
|
|
|
4199
4839
|
}
|
|
4200
4840
|
}
|
|
4201
4841
|
}
|
|
4842
|
+
// Cloud boot can revoke sessions before routes are registered. Create the
|
|
4843
|
+
// viewer manager before installing any revocation callbacks.
|
|
4844
|
+
const desktopViewer = createDesktopViewer({
|
|
4845
|
+
target: (id) => {
|
|
4846
|
+
if (id.startsWith("vps/")) {
|
|
4847
|
+
const botId = id.slice(4);
|
|
4848
|
+
if (store.bot(botId)?.cloudBackend !== "vps")
|
|
4849
|
+
return;
|
|
4850
|
+
return { key: id, resolve: async () => {
|
|
4851
|
+
const connection = vps.vpsDesktopConnection(botId);
|
|
4852
|
+
if (!connection)
|
|
4853
|
+
throw Object.assign(new Error("VPS viewer is not open"), { status: 409 });
|
|
4854
|
+
return connection;
|
|
4855
|
+
} };
|
|
4856
|
+
}
|
|
4857
|
+
const targets = [SHARED_LOCAL_VM_TARGET, ...store.bots.map(bot => perBotLocalVmTarget(bot.id)),
|
|
4858
|
+
...Array.from({ length: localVmMaxInstances(cfg) }, (_, seat) => poolLocalVmTarget(seat))];
|
|
4859
|
+
const target = targets.find(target => viewerTargetId(target) === id);
|
|
4860
|
+
return target && localDesktopTarget(target, {
|
|
4861
|
+
// A viewer can repair a sick CUA driver; skip desktop health probes.
|
|
4862
|
+
status: target => containerComputerStatus(undefined, undefined, target, { probeDesktop: false }),
|
|
4863
|
+
touch: target => localVmIdleFor(target).touch(),
|
|
4864
|
+
});
|
|
4865
|
+
},
|
|
4866
|
+
live: auth => auth.kind === "loopback" || sessions.isLive(auth.session.id),
|
|
4867
|
+
});
|
|
4202
4868
|
function closeSessionStreams(sessionId) {
|
|
4203
4869
|
browserLive.closeForOwner(sessionId);
|
|
4870
|
+
desktopViewer.closeForOwner(sessionId);
|
|
4204
4871
|
for (const client of sseClients) {
|
|
4205
4872
|
if (client.sessionId !== sessionId)
|
|
4206
4873
|
continue;
|
|
@@ -4215,8 +4882,35 @@ function closeSessionStreams(sessionId) {
|
|
|
4215
4882
|
}
|
|
4216
4883
|
sessions.onSessionRevoked((sessionId) => {
|
|
4217
4884
|
providerAuthSessions.revokeOwner(sessionId);
|
|
4885
|
+
mcpOAuth.revokeOwner(sessionId);
|
|
4218
4886
|
closeSessionStreams(sessionId);
|
|
4887
|
+
// A revoked owner device no longer vouches for its lines (cloudOwnerPerson).
|
|
4888
|
+
ownerOnlyCache.clear();
|
|
4219
4889
|
});
|
|
4890
|
+
/** On a Cloud home: a line someone else adds to a conversation while one of
|
|
4891
|
+
* the owner's turns runs in it (steered into it, or handed in) makes that
|
|
4892
|
+
* turn someone else's from then on. The lent Mac is refused at its next use
|
|
4893
|
+
* anyway (cloudLendingRefusal reads the whole conversation each time); this
|
|
4894
|
+
* makes whatever the bot writes to its memory from now on count as theirs
|
|
4895
|
+
* too (server/lending-memory.ts). */
|
|
4896
|
+
function noteSomeoneElsesLine(threadId) {
|
|
4897
|
+
const generation = activeInternalGenerationByThread.get(threadId);
|
|
4898
|
+
if (!generation || foreignTurns.has(generation))
|
|
4899
|
+
return;
|
|
4900
|
+
const bot = store.botByThread(threadId);
|
|
4901
|
+
if (bot && cloudHomeLendingRefusal(cloudLendingTurn({ botId: bot.id, threadId, generation })) !== null) {
|
|
4902
|
+
noteForeignTurn(bot.id, threadId, generation);
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
if (lendingMemory) {
|
|
4906
|
+
store.onChange((change) => {
|
|
4907
|
+
if (change.type === "message" && change.message.role === "user")
|
|
4908
|
+
noteSomeoneElsesLine(change.threadId);
|
|
4909
|
+
// A deleted bot's record goes with it (and nothing recreates its workspace).
|
|
4910
|
+
else if (change.type === "bot.deleted")
|
|
4911
|
+
lendingMemory.forget(change.botId);
|
|
4912
|
+
});
|
|
4913
|
+
}
|
|
4220
4914
|
/** Every frame is numbered, and the last few hundred are kept, so a client
|
|
4221
4915
|
* whose connection dropped can ask for what it missed instead of
|
|
4222
4916
|
* re-downloading every transcript. The desktop reconnects in milliseconds
|
|
@@ -4465,6 +5159,10 @@ function notify(notification) {
|
|
|
4465
5159
|
// Group threads: the fold needs to know WHO is talking — the turn engine
|
|
4466
5160
|
// records the active member here before dispatching its turn.
|
|
4467
5161
|
const groupSpeakers = new Map();
|
|
5162
|
+
/** Auto-room rounds the decision model routed, by thread: the named
|
|
5163
|
+
* speaker's next reply line carries `routedBy` (stamped in the runtime fold
|
|
5164
|
+
* below), so the room can say "Picked by Jev". Only a routed round sets it. */
|
|
5165
|
+
const routedRoomReplies = new Map();
|
|
4468
5166
|
// The latest running token totals for the turn in flight on each thread.
|
|
4469
5167
|
// Providers report cumulative-within-turn numbers; the final value is folded
|
|
4470
5168
|
// into the task's tally when the turn settles.
|
|
@@ -4685,10 +5383,34 @@ const memoryUpkeep = createMemoryUpkeep({
|
|
|
4685
5383
|
return facts.length;
|
|
4686
5384
|
},
|
|
4687
5385
|
sourceLabel: (botId, threadId) => memorySourceLabel({ task: store.taskByThread(botId, threadId), threadId }),
|
|
5386
|
+
// On a Cloud home upkeep only works on the owner's conversations (see the
|
|
5387
|
+
// capture hook below), so its writes are the owner's (lending-memory.ts).
|
|
5388
|
+
...(lendingMemory ? { writing: (botId, write) => lendingMemory.trustedWrite(botId, write) } : {}),
|
|
4688
5389
|
quietMs: () => captureQuietMs(cfg),
|
|
4689
5390
|
tidyHour: () => tidyHour(cfg),
|
|
4690
5391
|
log: (line) => console.log(line),
|
|
4691
5392
|
});
|
|
5393
|
+
// OMB Cloud home: the first bot turn that finishes here ticks the setup
|
|
5394
|
+
// checklist's "try something" step (firstCloudTurnPatch). The server writes
|
|
5395
|
+
// it, not an admin, so it is kept out of any admin's recorded changes.
|
|
5396
|
+
bus.subscribe((event) => {
|
|
5397
|
+
if (!CLOUD_HOME || event.type !== "turn.completed" || shouldIgnoreProviderEvent(event))
|
|
5398
|
+
return;
|
|
5399
|
+
const patch = firstCloudTurnPatch({
|
|
5400
|
+
cloudHome: true, recorded: cfg.onboarding?.firstTurnAt, ok: event.ok,
|
|
5401
|
+
known: Boolean(store.botByThread(event.threadId) ?? store.groupByThread(event.threadId)),
|
|
5402
|
+
});
|
|
5403
|
+
if (!patch)
|
|
5404
|
+
return;
|
|
5405
|
+
try {
|
|
5406
|
+
adminActorScope.exit(() => saveConfig(patch));
|
|
5407
|
+
Object.assign(cfg, loadConfig());
|
|
5408
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
5409
|
+
}
|
|
5410
|
+
catch (error) {
|
|
5411
|
+
console.warn(`cloud home: could not record the first finished turn (${error instanceof Error ? error.message : "unknown"})`);
|
|
5412
|
+
}
|
|
5413
|
+
});
|
|
4692
5414
|
// A finished 1:1 turn of an upkeep bot waits for capture. Only the person's
|
|
4693
5415
|
// own conversation: not a room, not a turn another bot or the harness
|
|
4694
5416
|
// started (its "user" line is not the person), not a failed turn.
|
|
@@ -4702,6 +5424,10 @@ bus.subscribe((event) => {
|
|
|
4702
5424
|
if (!bot || !upkeepEnabled(bot) || store.groupByThread(event.threadId) || isInternalTurn(event.threadId)
|
|
4703
5425
|
|| isUnattended(bot.id, event.threadId) || routines?.runForThread(event.threadId))
|
|
4704
5426
|
return;
|
|
5427
|
+
// On a Cloud home the bot's memory reaches its owner's turns (and a lent
|
|
5428
|
+
// Mac): nothing is captured from a conversation anyone else wrote in.
|
|
5429
|
+
if (CLOUD_HOME && !cloudOwnerOnlyThread(event.threadId))
|
|
5430
|
+
return;
|
|
4705
5431
|
const path = store.activePath(event.threadId);
|
|
4706
5432
|
const replyAt = path.findLastIndex((message) => message.role === "bot" && message.kind === "text" && message.turnId === event.turnId);
|
|
4707
5433
|
if (replyAt < 0)
|
|
@@ -4734,7 +5460,7 @@ bus.subscribe((event) => {
|
|
|
4734
5460
|
const messages = store.messagesFor(event.threadId);
|
|
4735
5461
|
const reply = messages.findLast((message) => message.role === "bot" && message.kind === "text" && message.turnId === event.turnId);
|
|
4736
5462
|
const bot = store.botByThread(event.threadId) ?? (reply?.from ? store.bot(reply.from.botId) : undefined);
|
|
4737
|
-
if (!bot)
|
|
5463
|
+
if (!bot || bot.memoryEnabled === false)
|
|
4738
5464
|
return;
|
|
4739
5465
|
const tools = messages
|
|
4740
5466
|
.filter((message) => message.kind === "activity" && message.turnId === event.turnId && message.tool?.name)
|
|
@@ -4747,13 +5473,21 @@ bus.subscribe((event) => {
|
|
|
4747
5473
|
const room = store.groupByThread(event.threadId);
|
|
4748
5474
|
if (room && !roomFeedsBot(room, bot))
|
|
4749
5475
|
return;
|
|
4750
|
-
|
|
5476
|
+
// On a Cloud home the log feeds recall in the owner's turns: only the
|
|
5477
|
+
// owner's own conversations leave a line.
|
|
5478
|
+
if (CLOUD_HOME && (room || !cloudOwnerOnlyThread(event.threadId)))
|
|
5479
|
+
return;
|
|
5480
|
+
const writeLine = () => appendMemoryLog(bot.id, line, {
|
|
4751
5481
|
source: memorySourceLabel({
|
|
4752
5482
|
room,
|
|
4753
5483
|
task: store.taskByThread(bot.id, event.threadId),
|
|
4754
5484
|
threadId: event.threadId,
|
|
4755
5485
|
}),
|
|
4756
5486
|
});
|
|
5487
|
+
if (lendingMemory)
|
|
5488
|
+
lendingMemory.trustedWrite(bot.id, writeLine);
|
|
5489
|
+
else
|
|
5490
|
+
writeLine();
|
|
4757
5491
|
}
|
|
4758
5492
|
catch {
|
|
4759
5493
|
// a missing note never fails a turn
|
|
@@ -5038,13 +5772,18 @@ const boatLifecycleBusyBots = new Set();
|
|
|
5038
5772
|
// A refresh is a reader, not a lifecycle change. Keep its reservation until
|
|
5039
5773
|
// the provider settles even if the HTTP client leaves, and share it on retry.
|
|
5040
5774
|
const vpsPreviewRequests = new Map();
|
|
5775
|
+
// Joins in flight per bot. A second tab or a quick reconnect waits for the
|
|
5776
|
+
// pending join, then reuses its tunnel, instead of being refused by its lane.
|
|
5777
|
+
const vpsJoinRequests = new Map();
|
|
5041
5778
|
const orphanBoatLifecycleBusyIds = new Set();
|
|
5042
5779
|
const boatInventoryRequestsBusyIds = new Set();
|
|
5043
5780
|
const computerProviderConfigTransitions = new Set();
|
|
5044
5781
|
// A restore mutates and cleans a project work tree. Claim the bot across the
|
|
5045
5782
|
// entire async Git operation so a turn cannot start in that folder midway.
|
|
5046
5783
|
const checkpointRestoreLeases = new Set();
|
|
5047
|
-
|
|
5784
|
+
/** The effective idle window, read from config so a Settings change applies
|
|
5785
|
+
* to both new and already-armed timers (see the config route). */
|
|
5786
|
+
const localVmIdleMs = () => localVmIdleTimeoutMinutes(cfg) * 60_000;
|
|
5048
5787
|
/** How long a turn waits for Cua Driver after starting the container itself.
|
|
5049
5788
|
* A cold XFCE desktop needs some seconds; past this the turn reports the
|
|
5050
5789
|
* status it has rather than hanging on a container that will not come up. */
|
|
@@ -5175,7 +5914,7 @@ async function teamComputersPayload() {
|
|
|
5175
5914
|
* Assignment authorizes waking, never replacing a missing paid machine. */
|
|
5176
5915
|
async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
5177
5916
|
if (!canMount)
|
|
5178
|
-
throw new Error("This model
|
|
5917
|
+
throw new Error("This model cannot use the team's Boat computer; choose a model provider with computer tools or an explicit bot destination");
|
|
5179
5918
|
if (!boat.boatConfigured(cfg))
|
|
5180
5919
|
throw new Error("The team's Boat account is not configured; reconnect it in Settings");
|
|
5181
5920
|
const ownerId = teamComputerOwner(computer.id);
|
|
@@ -5199,7 +5938,7 @@ async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
|
5199
5938
|
!turnResources.owns(`computer:box:${machine.id}`, owner))
|
|
5200
5939
|
throw new Error("This computer turn ended while its machine was starting");
|
|
5201
5940
|
return {
|
|
5202
|
-
integration: { kind: "box", boxId: machine.id, token: cfg
|
|
5941
|
+
integration: { kind: "box", boxId: machine.id, token: boat.boatAccount(cfg)?.token ?? "", control: controlIntegration(botId, owner.threadId, owner.generation) },
|
|
5203
5942
|
capture: () => boat.screenshotBoat(cfg, ownerId, machine.id),
|
|
5204
5943
|
};
|
|
5205
5944
|
}
|
|
@@ -5211,7 +5950,7 @@ async function mountHostComputer(owner, botId, providerSupportsLocal) {
|
|
|
5211
5950
|
// ACP engine" while already on one has nowhere to go.
|
|
5212
5951
|
throw new Error(providerSupportsLocal
|
|
5213
5952
|
? `local computer control is not available on ${process.platform} — select another destination`
|
|
5214
|
-
: "this model
|
|
5953
|
+
: "this model cannot control this computer — choose Claude or an ACP model provider, or select another destination");
|
|
5215
5954
|
}
|
|
5216
5955
|
const cua = readCuaConnection();
|
|
5217
5956
|
if (!cua) {
|
|
@@ -5305,7 +6044,7 @@ async function attachBotBoat(bot, owner, opts) {
|
|
|
5305
6044
|
return {
|
|
5306
6045
|
capture: () => boat.screenshotBoat(cfg, bot.id, machine.id),
|
|
5307
6046
|
integration: opts.canMount
|
|
5308
|
-
? { kind: "box", boxId: machine.id, token: cfg
|
|
6047
|
+
? { kind: "box", boxId: machine.id, token: boat.boatAccount(cfg)?.token ?? "", control: controlIntegration(bot.id, owner.threadId, owner.generation) }
|
|
5309
6048
|
: null,
|
|
5310
6049
|
};
|
|
5311
6050
|
}
|
|
@@ -5418,12 +6157,13 @@ async function computerPreviewSurface(bot, threadId) {
|
|
|
5418
6157
|
return "cloud";
|
|
5419
6158
|
}
|
|
5420
6159
|
const target = localVmTargetForStatus(bot.id, threadId);
|
|
5421
|
-
|
|
6160
|
+
// A Cloud home has neither place (cloud-home.ts), so Auto never shows one.
|
|
6161
|
+
if (!CLOUD_HOME && instance?.adapter.capabilities.computerMcp && localVmSeen.has(target.key)) {
|
|
5422
6162
|
const vm = await containerComputerStatus(undefined, undefined, target).catch(() => null);
|
|
5423
6163
|
if (vm && autoLocalVmAttachable(vm))
|
|
5424
6164
|
return "vm";
|
|
5425
6165
|
}
|
|
5426
|
-
if (shouldMountLocalComputer({ requested: undefined, hostPlatform: process.platform,
|
|
6166
|
+
if (!CLOUD_HOME && shouldMountLocalComputer({ requested: undefined, hostPlatform: process.platform,
|
|
5427
6167
|
providerSupportsLocal: instance?.adapter.capabilities.localComputerMcp === true }) && readCuaConnection())
|
|
5428
6168
|
return "local";
|
|
5429
6169
|
if (bot.cloudBackend === "vps")
|
|
@@ -5431,16 +6171,23 @@ async function computerPreviewSurface(bot, threadId) {
|
|
|
5431
6171
|
return plan.browser ? "browser" : "off";
|
|
5432
6172
|
}
|
|
5433
6173
|
/** 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.
|
|
6174
|
+
* deferred until a chat tool selects it and the old turn releases its tools.
|
|
6175
|
+
* A Cloud home lists only the places it has (cloud-home.ts), so a bot never
|
|
6176
|
+
* tells the person to set up this computer or a Local VM there. */
|
|
5435
6177
|
async function selectableComputers(bot) {
|
|
5436
6178
|
const caps = registry.get(bot.modelSelection.instanceId)?.adapter.capabilities;
|
|
5437
6179
|
const off = bot.computer === "off";
|
|
5438
6180
|
const localEngine = caps?.remoteAgent !== true;
|
|
5439
|
-
|
|
6181
|
+
const surfaces = ["cloud", "vm", "local", "browser"].filter(surface => !CLOUD_HOME || cloudHomeOffersPlace(surface));
|
|
6182
|
+
return Promise.all(surfaces.map(async (surface) => {
|
|
5440
6183
|
let ready = false;
|
|
5441
6184
|
let canStart = false;
|
|
5442
6185
|
let canCreate = false;
|
|
5443
|
-
|
|
6186
|
+
// On a Cloud home "this computer" reads as the person's own; name the
|
|
6187
|
+
// place the reason is about.
|
|
6188
|
+
let reason = CLOUD_HOME && surface === "cloud"
|
|
6189
|
+
? "The cloud computer is not set up or running. Open the Computer panel to set it up."
|
|
6190
|
+
: "This computer is not configured or running. Open the Computer panel to set it up.";
|
|
5444
6191
|
try {
|
|
5445
6192
|
if (off)
|
|
5446
6193
|
reason = "Computer access is Off in this bot's settings.";
|
|
@@ -5476,7 +6223,7 @@ async function selectableComputers(bot) {
|
|
|
5476
6223
|
}
|
|
5477
6224
|
else if (surface === "browser") {
|
|
5478
6225
|
ready = caps?.browserMcp === true && builtInBrowserEnabled(cfg) && bot.browser !== false && browserEngineStatus().kind === "ready";
|
|
5479
|
-
reason = "The built-in browser is disabled, not installed, or unsupported by this model
|
|
6226
|
+
reason = "The built-in browser is disabled, not installed, or unsupported by this model.";
|
|
5480
6227
|
}
|
|
5481
6228
|
}
|
|
5482
6229
|
catch (error) {
|
|
@@ -5627,7 +6374,7 @@ function localVmIdleFor(target) {
|
|
|
5627
6374
|
let idle = localVmIdles.get(target.key);
|
|
5628
6375
|
if (idle)
|
|
5629
6376
|
return idle;
|
|
5630
|
-
idle = new LocalVmIdleTimer(
|
|
6377
|
+
idle = new LocalVmIdleTimer(localVmIdleMs(), () => localVmImageBusy || localVmLifecycleBusy.has(target.key) || localVmActiveThreads.has(target.key), async () => {
|
|
5631
6378
|
localVmLifecycleBusy.add(target.key);
|
|
5632
6379
|
try {
|
|
5633
6380
|
const status = await containerComputerStatus(undefined, undefined, target);
|
|
@@ -5826,7 +6573,12 @@ bus.subscribe((event) => {
|
|
|
5826
6573
|
const owner = bot ? directRequestOwners.get(event.threadId) : undefined;
|
|
5827
6574
|
const proven = owner?.messageId && !owner.stopped && owner.generation === directTurnGenerationByThread.get(event.threadId) &&
|
|
5828
6575
|
owner.turnId && (!event.turnId || event.turnId === owner.turnId);
|
|
5829
|
-
|
|
6576
|
+
// One line per routed reply: the routed speaker's first text of its turn.
|
|
6577
|
+
const routed = group && m.role === "bot" && m.kind === "text" ? routedRoomReplies.get(event.threadId) : undefined;
|
|
6578
|
+
const routedBy = routed && speaker && routed.botId === speaker.botId ? routed.routedBy : undefined;
|
|
6579
|
+
if (routedBy)
|
|
6580
|
+
routedRoomReplies.delete(event.threadId);
|
|
6581
|
+
const message = store.appendMessage(event.threadId, group && m.role === "bot" ? { ...m, from: speaker, ...(routedBy ? { routedBy } : {}) }
|
|
5830
6582
|
: proven ? { ...m, requestMessageId: owner.messageId } : m);
|
|
5831
6583
|
return message;
|
|
5832
6584
|
};
|
|
@@ -5963,10 +6715,13 @@ bus.subscribe((event) => {
|
|
|
5963
6715
|
// always reaches the human — even Full access never invents an answer.
|
|
5964
6716
|
const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
5965
6717
|
const unattended = permission && asker && event.requestId ? isUnattended(asker.id, event.threadId) : false;
|
|
5966
|
-
|
|
6718
|
+
// A turn a guest drives on a Cloud home is Ask, room turns included,
|
|
6719
|
+
// and no command the owner saved answers for it.
|
|
6720
|
+
const guestDriven = cloudGuestDriven(event.threadId);
|
|
6721
|
+
const effectiveApprovalMode = asker && !guestDriven ? approvalModeForTurn(asker, isInternalTurn(event.threadId), event.threadId) : "ask";
|
|
5967
6722
|
// Native shell descriptors are distinct from computer/MCP permissions;
|
|
5968
6723
|
// choosing a local desktop must not disable an exact shell grant.
|
|
5969
|
-
const command = permission && asker && event.requestId && !event.requiresExplicitApproval && event.command
|
|
6724
|
+
const command = permission && asker && event.requestId && !event.requiresExplicitApproval && event.command && !guestDriven
|
|
5970
6725
|
? commandAllowlistCandidate({ ...event.command, providerInstanceId: event.providerInstanceId ?? asker.modelSelection.instanceId })
|
|
5971
6726
|
: null;
|
|
5972
6727
|
const verdict = permission && asker && event.requestId
|
|
@@ -6071,7 +6826,8 @@ bus.subscribe((event) => {
|
|
|
6071
6826
|
: undefined,
|
|
6072
6827
|
commandAllowlist: command ?? undefined,
|
|
6073
6828
|
// Provider-owned session grants remain separate from exact commands.
|
|
6074
|
-
|
|
6829
|
+
// Never on a guest's turn: an "always" for it would outlive it.
|
|
6830
|
+
allowSession: permission && event.allowSession && !event.requiresExplicitApproval && !guestDriven ? true : undefined,
|
|
6075
6831
|
// The text stays for cards saved before heldCode existed, and for
|
|
6076
6832
|
// clients that do not know the key yet.
|
|
6077
6833
|
held: approvalHeldReason(heldContext),
|
|
@@ -6149,12 +6905,19 @@ bus.subscribe((event) => {
|
|
|
6149
6905
|
tool: { name: `retrying — attempt ${event.attempt + 1}/${RETRY_MAX_ATTEMPTS} in ${Math.round(event.delayMs / 1000)}s — ${event.reason}`, ok: true },
|
|
6150
6906
|
});
|
|
6151
6907
|
break;
|
|
6908
|
+
case "runtime.notice":
|
|
6909
|
+
// informational: the turn carries on. The prefix keeps it a status row
|
|
6910
|
+
// the person always sees (src/lib/activity-runs.ts), never a tool step
|
|
6911
|
+
// hidden with Tool calls off.
|
|
6912
|
+
pushMessage({ role: "bot", kind: "activity", tool: { name: `notice: ${event.message.slice(0, 240)}`, ok: true } });
|
|
6913
|
+
break;
|
|
6152
6914
|
case "runtime.error":
|
|
6153
6915
|
pushMessage({
|
|
6154
6916
|
role: "bot",
|
|
6155
6917
|
kind: "activity",
|
|
6156
6918
|
tool: {
|
|
6157
|
-
|
|
6919
|
+
// a setup error carries its fix, which must not be cut mid-command
|
|
6920
|
+
name: `error: ${event.message.slice(0, event.setup ? 320 : 160)}`,
|
|
6158
6921
|
ok: false,
|
|
6159
6922
|
setup: event.setup,
|
|
6160
6923
|
...(event.terminal ? { terminal: true } : {}),
|
|
@@ -7399,14 +8162,10 @@ async function finalScreenFrame(_botId, threadId) {
|
|
|
7399
8162
|
* (generateText — Haiku on Claude, the chat completion endpoint's text
|
|
7400
8163
|
* path on OpenAI-compatible engines). Null whenever that call cannot run,
|
|
7401
8164
|
* runs long, or answers with something that is not a plain short title;
|
|
7402
|
-
* the caller keeps the snippet it already applied.
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
"Reply with only a short title: 3 to 6 words, plain text, no quotes, no trailing period.",
|
|
7407
|
-
"Message:",
|
|
7408
|
-
text.trim().slice(0, 1_500),
|
|
7409
|
-
].join("\n");
|
|
8165
|
+
* the caller keeps the snippet it already applied. "conversation" names an
|
|
8166
|
+
* existing thread from titleConversationExcerpt (Regenerate title). */
|
|
8167
|
+
async function generateThreadTitle(provider, text, source = "first-message") {
|
|
8168
|
+
const prompt = threadTitlePrompt(text, source);
|
|
7410
8169
|
const expiry = new AbortController();
|
|
7411
8170
|
let timer;
|
|
7412
8171
|
try {
|
|
@@ -7430,6 +8189,8 @@ async function generateThreadTitle(provider, text) {
|
|
|
7430
8189
|
clearTimeout(timer);
|
|
7431
8190
|
}
|
|
7432
8191
|
}
|
|
8192
|
+
/** Threads whose Regenerate title one-shot is still running: one at a time. */
|
|
8193
|
+
const titleRegenerations = new Set();
|
|
7433
8194
|
// ── turn dispatch (upstream ProviderCommandReactor, miniature) ──────────
|
|
7434
8195
|
async function startTurn(botId, text, opts) {
|
|
7435
8196
|
workspaceMaintenance.assertAvailable();
|
|
@@ -7516,6 +8277,15 @@ async function startTurn(botId, text, opts) {
|
|
|
7516
8277
|
const policyRefusal = policyModelRefusal(instance);
|
|
7517
8278
|
if (policyRefusal)
|
|
7518
8279
|
throw Object.assign(new Error(policyRefusal), { status: 409, code: "managed_policy" });
|
|
8280
|
+
// On a Cloud home a guest's turn never gets a shell or reads outside its
|
|
8281
|
+
// own folder (docs/cloud-pro.md): an engine that cannot run it that way is
|
|
8282
|
+
// refused, in one plain line, before anything is recorded.
|
|
8283
|
+
// (The thread is idle here: a mark a turn that never started left is stale.)
|
|
8284
|
+
guestDrivenTurns.delete(threadId);
|
|
8285
|
+
const guestConfined = cloudGuestDriven(threadId) || cloudGuestDrivenHandoff(opts?.coordination?.id);
|
|
8286
|
+
if (guestConfined && instance.adapter.capabilities.guestTurns !== "confined") {
|
|
8287
|
+
throw Object.assign(new Error(guestEngineRefusal(threadId)), { status: 409, code: "guest_engine" });
|
|
8288
|
+
}
|
|
7519
8289
|
// Resolve only transport tags from this newly submitted text. The original
|
|
7520
8290
|
// string remains the durable message. Native-image providers get a
|
|
7521
8291
|
// path-free prompt and bounded inputs instead of needing a Read tool;
|
|
@@ -7536,11 +8306,9 @@ async function startTurn(botId, text, opts) {
|
|
|
7536
8306
|
const titled = store.titleTaskFromFirstMessage(bot.id, resolvedImages.text, threadId);
|
|
7537
8307
|
// The snippet is only the fallback name. A cheap one-shot may trade it
|
|
7538
8308
|
// for a title a person would have typed, but never on a peer-opened
|
|
7539
|
-
// row:
|
|
7540
|
-
//
|
|
7541
|
-
//
|
|
7542
|
-
// while the row still carries the snippet — a rename by the person or
|
|
7543
|
-
// by adoption has already broken that equality by then.
|
|
8309
|
+
// row: its title belongs to the assigned work. Everywhere else, the
|
|
8310
|
+
// swap happens only while the row still carries the snippet — a rename
|
|
8311
|
+
// by a person has already broken that equality by then.
|
|
7544
8312
|
const snippet = titled?.title;
|
|
7545
8313
|
if (titled && snippet && !titled.openedBy?.botId && llmThreadTitlesEnabled(cfg) && instance.generateText) {
|
|
7546
8314
|
void generateThreadTitle(instance, resolvedImages.text)
|
|
@@ -7572,7 +8340,7 @@ async function startTurn(botId, text, opts) {
|
|
|
7572
8340
|
// an edit hands us its already-branched user message; a plain send appends
|
|
7573
8341
|
let userMessage = opts?.userMessage;
|
|
7574
8342
|
if (opts?.editedMessageId) {
|
|
7575
|
-
const edited = store.branchMessage(threadId, opts.editedMessageId, text, opts.sendId);
|
|
8343
|
+
const edited = store.branchMessage(threadId, opts.editedMessageId, text, opts.sendId, opts.sender);
|
|
7576
8344
|
if (!edited)
|
|
7577
8345
|
throw Object.assign(new Error("only a user text message can be edited"), { status: 400 });
|
|
7578
8346
|
store.patchTask(bot.id, threadId, { rewound: true });
|
|
@@ -7617,6 +8385,9 @@ async function startTurn(botId, text, opts) {
|
|
|
7617
8385
|
// in the background — boat provisioning can take ~90s and must never
|
|
7618
8386
|
// hang the HTTP request
|
|
7619
8387
|
const dispatchClaimId = randomUUID();
|
|
8388
|
+
// Guest-driven for as long as it runs, wherever it runs (cloudGuestDriven).
|
|
8389
|
+
if (guestConfined)
|
|
8390
|
+
guestDrivenTurns.set(threadId, dispatchClaimId);
|
|
7620
8391
|
const resourceOwner = { threadId, generation: dispatchClaimId };
|
|
7621
8392
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
7622
8393
|
directTurnGenerationByThread.set(threadId, dispatchClaimId);
|
|
@@ -7630,7 +8401,11 @@ async function startTurn(botId, text, opts) {
|
|
|
7630
8401
|
}
|
|
7631
8402
|
requestGenerations.add(dispatchClaimId);
|
|
7632
8403
|
directRequestOwners.set(threadId, { generation: dispatchClaimId, messageId: requestMessageId,
|
|
7633
|
-
generations: requestGenerations, turnId: null });
|
|
8404
|
+
generations: requestGenerations, turnId: null, ...(opts?.automationSource ? { automation: opts.automationSource } : {}) });
|
|
8405
|
+
// On a Cloud home, a turn that is not provably the owner's leaves this
|
|
8406
|
+
// bot's memory "pending" until it ends (server/lending-memory.ts).
|
|
8407
|
+
if (CLOUD_HOME && cloudHomeLendingRefusal(cloudLendingTurn({ botId, threadId, generation: dispatchClaimId })) !== null)
|
|
8408
|
+
noteForeignTurn(botId, threadId, dispatchClaimId);
|
|
7634
8409
|
// An unknown control-plane wake may invalidate a prior final, but it must
|
|
7635
8410
|
// never acquire authority by guessing the latest user as its origin.
|
|
7636
8411
|
const pendingSource = requestMessageId ?? store.activePath(threadId).findLast(message => message.role === "user")?.id;
|
|
@@ -7756,9 +8531,9 @@ async function startTurn(botId, text, opts) {
|
|
|
7756
8531
|
const externalUpdate = Boolean(opts?.coordination?.resumed || unseen?.some((m) => m.keep));
|
|
7757
8532
|
// What a resumed session keeps from its launch: the standing instructions
|
|
7758
8533
|
// (tools, servers and — for Claude — the model are passed on every launch),
|
|
7759
|
-
// plus
|
|
7760
|
-
//
|
|
7761
|
-
//
|
|
8534
|
+
// plus settings that a live turn cannot change. Codex now reasserts its
|
|
8535
|
+
// model/provider on resume, but an in-flight turn and an omitted effort
|
|
8536
|
+
// retain the prior selection, so both still belong to the session here. An
|
|
7762
8537
|
// external update that finds any of it changed since the session started
|
|
7763
8538
|
// gets the fresh session and replay it always got, rather than a resume.
|
|
7764
8539
|
const persistentConfig = [bot.name, bot.title, bot.description, sectionContextSystemPrompt(bot.section),
|
|
@@ -7859,7 +8634,7 @@ async function startTurn(botId, text, opts) {
|
|
|
7859
8634
|
// composio — only to a driver that can mount them. Their tools are
|
|
7860
8635
|
// never pre-allowed, so every call rides the normal permission flow.
|
|
7861
8636
|
if (instance.adapter.capabilities.customMcp === true) {
|
|
7862
|
-
const custom = engineMcpServers(bot);
|
|
8637
|
+
const custom = await withMcpSignIn(engineMcpServers(bot), mcpOAuth);
|
|
7863
8638
|
if (Object.keys(custom).length)
|
|
7864
8639
|
integrations.custom = custom;
|
|
7865
8640
|
}
|
|
@@ -7871,7 +8646,8 @@ async function startTurn(botId, text, opts) {
|
|
|
7871
8646
|
if (worksInWorkspace) {
|
|
7872
8647
|
ensureWorkspace(bot.id);
|
|
7873
8648
|
// baseline for the journal's turn-boundary diff (see the bus hook)
|
|
7874
|
-
|
|
8649
|
+
if (bot.memoryEnabled !== false)
|
|
8650
|
+
beginMemoryTurn(bot.id, threadId);
|
|
7875
8651
|
}
|
|
7876
8652
|
const privateWorkspace = worksInWorkspace ? ensureTaskWorkspace(bot.id, threadId) : undefined;
|
|
7877
8653
|
const skillInstructions = renderSkillInstructions(selectedSkills, {
|
|
@@ -7887,8 +8663,13 @@ async function startTurn(botId, text, opts) {
|
|
|
7887
8663
|
// bot's folder for a task that runs elsewhere.
|
|
7888
8664
|
if (opts?.runOn === "cloud")
|
|
7889
8665
|
store.pinTaskCwd(bot.id, threadId, undefined, { none: true });
|
|
8666
|
+
// On a Cloud home a conversation a guest opened is pinned to its own
|
|
8667
|
+
// folder when it first runs, not the bot's project folder the owner's
|
|
8668
|
+
// conversations share (what it leaves there reaches none of them). One
|
|
8669
|
+
// that ran before keeps its pin, unless the session that opened it was
|
|
8670
|
+
// revoked: then it was unpinned at boot (server/cloud-owner.ts).
|
|
7890
8671
|
const pinnedCwd = privateWorkspace && opts?.runOn !== "cloud"
|
|
7891
|
-
? store.pinTaskCwd(bot.id, threadId, privateWorkspace)
|
|
8672
|
+
? store.pinTaskCwd(bot.id, threadId, privateWorkspace, { privateOnly: cloudGuestOpened(threadId) })
|
|
7892
8673
|
: null;
|
|
7893
8674
|
const cwd = pinnedCwd ?? undefined;
|
|
7894
8675
|
if (cwd && !claimTurnResource(resourceOwner, workspaceResource(cwd))) {
|
|
@@ -7926,13 +8707,13 @@ async function startTurn(botId, text, opts) {
|
|
|
7926
8707
|
throw new Error("the Computer engine works on the cloud computer — set Works on to Cloud, or choose another engine");
|
|
7927
8708
|
}
|
|
7928
8709
|
const wants = plan.computer;
|
|
7929
|
-
// A place the organisation disallows
|
|
7930
|
-
// prepared; Auto below simply skips
|
|
8710
|
+
// A place the organisation disallows, or a Cloud home never offers, is
|
|
8711
|
+
// refused before anything is prepared; Auto below simply skips them.
|
|
7931
8712
|
const wantedKind = teamComputer ? "box" : wants === "local" ? "thisComputer" : wants === "vm" ? "localVm"
|
|
7932
8713
|
: wants === "cloud" ? (cloudBackend === "vps" ? "vps" : "box") : undefined;
|
|
7933
|
-
const placeRefusal = wantedKind &&
|
|
8714
|
+
const placeRefusal = wantedKind && computerPlaceRefusal(wantedKind);
|
|
7934
8715
|
if (placeRefusal)
|
|
7935
|
-
throw Object.assign(new Error(placeRefusal), { status: 409, code:
|
|
8716
|
+
throw Object.assign(new Error(placeRefusal.message), { status: 409, code: placeRefusal.code });
|
|
7936
8717
|
let previewCapture = null;
|
|
7937
8718
|
let browserCapture = null;
|
|
7938
8719
|
let computerKind = null;
|
|
@@ -8084,7 +8865,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8084
8865
|
if (!mountsComputerMcp || instance.adapter.capabilities.remoteAgent === true) {
|
|
8085
8866
|
if (!strict)
|
|
8086
8867
|
return false;
|
|
8087
|
-
throw new Error("this model
|
|
8868
|
+
throw new Error("this model cannot use the Local VM — choose Claude or an ACP model provider, or select another computer destination");
|
|
8088
8869
|
}
|
|
8089
8870
|
if (!strict && localVmMode(cfg) === "pool") {
|
|
8090
8871
|
// localVmTargetForThread records pool affinity, so a turn the
|
|
@@ -8243,7 +9024,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8243
9024
|
}
|
|
8244
9025
|
}
|
|
8245
9026
|
if (wants === "cloud" && cloudBackend === "box" && !boat.boatConfigured(cfg)) {
|
|
8246
|
-
throw new Error(
|
|
9027
|
+
throw new Error(BOAT_NOT_CONFIGURED);
|
|
8247
9028
|
}
|
|
8248
9029
|
if (wants === "cloud" && cloudBackend === "box" && !integrations.computer) {
|
|
8249
9030
|
throw new Error("the cloud computer could not be created or reached");
|
|
@@ -8253,7 +9034,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8253
9034
|
// Auto reaches a Local VM this bot already has before it ever touches the
|
|
8254
9035
|
// host's own desktop: on a headless server that VM is the only desktop
|
|
8255
9036
|
// there is, and a person who prepared one meant it to be used.
|
|
8256
|
-
if (wants === undefined && !integrations.computer && !integrations.localComputer &&
|
|
9037
|
+
if (wants === undefined && !integrations.computer && !integrations.localComputer && !computerPlaceRefusal("localVm") && await attachLocalVm(false))
|
|
8257
9038
|
computerKind = "vm";
|
|
8258
9039
|
// An unattended run on a bot with a VPS configured never lands on the
|
|
8259
9040
|
// host's own desktop instead: a scheduled job clicking on someone's
|
|
@@ -8263,7 +9044,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8263
9044
|
!integrations.localComputer &&
|
|
8264
9045
|
wants === undefined &&
|
|
8265
9046
|
!unattendedVps &&
|
|
8266
|
-
|
|
9047
|
+
!computerPlaceRefusal("thisComputer") &&
|
|
8267
9048
|
shouldMountLocalComputer({
|
|
8268
9049
|
requested: undefined,
|
|
8269
9050
|
hostPlatform: process.platform,
|
|
@@ -8300,9 +9081,9 @@ async function startTurn(botId, text, opts) {
|
|
|
8300
9081
|
autoVpsProblem &&
|
|
8301
9082
|
!computerSelectionTurns.has(threadId)) {
|
|
8302
9083
|
const hint = opts?.automationSource
|
|
8303
|
-
? "This scheduled run tried to start the VPS computer and could not reach it. Check the VPS connection in
|
|
9084
|
+
? "This scheduled run tried to start the VPS computer and could not reach it. Check the VPS connection in Settings → API keys."
|
|
8304
9085
|
: bot.autoStartVps
|
|
8305
|
-
? "Check the VPS connection in
|
|
9086
|
+
? "Check the VPS connection in Settings → API keys."
|
|
8306
9087
|
: "Open Computer and enable Start VPS automatically, or choose Cloud to start it manually.";
|
|
8307
9088
|
throw new Error(`${autoVpsProblem}. ${hint}`);
|
|
8308
9089
|
}
|
|
@@ -8348,7 +9129,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8348
9129
|
const credentialPrompt = integrations.agents ? CREDENTIAL_PROMPT + (boundedCoordination ? "" : THREADS_PROMPT) : "";
|
|
8349
9130
|
const routinePrompt = integrations.agents ? ROUTINE_PROMPT : "";
|
|
8350
9131
|
const profilePrompt = integrations.agents ? PROFILE_PROMPT : "";
|
|
8351
|
-
const recallPrompt = integrations.agents ? SESSION_SEARCH_SYSTEM_PROMPT : "";
|
|
9132
|
+
const recallPrompt = integrations.agents && bot.memoryEnabled !== false ? SESSION_SEARCH_SYSTEM_PROMPT : "";
|
|
8352
9133
|
const learnPrompt = skillAuthoring ? LEARN_PROMPT : "";
|
|
8353
9134
|
// (activeVpsThreads was already claimed above, before the provision or
|
|
8354
9135
|
// reuse await, so the backend guards saw this turn the whole time.)
|
|
@@ -8462,7 +9243,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8462
9243
|
{ id: "files", label: "File locations", text: worksInWorkspace && opts?.runOn !== "cloud" ? workspaceLocationsPrompt(bot.id, cwd, liveBot?.cwd ?? bot.cwd) : "" },
|
|
8463
9244
|
{ id: "computer", label: "Computer", text: computerPrompt(computerPromptKind) },
|
|
8464
9245
|
{ 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) }) },
|
|
9246
|
+
{ 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) }) },
|
|
9247
|
+
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
8466
9248
|
// gated on the integration, not the key: the hint only goes to a
|
|
8467
9249
|
// bot whose driver actually mounted the tools
|
|
8468
9250
|
{ id: "composio", label: "Connected apps", text: integrations.composio ? composioSystemPrompt(liveBot?.connectorTools ?? bot.connectorTools) : "" },
|
|
@@ -8480,8 +9262,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8480
9262
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
8481
9263
|
// what the bot said lately in its other conversations, so a task
|
|
8482
9264
|
// never redoes — or forgets — what another one already did
|
|
8483
|
-
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentWork(recentWorkSources(bot), bot, { userName: cfg.profile?.name?.trim() || "User", currentThreadId: threadId })) },
|
|
8484
|
-
{ id: "memory", label: "Memory", text: memorySystemPrompt(bot.id, { managedWrites: Boolean(integrations.agents), fileTools: worksInWorkspace }) },
|
|
9265
|
+
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentWork(recentWorkSources(bot), bot, { userName: cfg.profile?.name?.trim() || "User", currentThreadId: threadId, ...recentWorkFilter() })) },
|
|
9266
|
+
{ id: "memory", label: "Memory", text: memorySystemPrompt(bot.id, { managedWrites: Boolean(integrations.agents), fileTools: worksInWorkspace, enabled: bot.memoryEnabled !== false }) },
|
|
8485
9267
|
{ id: "skills", label: "Skills index", text: privateWorkspace ? skillsSystemPrompt(bot.id) : "" },
|
|
8486
9268
|
{ id: "skill-instructions", label: "Skill instructions", text: skillInstructions },
|
|
8487
9269
|
{ id: "playbooks", label: "Playbooks", text: packagePlaybooks },
|
|
@@ -8525,7 +9307,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8525
9307
|
text: withRecalled(recalled, dispatchContext.turnText),
|
|
8526
9308
|
refreshSystemPrompt: true,
|
|
8527
9309
|
images: turnImages,
|
|
8528
|
-
approvalMode: approvalModeForTurn(bot, commsDepth > 0),
|
|
9310
|
+
approvalMode: approvalModeForTurn(bot, commsDepth > 0, threadId),
|
|
9311
|
+
...(guestConfined ? { guestConfined: true, confinedWhy: confinedWhy(threadId) } : {}),
|
|
8529
9312
|
model,
|
|
8530
9313
|
effort,
|
|
8531
9314
|
variant,
|
|
@@ -8816,7 +9599,10 @@ function routineRunFallbackText(card) {
|
|
|
8816
9599
|
: card.status === "queued" && card.deferredAt != null
|
|
8817
9600
|
? "deferred: target busy"
|
|
8818
9601
|
: card.status);
|
|
8819
|
-
|
|
9602
|
+
// Clients without the card (older phones) draw only this line, so it
|
|
9603
|
+
// carries what the run said, not just that it finished.
|
|
9604
|
+
const detail = card.summary ?? card.error;
|
|
9605
|
+
return `Routine “${card.routineName}” ${state}${detail ? `\n\n${detail}` : ""}`;
|
|
8820
9606
|
}
|
|
8821
9607
|
/** Upsert one durable lifecycle card per run. Replaying the same transition,
|
|
8822
9608
|
* including restart recovery, patches the existing run id instead of adding
|
|
@@ -8861,9 +9647,14 @@ function syncRoutineRunToSource(run) {
|
|
|
8861
9647
|
const execution = store.taskByThread(run.botId, run.threadId);
|
|
8862
9648
|
const freshExecution = execution && run.status === "running" && !execution.routineRunId &&
|
|
8863
9649
|
store.messagesFor(run.threadId).length === 0;
|
|
8864
|
-
if (execution &&
|
|
9650
|
+
if (execution && freshExecution) {
|
|
8865
9651
|
store.patchTask(run.botId, run.threadId, { routineRunId: run.id, unread: false });
|
|
8866
9652
|
}
|
|
9653
|
+
else if (execution?.routineRunId === run.id && execution.unread) {
|
|
9654
|
+
// Already this run. Clear the dot without rewriting the tag, so a
|
|
9655
|
+
// promoted thread (tag cleared, transcript present) stays visible.
|
|
9656
|
+
store.patchTask(run.botId, run.threadId, { unread: false });
|
|
9657
|
+
}
|
|
8867
9658
|
}
|
|
8868
9659
|
// Merely queueing/running is ambient progress. Attention and terminal
|
|
8869
9660
|
// states become unread in the conversation where the user asked for them.
|
|
@@ -8955,8 +9746,13 @@ routines = new RoutineManager({
|
|
|
8955
9746
|
}
|
|
8956
9747
|
return groupIsWorking(group) || coordinator.busy ? "busy" : "ready";
|
|
8957
9748
|
},
|
|
8958
|
-
createTask: (botId, title, activate = false) => {
|
|
9749
|
+
createTask: (botId, title, activate = false, routineId) => {
|
|
8959
9750
|
const task = store.createTask(botId, title, activate);
|
|
9751
|
+
// On a Cloud home a run's conversation is opened, like its results
|
|
9752
|
+
// conversation, by whoever wrote the routine: a run of one that is
|
|
9753
|
+
// nobody's is confined to a folder of its own, never the bot's project.
|
|
9754
|
+
if (task && CLOUD_HOME && routineId)
|
|
9755
|
+
threadStarters.set(task.threadId, routineOpener(routineId));
|
|
8960
9756
|
const bot = store.bot(botId);
|
|
8961
9757
|
if (task && bot)
|
|
8962
9758
|
broadcast({ kind: "bot", bot: publicBot(bot) });
|
|
@@ -8984,7 +9780,12 @@ routines = new RoutineManager({
|
|
|
8984
9780
|
// explicit resultsThreadId must be a visible task owned by the running bot.
|
|
8985
9781
|
if (!forceNew && routineSourceOwner(routine))
|
|
8986
9782
|
return routine.resultsThreadId;
|
|
8987
|
-
|
|
9783
|
+
const threadId = store.createTask(routine.botId, `${routine.name} · Results`, false)?.threadId;
|
|
9784
|
+
// On a Cloud home the results conversation is opened by whoever wrote the
|
|
9785
|
+
// routine: it carries their name for it (cloudGuestOpened).
|
|
9786
|
+
if (threadId && CLOUD_HOME)
|
|
9787
|
+
threadStarters.set(threadId, routineOpener(routine.id));
|
|
9788
|
+
return threadId;
|
|
8988
9789
|
},
|
|
8989
9790
|
discardResultsThread: (botId, threadId) => {
|
|
8990
9791
|
const task = store.taskByThread(botId, threadId);
|
|
@@ -9046,6 +9847,63 @@ routines = new RoutineManager({
|
|
|
9046
9847
|
notify(buildNotification("routine-deferred", notificationBot, routineSourceThread(run) ?? bot.threadId, detail));
|
|
9047
9848
|
},
|
|
9048
9849
|
});
|
|
9850
|
+
// A Cloud home is personal (server/cloud-owner.ts): every stored session
|
|
9851
|
+
// without admin scope is revoked and what it opened or wrote is nobody's;
|
|
9852
|
+
// once (and after a restore) the earlier keys are settled. Before anything
|
|
9853
|
+
// is served, and before any routine can run.
|
|
9854
|
+
if (CLOUD_HOME && CLOUD_OWNER_KEY && cloudRoutineAuthors) {
|
|
9855
|
+
const authors = cloudRoutineAuthors;
|
|
9856
|
+
const routineById = (id) => routines?.listRoutines().find((routine) => routine.id === id);
|
|
9857
|
+
cloudEarlierOwners = settleCloudOwnership({
|
|
9858
|
+
file: join(DATA_DIR, "cloud-owner.json"),
|
|
9859
|
+
machineId: CLOUD_HOME.machineId,
|
|
9860
|
+
ownerKey: CLOUD_OWNER_KEY,
|
|
9861
|
+
nobodyKey: CLOUD_NOBODY_KEY,
|
|
9862
|
+
sessions,
|
|
9863
|
+
personKey,
|
|
9864
|
+
starters: threadStarters,
|
|
9865
|
+
writers: authors,
|
|
9866
|
+
routines: {
|
|
9867
|
+
ids: () => (routines?.listRoutines() ?? []).map((routine) => routine.id),
|
|
9868
|
+
writer: (id) => authors.writer(id),
|
|
9869
|
+
fingerprinted: (id) => { const routine = routineById(id); return Boolean(routine) && authors.authored(id, routine); },
|
|
9870
|
+
resultsOpener: (id) => { const results = routineById(id)?.resultsThreadId; return results ? threadStarters.get(results) : undefined; },
|
|
9871
|
+
shape: (id) => { const routine = routineById(id); return routine ? approvalShape(routine) : undefined; },
|
|
9872
|
+
name: (id, person) => authors.wrote(id, person),
|
|
9873
|
+
pause: (ids) => { for (const id of ids)
|
|
9874
|
+
routines?.update(id, { enabled: false }); },
|
|
9875
|
+
},
|
|
9876
|
+
unpin: (threadIds) => { for (const threadId of threadIds)
|
|
9877
|
+
store.unpinTaskCwd(threadId); },
|
|
9878
|
+
lines: () => {
|
|
9879
|
+
const senders = new Set(), answerers = new Set();
|
|
9880
|
+
const approvals = [];
|
|
9881
|
+
const threads = new Set([...store.bots.flatMap((bot) => store.tasks(bot.id).map((task) => task.threadId)), ...store.groups.map((group) => group.threadId)]);
|
|
9882
|
+
for (const threadId of threads) {
|
|
9883
|
+
for (const line of store.messagesFor(threadId)) {
|
|
9884
|
+
const sender = linePersonKey(line);
|
|
9885
|
+
if (sender)
|
|
9886
|
+
senders.add(sender);
|
|
9887
|
+
const answerer = line.card?.answeredBy?.kind === "session" ? line.card.answeredBy.person : undefined;
|
|
9888
|
+
if (answerer)
|
|
9889
|
+
answerers.add(answerer);
|
|
9890
|
+
// A routine a bot proposed, created when this person allowed its card.
|
|
9891
|
+
const request = line.card?.routineRequest;
|
|
9892
|
+
if (answerer && line.card?.answered === "allow" && request?.operation.action === "create" && request.resultId) {
|
|
9893
|
+
const definition = request.operation.routine;
|
|
9894
|
+
approvals.push([request.resultId, answerer, approvalShape({ prompt: definition.instructions, botId: request.operation.forBot?.botId ?? request.botId,
|
|
9895
|
+
runOn: definition.runOn, schedule: asSchedule(definition.schedule, 0) })]);
|
|
9896
|
+
}
|
|
9897
|
+
}
|
|
9898
|
+
}
|
|
9899
|
+
return { senders, answerers, approvals };
|
|
9900
|
+
},
|
|
9901
|
+
...(restoredAtStart ? { restoredNow: restoredAtStart } : {}),
|
|
9902
|
+
...(workspaceRestore.restored && workspaceRestore.id ? { lastRestore: workspaceRestore.id } : {}),
|
|
9903
|
+
log: (line) => console.log(line),
|
|
9904
|
+
});
|
|
9905
|
+
ownerOnlyCache.clear();
|
|
9906
|
+
}
|
|
9049
9907
|
orgLibrary = new OrgLibrary({
|
|
9050
9908
|
dataDir: DATA_DIR,
|
|
9051
9909
|
store,
|
|
@@ -9120,7 +9978,7 @@ async function cloudRoutineReadiness(botId, threadId) {
|
|
|
9120
9978
|
return { ready: false, reason: "The Cloud VM runner is unavailable. Restart OpenMausBot and try again." };
|
|
9121
9979
|
try {
|
|
9122
9980
|
if ((await instance.snapshot()).state !== "available") {
|
|
9123
|
-
return { ready: false, reason: "The target bot's model
|
|
9981
|
+
return { ready: false, reason: "The target bot's model is not ready to use the Boat cloud computer." };
|
|
9124
9982
|
}
|
|
9125
9983
|
const teamComputer = inheritedTeamComputer(bot);
|
|
9126
9984
|
const ownerId = teamComputer ? teamComputerOwner(teamComputer.id) : bot.id;
|
|
@@ -9673,9 +10531,15 @@ function sendRoutineResolution(res, result) {
|
|
|
9673
10531
|
});
|
|
9674
10532
|
return true;
|
|
9675
10533
|
}
|
|
9676
|
-
function resolveAndSendRoutine(res, args) {
|
|
10534
|
+
function resolveAndSendRoutine(res, args, ownersAnswer = false) {
|
|
9677
10535
|
const card = store.messagesFor(args.threadId).find((message) => message.card?.requestId === args.requestId && message.card.routineRequest)?.card;
|
|
10536
|
+
// On a Cloud home a routine the owner approved on its card is theirs; one
|
|
10537
|
+
// it only changed stays whosever it was (cloudOwnerApplied).
|
|
10538
|
+
const operation = card?.routineRequest?.operation;
|
|
10539
|
+
const wasOwners = operation && operation.action !== "create" ? cloudRoutineIsOwners(operation.routineId) : false;
|
|
9678
10540
|
const result = routineRequests.resolve(args);
|
|
10541
|
+
if (ownersAnswer && result.claimed && result.state === "applied")
|
|
10542
|
+
cloudOwnerApplied(result.action, result.resultId, wasOwners);
|
|
9679
10543
|
if (result.claimed &&
|
|
9680
10544
|
(result.state === "applied" || result.state === "denied")) {
|
|
9681
10545
|
appendDecision(DATA_DIR, {
|
|
@@ -9811,12 +10675,25 @@ const webhooks = new WebhookManager({
|
|
|
9811
10675
|
findRun: (webhookId, deliveryId) => routines.webhookRunReceipt(webhookId, deliveryId),
|
|
9812
10676
|
cancelQueued: (webhookId, message) => routines.cancelQueuedWebhook(webhookId, message),
|
|
9813
10677
|
pendingRuns: (webhookId) => routines.activeWebhookRunCount(webhookId),
|
|
9814
|
-
// delivery:"post" webhooks land in
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
10678
|
+
// delivery:"post" webhooks land in a dedicated "Updates" task, never
|
|
10679
|
+
// bot.threadId (the bot's currently-selected task) -- see
|
|
10680
|
+
// resolvePostThread below. Fixes
|
|
10681
|
+
// https://github.com/milind-soni/OpenMausBot/issues/2071: a post used to
|
|
10682
|
+
// land wherever the owner (or another automation) had last switched
|
|
10683
|
+
// that bot's selection, including a live conversation.
|
|
10684
|
+
post: (botId, threadId, text) => {
|
|
10685
|
+
if (!store.bot(botId))
|
|
9818
10686
|
return;
|
|
9819
|
-
store.appendMessage(
|
|
10687
|
+
store.appendMessage(threadId, { role: "bot", kind: "text", text });
|
|
10688
|
+
},
|
|
10689
|
+
// Mirrors resolveResultsThread's routines wiring a few hundred lines up
|
|
10690
|
+
// in this same file: create-on-first-use, never activated (so it never
|
|
10691
|
+
// steals the bot's live selection the way POST /api/bots/:id/tasks
|
|
10692
|
+
// does), reused forever after via trigger.resultsThreadId.
|
|
10693
|
+
resolvePostThread: (trigger, forceNew) => {
|
|
10694
|
+
if (!forceNew && trigger.resultsThreadId)
|
|
10695
|
+
return trigger.resultsThreadId;
|
|
10696
|
+
return store.createTask(trigger.botId, "Updates", false)?.threadId;
|
|
9820
10697
|
},
|
|
9821
10698
|
});
|
|
9822
10699
|
let webhookIngress = null;
|
|
@@ -9903,6 +10780,33 @@ function serializeRoomContext(threadId, userName, textOverrides, readerBotId) {
|
|
|
9903
10780
|
})
|
|
9904
10781
|
.join("\n");
|
|
9905
10782
|
}
|
|
10783
|
+
/** The decision model's view of a room message: the room, its people, the
|
|
10784
|
+
* last room lines before this one, and the message. Text is scrubbed of
|
|
10785
|
+
* secrets and attachment markup before it leaves this machine. */
|
|
10786
|
+
function roomRoutingInput(roomName, text, members, history, sender) {
|
|
10787
|
+
const userName = cfg.profile?.name?.trim() || "User";
|
|
10788
|
+
const clean = (value) => redactSecretsInText(extractTurnImages(value).text);
|
|
10789
|
+
const humans = new Set([userName]);
|
|
10790
|
+
const recent = history.flatMap((m) => {
|
|
10791
|
+
if (m.kind !== "text" || !m.text)
|
|
10792
|
+
return [];
|
|
10793
|
+
if (m.role === "user") {
|
|
10794
|
+
const person = m.sender?.name ?? userName;
|
|
10795
|
+
humans.add(person);
|
|
10796
|
+
return [{ from: person, text: clean(m.text) }];
|
|
10797
|
+
}
|
|
10798
|
+
return [{ from: `${m.from?.name ?? "A bot"} (bot)`, text: clean(m.text) }];
|
|
10799
|
+
});
|
|
10800
|
+
const from = sender?.name ?? userName;
|
|
10801
|
+
humans.add(from);
|
|
10802
|
+
return {
|
|
10803
|
+
room: roomName,
|
|
10804
|
+
humans: [...humans],
|
|
10805
|
+
members: members.map((member) => ({ id: member.id, name: member.name, title: member.title, description: member.description })),
|
|
10806
|
+
recent,
|
|
10807
|
+
message: { from, text: clean(text) },
|
|
10808
|
+
};
|
|
10809
|
+
}
|
|
9906
10810
|
// What each room has already taken from its bots. Keyed by room because the
|
|
9907
10811
|
// loop post_to_room can start is a property of the room, not of any one
|
|
9908
10812
|
// caller — three bots posting twice each is the same runaway as one bot
|
|
@@ -9961,7 +10865,7 @@ setupRetry = 0) {
|
|
|
9961
10865
|
// delegated Full elevation makes the two disagree by construction: every
|
|
9962
10866
|
// such room turn then reads as "settings changed", retries once, and
|
|
9963
10867
|
// settles as busy without ever dispatching.
|
|
9964
|
-
const preparedApprovalMode = roomTurnApprovalMode(bot, orchestration);
|
|
10868
|
+
const preparedApprovalMode = roomTurnApprovalMode(bot, threadId, orchestration);
|
|
9965
10869
|
const preparedSelection = { ...bot.modelSelection };
|
|
9966
10870
|
const preparedComposio = bot.composio;
|
|
9967
10871
|
const instance = turnInstance(bot);
|
|
@@ -9971,8 +10875,14 @@ setupRetry = 0) {
|
|
|
9971
10875
|
return true;
|
|
9972
10876
|
}
|
|
9973
10877
|
const roomPolicyRefusal = instance ? policyModelRefusal(instance) : undefined;
|
|
9974
|
-
|
|
9975
|
-
|
|
10878
|
+
// A room turn a guest drives on a Cloud home is confined like a guest's
|
|
10879
|
+
// own conversation, or refused on an engine that cannot be.
|
|
10880
|
+
if (!groupSpeakers.has(threadId))
|
|
10881
|
+
guestDrivenTurns.delete(threadId);
|
|
10882
|
+
const roomGuestConfined = cloudGuestDriven(threadId) || cloudGuestDrivenHandoff(orchestration?.roomHandoffId);
|
|
10883
|
+
const roomGuestRefusal = roomGuestConfined && instance?.adapter.capabilities.guestTurns !== "confined" ? guestEngineRefusal(threadId) : undefined;
|
|
10884
|
+
if (!instance || roomPolicyRefusal || roomGuestRefusal) {
|
|
10885
|
+
const message = roomPolicyRefusal ?? roomGuestRefusal ?? `${bot.name}'s model is unavailable`;
|
|
9976
10886
|
store.appendMessage(threadId, {
|
|
9977
10887
|
role: "bot",
|
|
9978
10888
|
kind: "activity",
|
|
@@ -10005,6 +10915,11 @@ setupRetry = 0) {
|
|
|
10005
10915
|
return true;
|
|
10006
10916
|
}
|
|
10007
10917
|
const internalGeneration = beginInternalCapabilityGeneration(threadId);
|
|
10918
|
+
if (roomGuestConfined)
|
|
10919
|
+
guestDrivenTurns.set(threadId, internalGeneration);
|
|
10920
|
+
// A room turn is never provably the owner's alone (server/lending-memory.ts).
|
|
10921
|
+
if (CLOUD_HOME)
|
|
10922
|
+
noteForeignTurn(bot.id, threadId, internalGeneration);
|
|
10008
10923
|
const resourceOwner = { threadId, generation: internalGeneration };
|
|
10009
10924
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
10010
10925
|
let roomVmTarget = null;
|
|
@@ -10111,7 +11026,7 @@ setupRetry = 0) {
|
|
|
10111
11026
|
}
|
|
10112
11027
|
// user-configured MCP servers: same gating as the 1:1 site above.
|
|
10113
11028
|
if (instance.adapter.capabilities.customMcp === true) {
|
|
10114
|
-
const custom = engineMcpServers(bot);
|
|
11029
|
+
const custom = await withMcpSignIn(engineMcpServers(bot), mcpOAuth);
|
|
10115
11030
|
if (Object.keys(custom).length)
|
|
10116
11031
|
integrations.custom = custom;
|
|
10117
11032
|
}
|
|
@@ -10133,7 +11048,7 @@ setupRetry = 0) {
|
|
|
10133
11048
|
if (!readyGroup || !stillOwnsThread || !readyGroup.memberIds.includes(readyBot.id))
|
|
10134
11049
|
return false;
|
|
10135
11050
|
const setupChanged = turnInstance(readyBot) !== instance ||
|
|
10136
|
-
roomTurnApprovalMode(readyBot, orchestration) !== preparedApprovalMode ||
|
|
11051
|
+
roomTurnApprovalMode(readyBot, threadId, orchestration) !== preparedApprovalMode ||
|
|
10137
11052
|
readyBot.modelSelection.instanceId !== preparedSelection.instanceId ||
|
|
10138
11053
|
readyBot.modelSelection.model !== preparedSelection.model ||
|
|
10139
11054
|
readyBot.modelSelection.effort !== preparedSelection.effort ||
|
|
@@ -10244,13 +11159,14 @@ setupRetry = 0) {
|
|
|
10244
11159
|
readyBot.browser !== false &&
|
|
10245
11160
|
instance.adapter.capabilities.browserMcp === true,
|
|
10246
11161
|
});
|
|
10247
|
-
// A place the organisation disallows
|
|
10248
|
-
// provisioned or started, exactly as a bot
|
|
11162
|
+
// A place the organisation disallows, or a Cloud home never offers, is
|
|
11163
|
+
// refused before anything is provisioned or started, exactly as a bot
|
|
11164
|
+
// thread refuses it.
|
|
10249
11165
|
const roomKind = roomTeamComputer ? "box" : roomPlan.computer === "local" ? "thisComputer" : readyBot.computer === "vm" ? "localVm"
|
|
10250
11166
|
: roomPlan.computer === "cloud" ? (turnProvider(readyBot) === "vps" ? "vps" : "box") : undefined;
|
|
10251
|
-
const roomPlaceRefusal = roomKind &&
|
|
11167
|
+
const roomPlaceRefusal = roomKind && computerPlaceRefusal(roomKind);
|
|
10252
11168
|
if (roomPlaceRefusal)
|
|
10253
|
-
throw Object.assign(new Error(roomPlaceRefusal), { code:
|
|
11169
|
+
throw Object.assign(new Error(roomPlaceRefusal.message), { code: roomPlaceRefusal.code });
|
|
10254
11170
|
// The Computer engine runs on the Boat; this computer has no tools it can
|
|
10255
11171
|
// reach, exactly as a bot thread refuses it.
|
|
10256
11172
|
if (roomPlan.computer === "local" && instance.adapter.capabilities.remoteAgent === true) {
|
|
@@ -10318,7 +11234,7 @@ setupRetry = 0) {
|
|
|
10318
11234
|
}
|
|
10319
11235
|
else {
|
|
10320
11236
|
if (!boat.boatConfigured(cfg))
|
|
10321
|
-
throw new Error(
|
|
11237
|
+
throw new Error(BOAT_NOT_CONFIGURED);
|
|
10322
11238
|
const remoteAgent = instance.adapter.capabilities.remoteAgent === true;
|
|
10323
11239
|
const attached = await attachBotBoat(readyBot, resourceOwner, { explicitCloud: true,
|
|
10324
11240
|
canMount: instance.adapter.capabilities.usesCloudComputer === true, remoteAgent });
|
|
@@ -10336,7 +11252,7 @@ setupRetry = 0) {
|
|
|
10336
11252
|
// Claim the same lease as direct turns before asynchronous VM setup.
|
|
10337
11253
|
if (readyBot.computer === "vm") {
|
|
10338
11254
|
if (instance.adapter.capabilities.computerMcp !== true || instance.adapter.capabilities.remoteAgent === true) {
|
|
10339
|
-
throw new Error("this model
|
|
11255
|
+
throw new Error("this model cannot use the Local VM");
|
|
10340
11256
|
}
|
|
10341
11257
|
// A distinct identity fences cleanup even in shared mode on the same room thread.
|
|
10342
11258
|
const target = { ...localVmTargetForThread(readyBot.id, threadId) };
|
|
@@ -10429,7 +11345,7 @@ setupRetry = 0) {
|
|
|
10429
11345
|
const worksInWorkspace = supportsWorkspaceFiles(instance.driverKind);
|
|
10430
11346
|
const workspace = worksInWorkspace ? ensureWorkspace(bot.id) : undefined;
|
|
10431
11347
|
// a room member's memory writes are journaled the same as a 1:1 turn's
|
|
10432
|
-
if (workspace)
|
|
11348
|
+
if (workspace && bot.memoryEnabled !== false)
|
|
10433
11349
|
beginMemoryTurn(bot.id, threadId);
|
|
10434
11350
|
// The room's folder pins here — on the first turn that actually
|
|
10435
11351
|
// dispatches, not at PATCH time — so a folder set on a never-used room
|
|
@@ -10437,19 +11353,22 @@ setupRetry = 0) {
|
|
|
10437
11353
|
// has its folder moved underneath it. Off-host members skip the folder
|
|
10438
11354
|
// but must not decide the pin: the room's desk is a property of the
|
|
10439
11355
|
// room, not of whichever member happened to speak first.
|
|
10440
|
-
|
|
11356
|
+
// A room a guest opened on a Cloud home works in a folder of its own too.
|
|
11357
|
+
const cwd = workspace && cloudGuestOpened(threadId)
|
|
11358
|
+
? ensureTaskWorkspace(bot.id, threadId)
|
|
11359
|
+
: groupTurnCwd(workspace, () => store.pinGroupCwd(readyGroup.id, threadId));
|
|
10441
11360
|
{
|
|
10442
11361
|
const drift = checkSoulDrift(bot.id, bot.soul ?? "", bot.soulHash ?? "");
|
|
10443
11362
|
if (drift.drift !== Boolean(bot.soulDrift))
|
|
10444
11363
|
store.patchBot(bot.id, { soulDrift: drift.drift });
|
|
10445
11364
|
}
|
|
10446
|
-
const roomMemory = memorySystemPrompt(bot.id, { managedWrites: Boolean(integrations.agents), fileTools: worksInWorkspace });
|
|
11365
|
+
const roomMemory = memorySystemPrompt(bot.id, { managedWrites: Boolean(integrations.agents), fileTools: worksInWorkspace, enabled: bot.memoryEnabled !== false });
|
|
10447
11366
|
// The same brief a 1:1 turn gets: what this member said lately in its
|
|
10448
11367
|
// other conversations, so a standup is answered from what happened. A
|
|
10449
11368
|
// brief can carry a private chat into the room; as with session_search
|
|
10450
11369
|
// (#754) the room is told, once per source thread, rather than the
|
|
10451
11370
|
// crossing being blocked.
|
|
10452
|
-
const recentLines = recentWork(recentWorkSources(bot), bot, { userName, currentThreadId: threadId });
|
|
11371
|
+
const recentLines = recentWork(recentWorkSources(bot), bot, { userName, currentThreadId: threadId, ...recentWorkFilter() });
|
|
10453
11372
|
const roomComputerPromptKind = resolveComputerPromptKind({
|
|
10454
11373
|
kind: roomTeamComputer || roomComputerKind === "box" ? "box" : roomVmTarget ? "vm" : roomComputerKind,
|
|
10455
11374
|
driverKind: instance.driverKind, cloudComputerMcp: instance.adapter.capabilities.cloudComputerMcp,
|
|
@@ -10472,9 +11391,10 @@ setupRetry = 0) {
|
|
|
10472
11391
|
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
10473
11392
|
{ id: "computer", label: "Computer", text: computerPrompt(roomComputerPromptKind) },
|
|
10474
11393
|
{ 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 }) },
|
|
11394
|
+
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: roomTeamComputer ? "cloud" : roomVmTarget ? "vm" : surfaceOfComputerKind(roomComputerKind), browser: Boolean(integrations.browser) }, { note: roomPlan.note, cloudHome: Boolean(CLOUD_HOME) }) },
|
|
11395
|
+
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
10476
11396
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
10477
|
-
{ id: "recall", label: "Recall", text: integrations.agents ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
|
|
11397
|
+
{ id: "recall", label: "Recall", text: integrations.agents && bot.memoryEnabled !== false ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
|
|
10478
11398
|
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentLines) },
|
|
10479
11399
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
10480
11400
|
// the room path has always put a newline before memory and trimmed
|
|
@@ -10609,7 +11529,8 @@ setupRetry = 0) {
|
|
|
10609
11529
|
text: withRecalled(roomRecalled, text),
|
|
10610
11530
|
refreshSystemPrompt: true,
|
|
10611
11531
|
images: turnImages,
|
|
10612
|
-
approvalMode: roomTurnApprovalMode(readyBot, orchestration),
|
|
11532
|
+
approvalMode: roomTurnApprovalMode(readyBot, threadId, orchestration),
|
|
11533
|
+
...(roomGuestConfined ? { guestConfined: true, confinedWhy: confinedWhy(threadId) } : {}),
|
|
10613
11534
|
system: roomSystem.text,
|
|
10614
11535
|
systemStable: roomSystem.stable,
|
|
10615
11536
|
systemVolatile: roomSystem.volatile,
|
|
@@ -11131,6 +12052,13 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
11131
12052
|
if (options.goalCoordinatorBotId && (channelMode !== "goal" || !requestedGoalCoordinator)) {
|
|
11132
12053
|
throw Object.assign(new Error("the selected goal coordinator is not an active room member"), { status: 409 });
|
|
11133
12054
|
}
|
|
12055
|
+
// An Auto room may ask the decision model who answers. It reads the room
|
|
12056
|
+
// as it stood before this message, which is appended just below.
|
|
12057
|
+
const autoCandidate = !group.dm && channelMode === "chat" && group.defaultResponder.kind === "auto" &&
|
|
12058
|
+
deciderReady(cfg, "roomRouting");
|
|
12059
|
+
const historyBeforeSend = autoCandidate
|
|
12060
|
+
? store.messagesFor(threadId).filter((m) => m.kind === "text" && m.text).slice(-GROUP_CONTEXT_MESSAGES)
|
|
12061
|
+
: [];
|
|
11134
12062
|
// A coalesced drain appends every queued line under its own queueId (the
|
|
11135
12063
|
// chips and sendId receipts stay per-item); the LAST line is the turn's
|
|
11136
12064
|
// user message, exactly like the 1:1 drain's userMessage.
|
|
@@ -11223,25 +12151,42 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
11223
12151
|
}
|
|
11224
12152
|
return message;
|
|
11225
12153
|
}
|
|
12154
|
+
// Auto with nobody addressed: the responders above are only the fallback
|
|
12155
|
+
// until the decision model answers, in the async round below. The append
|
|
12156
|
+
// and this function's return stay synchronous either way.
|
|
12157
|
+
const autoRoute = autoCandidate && !goalCoordinator && availableMembers.length > 1 &&
|
|
12158
|
+
roomResponders(text, availableMembers, { kind: "mentions" }).length === 0;
|
|
11226
12159
|
// The snippet is only the fallback name here too. The member about to
|
|
11227
12160
|
// answer supplies the same cheap one-shot the bot path uses, and the
|
|
11228
12161
|
// swap lands only while the row still carries the snippet — a rename by
|
|
11229
12162
|
// the person always wins. Channel tasks carry no peer provenance (no
|
|
11230
12163
|
// assignment opens them), and a member whose engine offers no text
|
|
11231
12164
|
// one-shot simply keeps the snippet.
|
|
11232
|
-
const titleBot = goalCoordinator ?? responders[0];
|
|
11233
|
-
const titleInstance = registry.get(titleBot.modelSelection.instanceId);
|
|
11234
12165
|
const titleText = extractTurnImages(text).text;
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
12166
|
+
const startTitle = (titleBot) => {
|
|
12167
|
+
const titleInstance = registry.get(titleBot.modelSelection.instanceId);
|
|
12168
|
+
// An engine the organisation disallows never receives the text, even for a title.
|
|
12169
|
+
if (titled && snippet && titleText.trim() && llmThreadTitlesEnabled(cfg) && titleInstance?.generateText && !policyModelRefusal(titleInstance)) {
|
|
12170
|
+
void generateThreadTitle(titleInstance, titleText)
|
|
12171
|
+
.then((title) => {
|
|
12172
|
+
if (title)
|
|
12173
|
+
store.retitleGroupTask(group.id, threadId, snippet, title);
|
|
12174
|
+
})
|
|
12175
|
+
.catch(() => undefined);
|
|
12176
|
+
}
|
|
12177
|
+
};
|
|
12178
|
+
// An Auto round titles with whoever the decision picks, once it has.
|
|
12179
|
+
if (!autoRoute)
|
|
12180
|
+
startTitle(goalCoordinator ?? responders[0]);
|
|
12181
|
+
const operation = beginGroupTurnOperation(groupId, threadId, goalCoordinator || autoRoute ? [] : responders.map((responder) => responder.id));
|
|
12182
|
+
// Asked now, in parallel with any earlier room work still queued: the
|
|
12183
|
+
// answer is waited for only when this round's turn comes up. It never
|
|
12184
|
+
// rejects; a Stop aborts it through the operation's signal.
|
|
12185
|
+
const routing = autoRoute
|
|
12186
|
+
? decideRoomResponder(decider, roomRoutingInput(group.name, text, availableMembers, historyBeforeSend, options.sender), {
|
|
12187
|
+
signal: operation.cancellation.signal,
|
|
11241
12188
|
})
|
|
11242
|
-
|
|
11243
|
-
}
|
|
11244
|
-
const operation = beginGroupTurnOperation(groupId, threadId, goalCoordinator ? [] : responders.map((responder) => responder.id));
|
|
12189
|
+
: null;
|
|
11245
12190
|
if (queuedGroup && queuedGroup.length > 0) {
|
|
11246
12191
|
// member turns use these stable ids to carry every burst line's words
|
|
11247
12192
|
// and attachments through the turn that swallowed the burst
|
|
@@ -11298,9 +12243,27 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
11298
12243
|
await runGroupGoalOperation({ groupId, threadId, coordinator: goalCoordinator, members, operation });
|
|
11299
12244
|
}
|
|
11300
12245
|
else {
|
|
12246
|
+
let round = responders;
|
|
12247
|
+
let routedBy;
|
|
12248
|
+
if (routing) {
|
|
12249
|
+
const route = await routing;
|
|
12250
|
+
if (operation.cancelled)
|
|
12251
|
+
return;
|
|
12252
|
+
const picked = route.kind === "member" ? availableMembers.filter((member) => member.id === route.botId)
|
|
12253
|
+
: route.kind === "everyone" ? availableMembers
|
|
12254
|
+
: [];
|
|
12255
|
+
if (picked.length && route.kind !== "fallback") {
|
|
12256
|
+
round = picked;
|
|
12257
|
+
routedBy = { provider: "jev", probability: Math.round(route.probability * 100) / 100 };
|
|
12258
|
+
}
|
|
12259
|
+
// What a routed round holds from the start, like any other round.
|
|
12260
|
+
for (const responder of round)
|
|
12261
|
+
operation.botIds.add(responder.id);
|
|
12262
|
+
startTitle(round[0]);
|
|
12263
|
+
}
|
|
11301
12264
|
const spoken = new Set();
|
|
11302
12265
|
const skillAuthoringClaim = { claimed: false };
|
|
11303
|
-
for (const responder of
|
|
12266
|
+
for (const responder of round) {
|
|
11304
12267
|
if (operation.cancelled)
|
|
11305
12268
|
break;
|
|
11306
12269
|
if (spoken.has(responder.id))
|
|
@@ -11316,7 +12279,19 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
11316
12279
|
spoken.add(responder.id);
|
|
11317
12280
|
continue;
|
|
11318
12281
|
}
|
|
11319
|
-
|
|
12282
|
+
// This speaker's first reply line says the decision model chose it.
|
|
12283
|
+
const stamp = routedBy ? { botId: responder.id, routedBy } : undefined;
|
|
12284
|
+
if (stamp)
|
|
12285
|
+
routedRoomReplies.set(threadId, stamp);
|
|
12286
|
+
let proceed;
|
|
12287
|
+
try {
|
|
12288
|
+
proceed = await runGroupMemberTurn(groupId, threadId, responder.id, 0, spoken, undefined, undefined, () => operation.cancelled, () => groupProviderHandshakeStarted(operation), () => groupProviderHandshakeSettled(operation), skillAuthoringClaim, undefined, operation);
|
|
12289
|
+
}
|
|
12290
|
+
finally {
|
|
12291
|
+
if (stamp && routedRoomReplies.get(threadId) === stamp)
|
|
12292
|
+
routedRoomReplies.delete(threadId);
|
|
12293
|
+
}
|
|
12294
|
+
if (!proceed)
|
|
11320
12295
|
break;
|
|
11321
12296
|
}
|
|
11322
12297
|
}
|
|
@@ -12188,24 +13163,15 @@ async function testCliBinary(cli, driver) {
|
|
|
12188
13163
|
});
|
|
12189
13164
|
}
|
|
12190
13165
|
/** A pre-save probe only needs PATH. Never hand credentials inherited by the
|
|
12191
|
-
* desktop/server process to an arbitrary wrapper selected through Settings.
|
|
13166
|
+
* desktop/server process to an arbitrary wrapper selected through Settings.
|
|
13167
|
+
* The shared lists (config.ts) cover every workspace, control-plane and
|
|
13168
|
+
* provider credential, including ones added later; the probe and `claude
|
|
13169
|
+
* update` keep everything else (HOME, proxy settings, CA bundles). */
|
|
12192
13170
|
function cliProbeEnvironment() {
|
|
12193
13171
|
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
|
-
]) {
|
|
13172
|
+
stripWorkspaceCredentialEnv(env);
|
|
13173
|
+
for (const key of PROVIDER_CREDENTIAL_ENV)
|
|
12207
13174
|
delete env[key];
|
|
12208
|
-
}
|
|
12209
13175
|
return env;
|
|
12210
13176
|
}
|
|
12211
13177
|
/** execFile's error carries the child's stderr in .stderr. */
|
|
@@ -12213,12 +13179,12 @@ function stderrOf(err) {
|
|
|
12213
13179
|
const s = err.stderr;
|
|
12214
13180
|
return typeof s === "string" ? s : Buffer.isBuffer(s) ? s.toString("utf8") : "";
|
|
12215
13181
|
}
|
|
12216
|
-
async function localVmPayload(target) {
|
|
13182
|
+
async function localVmPayload(target, auth) {
|
|
12217
13183
|
const status = await containerComputerStatus(undefined, undefined, target);
|
|
12218
13184
|
return {
|
|
12219
|
-
...status,
|
|
13185
|
+
...localVmViewerStatus(status, auth),
|
|
12220
13186
|
commands: setupCommands(status.runtime, process.platform, target),
|
|
12221
|
-
idle_timeout_ms:
|
|
13187
|
+
idle_timeout_ms: localVmIdleMs(),
|
|
12222
13188
|
mode: localVmMode(cfg),
|
|
12223
13189
|
max_instances: localVmMaxInstances(cfg),
|
|
12224
13190
|
};
|
|
@@ -12346,7 +13312,9 @@ function configStatus() {
|
|
|
12346
13312
|
return {
|
|
12347
13313
|
xai: { configured: Boolean(cfg.xai?.key) },
|
|
12348
13314
|
mistral: { configured: Boolean(cfg.mistral?.key) },
|
|
12349
|
-
anthropic: { configured: Boolean(cfg.anthropic?.key) },
|
|
13315
|
+
anthropic: { configured: Boolean(cfg.anthropic?.key), everyClaudeBot: cfg.anthropic?.everyClaudeBot !== false },
|
|
13316
|
+
openai: { configured: Boolean(cfg.openai?.key) },
|
|
13317
|
+
openrouter: { configured: Boolean(cfg.openrouter?.key) },
|
|
12350
13318
|
// a fleet agent on this server means Settings → Workspaces has something to drive
|
|
12351
13319
|
fleet: { available: fleetAvailable(fleetSocketPath()) },
|
|
12352
13320
|
// what this build is entitled to, so Settings shows only what works here
|
|
@@ -12365,17 +13333,24 @@ function configStatus() {
|
|
|
12365
13333
|
configured: composio.configured(cfg),
|
|
12366
13334
|
mode: composio.connectionMode(cfg),
|
|
12367
13335
|
},
|
|
12368
|
-
|
|
13336
|
+
// configured = cloud computers work here; included = through Cloud Pro,
|
|
13337
|
+
// not a saved key
|
|
13338
|
+
box: boat.describeBoatAccount(cfg),
|
|
12369
13339
|
vps: { configured: Boolean(vpsSshAlias(cfg)), sshAlias: vpsSshAlias(cfg) ?? "" },
|
|
12370
13340
|
opencodeGo: { configured: Boolean(cfg.opencodeGo?.apiKey) },
|
|
12371
13341
|
// the chosen voice is a setting, not a secret; the key is reported the
|
|
12372
13342
|
// same configured-or-not way as every other credential
|
|
12373
13343
|
tts: tts.describeVoice(cfg),
|
|
13344
|
+
// the decision model: switches and configured-or-not, never the key
|
|
13345
|
+
decider: describeDecider(cfg),
|
|
12374
13346
|
imageGen: avatarImageStatus(cfg),
|
|
12375
13347
|
// not a secret — the sidebar shows it
|
|
12376
13348
|
profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "", aboutMe: cfg.profile?.aboutMe ?? "" },
|
|
12377
13349
|
// the enrolled organisation's read-only desktop policy; null when not enrolled
|
|
12378
13350
|
managedPolicy: managedPolicy.summary(),
|
|
13351
|
+
// an OMB Cloud home (cloud-home.ts): the app offers no this computer and
|
|
13352
|
+
// no Local VM here
|
|
13353
|
+
...(CLOUD_HOME ? { cloudHome: true } : {}),
|
|
12379
13354
|
// not a secret — the settings picker shows it; "" = follow the system
|
|
12380
13355
|
language: cfg.language ?? "",
|
|
12381
13356
|
rooms: { turnTimeoutMinutes: roomTurnTimeoutMinutes(cfg) },
|
|
@@ -12390,6 +13365,7 @@ function configStatus() {
|
|
|
12390
13365
|
localVm: {
|
|
12391
13366
|
mode: localVmMode(cfg),
|
|
12392
13367
|
maxInstances: localVmMaxInstances(cfg),
|
|
13368
|
+
idleTimeoutMinutes: localVmIdleTimeoutMinutes(cfg),
|
|
12393
13369
|
},
|
|
12394
13370
|
features: {
|
|
12395
13371
|
skillAuthoring: skillAuthoringEnabled(cfg),
|
|
@@ -12404,6 +13380,8 @@ function configStatus() {
|
|
|
12404
13380
|
// own Claude Code MCP servers
|
|
12405
13381
|
claudeUserMcp: claudeUserMcpEnabled(cfg),
|
|
12406
13382
|
autoRecall: autoRecallEnabled(cfg),
|
|
13383
|
+
// hand-edited config.json only; the thread menu offers Regenerate title from it
|
|
13384
|
+
llmThreadTitles: llmThreadTitlesEnabled(cfg),
|
|
12407
13385
|
},
|
|
12408
13386
|
// first-run progress — not a secret; the app decides whether to show
|
|
12409
13387
|
// the welcome tour from this, never from browser storage
|
|
@@ -12412,6 +13390,7 @@ function configStatus() {
|
|
|
12412
13390
|
version: cfg.onboarding?.version ?? 0,
|
|
12413
13391
|
reelSeen: cfg.onboarding?.reelSeen === true,
|
|
12414
13392
|
hintsSeen: cfg.onboarding?.hintsSeen ?? [],
|
|
13393
|
+
...(cfg.onboarding?.firstTurnAt ? { firstTurnAt: cfg.onboarding.firstTurnAt } : {}),
|
|
12415
13394
|
},
|
|
12416
13395
|
// Which browser this server can give bots: the desktop app's surface,
|
|
12417
13396
|
// the agent-browser engine, or nothing yet (with the reason).
|
|
@@ -12445,7 +13424,14 @@ function mcpServerResponse() {
|
|
|
12445
13424
|
// While enrolled with custom servers off, say which servers stay configured
|
|
12446
13425
|
// but never reach bots, and why. Nothing here is written to config.json.
|
|
12447
13426
|
const policy = managedPolicy.current();
|
|
12448
|
-
const servers = listMcpServers(cfg.mcpServers).map(
|
|
13427
|
+
const servers = listMcpServers(cfg.mcpServers).map((server) => {
|
|
13428
|
+
if (!("url" in server))
|
|
13429
|
+
return server;
|
|
13430
|
+
const auth = mcpOAuth.authState(server.name, server.url);
|
|
13431
|
+
// the redirect URI to register with a pre-registered sign-in app
|
|
13432
|
+
const listed = server.oauth ? { ...server, oauth: { ...server.oauth, redirectUri: mcpOAuthRedirectUri(server.url) } } : server;
|
|
13433
|
+
return auth === "none" ? listed : { ...listed, auth };
|
|
13434
|
+
}).map(server => managedPolicy.mcpAllowed(server.name, "url" in server ? server.url : undefined) ? server : { ...server, managedBy: policy.organizationName });
|
|
12449
13435
|
return { servers, ...(policy && !policy.mcp.allowCustom ? { managed: { organizationName: policy.organizationName, allowlist: policy.mcp.allowlist } } : {}) };
|
|
12450
13436
|
}
|
|
12451
13437
|
/** Adding or editing a server the organisation has not approved is refused. */
|
|
@@ -12460,7 +13446,7 @@ function mcpServerBody(body) {
|
|
|
12460
13446
|
if (!body || typeof body !== "object")
|
|
12461
13447
|
return out;
|
|
12462
13448
|
const record = body;
|
|
12463
|
-
for (const key of ["command", "args", "env", "type", "url", "headers", "enabled"]) {
|
|
13449
|
+
for (const key of ["command", "args", "env", "type", "url", "headers", "oauth", "enabled"]) {
|
|
12464
13450
|
if (record[key] !== undefined)
|
|
12465
13451
|
out[key] = record[key];
|
|
12466
13452
|
}
|
|
@@ -12469,7 +13455,7 @@ function mcpServerBody(body) {
|
|
|
12469
13455
|
/** Configured MCP servers that may reach this bot's engine. While enrolled,
|
|
12470
13456
|
* the organisation's allow-list filters them; config.json is never changed. */
|
|
12471
13457
|
function engineMcpServers(bot) {
|
|
12472
|
-
return managedPolicy.filterMcp(customMcpServers(cfg, bot.mcpServers));
|
|
13458
|
+
return withoutPendingSignIn(managedPolicy.filterMcp(customMcpServers(cfg, bot.mcpServers)), mcpOAuth);
|
|
12473
13459
|
}
|
|
12474
13460
|
function persistMcpServers(next) {
|
|
12475
13461
|
saveConfig({ mcpServers: next });
|
|
@@ -12483,7 +13469,11 @@ async function describeInstances() {
|
|
|
12483
13469
|
const configs = providerConfigs();
|
|
12484
13470
|
return (await registry.describe()).map((instance) => {
|
|
12485
13471
|
const entry = configs[instance.instanceId];
|
|
12486
|
-
const described =
|
|
13472
|
+
const described = {
|
|
13473
|
+
...instance,
|
|
13474
|
+
...(entry?.icon ? { icon: entry.icon } : {}),
|
|
13475
|
+
...(entry?.access ? { access: entry.access } : {}),
|
|
13476
|
+
};
|
|
12487
13477
|
if (hostedModels)
|
|
12488
13478
|
return { ...described, readOnly: true,
|
|
12489
13479
|
install: undefined, authentication: undefined, cli: undefined, cliCandidates: [],
|
|
@@ -12495,8 +13485,16 @@ async function describeInstances() {
|
|
|
12495
13485
|
...described, ...policy, readOnly: true, managed: managedDesktop.info(instance.instanceId),
|
|
12496
13486
|
install: undefined, authentication: undefined, cli: undefined, cliCandidates: [],
|
|
12497
13487
|
};
|
|
13488
|
+
// Windows keeps what Antigravity's runtime unpacked whenever it is
|
|
13489
|
+
// stopped by force; its Settings card offers to clear that.
|
|
13490
|
+
if (instance.driverKind === "antigravityAgent" && process.platform === "win32")
|
|
13491
|
+
return { ...described, ...policy, freeUpSpace: true };
|
|
12498
13492
|
if (entry?.driver !== "claudeAgent")
|
|
12499
13493
|
return { ...described, ...policy };
|
|
13494
|
+
// Claude on the workspace API key has no account to sign in to.
|
|
13495
|
+
if (instance.instanceId === CLAUDE_API_INSTANCE) {
|
|
13496
|
+
return { ...described, ...policy, authentication: undefined, install: { ...instance.install, signInCommand: undefined } };
|
|
13497
|
+
}
|
|
12500
13498
|
try {
|
|
12501
13499
|
const claudeAccount = claudeAccountInfo(instance.instanceId, entry, instance.cli ?? instance.cliDefault ?? "claude");
|
|
12502
13500
|
return { ...described, ...policy, claudeAccount, install: { ...instance.install, signInCommand: claudeAccount.signInCommand } };
|
|
@@ -12635,6 +13633,7 @@ async function reloadProviders() {
|
|
|
12635
13633
|
stopScreenPoller(botId, threadId);
|
|
12636
13634
|
releaseLocalVmThread(threadId);
|
|
12637
13635
|
releaseTurnResources(owner);
|
|
13636
|
+
endForeignTurns(threadId);
|
|
12638
13637
|
vpsThreadEnded(botId, threadId);
|
|
12639
13638
|
watchdog.settle(threadId);
|
|
12640
13639
|
closeOpenApprovals(threadId);
|
|
@@ -12654,6 +13653,7 @@ async function reloadProviders() {
|
|
|
12654
13653
|
for (const [threadId, speaker] of rooms) {
|
|
12655
13654
|
releaseLocalVmThread(threadId);
|
|
12656
13655
|
releaseTurnResources(turnResourceOwners.get(threadId));
|
|
13656
|
+
endForeignTurns(threadId);
|
|
12657
13657
|
watchdog.settle(threadId);
|
|
12658
13658
|
closeOpenApprovals(threadId);
|
|
12659
13659
|
if (groupSpeakers.get(threadId) !== speaker)
|
|
@@ -12685,6 +13685,10 @@ let mcpProbesInFlight = 0;
|
|
|
12685
13685
|
// install, and running two self-updates against it would race its files.
|
|
12686
13686
|
const claudeUpdatesInFlight = new Set();
|
|
12687
13687
|
// ── HTTP plumbing ─────────────────────────────────────────────────────
|
|
13688
|
+
/** The web UI's pages: never framed by another page (a hidden frame could
|
|
13689
|
+
* otherwise act on a pair or sign-in page unseen), and they send no Referer.
|
|
13690
|
+
* Nothing frames the web UI: the desktop app shows it in its own window. */
|
|
13691
|
+
const PAGE_HEADERS = { "content-security-policy": "frame-ancestors 'none'", "x-frame-options": "DENY", "referrer-policy": "no-referrer" };
|
|
12688
13692
|
/** The built UI, when this process serves it (OMB_STATIC_DIR: set by the
|
|
12689
13693
|
* desktop app and by the container image). Public by design: it is the same
|
|
12690
13694
|
* bundle anyone can download, holds no secrets, and a remote browser must be
|
|
@@ -12697,7 +13701,8 @@ function serveStatic(res, path) {
|
|
|
12697
13701
|
const file = join(STATIC_DIR, safe);
|
|
12698
13702
|
try {
|
|
12699
13703
|
const data = readFileSync(file);
|
|
12700
|
-
|
|
13704
|
+
const type = MIME[extname(file)] ?? "application/octet-stream";
|
|
13705
|
+
res.writeHead(200, { "content-type": type, ...(type === "text/html" ? PAGE_HEADERS : {}) });
|
|
12701
13706
|
res.end(data);
|
|
12702
13707
|
return true;
|
|
12703
13708
|
}
|
|
@@ -12705,7 +13710,7 @@ function serveStatic(res, path) {
|
|
|
12705
13710
|
// SPA fallback
|
|
12706
13711
|
try {
|
|
12707
13712
|
const data = readFileSync(join(STATIC_DIR, "index.html"));
|
|
12708
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
13713
|
+
res.writeHead(200, { "content-type": "text/html", ...PAGE_HEADERS });
|
|
12709
13714
|
res.end(data);
|
|
12710
13715
|
return true;
|
|
12711
13716
|
}
|
|
@@ -12718,12 +13723,7 @@ function serveStatic(res, path) {
|
|
|
12718
13723
|
// requests from any loopback connection and any web page that DNS-rebinds
|
|
12719
13724
|
// onto it. Reject non-loopback Hosts outright (defeats rebinding) and
|
|
12720
13725
|
// 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 }),
|
|
13726
|
+
const workspaceBackupAccess = {
|
|
12727
13727
|
authorized: (req, original) => {
|
|
12728
13728
|
const current = resolveRequestAuth(req, {
|
|
12729
13729
|
sessions, cookieName: SESSION_COOKIE, streamPath: "/api/events",
|
|
@@ -12759,6 +13759,27 @@ const workspaceBackupRoutes = createWorkspaceBackupRoutes({
|
|
|
12759
13759
|
closeMessageDb();
|
|
12760
13760
|
},
|
|
12761
13761
|
}, keepLocked),
|
|
13762
|
+
status: () => ({ busy: workspaceMaintenance.active, pendingRestore: workspaceMaintenance.pendingRestore }),
|
|
13763
|
+
};
|
|
13764
|
+
const workspaceBackupRoutes = createWorkspaceBackupRoutes({
|
|
13765
|
+
dataDir: DATA_DIR,
|
|
13766
|
+
appVersion: serverVersion(),
|
|
13767
|
+
readBody,
|
|
13768
|
+
restored: workspaceRestore,
|
|
13769
|
+
...workspaceBackupAccess,
|
|
13770
|
+
});
|
|
13771
|
+
// Move to Cloud (server/cloud-move-http.ts): every server sizes its own
|
|
13772
|
+
// workspace for the desktop; only a Cloud home receives one. After its
|
|
13773
|
+
// restore commits, the Cloud home restarts so startup installs it.
|
|
13774
|
+
let cloudHomeRestartRequested = false;
|
|
13775
|
+
const cloudMoveRoutes = createCloudMoveRoutes({
|
|
13776
|
+
dataDir: DATA_DIR,
|
|
13777
|
+
appVersion: serverVersion(),
|
|
13778
|
+
cloudHome: Boolean(CLOUD_HOME),
|
|
13779
|
+
readBody,
|
|
13780
|
+
restored: workspaceRestore,
|
|
13781
|
+
...workspaceBackupAccess,
|
|
13782
|
+
restart: () => { cloudHomeRestartRequested = true; gracefulShutdown(); },
|
|
12762
13783
|
});
|
|
12763
13784
|
// Route modules (server/routes/README.md). `workspaceAccess` is assigned at
|
|
12764
13785
|
// boot, after this line, so the dependency reads it per request.
|
|
@@ -12773,6 +13794,23 @@ ROUTES.push(createBotMemoryRoutes({
|
|
|
12773
13794
|
bot: (id) => store.bot(id),
|
|
12774
13795
|
taskByThread: (botId, threadId) => store.taskByThread(botId, threadId),
|
|
12775
13796
|
upkeep: memoryUpkeep,
|
|
13797
|
+
...(lendingMemory ? {
|
|
13798
|
+
lendingReview: {
|
|
13799
|
+
needed: (botId) => {
|
|
13800
|
+
if (!lendingMemory.reconcile(botId, foreignTurnRunning(botId), { cached: true }).changedBySomeoneElse)
|
|
13801
|
+
return null;
|
|
13802
|
+
const { token, changed } = lendingMemory.reviewInfo(botId);
|
|
13803
|
+
return { token, changed };
|
|
13804
|
+
},
|
|
13805
|
+
accept: (botId, token) => lendingMemory.review(botId, foreignTurnRunning(botId), token).ok,
|
|
13806
|
+
// Like Move to Cloud: one of the owner's own devices, never loopback
|
|
13807
|
+
// (on a Cloud home that is a bot's shell, not the owner).
|
|
13808
|
+
owner: cloudOwnerSession,
|
|
13809
|
+
},
|
|
13810
|
+
// The owner's own Memory edits are theirs even while someone else's
|
|
13811
|
+
// conversation runs; anyone else's are judged like a bot's own write.
|
|
13812
|
+
ownersWrite: (auth, botId, write) => cloudOwnerSession(auth) ? lendingMemory.trustedWrite(botId, write) : write(),
|
|
13813
|
+
} : {}),
|
|
12776
13814
|
aboutMe: () => cfg.profile?.aboutMe ?? "",
|
|
12777
13815
|
saveAboutMe: (aboutMe) => {
|
|
12778
13816
|
saveConfig({ profile: { ...cfg.profile, aboutMe } });
|
|
@@ -12780,6 +13818,14 @@ ROUTES.push(createBotMemoryRoutes({
|
|
|
12780
13818
|
broadcast({ kind: "config", ...configStatus() });
|
|
12781
13819
|
},
|
|
12782
13820
|
}));
|
|
13821
|
+
ROUTES.push(createDeciderRoutes({ decider }));
|
|
13822
|
+
ROUTES.push(createAntigravityLeftoverRoutes({
|
|
13823
|
+
hosted: Boolean(hostedModels),
|
|
13824
|
+
isAntigravity: (instanceId) => registry.get(instanceId)?.driverKind === "antigravityAgent",
|
|
13825
|
+
find: () => findAntigravityLeftovers(),
|
|
13826
|
+
remove: () => removeAntigravityLeftovers(),
|
|
13827
|
+
}));
|
|
13828
|
+
ROUTES.push(desktopViewer.route);
|
|
12783
13829
|
const toolResults = new ToolResults();
|
|
12784
13830
|
const handleRequest = async (req, res) => {
|
|
12785
13831
|
let url;
|
|
@@ -12836,7 +13882,7 @@ const handleRequest = async (req, res) => {
|
|
|
12836
13882
|
if (method === "GET" && !path.startsWith("/api/") && !path.startsWith("/.well-known/") && serveStatic(res, path))
|
|
12837
13883
|
return;
|
|
12838
13884
|
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:
|
|
13885
|
+
return json(res, 200, environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: !HOSTED_WORKSPACE && !CLOUD_HOME && emailSignIn.enabled(), sharedComputers: lendingEnabled() }));
|
|
12840
13886
|
}
|
|
12841
13887
|
const domainCheck = /^\/\.well-known\/openmausbot\/domain-check\/([a-f0-9]{64})$/.exec(path);
|
|
12842
13888
|
if (method === "GET" && domainCheck) {
|
|
@@ -12846,7 +13892,11 @@ const handleRequest = async (req, res) => {
|
|
|
12846
13892
|
}
|
|
12847
13893
|
// Sign in with an emailed code (server/account-signin.ts). Public like
|
|
12848
13894
|
// /api/auth/pair, JSON-only for the same reason, and counted against the
|
|
12849
|
-
// same per-source lockout so a code cannot be guessed.
|
|
13895
|
+
// same per-source lockout so a code cannot be guessed. Never on a Cloud
|
|
13896
|
+
// home: it is personal, and the owner's devices sign in through the Admin.
|
|
13897
|
+
if (CLOUD_HOME && method === "POST" && (path === "/api/auth/email/start" || path === "/api/auth/email/verify")) {
|
|
13898
|
+
return json(res, 403, { error: CLOUD_PERSONAL_REFUSAL, code: "cloud_personal" });
|
|
13899
|
+
}
|
|
12850
13900
|
if (method === "POST" && (path === "/api/auth/email/start" || path === "/api/auth/email/verify")) {
|
|
12851
13901
|
if (!/^application\/json\b/i.test(String(req.headers["content-type"] ?? ""))) {
|
|
12852
13902
|
return json(res, 415, { error: "send the sign-in request as JSON (content-type: application/json)" });
|
|
@@ -12879,7 +13929,7 @@ const handleRequest = async (req, res) => {
|
|
|
12879
13929
|
}
|
|
12880
13930
|
sessions.clearFailures(source);
|
|
12881
13931
|
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:
|
|
13932
|
+
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: true, sharedComputers: lendingEnabled() });
|
|
12883
13933
|
const secure = requestOrigin(req)?.startsWith("https://") === true;
|
|
12884
13934
|
res.setHeader("set-cookie", serializeSessionCookie(SESSION_COOKIE, issued.token, { secure, maxAgeSeconds: cookieMaxAgeSeconds(issued.session) }));
|
|
12885
13935
|
return json(res, 200, { session: issued.session, environment });
|
|
@@ -12916,13 +13966,27 @@ const handleRequest = async (req, res) => {
|
|
|
12916
13966
|
const wantsCookie = body?.cookie === true;
|
|
12917
13967
|
const label = typeof body?.label === "string" ? body.label : "";
|
|
12918
13968
|
const attemptId = typeof body?.attemptId === "string" ? body.attemptId : undefined;
|
|
12919
|
-
|
|
13969
|
+
// A browser sign-in (the OMB Cloud page's "Use in your browser", docs/cloud-pro.md)
|
|
13970
|
+
// redeems only a window opened for one, and only into this browser's cookie.
|
|
13971
|
+
const browser = body?.browser === true;
|
|
13972
|
+
if (browser && body?.preview === true) {
|
|
13973
|
+
// Whose Cloud it signs in to, shown before the person continues. Redeems nothing.
|
|
13974
|
+
const preview = sessions.previewBrowserSignIn(code);
|
|
13975
|
+
return preview ? json(res, 200, preview) : json(res, 401, { error: "this sign-in link has expired or was already used" });
|
|
13976
|
+
}
|
|
13977
|
+
if (browser && !wantsCookie)
|
|
13978
|
+
return json(res, 400, { error: "a browser sign-in sets this browser's cookie; send cookie: true" });
|
|
13979
|
+
const result = sessions.exchange({ code, label, attemptId, browser, source: requestSource(req), fallbackLabel: labelFromUserAgent(req.headers["user-agent"]) });
|
|
12920
13980
|
if (!result.ok) {
|
|
12921
13981
|
console.warn(`pairing refused from ${requestSource(req)}: ${result.error}`);
|
|
12922
13982
|
return json(res, result.status, { error: result.error });
|
|
12923
13983
|
}
|
|
12924
|
-
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: emailSignIn.enabled(), sharedComputers:
|
|
13984
|
+
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: !CLOUD_HOME && emailSignIn.enabled(), sharedComputers: lendingEnabled() });
|
|
12925
13985
|
if (wantsCookie) {
|
|
13986
|
+
// A browser sign-in replaces this browser's own session here, if it had one, rather than leaving it behind.
|
|
13987
|
+
const previous = browser ? sessions.authenticate(parseCookies(req.headers.cookie).get(SESSION_COOKIE)) : null;
|
|
13988
|
+
if (previous && previous.id !== result.session.id)
|
|
13989
|
+
sessions.revoke(previous.id);
|
|
12926
13990
|
const secure = requestOrigin(req)?.startsWith("https://") === true;
|
|
12927
13991
|
res.setHeader("set-cookie", serializeSessionCookie(SESSION_COOKIE, result.token, { secure, maxAgeSeconds: cookieMaxAgeSeconds(result.session) }));
|
|
12928
13992
|
return json(res, 200, { session: result.session, environment });
|
|
@@ -12973,7 +14037,7 @@ const handleRequest = async (req, res) => {
|
|
|
12973
14037
|
url,
|
|
12974
14038
|
loopbackMutationToken: desktopMutationToken,
|
|
12975
14039
|
companionMutationToken,
|
|
12976
|
-
features: { sharedComputers:
|
|
14040
|
+
features: { sharedComputers: lendingEnabled() },
|
|
12977
14041
|
loopbackTrust: LOOPBACK.trust,
|
|
12978
14042
|
cliOwnerToken,
|
|
12979
14043
|
});
|
|
@@ -13031,6 +14095,8 @@ const handleRequest = async (req, res) => {
|
|
|
13031
14095
|
}
|
|
13032
14096
|
if (await workspaceBackupRoutes(req, res, path, auth))
|
|
13033
14097
|
return;
|
|
14098
|
+
if (await cloudMoveRoutes(req, res, path, auth))
|
|
14099
|
+
return;
|
|
13034
14100
|
// Count ordinary requests until their asynchronous handler returns, not
|
|
13035
14101
|
// merely until the browser disconnects. A cancelled upload can still write.
|
|
13036
14102
|
if (path.startsWith("/api/") && path !== "/api/events" && path !== "/api/health" && !path.startsWith("/api/shared-computers/") && !isWorkspaceBackupSessionControl(method, path)) {
|
|
@@ -13054,12 +14120,18 @@ const handleRequest = async (req, res) => {
|
|
|
13054
14120
|
environmentId: ENVIRONMENT_ID,
|
|
13055
14121
|
// the account behind the session, when it came from a sign-in
|
|
13056
14122
|
...(auth.session.email ? { email: auth.session.email } : {}),
|
|
14123
|
+
// whose OMB Cloud a browser sign-in signed in to
|
|
14124
|
+
...(auth.session.owner ? { owner: auth.session.owner } : {}),
|
|
13057
14125
|
// a hosted team workspace: the web UI's first run skips the
|
|
13058
14126
|
// desktop-only beats there. Absent everywhere else.
|
|
13059
14127
|
...(HOSTED_WORKSPACE ? { hosted: true } : {}),
|
|
13060
14128
|
// an OMB Cloud home: the web UI's first run is its engine
|
|
13061
14129
|
// sign-in (docs/cloud-pro.md). Absent everywhere else.
|
|
13062
14130
|
...(CLOUD_HOME ? { cloudHome: true } : {}),
|
|
14131
|
+
// a device that is not one of the owner's own on a Cloud home
|
|
14132
|
+
// writes only in conversations it opened: the composer offers
|
|
14133
|
+
// a new conversation everywhere else.
|
|
14134
|
+
...(CLOUD_HOME && !cloudOwnerSession(auth) ? { cloudGuest: true, openedThreads: threadStarters.threadsOf(actorKey(auth), 2_000) } : {}),
|
|
13063
14135
|
});
|
|
13064
14136
|
}
|
|
13065
14137
|
if (method === "POST" && path === "/api/auth/stream-ticket") {
|
|
@@ -13083,7 +14155,16 @@ const handleRequest = async (req, res) => {
|
|
|
13083
14155
|
}
|
|
13084
14156
|
const requested = body?.scopes;
|
|
13085
14157
|
const scopes = Array.isArray(requested) ? requested.filter((v) => v === "admin" || v === "client") : undefined;
|
|
13086
|
-
|
|
14158
|
+
let opened;
|
|
14159
|
+
try {
|
|
14160
|
+
opened = sessions.openPairing({ label: typeof body?.label === "string" ? body.label : undefined, scopes });
|
|
14161
|
+
}
|
|
14162
|
+
catch (error) {
|
|
14163
|
+
// A Cloud home is personal: a device paired to it has admin scope (sessions.requireAdmin).
|
|
14164
|
+
if (error.code === "cloud_personal")
|
|
14165
|
+
return json(res, 403, { error: error.message, code: "cloud_personal" });
|
|
14166
|
+
throw error;
|
|
14167
|
+
}
|
|
13087
14168
|
const origin = requestOrigin(req);
|
|
13088
14169
|
const base = publicUrl() ?? (auth.kind === "session" && origin ? origin : null);
|
|
13089
14170
|
const code = formatPairingCode(opened.code);
|
|
@@ -13150,11 +14231,15 @@ const handleRequest = async (req, res) => {
|
|
|
13150
14231
|
const cancelled = sessions.cancelPairing(m[1]);
|
|
13151
14232
|
return json(res, cancelled ? 200 : 404, cancelled ? { ok: true } : { error: "no such pairing code" });
|
|
13152
14233
|
}
|
|
13153
|
-
|
|
14234
|
+
// The desktop's own server always answers its desktop here: lending to the
|
|
14235
|
+
// person's Cloud (docs/cloud-pro.md) needs the lease without the
|
|
14236
|
+
// maintainer flag. It grants nothing by itself; it only keeps local bot
|
|
14237
|
+
// turns and a remote screen action from using the screen at once.
|
|
14238
|
+
if (method === "POST" && path === "/api/desktop/shared-computer-control" && (sharedComputersEnabled(cfg) || DESKTOP_MANAGED)) {
|
|
13154
14239
|
if (auth.kind !== "loopback")
|
|
13155
14240
|
return json(res, 403, { error: "Local desktop only" });
|
|
13156
14241
|
const body = await readBody(req, 1024);
|
|
13157
|
-
if (!sharedComputersEnabled(cfg))
|
|
14242
|
+
if (!sharedComputersEnabled(cfg) && !DESKTOP_MANAGED)
|
|
13158
14243
|
return json(res, 404, { error: `no route: ${method} ${path}` });
|
|
13159
14244
|
if (!z.string().uuid().safeParse(body?.id).success || !["acquire", "release", "renew"].includes(body?.action))
|
|
13160
14245
|
return json(res, 400, { error: "Invalid computer lease" });
|
|
@@ -13177,13 +14262,19 @@ const handleRequest = async (req, res) => {
|
|
|
13177
14262
|
// With features.sharedComputers off the whole family falls through to the
|
|
13178
14263
|
// generic "no route" 404, so a probe cannot tell a disabled feature from
|
|
13179
14264
|
// a build that never had one.
|
|
13180
|
-
|
|
14265
|
+
// What the asking person has lent to this server, online or not, with its
|
|
14266
|
+
// scopes (docs/cloud-pro.md, "What the Cloud can see"). Owner-scoped like
|
|
14267
|
+
// the bot routes; no id a session could not already use, and no secret.
|
|
14268
|
+
if (method === "GET" && path === "/api/shared-computers" && lendingEnabled()) {
|
|
14269
|
+
return json(res, 200, { computers: sharedComputers.status(sessionLendingPrincipal(auth)) });
|
|
14270
|
+
}
|
|
14271
|
+
if (method === "POST" && path.startsWith("/api/shared-computers/") && lendingEnabled()) {
|
|
13181
14272
|
if (auth.kind !== "session")
|
|
13182
14273
|
return json(res, 403, { error: "Pair this desktop first" });
|
|
13183
14274
|
if (!/^application\/json\b/i.test(String(req.headers["content-type"] ?? "")))
|
|
13184
14275
|
return json(res, 415, { error: "JSON required" });
|
|
13185
14276
|
const body = await readBody(req, 4_000_000);
|
|
13186
|
-
if (!
|
|
14277
|
+
if (!lendingEnabled())
|
|
13187
14278
|
return json(res, 404, { error: `no route: ${method} ${path}` });
|
|
13188
14279
|
if (!sessions.isLive(auth.session.id))
|
|
13189
14280
|
return json(res, 401, { error: "Session ended" });
|
|
@@ -13195,7 +14286,11 @@ const handleRequest = async (req, res) => {
|
|
|
13195
14286
|
const registration = parsed.data;
|
|
13196
14287
|
if (registration.environmentId !== ENVIRONMENT_ID)
|
|
13197
14288
|
return json(res, 409, { error: "Workspace identity changed. Pair again before sharing this computer." });
|
|
13198
|
-
|
|
14289
|
+
// A Cloud home is one person's: only their own devices, which the
|
|
14290
|
+
// Admin's pairing signs in with admin scope, may lend to it.
|
|
14291
|
+
if (CLOUD_HOME && !auth.scopes.includes("admin"))
|
|
14292
|
+
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." });
|
|
14293
|
+
sharedComputers.register(registration, { session: auth.session.id, person: CLOUD_HOME ? CLOUD_HOME_LENDER : personKey(auth.session) }, secret);
|
|
13199
14294
|
return json(res, 200, { ok: true });
|
|
13200
14295
|
}
|
|
13201
14296
|
const route = /^\/api\/shared-computers\/([\w-]+)\/(poll|lease|result|disconnect)$/.exec(path);
|
|
@@ -13364,6 +14459,7 @@ const handleRequest = async (req, res) => {
|
|
|
13364
14459
|
if (method === "POST" && requested !== "auto" && !parseSurface(requested)) {
|
|
13365
14460
|
return json(res, 400, { error: "surface must be auto, cloud, vm, local, or browser" });
|
|
13366
14461
|
}
|
|
14462
|
+
const unoffered = CLOUD_HOME && method === "POST" && requested !== "auto" ? cloudHomePlaceRefusal(parseSurface(requested)) : undefined;
|
|
13367
14463
|
const options = await selectableComputers(bot);
|
|
13368
14464
|
const current = source ? source.mounted ?? "off" : await computerPreviewSurface(bot, bot.threadId);
|
|
13369
14465
|
requireActiveInternalCapability();
|
|
@@ -13371,6 +14467,8 @@ const handleRequest = async (req, res) => {
|
|
|
13371
14467
|
return json(res, 200, { current, canSelect, options });
|
|
13372
14468
|
if (computerSelectionTurns.get(internalCapability.threadId) !== source)
|
|
13373
14469
|
return json(res, 409, { error: "The user request ended before its computer was selected." });
|
|
14470
|
+
if (unoffered)
|
|
14471
|
+
return json(res, 409, { error: unoffered, options });
|
|
13374
14472
|
const option = requested === "auto"
|
|
13375
14473
|
? 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
14474
|
: options.find(option => option.surface === requested);
|
|
@@ -13429,19 +14527,31 @@ const handleRequest = async (req, res) => {
|
|
|
13429
14527
|
// Notes written from a room fewer people can see than this bot would
|
|
13430
14528
|
// carry that room's words to everyone who can see the bot.
|
|
13431
14529
|
if ((path === "/api/internal/memory" || path === "/api/internal/memory/log") && method === "POST") {
|
|
14530
|
+
// On a Cloud home a bot's memory loads into its owner's turns, and
|
|
14531
|
+
// through them can reach a lent Mac: only a conversation that is
|
|
14532
|
+
// provably the owner's may write it.
|
|
14533
|
+
if (CLOUD_HOME && cloudHomeLendingRefusal(cloudLendingTurn(internalCapability)) !== null) {
|
|
14534
|
+
return json(res, 403, { error: "Notes can only be saved from a conversation that only the owner of this Cloud has written in." });
|
|
14535
|
+
}
|
|
14536
|
+
if (internalSender.memoryEnabled === false) {
|
|
14537
|
+
return json(res, 403, { error: "Memory is disabled for this bot." });
|
|
14538
|
+
}
|
|
13432
14539
|
const room = store.groupByThread(internalCapability.threadId);
|
|
13433
14540
|
if (room && !roomFeedsBot(room, internalSender)) {
|
|
13434
14541
|
return json(res, 403, { error: "This room is visible to fewer people than you are, so notes from it can't go into your memory. Keep them in the room." });
|
|
13435
14542
|
}
|
|
13436
14543
|
}
|
|
14544
|
+
// On a Cloud home only the owner's conversations get here (above): the
|
|
14545
|
+
// write is theirs (server/lending-memory.ts).
|
|
14546
|
+
const ownersWrite = (write) => lendingMemory ? lendingMemory.trustedWrite(internalSender.id, write) : write();
|
|
13437
14547
|
if (method === "POST" && path === "/api/internal/memory") {
|
|
13438
14548
|
const body = await readInternalBody();
|
|
13439
|
-
const result = updateMemory(internalSender.id, { action: body.action, text: body.text, oldText: body.oldText, ...(body.until !== undefined ? { until: body.until } : {}) }, { source: memorySource() });
|
|
14549
|
+
const result = ownersWrite(() => updateMemory(internalSender.id, { action: body.action, text: body.text, oldText: body.oldText, ...(body.until !== undefined ? { until: body.until } : {}) }, { source: memorySource() }));
|
|
13440
14550
|
return json(res, result.ok ? 200 : result.code === "conflict" ? 409 : result.code === "over-budget" ? 413 : 400, result);
|
|
13441
14551
|
}
|
|
13442
14552
|
if (method === "POST" && path === "/api/internal/memory/log") {
|
|
13443
14553
|
const body = await readInternalBody();
|
|
13444
|
-
const result = appendMemoryLog(internalSender.id, body.text, { source: memorySource() });
|
|
14554
|
+
const result = ownersWrite(() => appendMemoryLog(internalSender.id, body.text, { source: memorySource() }));
|
|
13445
14555
|
return json(res, result.ok ? 200 : 400, result);
|
|
13446
14556
|
}
|
|
13447
14557
|
if (method === "POST" && path === "/api/internal/browser/mcp") {
|
|
@@ -13484,15 +14594,31 @@ const handleRequest = async (req, res) => {
|
|
|
13484
14594
|
}
|
|
13485
14595
|
// Off by default: both fall through to the same "unknown internal
|
|
13486
14596
|
// endpoint" 404 a never-implemented route returns.
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
if (method === "
|
|
14597
|
+
// Owner-scoped: a turn sees and uses only the computers of the person it
|
|
14598
|
+
// provably acts for (sharedComputerPrincipal), never anyone else's.
|
|
14599
|
+
if (method === "GET" && path === "/api/internal/shared-computers" && lendingEnabled()) {
|
|
14600
|
+
const principal = sharedComputerPrincipal(internalCapability);
|
|
14601
|
+
// Say why an owner's own conversation sees no Mac, so the bot can tell them.
|
|
14602
|
+
const why = CLOUD_HOME && !principal && sharedComputers.status(CLOUD_HOME_LENDER).length > 0 ? cloudLendingRefusal(internalCapability) : null;
|
|
14603
|
+
const notice = why === "someone-else" ? { unavailable: LENDING_SOMEONE_ELSE } : why === "memory-changed" ? { unavailable: LENDING_MEMORY_CHANGED } : {};
|
|
14604
|
+
return json(res, 200, { computers: sharedComputers.list(principal), ...notice });
|
|
14605
|
+
}
|
|
14606
|
+
if (method === "POST" && path === "/api/internal/shared-computers" && lendingEnabled()) {
|
|
13490
14607
|
const parsed = sharedComputerOperation.safeParse(await readInternalBody());
|
|
13491
|
-
if (!
|
|
14608
|
+
if (!lendingEnabled())
|
|
13492
14609
|
return json(res, 404, { error: "unknown internal endpoint" });
|
|
13493
14610
|
if (!parsed.success)
|
|
13494
14611
|
return json(res, 400, { error: "Invalid shared computer operation" });
|
|
13495
|
-
|
|
14612
|
+
// The bot's own "no computer" setting holds on a lent screen too.
|
|
14613
|
+
if (["computer_tools", "computer_call"].includes(parsed.data.action) && store.bot(internalCapability.botId)?.computer === "off") {
|
|
14614
|
+
return json(res, 403, { error: "This bot has no computer. Change its Computer setting to use a shared computer's apps and screen." });
|
|
14615
|
+
}
|
|
14616
|
+
const principal = sharedComputerPrincipal(internalCapability);
|
|
14617
|
+
const why = CLOUD_HOME && !principal ? cloudLendingRefusal(internalCapability) : null;
|
|
14618
|
+
if (why === "someone-else" || why === "memory-changed") {
|
|
14619
|
+
return json(res, 403, { error: why === "someone-else" ? LENDING_SOMEONE_ELSE : LENDING_MEMORY_CHANGED });
|
|
14620
|
+
}
|
|
14621
|
+
return json(res, 200, { result: await sharedComputers.request(parsed.data, principal, () => lendingEnabled() && internalCapabilityIsActive(internalCapability)) });
|
|
13496
14622
|
}
|
|
13497
14623
|
if (method === "GET" && path === "/api/internal/agents") {
|
|
13498
14624
|
const sender = internalSender;
|
|
@@ -13549,7 +14675,13 @@ const handleRequest = async (req, res) => {
|
|
|
13549
14675
|
return "closed";
|
|
13550
14676
|
return "idle";
|
|
13551
14677
|
};
|
|
14678
|
+
// On a Cloud home a turn that may use the lent Mac sees only the
|
|
14679
|
+
// conversations the owner alone opened and wrote in: a title is words
|
|
14680
|
+
// too (cloudLendingView).
|
|
14681
|
+
const shown = cloudLendingView(internalCapability) ? cloudOwnerOnlyThread : () => true;
|
|
13552
14682
|
for (const task of store.tasks(from.id)) {
|
|
14683
|
+
if (!shown(task.threadId))
|
|
14684
|
+
continue;
|
|
13553
14685
|
rows.push({
|
|
13554
14686
|
threadId: task.threadId, botId: from.id, botName: from.name, title: task.title,
|
|
13555
14687
|
state: stateOf(from, task), unread: task.unread === true, openedAt: task.openedBy?.at ?? task.createdAt,
|
|
@@ -13558,7 +14690,7 @@ const handleRequest = async (req, res) => {
|
|
|
13558
14690
|
}
|
|
13559
14691
|
for (const peer of reachablePeers(store.bots, from)) {
|
|
13560
14692
|
for (const task of store.tasks(peer.id)) {
|
|
13561
|
-
if (task.openedBy?.botId !== from.id)
|
|
14693
|
+
if (task.openedBy?.botId !== from.id || !shown(task.threadId))
|
|
13562
14694
|
continue;
|
|
13563
14695
|
rows.push({
|
|
13564
14696
|
threadId: task.threadId, botId: peer.id, botName: peer.name, title: task.title,
|
|
@@ -13791,6 +14923,12 @@ const handleRequest = async (req, res) => {
|
|
|
13791
14923
|
: body.action === "update"
|
|
13792
14924
|
? { action: body.action, routineId: body.routineId, changes: body.changes, forBot }
|
|
13793
14925
|
: { action: body.action, routineId: body.routineId, forBot };
|
|
14926
|
+
// Whether the routine a change is for is the owner's, and whether this
|
|
14927
|
+
// turn acts for the owner (their own conversation, or a run of a
|
|
14928
|
+
// routine that is theirs), both before the change.
|
|
14929
|
+
const wasOwners = proposedInput.action !== "create" && typeof proposedInput.routineId === "string" ? cloudRoutineIsOwners(proposedInput.routineId) : false;
|
|
14930
|
+
const run = activeRoutineRunForThread(fromThreadId);
|
|
14931
|
+
const ownersTurn = cloudOwnerOnlyThread(fromThreadId) || cloudRoutineRunIsOwners(run, from.id, fromThreadId);
|
|
13794
14932
|
const proposed = await routineRequests.submit({
|
|
13795
14933
|
botId: from.id,
|
|
13796
14934
|
threadId: fromThreadId,
|
|
@@ -13799,6 +14937,17 @@ const handleRequest = async (req, res) => {
|
|
|
13799
14937
|
canCommit: () => internalCapabilityIsActive(internalCapability),
|
|
13800
14938
|
});
|
|
13801
14939
|
const proposedCard = store.messagesFor(fromThreadId).find((message) => message.id === proposed.messageId)?.card;
|
|
14940
|
+
// Applied at once in a Full-access conversation. In the owner's own (one
|
|
14941
|
+
// only they provably wrote in) it is theirs, as if they had approved
|
|
14942
|
+
// the card. From anywhere else, a routine it created or changed is no
|
|
14943
|
+
// longer the owner's, like anyone else's edit (the routine PATCH).
|
|
14944
|
+
const applied = CLOUD_HOME && proposed.state === "applied" && "result" in proposed ? proposed.result : undefined;
|
|
14945
|
+
if (applied?.resultId && ownersTurn)
|
|
14946
|
+
cloudOwnerApplied(applied.action, applied.resultId, wasOwners);
|
|
14947
|
+
else if (applied?.resultId && (applied.action === "create" || applied.action === "update")) {
|
|
14948
|
+
cloudRoutineAuthors?.forget(applied.resultId);
|
|
14949
|
+
cloudRoutineAuthors?.wrote(applied.resultId, CLOUD_NOBODY_KEY);
|
|
14950
|
+
}
|
|
13802
14951
|
appendDecision(DATA_DIR, {
|
|
13803
14952
|
threadId: fromThreadId,
|
|
13804
14953
|
requestId: proposed.requestId,
|
|
@@ -14017,7 +15166,7 @@ const handleRequest = async (req, res) => {
|
|
|
14017
15166
|
if (scope !== "all" && scope !== "conversations" && scope !== "memory") {
|
|
14018
15167
|
return json(res, 400, { error: "scope must be all, conversations, or memory" });
|
|
14019
15168
|
}
|
|
14020
|
-
const memoryHits = scope === "conversations" || !q ? [] : searchMemoryFiles(from.id, q, limit);
|
|
15169
|
+
const memoryHits = scope === "conversations" || !q || from.memoryEnabled === false ? [] : searchMemoryFiles(from.id, q, limit);
|
|
14021
15170
|
if (scope === "memory")
|
|
14022
15171
|
return json(res, 200, { hits: [], memoryHits });
|
|
14023
15172
|
// Own threads: the bot's main chat and tasks, and the rooms it is a
|
|
@@ -14033,7 +15182,10 @@ const handleRequest = async (req, res) => {
|
|
|
14033
15182
|
for (const task of group.tasks ?? [])
|
|
14034
15183
|
roomByThread.set(task.threadId, group);
|
|
14035
15184
|
}
|
|
14036
|
-
const ownThreads = [...new Set([from.threadId, ...(from.tasks ?? []).map((task) => task.threadId), ...roomByThread.keys()])]
|
|
15185
|
+
const ownThreads = [...new Set([from.threadId, ...(from.tasks ?? []).map((task) => task.threadId), ...roomByThread.keys()])]
|
|
15186
|
+
// On a Cloud home a turn that may use the lent Mac searches only the
|
|
15187
|
+
// conversations the owner alone opened and wrote in.
|
|
15188
|
+
.filter(cloudLendingView(internalCapability) ? cloudOwnerOnlyThread : () => true);
|
|
14037
15189
|
// A room is the only place a recall can be a disclosure, and only a
|
|
14038
15190
|
// private chat is one: in a 1:1 the user already owns every thread
|
|
14039
15191
|
// the bot can reach, and a room's lines were said in the open.
|
|
@@ -14072,7 +15224,8 @@ const handleRequest = async (req, res) => {
|
|
|
14072
15224
|
const messageId = String(url.searchParams.get("messageId") ?? "").trim();
|
|
14073
15225
|
if (!threadId || !messageId)
|
|
14074
15226
|
return json(res, 400, { error: "threadId and messageId are required" });
|
|
14075
|
-
const own = threadId === from.threadId || Boolean(store.taskByThread(from.id, threadId))
|
|
15227
|
+
const own = (threadId === from.threadId || Boolean(store.taskByThread(from.id, threadId))) &&
|
|
15228
|
+
(!cloudLendingView(internalCapability) || cloudOwnerOnlyThread(threadId));
|
|
14076
15229
|
const message = own ? readMessageText(threadId, messageId) : null;
|
|
14077
15230
|
if (!message)
|
|
14078
15231
|
return json(res, 404, { error: "no such message in your conversations" });
|
|
@@ -14242,7 +15395,12 @@ const handleRequest = async (req, res) => {
|
|
|
14242
15395
|
// can check next turn. If the ledger refuses (cap/depth), fall back
|
|
14243
15396
|
// to the plain busy bounce rather than dropping the refusal reason.
|
|
14244
15397
|
const queueBusyFallback = (approvalAlreadyGranted = false) => {
|
|
14245
|
-
const
|
|
15398
|
+
const guestThread = guestWorkThread(from, target, fromThreadId, message);
|
|
15399
|
+
if (guestThread === null)
|
|
15400
|
+
return json(res, 500, { error: "couldn't open a conversation for that work" });
|
|
15401
|
+
const queued = queueDelegation(commsBus, from, { toBotId, message, reason: "asked while busy", depth, approvalAlreadyGranted, ...(guestThread ? { targetThreadId: guestThread } : {}) }, MAX_COMMS_DEPTH, fromThreadId);
|
|
15402
|
+
if ((queued.result !== "ok" || !queued.id) && guestThread)
|
|
15403
|
+
store.deleteTask(toBotId, guestThread);
|
|
14246
15404
|
if (queued.result !== "ok" || !queued.id)
|
|
14247
15405
|
return json(res, 200, { busy: true, receipt: peerDeliveryReceipt({
|
|
14248
15406
|
botId: toBotId, botName: target.name, outcome: "failed",
|
|
@@ -14270,6 +15428,10 @@ const handleRequest = async (req, res) => {
|
|
|
14270
15428
|
const tryAsideDelivery = async () => {
|
|
14271
15429
|
if (peerReviewRequired(from, fromThreadId))
|
|
14272
15430
|
return null;
|
|
15431
|
+
// A guest's request never folds into a turn running in the
|
|
15432
|
+
// owner's conversation; it waits for a confined one of its own.
|
|
15433
|
+
if (cloudGuestDriven(fromThreadId))
|
|
15434
|
+
return null;
|
|
14273
15435
|
// Busy elsewhere (room turn, sibling thread): the thread this ask
|
|
14274
15436
|
// targets is not the running one, so there is nothing to fold
|
|
14275
15437
|
// into here — the queue lane keeps it.
|
|
@@ -14376,7 +15538,10 @@ const handleRequest = async (req, res) => {
|
|
|
14376
15538
|
delivery: "ask_bot",
|
|
14377
15539
|
unattended: isUnattended(currentFrom.id, fromThreadId),
|
|
14378
15540
|
});
|
|
14379
|
-
const
|
|
15541
|
+
const guestThread = guestWorkThread(currentFrom, currentTarget, fromThreadId, message);
|
|
15542
|
+
if (guestThread === null)
|
|
15543
|
+
return json(res, 500, { error: "couldn't open a conversation for that work" });
|
|
15544
|
+
const targetThreadId = guestThread ?? currentTarget.threadId;
|
|
14380
15545
|
const outcome = await askBotAndWait(toBotId, prefixed, depth, fromBotId, fromThreadId, targetThreadId);
|
|
14381
15546
|
requireActiveInternalCapability();
|
|
14382
15547
|
const replySender = store.bot(fromBotId);
|
|
@@ -14588,7 +15753,12 @@ const handleRequest = async (req, res) => {
|
|
|
14588
15753
|
if (!connectorThread(from.id, fromThreadId)) {
|
|
14589
15754
|
return json(res, 403, { error: "source thread does not belong to sender" });
|
|
14590
15755
|
}
|
|
14591
|
-
const
|
|
15756
|
+
const guestThread = guestWorkThread(from, target, fromThreadId, reason ?? message);
|
|
15757
|
+
if (guestThread === null)
|
|
15758
|
+
return json(res, 500, { error: "couldn't open a conversation for that work" });
|
|
15759
|
+
const queued = queueDelegation(commsBus, from, { toBotId, message, reason, depth, ...(guestThread ? { targetThreadId: guestThread } : {}) }, MAX_COMMS_DEPTH, fromThreadId);
|
|
15760
|
+
if ((queued.result !== "ok" || !queued.id) && guestThread)
|
|
15761
|
+
store.deleteTask(toBotId, guestThread);
|
|
14592
15762
|
if (queued.result !== "ok" || !queued.id) {
|
|
14593
15763
|
// the agent reads this string — a bare enum ("too_deep") tells it
|
|
14594
15764
|
// nothing about what to do instead
|
|
@@ -14656,7 +15826,7 @@ const handleRequest = async (req, res) => {
|
|
|
14656
15826
|
})).filter(g => g.members.length);
|
|
14657
15827
|
return json(res, 200, { currentRoom: source ? { id: source.id, name: source.name, workingFolder: source.cwd || null } : null,
|
|
14658
15828
|
bots: reachablePeers(store.bots, internalSender).map(bot => ({ id: bot.id, name: bot.name, title: bot.title, section: bot.section, busy: bot.busy })),
|
|
14659
|
-
rooms, note: "Without group_id: use this room when in a room, otherwise
|
|
15829
|
+
rooms, note: "Without group_id: use this room when in a room, otherwise each distinct assignment starts a fresh thread for that teammate. Give a self-contained brief. Each bot uses its own environment and permissions. Files are not transferred: pass absolute paths only when accessible to the recipient, otherwise pass the content." });
|
|
14660
15830
|
}
|
|
14661
15831
|
if (method === "POST" && path === "/api/internal/coordinate-bots") {
|
|
14662
15832
|
const parsed = z.object({
|
|
@@ -14733,39 +15903,21 @@ const handleRequest = async (req, res) => {
|
|
|
14733
15903
|
try {
|
|
14734
15904
|
requireActiveInternalCapability();
|
|
14735
15905
|
if (!destination) {
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
// request key, or the thread the person has selected there.
|
|
14739
|
-
const resolved = store.resolvePairConversation(internalSender, target.botId, {
|
|
14740
|
-
label: parsed.data.label,
|
|
14741
|
-
// "Still working" exactly as close_thread reads it: a
|
|
14742
|
-
// running turn, a queued one, or coordinated work already
|
|
14743
|
-
// addressed at that thread.
|
|
14744
|
-
working: threadId => threadBusy(target.botId, threadId)
|
|
14745
|
-
|| queuedThreadPosition(target.botId, threadId) !== null
|
|
14746
|
-
|| roomHandoffs.activeDirect(threadId),
|
|
14747
|
-
});
|
|
14748
|
-
if (!resolved)
|
|
15906
|
+
const task = store.createTask(target.botId, `@${internalSender.name}${parsed.data.label ? ` · ${parsed.data.label}` : " · work"}`, false, undefined, { botId: internalSender.id, name: internalSender.name, kind: "work", at: Date.now() });
|
|
15907
|
+
if (!task)
|
|
14749
15908
|
throw new Error("The recipient no longer exists");
|
|
14750
|
-
target.threadId =
|
|
14751
|
-
if (resolved.created)
|
|
14752
|
-
createdThread = resolved.task.threadId;
|
|
14753
|
-
// A work thread carries one assignment: it is for the person
|
|
14754
|
-
// this coordination serves. The durable pair conversation is
|
|
14755
|
-
// shared by every assignment between two bots, so it names nobody.
|
|
14756
|
-
if (resolved.created && resolved.task.openedBy?.kind === "work")
|
|
14757
|
-
threadStarters.set(resolved.task.threadId, threadPersonKey(address.threadId));
|
|
15909
|
+
target.threadId = createdThread = task.threadId;
|
|
14758
15910
|
if (delegatedFullAccess(internalSender, internalCapability.threadId, store.bot(target.botId))) {
|
|
14759
15911
|
grantDelegatedFullAccess(internalSender, store.bot(target.botId), target.threadId);
|
|
14760
15912
|
}
|
|
14761
15913
|
}
|
|
14762
15914
|
const { node, duplicate } = roomHandoffs.enqueue(address, internalCapability.generation, internalCapability.roomHandoffId, target, parsed.data.requestKey + ":" + target.botId, parsed.data.message, approvalGranted, parsed.data.rework, [...store.messagesFor(address.threadId)].reverse().find(m => m.role === "user" && m.kind === "text")?.text ?? "", destination ? parsed.data.requestKey : undefined);
|
|
14763
|
-
// A
|
|
14764
|
-
|
|
14765
|
-
// conversation was busy with that very request) goes back
|
|
14766
|
-
// before anyone sees a row that leads nowhere.
|
|
14767
|
-
if (duplicate && createdThread && createdThread !== node.threadId)
|
|
15915
|
+
// A retry keeps its original task; discard the speculative row.
|
|
15916
|
+
if (duplicate && createdThread && createdThread !== node.threadId) {
|
|
14768
15917
|
store.deleteTask(target.botId, createdThread);
|
|
15918
|
+
}
|
|
15919
|
+
if (!duplicate && createdThread)
|
|
15920
|
+
threadStarters.set(createdThread, openerFrom(address.threadId));
|
|
14769
15921
|
createdThread = undefined; // The durable coordinator now owns this task.
|
|
14770
15922
|
accepted.push({ requestId: node.id, botId: node.botId, duplicate, status: node.status });
|
|
14771
15923
|
// A duplicate request_key lands on the node enqueue already
|
|
@@ -14805,8 +15957,9 @@ const handleRequest = async (req, res) => {
|
|
|
14805
15957
|
}
|
|
14806
15958
|
}
|
|
14807
15959
|
catch (error) {
|
|
14808
|
-
if (createdThread)
|
|
15960
|
+
if (createdThread) {
|
|
14809
15961
|
store.deleteTask(target.botId, createdThread);
|
|
15962
|
+
}
|
|
14810
15963
|
const said = error instanceof Error ? error.message : String(error);
|
|
14811
15964
|
errors.push({ botId: target.botId, error: said });
|
|
14812
15965
|
receipts.push(peerDeliveryReceipt({
|
|
@@ -15049,10 +16202,11 @@ const handleRequest = async (req, res) => {
|
|
|
15049
16202
|
}
|
|
15050
16203
|
const sourceTitle = owner.group ? owner.group.name : (store.taskByThread(from.id, fromThreadId)?.title ?? "");
|
|
15051
16204
|
if (target.id === from.id) {
|
|
15052
|
-
const
|
|
16205
|
+
const inherited = parentThreadModel(from.id, fromThreadId);
|
|
16206
|
+
const task = store.createTask(from.id, title, false, projectId, { botId: from.id, name: from.name, at: Date.now() }, inherited.approvalMode, inherited.modelSelection);
|
|
15053
16207
|
if (!task)
|
|
15054
16208
|
return json(res, 500, { error: "couldn't create that thread" });
|
|
15055
|
-
threadStarters.set(task.threadId,
|
|
16209
|
+
threadStarters.set(task.threadId, openerFrom(fromThreadId));
|
|
15056
16210
|
internalCapability.openedThreads += 1;
|
|
15057
16211
|
const chip = {
|
|
15058
16212
|
role: "bot",
|
|
@@ -15095,7 +16249,7 @@ const handleRequest = async (req, res) => {
|
|
|
15095
16249
|
if (!task)
|
|
15096
16250
|
return json(res, 500, { error: "couldn't create that thread" });
|
|
15097
16251
|
// The work is still for the person whose request the opener is on.
|
|
15098
|
-
threadStarters.set(task.threadId,
|
|
16252
|
+
threadStarters.set(task.threadId, openerFrom(fromThreadId));
|
|
15099
16253
|
if (delegatedFullAccess(from, fromThreadId, target))
|
|
15100
16254
|
grantDelegatedFullAccess(from, target, task.threadId);
|
|
15101
16255
|
const queued = queueDelegation(commsBus, from, { toBotId: target.id, message, depth, targetThreadId: task.threadId }, MAX_COMMS_DEPTH, fromThreadId);
|
|
@@ -15266,7 +16420,11 @@ const handleRequest = async (req, res) => {
|
|
|
15266
16420
|
}
|
|
15267
16421
|
const group = createChannel({
|
|
15268
16422
|
name: redactSecretsInText(parsed.data.name), memberIds, section: chief.section,
|
|
15269
|
-
setup: {
|
|
16423
|
+
setup: {
|
|
16424
|
+
bulletin: redactSecretsInText(parsed.data.bulletin ?? ""),
|
|
16425
|
+
// The Chief leads; on Auto it is the fallback when the decision model is unsure.
|
|
16426
|
+
defaultResponder: deciderReady(cfg, "roomRouting") ? { kind: "auto", fallbackBotId: chief.id } : { kind: "member", botId: chief.id },
|
|
16427
|
+
},
|
|
15270
16428
|
});
|
|
15271
16429
|
internalCapability.createdRooms = (internalCapability.createdRooms ?? 0) + 1;
|
|
15272
16430
|
return json(res, 201, { id: group.id, name: group.name, section: group.section || "General", memberIds: group.memberIds, memberCount: group.memberIds.length });
|
|
@@ -15640,12 +16798,26 @@ const handleRequest = async (req, res) => {
|
|
|
15640
16798
|
.filter((run) => routineVisible(run, visible)),
|
|
15641
16799
|
});
|
|
15642
16800
|
}
|
|
16801
|
+
// On a Cloud home a routine reports only where its writer may write: a
|
|
16802
|
+
// guest's into a conversation the guest started, never the owner's.
|
|
16803
|
+
const routineResultsRefusal = (body) => body && typeof body === "object" && typeof body.resultsThreadId === "string"
|
|
16804
|
+
? cloudThreadRefusal(auth, body.resultsThreadId) : null;
|
|
15643
16805
|
if (path === "/api/routines" && method === "POST") {
|
|
15644
16806
|
const body = await readBody(req);
|
|
15645
16807
|
const hidden = hiddenRoutineTarget(body, visible);
|
|
15646
16808
|
if (hidden)
|
|
15647
16809
|
return json(res, 404, { error: hidden });
|
|
15648
|
-
|
|
16810
|
+
const notYours = routineResultsRefusal(body);
|
|
16811
|
+
if (notYours)
|
|
16812
|
+
return json(res, 403, { error: notYours });
|
|
16813
|
+
const writer = auth.kind === "session" ? actorKey(auth) : CLOUD_NOBODY_KEY;
|
|
16814
|
+
const routine = withRoutineWriter(writer, () => routines.create(body));
|
|
16815
|
+
// On a Cloud home, a routine the owner writes from their own device may
|
|
16816
|
+
// use their lent Mac when it runs (server/cloud-lending.ts).
|
|
16817
|
+
if (cloudRoutineAuthors && cloudOwnerSession(auth))
|
|
16818
|
+
cloudRoutineAuthors.record(routine.id, routine);
|
|
16819
|
+
cloudRoutineAuthors?.wrote(routine.id, writer);
|
|
16820
|
+
return json(res, 201, { routine });
|
|
15649
16821
|
}
|
|
15650
16822
|
// The desktop shell polls this to decide whether to hold the computer
|
|
15651
16823
|
// awake: a run in flight, or a routine due within the hour.
|
|
@@ -15655,6 +16827,8 @@ const handleRequest = async (req, res) => {
|
|
|
15655
16827
|
let routineMatch = path.match(/^\/api\/routines\/([\w-]+)\/run$/);
|
|
15656
16828
|
if (routineMatch && method === "POST") {
|
|
15657
16829
|
const run = routines.runNow(routineMatch[1]);
|
|
16830
|
+
if (run && CLOUD_HOME && cloudOwnerSession(auth))
|
|
16831
|
+
ownerStartedRoutineRuns.add(run.id);
|
|
15658
16832
|
return run ? json(res, 201, { run }) : json(res, 404, { error: "no such routine" });
|
|
15659
16833
|
}
|
|
15660
16834
|
routineMatch = path.match(/^\/api\/routines\/([\w-]+)$/);
|
|
@@ -15663,11 +16837,45 @@ const handleRequest = async (req, res) => {
|
|
|
15663
16837
|
const hidden = hiddenRoutineTarget(body, visible);
|
|
15664
16838
|
if (hidden)
|
|
15665
16839
|
return json(res, 404, { error: hidden });
|
|
15666
|
-
const
|
|
16840
|
+
const notYours = routineResultsRefusal(body);
|
|
16841
|
+
if (notYours)
|
|
16842
|
+
return json(res, 403, { error: notYours });
|
|
16843
|
+
const before = cloudRoutineAuthors ? routines.listRoutines().find((candidate) => candidate.id === routineMatch[1]) : undefined;
|
|
16844
|
+
const wasOwners = Boolean(before && cloudRoutineAuthors?.authored(before.id, before));
|
|
16845
|
+
const previous = cloudRoutineAuthors?.writer(routineMatch[1]);
|
|
16846
|
+
// Who this edit leaves as the routine's writer: the owner when the
|
|
16847
|
+
// routine stays (or becomes) theirs (their fingerprint, or already
|
|
16848
|
+
// theirs as its writer: a template, a restore), a guest when a guest
|
|
16849
|
+
// edits it, else whoever wrote it before.
|
|
16850
|
+
// An owner writer is proof only for a routine that never had the
|
|
16851
|
+
// owner's fingerprint (a template, a restore, one the settlement named):
|
|
16852
|
+
// a fingerprint that no longer matches means someone else changed what
|
|
16853
|
+
// it runs, and only the owner rewriting its instructions renews it.
|
|
16854
|
+
const ownerWriterOnly = CLOUD_OWNER_KEY !== null && previous === CLOUD_OWNER_KEY && !cloudRoutineAuthors?.recorded(routineMatch[1]);
|
|
16855
|
+
const ownerAuthors = cloudOwnerSession(auth) && (wasOwners || ownerWriterOnly ||
|
|
16856
|
+
(body && typeof body === "object" && "prompt" in body));
|
|
16857
|
+
const writer = ownerAuthors && CLOUD_OWNER_KEY ? CLOUD_OWNER_KEY
|
|
16858
|
+
: auth.kind === "session" && !cloudOwnerSession(auth) ? actorKey(auth)
|
|
16859
|
+
: previous && !cloudOwnerPerson(previous) ? previous : CLOUD_NOBODY_KEY;
|
|
16860
|
+
const routine = withRoutineWriter(writer, () => routines.update(routineMatch[1], body));
|
|
16861
|
+
// The owner's own edit keeps (or, when it rewrites the instructions,
|
|
16862
|
+
// makes) the routine theirs. Anyone else's edit, of any field (its
|
|
16863
|
+
// schedule, where its results go, whether it is on), makes it no longer
|
|
16864
|
+
// the owner's: it stops lending until the owner rewrites it.
|
|
16865
|
+
if (routine && cloudRoutineAuthors) {
|
|
16866
|
+
if (!cloudOwnerSession(auth))
|
|
16867
|
+
cloudRoutineAuthors.forget(routine.id);
|
|
16868
|
+
else if (ownerAuthors)
|
|
16869
|
+
cloudRoutineAuthors.record(routine.id, routine);
|
|
16870
|
+
cloudRoutineAuthors.wrote(routine.id, writer);
|
|
16871
|
+
}
|
|
15667
16872
|
return routine ? json(res, 200, { routine }) : json(res, 404, { error: "no such routine" });
|
|
15668
16873
|
}
|
|
15669
16874
|
if (routineMatch && method === "DELETE") {
|
|
15670
|
-
|
|
16875
|
+
const removed = routines.remove(routineMatch[1]);
|
|
16876
|
+
if (removed)
|
|
16877
|
+
cloudRoutineAuthors?.remove(routineMatch[1]);
|
|
16878
|
+
return removed
|
|
15671
16879
|
? json(res, 200, { ok: true })
|
|
15672
16880
|
: json(res, 404, { error: "no such routine" });
|
|
15673
16881
|
}
|
|
@@ -16340,7 +17548,7 @@ const handleRequest = async (req, res) => {
|
|
|
16340
17548
|
const current = resolveRequestAuth(req, {
|
|
16341
17549
|
sessions, cookieName: SESSION_COOKIE, streamPath: "/api/events", url,
|
|
16342
17550
|
loopbackMutationToken: desktopMutationToken, companionMutationToken,
|
|
16343
|
-
features: { sharedComputers:
|
|
17551
|
+
features: { sharedComputers: lendingEnabled() }, loopbackTrust: LOOPBACK.trust, cliOwnerToken,
|
|
16344
17552
|
});
|
|
16345
17553
|
if (!current.auth)
|
|
16346
17554
|
return json(res, current.status, { error: current.error });
|
|
@@ -16428,6 +17636,9 @@ const handleRequest = async (req, res) => {
|
|
|
16428
17636
|
if (hiddenMember)
|
|
16429
17637
|
return json(res, 400, { error: `unknown channel member: ${String(hiddenMember)}` });
|
|
16430
17638
|
const group = createChannel(body);
|
|
17639
|
+
// On a Cloud home whoever opens a room names it (cloudThreadRefusal).
|
|
17640
|
+
if (CLOUD_HOME && auth.kind === "session")
|
|
17641
|
+
threadStarters.set(group.threadId, actorKey(auth));
|
|
16431
17642
|
return json(res, 201, { group: { ...publicGroupState(group), messages: [] } });
|
|
16432
17643
|
}
|
|
16433
17644
|
if (method === "POST" && path === "/api/teams/export") {
|
|
@@ -16838,15 +18049,7 @@ const handleRequest = async (req, res) => {
|
|
|
16838
18049
|
return json(res, 400, { error: "bulletin must be a string" });
|
|
16839
18050
|
if (body.bulletin.length > 12_000)
|
|
16840
18051
|
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
|
-
}
|
|
18052
|
+
const responder = checkedGroupResponder(body.defaultResponder, group.memberIds);
|
|
16850
18053
|
if (!responder)
|
|
16851
18054
|
return json(res, 400, { error: "invalid default responder" });
|
|
16852
18055
|
patch.cwd = checked.cwd ?? undefined;
|
|
@@ -16903,7 +18106,7 @@ const handleRequest = async (req, res) => {
|
|
|
16903
18106
|
return json(res, 500, { error: "couldn't create that task" });
|
|
16904
18107
|
// Who opened it decides who may answer its cards on a shared workspace.
|
|
16905
18108
|
if (auth.kind === "session")
|
|
16906
|
-
threadStarters.set(task.threadId,
|
|
18109
|
+
threadStarters.set(task.threadId, actorKey(auth));
|
|
16907
18110
|
const fresh = groupWithThread(store.group(group.id));
|
|
16908
18111
|
broadcast({ kind: "group", group: fresh });
|
|
16909
18112
|
return json(res, 201, { group: fresh, task });
|
|
@@ -16980,6 +18183,9 @@ const handleRequest = async (req, res) => {
|
|
|
16980
18183
|
// without the old handler turning a missing title into "Untitled".
|
|
16981
18184
|
// That echo is not a rename. A real rename stays blocked while working.
|
|
16982
18185
|
const titleChange = renaming && body.title !== existing.title;
|
|
18186
|
+
const notYours = titleChange ? cloudThreadRefusal(auth, m[2]) : null;
|
|
18187
|
+
if (notYours)
|
|
18188
|
+
return json(res, 403, { error: notYours });
|
|
16983
18189
|
if (channelTaskBlocked(group) && !(pinning && !titleChange)) {
|
|
16984
18190
|
return json(res, 409, { error: "this channel is working or waiting on you — finish that turn first" });
|
|
16985
18191
|
}
|
|
@@ -17012,6 +18218,9 @@ const handleRequest = async (req, res) => {
|
|
|
17012
18218
|
}
|
|
17013
18219
|
if (!store.groupTaskByThread(group.id, m[2]))
|
|
17014
18220
|
return json(res, 404, { error: "no such channel task" });
|
|
18221
|
+
const notYours = cloudThreadRefusal(auth, m[2]);
|
|
18222
|
+
if (notYours)
|
|
18223
|
+
return json(res, 403, { error: notYours });
|
|
17015
18224
|
const stagedSkillCleanups = stagedSkillCleanupsForThread(m[2]);
|
|
17016
18225
|
lastReply.delete(m[2]);
|
|
17017
18226
|
cancelTeamSetupResumesForThread(m[2]);
|
|
@@ -17033,6 +18242,14 @@ const handleRequest = async (req, res) => {
|
|
|
17033
18242
|
if (field)
|
|
17034
18243
|
return json(res, 403, { error: `forbidden: this session may rename or mark a room, not change "${field}" (needs the admin scope)` });
|
|
17035
18244
|
}
|
|
18245
|
+
// On a Cloud home a room's name and bulletin are its opener's (or the owner's).
|
|
18246
|
+
const renamed = store.group(m[1]);
|
|
18247
|
+
if (renamed && body && typeof body === "object" &&
|
|
18248
|
+
((body.name !== undefined && body.name !== renamed.name) || (body.bulletin !== undefined && body.bulletin !== (renamed.bulletin ?? "")))) {
|
|
18249
|
+
const notYours = cloudThreadRefusal(auth, renamed.threadId);
|
|
18250
|
+
if (notYours)
|
|
18251
|
+
return json(res, 403, { error: notYours });
|
|
18252
|
+
}
|
|
17036
18253
|
const group = updateChannel(m[1], body);
|
|
17037
18254
|
return json(res, 200, { group: publicGroupState(group) });
|
|
17038
18255
|
}
|
|
@@ -17657,9 +18874,14 @@ const handleRequest = async (req, res) => {
|
|
|
17657
18874
|
throw Object.assign(new Error(enabled.error), { status: 422 });
|
|
17658
18875
|
}
|
|
17659
18876
|
}
|
|
18877
|
+
// On a Cloud home only the owner's devices create bots: its routines
|
|
18878
|
+
// (and the conversations they report into) are theirs.
|
|
18879
|
+
const ownersRoutines = CLOUD_OWNER_KEY && auth.kind === "session" && cloudOwnerSession(auth) ? CLOUD_OWNER_KEY : null;
|
|
17660
18880
|
for (const routine of template.routines) {
|
|
17661
18881
|
const created = routines.create({ ...routine, botId: bot.id, enabled: false });
|
|
17662
18882
|
createdRoutines.push({ id: created.id, enabled: routine.enabled !== false });
|
|
18883
|
+
if (ownersRoutines)
|
|
18884
|
+
cloudRoutineAuthors?.wrote(created.id, ownersRoutines);
|
|
17663
18885
|
}
|
|
17664
18886
|
}
|
|
17665
18887
|
catch (error) {
|
|
@@ -17745,6 +18967,17 @@ const handleRequest = async (req, res) => {
|
|
|
17745
18967
|
return json(res, 400, { error: "avatarUrl must reference an existing stored image" });
|
|
17746
18968
|
}
|
|
17747
18969
|
const existingBot = store.bot(m[1]);
|
|
18970
|
+
// On a Cloud home a bot's name, title, description and standing
|
|
18971
|
+
// instructions ride every one of the owner's turns (and a lent Mac):
|
|
18972
|
+
// only the owner's own devices change them. Anyone else keeps the
|
|
18973
|
+
// picture and the voice. Resending an unchanged value is not a change.
|
|
18974
|
+
if (CLOUD_HOME && existingBot && !cloudOwnerSession(auth)) {
|
|
18975
|
+
const changed = CLOUD_OWNER_PROFILE_FIELDS.find((field) => parsed.patch[field] !== undefined && (field === "notifications"
|
|
18976
|
+
? (parsed.patch.notifications !== false) !== (existingBot.notifications !== false)
|
|
18977
|
+
: parsed.patch[field] !== (existingBot[field] ?? "")));
|
|
18978
|
+
if (changed)
|
|
18979
|
+
return json(res, 403, { error: "On this Cloud only its owner can change a bot's name, title, description, instructions or notifications." });
|
|
18980
|
+
}
|
|
17748
18981
|
const beforeProfile = existingBot ? profileSnapshot(existingBot) : undefined;
|
|
17749
18982
|
const bot = store.patchBotProfile(m[1], parsed.patch);
|
|
17750
18983
|
if (!bot)
|
|
@@ -17998,6 +19231,15 @@ const handleRequest = async (req, res) => {
|
|
|
17998
19231
|
return json(res, 400, { error: "voiceNotes must be true or false" });
|
|
17999
19232
|
patch.voiceNotes = body.voiceNotes;
|
|
18000
19233
|
}
|
|
19234
|
+
if (body.memoryEnabled !== undefined) {
|
|
19235
|
+
if (typeof body.memoryEnabled !== "boolean")
|
|
19236
|
+
return json(res, 400, { error: "memoryEnabled must be true or false" });
|
|
19237
|
+
if (existingBot && (existingBot.busy || activeGroupTurnForBot(existingBot.id)) &&
|
|
19238
|
+
body.memoryEnabled !== (existingBot.memoryEnabled !== false)) {
|
|
19239
|
+
return json(res, 409, { error: "stop this bot's turn before changing its memory setting" });
|
|
19240
|
+
}
|
|
19241
|
+
patch.memoryEnabled = body.memoryEnabled;
|
|
19242
|
+
}
|
|
18001
19243
|
// which of those apps' tools this bot may call (connector grants 1/5:
|
|
18002
19244
|
// data model only — enforcement lands with slice 2). null returns the
|
|
18003
19245
|
// bot to boolean-only legacy behavior; {} means no tools.
|
|
@@ -18334,6 +19576,13 @@ const handleRequest = async (req, res) => {
|
|
|
18334
19576
|
sectionKey(existingBot?.section) !== sectionKey(section);
|
|
18335
19577
|
let bot;
|
|
18336
19578
|
const freshBrowserBot = store.bot(m[1]);
|
|
19579
|
+
// Connector validation and runtime revocation can yield after the first
|
|
19580
|
+
// memory check. Keep the same idle-only transition at the final commit.
|
|
19581
|
+
if (freshBrowserBot && body.memoryEnabled !== undefined &&
|
|
19582
|
+
body.memoryEnabled !== (freshBrowserBot.memoryEnabled !== false) &&
|
|
19583
|
+
(freshBrowserBot.busy || activeGroupTurnForBot(freshBrowserBot.id))) {
|
|
19584
|
+
return json(res, 409, { error: "stop this bot's turn before changing its memory setting" });
|
|
19585
|
+
}
|
|
18337
19586
|
// Custom servers are process configuration: a running turn cannot
|
|
18338
19587
|
// unmount them. Recheck after any awaited runtime revocation above.
|
|
18339
19588
|
if (body.mcpServers !== undefined) {
|
|
@@ -18389,7 +19638,7 @@ const handleRequest = async (req, res) => {
|
|
|
18389
19638
|
}
|
|
18390
19639
|
if (!bot)
|
|
18391
19640
|
return json(res, 404, { error: "no such bot" });
|
|
18392
|
-
if (body.memoryUpkeep === false)
|
|
19641
|
+
if (body.memoryUpkeep === false || body.memoryEnabled === false)
|
|
18393
19642
|
memoryUpkeep.dropBot(bot.id);
|
|
18394
19643
|
// A defined Works on is the newest explicit choice: this bot's
|
|
18395
19644
|
// auto-recorded pins that now point elsewhere give way immediately, so
|
|
@@ -18775,6 +20024,12 @@ const handleRequest = async (req, res) => {
|
|
|
18775
20024
|
if (existing.card.requestId) {
|
|
18776
20025
|
return json(res, 409, { error: "request cards must be answered through the approval endpoint" });
|
|
18777
20026
|
}
|
|
20027
|
+
// On a Cloud home only the owner answers cards (cardAnswerRefusal).
|
|
20028
|
+
// This route only keeps how an options card looks; the answer itself
|
|
20029
|
+
// reaches the bot as a chat line, which lending already checks.
|
|
20030
|
+
const cloudRefusal = CLOUD_HOME && body.answered !== undefined && auth.kind === "session" ? cloudCardRefusal(auth) : null;
|
|
20031
|
+
if (cloudRefusal)
|
|
20032
|
+
return json(res, 403, { error: cloudRefusal });
|
|
18778
20033
|
if (Object.keys(body).some((key) => key !== "answered" && key !== "dismissed" && key !== "threadId")) {
|
|
18779
20034
|
return json(res, 400, { error: "only answered and dismissed may be changed" });
|
|
18780
20035
|
}
|
|
@@ -18789,6 +20044,7 @@ const handleRequest = async (req, res) => {
|
|
|
18789
20044
|
...existing.card,
|
|
18790
20045
|
...(body.answered !== undefined ? { answered: body.answered } : {}),
|
|
18791
20046
|
...(body.dismissed !== undefined ? { dismissed: body.dismissed } : {}),
|
|
20047
|
+
...(CLOUD_HOME && body.answered !== undefined ? { answeredBy: cardAnswererFor(auth) } : {}),
|
|
18792
20048
|
},
|
|
18793
20049
|
});
|
|
18794
20050
|
return json(res, 200, { message: patched });
|
|
@@ -18852,6 +20108,9 @@ const handleRequest = async (req, res) => {
|
|
|
18852
20108
|
// receipt after a task switch, while a genuinely new send still has to
|
|
18853
20109
|
// target the task that is active now.
|
|
18854
20110
|
const threadId = body.threadId ?? bot.threadId;
|
|
20111
|
+
const notYours = cloudGuestSendRefusal(auth, threadId);
|
|
20112
|
+
if (notYours)
|
|
20113
|
+
return json(res, 403, { error: notYours });
|
|
18855
20114
|
// Who this message is from, for the ledger. It is captured here and
|
|
18856
20115
|
// travels with the message: into the turn it starts, or into the queue
|
|
18857
20116
|
// until it drains. A message steered into someone else's running turn
|
|
@@ -19112,6 +20371,9 @@ const handleRequest = async (req, res) => {
|
|
|
19112
20371
|
const body = await readBody(req);
|
|
19113
20372
|
requirePinnedClientThread(m[1], body?.threadId);
|
|
19114
20373
|
const bot = requestedTaskBot(m[1], body?.threadId);
|
|
20374
|
+
const notYours = cloudThreadRefusal(auth, bot.threadId);
|
|
20375
|
+
if (notYours)
|
|
20376
|
+
return json(res, 403, { error: notYours });
|
|
19115
20377
|
await startTurn(bot.id, "Summarize conversation context", {
|
|
19116
20378
|
threadId: bot.threadId, compactOnly: true, cardContinuation: true, trigger: usageTriggerFor(auth),
|
|
19117
20379
|
});
|
|
@@ -19127,6 +20389,9 @@ const handleRequest = async (req, res) => {
|
|
|
19127
20389
|
const body = await readBody(req);
|
|
19128
20390
|
requirePinnedClientThread(m[1], body?.threadId);
|
|
19129
20391
|
const bot = requestedTaskBot(m[1], body.threadId);
|
|
20392
|
+
const notYours = cloudThreadRefusal(auth, bot.threadId);
|
|
20393
|
+
if (notYours)
|
|
20394
|
+
return json(res, 403, { error: notYours });
|
|
19130
20395
|
const text = String(body.text ?? "").trim();
|
|
19131
20396
|
if (!text)
|
|
19132
20397
|
return json(res, 400, { error: "text required" });
|
|
@@ -19167,7 +20432,10 @@ const handleRequest = async (req, res) => {
|
|
|
19167
20432
|
// startTurn admits the rerun before branching. A shared-resource or
|
|
19168
20433
|
// concurrency-limit refusal must leave the original transcript intact.
|
|
19169
20434
|
const replyTo = source.replyToId ? resolveReplyTarget(bot.threadId, source.replyToId) : undefined;
|
|
19170
|
-
|
|
20435
|
+
// On a Cloud home an edit is its author's line, so the owner's own edit
|
|
20436
|
+
// keeps their conversation theirs for lending (server/cloud-lending.ts).
|
|
20437
|
+
const message = await startTurn(bot.id, text, { threadId: bot.threadId, editedMessageId: messageId, replyTo, sendId, trigger: usageTriggerFor(auth),
|
|
20438
|
+
...(CLOUD_HOME ? { sender: messageSender(auth) } : {}) });
|
|
19171
20439
|
return json(res, 202, { ok: true, message });
|
|
19172
20440
|
}
|
|
19173
20441
|
// switch which fork of the conversation is visible (no new turn)
|
|
@@ -19176,6 +20444,9 @@ const handleRequest = async (req, res) => {
|
|
|
19176
20444
|
const body = await readBody(req);
|
|
19177
20445
|
requirePinnedClientThread(m[1], body?.threadId);
|
|
19178
20446
|
const bot = requestedTaskBot(m[1], body.threadId);
|
|
20447
|
+
const notYours = cloudThreadRefusal(auth, bot.threadId);
|
|
20448
|
+
if (notYours)
|
|
20449
|
+
return json(res, 403, { error: notYours });
|
|
19179
20450
|
if (threadBusy(bot.id, bot.threadId))
|
|
19180
20451
|
return json(res, 409, { error: "the thread is working — stop it before switching versions" });
|
|
19181
20452
|
if (phoneSecretSubmissions.hasThread(bot.threadId)) {
|
|
@@ -19216,7 +20487,7 @@ const handleRequest = async (req, res) => {
|
|
|
19216
20487
|
threadId: bot.threadId,
|
|
19217
20488
|
requestId: String(body.requestId),
|
|
19218
20489
|
behavior,
|
|
19219
|
-
}))
|
|
20490
|
+
}, auth.kind === "session" && cloudOwnerSession(auth)))
|
|
19220
20491
|
return;
|
|
19221
20492
|
if (resolveAndSendProfile(res, {
|
|
19222
20493
|
botId: bot.id,
|
|
@@ -19314,7 +20585,7 @@ const handleRequest = async (req, res) => {
|
|
|
19314
20585
|
threadId,
|
|
19315
20586
|
requestId,
|
|
19316
20587
|
behavior,
|
|
19317
|
-
}))
|
|
20588
|
+
}, auth.kind === "session" && cloudOwnerSession(auth)))
|
|
19318
20589
|
return;
|
|
19319
20590
|
}
|
|
19320
20591
|
const setupCard = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId && message.card.teamSetupRequest);
|
|
@@ -19553,7 +20824,7 @@ const handleRequest = async (req, res) => {
|
|
|
19553
20824
|
return json(res, 500, { error: "couldn't create that task" });
|
|
19554
20825
|
// Who opened it decides who may answer its cards on a shared workspace.
|
|
19555
20826
|
if (auth.kind === "session")
|
|
19556
|
-
threadStarters.set(task.threadId,
|
|
20827
|
+
threadStarters.set(task.threadId, actorKey(auth));
|
|
19557
20828
|
const fresh = botWithThread(store.bot(bot.id));
|
|
19558
20829
|
broadcast({ kind: "bot", bot: fresh });
|
|
19559
20830
|
return json(res, 201, { bot: fresh, task: wireTask(task) });
|
|
@@ -19599,6 +20870,16 @@ const handleRequest = async (req, res) => {
|
|
|
19599
20870
|
const allowed = new Set(["title", "projectId", "modelSelection", "updateBotDefault", "resetApprovalToAsk", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto", "archivedAt", "pinned", "snoozedUntil", "surface"]);
|
|
19600
20871
|
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
19601
20872
|
return json(res, 400, { error: "unsupported thread setting" });
|
|
20873
|
+
const notYours = cloudThreadRefusal(auth, m[2]);
|
|
20874
|
+
if (notYours)
|
|
20875
|
+
return json(res, 403, { error: notYours });
|
|
20876
|
+
// On a Cloud home only the owner decides whether a bot may act without
|
|
20877
|
+
// asking them, even in a conversation someone else started, and what
|
|
20878
|
+
// the bot itself defaults to.
|
|
20879
|
+
if (CLOUD_HOME && !cloudOwnerSession(auth) &&
|
|
20880
|
+
(body.approvalMode !== undefined || body.autoApprove !== undefined || body.acknowledgeLocalAuto !== undefined || body.updateBotDefault === true)) {
|
|
20881
|
+
return json(res, 403, { error: "On this Cloud only its owner can change how a bot asks for approval, or its default model." });
|
|
20882
|
+
}
|
|
19602
20883
|
for (const key of ["requireAvailableModel", "acknowledgeLocalAuto", "updateBotDefault", "resetApprovalToAsk"]) {
|
|
19603
20884
|
if (body[key] !== undefined && typeof body[key] !== "boolean")
|
|
19604
20885
|
return json(res, 400, { error: `${key} must be a boolean` });
|
|
@@ -19724,6 +21005,9 @@ const handleRequest = async (req, res) => {
|
|
|
19724
21005
|
if (!bot || !store.taskByThread(bot.id, m[2])) {
|
|
19725
21006
|
return json(res, 404, { error: "no such task" });
|
|
19726
21007
|
}
|
|
21008
|
+
const notYours = cloudThreadRefusal(auth, m[2]);
|
|
21009
|
+
if (notYours)
|
|
21010
|
+
return json(res, 403, { error: notYours });
|
|
19727
21011
|
if (phoneSecretSubmissions.hasThread(m[2])) {
|
|
19728
21012
|
return json(res, 409, { error: "this task is securely saving a credential — try again when it finishes" });
|
|
19729
21013
|
}
|
|
@@ -19744,6 +21028,55 @@ const handleRequest = async (req, res) => {
|
|
|
19744
21028
|
broadcast({ kind: "bot", bot: fresh });
|
|
19745
21029
|
return json(res, 200, { bot: fresh });
|
|
19746
21030
|
}
|
|
21031
|
+
// Regenerate title (#1858): name the thread again from its conversation
|
|
21032
|
+
// as it is now. The person asked, so the result replaces any title, but
|
|
21033
|
+
// only the one this started from: a rename that lands while the one-shot
|
|
21034
|
+
// runs wins, and the late result is dropped.
|
|
21035
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/tasks\/([\w-]+)\/title$/);
|
|
21036
|
+
if (m && method === "POST") {
|
|
21037
|
+
const current = store.projectBotForTask(m[1], m[2]);
|
|
21038
|
+
if (!current)
|
|
21039
|
+
return json(res, 404, { error: "no such task" });
|
|
21040
|
+
const notYours = cloudThreadRefusal(auth, m[2]);
|
|
21041
|
+
if (notYours)
|
|
21042
|
+
return json(res, 403, { error: notYours });
|
|
21043
|
+
if (!llmThreadTitlesEnabled(cfg))
|
|
21044
|
+
return json(res, 409, { error: "generated thread titles are turned off on this server" });
|
|
21045
|
+
if (hostedModels && !hostedModels.allows(current.modelSelection))
|
|
21046
|
+
return json(res, 409, { error: hostedModels.error() });
|
|
21047
|
+
const instance = registry.get(current.modelSelection.instanceId);
|
|
21048
|
+
if (!instance?.generateText)
|
|
21049
|
+
return json(res, 409, { error: "this bot's model can't generate a title — rename the thread instead" });
|
|
21050
|
+
// An engine the organisation disallows never receives the text, even for a title.
|
|
21051
|
+
const refusal = policyModelRefusal(instance);
|
|
21052
|
+
if (refusal)
|
|
21053
|
+
return json(res, 409, { error: refusal });
|
|
21054
|
+
if (titleRegenerations.has(m[2]))
|
|
21055
|
+
return json(res, 409, { error: "a new title is already being generated for this thread" });
|
|
21056
|
+
const excerpt = titleConversationExcerpt(store.messagesTail(m[2], 200).messages);
|
|
21057
|
+
if (!excerpt)
|
|
21058
|
+
return json(res, 409, { error: "this thread has no messages to take a title from yet" });
|
|
21059
|
+
try {
|
|
21060
|
+
assertWithinBudget(cfg, DATA_DIR);
|
|
21061
|
+
}
|
|
21062
|
+
catch (error) {
|
|
21063
|
+
return json(res, 409, { error: error instanceof Error ? error.message : String(error) });
|
|
21064
|
+
}
|
|
21065
|
+
const startedFrom = store.taskByThread(m[1], m[2]).title;
|
|
21066
|
+
titleRegenerations.add(m[2]);
|
|
21067
|
+
try {
|
|
21068
|
+
const title = await generateThreadTitle(instance, excerpt, "conversation");
|
|
21069
|
+
if (!title)
|
|
21070
|
+
return json(res, 502, { error: "couldn't generate a title — try again, or rename the thread" });
|
|
21071
|
+
const task = store.retitleTask(m[1], m[2], startedFrom, title) ?? store.taskByThread(m[1], m[2]);
|
|
21072
|
+
if (!task)
|
|
21073
|
+
return json(res, 404, { error: "no such task" });
|
|
21074
|
+
return json(res, 200, { task: wireTask(task) });
|
|
21075
|
+
}
|
|
21076
|
+
finally {
|
|
21077
|
+
titleRegenerations.delete(m[2]);
|
|
21078
|
+
}
|
|
21079
|
+
}
|
|
19747
21080
|
// Named team Boats use real independent ownership, never a hidden bot or
|
|
19748
21081
|
// an arbitrary provider id. These new routes remain admin-only by default.
|
|
19749
21082
|
if (path === "/api/team-computers" && method === "GET") {
|
|
@@ -19937,7 +21270,7 @@ const handleRequest = async (req, res) => {
|
|
|
19937
21270
|
// what the user's machine can host: which runtime is installed, whether
|
|
19938
21271
|
// its daemon is up, and whether the desktop image and container exist
|
|
19939
21272
|
if (method === "GET" && path === "/api/local-computer") {
|
|
19940
|
-
return json(res, 200, await localVmPayload(SHARED_LOCAL_VM_TARGET));
|
|
21273
|
+
return json(res, 200, await localVmPayload(SHARED_LOCAL_VM_TARGET, auth));
|
|
19941
21274
|
}
|
|
19942
21275
|
if (method === "GET" && path === "/api/local-computer/instances") {
|
|
19943
21276
|
res.setHeader("cache-control", "private, no-store");
|
|
@@ -19983,9 +21316,9 @@ const handleRequest = async (req, res) => {
|
|
|
19983
21316
|
if (action === "stop" || action === "remove")
|
|
19984
21317
|
localVmIdleFor(SHARED_LOCAL_VM_TARGET).cancel();
|
|
19985
21318
|
return json(res, 200, {
|
|
19986
|
-
...status,
|
|
21319
|
+
...localVmViewerStatus(status, auth),
|
|
19987
21320
|
commands: setupCommands(status.runtime, process.platform, SHARED_LOCAL_VM_TARGET),
|
|
19988
|
-
idle_timeout_ms:
|
|
21321
|
+
idle_timeout_ms: localVmIdleMs(),
|
|
19989
21322
|
mode: localVmMode(cfg),
|
|
19990
21323
|
max_instances: localVmMaxInstances(cfg),
|
|
19991
21324
|
});
|
|
@@ -20009,7 +21342,7 @@ const handleRequest = async (req, res) => {
|
|
|
20009
21342
|
const bot = computerPreviewBot(m[1], url);
|
|
20010
21343
|
if (!bot)
|
|
20011
21344
|
return json(res, 404, { error: "no such bot" });
|
|
20012
|
-
return json(res, 200, await localVmPayload(localVmTargetForStatus(bot.id, bot.threadId)));
|
|
21345
|
+
return json(res, 200, await localVmPayload(localVmTargetForStatus(bot.id, bot.threadId), auth));
|
|
20013
21346
|
}
|
|
20014
21347
|
m = path.match(/^\/api\/bots\/([\w-]+)\/local-computer\/(run|stop|remove)$/);
|
|
20015
21348
|
if (m && method === "POST") {
|
|
@@ -20066,9 +21399,9 @@ const handleRequest = async (req, res) => {
|
|
|
20066
21399
|
if (action === "stop" || action === "remove")
|
|
20067
21400
|
localVmIdleFor(target).cancel();
|
|
20068
21401
|
return json(res, 200, {
|
|
20069
|
-
...status,
|
|
21402
|
+
...localVmViewerStatus(status, auth),
|
|
20070
21403
|
commands: setupCommands(status.runtime, process.platform, target),
|
|
20071
|
-
idle_timeout_ms:
|
|
21404
|
+
idle_timeout_ms: localVmIdleMs(),
|
|
20072
21405
|
mode: localVmMode(cfg),
|
|
20073
21406
|
max_instances: localVmMaxInstances(cfg),
|
|
20074
21407
|
});
|
|
@@ -20233,7 +21566,7 @@ const handleRequest = async (req, res) => {
|
|
|
20233
21566
|
return json(res, 400, { error: `provider must be one of ${PROVIDER_KEY_KINDS.join(", ")}` });
|
|
20234
21567
|
}
|
|
20235
21568
|
const kind = provider;
|
|
20236
|
-
const saved =
|
|
21569
|
+
const saved = { anthropic: cfg.anthropic, openai: cfg.openai, openrouter: cfg.openrouter, openaiCompat: cfg.openaiCompat, mistral: cfg.mistral, xai: cfg.xai }[kind];
|
|
20237
21570
|
if (body?.key !== undefined && typeof body.key !== "string") {
|
|
20238
21571
|
return json(res, 400, { error: "key must be a string" });
|
|
20239
21572
|
}
|
|
@@ -20350,6 +21683,30 @@ const handleRequest = async (req, res) => {
|
|
|
20350
21683
|
providerConfigBusy = false;
|
|
20351
21684
|
}
|
|
20352
21685
|
}
|
|
21686
|
+
if (method === "POST" && path === "/api/instances/chatgpt-accounts") {
|
|
21687
|
+
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json"))
|
|
21688
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
21689
|
+
const parsed = createClaudeAccountSchema.pick({ displayName: true }).safeParse(await readBody(req, 8192));
|
|
21690
|
+
if (!parsed.success)
|
|
21691
|
+
return json(res, 400, { error: "Enter an account name (up to 80 characters)." });
|
|
21692
|
+
if (providerConfigBusy)
|
|
21693
|
+
return json(res, 409, { error: "provider settings are already being updated" });
|
|
21694
|
+
providerConfigBusy = true;
|
|
21695
|
+
try {
|
|
21696
|
+
const instances = persistableInstanceConfigs(cfg);
|
|
21697
|
+
const instanceId = `chatgpt-${randomUUID()}`;
|
|
21698
|
+
// Reuse the selected executable, never another account's environment.
|
|
21699
|
+
const cli = instances.chatgpt?.config?.cli
|
|
21700
|
+
?? instances.codex?.config?.cli;
|
|
21701
|
+
instances[instanceId] = { driver: "codex", displayName: parsed.data.displayName,
|
|
21702
|
+
config: { authMode: "chatgpt-plan", ...(typeof cli === "string" && cli ? { cli } : {}) } };
|
|
21703
|
+
await persistProviderInstance(instanceId, instances);
|
|
21704
|
+
return json(res, 201, { instanceId, instances: await describeInstances() });
|
|
21705
|
+
}
|
|
21706
|
+
finally {
|
|
21707
|
+
providerConfigBusy = false;
|
|
21708
|
+
}
|
|
21709
|
+
}
|
|
20353
21710
|
if (method === "POST" && path === "/api/instances/claude-accounts") {
|
|
20354
21711
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
20355
21712
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
@@ -20400,6 +21757,11 @@ const handleRequest = async (req, res) => {
|
|
|
20400
21757
|
const instance = registry.get(instanceId);
|
|
20401
21758
|
if (!instance)
|
|
20402
21759
|
return json(res, 404, { error: "unknown instance" });
|
|
21760
|
+
// The official local OAuth callback terminates on this machine,
|
|
21761
|
+
// not a phone or the browser visiting a remotely hosted workspace.
|
|
21762
|
+
if (instance.authenticationMethod === "browser-pkce" && (isProxied(req) || !isLoopbackHost(req.socket.remoteAddress))) {
|
|
21763
|
+
return json(res, 403, { error: "Continue with ChatGPT on the computer running OpenMausBot. Hosted Pro sign-in requires OpenAI's hosted-app approval." });
|
|
21764
|
+
}
|
|
20403
21765
|
const started = await providerAuthSessions.start(instance, owner);
|
|
20404
21766
|
// Revocation can arrive while the CLI is obtaining a device code.
|
|
20405
21767
|
if (auth.kind === "session" && !sessions.isLive(auth.session.id)) {
|
|
@@ -20623,13 +21985,78 @@ const handleRequest = async (req, res) => {
|
|
|
20623
21985
|
res.once("close", disconnect);
|
|
20624
21986
|
mcpProbesInFlight += 1;
|
|
20625
21987
|
try {
|
|
20626
|
-
|
|
21988
|
+
const name = mcpTest[1];
|
|
21989
|
+
let target = parsed.server;
|
|
21990
|
+
if (isRemoteMcpServer(target) && mcpOAuth.authState(name, target.url) === "signed-in") {
|
|
21991
|
+
const signedIn = (await withMcpSignIn({ [name]: target }, mcpOAuth))[name];
|
|
21992
|
+
if (signedIn)
|
|
21993
|
+
target = { ...signedIn, enabled: target.enabled };
|
|
21994
|
+
}
|
|
21995
|
+
const result = await probeMcpServer(target, undefined, controller.signal);
|
|
21996
|
+
if (!result.ok && result.auth === "required" && isRemoteMcpServer(target))
|
|
21997
|
+
mcpOAuth.markNeedsSignIn(name, target.url);
|
|
21998
|
+
return json(res, 200, result);
|
|
20627
21999
|
}
|
|
20628
22000
|
finally {
|
|
20629
22001
|
res.off("close", disconnect);
|
|
20630
22002
|
mcpProbesInFlight -= 1;
|
|
20631
22003
|
}
|
|
20632
22004
|
}
|
|
22005
|
+
// The loopback listener and authenticated paste-back complete the same flow.
|
|
22006
|
+
const mcpSignIn = /^\/api\/mcp\/servers\/([a-z][a-z0-9_-]{0,31})\/(sign-in|sign-out)(?:\/([0-9a-f-]{36}))?$/.exec(path);
|
|
22007
|
+
if (mcpSignIn) {
|
|
22008
|
+
const [, name, action, flowId] = mcpSignIn;
|
|
22009
|
+
const raw = cfg.mcpServers?.[name];
|
|
22010
|
+
const parsed = raw === undefined ? null : parseStoredMcpServer(name, raw);
|
|
22011
|
+
if (!parsed?.ok)
|
|
22012
|
+
return json(res, 404, { error: "MCP server not found." });
|
|
22013
|
+
if (!isRemoteMcpServer(parsed.server))
|
|
22014
|
+
return json(res, 400, { error: "Only servers reached by URL can be signed in to." });
|
|
22015
|
+
const url = parsed.server.url;
|
|
22016
|
+
if (action === "sign-out" && method === "POST" && !flowId) {
|
|
22017
|
+
await mcpOAuth.signOut(name, url);
|
|
22018
|
+
return json(res, 200, mcpServerResponse());
|
|
22019
|
+
}
|
|
22020
|
+
const owner = auth.kind === "session" ? auth.session.id : "loopback";
|
|
22021
|
+
res.setHeader("Cache-Control", "no-store");
|
|
22022
|
+
try {
|
|
22023
|
+
if (action === "sign-in" && method === "GET" && flowId) {
|
|
22024
|
+
const status = mcpOAuth.status(name, flowId, owner);
|
|
22025
|
+
return status ? json(res, 200, { auth: status }) : json(res, 404, { error: "This sign-in is no longer available in this browser. Start again." });
|
|
22026
|
+
}
|
|
22027
|
+
if (action === "sign-in" && method === "DELETE") {
|
|
22028
|
+
mcpOAuth.cancelFlow(name, owner, flowId);
|
|
22029
|
+
return json(res, 200, { ok: true });
|
|
22030
|
+
}
|
|
22031
|
+
if (action === "sign-in" && method === "POST" && flowId) {
|
|
22032
|
+
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
22033
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
22034
|
+
}
|
|
22035
|
+
const body = await readBody(req, 20_480);
|
|
22036
|
+
if (typeof body?.callbackUrl !== "string")
|
|
22037
|
+
return json(res, 400, { error: "A complete callbackUrl is required." });
|
|
22038
|
+
return json(res, 200, { auth: await mcpOAuth.completeCallback(name, flowId, body.callbackUrl, owner) });
|
|
22039
|
+
}
|
|
22040
|
+
if (action === "sign-in" && method === "POST" && !flowId) {
|
|
22041
|
+
const started = await mcpOAuth.start(name, url, undefined, owner);
|
|
22042
|
+
if (auth.kind === "session" && !sessions.isLive(auth.session.id)) {
|
|
22043
|
+
mcpOAuth.revokeOwner(owner);
|
|
22044
|
+
return json(res, 401, { error: "Your session ended. Start a new sign-in." });
|
|
22045
|
+
}
|
|
22046
|
+
return json(res, 200, { auth: started });
|
|
22047
|
+
}
|
|
22048
|
+
}
|
|
22049
|
+
catch (error) {
|
|
22050
|
+
if (error instanceof McpSignInError)
|
|
22051
|
+
return json(res, error.status, { error: error.message });
|
|
22052
|
+
if (error instanceof McpOAuthError)
|
|
22053
|
+
return json(res, 400, { error: error.message, code: error.code });
|
|
22054
|
+
if (method === "POST" && !flowId)
|
|
22055
|
+
return json(res, 502, { error: error instanceof Error ? error.message : "Sign-in could not start." });
|
|
22056
|
+
throw error;
|
|
22057
|
+
}
|
|
22058
|
+
return json(res, 405, { error: "method not allowed" });
|
|
22059
|
+
}
|
|
20633
22060
|
if (method === "POST" && path === "/api/mcp/servers") {
|
|
20634
22061
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
20635
22062
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
@@ -20715,6 +22142,7 @@ const handleRequest = async (req, res) => {
|
|
|
20715
22142
|
const next = { ...current };
|
|
20716
22143
|
delete next[name];
|
|
20717
22144
|
persistMcpServers(next);
|
|
22145
|
+
mcpOAuth.forget(name);
|
|
20718
22146
|
return json(res, 200, mcpServerResponse());
|
|
20719
22147
|
}
|
|
20720
22148
|
const existing = parseStoredMcpServer(name, current[name]);
|
|
@@ -20736,6 +22164,17 @@ const handleRequest = async (req, res) => {
|
|
|
20736
22164
|
if (refusal)
|
|
20737
22165
|
return json(res, 403, { error: refusal, code: "managed_policy" });
|
|
20738
22166
|
persistMcpServers({ ...current, [name]: parsed.server });
|
|
22167
|
+
// a sign-in belongs to one address; a token never follows the entry elsewhere
|
|
22168
|
+
const before = isRemoteMcpServer(existing.server) ? existing.server.url : null;
|
|
22169
|
+
const after = isRemoteMcpServer(parsed.server) ? parsed.server.url : null;
|
|
22170
|
+
// ...and to one sign-in app with its scopes; a new secret for the same app keeps it
|
|
22171
|
+
const app = (server) => isRemoteMcpServer(server) && server.oauth
|
|
22172
|
+
? JSON.stringify([server.oauth.clientId, server.oauth.scopes ?? []]) : null;
|
|
22173
|
+
// Any other edit (a new header, say) drops a sign-in request that has
|
|
22174
|
+
// no tokens behind it; the next Test asks again if it still applies.
|
|
22175
|
+
if (before !== after || app(existing.server) !== app(parsed.server)
|
|
22176
|
+
|| (after && mcpOAuth.authState(name, after) === "needs-sign-in"))
|
|
22177
|
+
mcpOAuth.forget(name);
|
|
20739
22178
|
return json(res, 200, mcpServerResponse());
|
|
20740
22179
|
}
|
|
20741
22180
|
finally {
|
|
@@ -20748,9 +22187,13 @@ const handleRequest = async (req, res) => {
|
|
|
20748
22187
|
}
|
|
20749
22188
|
if ((method === "PUT" || method === "PATCH") && path === "/api/config") {
|
|
20750
22189
|
const body = await readBody(req);
|
|
20751
|
-
if (hostedModels && ["instances", "anthropic", "openaiCompat", "xai", "mistral", "opencodeGo"].some(key => Object.hasOwn(body, key)))
|
|
22190
|
+
if (hostedModels && ["instances", "anthropic", "openai", "openrouter", "openaiCompat", "xai", "mistral", "opencodeGo"].some(key => Object.hasOwn(body, key)))
|
|
20752
22191
|
return json(res, 403, { error: HOSTED_PROVIDER_SETTINGS_ERROR });
|
|
20753
22192
|
const patch = parseConfigPatch(body);
|
|
22193
|
+
// A Cloud home is personal: nobody is invited to sign in to it.
|
|
22194
|
+
if (CLOUD_HOME && (patch.signIn?.admins?.length || patch.signIn?.members?.length)) {
|
|
22195
|
+
return json(res, 403, { error: CLOUD_PERSONAL_REFUSAL, code: "cloud_personal" });
|
|
22196
|
+
}
|
|
20754
22197
|
if (patch.newBotDefaults) {
|
|
20755
22198
|
if (patch.newBotDefaults.profile.modelSelection) {
|
|
20756
22199
|
const checked = checkedModelSelection(patch.newBotDefaults.profile.modelSelection);
|
|
@@ -20783,6 +22226,14 @@ const handleRequest = async (req, res) => {
|
|
|
20783
22226
|
}
|
|
20784
22227
|
if (!Object.keys(patch).length)
|
|
20785
22228
|
return json(res, 400, { error: "nothing to save" });
|
|
22229
|
+
if (patch.decider) {
|
|
22230
|
+
// Saving a key is the "turn it on"; clearing it turns it off, or
|
|
22231
|
+
// falls back to Cloud Pro's included decisions where there are some.
|
|
22232
|
+
const planned = deciderSavePatch(patch.decider, cfg.decider, deciderIncludedHere());
|
|
22233
|
+
if (!planned.ok)
|
|
22234
|
+
return json(res, 400, { error: planned.error });
|
|
22235
|
+
patch.decider = planned.patch;
|
|
22236
|
+
}
|
|
20786
22237
|
const changingVoiceProvider = patch.tts?.provider !== undefined
|
|
20787
22238
|
&& patch.tts.provider !== tts.voiceProvider(cfg);
|
|
20788
22239
|
if (changingVoiceProvider && patch.tts?.voice === undefined) {
|
|
@@ -21072,6 +22523,15 @@ const handleRequest = async (req, res) => {
|
|
|
21072
22523
|
if (!check.ok)
|
|
21073
22524
|
return json(res, 400, { error: check.message });
|
|
21074
22525
|
}
|
|
22526
|
+
// A pasted decision-model key is tried once (one tiny yes/no call).
|
|
22527
|
+
// Only a refusal blocks the save: an offline or overloaded service must
|
|
22528
|
+
// not stop a person saving a key that may well be fine.
|
|
22529
|
+
if (patch.decider?.key) {
|
|
22530
|
+
const check = await decider.testKey({ key: patch.decider.key });
|
|
22531
|
+
if (!check.ok && check.reason === "rejected") {
|
|
22532
|
+
return json(res, 400, { error: "Jev rejected this key. Check it at typesafe.ai and paste it again.", code: "decider_rejected" });
|
|
22533
|
+
}
|
|
22534
|
+
}
|
|
21075
22535
|
if (patch.browserProfiles !== undefined) {
|
|
21076
22536
|
// Provider/credential validation above may await the network. A turn
|
|
21077
22537
|
// can start during that window and claim a profile which looked idle
|
|
@@ -21139,6 +22599,8 @@ const handleRequest = async (req, res) => {
|
|
|
21139
22599
|
persisted.tts.key = "";
|
|
21140
22600
|
if (persisted.tts?.fishKey !== undefined)
|
|
21141
22601
|
persisted.tts.fishKey = "";
|
|
22602
|
+
if (persisted.decider?.key !== undefined)
|
|
22603
|
+
persisted.decider.key = "";
|
|
21142
22604
|
if (persisted.imageGen?.key !== undefined)
|
|
21143
22605
|
persisted.imageGen.key = "";
|
|
21144
22606
|
if (persisted.imageGen?.customApiKey !== undefined)
|
|
@@ -21171,10 +22633,18 @@ const handleRequest = async (req, res) => {
|
|
|
21171
22633
|
}
|
|
21172
22634
|
throw error;
|
|
21173
22635
|
}
|
|
22636
|
+
// Desktops already counting down move to the new window at once, in
|
|
22637
|
+
// every isolation mode; busy work still defers its expiry.
|
|
22638
|
+
if (patch.localVm?.idleTimeoutMinutes !== undefined) {
|
|
22639
|
+
for (const idle of localVmIdles.values())
|
|
22640
|
+
idle.setIdleMs(localVmIdleMs());
|
|
22641
|
+
}
|
|
21174
22642
|
let browserReferenceCleanupError = null;
|
|
21175
|
-
if (patch.signIn !== undefined)
|
|
22643
|
+
if (patch.signIn !== undefined) {
|
|
21176
22644
|
sessions.revalidateEmailSessions();
|
|
21177
|
-
|
|
22645
|
+
resyncOpenCodeProviderKeys();
|
|
22646
|
+
}
|
|
22647
|
+
if (!lendingEnabled()) {
|
|
21178
22648
|
sharedComputers.close();
|
|
21179
22649
|
sharedComputerControl.close();
|
|
21180
22650
|
}
|
|
@@ -21634,6 +23104,10 @@ const handleRequest = async (req, res) => {
|
|
|
21634
23104
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
21635
23105
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
21636
23106
|
}
|
|
23107
|
+
if (m[2] === "join" && bot.cloudBackend === "vps") {
|
|
23108
|
+
for (let pending; (pending = vpsJoinRequests.get(botId));)
|
|
23109
|
+
await pending;
|
|
23110
|
+
}
|
|
21637
23111
|
const remoteProvider = bot.cloudBackend === "vps" ? "vps" : "box";
|
|
21638
23112
|
if (computerProviderConfigTransitions.has(remoteProvider)) {
|
|
21639
23113
|
return json(res, 409, { error: providerTransitionMessage(remoteProvider) });
|
|
@@ -21681,6 +23155,10 @@ const handleRequest = async (req, res) => {
|
|
|
21681
23155
|
// Opening the existing SSH viewer can coexist with a capture. Start,
|
|
21682
23156
|
// stop, remove and Settings deletion still exclude pending previews.
|
|
21683
23157
|
const releaseComputerLifecycle = claimBotComputerLifecycle(botId, m[2] === "join");
|
|
23158
|
+
// Settles only after the lane is released, so a waiter can claim it.
|
|
23159
|
+
let joinSettled;
|
|
23160
|
+
if (m[2] === "join")
|
|
23161
|
+
vpsJoinRequests.set(botId, new Promise(resolve => { joinSettled = resolve; }));
|
|
21684
23162
|
try {
|
|
21685
23163
|
if (m[2] === "exec") {
|
|
21686
23164
|
return json(res, 409, { error: "the VPS console is available to the bot through its scoped computer tools" });
|
|
@@ -21692,13 +23170,20 @@ const handleRequest = async (req, res) => {
|
|
|
21692
23170
|
return json(res, 409, { error: "the VPS computer is being used by this bot — interrupt the turn first" });
|
|
21693
23171
|
}
|
|
21694
23172
|
if (m[2] === "join") {
|
|
21695
|
-
|
|
23173
|
+
const remote = auth.kind !== "loopback";
|
|
23174
|
+
const result = await vps.vpsComputerJoin(cfg, botId, undefined, remote);
|
|
23175
|
+
return json(res, 200, { ...result, joinUrl: remote
|
|
23176
|
+
? desktopViewerUrl(`vps/${botId}`, bot.threadId) : result.joinUrl });
|
|
21696
23177
|
}
|
|
21697
23178
|
const action = m[2] === "provision" ? "provision" : m[2] === "remove" ? "remove" : "stop";
|
|
21698
23179
|
return json(res, 200, await vps.vpsComputerAction(action, cfg, botId));
|
|
21699
23180
|
}
|
|
21700
23181
|
finally {
|
|
21701
23182
|
releaseComputerLifecycle();
|
|
23183
|
+
if (joinSettled) {
|
|
23184
|
+
vpsJoinRequests.delete(botId);
|
|
23185
|
+
joinSettled();
|
|
23186
|
+
}
|
|
21702
23187
|
}
|
|
21703
23188
|
}
|
|
21704
23189
|
const activeBoatTurn = botHasActiveTurn(botId);
|
|
@@ -21763,6 +23248,7 @@ const handleRequest = async (req, res) => {
|
|
|
21763
23248
|
}
|
|
21764
23249
|
};
|
|
21765
23250
|
const server = createServer(handleRequest);
|
|
23251
|
+
desktopViewer.attach(server, handleRequest);
|
|
21766
23252
|
calendarCalls.start();
|
|
21767
23253
|
// Resolve the edition before accepting requests so /api/edition is never a guess.
|
|
21768
23254
|
console.log(describeEdition(await loadEnterpriseLayer()));
|
|
@@ -21943,6 +23429,7 @@ if (TUNNEL_SOCKET) {
|
|
|
21943
23429
|
if (process.platform !== "win32")
|
|
21944
23430
|
rmSync(TUNNEL_SOCKET, { force: true });
|
|
21945
23431
|
tunnelListener = createServer(handleRequest);
|
|
23432
|
+
desktopViewer.attach(tunnelListener, handleRequest);
|
|
21946
23433
|
tunnelListener.listen(TUNNEL_SOCKET, () => {
|
|
21947
23434
|
console.log(`openmausbot tunnel listener on ${TUNNEL_SOCKET}`);
|
|
21948
23435
|
});
|
|
@@ -21961,6 +23448,7 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
21961
23448
|
sharedComputers.close();
|
|
21962
23449
|
sharedComputerControl.close();
|
|
21963
23450
|
browserLive.closeAll();
|
|
23451
|
+
desktopViewer.closeAll();
|
|
21964
23452
|
for (const idle of localVmIdles.values())
|
|
21965
23453
|
idle.cancel();
|
|
21966
23454
|
vps.closeAllVpsDesktopTunnels();
|
|
@@ -21997,8 +23485,10 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
21997
23485
|
code = 1;
|
|
21998
23486
|
}
|
|
21999
23487
|
closeMessageDb();
|
|
23488
|
+
mcpOAuth.dispose();
|
|
22000
23489
|
releaseDataDirLeaseAtExit();
|
|
22001
|
-
|
|
23490
|
+
// A Cloud home's launcher starts the server again on this code only.
|
|
23491
|
+
process.exit(cloudHomeRestartRequested && code === 0 ? CLOUD_HOME_RESTART_EXIT_CODE : code);
|
|
22002
23492
|
},
|
|
22003
23493
|
});
|
|
22004
23494
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|