openmausbot 0.1.91 → 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-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
- package/dist/assets/index-Cdmjrl28.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
- package/dist/assets/index-DR7YPMkW.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.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 +112 -14
- package/dist-server/container-mcp.js +195 -15
- package/dist-server/drivers/agents-proxy.js +5 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +9300 -4155
- 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 +3910 -1182
- package/dist-server/pair-cli.js +3910 -1182
- package/dist-server/server/boat.js +2 -2
- 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 +118 -26
- package/dist-server/server/cloud-home.js +39 -3
- package/dist-server/server/cloud-lending.js +74 -10
- 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 +174 -14
- package/dist-server/server/container-computer.js +3 -2
- 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 +6 -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/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +312 -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/harness/registry.js +2 -2
- package/dist-server/server/index.js +1338 -184
- 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/provider-key-check.js +4 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +23 -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/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/skills.js +25 -10
- package/dist-server/server/store.js +62 -122
- 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/fish.js +4 -5
- package/dist-server/server/tts/index.js +5 -3
- 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 +3 -0
- package/dist-server/server/workspace-backup.js +3 -0
- package/dist-server/server/workspace.js +65 -15
- 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 +196 -15
- package/dist-server/workspace-backup.worker.js +73 -9
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-DrfGMu7X.js +0 -1
- package/dist/assets/index-BlnCEcU6.js +0 -588
- package/dist/assets/index-DVxjOvID.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.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";
|
|
@@ -16,7 +19,8 @@ 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
21
|
import { provenRequestPerson, SharedComputers, sharedComputerOperation, sharedComputerRegistration } from "./shared-computers.js";
|
|
19
|
-
import { cloudHomeLendingRefusal, createCloudRoutineAuthors } from "./cloud-lending.js";
|
|
22
|
+
import { cloudHomeLendingRefusal, createCloudRoutineAuthors, ownerOnlyConversation } from "./cloud-lending.js";
|
|
23
|
+
import { botMemoryFiles, createLendingMemory } from "./lending-memory.js";
|
|
20
24
|
import { SharedComputerControl } from "./shared-computer-control.js";
|
|
21
25
|
import { RoomHandoffs } from "./room-handoffs.js";
|
|
22
26
|
import { assertRequestTarget, guardedRequestPath, requestConflict, requestNeedsInput, requestSourceForCard } from "./guarded-requests.js";
|
|
@@ -61,7 +65,7 @@ import { canAccessTeam, canReachPeer, coordinatorSupervises, livePeerRoster, liv
|
|
|
61
65
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
62
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";
|
|
63
67
|
import { attachForTurn, saveBotAttachment } from "./bot-attachment.js";
|
|
64
|
-
import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, routinesInConversationEnabled, claudeUserMcpEnabled, skillAuthoringEnabled, autoRecallEnabled, captureQuietMs, tidyHour, sharedComputersEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, computerClaimIdleReleaseEnabled, cloudOverflowAllowlistedThreads, cloudOverflowEnabled, cloudOverflowIdleStopMs, cloudOverflowPerSecondCostUsd, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, stripWorkspaceCredentialEnv, PROVIDER_CREDENTIAL_ENV, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, } from "./config.js";
|
|
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";
|
|
65
69
|
import { sweepThreadEventLogs } from "./thread-retention.js";
|
|
66
70
|
import { ComputerControl } from "./computer-control.js";
|
|
67
71
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -80,10 +84,12 @@ import { isModelVariant, TurnNotStartedError, newId, } from "./contracts.js";
|
|
|
80
84
|
import { RETRY_MAX_ATTEMPTS } from "./drivers/retry.js";
|
|
81
85
|
import { recoveryCapabilityError } from "./automatic-recovery.js";
|
|
82
86
|
import { decodeGeneratedImage } from "./generated-image.js";
|
|
83
|
-
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";
|
|
84
89
|
import { probeMcpServer } from "./mcp-probe.js";
|
|
85
90
|
import { GROUP_GOAL_MAX_TURNS, groupGoalAssignmentKey, groupGoalCompletionTurnId, groupGoalCoordinatorInstructions, groupGoalWorkerInstructions, parseGroupGoalDecision, resolveGroupGoalMember, selectGroupGoalCoordinator, } from "./group-goal-run.js";
|
|
86
91
|
import { BUILT_IN_DRIVERS } from "./drivers/builtIn.js";
|
|
92
|
+
import { openCodeProviderKeysAllowed, setOpenCodeProviderKeyPolicy } from "./drivers/acp/opencode-go.js";
|
|
87
93
|
import { getOrCreateChannel, mirrorActivity, mirrorExchange, mirrorReply } from "./comms-visibility.js";
|
|
88
94
|
import { readMessageText, recallMessages, recentMessages, searchMessagesAsync, closeMessageSearch, closeMessageDb, chatFollowups, cancelledChatFollowup, settleChatFollowups, threadsReferencing } from "./message-db.js";
|
|
89
95
|
import { briefCrossingLabel, claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.js";
|
|
@@ -104,7 +110,8 @@ import { ProviderRegistry } from "./harness/registry.js";
|
|
|
104
110
|
import { ManagedDesktopProviders } from "./managed-desktop.js";
|
|
105
111
|
import { computerKindForResource, ManagedDesktopPolicy } from "./managed-policy.js";
|
|
106
112
|
import { hostedModelPolicy, HOSTED_MODEL_POLICY_HEADER, HOSTED_PROVIDER_SETTINGS_ERROR } from "./hosted-models.js";
|
|
107
|
-
import { boatNotConfiguredMessage, CLOUD_HOME_RESTART_EXIT_CODE, CLOUD_IGNORED_KEYS, CLOUD_PAIRING_PATH, cloudHomeConfiguration, cloudHomeOffersPlace, cloudHomePlaceRefusal, 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";
|
|
108
115
|
import { createCloudMoveRoutes } from "./cloud-move-http.js";
|
|
109
116
|
import { holdIncludedServices } from "./included-services.js";
|
|
110
117
|
import { selectDefaultModelSelection, withNewBotEffort } from "./default-model-selection.js";
|
|
@@ -120,10 +127,11 @@ import { narrateTool, toUtterances } from "./tts/speech-text.js";
|
|
|
120
127
|
import { buildRecoveryText, buildTurnContext, engineIsFresh, NATIVELY_REPLAYING_DRIVER_KINDS, peerMessageText } from "./turn-context.js";
|
|
121
128
|
import { Handoffs, handedStateUsable, recordHanded, renderUnseen, sessionStart, unseenMessages, withUnseenMessages } from "./delta-context.js";
|
|
122
129
|
import { extractTurnImages } from "./turn-images.js";
|
|
130
|
+
import { threadTitlePrompt, titleConversationExcerpt } from "./thread-title.js";
|
|
123
131
|
import { TurnWatchdog } from "./turn-watchdog.js";
|
|
124
132
|
import { TurnResources, workspaceResource } from "./turn-resources.js";
|
|
125
133
|
import { IdleReleasePolicy } from "./claim-idle.js";
|
|
126
|
-
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";
|
|
127
135
|
import { listMemoryTopics, memoryDate, readMemoryFile, readMemoryTopic, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
|
|
128
136
|
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, journalMemoryWrite, } from "./memory-journal.js";
|
|
129
137
|
import { readSectionContext, readSections, sectionContextKey, sectionContextLabel, sectionContextSystemPrompt, writeSectionContext, SECTION_CONTEXT_MAX_BYTES, } from "./section-context.js";
|
|
@@ -146,12 +154,12 @@ import * as vps from "./vps-computer.js";
|
|
|
146
154
|
import { RoutineManager } from "./routines.js";
|
|
147
155
|
import { CalendarCallManager } from "./calendar-calls.js";
|
|
148
156
|
import { BUILT_IN_BROWSER_SYSTEM_PROMPT } from "./browser-engine.js";
|
|
149
|
-
import { BrowserRuntime } from "./browser-runtime.js";
|
|
157
|
+
import { BrowserRuntime, browserRuntimeEnv } from "./browser-runtime.js";
|
|
150
158
|
import { BrowserLive } from "./browser-live.js";
|
|
151
|
-
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";
|
|
152
160
|
import { createScreenFrameSource } from "./screen-frame-source.js";
|
|
153
161
|
import { screenFrameHash, screenSurfaceForTool, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
|
|
154
|
-
import { RoutineRequestService } from "./routine-requests.js";
|
|
162
|
+
import { asSchedule, RoutineRequestService } from "./routine-requests.js";
|
|
155
163
|
import { createOptionsCard } from "./options-card.js";
|
|
156
164
|
import { buildBotOverview, connectedAppsFacts } from "./bot-overview.js";
|
|
157
165
|
import { ProfileRequestService } from "./profile-requests.js";
|
|
@@ -214,6 +222,10 @@ import { createHostedSlackRoutes } from "./routes/hosted-slack.js";
|
|
|
214
222
|
import { createBotPresetRoutes } from "./routes/bot-presets.js";
|
|
215
223
|
import { createBotMemoryRoutes } from "./routes/bot-memory.js";
|
|
216
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";
|
|
217
229
|
const PORT = Number(process.env.OMB_PORT || process.env.OGB_PORT || 8799);
|
|
218
230
|
const WEBHOOK_PORT = Number(process.env.OMB_WEBHOOK_PORT || PORT + 1);
|
|
219
231
|
// Behind a proxy or tunnel, the base URL senders should use (docs/self-hosting.md).
|
|
@@ -223,6 +235,7 @@ const MIME = {
|
|
|
223
235
|
".html": "text/html",
|
|
224
236
|
".js": "text/javascript",
|
|
225
237
|
".css": "text/css",
|
|
238
|
+
".txt": "text/plain; charset=utf-8",
|
|
226
239
|
".svg": "image/svg+xml",
|
|
227
240
|
".png": "image/png",
|
|
228
241
|
".ico": "image/x-icon",
|
|
@@ -252,8 +265,12 @@ process.once("exit", releaseDataDirLeaseAtExit);
|
|
|
252
265
|
// Restore before constructing any long-lived config, Store, session or provider
|
|
253
266
|
// objects. Replacing files underneath a live Store would overwrite restored data.
|
|
254
267
|
let workspaceRestore = { restored: false };
|
|
268
|
+
/** A restore applied at this very start (not the last one on record). */
|
|
269
|
+
let restoredAtStart;
|
|
255
270
|
if (existsSync(join(DATA_DIR, ".backups"))) {
|
|
256
271
|
workspaceRestore = applyPendingWorkspaceRestore(DATA_DIR);
|
|
272
|
+
if (workspaceRestore.restored)
|
|
273
|
+
restoredAtStart = workspaceRestore.id;
|
|
257
274
|
if (!workspaceRestore.restored && !workspaceRestore.rolledBack) {
|
|
258
275
|
workspaceRestore = readLastWorkspaceRestore(DATA_DIR) ?? workspaceRestore;
|
|
259
276
|
}
|
|
@@ -279,7 +296,16 @@ const sessions = new SessionRegistry({
|
|
|
279
296
|
const sharedComputers = new SharedComputers(id => sessions.isLive(id));
|
|
280
297
|
// OMB Cloud Pro home machine (server/cloud-home.ts, docs/cloud-pro.md). A
|
|
281
298
|
// partial or invalid boot contract stops the server here, before it serves.
|
|
282
|
-
|
|
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);
|
|
283
309
|
/** Lending a computer to this server (the shared-computer routes, the two
|
|
284
310
|
* agent tools, the advertised capability): the maintainer flag anywhere, and
|
|
285
311
|
* always on an OMB Cloud home, where it is the person's own Mac lent to their
|
|
@@ -291,6 +317,29 @@ const CLOUD_HOME_LENDER = "cloud-home";
|
|
|
291
317
|
* the owner started: the only routine turns that may use a lent Mac. */
|
|
292
318
|
const cloudRoutineAuthors = CLOUD_HOME ? createCloudRoutineAuthors(join(DATA_DIR, "lending-routines.json")) : null;
|
|
293
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();
|
|
294
343
|
const cloudPairing = CLOUD_HOME ? createCloudPairing({ secret: CLOUD_HOME.bootstrapSecret, sessions }) : null;
|
|
295
344
|
if (CLOUD_HOME) {
|
|
296
345
|
// The signing secret is held in memory from here on, and a platform
|
|
@@ -305,7 +354,12 @@ if (CLOUD_HOME) {
|
|
|
305
354
|
}
|
|
306
355
|
// Cloud Pro's included Boat, voice and decision relay tokens, when the Admin
|
|
307
356
|
// set them: held in memory from here on, like the signing secret.
|
|
308
|
-
holdIncludedServices();
|
|
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
|
+
}
|
|
309
363
|
// Who each thread is for, when a signed-in person can be named (server-private).
|
|
310
364
|
const threadStarters = new ThreadStarters(join(DATA_DIR, "thread-starters.json"));
|
|
311
365
|
const commandAllowlist = new CommandAllowlistStore(join(DATA_DIR, "command-allowlist.json"));
|
|
@@ -318,7 +372,7 @@ const sharedWorkspaceFullAccessEnabled = () => SHARED_WORKSPACE_FULL_ACCESS && B
|
|
|
318
372
|
// Who a loopback request without a session is (server/request-auth.ts
|
|
319
373
|
// LoopbackTrust): the owner on a desktop or a one-person server; a service on
|
|
320
374
|
// a shared workspace, where every bot's shell is a loopback caller too.
|
|
321
|
-
const LOOPBACK = resolveLoopbackTrust({ desktopManaged: DESKTOP_MANAGED, hostedWorkspace: HOSTED_WORKSPACE });
|
|
375
|
+
const LOOPBACK = resolveLoopbackTrust({ desktopManaged: DESKTOP_MANAGED, hostedWorkspace: HOSTED_WORKSPACE, cloudHome: Boolean(CLOUD_HOME) });
|
|
322
376
|
// `openmausbot serve` on a service-trust server hands the server it starts a
|
|
323
377
|
// per-launch secret on stdin, then closes it (server/cli.ts). It opens only
|
|
324
378
|
// the pairing route, for that CLI. Never an environment variable: every
|
|
@@ -416,7 +470,7 @@ function messageSender(auth) {
|
|
|
416
470
|
if (auth.kind !== "session")
|
|
417
471
|
return undefined;
|
|
418
472
|
const name = (auth.session.email ?? auth.session.label ?? "").trim();
|
|
419
|
-
return name ? { name, id:
|
|
473
|
+
return name ? { name, id: actorKey(auth) } : undefined;
|
|
420
474
|
}
|
|
421
475
|
/** An opaque, stable key for the person behind a session: their account
|
|
422
476
|
* email when they signed in with one (a new device is still them), else the
|
|
@@ -474,7 +528,7 @@ function sharedComputerPrincipal(capability) {
|
|
|
474
528
|
// their devices, or a routine they wrote. Never a guest, a webhook, a room
|
|
475
529
|
// or a line someone else slipped in.
|
|
476
530
|
if (CLOUD_HOME)
|
|
477
|
-
return
|
|
531
|
+
return cloudLendingRefusal(capability) === null ? CLOUD_HOME_LENDER : null;
|
|
478
532
|
return provenRequestPerson(directRequestOwners.get(capability.threadId), capability.generation, (messageId) => linePersonKey(store.messagesFor(capability.threadId).find((message) => message.id === messageId)));
|
|
479
533
|
}
|
|
480
534
|
/** What the lending rule needs to know about one bot turn on a Cloud home. */
|
|
@@ -483,7 +537,9 @@ function cloudLendingTurn(capability) {
|
|
|
483
537
|
request: directRequestOwners.get(capability.threadId),
|
|
484
538
|
generation: capability.generation,
|
|
485
539
|
thread: store.messagesFor(capability.threadId),
|
|
486
|
-
|
|
540
|
+
starters: cloudThreadStarters(capability.threadId),
|
|
541
|
+
reportsFromOthers: store.messagesFor(capability.threadId).some(cloudOthersRoutineReport),
|
|
542
|
+
ownerPerson: cloudProvenOwnerPerson,
|
|
487
543
|
cardAnswerer: cloudCardAnswerer,
|
|
488
544
|
routineRun: () => {
|
|
489
545
|
const run = activeRoutineRunForThread(capability.threadId);
|
|
@@ -493,9 +549,7 @@ function cloudLendingTurn(capability) {
|
|
|
493
549
|
triggerSource: run.triggerSource ?? (run.manual ? "manual" : "schedule"),
|
|
494
550
|
ownerStarted: ownerStartedRoutineRuns.has(run.id),
|
|
495
551
|
// What the run snapshotted, on the schedule its routine has now.
|
|
496
|
-
ownerAuthored: (
|
|
497
|
-
...routine, prompt: run.prompt, target: run.target, botId: run.botId, groupId: run.groupId, attachments: run.attachments, runOn: run.runOn,
|
|
498
|
-
}) === true)(routines?.listRoutines().find((candidate) => candidate.id === run.routineId)),
|
|
552
|
+
ownerAuthored: cloudRoutineRunIsOwners(run, capability.botId, capability.threadId),
|
|
499
553
|
};
|
|
500
554
|
},
|
|
501
555
|
};
|
|
@@ -503,6 +557,82 @@ function cloudLendingTurn(capability) {
|
|
|
503
557
|
/** What a bot on a Cloud home is told when its conversation cannot use the
|
|
504
558
|
* owner's lent Mac because someone else wrote in it. The bot relays it. */
|
|
505
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
|
+
}
|
|
506
636
|
/** Whose lent computers a status reader may see: on a Cloud home, only the
|
|
507
637
|
* owner's own devices (admin sessions) see the lent Mac, never a guest or a
|
|
508
638
|
* local process; elsewhere, the person behind the asking session. */
|
|
@@ -511,6 +641,239 @@ function sessionLendingPrincipal(auth) {
|
|
|
511
641
|
return cloudOwnerSession(auth) ? CLOUD_HOME_LENDER : null;
|
|
512
642
|
return auth.kind === "session" ? personKey(auth.session) : null;
|
|
513
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
|
+
}
|
|
514
877
|
/** On a Cloud home: a request from one of the owner's own devices. The
|
|
515
878
|
* Admin's signed pairing gives the desktop an admin session; a guest the
|
|
516
879
|
* owner pairs with chat-only access has client scope. Loopback is not the
|
|
@@ -519,10 +882,31 @@ function sessionLendingPrincipal(auth) {
|
|
|
519
882
|
function cloudOwnerSession(auth) {
|
|
520
883
|
return auth.kind === "session" && auth.scopes.includes("admin");
|
|
521
884
|
}
|
|
522
|
-
/** On a Cloud home: whether a
|
|
523
|
-
*
|
|
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). */
|
|
524
889
|
function cloudOwnerPerson(person) {
|
|
525
|
-
|
|
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);
|
|
526
910
|
}
|
|
527
911
|
/** On a Cloud home, a session that is not one of the owner's own devices may
|
|
528
912
|
* not answer a card of any kind. */
|
|
@@ -594,7 +978,7 @@ function cardAnswererFor(auth) {
|
|
|
594
978
|
if (auth.kind === "loopback")
|
|
595
979
|
return auth.trust === "service" ? { kind: "worker" } : { kind: "loopback" };
|
|
596
980
|
return { kind: "session", name: (auth.session.email ?? auth.session.label ?? "").trim() || "Signed-in user",
|
|
597
|
-
...(CLOUD_HOME ? { person:
|
|
981
|
+
...(CLOUD_HOME ? { person: actorKey(auth) } : {}) };
|
|
598
982
|
}
|
|
599
983
|
/** On a Cloud home, who is answering a card right now, by request id: the
|
|
600
984
|
* answer reaches the turn before `answeredBy` is written, and a lending turn
|
|
@@ -616,7 +1000,7 @@ async function answeringCardAs(auth, threadId, requestId, work) {
|
|
|
616
1000
|
return Boolean(card && !card.answered && !card.dismissed && !card.expired);
|
|
617
1001
|
})();
|
|
618
1002
|
if (CLOUD_HOME && open)
|
|
619
|
-
cloudCardAnswersInFlight.set(requestId, auth.kind === "session" ?
|
|
1003
|
+
cloudCardAnswersInFlight.set(requestId, auth.kind === "session" ? actorKey(auth) : undefined);
|
|
620
1004
|
try {
|
|
621
1005
|
await withDecisionActor(decisionActorFor(auth), work);
|
|
622
1006
|
}
|
|
@@ -743,6 +1127,11 @@ function settleRoomFloor(group) {
|
|
|
743
1127
|
return;
|
|
744
1128
|
store.patchGroup(group.id, { audienceFloor: next === "everyone" ? undefined : next });
|
|
745
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
|
+
}
|
|
746
1135
|
/** The conversations a bot's recent-work brief may draw on: its own, and the
|
|
747
1136
|
* rooms roomFeedsBot allows. */
|
|
748
1137
|
function recentWorkSources(bot) {
|
|
@@ -757,7 +1146,7 @@ function recentWorkSources(bot) {
|
|
|
757
1146
|
* or a room must not be able to pull a private chat into its reply. The
|
|
758
1147
|
* conversations are the ones session_search would search, minus this one. */
|
|
759
1148
|
function autoRecallPrompt(bot, threadId, message, opts) {
|
|
760
|
-
if (!autoRecallEnabled(cfg))
|
|
1149
|
+
if (bot.memoryEnabled === false || !autoRecallEnabled(cfg))
|
|
761
1150
|
return "";
|
|
762
1151
|
const roomByThread = new Map();
|
|
763
1152
|
if (opts.conversations) {
|
|
@@ -770,7 +1159,10 @@ function autoRecallPrompt(bot, threadId, message, opts) {
|
|
|
770
1159
|
}
|
|
771
1160
|
}
|
|
772
1161
|
const threadIds = opts.conversations
|
|
773
|
-
? [...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)))
|
|
774
1166
|
: [];
|
|
775
1167
|
try {
|
|
776
1168
|
const recalled = buildRecall({
|
|
@@ -902,11 +1294,13 @@ function adminAuditPlan(method, path) {
|
|
|
902
1294
|
m = /^\/api\/groups\/([\w-]+)$/.exec(path);
|
|
903
1295
|
if (m && method === "PATCH")
|
|
904
1296
|
plan.roomId = m[1];
|
|
905
|
-
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);
|
|
906
1298
|
if (m && method === "POST")
|
|
907
1299
|
plan.engineAction = { id: m[1], action: m[2].replace("/", "-") };
|
|
908
1300
|
if (method === "POST" && path === "/api/instances/claude-accounts")
|
|
909
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" };
|
|
910
1304
|
return plan.config || plan.botId || plan.createsBots || plan.deletesBot || plan.createsWebhook || plan.webhookId || plan.sessionId || plan.pairing || plan.engineAction || plan.roomId ? plan : null;
|
|
911
1305
|
}
|
|
912
1306
|
const WEBHOOK_AUDIT_FIELDS = ["name", "prompt", "botId", "runOn", "enabled", "eventTypes"];
|
|
@@ -1081,6 +1475,46 @@ function queuedTurnTrigger(item) {
|
|
|
1081
1475
|
return item.sender ? { kind: "user", label: item.sender.name } : { kind: "owner" };
|
|
1082
1476
|
}
|
|
1083
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
|
+
}
|
|
1084
1518
|
await registry.load(providerConfigs(), decorateHostedProvider);
|
|
1085
1519
|
const bundledSkills = loadBundledSkills();
|
|
1086
1520
|
const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
|
|
@@ -1179,7 +1613,10 @@ let companyRuntimeReady;
|
|
|
1179
1613
|
const companyRuntimeStarted = new Promise(resolve => { companyRuntimeReady = resolve; });
|
|
1180
1614
|
/** The enrolled organisation's desktop policy: in memory only, read-only, and
|
|
1181
1615
|
* inert (null) unless Electron's enrolled parent sends one. */
|
|
1182
|
-
const managedPolicy = new ManagedDesktopPolicy({ onChange: () =>
|
|
1616
|
+
const managedPolicy = new ManagedDesktopPolicy({ onChange: () => {
|
|
1617
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
1618
|
+
resyncOpenCodeProviderKeys();
|
|
1619
|
+
} });
|
|
1183
1620
|
/** A computer kind this server will not use, refused before anything is
|
|
1184
1621
|
* prepared: a Cloud home never offers this computer or a Local VM
|
|
1185
1622
|
* (cloud-home.ts), and an enrolled organisation may disallow any kind. */
|
|
@@ -1203,7 +1640,10 @@ const managedDesktop = new ManagedDesktopProviders({
|
|
|
1203
1640
|
beforeReplace: stopCompanyInstances,
|
|
1204
1641
|
// Ids became stable across re-enrolment; carry old references forward once.
|
|
1205
1642
|
migrate: aliases => { store.renameInstances(new Map(aliases.map(({ from, to }) => [from, to]))); },
|
|
1206
|
-
onAvailability: () =>
|
|
1643
|
+
onAvailability: () => {
|
|
1644
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
1645
|
+
resyncOpenCodeProviderKeys();
|
|
1646
|
+
},
|
|
1207
1647
|
afterReplace: ids => {
|
|
1208
1648
|
for (const id of providerInstancesChanging)
|
|
1209
1649
|
if (managedDesktop.owns(id))
|
|
@@ -1212,8 +1652,11 @@ const managedDesktop = new ManagedDesktopProviders({
|
|
|
1212
1652
|
// Existing renderer config events refresh /api/instances as well, so
|
|
1213
1653
|
// Company grants and revocations appear without reloading the window.
|
|
1214
1654
|
broadcast({ kind: "config", ...configStatus() });
|
|
1655
|
+
resyncOpenCodeProviderKeys();
|
|
1215
1656
|
},
|
|
1216
1657
|
});
|
|
1658
|
+
// An organisation-managed desktop's environment is not the person's shell.
|
|
1659
|
+
openCodeOrganisationManaged = () => managedPolicy.current() !== null || managedDesktop.enrolled();
|
|
1217
1660
|
utilityParentPort?.on("message", event => {
|
|
1218
1661
|
const message = event.data;
|
|
1219
1662
|
if (message?.type !== "openmausbot:managed-desktop-identity")
|
|
@@ -1336,7 +1779,27 @@ function mintInternalCapability(capability) {
|
|
|
1336
1779
|
});
|
|
1337
1780
|
return token;
|
|
1338
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
|
+
}
|
|
1339
1799
|
function revokeInternalCapabilityGeneration(threadId, generation) {
|
|
1800
|
+
endForeignTurns(threadId, generation);
|
|
1801
|
+
if (guestDrivenTurns.get(threadId) === generation)
|
|
1802
|
+
guestDrivenTurns.delete(threadId);
|
|
1340
1803
|
for (const [token, capability] of internalCapabilities) {
|
|
1341
1804
|
if (capability.threadId === threadId && capability.generation === generation) {
|
|
1342
1805
|
internalCapabilities.delete(token);
|
|
@@ -1363,6 +1826,9 @@ function revokeInternalCapabilitiesForThread(threadId) {
|
|
|
1363
1826
|
function revokeAllInternalCapabilities() {
|
|
1364
1827
|
computerSelectionTurns.clear();
|
|
1365
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).
|
|
1366
1832
|
activeInternalGenerationByThread.clear();
|
|
1367
1833
|
internalGenerationByProviderTurn.clear();
|
|
1368
1834
|
}
|
|
@@ -1474,6 +1940,7 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation, r
|
|
|
1474
1940
|
OMB_ROOM_TURN: roomCoordination ? "1" : "0",
|
|
1475
1941
|
OMB_OWN_THREAD_CREATION: ownThreadCreation ? "1" : "0",
|
|
1476
1942
|
OMB_SKILL_AUTHORING_ENABLED: skillAuthoring ? "1" : "0",
|
|
1943
|
+
OMB_MEMORY_ENABLED: store.bot(botId)?.memoryEnabled === false ? "0" : "1",
|
|
1477
1944
|
// The shared-computer tools are advertised only while the workspace
|
|
1478
1945
|
// gate is on; the routes behind them refuse regardless.
|
|
1479
1946
|
OMB_SHARED_COMPUTERS_ENABLED: lendingEnabled() ? "1" : "0",
|
|
@@ -1902,7 +2369,15 @@ function canAdmitDirectTurn(botId, threadId) {
|
|
|
1902
2369
|
/** Routine and webhook dispatch shares startTurn's admission preconditions
|
|
1903
2370
|
* instead of waiting for whole-bot idleness: a free thread slot and no
|
|
1904
2371
|
* active group turn. A group turn blocks scheduled starts the same way it
|
|
1905
|
-
* 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. */
|
|
1906
2381
|
function unattendedDispatchState(botId) {
|
|
1907
2382
|
const bot = store.bot(botId);
|
|
1908
2383
|
const decision = admit("unattended", {}, {
|
|
@@ -1910,7 +2385,19 @@ function unattendedDispatchState(botId) {
|
|
|
1910
2385
|
atCapacity: Boolean(bot) && botAtThreadCapacity(botId),
|
|
1911
2386
|
groupTurn: Boolean(bot) && Boolean(activeGroupTurnForBot(botId)),
|
|
1912
2387
|
});
|
|
1913
|
-
|
|
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";
|
|
1914
2401
|
}
|
|
1915
2402
|
function requestedTaskBot(botId, rawThreadId) {
|
|
1916
2403
|
const profile = store.bot(botId);
|
|
@@ -2076,7 +2563,16 @@ function cancelDirectTurnDispatch(botId, expectedThreadId) {
|
|
|
2076
2563
|
/** The bot's browser for this turn: agent-browser, one isolated session per
|
|
2077
2564
|
* browser profile or per bot (docs/plans/browser-engine.md). Null, with the
|
|
2078
2565
|
* reason logged once, when the engine is not on this machine. */
|
|
2079
|
-
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
|
+
});
|
|
2080
2576
|
const browserLive = new BrowserLive({ runtime: browserRuntime });
|
|
2081
2577
|
// Temporary profiles last for this server run, but are never saved to disk.
|
|
2082
2578
|
// The viewer and the agent must address the SAME temporary browser.
|
|
@@ -2763,7 +3259,7 @@ function previewSystemPrompt(bot) {
|
|
|
2763
3259
|
{ id: "routine", label: "Routines", text: agentsMounted ? ROUTINE_PROMPT : "" },
|
|
2764
3260
|
{ id: "profile", label: "Profile changes", text: agentsMounted ? PROFILE_PROMPT : "" },
|
|
2765
3261
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
2766
|
-
{ 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 }) },
|
|
2767
3263
|
{ id: "skills", label: "Skills index", text: privateWorkspace ? skillsSystemPrompt(bot.id) : "" },
|
|
2768
3264
|
]);
|
|
2769
3265
|
const totalBytes = built.sections.reduce((n, s) => n + s.bytes, 0);
|
|
@@ -2851,7 +3347,12 @@ async function botOverview(bot) {
|
|
|
2851
3347
|
* with one deliberate exception: a Chief of Staff with Full access makes the
|
|
2852
3348
|
* threads it delegates Full too (delegatedFullAccess), so the grant the
|
|
2853
3349
|
* person gave the Chief covers the work the Chief hands out. */
|
|
2854
|
-
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";
|
|
2855
3356
|
const mode = approvalModeForOrigin(approvalModeFor(bot), { peerInitiated });
|
|
2856
3357
|
if (!supportsApprovalMode(bot.modelSelection, mode)) {
|
|
2857
3358
|
return "ask";
|
|
@@ -2866,7 +3367,7 @@ function fullAccessForSource(botId, threadId) {
|
|
|
2866
3367
|
return false;
|
|
2867
3368
|
const bot = store.projectBotForTask(botId, threadId) ?? owner.bot;
|
|
2868
3369
|
// Origin changes Custom to Auto, never Full; no live-turn state is needed.
|
|
2869
|
-
return approvalModeForTurn(bot) === "full";
|
|
3370
|
+
return approvalModeForTurn(bot, false, threadId) === "full";
|
|
2870
3371
|
}
|
|
2871
3372
|
function peerReviewRequired(bot, threadId) {
|
|
2872
3373
|
return Boolean(bot.approvePeerComms && !fullAccessForSource(bot.id, threadId));
|
|
@@ -2890,8 +3391,7 @@ function delegatedFullAccess(from, fromThreadId, target) {
|
|
|
2890
3391
|
}
|
|
2891
3392
|
/** Make a delegated thread Full and say so in it once, so the level the
|
|
2892
3393
|
* chip shows and the level the turns run at agree, and the person can see
|
|
2893
|
-
* where the access came from.
|
|
2894
|
-
* across delegations and must not collect a chip per request. */
|
|
3394
|
+
* where the access came from. */
|
|
2895
3395
|
function grantDelegatedFullAccess(from, target, threadId) {
|
|
2896
3396
|
if (store.taskByThread(target.id, threadId)?.approvalMode === "full")
|
|
2897
3397
|
return;
|
|
@@ -2905,13 +3405,16 @@ function grantDelegatedFullAccess(from, target, threadId) {
|
|
|
2905
3405
|
/** A room member's level for one turn. Work a Full-access Chief hands out
|
|
2906
3406
|
* in a room runs Full for that turn: the room thread is shared, so the
|
|
2907
3407
|
* level is not stored on it — it rides the handoff. */
|
|
2908
|
-
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";
|
|
2909
3412
|
const handoff = orchestration?.roomHandoffId ? roomHandoffs.nodes.get(orchestration.roomHandoffId) : undefined;
|
|
2910
3413
|
const source = handoff?.parentId ? roomHandoffs.nodes.get(handoff.parentId) : undefined;
|
|
2911
3414
|
const from = source ? store.bot(source.botId) : undefined;
|
|
2912
3415
|
if (from && source && delegatedFullAccess(from, source.threadId, bot))
|
|
2913
3416
|
return "full";
|
|
2914
|
-
return approvalModeForTurn(bot, Boolean(orchestration?.roomHandoffId));
|
|
3417
|
+
return approvalModeForTurn(bot, Boolean(orchestration?.roomHandoffId), threadId);
|
|
2915
3418
|
}
|
|
2916
3419
|
/** Privileged approval-mode transitions are deliberately absent from the
|
|
2917
3420
|
* loopback HTTP authority model: a bot with shell access can curl that
|
|
@@ -3668,6 +4171,10 @@ const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
|
|
|
3668
4171
|
: child.status === "completed" ? `${bot?.name ?? "Teammate"} replied${child.groupId ? ` · ${store.group(child.groupId)?.name ?? "Room"}` : ""}`
|
|
3669
4172
|
: `${bot?.name ?? "Teammate"} — ${child.status}: ${child.result.slice(0, 180)}`,
|
|
3670
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) } : {}),
|
|
3671
4178
|
},
|
|
3672
4179
|
...(child.groupId ? { comm: { groupId: child.groupId, threadId: child.threadId, withBotId: child.botId,
|
|
3673
4180
|
withName: bot?.name ?? "Teammate", withColor: bot?.color ?? "blue" } }
|
|
@@ -3681,14 +4188,9 @@ const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
|
|
|
3681
4188
|
if (source)
|
|
3682
4189
|
markTaskContextExternallyUpdated(source, parent.threadId);
|
|
3683
4190
|
}
|
|
3684
|
-
// A work thread
|
|
3685
|
-
//
|
|
3686
|
-
//
|
|
3687
|
-
// sidebar, never deleted, and open again the moment anyone speaks
|
|
3688
|
-
// there. A finished job tidies up after itself; a failed or withheld
|
|
3689
|
-
// one stays in the sidebar where the person can see it. The pair
|
|
3690
|
-
// conversation is the standing line between two bots and never
|
|
3691
|
-
// 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.
|
|
3692
4194
|
const childTask = store.taskByThread(child.botId, child.threadId);
|
|
3693
4195
|
if (!child.groupId && child.status === "completed" && !problem && !childTask?.closedBy
|
|
3694
4196
|
&& childTask?.openedBy?.kind === "work" && childTask.openedBy.botId === parent.botId) {
|
|
@@ -4337,8 +4839,35 @@ function audienceChanged() {
|
|
|
4337
4839
|
}
|
|
4338
4840
|
}
|
|
4339
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
|
+
});
|
|
4340
4868
|
function closeSessionStreams(sessionId) {
|
|
4341
4869
|
browserLive.closeForOwner(sessionId);
|
|
4870
|
+
desktopViewer.closeForOwner(sessionId);
|
|
4342
4871
|
for (const client of sseClients) {
|
|
4343
4872
|
if (client.sessionId !== sessionId)
|
|
4344
4873
|
continue;
|
|
@@ -4353,8 +4882,35 @@ function closeSessionStreams(sessionId) {
|
|
|
4353
4882
|
}
|
|
4354
4883
|
sessions.onSessionRevoked((sessionId) => {
|
|
4355
4884
|
providerAuthSessions.revokeOwner(sessionId);
|
|
4885
|
+
mcpOAuth.revokeOwner(sessionId);
|
|
4356
4886
|
closeSessionStreams(sessionId);
|
|
4887
|
+
// A revoked owner device no longer vouches for its lines (cloudOwnerPerson).
|
|
4888
|
+
ownerOnlyCache.clear();
|
|
4357
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
|
+
}
|
|
4358
4914
|
/** Every frame is numbered, and the last few hundred are kept, so a client
|
|
4359
4915
|
* whose connection dropped can ask for what it missed instead of
|
|
4360
4916
|
* re-downloading every transcript. The desktop reconnects in milliseconds
|
|
@@ -4827,10 +5383,34 @@ const memoryUpkeep = createMemoryUpkeep({
|
|
|
4827
5383
|
return facts.length;
|
|
4828
5384
|
},
|
|
4829
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) } : {}),
|
|
4830
5389
|
quietMs: () => captureQuietMs(cfg),
|
|
4831
5390
|
tidyHour: () => tidyHour(cfg),
|
|
4832
5391
|
log: (line) => console.log(line),
|
|
4833
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
|
+
});
|
|
4834
5414
|
// A finished 1:1 turn of an upkeep bot waits for capture. Only the person's
|
|
4835
5415
|
// own conversation: not a room, not a turn another bot or the harness
|
|
4836
5416
|
// started (its "user" line is not the person), not a failed turn.
|
|
@@ -4844,6 +5424,10 @@ bus.subscribe((event) => {
|
|
|
4844
5424
|
if (!bot || !upkeepEnabled(bot) || store.groupByThread(event.threadId) || isInternalTurn(event.threadId)
|
|
4845
5425
|
|| isUnattended(bot.id, event.threadId) || routines?.runForThread(event.threadId))
|
|
4846
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;
|
|
4847
5431
|
const path = store.activePath(event.threadId);
|
|
4848
5432
|
const replyAt = path.findLastIndex((message) => message.role === "bot" && message.kind === "text" && message.turnId === event.turnId);
|
|
4849
5433
|
if (replyAt < 0)
|
|
@@ -4876,7 +5460,7 @@ bus.subscribe((event) => {
|
|
|
4876
5460
|
const messages = store.messagesFor(event.threadId);
|
|
4877
5461
|
const reply = messages.findLast((message) => message.role === "bot" && message.kind === "text" && message.turnId === event.turnId);
|
|
4878
5462
|
const bot = store.botByThread(event.threadId) ?? (reply?.from ? store.bot(reply.from.botId) : undefined);
|
|
4879
|
-
if (!bot)
|
|
5463
|
+
if (!bot || bot.memoryEnabled === false)
|
|
4880
5464
|
return;
|
|
4881
5465
|
const tools = messages
|
|
4882
5466
|
.filter((message) => message.kind === "activity" && message.turnId === event.turnId && message.tool?.name)
|
|
@@ -4889,13 +5473,21 @@ bus.subscribe((event) => {
|
|
|
4889
5473
|
const room = store.groupByThread(event.threadId);
|
|
4890
5474
|
if (room && !roomFeedsBot(room, bot))
|
|
4891
5475
|
return;
|
|
4892
|
-
|
|
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, {
|
|
4893
5481
|
source: memorySourceLabel({
|
|
4894
5482
|
room,
|
|
4895
5483
|
task: store.taskByThread(bot.id, event.threadId),
|
|
4896
5484
|
threadId: event.threadId,
|
|
4897
5485
|
}),
|
|
4898
5486
|
});
|
|
5487
|
+
if (lendingMemory)
|
|
5488
|
+
lendingMemory.trustedWrite(bot.id, writeLine);
|
|
5489
|
+
else
|
|
5490
|
+
writeLine();
|
|
4899
5491
|
}
|
|
4900
5492
|
catch {
|
|
4901
5493
|
// a missing note never fails a turn
|
|
@@ -5180,13 +5772,18 @@ const boatLifecycleBusyBots = new Set();
|
|
|
5180
5772
|
// A refresh is a reader, not a lifecycle change. Keep its reservation until
|
|
5181
5773
|
// the provider settles even if the HTTP client leaves, and share it on retry.
|
|
5182
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();
|
|
5183
5778
|
const orphanBoatLifecycleBusyIds = new Set();
|
|
5184
5779
|
const boatInventoryRequestsBusyIds = new Set();
|
|
5185
5780
|
const computerProviderConfigTransitions = new Set();
|
|
5186
5781
|
// A restore mutates and cleans a project work tree. Claim the bot across the
|
|
5187
5782
|
// entire async Git operation so a turn cannot start in that folder midway.
|
|
5188
5783
|
const checkpointRestoreLeases = new Set();
|
|
5189
|
-
|
|
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;
|
|
5190
5787
|
/** How long a turn waits for Cua Driver after starting the container itself.
|
|
5191
5788
|
* A cold XFCE desktop needs some seconds; past this the turn reports the
|
|
5192
5789
|
* status it has rather than hanging on a container that will not come up. */
|
|
@@ -5317,7 +5914,7 @@ async function teamComputersPayload() {
|
|
|
5317
5914
|
* Assignment authorizes waking, never replacing a missing paid machine. */
|
|
5318
5915
|
async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
5319
5916
|
if (!canMount)
|
|
5320
|
-
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");
|
|
5321
5918
|
if (!boat.boatConfigured(cfg))
|
|
5322
5919
|
throw new Error("The team's Boat account is not configured; reconnect it in Settings");
|
|
5323
5920
|
const ownerId = teamComputerOwner(computer.id);
|
|
@@ -5353,7 +5950,7 @@ async function mountHostComputer(owner, botId, providerSupportsLocal) {
|
|
|
5353
5950
|
// ACP engine" while already on one has nowhere to go.
|
|
5354
5951
|
throw new Error(providerSupportsLocal
|
|
5355
5952
|
? `local computer control is not available on ${process.platform} — select another destination`
|
|
5356
|
-
: "this model
|
|
5953
|
+
: "this model cannot control this computer — choose Claude or an ACP model provider, or select another destination");
|
|
5357
5954
|
}
|
|
5358
5955
|
const cua = readCuaConnection();
|
|
5359
5956
|
if (!cua) {
|
|
@@ -5626,7 +6223,7 @@ async function selectableComputers(bot) {
|
|
|
5626
6223
|
}
|
|
5627
6224
|
else if (surface === "browser") {
|
|
5628
6225
|
ready = caps?.browserMcp === true && builtInBrowserEnabled(cfg) && bot.browser !== false && browserEngineStatus().kind === "ready";
|
|
5629
|
-
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.";
|
|
5630
6227
|
}
|
|
5631
6228
|
}
|
|
5632
6229
|
catch (error) {
|
|
@@ -5777,7 +6374,7 @@ function localVmIdleFor(target) {
|
|
|
5777
6374
|
let idle = localVmIdles.get(target.key);
|
|
5778
6375
|
if (idle)
|
|
5779
6376
|
return idle;
|
|
5780
|
-
idle = new LocalVmIdleTimer(
|
|
6377
|
+
idle = new LocalVmIdleTimer(localVmIdleMs(), () => localVmImageBusy || localVmLifecycleBusy.has(target.key) || localVmActiveThreads.has(target.key), async () => {
|
|
5781
6378
|
localVmLifecycleBusy.add(target.key);
|
|
5782
6379
|
try {
|
|
5783
6380
|
const status = await containerComputerStatus(undefined, undefined, target);
|
|
@@ -6118,10 +6715,13 @@ bus.subscribe((event) => {
|
|
|
6118
6715
|
// always reaches the human — even Full access never invents an answer.
|
|
6119
6716
|
const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
6120
6717
|
const unattended = permission && asker && event.requestId ? isUnattended(asker.id, event.threadId) : false;
|
|
6121
|
-
|
|
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";
|
|
6122
6722
|
// Native shell descriptors are distinct from computer/MCP permissions;
|
|
6123
6723
|
// choosing a local desktop must not disable an exact shell grant.
|
|
6124
|
-
const command = permission && asker && event.requestId && !event.requiresExplicitApproval && event.command
|
|
6724
|
+
const command = permission && asker && event.requestId && !event.requiresExplicitApproval && event.command && !guestDriven
|
|
6125
6725
|
? commandAllowlistCandidate({ ...event.command, providerInstanceId: event.providerInstanceId ?? asker.modelSelection.instanceId })
|
|
6126
6726
|
: null;
|
|
6127
6727
|
const verdict = permission && asker && event.requestId
|
|
@@ -6226,7 +6826,8 @@ bus.subscribe((event) => {
|
|
|
6226
6826
|
: undefined,
|
|
6227
6827
|
commandAllowlist: command ?? undefined,
|
|
6228
6828
|
// Provider-owned session grants remain separate from exact commands.
|
|
6229
|
-
|
|
6829
|
+
// Never on a guest's turn: an "always" for it would outlive it.
|
|
6830
|
+
allowSession: permission && event.allowSession && !event.requiresExplicitApproval && !guestDriven ? true : undefined,
|
|
6230
6831
|
// The text stays for cards saved before heldCode existed, and for
|
|
6231
6832
|
// clients that do not know the key yet.
|
|
6232
6833
|
held: approvalHeldReason(heldContext),
|
|
@@ -6304,12 +6905,19 @@ bus.subscribe((event) => {
|
|
|
6304
6905
|
tool: { name: `retrying — attempt ${event.attempt + 1}/${RETRY_MAX_ATTEMPTS} in ${Math.round(event.delayMs / 1000)}s — ${event.reason}`, ok: true },
|
|
6305
6906
|
});
|
|
6306
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;
|
|
6307
6914
|
case "runtime.error":
|
|
6308
6915
|
pushMessage({
|
|
6309
6916
|
role: "bot",
|
|
6310
6917
|
kind: "activity",
|
|
6311
6918
|
tool: {
|
|
6312
|
-
|
|
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)}`,
|
|
6313
6921
|
ok: false,
|
|
6314
6922
|
setup: event.setup,
|
|
6315
6923
|
...(event.terminal ? { terminal: true } : {}),
|
|
@@ -7554,14 +8162,10 @@ async function finalScreenFrame(_botId, threadId) {
|
|
|
7554
8162
|
* (generateText — Haiku on Claude, the chat completion endpoint's text
|
|
7555
8163
|
* path on OpenAI-compatible engines). Null whenever that call cannot run,
|
|
7556
8164
|
* runs long, or answers with something that is not a plain short title;
|
|
7557
|
-
* the caller keeps the snippet it already applied.
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
"Reply with only a short title: 3 to 6 words, plain text, no quotes, no trailing period.",
|
|
7562
|
-
"Message:",
|
|
7563
|
-
text.trim().slice(0, 1_500),
|
|
7564
|
-
].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);
|
|
7565
8169
|
const expiry = new AbortController();
|
|
7566
8170
|
let timer;
|
|
7567
8171
|
try {
|
|
@@ -7585,6 +8189,8 @@ async function generateThreadTitle(provider, text) {
|
|
|
7585
8189
|
clearTimeout(timer);
|
|
7586
8190
|
}
|
|
7587
8191
|
}
|
|
8192
|
+
/** Threads whose Regenerate title one-shot is still running: one at a time. */
|
|
8193
|
+
const titleRegenerations = new Set();
|
|
7588
8194
|
// ── turn dispatch (upstream ProviderCommandReactor, miniature) ──────────
|
|
7589
8195
|
async function startTurn(botId, text, opts) {
|
|
7590
8196
|
workspaceMaintenance.assertAvailable();
|
|
@@ -7671,6 +8277,15 @@ async function startTurn(botId, text, opts) {
|
|
|
7671
8277
|
const policyRefusal = policyModelRefusal(instance);
|
|
7672
8278
|
if (policyRefusal)
|
|
7673
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
|
+
}
|
|
7674
8289
|
// Resolve only transport tags from this newly submitted text. The original
|
|
7675
8290
|
// string remains the durable message. Native-image providers get a
|
|
7676
8291
|
// path-free prompt and bounded inputs instead of needing a Read tool;
|
|
@@ -7691,11 +8306,9 @@ async function startTurn(botId, text, opts) {
|
|
|
7691
8306
|
const titled = store.titleTaskFromFirstMessage(bot.id, resolvedImages.text, threadId);
|
|
7692
8307
|
// The snippet is only the fallback name. A cheap one-shot may trade it
|
|
7693
8308
|
// for a title a person would have typed, but never on a peer-opened
|
|
7694
|
-
// row:
|
|
7695
|
-
//
|
|
7696
|
-
//
|
|
7697
|
-
// while the row still carries the snippet — a rename by the person or
|
|
7698
|
-
// 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.
|
|
7699
8312
|
const snippet = titled?.title;
|
|
7700
8313
|
if (titled && snippet && !titled.openedBy?.botId && llmThreadTitlesEnabled(cfg) && instance.generateText) {
|
|
7701
8314
|
void generateThreadTitle(instance, resolvedImages.text)
|
|
@@ -7772,6 +8385,9 @@ async function startTurn(botId, text, opts) {
|
|
|
7772
8385
|
// in the background — boat provisioning can take ~90s and must never
|
|
7773
8386
|
// hang the HTTP request
|
|
7774
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);
|
|
7775
8391
|
const resourceOwner = { threadId, generation: dispatchClaimId };
|
|
7776
8392
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
7777
8393
|
directTurnGenerationByThread.set(threadId, dispatchClaimId);
|
|
@@ -7786,6 +8402,10 @@ async function startTurn(botId, text, opts) {
|
|
|
7786
8402
|
requestGenerations.add(dispatchClaimId);
|
|
7787
8403
|
directRequestOwners.set(threadId, { generation: dispatchClaimId, messageId: requestMessageId,
|
|
7788
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);
|
|
7789
8409
|
// An unknown control-plane wake may invalidate a prior final, but it must
|
|
7790
8410
|
// never acquire authority by guessing the latest user as its origin.
|
|
7791
8411
|
const pendingSource = requestMessageId ?? store.activePath(threadId).findLast(message => message.role === "user")?.id;
|
|
@@ -7911,9 +8531,9 @@ async function startTurn(botId, text, opts) {
|
|
|
7911
8531
|
const externalUpdate = Boolean(opts?.coordination?.resumed || unseen?.some((m) => m.keep));
|
|
7912
8532
|
// What a resumed session keeps from its launch: the standing instructions
|
|
7913
8533
|
// (tools, servers and — for Claude — the model are passed on every launch),
|
|
7914
|
-
// plus
|
|
7915
|
-
//
|
|
7916
|
-
//
|
|
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
|
|
7917
8537
|
// external update that finds any of it changed since the session started
|
|
7918
8538
|
// gets the fresh session and replay it always got, rather than a resume.
|
|
7919
8539
|
const persistentConfig = [bot.name, bot.title, bot.description, sectionContextSystemPrompt(bot.section),
|
|
@@ -8014,7 +8634,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8014
8634
|
// composio — only to a driver that can mount them. Their tools are
|
|
8015
8635
|
// never pre-allowed, so every call rides the normal permission flow.
|
|
8016
8636
|
if (instance.adapter.capabilities.customMcp === true) {
|
|
8017
|
-
const custom = engineMcpServers(bot);
|
|
8637
|
+
const custom = await withMcpSignIn(engineMcpServers(bot), mcpOAuth);
|
|
8018
8638
|
if (Object.keys(custom).length)
|
|
8019
8639
|
integrations.custom = custom;
|
|
8020
8640
|
}
|
|
@@ -8026,7 +8646,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8026
8646
|
if (worksInWorkspace) {
|
|
8027
8647
|
ensureWorkspace(bot.id);
|
|
8028
8648
|
// baseline for the journal's turn-boundary diff (see the bus hook)
|
|
8029
|
-
|
|
8649
|
+
if (bot.memoryEnabled !== false)
|
|
8650
|
+
beginMemoryTurn(bot.id, threadId);
|
|
8030
8651
|
}
|
|
8031
8652
|
const privateWorkspace = worksInWorkspace ? ensureTaskWorkspace(bot.id, threadId) : undefined;
|
|
8032
8653
|
const skillInstructions = renderSkillInstructions(selectedSkills, {
|
|
@@ -8042,8 +8663,13 @@ async function startTurn(botId, text, opts) {
|
|
|
8042
8663
|
// bot's folder for a task that runs elsewhere.
|
|
8043
8664
|
if (opts?.runOn === "cloud")
|
|
8044
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).
|
|
8045
8671
|
const pinnedCwd = privateWorkspace && opts?.runOn !== "cloud"
|
|
8046
|
-
? store.pinTaskCwd(bot.id, threadId, privateWorkspace)
|
|
8672
|
+
? store.pinTaskCwd(bot.id, threadId, privateWorkspace, { privateOnly: cloudGuestOpened(threadId) })
|
|
8047
8673
|
: null;
|
|
8048
8674
|
const cwd = pinnedCwd ?? undefined;
|
|
8049
8675
|
if (cwd && !claimTurnResource(resourceOwner, workspaceResource(cwd))) {
|
|
@@ -8239,7 +8865,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8239
8865
|
if (!mountsComputerMcp || instance.adapter.capabilities.remoteAgent === true) {
|
|
8240
8866
|
if (!strict)
|
|
8241
8867
|
return false;
|
|
8242
|
-
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");
|
|
8243
8869
|
}
|
|
8244
8870
|
if (!strict && localVmMode(cfg) === "pool") {
|
|
8245
8871
|
// localVmTargetForThread records pool affinity, so a turn the
|
|
@@ -8455,9 +9081,9 @@ async function startTurn(botId, text, opts) {
|
|
|
8455
9081
|
autoVpsProblem &&
|
|
8456
9082
|
!computerSelectionTurns.has(threadId)) {
|
|
8457
9083
|
const hint = opts?.automationSource
|
|
8458
|
-
? "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."
|
|
8459
9085
|
: bot.autoStartVps
|
|
8460
|
-
? "Check the VPS connection in
|
|
9086
|
+
? "Check the VPS connection in Settings → API keys."
|
|
8461
9087
|
: "Open Computer and enable Start VPS automatically, or choose Cloud to start it manually.";
|
|
8462
9088
|
throw new Error(`${autoVpsProblem}. ${hint}`);
|
|
8463
9089
|
}
|
|
@@ -8503,7 +9129,7 @@ async function startTurn(botId, text, opts) {
|
|
|
8503
9129
|
const credentialPrompt = integrations.agents ? CREDENTIAL_PROMPT + (boundedCoordination ? "" : THREADS_PROMPT) : "";
|
|
8504
9130
|
const routinePrompt = integrations.agents ? ROUTINE_PROMPT : "";
|
|
8505
9131
|
const profilePrompt = integrations.agents ? PROFILE_PROMPT : "";
|
|
8506
|
-
const recallPrompt = integrations.agents ? SESSION_SEARCH_SYSTEM_PROMPT : "";
|
|
9132
|
+
const recallPrompt = integrations.agents && bot.memoryEnabled !== false ? SESSION_SEARCH_SYSTEM_PROMPT : "";
|
|
8507
9133
|
const learnPrompt = skillAuthoring ? LEARN_PROMPT : "";
|
|
8508
9134
|
// (activeVpsThreads was already claimed above, before the provision or
|
|
8509
9135
|
// reuse await, so the backend guards saw this turn the whole time.)
|
|
@@ -8636,8 +9262,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8636
9262
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
8637
9263
|
// what the bot said lately in its other conversations, so a task
|
|
8638
9264
|
// never redoes — or forgets — what another one already did
|
|
8639
|
-
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentWork(recentWorkSources(bot), bot, { userName: cfg.profile?.name?.trim() || "User", currentThreadId: threadId })) },
|
|
8640
|
-
{ 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 }) },
|
|
8641
9267
|
{ id: "skills", label: "Skills index", text: privateWorkspace ? skillsSystemPrompt(bot.id) : "" },
|
|
8642
9268
|
{ id: "skill-instructions", label: "Skill instructions", text: skillInstructions },
|
|
8643
9269
|
{ id: "playbooks", label: "Playbooks", text: packagePlaybooks },
|
|
@@ -8681,7 +9307,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8681
9307
|
text: withRecalled(recalled, dispatchContext.turnText),
|
|
8682
9308
|
refreshSystemPrompt: true,
|
|
8683
9309
|
images: turnImages,
|
|
8684
|
-
approvalMode: approvalModeForTurn(bot, commsDepth > 0),
|
|
9310
|
+
approvalMode: approvalModeForTurn(bot, commsDepth > 0, threadId),
|
|
9311
|
+
...(guestConfined ? { guestConfined: true, confinedWhy: confinedWhy(threadId) } : {}),
|
|
8685
9312
|
model,
|
|
8686
9313
|
effort,
|
|
8687
9314
|
variant,
|
|
@@ -8972,7 +9599,10 @@ function routineRunFallbackText(card) {
|
|
|
8972
9599
|
: card.status === "queued" && card.deferredAt != null
|
|
8973
9600
|
? "deferred: target busy"
|
|
8974
9601
|
: card.status);
|
|
8975
|
-
|
|
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}` : ""}`;
|
|
8976
9606
|
}
|
|
8977
9607
|
/** Upsert one durable lifecycle card per run. Replaying the same transition,
|
|
8978
9608
|
* including restart recovery, patches the existing run id instead of adding
|
|
@@ -9017,9 +9647,14 @@ function syncRoutineRunToSource(run) {
|
|
|
9017
9647
|
const execution = store.taskByThread(run.botId, run.threadId);
|
|
9018
9648
|
const freshExecution = execution && run.status === "running" && !execution.routineRunId &&
|
|
9019
9649
|
store.messagesFor(run.threadId).length === 0;
|
|
9020
|
-
if (execution &&
|
|
9650
|
+
if (execution && freshExecution) {
|
|
9021
9651
|
store.patchTask(run.botId, run.threadId, { routineRunId: run.id, unread: false });
|
|
9022
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
|
+
}
|
|
9023
9658
|
}
|
|
9024
9659
|
// Merely queueing/running is ambient progress. Attention and terminal
|
|
9025
9660
|
// states become unread in the conversation where the user asked for them.
|
|
@@ -9111,8 +9746,13 @@ routines = new RoutineManager({
|
|
|
9111
9746
|
}
|
|
9112
9747
|
return groupIsWorking(group) || coordinator.busy ? "busy" : "ready";
|
|
9113
9748
|
},
|
|
9114
|
-
createTask: (botId, title, activate = false) => {
|
|
9749
|
+
createTask: (botId, title, activate = false, routineId) => {
|
|
9115
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));
|
|
9116
9756
|
const bot = store.bot(botId);
|
|
9117
9757
|
if (task && bot)
|
|
9118
9758
|
broadcast({ kind: "bot", bot: publicBot(bot) });
|
|
@@ -9140,7 +9780,12 @@ routines = new RoutineManager({
|
|
|
9140
9780
|
// explicit resultsThreadId must be a visible task owned by the running bot.
|
|
9141
9781
|
if (!forceNew && routineSourceOwner(routine))
|
|
9142
9782
|
return routine.resultsThreadId;
|
|
9143
|
-
|
|
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;
|
|
9144
9789
|
},
|
|
9145
9790
|
discardResultsThread: (botId, threadId) => {
|
|
9146
9791
|
const task = store.taskByThread(botId, threadId);
|
|
@@ -9202,6 +9847,63 @@ routines = new RoutineManager({
|
|
|
9202
9847
|
notify(buildNotification("routine-deferred", notificationBot, routineSourceThread(run) ?? bot.threadId, detail));
|
|
9203
9848
|
},
|
|
9204
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
|
+
}
|
|
9205
9907
|
orgLibrary = new OrgLibrary({
|
|
9206
9908
|
dataDir: DATA_DIR,
|
|
9207
9909
|
store,
|
|
@@ -9276,7 +9978,7 @@ async function cloudRoutineReadiness(botId, threadId) {
|
|
|
9276
9978
|
return { ready: false, reason: "The Cloud VM runner is unavailable. Restart OpenMausBot and try again." };
|
|
9277
9979
|
try {
|
|
9278
9980
|
if ((await instance.snapshot()).state !== "available") {
|
|
9279
|
-
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." };
|
|
9280
9982
|
}
|
|
9281
9983
|
const teamComputer = inheritedTeamComputer(bot);
|
|
9282
9984
|
const ownerId = teamComputer ? teamComputerOwner(teamComputer.id) : bot.id;
|
|
@@ -9829,9 +10531,15 @@ function sendRoutineResolution(res, result) {
|
|
|
9829
10531
|
});
|
|
9830
10532
|
return true;
|
|
9831
10533
|
}
|
|
9832
|
-
function resolveAndSendRoutine(res, args) {
|
|
10534
|
+
function resolveAndSendRoutine(res, args, ownersAnswer = false) {
|
|
9833
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;
|
|
9834
10540
|
const result = routineRequests.resolve(args);
|
|
10541
|
+
if (ownersAnswer && result.claimed && result.state === "applied")
|
|
10542
|
+
cloudOwnerApplied(result.action, result.resultId, wasOwners);
|
|
9835
10543
|
if (result.claimed &&
|
|
9836
10544
|
(result.state === "applied" || result.state === "denied")) {
|
|
9837
10545
|
appendDecision(DATA_DIR, {
|
|
@@ -9967,12 +10675,25 @@ const webhooks = new WebhookManager({
|
|
|
9967
10675
|
findRun: (webhookId, deliveryId) => routines.webhookRunReceipt(webhookId, deliveryId),
|
|
9968
10676
|
cancelQueued: (webhookId, message) => routines.cancelQueuedWebhook(webhookId, message),
|
|
9969
10677
|
pendingRuns: (webhookId) => routines.activeWebhookRunCount(webhookId),
|
|
9970
|
-
// delivery:"post" webhooks land in
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
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))
|
|
9974
10686
|
return;
|
|
9975
|
-
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;
|
|
9976
10697
|
},
|
|
9977
10698
|
});
|
|
9978
10699
|
let webhookIngress = null;
|
|
@@ -10144,7 +10865,7 @@ setupRetry = 0) {
|
|
|
10144
10865
|
// delegated Full elevation makes the two disagree by construction: every
|
|
10145
10866
|
// such room turn then reads as "settings changed", retries once, and
|
|
10146
10867
|
// settles as busy without ever dispatching.
|
|
10147
|
-
const preparedApprovalMode = roomTurnApprovalMode(bot, orchestration);
|
|
10868
|
+
const preparedApprovalMode = roomTurnApprovalMode(bot, threadId, orchestration);
|
|
10148
10869
|
const preparedSelection = { ...bot.modelSelection };
|
|
10149
10870
|
const preparedComposio = bot.composio;
|
|
10150
10871
|
const instance = turnInstance(bot);
|
|
@@ -10154,8 +10875,14 @@ setupRetry = 0) {
|
|
|
10154
10875
|
return true;
|
|
10155
10876
|
}
|
|
10156
10877
|
const roomPolicyRefusal = instance ? policyModelRefusal(instance) : undefined;
|
|
10157
|
-
|
|
10158
|
-
|
|
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`;
|
|
10159
10886
|
store.appendMessage(threadId, {
|
|
10160
10887
|
role: "bot",
|
|
10161
10888
|
kind: "activity",
|
|
@@ -10188,6 +10915,11 @@ setupRetry = 0) {
|
|
|
10188
10915
|
return true;
|
|
10189
10916
|
}
|
|
10190
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);
|
|
10191
10923
|
const resourceOwner = { threadId, generation: internalGeneration };
|
|
10192
10924
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
10193
10925
|
let roomVmTarget = null;
|
|
@@ -10294,7 +11026,7 @@ setupRetry = 0) {
|
|
|
10294
11026
|
}
|
|
10295
11027
|
// user-configured MCP servers: same gating as the 1:1 site above.
|
|
10296
11028
|
if (instance.adapter.capabilities.customMcp === true) {
|
|
10297
|
-
const custom = engineMcpServers(bot);
|
|
11029
|
+
const custom = await withMcpSignIn(engineMcpServers(bot), mcpOAuth);
|
|
10298
11030
|
if (Object.keys(custom).length)
|
|
10299
11031
|
integrations.custom = custom;
|
|
10300
11032
|
}
|
|
@@ -10316,7 +11048,7 @@ setupRetry = 0) {
|
|
|
10316
11048
|
if (!readyGroup || !stillOwnsThread || !readyGroup.memberIds.includes(readyBot.id))
|
|
10317
11049
|
return false;
|
|
10318
11050
|
const setupChanged = turnInstance(readyBot) !== instance ||
|
|
10319
|
-
roomTurnApprovalMode(readyBot, orchestration) !== preparedApprovalMode ||
|
|
11051
|
+
roomTurnApprovalMode(readyBot, threadId, orchestration) !== preparedApprovalMode ||
|
|
10320
11052
|
readyBot.modelSelection.instanceId !== preparedSelection.instanceId ||
|
|
10321
11053
|
readyBot.modelSelection.model !== preparedSelection.model ||
|
|
10322
11054
|
readyBot.modelSelection.effort !== preparedSelection.effort ||
|
|
@@ -10520,7 +11252,7 @@ setupRetry = 0) {
|
|
|
10520
11252
|
// Claim the same lease as direct turns before asynchronous VM setup.
|
|
10521
11253
|
if (readyBot.computer === "vm") {
|
|
10522
11254
|
if (instance.adapter.capabilities.computerMcp !== true || instance.adapter.capabilities.remoteAgent === true) {
|
|
10523
|
-
throw new Error("this model
|
|
11255
|
+
throw new Error("this model cannot use the Local VM");
|
|
10524
11256
|
}
|
|
10525
11257
|
// A distinct identity fences cleanup even in shared mode on the same room thread.
|
|
10526
11258
|
const target = { ...localVmTargetForThread(readyBot.id, threadId) };
|
|
@@ -10613,7 +11345,7 @@ setupRetry = 0) {
|
|
|
10613
11345
|
const worksInWorkspace = supportsWorkspaceFiles(instance.driverKind);
|
|
10614
11346
|
const workspace = worksInWorkspace ? ensureWorkspace(bot.id) : undefined;
|
|
10615
11347
|
// a room member's memory writes are journaled the same as a 1:1 turn's
|
|
10616
|
-
if (workspace)
|
|
11348
|
+
if (workspace && bot.memoryEnabled !== false)
|
|
10617
11349
|
beginMemoryTurn(bot.id, threadId);
|
|
10618
11350
|
// The room's folder pins here — on the first turn that actually
|
|
10619
11351
|
// dispatches, not at PATCH time — so a folder set on a never-used room
|
|
@@ -10621,19 +11353,22 @@ setupRetry = 0) {
|
|
|
10621
11353
|
// has its folder moved underneath it. Off-host members skip the folder
|
|
10622
11354
|
// but must not decide the pin: the room's desk is a property of the
|
|
10623
11355
|
// room, not of whichever member happened to speak first.
|
|
10624
|
-
|
|
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));
|
|
10625
11360
|
{
|
|
10626
11361
|
const drift = checkSoulDrift(bot.id, bot.soul ?? "", bot.soulHash ?? "");
|
|
10627
11362
|
if (drift.drift !== Boolean(bot.soulDrift))
|
|
10628
11363
|
store.patchBot(bot.id, { soulDrift: drift.drift });
|
|
10629
11364
|
}
|
|
10630
|
-
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 });
|
|
10631
11366
|
// The same brief a 1:1 turn gets: what this member said lately in its
|
|
10632
11367
|
// other conversations, so a standup is answered from what happened. A
|
|
10633
11368
|
// brief can carry a private chat into the room; as with session_search
|
|
10634
11369
|
// (#754) the room is told, once per source thread, rather than the
|
|
10635
11370
|
// crossing being blocked.
|
|
10636
|
-
const recentLines = recentWork(recentWorkSources(bot), bot, { userName, currentThreadId: threadId });
|
|
11371
|
+
const recentLines = recentWork(recentWorkSources(bot), bot, { userName, currentThreadId: threadId, ...recentWorkFilter() });
|
|
10637
11372
|
const roomComputerPromptKind = resolveComputerPromptKind({
|
|
10638
11373
|
kind: roomTeamComputer || roomComputerKind === "box" ? "box" : roomVmTarget ? "vm" : roomComputerKind,
|
|
10639
11374
|
driverKind: instance.driverKind, cloudComputerMcp: instance.adapter.capabilities.cloudComputerMcp,
|
|
@@ -10659,7 +11394,7 @@ setupRetry = 0) {
|
|
|
10659
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) }) },
|
|
10660
11395
|
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
10661
11396
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
10662
|
-
{ 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 : "" },
|
|
10663
11398
|
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentLines) },
|
|
10664
11399
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
10665
11400
|
// the room path has always put a newline before memory and trimmed
|
|
@@ -10794,7 +11529,8 @@ setupRetry = 0) {
|
|
|
10794
11529
|
text: withRecalled(roomRecalled, text),
|
|
10795
11530
|
refreshSystemPrompt: true,
|
|
10796
11531
|
images: turnImages,
|
|
10797
|
-
approvalMode: roomTurnApprovalMode(readyBot, orchestration),
|
|
11532
|
+
approvalMode: roomTurnApprovalMode(readyBot, threadId, orchestration),
|
|
11533
|
+
...(roomGuestConfined ? { guestConfined: true, confinedWhy: confinedWhy(threadId) } : {}),
|
|
10798
11534
|
system: roomSystem.text,
|
|
10799
11535
|
systemStable: roomSystem.stable,
|
|
10800
11536
|
systemVolatile: roomSystem.volatile,
|
|
@@ -12443,12 +13179,12 @@ function stderrOf(err) {
|
|
|
12443
13179
|
const s = err.stderr;
|
|
12444
13180
|
return typeof s === "string" ? s : Buffer.isBuffer(s) ? s.toString("utf8") : "";
|
|
12445
13181
|
}
|
|
12446
|
-
async function localVmPayload(target) {
|
|
13182
|
+
async function localVmPayload(target, auth) {
|
|
12447
13183
|
const status = await containerComputerStatus(undefined, undefined, target);
|
|
12448
13184
|
return {
|
|
12449
|
-
...status,
|
|
13185
|
+
...localVmViewerStatus(status, auth),
|
|
12450
13186
|
commands: setupCommands(status.runtime, process.platform, target),
|
|
12451
|
-
idle_timeout_ms:
|
|
13187
|
+
idle_timeout_ms: localVmIdleMs(),
|
|
12452
13188
|
mode: localVmMode(cfg),
|
|
12453
13189
|
max_instances: localVmMaxInstances(cfg),
|
|
12454
13190
|
};
|
|
@@ -12576,7 +13312,9 @@ function configStatus() {
|
|
|
12576
13312
|
return {
|
|
12577
13313
|
xai: { configured: Boolean(cfg.xai?.key) },
|
|
12578
13314
|
mistral: { configured: Boolean(cfg.mistral?.key) },
|
|
12579
|
-
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) },
|
|
12580
13318
|
// a fleet agent on this server means Settings → Workspaces has something to drive
|
|
12581
13319
|
fleet: { available: fleetAvailable(fleetSocketPath()) },
|
|
12582
13320
|
// what this build is entitled to, so Settings shows only what works here
|
|
@@ -12627,6 +13365,7 @@ function configStatus() {
|
|
|
12627
13365
|
localVm: {
|
|
12628
13366
|
mode: localVmMode(cfg),
|
|
12629
13367
|
maxInstances: localVmMaxInstances(cfg),
|
|
13368
|
+
idleTimeoutMinutes: localVmIdleTimeoutMinutes(cfg),
|
|
12630
13369
|
},
|
|
12631
13370
|
features: {
|
|
12632
13371
|
skillAuthoring: skillAuthoringEnabled(cfg),
|
|
@@ -12641,6 +13380,8 @@ function configStatus() {
|
|
|
12641
13380
|
// own Claude Code MCP servers
|
|
12642
13381
|
claudeUserMcp: claudeUserMcpEnabled(cfg),
|
|
12643
13382
|
autoRecall: autoRecallEnabled(cfg),
|
|
13383
|
+
// hand-edited config.json only; the thread menu offers Regenerate title from it
|
|
13384
|
+
llmThreadTitles: llmThreadTitlesEnabled(cfg),
|
|
12644
13385
|
},
|
|
12645
13386
|
// first-run progress — not a secret; the app decides whether to show
|
|
12646
13387
|
// the welcome tour from this, never from browser storage
|
|
@@ -12649,6 +13390,7 @@ function configStatus() {
|
|
|
12649
13390
|
version: cfg.onboarding?.version ?? 0,
|
|
12650
13391
|
reelSeen: cfg.onboarding?.reelSeen === true,
|
|
12651
13392
|
hintsSeen: cfg.onboarding?.hintsSeen ?? [],
|
|
13393
|
+
...(cfg.onboarding?.firstTurnAt ? { firstTurnAt: cfg.onboarding.firstTurnAt } : {}),
|
|
12652
13394
|
},
|
|
12653
13395
|
// Which browser this server can give bots: the desktop app's surface,
|
|
12654
13396
|
// the agent-browser engine, or nothing yet (with the reason).
|
|
@@ -12682,7 +13424,14 @@ function mcpServerResponse() {
|
|
|
12682
13424
|
// While enrolled with custom servers off, say which servers stay configured
|
|
12683
13425
|
// but never reach bots, and why. Nothing here is written to config.json.
|
|
12684
13426
|
const policy = managedPolicy.current();
|
|
12685
|
-
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 });
|
|
12686
13435
|
return { servers, ...(policy && !policy.mcp.allowCustom ? { managed: { organizationName: policy.organizationName, allowlist: policy.mcp.allowlist } } : {}) };
|
|
12687
13436
|
}
|
|
12688
13437
|
/** Adding or editing a server the organisation has not approved is refused. */
|
|
@@ -12697,7 +13446,7 @@ function mcpServerBody(body) {
|
|
|
12697
13446
|
if (!body || typeof body !== "object")
|
|
12698
13447
|
return out;
|
|
12699
13448
|
const record = body;
|
|
12700
|
-
for (const key of ["command", "args", "env", "type", "url", "headers", "enabled"]) {
|
|
13449
|
+
for (const key of ["command", "args", "env", "type", "url", "headers", "oauth", "enabled"]) {
|
|
12701
13450
|
if (record[key] !== undefined)
|
|
12702
13451
|
out[key] = record[key];
|
|
12703
13452
|
}
|
|
@@ -12706,7 +13455,7 @@ function mcpServerBody(body) {
|
|
|
12706
13455
|
/** Configured MCP servers that may reach this bot's engine. While enrolled,
|
|
12707
13456
|
* the organisation's allow-list filters them; config.json is never changed. */
|
|
12708
13457
|
function engineMcpServers(bot) {
|
|
12709
|
-
return managedPolicy.filterMcp(customMcpServers(cfg, bot.mcpServers));
|
|
13458
|
+
return withoutPendingSignIn(managedPolicy.filterMcp(customMcpServers(cfg, bot.mcpServers)), mcpOAuth);
|
|
12710
13459
|
}
|
|
12711
13460
|
function persistMcpServers(next) {
|
|
12712
13461
|
saveConfig({ mcpServers: next });
|
|
@@ -12720,7 +13469,11 @@ async function describeInstances() {
|
|
|
12720
13469
|
const configs = providerConfigs();
|
|
12721
13470
|
return (await registry.describe()).map((instance) => {
|
|
12722
13471
|
const entry = configs[instance.instanceId];
|
|
12723
|
-
const described =
|
|
13472
|
+
const described = {
|
|
13473
|
+
...instance,
|
|
13474
|
+
...(entry?.icon ? { icon: entry.icon } : {}),
|
|
13475
|
+
...(entry?.access ? { access: entry.access } : {}),
|
|
13476
|
+
};
|
|
12724
13477
|
if (hostedModels)
|
|
12725
13478
|
return { ...described, readOnly: true,
|
|
12726
13479
|
install: undefined, authentication: undefined, cli: undefined, cliCandidates: [],
|
|
@@ -12732,8 +13485,16 @@ async function describeInstances() {
|
|
|
12732
13485
|
...described, ...policy, readOnly: true, managed: managedDesktop.info(instance.instanceId),
|
|
12733
13486
|
install: undefined, authentication: undefined, cli: undefined, cliCandidates: [],
|
|
12734
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 };
|
|
12735
13492
|
if (entry?.driver !== "claudeAgent")
|
|
12736
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
|
+
}
|
|
12737
13498
|
try {
|
|
12738
13499
|
const claudeAccount = claudeAccountInfo(instance.instanceId, entry, instance.cli ?? instance.cliDefault ?? "claude");
|
|
12739
13500
|
return { ...described, ...policy, claudeAccount, install: { ...instance.install, signInCommand: claudeAccount.signInCommand } };
|
|
@@ -12872,6 +13633,7 @@ async function reloadProviders() {
|
|
|
12872
13633
|
stopScreenPoller(botId, threadId);
|
|
12873
13634
|
releaseLocalVmThread(threadId);
|
|
12874
13635
|
releaseTurnResources(owner);
|
|
13636
|
+
endForeignTurns(threadId);
|
|
12875
13637
|
vpsThreadEnded(botId, threadId);
|
|
12876
13638
|
watchdog.settle(threadId);
|
|
12877
13639
|
closeOpenApprovals(threadId);
|
|
@@ -12891,6 +13653,7 @@ async function reloadProviders() {
|
|
|
12891
13653
|
for (const [threadId, speaker] of rooms) {
|
|
12892
13654
|
releaseLocalVmThread(threadId);
|
|
12893
13655
|
releaseTurnResources(turnResourceOwners.get(threadId));
|
|
13656
|
+
endForeignTurns(threadId);
|
|
12894
13657
|
watchdog.settle(threadId);
|
|
12895
13658
|
closeOpenApprovals(threadId);
|
|
12896
13659
|
if (groupSpeakers.get(threadId) !== speaker)
|
|
@@ -12922,6 +13685,10 @@ let mcpProbesInFlight = 0;
|
|
|
12922
13685
|
// install, and running two self-updates against it would race its files.
|
|
12923
13686
|
const claudeUpdatesInFlight = new Set();
|
|
12924
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" };
|
|
12925
13692
|
/** The built UI, when this process serves it (OMB_STATIC_DIR: set by the
|
|
12926
13693
|
* desktop app and by the container image). Public by design: it is the same
|
|
12927
13694
|
* bundle anyone can download, holds no secrets, and a remote browser must be
|
|
@@ -12934,7 +13701,8 @@ function serveStatic(res, path) {
|
|
|
12934
13701
|
const file = join(STATIC_DIR, safe);
|
|
12935
13702
|
try {
|
|
12936
13703
|
const data = readFileSync(file);
|
|
12937
|
-
|
|
13704
|
+
const type = MIME[extname(file)] ?? "application/octet-stream";
|
|
13705
|
+
res.writeHead(200, { "content-type": type, ...(type === "text/html" ? PAGE_HEADERS : {}) });
|
|
12938
13706
|
res.end(data);
|
|
12939
13707
|
return true;
|
|
12940
13708
|
}
|
|
@@ -12942,7 +13710,7 @@ function serveStatic(res, path) {
|
|
|
12942
13710
|
// SPA fallback
|
|
12943
13711
|
try {
|
|
12944
13712
|
const data = readFileSync(join(STATIC_DIR, "index.html"));
|
|
12945
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
13713
|
+
res.writeHead(200, { "content-type": "text/html", ...PAGE_HEADERS });
|
|
12946
13714
|
res.end(data);
|
|
12947
13715
|
return true;
|
|
12948
13716
|
}
|
|
@@ -13026,6 +13794,23 @@ ROUTES.push(createBotMemoryRoutes({
|
|
|
13026
13794
|
bot: (id) => store.bot(id),
|
|
13027
13795
|
taskByThread: (botId, threadId) => store.taskByThread(botId, threadId),
|
|
13028
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
|
+
} : {}),
|
|
13029
13814
|
aboutMe: () => cfg.profile?.aboutMe ?? "",
|
|
13030
13815
|
saveAboutMe: (aboutMe) => {
|
|
13031
13816
|
saveConfig({ profile: { ...cfg.profile, aboutMe } });
|
|
@@ -13034,6 +13819,13 @@ ROUTES.push(createBotMemoryRoutes({
|
|
|
13034
13819
|
},
|
|
13035
13820
|
}));
|
|
13036
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);
|
|
13037
13829
|
const toolResults = new ToolResults();
|
|
13038
13830
|
const handleRequest = async (req, res) => {
|
|
13039
13831
|
let url;
|
|
@@ -13090,7 +13882,7 @@ const handleRequest = async (req, res) => {
|
|
|
13090
13882
|
if (method === "GET" && !path.startsWith("/api/") && !path.startsWith("/.well-known/") && serveStatic(res, path))
|
|
13091
13883
|
return;
|
|
13092
13884
|
if (method === "GET" && path === "/.well-known/openmausbot/environment") {
|
|
13093
|
-
return json(res, 200, environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: !HOSTED_WORKSPACE && emailSignIn.enabled(), sharedComputers: lendingEnabled() }));
|
|
13885
|
+
return json(res, 200, environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: !HOSTED_WORKSPACE && !CLOUD_HOME && emailSignIn.enabled(), sharedComputers: lendingEnabled() }));
|
|
13094
13886
|
}
|
|
13095
13887
|
const domainCheck = /^\/\.well-known\/openmausbot\/domain-check\/([a-f0-9]{64})$/.exec(path);
|
|
13096
13888
|
if (method === "GET" && domainCheck) {
|
|
@@ -13100,7 +13892,11 @@ const handleRequest = async (req, res) => {
|
|
|
13100
13892
|
}
|
|
13101
13893
|
// Sign in with an emailed code (server/account-signin.ts). Public like
|
|
13102
13894
|
// /api/auth/pair, JSON-only for the same reason, and counted against the
|
|
13103
|
-
// 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
|
+
}
|
|
13104
13900
|
if (method === "POST" && (path === "/api/auth/email/start" || path === "/api/auth/email/verify")) {
|
|
13105
13901
|
if (!/^application\/json\b/i.test(String(req.headers["content-type"] ?? ""))) {
|
|
13106
13902
|
return json(res, 415, { error: "send the sign-in request as JSON (content-type: application/json)" });
|
|
@@ -13170,13 +13966,27 @@ const handleRequest = async (req, res) => {
|
|
|
13170
13966
|
const wantsCookie = body?.cookie === true;
|
|
13171
13967
|
const label = typeof body?.label === "string" ? body.label : "";
|
|
13172
13968
|
const attemptId = typeof body?.attemptId === "string" ? body.attemptId : undefined;
|
|
13173
|
-
|
|
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"]) });
|
|
13174
13980
|
if (!result.ok) {
|
|
13175
13981
|
console.warn(`pairing refused from ${requestSource(req)}: ${result.error}`);
|
|
13176
13982
|
return json(res, result.status, { error: result.error });
|
|
13177
13983
|
}
|
|
13178
|
-
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: emailSignIn.enabled(), sharedComputers: lendingEnabled() });
|
|
13984
|
+
const environment = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED, emailSignIn: !CLOUD_HOME && emailSignIn.enabled(), sharedComputers: lendingEnabled() });
|
|
13179
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);
|
|
13180
13990
|
const secure = requestOrigin(req)?.startsWith("https://") === true;
|
|
13181
13991
|
res.setHeader("set-cookie", serializeSessionCookie(SESSION_COOKIE, result.token, { secure, maxAgeSeconds: cookieMaxAgeSeconds(result.session) }));
|
|
13182
13992
|
return json(res, 200, { session: result.session, environment });
|
|
@@ -13310,12 +14120,18 @@ const handleRequest = async (req, res) => {
|
|
|
13310
14120
|
environmentId: ENVIRONMENT_ID,
|
|
13311
14121
|
// the account behind the session, when it came from a sign-in
|
|
13312
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 } : {}),
|
|
13313
14125
|
// a hosted team workspace: the web UI's first run skips the
|
|
13314
14126
|
// desktop-only beats there. Absent everywhere else.
|
|
13315
14127
|
...(HOSTED_WORKSPACE ? { hosted: true } : {}),
|
|
13316
14128
|
// an OMB Cloud home: the web UI's first run is its engine
|
|
13317
14129
|
// sign-in (docs/cloud-pro.md). Absent everywhere else.
|
|
13318
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) } : {}),
|
|
13319
14135
|
});
|
|
13320
14136
|
}
|
|
13321
14137
|
if (method === "POST" && path === "/api/auth/stream-ticket") {
|
|
@@ -13339,7 +14155,16 @@ const handleRequest = async (req, res) => {
|
|
|
13339
14155
|
}
|
|
13340
14156
|
const requested = body?.scopes;
|
|
13341
14157
|
const scopes = Array.isArray(requested) ? requested.filter((v) => v === "admin" || v === "client") : undefined;
|
|
13342
|
-
|
|
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
|
+
}
|
|
13343
14168
|
const origin = requestOrigin(req);
|
|
13344
14169
|
const base = publicUrl() ?? (auth.kind === "session" && origin ? origin : null);
|
|
13345
14170
|
const code = formatPairingCode(opened.code);
|
|
@@ -13702,19 +14527,31 @@ const handleRequest = async (req, res) => {
|
|
|
13702
14527
|
// Notes written from a room fewer people can see than this bot would
|
|
13703
14528
|
// carry that room's words to everyone who can see the bot.
|
|
13704
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
|
+
}
|
|
13705
14539
|
const room = store.groupByThread(internalCapability.threadId);
|
|
13706
14540
|
if (room && !roomFeedsBot(room, internalSender)) {
|
|
13707
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." });
|
|
13708
14542
|
}
|
|
13709
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();
|
|
13710
14547
|
if (method === "POST" && path === "/api/internal/memory") {
|
|
13711
14548
|
const body = await readInternalBody();
|
|
13712
|
-
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() }));
|
|
13713
14550
|
return json(res, result.ok ? 200 : result.code === "conflict" ? 409 : result.code === "over-budget" ? 413 : 400, result);
|
|
13714
14551
|
}
|
|
13715
14552
|
if (method === "POST" && path === "/api/internal/memory/log") {
|
|
13716
14553
|
const body = await readInternalBody();
|
|
13717
|
-
const result = appendMemoryLog(internalSender.id, body.text, { source: memorySource() });
|
|
14554
|
+
const result = ownersWrite(() => appendMemoryLog(internalSender.id, body.text, { source: memorySource() }));
|
|
13718
14555
|
return json(res, result.ok ? 200 : 400, result);
|
|
13719
14556
|
}
|
|
13720
14557
|
if (method === "POST" && path === "/api/internal/browser/mcp") {
|
|
@@ -13762,8 +14599,8 @@ const handleRequest = async (req, res) => {
|
|
|
13762
14599
|
if (method === "GET" && path === "/api/internal/shared-computers" && lendingEnabled()) {
|
|
13763
14600
|
const principal = sharedComputerPrincipal(internalCapability);
|
|
13764
14601
|
// Say why an owner's own conversation sees no Mac, so the bot can tell them.
|
|
13765
|
-
const
|
|
13766
|
-
|
|
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 } : {};
|
|
13767
14604
|
return json(res, 200, { computers: sharedComputers.list(principal), ...notice });
|
|
13768
14605
|
}
|
|
13769
14606
|
if (method === "POST" && path === "/api/internal/shared-computers" && lendingEnabled()) {
|
|
@@ -13777,8 +14614,9 @@ const handleRequest = async (req, res) => {
|
|
|
13777
14614
|
return json(res, 403, { error: "This bot has no computer. Change its Computer setting to use a shared computer's apps and screen." });
|
|
13778
14615
|
}
|
|
13779
14616
|
const principal = sharedComputerPrincipal(internalCapability);
|
|
13780
|
-
|
|
13781
|
-
|
|
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 });
|
|
13782
14620
|
}
|
|
13783
14621
|
return json(res, 200, { result: await sharedComputers.request(parsed.data, principal, () => lendingEnabled() && internalCapabilityIsActive(internalCapability)) });
|
|
13784
14622
|
}
|
|
@@ -13837,7 +14675,13 @@ const handleRequest = async (req, res) => {
|
|
|
13837
14675
|
return "closed";
|
|
13838
14676
|
return "idle";
|
|
13839
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;
|
|
13840
14682
|
for (const task of store.tasks(from.id)) {
|
|
14683
|
+
if (!shown(task.threadId))
|
|
14684
|
+
continue;
|
|
13841
14685
|
rows.push({
|
|
13842
14686
|
threadId: task.threadId, botId: from.id, botName: from.name, title: task.title,
|
|
13843
14687
|
state: stateOf(from, task), unread: task.unread === true, openedAt: task.openedBy?.at ?? task.createdAt,
|
|
@@ -13846,7 +14690,7 @@ const handleRequest = async (req, res) => {
|
|
|
13846
14690
|
}
|
|
13847
14691
|
for (const peer of reachablePeers(store.bots, from)) {
|
|
13848
14692
|
for (const task of store.tasks(peer.id)) {
|
|
13849
|
-
if (task.openedBy?.botId !== from.id)
|
|
14693
|
+
if (task.openedBy?.botId !== from.id || !shown(task.threadId))
|
|
13850
14694
|
continue;
|
|
13851
14695
|
rows.push({
|
|
13852
14696
|
threadId: task.threadId, botId: peer.id, botName: peer.name, title: task.title,
|
|
@@ -14079,6 +14923,12 @@ const handleRequest = async (req, res) => {
|
|
|
14079
14923
|
: body.action === "update"
|
|
14080
14924
|
? { action: body.action, routineId: body.routineId, changes: body.changes, forBot }
|
|
14081
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);
|
|
14082
14932
|
const proposed = await routineRequests.submit({
|
|
14083
14933
|
botId: from.id,
|
|
14084
14934
|
threadId: fromThreadId,
|
|
@@ -14087,6 +14937,17 @@ const handleRequest = async (req, res) => {
|
|
|
14087
14937
|
canCommit: () => internalCapabilityIsActive(internalCapability),
|
|
14088
14938
|
});
|
|
14089
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
|
+
}
|
|
14090
14951
|
appendDecision(DATA_DIR, {
|
|
14091
14952
|
threadId: fromThreadId,
|
|
14092
14953
|
requestId: proposed.requestId,
|
|
@@ -14305,7 +15166,7 @@ const handleRequest = async (req, res) => {
|
|
|
14305
15166
|
if (scope !== "all" && scope !== "conversations" && scope !== "memory") {
|
|
14306
15167
|
return json(res, 400, { error: "scope must be all, conversations, or memory" });
|
|
14307
15168
|
}
|
|
14308
|
-
const memoryHits = scope === "conversations" || !q ? [] : searchMemoryFiles(from.id, q, limit);
|
|
15169
|
+
const memoryHits = scope === "conversations" || !q || from.memoryEnabled === false ? [] : searchMemoryFiles(from.id, q, limit);
|
|
14309
15170
|
if (scope === "memory")
|
|
14310
15171
|
return json(res, 200, { hits: [], memoryHits });
|
|
14311
15172
|
// Own threads: the bot's main chat and tasks, and the rooms it is a
|
|
@@ -14321,7 +15182,10 @@ const handleRequest = async (req, res) => {
|
|
|
14321
15182
|
for (const task of group.tasks ?? [])
|
|
14322
15183
|
roomByThread.set(task.threadId, group);
|
|
14323
15184
|
}
|
|
14324
|
-
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);
|
|
14325
15189
|
// A room is the only place a recall can be a disclosure, and only a
|
|
14326
15190
|
// private chat is one: in a 1:1 the user already owns every thread
|
|
14327
15191
|
// the bot can reach, and a room's lines were said in the open.
|
|
@@ -14360,7 +15224,8 @@ const handleRequest = async (req, res) => {
|
|
|
14360
15224
|
const messageId = String(url.searchParams.get("messageId") ?? "").trim();
|
|
14361
15225
|
if (!threadId || !messageId)
|
|
14362
15226
|
return json(res, 400, { error: "threadId and messageId are required" });
|
|
14363
|
-
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));
|
|
14364
15229
|
const message = own ? readMessageText(threadId, messageId) : null;
|
|
14365
15230
|
if (!message)
|
|
14366
15231
|
return json(res, 404, { error: "no such message in your conversations" });
|
|
@@ -14530,7 +15395,12 @@ const handleRequest = async (req, res) => {
|
|
|
14530
15395
|
// can check next turn. If the ledger refuses (cap/depth), fall back
|
|
14531
15396
|
// to the plain busy bounce rather than dropping the refusal reason.
|
|
14532
15397
|
const queueBusyFallback = (approvalAlreadyGranted = false) => {
|
|
14533
|
-
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);
|
|
14534
15404
|
if (queued.result !== "ok" || !queued.id)
|
|
14535
15405
|
return json(res, 200, { busy: true, receipt: peerDeliveryReceipt({
|
|
14536
15406
|
botId: toBotId, botName: target.name, outcome: "failed",
|
|
@@ -14558,6 +15428,10 @@ const handleRequest = async (req, res) => {
|
|
|
14558
15428
|
const tryAsideDelivery = async () => {
|
|
14559
15429
|
if (peerReviewRequired(from, fromThreadId))
|
|
14560
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;
|
|
14561
15435
|
// Busy elsewhere (room turn, sibling thread): the thread this ask
|
|
14562
15436
|
// targets is not the running one, so there is nothing to fold
|
|
14563
15437
|
// into here — the queue lane keeps it.
|
|
@@ -14664,7 +15538,10 @@ const handleRequest = async (req, res) => {
|
|
|
14664
15538
|
delivery: "ask_bot",
|
|
14665
15539
|
unattended: isUnattended(currentFrom.id, fromThreadId),
|
|
14666
15540
|
});
|
|
14667
|
-
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;
|
|
14668
15545
|
const outcome = await askBotAndWait(toBotId, prefixed, depth, fromBotId, fromThreadId, targetThreadId);
|
|
14669
15546
|
requireActiveInternalCapability();
|
|
14670
15547
|
const replySender = store.bot(fromBotId);
|
|
@@ -14876,7 +15753,12 @@ const handleRequest = async (req, res) => {
|
|
|
14876
15753
|
if (!connectorThread(from.id, fromThreadId)) {
|
|
14877
15754
|
return json(res, 403, { error: "source thread does not belong to sender" });
|
|
14878
15755
|
}
|
|
14879
|
-
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);
|
|
14880
15762
|
if (queued.result !== "ok" || !queued.id) {
|
|
14881
15763
|
// the agent reads this string — a bare enum ("too_deep") tells it
|
|
14882
15764
|
// nothing about what to do instead
|
|
@@ -14944,7 +15826,7 @@ const handleRequest = async (req, res) => {
|
|
|
14944
15826
|
})).filter(g => g.members.length);
|
|
14945
15827
|
return json(res, 200, { currentRoom: source ? { id: source.id, name: source.name, workingFolder: source.cwd || null } : null,
|
|
14946
15828
|
bots: reachablePeers(store.bots, internalSender).map(bot => ({ id: bot.id, name: bot.name, title: bot.title, section: bot.section, busy: bot.busy })),
|
|
14947
|
-
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." });
|
|
14948
15830
|
}
|
|
14949
15831
|
if (method === "POST" && path === "/api/internal/coordinate-bots") {
|
|
14950
15832
|
const parsed = z.object({
|
|
@@ -15021,39 +15903,21 @@ const handleRequest = async (req, res) => {
|
|
|
15021
15903
|
try {
|
|
15022
15904
|
requireActiveInternalCapability();
|
|
15023
15905
|
if (!destination) {
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
// request key, or the thread the person has selected there.
|
|
15027
|
-
const resolved = store.resolvePairConversation(internalSender, target.botId, {
|
|
15028
|
-
label: parsed.data.label,
|
|
15029
|
-
// "Still working" exactly as close_thread reads it: a
|
|
15030
|
-
// running turn, a queued one, or coordinated work already
|
|
15031
|
-
// addressed at that thread.
|
|
15032
|
-
working: threadId => threadBusy(target.botId, threadId)
|
|
15033
|
-
|| queuedThreadPosition(target.botId, threadId) !== null
|
|
15034
|
-
|| roomHandoffs.activeDirect(threadId),
|
|
15035
|
-
});
|
|
15036
|
-
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)
|
|
15037
15908
|
throw new Error("The recipient no longer exists");
|
|
15038
|
-
target.threadId =
|
|
15039
|
-
if (resolved.created)
|
|
15040
|
-
createdThread = resolved.task.threadId;
|
|
15041
|
-
// A work thread carries one assignment: it is for the person
|
|
15042
|
-
// this coordination serves. The durable pair conversation is
|
|
15043
|
-
// shared by every assignment between two bots, so it names nobody.
|
|
15044
|
-
if (resolved.created && resolved.task.openedBy?.kind === "work")
|
|
15045
|
-
threadStarters.set(resolved.task.threadId, threadPersonKey(address.threadId));
|
|
15909
|
+
target.threadId = createdThread = task.threadId;
|
|
15046
15910
|
if (delegatedFullAccess(internalSender, internalCapability.threadId, store.bot(target.botId))) {
|
|
15047
15911
|
grantDelegatedFullAccess(internalSender, store.bot(target.botId), target.threadId);
|
|
15048
15912
|
}
|
|
15049
15913
|
}
|
|
15050
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);
|
|
15051
|
-
// A
|
|
15052
|
-
|
|
15053
|
-
// conversation was busy with that very request) goes back
|
|
15054
|
-
// before anyone sees a row that leads nowhere.
|
|
15055
|
-
if (duplicate && createdThread && createdThread !== node.threadId)
|
|
15915
|
+
// A retry keeps its original task; discard the speculative row.
|
|
15916
|
+
if (duplicate && createdThread && createdThread !== node.threadId) {
|
|
15056
15917
|
store.deleteTask(target.botId, createdThread);
|
|
15918
|
+
}
|
|
15919
|
+
if (!duplicate && createdThread)
|
|
15920
|
+
threadStarters.set(createdThread, openerFrom(address.threadId));
|
|
15057
15921
|
createdThread = undefined; // The durable coordinator now owns this task.
|
|
15058
15922
|
accepted.push({ requestId: node.id, botId: node.botId, duplicate, status: node.status });
|
|
15059
15923
|
// A duplicate request_key lands on the node enqueue already
|
|
@@ -15093,8 +15957,9 @@ const handleRequest = async (req, res) => {
|
|
|
15093
15957
|
}
|
|
15094
15958
|
}
|
|
15095
15959
|
catch (error) {
|
|
15096
|
-
if (createdThread)
|
|
15960
|
+
if (createdThread) {
|
|
15097
15961
|
store.deleteTask(target.botId, createdThread);
|
|
15962
|
+
}
|
|
15098
15963
|
const said = error instanceof Error ? error.message : String(error);
|
|
15099
15964
|
errors.push({ botId: target.botId, error: said });
|
|
15100
15965
|
receipts.push(peerDeliveryReceipt({
|
|
@@ -15337,10 +16202,11 @@ const handleRequest = async (req, res) => {
|
|
|
15337
16202
|
}
|
|
15338
16203
|
const sourceTitle = owner.group ? owner.group.name : (store.taskByThread(from.id, fromThreadId)?.title ?? "");
|
|
15339
16204
|
if (target.id === from.id) {
|
|
15340
|
-
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);
|
|
15341
16207
|
if (!task)
|
|
15342
16208
|
return json(res, 500, { error: "couldn't create that thread" });
|
|
15343
|
-
threadStarters.set(task.threadId,
|
|
16209
|
+
threadStarters.set(task.threadId, openerFrom(fromThreadId));
|
|
15344
16210
|
internalCapability.openedThreads += 1;
|
|
15345
16211
|
const chip = {
|
|
15346
16212
|
role: "bot",
|
|
@@ -15383,7 +16249,7 @@ const handleRequest = async (req, res) => {
|
|
|
15383
16249
|
if (!task)
|
|
15384
16250
|
return json(res, 500, { error: "couldn't create that thread" });
|
|
15385
16251
|
// The work is still for the person whose request the opener is on.
|
|
15386
|
-
threadStarters.set(task.threadId,
|
|
16252
|
+
threadStarters.set(task.threadId, openerFrom(fromThreadId));
|
|
15387
16253
|
if (delegatedFullAccess(from, fromThreadId, target))
|
|
15388
16254
|
grantDelegatedFullAccess(from, target, task.threadId);
|
|
15389
16255
|
const queued = queueDelegation(commsBus, from, { toBotId: target.id, message, depth, targetThreadId: task.threadId }, MAX_COMMS_DEPTH, fromThreadId);
|
|
@@ -15932,16 +16798,25 @@ const handleRequest = async (req, res) => {
|
|
|
15932
16798
|
.filter((run) => routineVisible(run, visible)),
|
|
15933
16799
|
});
|
|
15934
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;
|
|
15935
16805
|
if (path === "/api/routines" && method === "POST") {
|
|
15936
16806
|
const body = await readBody(req);
|
|
15937
16807
|
const hidden = hiddenRoutineTarget(body, visible);
|
|
15938
16808
|
if (hidden)
|
|
15939
16809
|
return json(res, 404, { error: hidden });
|
|
15940
|
-
const
|
|
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));
|
|
15941
16815
|
// On a Cloud home, a routine the owner writes from their own device may
|
|
15942
16816
|
// use their lent Mac when it runs (server/cloud-lending.ts).
|
|
15943
16817
|
if (cloudRoutineAuthors && cloudOwnerSession(auth))
|
|
15944
16818
|
cloudRoutineAuthors.record(routine.id, routine);
|
|
16819
|
+
cloudRoutineAuthors?.wrote(routine.id, writer);
|
|
15945
16820
|
return json(res, 201, { routine });
|
|
15946
16821
|
}
|
|
15947
16822
|
// The desktop shell polls this to decide whether to hold the computer
|
|
@@ -15962,9 +16837,27 @@ const handleRequest = async (req, res) => {
|
|
|
15962
16837
|
const hidden = hiddenRoutineTarget(body, visible);
|
|
15963
16838
|
if (hidden)
|
|
15964
16839
|
return json(res, 404, { error: hidden });
|
|
16840
|
+
const notYours = routineResultsRefusal(body);
|
|
16841
|
+
if (notYours)
|
|
16842
|
+
return json(res, 403, { error: notYours });
|
|
15965
16843
|
const before = cloudRoutineAuthors ? routines.listRoutines().find((candidate) => candidate.id === routineMatch[1]) : undefined;
|
|
15966
16844
|
const wasOwners = Boolean(before && cloudRoutineAuthors?.authored(before.id, before));
|
|
15967
|
-
const
|
|
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));
|
|
15968
16861
|
// The owner's own edit keeps (or, when it rewrites the instructions,
|
|
15969
16862
|
// makes) the routine theirs. Anyone else's edit, of any field (its
|
|
15970
16863
|
// schedule, where its results go, whether it is on), makes it no longer
|
|
@@ -15972,15 +16865,16 @@ const handleRequest = async (req, res) => {
|
|
|
15972
16865
|
if (routine && cloudRoutineAuthors) {
|
|
15973
16866
|
if (!cloudOwnerSession(auth))
|
|
15974
16867
|
cloudRoutineAuthors.forget(routine.id);
|
|
15975
|
-
else if (
|
|
16868
|
+
else if (ownerAuthors)
|
|
15976
16869
|
cloudRoutineAuthors.record(routine.id, routine);
|
|
16870
|
+
cloudRoutineAuthors.wrote(routine.id, writer);
|
|
15977
16871
|
}
|
|
15978
16872
|
return routine ? json(res, 200, { routine }) : json(res, 404, { error: "no such routine" });
|
|
15979
16873
|
}
|
|
15980
16874
|
if (routineMatch && method === "DELETE") {
|
|
15981
16875
|
const removed = routines.remove(routineMatch[1]);
|
|
15982
16876
|
if (removed)
|
|
15983
|
-
cloudRoutineAuthors?.
|
|
16877
|
+
cloudRoutineAuthors?.remove(routineMatch[1]);
|
|
15984
16878
|
return removed
|
|
15985
16879
|
? json(res, 200, { ok: true })
|
|
15986
16880
|
: json(res, 404, { error: "no such routine" });
|
|
@@ -16742,6 +17636,9 @@ const handleRequest = async (req, res) => {
|
|
|
16742
17636
|
if (hiddenMember)
|
|
16743
17637
|
return json(res, 400, { error: `unknown channel member: ${String(hiddenMember)}` });
|
|
16744
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));
|
|
16745
17642
|
return json(res, 201, { group: { ...publicGroupState(group), messages: [] } });
|
|
16746
17643
|
}
|
|
16747
17644
|
if (method === "POST" && path === "/api/teams/export") {
|
|
@@ -17209,7 +18106,7 @@ const handleRequest = async (req, res) => {
|
|
|
17209
18106
|
return json(res, 500, { error: "couldn't create that task" });
|
|
17210
18107
|
// Who opened it decides who may answer its cards on a shared workspace.
|
|
17211
18108
|
if (auth.kind === "session")
|
|
17212
|
-
threadStarters.set(task.threadId,
|
|
18109
|
+
threadStarters.set(task.threadId, actorKey(auth));
|
|
17213
18110
|
const fresh = groupWithThread(store.group(group.id));
|
|
17214
18111
|
broadcast({ kind: "group", group: fresh });
|
|
17215
18112
|
return json(res, 201, { group: fresh, task });
|
|
@@ -17286,6 +18183,9 @@ const handleRequest = async (req, res) => {
|
|
|
17286
18183
|
// without the old handler turning a missing title into "Untitled".
|
|
17287
18184
|
// That echo is not a rename. A real rename stays blocked while working.
|
|
17288
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 });
|
|
17289
18189
|
if (channelTaskBlocked(group) && !(pinning && !titleChange)) {
|
|
17290
18190
|
return json(res, 409, { error: "this channel is working or waiting on you — finish that turn first" });
|
|
17291
18191
|
}
|
|
@@ -17318,6 +18218,9 @@ const handleRequest = async (req, res) => {
|
|
|
17318
18218
|
}
|
|
17319
18219
|
if (!store.groupTaskByThread(group.id, m[2]))
|
|
17320
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 });
|
|
17321
18224
|
const stagedSkillCleanups = stagedSkillCleanupsForThread(m[2]);
|
|
17322
18225
|
lastReply.delete(m[2]);
|
|
17323
18226
|
cancelTeamSetupResumesForThread(m[2]);
|
|
@@ -17339,6 +18242,14 @@ const handleRequest = async (req, res) => {
|
|
|
17339
18242
|
if (field)
|
|
17340
18243
|
return json(res, 403, { error: `forbidden: this session may rename or mark a room, not change "${field}" (needs the admin scope)` });
|
|
17341
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
|
+
}
|
|
17342
18253
|
const group = updateChannel(m[1], body);
|
|
17343
18254
|
return json(res, 200, { group: publicGroupState(group) });
|
|
17344
18255
|
}
|
|
@@ -17963,9 +18874,14 @@ const handleRequest = async (req, res) => {
|
|
|
17963
18874
|
throw Object.assign(new Error(enabled.error), { status: 422 });
|
|
17964
18875
|
}
|
|
17965
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;
|
|
17966
18880
|
for (const routine of template.routines) {
|
|
17967
18881
|
const created = routines.create({ ...routine, botId: bot.id, enabled: false });
|
|
17968
18882
|
createdRoutines.push({ id: created.id, enabled: routine.enabled !== false });
|
|
18883
|
+
if (ownersRoutines)
|
|
18884
|
+
cloudRoutineAuthors?.wrote(created.id, ownersRoutines);
|
|
17969
18885
|
}
|
|
17970
18886
|
}
|
|
17971
18887
|
catch (error) {
|
|
@@ -18051,6 +18967,17 @@ const handleRequest = async (req, res) => {
|
|
|
18051
18967
|
return json(res, 400, { error: "avatarUrl must reference an existing stored image" });
|
|
18052
18968
|
}
|
|
18053
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
|
+
}
|
|
18054
18981
|
const beforeProfile = existingBot ? profileSnapshot(existingBot) : undefined;
|
|
18055
18982
|
const bot = store.patchBotProfile(m[1], parsed.patch);
|
|
18056
18983
|
if (!bot)
|
|
@@ -18304,6 +19231,15 @@ const handleRequest = async (req, res) => {
|
|
|
18304
19231
|
return json(res, 400, { error: "voiceNotes must be true or false" });
|
|
18305
19232
|
patch.voiceNotes = body.voiceNotes;
|
|
18306
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
|
+
}
|
|
18307
19243
|
// which of those apps' tools this bot may call (connector grants 1/5:
|
|
18308
19244
|
// data model only — enforcement lands with slice 2). null returns the
|
|
18309
19245
|
// bot to boolean-only legacy behavior; {} means no tools.
|
|
@@ -18640,6 +19576,13 @@ const handleRequest = async (req, res) => {
|
|
|
18640
19576
|
sectionKey(existingBot?.section) !== sectionKey(section);
|
|
18641
19577
|
let bot;
|
|
18642
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
|
+
}
|
|
18643
19586
|
// Custom servers are process configuration: a running turn cannot
|
|
18644
19587
|
// unmount them. Recheck after any awaited runtime revocation above.
|
|
18645
19588
|
if (body.mcpServers !== undefined) {
|
|
@@ -18695,7 +19638,7 @@ const handleRequest = async (req, res) => {
|
|
|
18695
19638
|
}
|
|
18696
19639
|
if (!bot)
|
|
18697
19640
|
return json(res, 404, { error: "no such bot" });
|
|
18698
|
-
if (body.memoryUpkeep === false)
|
|
19641
|
+
if (body.memoryUpkeep === false || body.memoryEnabled === false)
|
|
18699
19642
|
memoryUpkeep.dropBot(bot.id);
|
|
18700
19643
|
// A defined Works on is the newest explicit choice: this bot's
|
|
18701
19644
|
// auto-recorded pins that now point elsewhere give way immediately, so
|
|
@@ -19165,6 +20108,9 @@ const handleRequest = async (req, res) => {
|
|
|
19165
20108
|
// receipt after a task switch, while a genuinely new send still has to
|
|
19166
20109
|
// target the task that is active now.
|
|
19167
20110
|
const threadId = body.threadId ?? bot.threadId;
|
|
20111
|
+
const notYours = cloudGuestSendRefusal(auth, threadId);
|
|
20112
|
+
if (notYours)
|
|
20113
|
+
return json(res, 403, { error: notYours });
|
|
19168
20114
|
// Who this message is from, for the ledger. It is captured here and
|
|
19169
20115
|
// travels with the message: into the turn it starts, or into the queue
|
|
19170
20116
|
// until it drains. A message steered into someone else's running turn
|
|
@@ -19425,6 +20371,9 @@ const handleRequest = async (req, res) => {
|
|
|
19425
20371
|
const body = await readBody(req);
|
|
19426
20372
|
requirePinnedClientThread(m[1], body?.threadId);
|
|
19427
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 });
|
|
19428
20377
|
await startTurn(bot.id, "Summarize conversation context", {
|
|
19429
20378
|
threadId: bot.threadId, compactOnly: true, cardContinuation: true, trigger: usageTriggerFor(auth),
|
|
19430
20379
|
});
|
|
@@ -19440,6 +20389,9 @@ const handleRequest = async (req, res) => {
|
|
|
19440
20389
|
const body = await readBody(req);
|
|
19441
20390
|
requirePinnedClientThread(m[1], body?.threadId);
|
|
19442
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 });
|
|
19443
20395
|
const text = String(body.text ?? "").trim();
|
|
19444
20396
|
if (!text)
|
|
19445
20397
|
return json(res, 400, { error: "text required" });
|
|
@@ -19492,6 +20444,9 @@ const handleRequest = async (req, res) => {
|
|
|
19492
20444
|
const body = await readBody(req);
|
|
19493
20445
|
requirePinnedClientThread(m[1], body?.threadId);
|
|
19494
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 });
|
|
19495
20450
|
if (threadBusy(bot.id, bot.threadId))
|
|
19496
20451
|
return json(res, 409, { error: "the thread is working — stop it before switching versions" });
|
|
19497
20452
|
if (phoneSecretSubmissions.hasThread(bot.threadId)) {
|
|
@@ -19532,7 +20487,7 @@ const handleRequest = async (req, res) => {
|
|
|
19532
20487
|
threadId: bot.threadId,
|
|
19533
20488
|
requestId: String(body.requestId),
|
|
19534
20489
|
behavior,
|
|
19535
|
-
}))
|
|
20490
|
+
}, auth.kind === "session" && cloudOwnerSession(auth)))
|
|
19536
20491
|
return;
|
|
19537
20492
|
if (resolveAndSendProfile(res, {
|
|
19538
20493
|
botId: bot.id,
|
|
@@ -19630,7 +20585,7 @@ const handleRequest = async (req, res) => {
|
|
|
19630
20585
|
threadId,
|
|
19631
20586
|
requestId,
|
|
19632
20587
|
behavior,
|
|
19633
|
-
}))
|
|
20588
|
+
}, auth.kind === "session" && cloudOwnerSession(auth)))
|
|
19634
20589
|
return;
|
|
19635
20590
|
}
|
|
19636
20591
|
const setupCard = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId && message.card.teamSetupRequest);
|
|
@@ -19869,7 +20824,7 @@ const handleRequest = async (req, res) => {
|
|
|
19869
20824
|
return json(res, 500, { error: "couldn't create that task" });
|
|
19870
20825
|
// Who opened it decides who may answer its cards on a shared workspace.
|
|
19871
20826
|
if (auth.kind === "session")
|
|
19872
|
-
threadStarters.set(task.threadId,
|
|
20827
|
+
threadStarters.set(task.threadId, actorKey(auth));
|
|
19873
20828
|
const fresh = botWithThread(store.bot(bot.id));
|
|
19874
20829
|
broadcast({ kind: "bot", bot: fresh });
|
|
19875
20830
|
return json(res, 201, { bot: fresh, task: wireTask(task) });
|
|
@@ -19915,6 +20870,16 @@ const handleRequest = async (req, res) => {
|
|
|
19915
20870
|
const allowed = new Set(["title", "projectId", "modelSelection", "updateBotDefault", "resetApprovalToAsk", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto", "archivedAt", "pinned", "snoozedUntil", "surface"]);
|
|
19916
20871
|
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
19917
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
|
+
}
|
|
19918
20883
|
for (const key of ["requireAvailableModel", "acknowledgeLocalAuto", "updateBotDefault", "resetApprovalToAsk"]) {
|
|
19919
20884
|
if (body[key] !== undefined && typeof body[key] !== "boolean")
|
|
19920
20885
|
return json(res, 400, { error: `${key} must be a boolean` });
|
|
@@ -20040,6 +21005,9 @@ const handleRequest = async (req, res) => {
|
|
|
20040
21005
|
if (!bot || !store.taskByThread(bot.id, m[2])) {
|
|
20041
21006
|
return json(res, 404, { error: "no such task" });
|
|
20042
21007
|
}
|
|
21008
|
+
const notYours = cloudThreadRefusal(auth, m[2]);
|
|
21009
|
+
if (notYours)
|
|
21010
|
+
return json(res, 403, { error: notYours });
|
|
20043
21011
|
if (phoneSecretSubmissions.hasThread(m[2])) {
|
|
20044
21012
|
return json(res, 409, { error: "this task is securely saving a credential — try again when it finishes" });
|
|
20045
21013
|
}
|
|
@@ -20060,6 +21028,55 @@ const handleRequest = async (req, res) => {
|
|
|
20060
21028
|
broadcast({ kind: "bot", bot: fresh });
|
|
20061
21029
|
return json(res, 200, { bot: fresh });
|
|
20062
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
|
+
}
|
|
20063
21080
|
// Named team Boats use real independent ownership, never a hidden bot or
|
|
20064
21081
|
// an arbitrary provider id. These new routes remain admin-only by default.
|
|
20065
21082
|
if (path === "/api/team-computers" && method === "GET") {
|
|
@@ -20253,7 +21270,7 @@ const handleRequest = async (req, res) => {
|
|
|
20253
21270
|
// what the user's machine can host: which runtime is installed, whether
|
|
20254
21271
|
// its daemon is up, and whether the desktop image and container exist
|
|
20255
21272
|
if (method === "GET" && path === "/api/local-computer") {
|
|
20256
|
-
return json(res, 200, await localVmPayload(SHARED_LOCAL_VM_TARGET));
|
|
21273
|
+
return json(res, 200, await localVmPayload(SHARED_LOCAL_VM_TARGET, auth));
|
|
20257
21274
|
}
|
|
20258
21275
|
if (method === "GET" && path === "/api/local-computer/instances") {
|
|
20259
21276
|
res.setHeader("cache-control", "private, no-store");
|
|
@@ -20299,9 +21316,9 @@ const handleRequest = async (req, res) => {
|
|
|
20299
21316
|
if (action === "stop" || action === "remove")
|
|
20300
21317
|
localVmIdleFor(SHARED_LOCAL_VM_TARGET).cancel();
|
|
20301
21318
|
return json(res, 200, {
|
|
20302
|
-
...status,
|
|
21319
|
+
...localVmViewerStatus(status, auth),
|
|
20303
21320
|
commands: setupCommands(status.runtime, process.platform, SHARED_LOCAL_VM_TARGET),
|
|
20304
|
-
idle_timeout_ms:
|
|
21321
|
+
idle_timeout_ms: localVmIdleMs(),
|
|
20305
21322
|
mode: localVmMode(cfg),
|
|
20306
21323
|
max_instances: localVmMaxInstances(cfg),
|
|
20307
21324
|
});
|
|
@@ -20325,7 +21342,7 @@ const handleRequest = async (req, res) => {
|
|
|
20325
21342
|
const bot = computerPreviewBot(m[1], url);
|
|
20326
21343
|
if (!bot)
|
|
20327
21344
|
return json(res, 404, { error: "no such bot" });
|
|
20328
|
-
return json(res, 200, await localVmPayload(localVmTargetForStatus(bot.id, bot.threadId)));
|
|
21345
|
+
return json(res, 200, await localVmPayload(localVmTargetForStatus(bot.id, bot.threadId), auth));
|
|
20329
21346
|
}
|
|
20330
21347
|
m = path.match(/^\/api\/bots\/([\w-]+)\/local-computer\/(run|stop|remove)$/);
|
|
20331
21348
|
if (m && method === "POST") {
|
|
@@ -20382,9 +21399,9 @@ const handleRequest = async (req, res) => {
|
|
|
20382
21399
|
if (action === "stop" || action === "remove")
|
|
20383
21400
|
localVmIdleFor(target).cancel();
|
|
20384
21401
|
return json(res, 200, {
|
|
20385
|
-
...status,
|
|
21402
|
+
...localVmViewerStatus(status, auth),
|
|
20386
21403
|
commands: setupCommands(status.runtime, process.platform, target),
|
|
20387
|
-
idle_timeout_ms:
|
|
21404
|
+
idle_timeout_ms: localVmIdleMs(),
|
|
20388
21405
|
mode: localVmMode(cfg),
|
|
20389
21406
|
max_instances: localVmMaxInstances(cfg),
|
|
20390
21407
|
});
|
|
@@ -20549,7 +21566,7 @@ const handleRequest = async (req, res) => {
|
|
|
20549
21566
|
return json(res, 400, { error: `provider must be one of ${PROVIDER_KEY_KINDS.join(", ")}` });
|
|
20550
21567
|
}
|
|
20551
21568
|
const kind = provider;
|
|
20552
|
-
const saved =
|
|
21569
|
+
const saved = { anthropic: cfg.anthropic, openai: cfg.openai, openrouter: cfg.openrouter, openaiCompat: cfg.openaiCompat, mistral: cfg.mistral, xai: cfg.xai }[kind];
|
|
20553
21570
|
if (body?.key !== undefined && typeof body.key !== "string") {
|
|
20554
21571
|
return json(res, 400, { error: "key must be a string" });
|
|
20555
21572
|
}
|
|
@@ -20666,6 +21683,30 @@ const handleRequest = async (req, res) => {
|
|
|
20666
21683
|
providerConfigBusy = false;
|
|
20667
21684
|
}
|
|
20668
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
|
+
}
|
|
20669
21710
|
if (method === "POST" && path === "/api/instances/claude-accounts") {
|
|
20670
21711
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
20671
21712
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
@@ -20716,6 +21757,11 @@ const handleRequest = async (req, res) => {
|
|
|
20716
21757
|
const instance = registry.get(instanceId);
|
|
20717
21758
|
if (!instance)
|
|
20718
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
|
+
}
|
|
20719
21765
|
const started = await providerAuthSessions.start(instance, owner);
|
|
20720
21766
|
// Revocation can arrive while the CLI is obtaining a device code.
|
|
20721
21767
|
if (auth.kind === "session" && !sessions.isLive(auth.session.id)) {
|
|
@@ -20939,13 +21985,78 @@ const handleRequest = async (req, res) => {
|
|
|
20939
21985
|
res.once("close", disconnect);
|
|
20940
21986
|
mcpProbesInFlight += 1;
|
|
20941
21987
|
try {
|
|
20942
|
-
|
|
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);
|
|
20943
21999
|
}
|
|
20944
22000
|
finally {
|
|
20945
22001
|
res.off("close", disconnect);
|
|
20946
22002
|
mcpProbesInFlight -= 1;
|
|
20947
22003
|
}
|
|
20948
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
|
+
}
|
|
20949
22060
|
if (method === "POST" && path === "/api/mcp/servers") {
|
|
20950
22061
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
20951
22062
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
@@ -21031,6 +22142,7 @@ const handleRequest = async (req, res) => {
|
|
|
21031
22142
|
const next = { ...current };
|
|
21032
22143
|
delete next[name];
|
|
21033
22144
|
persistMcpServers(next);
|
|
22145
|
+
mcpOAuth.forget(name);
|
|
21034
22146
|
return json(res, 200, mcpServerResponse());
|
|
21035
22147
|
}
|
|
21036
22148
|
const existing = parseStoredMcpServer(name, current[name]);
|
|
@@ -21052,6 +22164,17 @@ const handleRequest = async (req, res) => {
|
|
|
21052
22164
|
if (refusal)
|
|
21053
22165
|
return json(res, 403, { error: refusal, code: "managed_policy" });
|
|
21054
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);
|
|
21055
22178
|
return json(res, 200, mcpServerResponse());
|
|
21056
22179
|
}
|
|
21057
22180
|
finally {
|
|
@@ -21064,9 +22187,13 @@ const handleRequest = async (req, res) => {
|
|
|
21064
22187
|
}
|
|
21065
22188
|
if ((method === "PUT" || method === "PATCH") && path === "/api/config") {
|
|
21066
22189
|
const body = await readBody(req);
|
|
21067
|
-
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)))
|
|
21068
22191
|
return json(res, 403, { error: HOSTED_PROVIDER_SETTINGS_ERROR });
|
|
21069
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
|
+
}
|
|
21070
22197
|
if (patch.newBotDefaults) {
|
|
21071
22198
|
if (patch.newBotDefaults.profile.modelSelection) {
|
|
21072
22199
|
const checked = checkedModelSelection(patch.newBotDefaults.profile.modelSelection);
|
|
@@ -21506,9 +22633,17 @@ const handleRequest = async (req, res) => {
|
|
|
21506
22633
|
}
|
|
21507
22634
|
throw error;
|
|
21508
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
|
+
}
|
|
21509
22642
|
let browserReferenceCleanupError = null;
|
|
21510
|
-
if (patch.signIn !== undefined)
|
|
22643
|
+
if (patch.signIn !== undefined) {
|
|
21511
22644
|
sessions.revalidateEmailSessions();
|
|
22645
|
+
resyncOpenCodeProviderKeys();
|
|
22646
|
+
}
|
|
21512
22647
|
if (!lendingEnabled()) {
|
|
21513
22648
|
sharedComputers.close();
|
|
21514
22649
|
sharedComputerControl.close();
|
|
@@ -21969,6 +23104,10 @@ const handleRequest = async (req, res) => {
|
|
|
21969
23104
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
21970
23105
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
21971
23106
|
}
|
|
23107
|
+
if (m[2] === "join" && bot.cloudBackend === "vps") {
|
|
23108
|
+
for (let pending; (pending = vpsJoinRequests.get(botId));)
|
|
23109
|
+
await pending;
|
|
23110
|
+
}
|
|
21972
23111
|
const remoteProvider = bot.cloudBackend === "vps" ? "vps" : "box";
|
|
21973
23112
|
if (computerProviderConfigTransitions.has(remoteProvider)) {
|
|
21974
23113
|
return json(res, 409, { error: providerTransitionMessage(remoteProvider) });
|
|
@@ -22016,6 +23155,10 @@ const handleRequest = async (req, res) => {
|
|
|
22016
23155
|
// Opening the existing SSH viewer can coexist with a capture. Start,
|
|
22017
23156
|
// stop, remove and Settings deletion still exclude pending previews.
|
|
22018
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; }));
|
|
22019
23162
|
try {
|
|
22020
23163
|
if (m[2] === "exec") {
|
|
22021
23164
|
return json(res, 409, { error: "the VPS console is available to the bot through its scoped computer tools" });
|
|
@@ -22027,13 +23170,20 @@ const handleRequest = async (req, res) => {
|
|
|
22027
23170
|
return json(res, 409, { error: "the VPS computer is being used by this bot — interrupt the turn first" });
|
|
22028
23171
|
}
|
|
22029
23172
|
if (m[2] === "join") {
|
|
22030
|
-
|
|
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 });
|
|
22031
23177
|
}
|
|
22032
23178
|
const action = m[2] === "provision" ? "provision" : m[2] === "remove" ? "remove" : "stop";
|
|
22033
23179
|
return json(res, 200, await vps.vpsComputerAction(action, cfg, botId));
|
|
22034
23180
|
}
|
|
22035
23181
|
finally {
|
|
22036
23182
|
releaseComputerLifecycle();
|
|
23183
|
+
if (joinSettled) {
|
|
23184
|
+
vpsJoinRequests.delete(botId);
|
|
23185
|
+
joinSettled();
|
|
23186
|
+
}
|
|
22037
23187
|
}
|
|
22038
23188
|
}
|
|
22039
23189
|
const activeBoatTurn = botHasActiveTurn(botId);
|
|
@@ -22098,6 +23248,7 @@ const handleRequest = async (req, res) => {
|
|
|
22098
23248
|
}
|
|
22099
23249
|
};
|
|
22100
23250
|
const server = createServer(handleRequest);
|
|
23251
|
+
desktopViewer.attach(server, handleRequest);
|
|
22101
23252
|
calendarCalls.start();
|
|
22102
23253
|
// Resolve the edition before accepting requests so /api/edition is never a guess.
|
|
22103
23254
|
console.log(describeEdition(await loadEnterpriseLayer()));
|
|
@@ -22278,6 +23429,7 @@ if (TUNNEL_SOCKET) {
|
|
|
22278
23429
|
if (process.platform !== "win32")
|
|
22279
23430
|
rmSync(TUNNEL_SOCKET, { force: true });
|
|
22280
23431
|
tunnelListener = createServer(handleRequest);
|
|
23432
|
+
desktopViewer.attach(tunnelListener, handleRequest);
|
|
22281
23433
|
tunnelListener.listen(TUNNEL_SOCKET, () => {
|
|
22282
23434
|
console.log(`openmausbot tunnel listener on ${TUNNEL_SOCKET}`);
|
|
22283
23435
|
});
|
|
@@ -22296,6 +23448,7 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
22296
23448
|
sharedComputers.close();
|
|
22297
23449
|
sharedComputerControl.close();
|
|
22298
23450
|
browserLive.closeAll();
|
|
23451
|
+
desktopViewer.closeAll();
|
|
22299
23452
|
for (const idle of localVmIdles.values())
|
|
22300
23453
|
idle.cancel();
|
|
22301
23454
|
vps.closeAllVpsDesktopTunnels();
|
|
@@ -22332,6 +23485,7 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
22332
23485
|
code = 1;
|
|
22333
23486
|
}
|
|
22334
23487
|
closeMessageDb();
|
|
23488
|
+
mcpOAuth.dispose();
|
|
22335
23489
|
releaseDataDirLeaseAtExit();
|
|
22336
23490
|
// A Cloud home's launcher starts the server again on this code only.
|
|
22337
23491
|
process.exit(cloudHomeRestartRequested && code === 0 ? CLOUD_HOME_RESTART_EXIT_CODE : code);
|