openmausbot 0.1.94 → 0.1.95
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-bxM7NV3N.js → DesktopViewer-BwWpc4RK.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-jsQkIqTn.js} +1 -1
- package/dist/assets/{arc-B5itKDcx.js → arc-7kei4Hsu.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-CRWDFDMq.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CTKW_IKs.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-BBTW7-6M.js} +1 -1
- package/dist/assets/channel-DS2NbT5x.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-M_Ox-MkD.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D2YiN-Ud.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-BmVYJ0f8.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-BtiLVVnZ.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-DFYSb6J6.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-mknKBzgV.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-DgSYGiOu.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-C7ZvuguH.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-CPqxINpA.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-Cd6xfJaC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-BSXY5qRh.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-D_lhEMj1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-Da9FrFwk.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-T77jMOXe.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-Dkj96X5E.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-CMi9zlD_.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-CXK6YUfS.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-BE7B1u9Q.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-CCMQphKr.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-iHJ_-tGH.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DONsCWQ7.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-CU_WOaXk.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-BeYwM04G.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-Bdw9DqEl.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-C9iwq3qm.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-Dx-VWzUg.js} +1 -1
- package/dist/assets/{index-CPiqc_DN.js → index-KygHRE3E.js} +1 -1
- package/dist/assets/index-NF7XCHAy.css +1 -0
- package/dist/assets/index-X_5ydXIE.js +610 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-BG7uaEd4.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-D72WAXnM.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-DKH1gjYT.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-3upm9JUT.js} +1 -1
- package/dist/assets/{linear-BpKhuyLm.js → linear-CcR4pc0-.js} +1 -1
- package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-C6yU5FSS.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-DOI2xEuv.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-BVWJSgLd.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-jtbYezhq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-g0u8mtPX.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-k8689Wny.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-DsJ22PDt.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-CneWBRgG.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-DWsFCWap.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-Bvf0YE3S.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-Bxb3Iei8.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-DLkk7W4A.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-KR9bIHYC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-DfsOrj8t.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-DRSTzG3h.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-DtEdhPgT.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-B-XhTxjG.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +2 -1
- package/dist-server/companion/src/routes.js +39 -0
- package/dist-server/container-mcp.js +2 -2
- package/dist-server/drivers/agents-proxy.js +58 -38
- package/dist-server/enterprise/server/index.js +2 -1
- package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +35 -19
- package/dist-server/index.js +3159 -2313
- package/dist-server/local-computer.js +2 -1
- package/dist-server/openmausbot.js +314 -1336
- package/dist-server/pair-cli.js +314 -1336
- package/dist-server/proxy-paths.js +2 -1
- package/dist-server/server/account-signin.js +0 -2
- package/dist-server/server/activity.js +201 -0
- package/dist-server/server/auto-approve.js +6 -0
- package/dist-server/server/automatic-recovery.js +1 -1
- package/dist-server/server/boat-create-idempotency.js +0 -7
- package/dist-server/server/boat-delete-journal.js +0 -4
- package/dist-server/server/bot-package.js +1 -19
- package/dist-server/server/browser-runtime.js +1 -2
- package/dist-server/server/chief-of-staff.js +2 -3
- package/dist-server/server/cloud-backend.js +0 -4
- package/dist-server/server/cloud-computer-tools.js +110 -0
- package/dist-server/server/cloud-home.js +0 -1
- package/dist-server/server/cloud-lending.js +0 -3
- package/dist-server/server/composio.js +31 -2
- package/dist-server/server/computer-control.js +0 -15
- package/dist-server/server/computer-observation.js +0 -74
- package/dist-server/server/config.js +29 -1
- package/dist-server/server/container-computer.js +7 -19
- package/dist-server/server/contracts.js +9 -0
- package/dist-server/server/control-client.js +4 -10
- package/dist-server/server/drivers/acp/core.js +7 -5
- package/dist-server/server/drivers/agents-call.js +42 -29
- package/dist-server/server/drivers/agents-catalog.js +24 -11
- package/dist-server/server/drivers/agents-proxy.js +8 -30
- package/dist-server/server/drivers/antigravity.js +1 -4
- package/dist-server/server/drivers/boatagent.js +23 -4
- package/dist-server/server/drivers/cerebras.js +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
- package/dist-server/server/drivers/claude.js +16 -14
- package/dist-server/server/drivers/openai-chat.js +3 -6
- package/dist-server/server/drivers/retry.js +8 -4
- package/dist-server/server/engine-install.js +1 -5
- package/dist-server/server/harness/registry.js +0 -1
- package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
- package/dist-server/server/index.js +789 -256
- package/dist-server/server/lending-memory.js +12 -8
- package/dist-server/server/memory-entries.js +38 -8
- package/dist-server/server/memory-organize.js +1 -8
- package/dist-server/server/memory-topics.js +4 -4
- package/dist-server/server/memory-upkeep.js +13 -15
- package/dist-server/server/message-file.js +5 -9
- package/dist-server/server/outbound-counts.js +79 -0
- package/dist-server/server/outbound-requests.js +60 -0
- package/dist-server/server/package-import.js +1 -1
- package/dist-server/server/peer-delivery.js +1 -0
- package/dist-server/server/proxy-paths.js +2 -1
- package/dist-server/server/room-handoffs.js +5 -13
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +5 -1
- package/dist-server/server/setup-mode.js +0 -2
- package/dist-server/server/skill-learn.js +0 -1
- package/dist-server/server/skill-library.js +0 -15
- package/dist-server/server/store.js +9 -1
- package/dist-server/server/surface.js +65 -20
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-memory.js +209 -0
- package/dist-server/server/thread-events.js +2 -2
- package/dist-server/server/vps-computer.js +4 -14
- package/dist-server/server/workspace.js +85 -47
- package/dist-server/shared/activity.js +3 -0
- package/dist-server/shared/bot-avatar.js +8 -0
- package/dist-server/shared/connector-scopes.js +78 -0
- package/dist-server/shared/outbound.js +128 -0
- package/dist-server/shared/tool-surface.js +3 -2
- package/dist-server/vps-container-mcp.js +2 -2
- package/enterprise/server/index.js +2 -1
- package/package.json +1 -1
- package/dist/assets/channel-CdJX6i8k.js +0 -1
- package/dist/assets/index-CPYrGsv1.js +0 -610
- package/dist/assets/index-CzT42GR8.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
- package/dist-server/server/drivers/chat-boat-tools.js +0 -90
- package/dist-server/server/drivers/grokagent.js +0 -4
- package/dist-server/server/openmaus-status-capsule.js +0 -329
|
@@ -32,7 +32,7 @@ import { createApprovalModeSupport } from "./harness-capabilities.js";
|
|
|
32
32
|
import { escapeAttribute } from "../src/lib/composer-attachments.js";
|
|
33
33
|
import { threadRefUrl } from "../src/lib/thread-refs.js";
|
|
34
34
|
import { CREDENTIAL_TARGETS, credentialResumeOutcome, credentialIsConfigured, isPendingCredentialRequest, isCredentialTargetId, } from "../shared/credential-request.js";
|
|
35
|
-
import { approvalHeldNote, approvalHeldReason, approvalModeForOrigin, autoVerdict, deliverFullAccessApproval, delegatedApprovalMode } from "./auto-approve.js";
|
|
35
|
+
import { HELD_NOTE, approvalHeldNote, approvalHeldReason, approvalModeForOrigin, autoVerdict, deliverFullAccessApproval, delegatedApprovalMode } from "./auto-approve.js";
|
|
36
36
|
import { CommandAllowlistStore, commandAllowlistCandidate } from "./command-allowlist.js";
|
|
37
37
|
import { updateClaudeCli } from "./claude-update.js";
|
|
38
38
|
import { configuredAccountDirectory, assertSeparateClaudeAccount, claudeAccountInfo, createClaudeAccountSchema, instanceSettingsSchema, newClaudeAccount } from "./claude-accounts.js";
|
|
@@ -51,6 +51,7 @@ import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
|
51
51
|
import { groupTurnCwd } from "./room-cwd.js";
|
|
52
52
|
import { RoomTurnDeadline, RoomTurnStallRegistry, roomTurnTimeoutMessage } from "./room-turn-timeout.js";
|
|
53
53
|
import * as boat from "./boat.js";
|
|
54
|
+
import { cloudComputerRpc } from "./cloud-computer-tools.js";
|
|
54
55
|
import { TeamComputers, teamComputerAssignment, teamComputerCreate, teamComputerOwner } from "./team-computers.js";
|
|
55
56
|
import { isEffortLevel } from "../shared/wire.js";
|
|
56
57
|
import { isPersistentQuestionCard, QUESTION_DISMISS_MESSAGE, shouldSettleRequestCard } from "../shared/ask-question.js";
|
|
@@ -66,10 +67,9 @@ import { buildRecall } from "./recall.js";
|
|
|
66
67
|
import { createMemoryUpkeep, upkeepEnabled } from "./memory-upkeep.js";
|
|
67
68
|
import { appendAboutMe, commitLearned, planLearned } from "./profile-learned.js";
|
|
68
69
|
import { canAccessTeam, canReachPeer, coordinatorSupervises, livePeerRoster, livePeerRosterBlock, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, resolveTeammate, roomPeerRosterSystemPrompt, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
|
|
69
|
-
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
70
70
|
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerExec, containerRuntimeStatus, localVmWakeAction, localVmWorkspaceExists, perBotLocalVmTarget, poolLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, VM_WORKSPACE_GUEST, } from "./container-computer.js";
|
|
71
71
|
import { attachForTurn, saveBotAttachment } from "./bot-attachment.js";
|
|
72
|
-
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, skillsLibraryEnabled, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, CLAUDE_API_INSTANCE, liveSettingsFor, } from "./config.js";
|
|
72
|
+
import { cacheUntilConfigChanges, 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, skillsLibraryEnabled, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, CLAUDE_API_INSTANCE, liveSettingsFor, } from "./config.js";
|
|
73
73
|
import { sweepThreadEventLogs } from "./thread-retention.js";
|
|
74
74
|
import { ComputerControl } from "./computer-control.js";
|
|
75
75
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -85,7 +85,7 @@ import { entitled } from "./enterprise.js";
|
|
|
85
85
|
import { HOSTED_CONTRACT_HEADER, HOSTED_CONTRACT_METADATA, HOSTED_CONTRACT_VERSION } from "./hosted-contract.js";
|
|
86
86
|
import { describeSpawnFailure, execCli } from "./procs.js";
|
|
87
87
|
import { blockedTarget, buildNotification, buildSpendNotification } from "./notify.js";
|
|
88
|
-
import { isModelVariant, TurnNotStartedError, newId, } from "./contracts.js";
|
|
88
|
+
import { isModelVariant, TurnNotStartedError, newId, usesCloudComputer, } from "./contracts.js";
|
|
89
89
|
import { RETRY_MAX_ATTEMPTS } from "./drivers/retry.js";
|
|
90
90
|
import { recoveryCapabilityError } from "./automatic-recovery.js";
|
|
91
91
|
import { decodeGeneratedImage } from "./generated-image.js";
|
|
@@ -150,7 +150,7 @@ import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefault
|
|
|
150
150
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
151
151
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
152
152
|
import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-folder.js";
|
|
153
|
-
import { buildSystemPrompt, userProfileSystemPrompt, cloudHomePrompt, computerPrompt, composioSystemPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, resolveComputerPromptKind, } from "./system-prompt.js";
|
|
153
|
+
import { buildSystemPrompt, userProfileSystemPrompt, cloudHomePrompt, computerPrompt, composioSystemPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, TEAM_MEMORY_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, resolveComputerPromptKind, } from "./system-prompt.js";
|
|
154
154
|
import { readCuaConnection, readCuaUnavailableReason, gatedLocalComputer } from "./local-computer.js";
|
|
155
155
|
import { discoverExistingPerBotLocalVms, discoverExistingPoolLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
156
156
|
import { localVmStopReason, recordLocalVmIdleStop } from "./local-vm-stop-reason.js";
|
|
@@ -183,6 +183,12 @@ import { fetchGithubTeam, fetchLibraryTeam, fetchTeamCatalog } from "./team-libr
|
|
|
183
183
|
import { BOT_PACKAGE_MAX_SKILLS, isBotPackage, packageSummary, parsePackageDocument, renderBotPackageMarkdown } from "./bot-package.js";
|
|
184
184
|
import { createTeamManifest, parseTeamManifest } from "./team-manifest.js";
|
|
185
185
|
import { readThreadEvents } from "./thread-events.js";
|
|
186
|
+
import { TeamMemory, TEAM_MEMORY_KINDS } from "./team-memory.js";
|
|
187
|
+
import { describeTool, readBotActivity } from "./activity.js";
|
|
188
|
+
import { OutboundCounts } from "./outbound-counts.js";
|
|
189
|
+
import { OutboundRequestService } from "./outbound-requests.js";
|
|
190
|
+
import { DEFAULT_OUTBOUND_POLICY, connectorCallsIn, normalizeOutboundPolicy, outboundCallsIn } from "../shared/outbound.js";
|
|
191
|
+
import { connectorAccessDecision, describeConnectorScopes, normalizeConnectorScopes } from "../shared/connector-scopes.js";
|
|
186
192
|
import { bindThreadLogCapProvider } from "./thread-log-rotation.js";
|
|
187
193
|
import { listenWebhookIngress, webhookCredential } from "./webhook-ingress.js";
|
|
188
194
|
import { assertModelVariantSupported, memberTurnSelection } from "./member-turn.js";
|
|
@@ -204,7 +210,7 @@ import { startAutoVmClaim } from "./auto-vm-claims.js";
|
|
|
204
210
|
import { computerFreeAfterText, computerParkedText, computerStoppedWaitingText, computerWaitingText } from "./computer-wait.js";
|
|
205
211
|
import { CloudOverflowConsent, CloudSeatLease, cloudOverflowAction, cloudOverflowConsentText, cloudOverflowOfferText, cloudSeatStartedText, cloudSeatStoppedText } from "./cloud-overflow.js";
|
|
206
212
|
import { modelContextWindow } from "./model-context-window.js";
|
|
207
|
-
import { parseSurface, resolveSurface, surfaceLabel, surfaceOfComputerKind, surfacePrompt } from "./surface.js";
|
|
213
|
+
import { cloudPlaceDriverError, parseSurface, PlaceUnavailableError, placeUnavailable, resolveSurface, surfaceLabel, surfaceOfComputerKind, surfacePrompt } from "./surface.js";
|
|
208
214
|
import { PendingTurnCancellations, ProviderTurnGenerationRegistry, RetiredTurnRegistry, guardTurnDispatch, isTurnAdmissionBlocked, isTurnEventQuarantined, } from "./turn-dispatch-guard.js";
|
|
209
215
|
import { createGracefulShutdown } from "./graceful-shutdown.js";
|
|
210
216
|
import { acquireDataDirLeaseForProcess } from "./data-dir-lease.js";
|
|
@@ -292,10 +298,12 @@ const workspaceMaintenance = new WorkspaceBackupMaintenance();
|
|
|
292
298
|
// Remote clients (server/request-auth.ts, server/sessions.ts): a stable identity
|
|
293
299
|
// for this server, the paired sessions, and the cookie the served UI uses.
|
|
294
300
|
const ENVIRONMENT_ID = loadEnvironmentId(DATA_DIR);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
301
|
+
// Every stream frame and request rechecks email sessions against this list,
|
|
302
|
+
// so config.json is parsed again only when the file changes.
|
|
303
|
+
const signInAllowList = cacheUntilConfigChanges((config) => ({
|
|
304
|
+
admins: parseAllowList(config.signIn?.admins?.join(",")),
|
|
305
|
+
members: parseAllowList(config.signIn?.members?.join(",")),
|
|
306
|
+
}));
|
|
299
307
|
const sessions = new SessionRegistry({
|
|
300
308
|
file: join(DATA_DIR, "sessions.json"),
|
|
301
309
|
emailScopesSnapshot: () => {
|
|
@@ -940,6 +948,9 @@ function cardAnswerRefusal(auth, threadId, requestId, behavior) {
|
|
|
940
948
|
? "A local service can only decline this request. Approve or answer it in OpenMausBot while signed in."
|
|
941
949
|
: null;
|
|
942
950
|
}
|
|
951
|
+
if (!auth.scopes.includes("admin") && store.messagesFor(threadId).some(message => message.card?.requestId === requestId && message.card.teamMemoryRequest)) {
|
|
952
|
+
return "Only a workspace admin can review shared team memory.";
|
|
953
|
+
}
|
|
943
954
|
// A Cloud home is one person's: a device paired with chat-only access (a
|
|
944
955
|
// guest) may read along but never answer, or its words would reach the
|
|
945
956
|
// owner's turn, and through it a lent Mac (docs/cloud-pro.md).
|
|
@@ -2724,8 +2735,8 @@ async function browserIntegration(botId, profile, turn) {
|
|
|
2724
2735
|
const token = mintInternalCapability({ botId, ...turn, browserSession: session,
|
|
2725
2736
|
kind: "browser", depth: 0, skillAuthoring: false, createdBots: 0, openedThreads: 0 });
|
|
2726
2737
|
return { profile: partitionId, session, spec, integration: {
|
|
2727
|
-
command: process.execPath, args: [SPAWNED_PROXIES.browser], env: {
|
|
2728
|
-
...AGENTS_NODE_FLAG,
|
|
2738
|
+
command: process.execPath, args: [SPAWNED_PROXIES.harnessMcp, "browser"], env: {
|
|
2739
|
+
...AGENTS_NODE_FLAG, OMB_MCP_TOKEN: token, OMB_HARNESS_URL: `http://127.0.0.1:${PORT}`,
|
|
2729
2740
|
},
|
|
2730
2741
|
} };
|
|
2731
2742
|
}
|
|
@@ -2826,8 +2837,10 @@ const routineRequestEnvelopeSchema = z.discriminatedUnion("action", [
|
|
|
2826
2837
|
}).strict(),
|
|
2827
2838
|
...["pause", "resume", "run_now", "delete"].map((action) => z.object({ ...routineRequestSourceSchema, action: z.literal(action), routineId: z.unknown(), forBotId: z.unknown().optional() }).strict()),
|
|
2828
2839
|
]);
|
|
2829
|
-
/** The loopback endpoint a bot's computer proxy polls before acting.
|
|
2830
|
-
|
|
2840
|
+
/** The loopback endpoint a bot's computer proxy polls before acting. With a
|
|
2841
|
+
* Boat id, the same turn-scoped token also serves that Boat's tools at
|
|
2842
|
+
* /api/internal/computer/mcp, and is revoked when the turn ends. */
|
|
2843
|
+
function controlIntegration(botId, threadId, generation, localVmTarget, boxId) {
|
|
2831
2844
|
return {
|
|
2832
2845
|
url: `http://127.0.0.1:${PORT}/api/internal/computer-control?botId=${encodeURIComponent(botId)}`,
|
|
2833
2846
|
token: mintInternalCapability({
|
|
@@ -2837,6 +2850,7 @@ function controlIntegration(botId, threadId, generation, localVmTarget) {
|
|
|
2837
2850
|
depth: 0,
|
|
2838
2851
|
kind: "computer",
|
|
2839
2852
|
...(localVmTarget ? { localVmTarget } : {}),
|
|
2853
|
+
...(boxId ? { boxId } : {}),
|
|
2840
2854
|
...(teamComputerTurns.get(threadId) ? { teamComputerId: teamComputerTurns.get(threadId).computerId } : {}),
|
|
2841
2855
|
skillAuthoring: false,
|
|
2842
2856
|
createdBots: 0,
|
|
@@ -3318,7 +3332,7 @@ function previewSystemPrompt(bot) {
|
|
|
3318
3332
|
]
|
|
3319
3333
|
.filter(Boolean)
|
|
3320
3334
|
.join(" ");
|
|
3321
|
-
const instance =
|
|
3335
|
+
const instance = registry.get(bot.modelSelection.instanceId);
|
|
3322
3336
|
const caps = instance?.adapter.capabilities;
|
|
3323
3337
|
const teamComputer = inheritedTeamComputer(bot);
|
|
3324
3338
|
const previewComputer = teamComputer ? "cloud" : bot.computer;
|
|
@@ -3326,12 +3340,11 @@ function previewSystemPrompt(bot) {
|
|
|
3326
3340
|
kind: previewComputer === "vm" ? "vm" : previewComputer === "cloud"
|
|
3327
3341
|
? bot.cloudBackend === "vps" ? "vps" : "box" : previewComputer === "local" ? "local" : null,
|
|
3328
3342
|
driverKind: instance?.driverKind,
|
|
3329
|
-
cloudComputerMcp: caps?.cloudComputerMcp,
|
|
3330
3343
|
vmPrivate: localVmMode(cfg) === "per-bot",
|
|
3331
3344
|
});
|
|
3332
3345
|
const peers = reachablePeers(store.bots, bot);
|
|
3333
3346
|
const coordination = bot.chiefOfStaff
|
|
3334
|
-
? chiefOfStaffSystemPrompt(bot.id, store.bots, true
|
|
3347
|
+
? chiefOfStaffSystemPrompt(bot.id, store.bots, true)
|
|
3335
3348
|
: peers.length > 0
|
|
3336
3349
|
? peerRosterSystemPrompt(peers)
|
|
3337
3350
|
: "";
|
|
@@ -3367,7 +3380,7 @@ function previewSystemPrompt(bot) {
|
|
|
3367
3380
|
browser: previewPlan.computer === undefined ? false : previewPlan.browser,
|
|
3368
3381
|
}, { note: previewPlan.note, cloudHome: Boolean(CLOUD_HOME) }) },
|
|
3369
3382
|
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(agentsMounted && lendingEnabled()) : "" },
|
|
3370
|
-
{ id: "composio", label: "Connected apps", text: caps?.composioMcp && bot.composio !== false && composio.configured(cfg) ? composioSystemPrompt(bot.connectorTools) : "" },
|
|
3383
|
+
{ id: "composio", label: "Connected apps", text: caps?.composioMcp && bot.composio !== false && composio.configured(cfg) ? composioSystemPrompt(bot.connectorTools) + describeConnectorScopes(bot.connectorScopes) : "" },
|
|
3371
3384
|
{ id: "mcp", label: "MCP servers", text: caps?.customMcp ? customMcpPrompt(Object.keys(engineMcpServers(bot))) : "" },
|
|
3372
3385
|
{ id: "browser", label: "Browser", text: previewPlan.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
3373
3386
|
{ id: "coordination", label: "Team", text: agentsMounted && coordination ? ` ${coordination}` : "" },
|
|
@@ -3375,6 +3388,7 @@ function previewSystemPrompt(bot) {
|
|
|
3375
3388
|
{ id: "routine", label: "Routines", text: agentsMounted ? ROUTINE_PROMPT : "" },
|
|
3376
3389
|
{ id: "profile", label: "Profile changes", text: agentsMounted ? PROFILE_PROMPT : "" },
|
|
3377
3390
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
3391
|
+
{ id: "team-memory", label: "Team memory", text: teamMemory.systemPrompt(bot.section) + (agentsMounted ? TEAM_MEMORY_PROMPT : "") },
|
|
3378
3392
|
{ id: "memory", label: "Memory", text: memorySystemPrompt(bot.id, { managedWrites: agentsMounted, fileTools: Boolean(privateWorkspace), enabled: bot.memoryEnabled !== false }) },
|
|
3379
3393
|
{ id: "skills", label: "Skills index", text: privateWorkspace ? skillsSystemPrompt(bot.id, skillsLibraryEnabled(cfg) ? bot.assignedSkills : undefined) : "" },
|
|
3380
3394
|
]);
|
|
@@ -4279,7 +4293,7 @@ function outstandingAssignmentsPrompt(threadId) {
|
|
|
4279
4293
|
assignment: node.text.slice(0, 1_000),
|
|
4280
4294
|
status: node.status === "queued" ? "waiting for that teammate to be free" : "working on it now",
|
|
4281
4295
|
}));
|
|
4282
|
-
return ` Assignments you already sent are still outstanding, and nothing in this conversation cancelled them: ${JSON.stringify(listed)}. Do not
|
|
4296
|
+
return ` Assignments you already sent are still outstanding, and nothing in this conversation cancelled them: ${JSON.stringify(listed)}. Do not resend them, do not poll or wait for them, and do not tell the user they were lost. Each result returns to this conversation on its own and resumes you then. To change or add to one, send the change to the same teammate with coordinate_bots; it runs after the current work. Answer the message above with that work still in flight.`;
|
|
4283
4297
|
}
|
|
4284
4298
|
const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
|
|
4285
4299
|
validate: (node, parent) => roomHandoffProblem(node, parent) ??
|
|
@@ -4353,9 +4367,10 @@ const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
|
|
|
4353
4367
|
}
|
|
4354
4368
|
// A successful direct work thread closes after its result is reported:
|
|
4355
4369
|
// folded out of the sidebar, never deleted, and reopened if addressed.
|
|
4356
|
-
// Failed or withheld work stays visible
|
|
4370
|
+
// Failed or withheld work stays visible, and so does a thread whose
|
|
4371
|
+
// next request is already waiting there.
|
|
4357
4372
|
const childTask = store.taskByThread(child.botId, child.threadId);
|
|
4358
|
-
if (!child.groupId && child.status === "completed" && !problem && !childTask?.closedBy
|
|
4373
|
+
if (!child.groupId && child.status === "completed" && !problem && !childTask?.closedBy && !roomHandoffs.activeDirect(child.threadId)
|
|
4359
4374
|
&& childTask?.openedBy?.kind === "work" && childTask.openedBy.botId === parent.botId) {
|
|
4360
4375
|
store.setTaskClosedBy(child.botId, child.threadId, { botId: parent.botId, name: store.bot(parent.botId)?.name ?? childTask.openedBy.name, at: Date.now() });
|
|
4361
4376
|
}
|
|
@@ -5280,7 +5295,10 @@ always, rememberCommand) {
|
|
|
5280
5295
|
}
|
|
5281
5296
|
if (messageId)
|
|
5282
5297
|
askMessageByRequest.delete(`${threadId}:${requestId}`);
|
|
5283
|
-
|
|
5298
|
+
// The card's own answered state gates the message too: a replayed respond
|
|
5299
|
+
// for a card that already settled must not read as a failed delivery —
|
|
5300
|
+
// nothing was lost, the earlier answer already ran.
|
|
5301
|
+
if (!persistentQuestion && !existing?.card?.answered)
|
|
5284
5302
|
store.appendMessage(threadId, {
|
|
5285
5303
|
role: "bot",
|
|
5286
5304
|
kind: "activity",
|
|
@@ -5451,8 +5469,8 @@ const watchdog = new TurnWatchdog({
|
|
|
5451
5469
|
const bot = botForThread(turn.botId, turn.threadId);
|
|
5452
5470
|
const routineRun = activeRoutineRunForThread(turn.threadId);
|
|
5453
5471
|
const instance = bot
|
|
5454
|
-
? runningTurnInstance(bot, turn.threadId
|
|
5455
|
-
:
|
|
5472
|
+
? runningTurnInstance(bot, turn.threadId)
|
|
5473
|
+
: runningTurnEngines.get(turn.threadId) ?? null;
|
|
5456
5474
|
void instance?.adapter.interruptTurn(turn.threadId).catch(() => { });
|
|
5457
5475
|
const minutes = Math.round(TURN_STALL_MS / 60_000);
|
|
5458
5476
|
if (routineRun?.target === "bot") {
|
|
@@ -6186,10 +6204,29 @@ async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
|
6186
6204
|
!turnResources.owns(`computer:box:${machine.id}`, owner))
|
|
6187
6205
|
throw new Error("This computer turn ended while its machine was starting");
|
|
6188
6206
|
return {
|
|
6189
|
-
|
|
6207
|
+
mount: cloudComputerMount(botId, owner, machine.id, remoteAgent),
|
|
6190
6208
|
capture: () => boat.screenshotBoat(cfg, ownerId, machine.id),
|
|
6191
6209
|
};
|
|
6192
6210
|
}
|
|
6211
|
+
/** How a turn's engine reaches a cloud computer. The Computer engine runs its
|
|
6212
|
+
* whole turn on the Boat. Every other engine keeps its own model and sign-in
|
|
6213
|
+
* and gets the Boat as one more stdio computer server (harness-mcp-proxy
|
|
6214
|
+
* computer), in the same slot a Local VM or VPS uses: one mechanism on a
|
|
6215
|
+
* desktop, a headless server and an OMB Cloud. The agent process holds only a
|
|
6216
|
+
* turn-scoped capability naming this Boat, never a Boat credential. */
|
|
6217
|
+
function cloudComputerMount(botId, owner, boxId, remoteAgent) {
|
|
6218
|
+
if (remoteAgent)
|
|
6219
|
+
return { computer: { kind: "box", boxId } };
|
|
6220
|
+
const control = controlIntegration(botId, owner.threadId, owner.generation, undefined, boxId);
|
|
6221
|
+
return {
|
|
6222
|
+
localComputer: {
|
|
6223
|
+
command: process.execPath,
|
|
6224
|
+
args: [SPAWNED_PROXIES.harnessMcp, "computer"],
|
|
6225
|
+
env: { ...AGENTS_NODE_FLAG, OMB_HARNESS_URL: `http://127.0.0.1:${PORT}`, OMB_MCP_TOKEN: control.token },
|
|
6226
|
+
platform: "linux",
|
|
6227
|
+
},
|
|
6228
|
+
};
|
|
6229
|
+
}
|
|
6193
6230
|
/** "Works on: This computer", strict. One mount for a bot thread and a group
|
|
6194
6231
|
* chat, so the two cannot drift on what they check or how they fail. */
|
|
6195
6232
|
async function mountHostComputer(owner, botId, providerSupportsLocal) {
|
|
@@ -6291,9 +6328,7 @@ async function attachBotBoat(bot, owner, opts) {
|
|
|
6291
6328
|
await bindTurnComputer(owner, `computer:box:${machine.id}`, opts.remoteAgent);
|
|
6292
6329
|
return {
|
|
6293
6330
|
capture: () => boat.screenshotBoat(cfg, bot.id, machine.id),
|
|
6294
|
-
|
|
6295
|
-
? { kind: "box", boxId: machine.id, token: boat.boatAccount(cfg)?.token ?? "", control: controlIntegration(bot.id, owner.threadId, owner.generation) }
|
|
6296
|
-
: null,
|
|
6331
|
+
mount: opts.canMount ? cloudComputerMount(bot.id, owner, machine.id, opts.remoteAgent) : null,
|
|
6297
6332
|
};
|
|
6298
6333
|
}
|
|
6299
6334
|
function managedBoatOwners() {
|
|
@@ -6370,17 +6405,6 @@ function turnProvider(bot, runOn, threadId) {
|
|
|
6370
6405
|
return "box";
|
|
6371
6406
|
return bot.cloudBackend === "vps" ? "vps" : wants === "cloud" ? "box" : null;
|
|
6372
6407
|
}
|
|
6373
|
-
/** A driver with a Boat bridge keeps the selected model. Other engines use
|
|
6374
|
-
* Boat's native runner. Start and interrupt must resolve the same owner. */
|
|
6375
|
-
function turnInstance(bot, runOn, threadId) {
|
|
6376
|
-
const selected = registry.get(bot.modelSelection.instanceId);
|
|
6377
|
-
if (selected?.adapter.capabilities.cloudComputerMcp)
|
|
6378
|
-
return selected;
|
|
6379
|
-
const onBoat = turnProvider(bot, runOn, threadId) === "box";
|
|
6380
|
-
return onBoat
|
|
6381
|
-
? registry.instances().find((candidate) => candidate.driverKind === "boxAgent") ?? null
|
|
6382
|
-
: registry.get(bot.modelSelection.instanceId);
|
|
6383
|
-
}
|
|
6384
6408
|
/** Preview requests carry the selected conversation, not whichever thread
|
|
6385
6409
|
* happens to be the bot's default. A query never grants lifecycle authority. */
|
|
6386
6410
|
function computerPreviewBot(botId, url) {
|
|
@@ -6448,7 +6472,7 @@ async function selectableComputers(bot) {
|
|
|
6448
6472
|
canCreate = Boolean(status?.configured && status.daemonUp && status.container === "missing");
|
|
6449
6473
|
reason = status?.problem ?? reason;
|
|
6450
6474
|
}
|
|
6451
|
-
else if (boat.boatConfigured(cfg) &&
|
|
6475
|
+
else if (boat.boatConfigured(cfg) && caps !== undefined && usesCloudComputer(caps)) {
|
|
6452
6476
|
const status = await boat.boatStatus(cfg, bot.id);
|
|
6453
6477
|
const lifecycle = boat.boatTurnLifecycleAction({ explicitCloud: true, canMount: true, state: status.box?.state ?? null });
|
|
6454
6478
|
ready = lifecycle === "attach";
|
|
@@ -6527,8 +6551,9 @@ function continueComputerSelection(threadId, generation, succeeded) {
|
|
|
6527
6551
|
/** The engine that dispatched each live turn. A bot's settings may change
|
|
6528
6552
|
* mid-turn; an interrupt or steer must reach the engine actually running. */
|
|
6529
6553
|
const runningTurnEngines = new Map();
|
|
6530
|
-
function runningTurnInstance(bot, threadId
|
|
6531
|
-
|
|
6554
|
+
function runningTurnInstance(bot, threadId) {
|
|
6555
|
+
// A turn always runs on the bot's own engine, wherever it works.
|
|
6556
|
+
return runningTurnEngines.get(threadId) ?? registry.get(bot.modelSelection.instanceId);
|
|
6532
6557
|
}
|
|
6533
6558
|
function providerTransitionForTurn(bot, runOn, threadId) {
|
|
6534
6559
|
const provider = turnProvider(bot, runOn, threadId);
|
|
@@ -7738,7 +7763,7 @@ function markTaskContextExternallyUpdated(bot, threadId) {
|
|
|
7738
7763
|
const owner = task.lastInstanceId;
|
|
7739
7764
|
const record = owner ? task.handedMessages?.[owner] : undefined;
|
|
7740
7765
|
if (owner && record?.session !== undefined && record.session === task.resumeCursors[owner] &&
|
|
7741
|
-
|
|
7766
|
+
(botForThread(bot.id, threadId) ?? bot).modelSelection.instanceId === owner &&
|
|
7742
7767
|
registry.get(owner)?.adapter.capabilities.strictResume) {
|
|
7743
7768
|
store.patchTask(bot.id, threadId, { unread: true });
|
|
7744
7769
|
return;
|
|
@@ -8796,11 +8821,9 @@ async function startTurn(botId, text, opts) {
|
|
|
8796
8821
|
if (hostedModels && !hostedModels.allows(bot.modelSelection))
|
|
8797
8822
|
throw Object.assign(new Error(hostedModels.error()), { status: 409 });
|
|
8798
8823
|
const plan = turnSurfacePlan(bot, opts?.runOn, threadId);
|
|
8799
|
-
const instance =
|
|
8824
|
+
const instance = registry.get(bot.modelSelection.instanceId);
|
|
8800
8825
|
if (!instance) {
|
|
8801
|
-
throw Object.assign(new Error(
|
|
8802
|
-
? "the Cloud VM runner is unavailable — configure Boat in App Settings"
|
|
8803
|
-
: `provider instance "${bot.modelSelection.instanceId}" is unavailable — pick another model in settings`), { status: 409 });
|
|
8826
|
+
throw Object.assign(new Error(`provider instance "${bot.modelSelection.instanceId}" is unavailable — pick another model in settings`), { status: 409 });
|
|
8804
8827
|
}
|
|
8805
8828
|
const policyRefusal = policyModelRefusal(instance);
|
|
8806
8829
|
if (policyRefusal)
|
|
@@ -8860,13 +8883,12 @@ async function startTurn(botId, text, opts) {
|
|
|
8860
8883
|
if (providerInstancesChanging.has(instanceId)) {
|
|
8861
8884
|
throw Object.assign(new Error("this provider account is being updated — try again shortly"), { status: 409 });
|
|
8862
8885
|
}
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
const
|
|
8869
|
-
const variant = useInstanceDefaults ? undefined : bot.modelSelection.variant;
|
|
8886
|
+
// A turn always runs on the bot's own engine, model, effort and variant,
|
|
8887
|
+
// wherever it works: a cloud computer is a tool it mounts, never a reason
|
|
8888
|
+
// to swap the engine (the provider_not_configured incident).
|
|
8889
|
+
const model = bot.modelSelection.model;
|
|
8890
|
+
const effort = bot.modelSelection.effort;
|
|
8891
|
+
const variant = bot.modelSelection.variant;
|
|
8870
8892
|
assertModelVariantSupported({ variant, effort }, instance.adapter.capabilities);
|
|
8871
8893
|
// A selection can be persisted while its engine is offline. Re-check when
|
|
8872
8894
|
// the engine returns so an old or unsupported value never reaches a CLI.
|
|
@@ -9190,24 +9212,23 @@ async function startTurn(botId, text, opts) {
|
|
|
9190
9212
|
}
|
|
9191
9213
|
const privateWorkspace = worksInWorkspace ? ensureTaskWorkspace(bot.id, threadId) : undefined;
|
|
9192
9214
|
const skillInstructions = renderSkillInstructions(selectedSkills, {
|
|
9193
|
-
includeRoot: worksInWorkspace
|
|
9215
|
+
includeRoot: worksInWorkspace,
|
|
9194
9216
|
});
|
|
9195
9217
|
const packagePlaybooks = installedPlaybookInstructions(providerText, bot.playbooks);
|
|
9196
9218
|
// An explicit working folder wins for new tasks; otherwise they use
|
|
9197
9219
|
// the private bot workspace. A legacy task with an existing provider
|
|
9198
9220
|
// session deliberately pins to null (the old home-folder behavior),
|
|
9199
9221
|
// because moving a live session would break resume.
|
|
9200
|
-
// A cloud run
|
|
9201
|
-
//
|
|
9202
|
-
//
|
|
9203
|
-
|
|
9204
|
-
store.pinTaskCwd(bot.id, threadId, undefined, { none: true });
|
|
9222
|
+
// A cloud run keeps the bot's own engine on this host, with the cloud
|
|
9223
|
+
// computer as one more tool, so it works in the same folder as any
|
|
9224
|
+
// other turn (only the Computer engine runs on the Boat, and it has no
|
|
9225
|
+
// workspace files).
|
|
9205
9226
|
// On a Cloud home a conversation a guest opened is pinned to its own
|
|
9206
9227
|
// folder when it first runs, not the bot's project folder the owner's
|
|
9207
9228
|
// conversations share (what it leaves there reaches none of them). One
|
|
9208
9229
|
// that ran before keeps its pin, unless the session that opened it was
|
|
9209
9230
|
// revoked: then it was unpinned at boot (server/cloud-owner.ts).
|
|
9210
|
-
const pinnedCwd = privateWorkspace
|
|
9231
|
+
const pinnedCwd = privateWorkspace
|
|
9211
9232
|
? store.pinTaskCwd(bot.id, threadId, privateWorkspace, { privateOnly: cloudGuestOpened(threadId) })
|
|
9212
9233
|
: null;
|
|
9213
9234
|
const cwd = pinnedCwd ?? undefined;
|
|
@@ -9224,8 +9245,8 @@ async function startTurn(botId, text, opts) {
|
|
|
9224
9245
|
const dwebUrl = process.env.DWEB_URL?.trim();
|
|
9225
9246
|
if (dwebUrl)
|
|
9226
9247
|
integrations.dweb = { url: dwebUrl };
|
|
9227
|
-
// Cloud routines always use Boat
|
|
9228
|
-
// only to ordinary turns
|
|
9248
|
+
// Cloud routines always use the bot's Boat. The per-bot backend applies
|
|
9249
|
+
// only to ordinary turns.
|
|
9229
9250
|
const teamComputer = inheritedTeamComputer(bot);
|
|
9230
9251
|
const cloudBackend = teamComputer || opts?.runOn === "cloud" || bot.cloudBackend !== "vps" ? "box" : "vps";
|
|
9231
9252
|
const mountsComputerMcp = instance.adapter.capabilities.computerMcp === true;
|
|
@@ -9253,6 +9274,20 @@ async function startTurn(botId, text, opts) {
|
|
|
9253
9274
|
const placeRefusal = wantedKind && computerPlaceRefusal(wantedKind);
|
|
9254
9275
|
if (placeRefusal)
|
|
9255
9276
|
throw Object.assign(new Error(placeRefusal.message), { status: 409, code: placeRefusal.code });
|
|
9277
|
+
// Where an explicit place came from, so its failure names the one
|
|
9278
|
+
// control that changes it (and a failed Auto-recorded pin is cleared).
|
|
9279
|
+
// Auto and a team's shared computer are nobody's per-turn choice.
|
|
9280
|
+
const placeSource = teamComputer || wants === undefined || wants === "off" ? undefined
|
|
9281
|
+
: opts?.runOn === "cloud" ? "routine"
|
|
9282
|
+
: plan.pinned ? (dispatchTask?.surfaceSource === "auto" ? "auto-pin" : "pin")
|
|
9283
|
+
: "works-on";
|
|
9284
|
+
// An engine that cannot use the cloud computer is refused here, before
|
|
9285
|
+
// anything is created or woken — never handed to another engine.
|
|
9286
|
+
if (wants === "cloud" && placeSource) {
|
|
9287
|
+
const unsupported = cloudPlaceDriverError(instance.adapter.capabilities, cloudBackend, placeSource);
|
|
9288
|
+
if (unsupported)
|
|
9289
|
+
throw new PlaceUnavailableError("cloud", placeSource, unsupported);
|
|
9290
|
+
}
|
|
9256
9291
|
let previewCapture = null;
|
|
9257
9292
|
let browserCapture = null;
|
|
9258
9293
|
let computerKind = null;
|
|
@@ -9506,24 +9541,23 @@ async function startTurn(botId, text, opts) {
|
|
|
9506
9541
|
return false;
|
|
9507
9542
|
}
|
|
9508
9543
|
};
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
if (
|
|
9525
|
-
|
|
9526
|
-
if (!unsupported) {
|
|
9544
|
+
// An explicit place that cannot be attached fails the turn with its
|
|
9545
|
+
// cause and the next action; it never quietly moves the work elsewhere.
|
|
9546
|
+
try {
|
|
9547
|
+
if (wants === "vm") {
|
|
9548
|
+
if (await attachLocalVm(true))
|
|
9549
|
+
computerKind = "vm";
|
|
9550
|
+
}
|
|
9551
|
+
else if (wants === "local") {
|
|
9552
|
+
integrations.localComputer = await mountHostComputer(resourceOwner, bot.id, mountsLocalComputer);
|
|
9553
|
+
computerKind = "local";
|
|
9554
|
+
}
|
|
9555
|
+
// A VPS is a local-agent computer mount, never a remote agent runner.
|
|
9556
|
+
// Explicit Cloud may prepare/start it. Auto remains read-only unless
|
|
9557
|
+
// the person explicitly opted this bot into remote lifecycle actions.
|
|
9558
|
+
// On Auto an engine that cannot use a computer simply skips it.
|
|
9559
|
+
if ((wants === "cloud" || (wants === undefined && managedPolicy.computerAllowed("vps"))) && cloudBackend === "vps" &&
|
|
9560
|
+
!cloudPlaceDriverError(instance.adapter.capabilities, "vps")) {
|
|
9527
9561
|
// The VPS "computer" is the desktop inside this bot's managed
|
|
9528
9562
|
// container, and only screen work needs that desktop to itself.
|
|
9529
9563
|
// So the lease is claimed on the first computer call, through the
|
|
@@ -9561,34 +9595,40 @@ async function startTurn(botId, text, opts) {
|
|
|
9561
9595
|
autoVpsProblem = mounted.problem ?? "the VPS computer could not be reached";
|
|
9562
9596
|
}
|
|
9563
9597
|
}
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
integrations.computer = attached.integration;
|
|
9570
|
-
previewCapture = attached.capture;
|
|
9571
|
-
computerKind = "box";
|
|
9572
|
-
}
|
|
9573
|
-
if (!teamComputer && instance.adapter.capabilities.usesCloudComputer === true && (wants === "cloud" || wants === undefined) && cloudBackend === "box" && boat.boatConfigured(cfg)) {
|
|
9574
|
-
const attached = await attachBotBoat(bot, resourceOwner, {
|
|
9575
|
-
explicitCloud: wants === "cloud",
|
|
9576
|
-
canMount: instance.adapter.capabilities.usesCloudComputer === true,
|
|
9577
|
-
remoteAgent: instance.adapter.capabilities.remoteAgent === true,
|
|
9578
|
-
});
|
|
9579
|
-
if (attached) {
|
|
9598
|
+
// Cloud is strict when selected. Every engine that can use it may also
|
|
9599
|
+
// reuse an already-ready Boat on Auto.
|
|
9600
|
+
if (teamComputer) {
|
|
9601
|
+
const attached = await attachTeamBoat(teamComputer, bot.id, resourceOwner, usesCloudComputer(instance.adapter.capabilities), instance.adapter.capabilities.remoteAgent === true);
|
|
9602
|
+
Object.assign(integrations, attached.mount);
|
|
9580
9603
|
previewCapture = attached.capture;
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9604
|
+
computerKind = "box";
|
|
9605
|
+
}
|
|
9606
|
+
if (!teamComputer && usesCloudComputer(instance.adapter.capabilities) && (wants === "cloud" || (wants === undefined && !computerPlaceRefusal("box"))) && cloudBackend === "box" && boat.boatConfigured(cfg)) {
|
|
9607
|
+
const attached = await attachBotBoat(bot, resourceOwner, {
|
|
9608
|
+
explicitCloud: wants === "cloud",
|
|
9609
|
+
canMount: usesCloudComputer(instance.adapter.capabilities),
|
|
9610
|
+
remoteAgent: instance.adapter.capabilities.remoteAgent === true,
|
|
9611
|
+
});
|
|
9612
|
+
if (attached) {
|
|
9613
|
+
previewCapture = attached.capture;
|
|
9614
|
+
if (attached.mount) {
|
|
9615
|
+
Object.assign(integrations, attached.mount);
|
|
9616
|
+
computerKind = "box";
|
|
9617
|
+
}
|
|
9584
9618
|
}
|
|
9585
9619
|
}
|
|
9620
|
+
if (wants === "cloud" && cloudBackend === "box" && !boat.boatConfigured(cfg)) {
|
|
9621
|
+
throw new Error(BOAT_NOT_CONFIGURED);
|
|
9622
|
+
}
|
|
9623
|
+
if (wants === "cloud" && cloudBackend === "box" && computerKind !== "box") {
|
|
9624
|
+
throw new Error("the cloud computer could not be created or reached");
|
|
9625
|
+
}
|
|
9586
9626
|
}
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
throw
|
|
9627
|
+
catch (error) {
|
|
9628
|
+
if (!placeSource || wants === undefined || wants === "off" || error instanceof DirectTurnSetupCancelled ||
|
|
9629
|
+
error instanceof ComputerWaitParked || error instanceof PlaceUnavailableError)
|
|
9630
|
+
throw error;
|
|
9631
|
+
throw placeUnavailable(wants, placeSource, error instanceof Error ? error.message : String(error));
|
|
9592
9632
|
}
|
|
9593
9633
|
// Auto-only host fallback. Electron owns cua-driver/TCC attribution;
|
|
9594
9634
|
// the harness only reads its already-running connection descriptor.
|
|
@@ -9679,7 +9719,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9679
9719
|
? mentionedBots(providerText, sectionPeers)
|
|
9680
9720
|
: [];
|
|
9681
9721
|
const coordinationPrompt = bot.chiefOfStaff
|
|
9682
|
-
? chiefOfStaffSystemPrompt(bot.id, store.bots, Boolean(integrations.agents),
|
|
9722
|
+
? chiefOfStaffSystemPrompt(bot.id, store.bots, Boolean(integrations.agents), boundedCoordination)
|
|
9683
9723
|
: integrations.agents && sectionPeers.length > 0
|
|
9684
9724
|
// Ordinary bots could always CALL the peer tools; until now the
|
|
9685
9725
|
// one generic sentence they got never named a teammate, so the
|
|
@@ -9788,7 +9828,6 @@ async function startTurn(botId, text, opts) {
|
|
|
9788
9828
|
const computerPromptKind = resolveComputerPromptKind({
|
|
9789
9829
|
kind: computerKind,
|
|
9790
9830
|
driverKind: instance.driverKind,
|
|
9791
|
-
cloudComputerMcp: instance.adapter.capabilities.cloudComputerMcp,
|
|
9792
9831
|
vmPrivate: localVmMode(cfg) === "per-bot",
|
|
9793
9832
|
});
|
|
9794
9833
|
const coordinationNode = opts?.coordination ? roomHandoffs.nodes.get(opts.coordination.id) : undefined;
|
|
@@ -9801,14 +9840,14 @@ async function startTurn(botId, text, opts) {
|
|
|
9801
9840
|
// to a turn whose engine actually mounted them (setupMode is already
|
|
9802
9841
|
// false when they are not — see agentsMounted above)
|
|
9803
9842
|
{ id: "setup", label: "Setup", text: setupSystemPrompt(setupMode, { skills: skillAuthoring, cwd: liveBot?.cwd ?? bot.cwd }) },
|
|
9804
|
-
{ id: "files", label: "File locations", text: worksInWorkspace
|
|
9843
|
+
{ id: "files", label: "File locations", text: worksInWorkspace ? workspaceLocationsPrompt(bot.id, cwd, liveBot?.cwd ?? bot.cwd) : "" },
|
|
9805
9844
|
{ id: "computer", label: "Computer", text: computerPrompt(computerPromptKind) },
|
|
9806
9845
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(teamComputer) },
|
|
9807
9846
|
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: mountedComputer, browser: Boolean(integrations.browser) }, { pinned: plan.pinned, note: plan.note, canSelect: computerSelectionTurns.has(threadId), cloudHome: Boolean(CLOUD_HOME) }) },
|
|
9808
9847
|
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
9809
9848
|
// gated on the integration, not the key: the hint only goes to a
|
|
9810
9849
|
// bot whose driver actually mounted the tools
|
|
9811
|
-
{ id: "composio", label: "Connected apps", text: integrations.composio ? composioSystemPrompt(liveBot?.connectorTools ?? bot.connectorTools) : "" },
|
|
9850
|
+
{ id: "composio", label: "Connected apps", text: integrations.composio ? composioSystemPrompt(liveBot?.connectorTools ?? bot.connectorTools) + describeConnectorScopes((liveBot ?? bot).connectorScopes) : "" },
|
|
9812
9851
|
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
9813
9852
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
9814
9853
|
{ id: "coordination", label: "Team", text: coordinationPrompt ? ` ${coordinationPrompt}` : "" },
|
|
@@ -9821,6 +9860,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9821
9860
|
{ id: "profile", label: "Profile changes", text: profilePrompt },
|
|
9822
9861
|
{ id: "learn", label: "Skill authoring", text: learnPrompt },
|
|
9823
9862
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
9863
|
+
{ id: "team-memory", label: "Team memory", text: teamMemory.systemPrompt(bot.section) + (integrations.agents ? TEAM_MEMORY_PROMPT : "") },
|
|
9824
9864
|
// what the bot said lately in its other conversations, so a task
|
|
9825
9865
|
// never redoes — or forgets — what another one already did
|
|
9826
9866
|
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentWork(recentWorkSources(bot), bot, { userName: cfg.profile?.name?.trim() || "User", currentThreadId: threadId, ...recentWorkFilter() })) },
|
|
@@ -9866,9 +9906,9 @@ async function startTurn(botId, text, opts) {
|
|
|
9866
9906
|
const dispatch = await guardTurnDispatch(instance.adapter.sendTurn({
|
|
9867
9907
|
threadId,
|
|
9868
9908
|
botId: bot.id,
|
|
9869
|
-
startupRecovery: cfg.automaticRecovery?.enabled === true &&
|
|
9909
|
+
startupRecovery: cfg.automaticRecovery?.enabled === true &&
|
|
9910
|
+
(opts?.automaticRecoveryIndex ?? 0) < ((liveBot ?? bot).fallback?.length || (cfg.automaticRecovery.backup ? 1 : 0)),
|
|
9870
9911
|
text: withRecalled(recalled, dispatchContext.turnText),
|
|
9871
|
-
refreshSystemPrompt: true,
|
|
9872
9912
|
images: turnImages,
|
|
9873
9913
|
approvalMode: approvalModeForTurn(bot, commsDepth > 0, threadId),
|
|
9874
9914
|
toolScope,
|
|
@@ -10028,10 +10068,26 @@ async function startTurn(botId, text, opts) {
|
|
|
10028
10068
|
drainDelegationWakes();
|
|
10029
10069
|
return;
|
|
10030
10070
|
}
|
|
10071
|
+
// A failed place never sticks. An Auto-recorded pin to it (a
|
|
10072
|
+
// select_computer switch, or where an earlier Auto turn landed) is the
|
|
10073
|
+
// machine's memory, not a choice: clear it so the next message runs on
|
|
10074
|
+
// Auto. A person's pin stays, and the message names the composer.
|
|
10075
|
+
if (e instanceof PlaceUnavailableError) {
|
|
10076
|
+
const pinned = store.taskByThread(bot.id, threadId);
|
|
10077
|
+
if (pinned?.surface === e.place && pinned.surfaceSource === "auto") {
|
|
10078
|
+
store.patchTask(bot.id, threadId, { surface: undefined, surfaceSource: undefined });
|
|
10079
|
+
}
|
|
10080
|
+
}
|
|
10031
10081
|
let message = e instanceof Error ? e.message : String(e);
|
|
10032
|
-
const backup = cfg.automaticRecovery?.enabled ? cfg.automaticRecovery.backup : undefined;
|
|
10033
10082
|
const current = store.projectBotForTask(bot.id, threadId);
|
|
10034
|
-
|
|
10083
|
+
const backups = cfg.automaticRecovery?.enabled
|
|
10084
|
+
? current?.fallback?.length ? current.fallback : cfg.automaticRecovery.backup ? [cfg.automaticRecovery.backup] : []
|
|
10085
|
+
: [];
|
|
10086
|
+
const nextIndex = backups.findIndex((candidate, index) => index >= (opts?.automaticRecoveryIndex ?? 0) &&
|
|
10087
|
+
registry.get(candidate.instanceId)?.enabled &&
|
|
10088
|
+
(candidate.instanceId !== instanceId || candidate.model !== model));
|
|
10089
|
+
const backup = backups[nextIndex];
|
|
10090
|
+
if (mayRecover && backup && current && (!opts?.cardContinuation || opts.coordination) &&
|
|
10035
10091
|
!directRequestOwners.get(threadId)?.stopped && !hasQueuedSteeredMessages(bot.id, threadId) &&
|
|
10036
10092
|
JSON.stringify(current.modelSelection) === JSON.stringify(bot.modelSelection) &&
|
|
10037
10093
|
(backup.instanceId !== instanceId || backup.model !== model) &&
|
|
@@ -10055,7 +10111,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10055
10111
|
name: `recovery: Automatic recovery: ${instance.displayName || instance.driverKind} could not start. Trying ${target.displayName || target.driverKind} · ${checked.selection.model} once in this thread.`, ok: true,
|
|
10056
10112
|
} });
|
|
10057
10113
|
await startTurn(bot.id, text, { ...opts, threadId, userMessage, editedMessageId: undefined,
|
|
10058
|
-
automaticRecoveryAttempted: true });
|
|
10114
|
+
automaticRecoveryAttempted: true, automaticRecoveryIndex: nextIndex + 1 });
|
|
10059
10115
|
// The backup now owns these callbacks; don't publish a failed
|
|
10060
10116
|
// routine/peer result or release a queued receipt in between.
|
|
10061
10117
|
directFollowupSettlers.delete(dispatchClaimId);
|
|
@@ -10395,15 +10451,15 @@ routines = new RoutineManager({
|
|
|
10395
10451
|
goalRunId: runId,
|
|
10396
10452
|
});
|
|
10397
10453
|
},
|
|
10398
|
-
interruptTurn: async (botId, threadId
|
|
10454
|
+
interruptTurn: async (botId, threadId) => {
|
|
10399
10455
|
const bot = botForThread(botId, threadId);
|
|
10400
10456
|
pendingDelegationWakes.delete(threadId);
|
|
10401
10457
|
discardDelegations(commsBus, threadId);
|
|
10402
10458
|
cancelDirectTurnDispatch(botId, threadId);
|
|
10403
10459
|
revokeInternalCapabilitiesForThread(threadId);
|
|
10404
10460
|
const instance = bot
|
|
10405
|
-
? runningTurnInstance(bot, threadId
|
|
10406
|
-
:
|
|
10461
|
+
? runningTurnInstance(bot, threadId)
|
|
10462
|
+
: runningTurnEngines.get(threadId) ?? null;
|
|
10407
10463
|
try {
|
|
10408
10464
|
await instance?.adapter.interruptTurn(threadId);
|
|
10409
10465
|
}
|
|
@@ -10564,9 +10620,13 @@ async function cloudRoutineReadiness(botId, threadId) {
|
|
|
10564
10620
|
reason: 'The Boat cloud computer needs a working Boat API key. For the bot’s configured computer, including a self-hosted VPS, set run_on="maus" instead.',
|
|
10565
10621
|
};
|
|
10566
10622
|
}
|
|
10567
|
-
const instance =
|
|
10623
|
+
const instance = registry.get(bot.modelSelection.instanceId);
|
|
10568
10624
|
if (!instance)
|
|
10569
|
-
return { ready: false, reason: "The
|
|
10625
|
+
return { ready: false, reason: "The target bot's model is unavailable. Choose another model in the bot's settings." };
|
|
10626
|
+
// The same rule a cloud turn applies, before anything is provisioned.
|
|
10627
|
+
const unsupported = cloudPlaceDriverError(instance.adapter.capabilities, "box");
|
|
10628
|
+
if (unsupported)
|
|
10629
|
+
return { ready: false, reason: "This model can't use a computer. Choose another model, or change where this routine runs." };
|
|
10570
10630
|
try {
|
|
10571
10631
|
if ((await instance.snapshot()).state !== "available") {
|
|
10572
10632
|
return { ready: false, reason: "The target bot's model is not ready to use the Boat cloud computer." };
|
|
@@ -10859,6 +10919,69 @@ const validateModelProposal = (selection, current) => {
|
|
|
10859
10919
|
}
|
|
10860
10920
|
return null;
|
|
10861
10921
|
};
|
|
10922
|
+
const teamMemory = new TeamMemory(join(DATA_DIR, "team-memory.json"));
|
|
10923
|
+
const outboundRequests = new OutboundRequestService();
|
|
10924
|
+
const outboundCounts = new OutboundCounts(join(DATA_DIR, "outbound-counts.json"));
|
|
10925
|
+
const OUTBOUND_HOLD_MS = 9 * 60_000;
|
|
10926
|
+
function resolveAndSendTrust(res, threadId, requestId, behavior) {
|
|
10927
|
+
const teamMemoryCard = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId && message.card.teamMemoryRequest);
|
|
10928
|
+
if (teamMemoryCard?.card?.teamMemoryRequest) {
|
|
10929
|
+
const request = teamMemoryCard.card.teamMemoryRequest;
|
|
10930
|
+
const proposer = teamMemoryCard.from?.botId ? store.bot(teamMemoryCard.from.botId) : store.botByThread(threadId);
|
|
10931
|
+
const result = teamMemory.resolve(request.section, request.entryId, behavior === "allow" ? "accept" : "reject");
|
|
10932
|
+
if (!result.claimed) {
|
|
10933
|
+
if (!teamMemoryCard.card.answered) {
|
|
10934
|
+
store.patchMessage(threadId, teamMemoryCard.id, { card: { ...teamMemoryCard.card, answered: "unavailable", dismissed: true } });
|
|
10935
|
+
}
|
|
10936
|
+
json(res, 200, { ok: true, outcome: "unavailable" });
|
|
10937
|
+
return true;
|
|
10938
|
+
}
|
|
10939
|
+
if (result.state === "already_settled") {
|
|
10940
|
+
json(res, 200, { ok: true, outcome: "allowed-once", alreadySettled: true });
|
|
10941
|
+
return true;
|
|
10942
|
+
}
|
|
10943
|
+
store.patchMessage(threadId, teamMemoryCard.id, {
|
|
10944
|
+
card: { ...teamMemoryCard.card, answered: result.state === "accepted" ? "allow" : "deny" },
|
|
10945
|
+
});
|
|
10946
|
+
appendDecision(DATA_DIR, {
|
|
10947
|
+
threadId, requestId, botId: proposer?.id, botName: proposer?.name,
|
|
10948
|
+
tool: "propose_team_memory", summary: teamMemoryCard.card.subtitle,
|
|
10949
|
+
decision: result.state === "accepted" ? "user-approved" : "user-denied", source: "user",
|
|
10950
|
+
});
|
|
10951
|
+
json(res, 200, { ok: true, outcome: result.state === "accepted" ? "allowed-once" : "rejected" });
|
|
10952
|
+
return true;
|
|
10953
|
+
}
|
|
10954
|
+
const outboundCard = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId && message.card.outboundRequest);
|
|
10955
|
+
if (outboundCard?.card?.outboundRequest) {
|
|
10956
|
+
const outboundBot = outboundCard.from?.botId ? store.bot(outboundCard.from.botId) : store.botByThread(threadId);
|
|
10957
|
+
const result = outboundRequests.resolve({ threadId, requestId, behavior });
|
|
10958
|
+
if (!result.claimed) {
|
|
10959
|
+
// The hold is memory-only: after a restart the card outlives the
|
|
10960
|
+
// relay that was waiting on it, so close it rather than leave an
|
|
10961
|
+
// approval nobody can deliver owning the composer.
|
|
10962
|
+
if (!outboundCard.card.answered) {
|
|
10963
|
+
store.patchMessage(threadId, outboundCard.id, { card: { ...outboundCard.card, answered: "unavailable", dismissed: true } });
|
|
10964
|
+
}
|
|
10965
|
+
json(res, 200, { ok: true, outcome: "unavailable" });
|
|
10966
|
+
return true;
|
|
10967
|
+
}
|
|
10968
|
+
if (result.state === "already_settled") {
|
|
10969
|
+
json(res, 200, { ok: true, outcome: result.behavior === "allow" ? "allowed-once" : "rejected", alreadySettled: true });
|
|
10970
|
+
return true;
|
|
10971
|
+
}
|
|
10972
|
+
store.patchMessage(threadId, outboundCard.id, {
|
|
10973
|
+
card: { ...outboundCard.card, answered: result.state === "allowed" ? "allow" : "deny" },
|
|
10974
|
+
});
|
|
10975
|
+
appendDecision(DATA_DIR, {
|
|
10976
|
+
threadId, requestId, botId: outboundBot?.id, botName: outboundBot?.name,
|
|
10977
|
+
tool: outboundCard.card.tool, summary: outboundCard.card.subtitle,
|
|
10978
|
+
decision: result.state === "allowed" ? "user-approved" : "user-denied", source: "user",
|
|
10979
|
+
});
|
|
10980
|
+
json(res, 200, { ok: true, outcome: result.state === "allowed" ? "allowed-once" : "rejected" });
|
|
10981
|
+
return true;
|
|
10982
|
+
}
|
|
10983
|
+
return false;
|
|
10984
|
+
}
|
|
10862
10985
|
const profileRequests = new ProfileRequestService({
|
|
10863
10986
|
store,
|
|
10864
10987
|
autoApply: fullAccessForSource,
|
|
@@ -11475,7 +11598,7 @@ setupRetry = 0) {
|
|
|
11475
11598
|
const preparedApprovalMode = roomTurnApprovalMode(bot, threadId, orchestration);
|
|
11476
11599
|
const preparedSelection = { ...bot.modelSelection };
|
|
11477
11600
|
const preparedComposio = bot.composio;
|
|
11478
|
-
const instance =
|
|
11601
|
+
const instance = registry.get(bot.modelSelection.instanceId);
|
|
11479
11602
|
const userName = cfg.profile?.name?.trim() || "User";
|
|
11480
11603
|
if (providerInstancesChanging.has(bot.modelSelection.instanceId)) {
|
|
11481
11604
|
onDispatchError?.(`${bot.name}'s provider account is being updated — try again shortly`);
|
|
@@ -11655,7 +11778,7 @@ setupRetry = 0) {
|
|
|
11655
11778
|
: Boolean(readyGroup && store.groupTaskByThread(readyGroup.id, threadId));
|
|
11656
11779
|
if (!readyGroup || !stillOwnsThread || !readyGroup.memberIds.includes(readyBot.id))
|
|
11657
11780
|
return false;
|
|
11658
|
-
const setupChanged =
|
|
11781
|
+
const setupChanged = registry.get(readyBot.modelSelection.instanceId) !== instance ||
|
|
11659
11782
|
roomTurnApprovalMode(readyBot, threadId, orchestration) !== preparedApprovalMode ||
|
|
11660
11783
|
readyBot.modelSelection.instanceId !== preparedSelection.instanceId ||
|
|
11661
11784
|
readyBot.modelSelection.model !== preparedSelection.model ||
|
|
@@ -11780,6 +11903,13 @@ setupRetry = 0) {
|
|
|
11780
11903
|
if (roomPlan.computer === "local" && instance.adapter.capabilities.remoteAgent === true) {
|
|
11781
11904
|
throw new Error("the Computer engine works on the cloud computer — set Works on to Cloud, or choose another engine");
|
|
11782
11905
|
}
|
|
11906
|
+
// The same one rule as a bot thread, before anything is provisioned: an
|
|
11907
|
+
// engine that cannot use the cloud computer is refused, never swapped.
|
|
11908
|
+
if (!roomTeamComputer && roomPlan.computer === "cloud") {
|
|
11909
|
+
const unsupported = cloudPlaceDriverError(instance.adapter.capabilities, turnProvider(readyBot) === "vps" ? "vps" : "box");
|
|
11910
|
+
if (unsupported)
|
|
11911
|
+
throw new PlaceUnavailableError("cloud", "works-on", unsupported);
|
|
11912
|
+
}
|
|
11783
11913
|
// One place per room turn as well: a team computer reached on Auto means
|
|
11784
11914
|
// no separate built-in browser.
|
|
11785
11915
|
if (roomPlan.browser && !(roomPlan.computer === undefined && roomTeamComputer)) {
|
|
@@ -11798,11 +11928,11 @@ setupRetry = 0) {
|
|
|
11798
11928
|
return false;
|
|
11799
11929
|
}
|
|
11800
11930
|
if (roomTeamComputer) {
|
|
11801
|
-
const attached = await attachTeamBoat(roomTeamComputer, readyBot.id, resourceOwner, instance.adapter.capabilities
|
|
11931
|
+
const attached = await attachTeamBoat(roomTeamComputer, readyBot.id, resourceOwner, usesCloudComputer(instance.adapter.capabilities), instance.adapter.capabilities.remoteAgent === true);
|
|
11802
11932
|
if (isCancelled?.() || groupSpeakers.get(threadId) !== roomSpeaker ||
|
|
11803
11933
|
activeInternalGenerationByThread.get(threadId) !== internalGeneration)
|
|
11804
11934
|
return false;
|
|
11805
|
-
integrations
|
|
11935
|
+
Object.assign(integrations, attached.mount);
|
|
11806
11936
|
startScreenPoller(readyBot.id, threadId, { computer: attached.capture }, { screenIsTheWork: instance.adapter.capabilities.remoteAgent === true });
|
|
11807
11937
|
}
|
|
11808
11938
|
// The speaker's own explicit place, mounted exactly as its bot thread
|
|
@@ -11811,89 +11941,99 @@ setupRetry = 0) {
|
|
|
11811
11941
|
const roomSetupIsCurrent = () => !isCancelled?.() &&
|
|
11812
11942
|
groupSpeakers.get(threadId) === roomSpeaker &&
|
|
11813
11943
|
activeInternalGenerationByThread.get(threadId) === internalGeneration;
|
|
11814
|
-
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
}
|
|
11820
|
-
if (!roomTeamComputer && roomPlan.computer === "cloud") {
|
|
11821
|
-
if (turnProvider(readyBot) === "vps") {
|
|
11822
|
-
const unsupported = vps.vpsDriverError(instance.driverKind, instance.adapter.capabilities.computerMcp === true);
|
|
11823
|
-
if (unsupported)
|
|
11824
|
-
throw new Error(unsupported);
|
|
11825
|
-
vpsThreadStarted(readyBot.id, threadId);
|
|
11826
|
-
roomVpsBotId = readyBot.id;
|
|
11827
|
-
const mounted = await mountBotVps(readyBot, resourceOwner, {
|
|
11828
|
-
start: true,
|
|
11829
|
-
onClaimed: (capture) => {
|
|
11830
|
-
if (roomSetupIsCurrent() && store.group(readyGroup.id)?.busyBotId === readyBot.id) {
|
|
11831
|
-
roomScreenBotId = readyBot.id;
|
|
11832
|
-
startScreenPoller(readyBot.id, threadId, { computer: capture });
|
|
11833
|
-
}
|
|
11834
|
-
},
|
|
11835
|
-
});
|
|
11944
|
+
// An explicit place that cannot be attached fails the member's turn with
|
|
11945
|
+
// its cause and the one setting that changes it.
|
|
11946
|
+
try {
|
|
11947
|
+
if (!roomTeamComputer && roomPlan.computer === "local") {
|
|
11948
|
+
integrations.localComputer = await mountHostComputer(resourceOwner, readyBot.id, instance.adapter.capabilities.localComputerMcp === true);
|
|
11836
11949
|
if (!roomSetupIsCurrent())
|
|
11837
11950
|
return false;
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11951
|
+
roomComputerKind = "local";
|
|
11952
|
+
}
|
|
11953
|
+
if (!roomTeamComputer && roomPlan.computer === "cloud") {
|
|
11954
|
+
if (turnProvider(readyBot) === "vps") {
|
|
11955
|
+
vpsThreadStarted(readyBot.id, threadId);
|
|
11956
|
+
roomVpsBotId = readyBot.id;
|
|
11957
|
+
const mounted = await mountBotVps(readyBot, resourceOwner, {
|
|
11958
|
+
start: true,
|
|
11959
|
+
onClaimed: (capture) => {
|
|
11960
|
+
if (roomSetupIsCurrent() && store.group(readyGroup.id)?.busyBotId === readyBot.id) {
|
|
11961
|
+
roomScreenBotId = readyBot.id;
|
|
11962
|
+
startScreenPoller(readyBot.id, threadId, { computer: capture });
|
|
11963
|
+
}
|
|
11964
|
+
},
|
|
11965
|
+
});
|
|
11966
|
+
if (!roomSetupIsCurrent())
|
|
11967
|
+
return false;
|
|
11968
|
+
if (!("integration" in mounted))
|
|
11969
|
+
throw new Error(mounted.problem ?? "the VPS computer could not be created or reached");
|
|
11970
|
+
integrations.localComputer = mounted.integration;
|
|
11971
|
+
roomComputerKind = "vps";
|
|
11972
|
+
}
|
|
11973
|
+
else {
|
|
11974
|
+
if (!boat.boatConfigured(cfg))
|
|
11975
|
+
throw new Error(BOAT_NOT_CONFIGURED);
|
|
11976
|
+
const remoteAgent = instance.adapter.capabilities.remoteAgent === true;
|
|
11977
|
+
const attached = await attachBotBoat(readyBot, resourceOwner, { explicitCloud: true,
|
|
11978
|
+
canMount: usesCloudComputer(instance.adapter.capabilities), remoteAgent });
|
|
11979
|
+
if (!roomSetupIsCurrent())
|
|
11980
|
+
return false;
|
|
11981
|
+
if (!attached?.mount)
|
|
11982
|
+
throw new Error("the cloud computer could not be created or reached");
|
|
11983
|
+
Object.assign(integrations, attached.mount);
|
|
11984
|
+
roomScreenBotId = readyBot.id;
|
|
11985
|
+
startScreenPoller(readyBot.id, threadId, { computer: attached.capture }, { screenIsTheWork: remoteAgent });
|
|
11986
|
+
roomComputerKind = "box";
|
|
11987
|
+
}
|
|
11842
11988
|
}
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11989
|
+
// Room and Goal turns use the speaker's desktop, never the coordinator's.
|
|
11990
|
+
// Claim the same lease as direct turns before asynchronous VM setup.
|
|
11991
|
+
if (readyBot.computer === "vm") {
|
|
11992
|
+
if (instance.adapter.capabilities.computerMcp !== true || instance.adapter.capabilities.remoteAgent === true) {
|
|
11993
|
+
throw new Error("this model cannot use the Local VM");
|
|
11994
|
+
}
|
|
11995
|
+
// A distinct identity fences cleanup even in shared mode on the same room thread.
|
|
11996
|
+
const target = { ...localVmTargetForThread(readyBot.id, threadId) };
|
|
11997
|
+
await bindTurnComputer(resourceOwner, `computer:vm:${target.key}`, true);
|
|
11998
|
+
if (localVmImageBusy || localVmModeChangeBusy || localVmLifecycleBusy.has(target.key)) {
|
|
11999
|
+
throw new Error("this Local VM is being started, stopped, or replaced");
|
|
12000
|
+
}
|
|
12001
|
+
if (!localVmLeaseFor(target).claim(threadId, readyBot.id, localVmOwnerBusy)) {
|
|
12002
|
+
throw new Error("this Local VM is already being used by another turn");
|
|
12003
|
+
}
|
|
12004
|
+
if (localVmMode(cfg) === "pool")
|
|
12005
|
+
localVmSeatPool.touch(threadId);
|
|
12006
|
+
roomVmTarget = target;
|
|
12007
|
+
localVmThreadTargets.set(threadId, target);
|
|
12008
|
+
localVmActiveThreads.set(target.key, threadId);
|
|
12009
|
+
localVmIdleFor(target).touch();
|
|
12010
|
+
const setupIsCurrent = () => !isCancelled?.() &&
|
|
12011
|
+
groupSpeakers.get(threadId) === roomSpeaker &&
|
|
12012
|
+
activeInternalGenerationByThread.get(threadId) === internalGeneration &&
|
|
12013
|
+
store.group(readyGroup.id)?.memberIds.includes(readyBot.id) === true &&
|
|
12014
|
+
store.bot(readyBot.id)?.busy === true &&
|
|
12015
|
+
store.group(readyGroup.id)?.busyBotId === readyBot.id;
|
|
12016
|
+
const vm = await readyLocalVmForTurn(readyBot.id, target, setupIsCurrent);
|
|
12017
|
+
if (!setupIsCurrent()) {
|
|
11850
12018
|
return false;
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
12019
|
+
}
|
|
12020
|
+
if (!vm.ready || !vm.runtime)
|
|
12021
|
+
throw new Error(vm.problem ?? "the Local VM is not ready");
|
|
12022
|
+
const owner = localVmLeaseFor(target).current(localVmOwnerBusy);
|
|
12023
|
+
if (owner?.threadId !== threadId || owner.botId !== readyBot.id) {
|
|
12024
|
+
throw new Error("the Local VM lease expired while preparing the turn");
|
|
12025
|
+
}
|
|
12026
|
+
integrations.localComputer = containerComputerMcp(vm.runtime, controlIntegration(readyBot.id, threadId, internalGeneration, target), target);
|
|
11857
12027
|
}
|
|
11858
12028
|
}
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
// A
|
|
11866
|
-
|
|
11867
|
-
await bindTurnComputer(resourceOwner, `computer:vm:${target.key}`, true);
|
|
11868
|
-
if (localVmImageBusy || localVmModeChangeBusy || localVmLifecycleBusy.has(target.key)) {
|
|
11869
|
-
throw new Error("this Local VM is being started, stopped, or replaced");
|
|
11870
|
-
}
|
|
11871
|
-
if (!localVmLeaseFor(target).claim(threadId, readyBot.id, localVmOwnerBusy)) {
|
|
11872
|
-
throw new Error("this Local VM is already being used by another turn");
|
|
11873
|
-
}
|
|
11874
|
-
if (localVmMode(cfg) === "pool")
|
|
11875
|
-
localVmSeatPool.touch(threadId);
|
|
11876
|
-
roomVmTarget = target;
|
|
11877
|
-
localVmThreadTargets.set(threadId, target);
|
|
11878
|
-
localVmActiveThreads.set(target.key, threadId);
|
|
11879
|
-
localVmIdleFor(target).touch();
|
|
11880
|
-
const setupIsCurrent = () => !isCancelled?.() &&
|
|
11881
|
-
groupSpeakers.get(threadId) === roomSpeaker &&
|
|
11882
|
-
activeInternalGenerationByThread.get(threadId) === internalGeneration &&
|
|
11883
|
-
store.group(readyGroup.id)?.memberIds.includes(readyBot.id) === true &&
|
|
11884
|
-
store.bot(readyBot.id)?.busy === true &&
|
|
11885
|
-
store.group(readyGroup.id)?.busyBotId === readyBot.id;
|
|
11886
|
-
const vm = await readyLocalVmForTurn(readyBot.id, target, setupIsCurrent);
|
|
11887
|
-
if (!setupIsCurrent()) {
|
|
11888
|
-
return false;
|
|
11889
|
-
}
|
|
11890
|
-
if (!vm.ready || !vm.runtime)
|
|
11891
|
-
throw new Error(vm.problem ?? "the Local VM is not ready");
|
|
11892
|
-
const owner = localVmLeaseFor(target).current(localVmOwnerBusy);
|
|
11893
|
-
if (owner?.threadId !== threadId || owner.botId !== readyBot.id) {
|
|
11894
|
-
throw new Error("the Local VM lease expired while preparing the turn");
|
|
11895
|
-
}
|
|
11896
|
-
integrations.localComputer = containerComputerMcp(vm.runtime, controlIntegration(readyBot.id, threadId, internalGeneration, target), target);
|
|
12029
|
+
catch (error) {
|
|
12030
|
+
const place = roomPlan.computer;
|
|
12031
|
+
if (place !== "cloud" && place !== "vm" && place !== "local")
|
|
12032
|
+
throw error;
|
|
12033
|
+
if (error instanceof DirectTurnSetupCancelled || error instanceof ComputerWaitParked || error instanceof PlaceUnavailableError)
|
|
12034
|
+
throw error;
|
|
12035
|
+
// A room row is not cut, so the cause keeps all its words.
|
|
12036
|
+
throw placeUnavailable(place, "works-on", error instanceof Error ? error.message : String(error), Infinity);
|
|
11897
12037
|
}
|
|
11898
12038
|
const roster = readyGroup.memberIds
|
|
11899
12039
|
.map((id) => store.bot(id))
|
|
@@ -11979,7 +12119,7 @@ setupRetry = 0) {
|
|
|
11979
12119
|
const recentLines = recentWork(recentWorkSources(bot), bot, { userName, currentThreadId: threadId, ...recentWorkFilter() });
|
|
11980
12120
|
const roomComputerPromptKind = resolveComputerPromptKind({
|
|
11981
12121
|
kind: roomTeamComputer || roomComputerKind === "box" ? "box" : roomVmTarget ? "vm" : roomComputerKind,
|
|
11982
|
-
driverKind: instance.driverKind,
|
|
12122
|
+
driverKind: instance.driverKind,
|
|
11983
12123
|
vmPrivate: localVmMode(cfg) === "per-bot",
|
|
11984
12124
|
});
|
|
11985
12125
|
{
|
|
@@ -12005,6 +12145,7 @@ setupRetry = 0) {
|
|
|
12005
12145
|
{ id: "recall", label: "Recall", text: integrations.agents && bot.memoryEnabled !== false ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
|
|
12006
12146
|
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentLines) },
|
|
12007
12147
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
12148
|
+
{ id: "team-memory", label: "Team memory", text: teamMemory.systemPrompt(bot.section) + (integrations.agents ? TEAM_MEMORY_PROMPT : "") },
|
|
12008
12149
|
// the room path has always put a newline before memory and trimmed
|
|
12009
12150
|
// the block's leading space; keep that so existing prompts are
|
|
12010
12151
|
// byte-identical. The write guidance follows the tools actually
|
|
@@ -12137,7 +12278,6 @@ setupRetry = 0) {
|
|
|
12137
12278
|
threadId,
|
|
12138
12279
|
botId: readyBot.id,
|
|
12139
12280
|
text: withRecalled(roomRecalled, text),
|
|
12140
|
-
refreshSystemPrompt: true,
|
|
12141
12281
|
images: turnImages,
|
|
12142
12282
|
approvalMode: roomTurnApprovalMode(readyBot, threadId, orchestration),
|
|
12143
12283
|
toolScope: toolScopeForTurn(readyBot.id),
|
|
@@ -15212,7 +15352,7 @@ const handleRequest = async (req, res) => {
|
|
|
15212
15352
|
? "phone"
|
|
15213
15353
|
: path.startsWith("/api/internal/connectors/")
|
|
15214
15354
|
? "connectors"
|
|
15215
|
-
: path === "/api/internal/computer-control"
|
|
15355
|
+
: path === "/api/internal/computer-control" || path === "/api/internal/computer/mcp"
|
|
15216
15356
|
? "computer"
|
|
15217
15357
|
: "agents";
|
|
15218
15358
|
if (internalCapability.kind !== requiredCapabilityKind) {
|
|
@@ -15369,7 +15509,7 @@ const handleRequest = async (req, res) => {
|
|
|
15369
15509
|
if (method === "POST" && path === "/api/internal/memory") {
|
|
15370
15510
|
const body = await readInternalBody();
|
|
15371
15511
|
const result = ownersWrite(() => updateMemory(internalSender.id, { action: body.action, text: body.text, oldText: body.oldText, ...(body.until !== undefined ? { until: body.until } : {}) }, { source: memorySource() }));
|
|
15372
|
-
return json(res, result.ok ? 200 : result.code === "conflict" ? 409 :
|
|
15512
|
+
return json(res, result.ok ? 200 : result.code === "conflict" ? 409 : 400, result);
|
|
15373
15513
|
}
|
|
15374
15514
|
if (method === "POST" && path === "/api/internal/memory/log") {
|
|
15375
15515
|
const body = await readInternalBody();
|
|
@@ -15403,6 +15543,22 @@ const handleRequest = async (req, res) => {
|
|
|
15403
15543
|
requireActiveInternalCapability();
|
|
15404
15544
|
return json(res, 200, { result });
|
|
15405
15545
|
}
|
|
15546
|
+
// The cloud computer's tools (harness-mcp-proxy computer): the turn's
|
|
15547
|
+
// capability names its Boat, and the Boat credential stays here.
|
|
15548
|
+
if (method === "POST" && path === "/api/internal/computer/mcp") {
|
|
15549
|
+
const body = await readInternalBody();
|
|
15550
|
+
const boxId = internalCapability.boxId;
|
|
15551
|
+
if (!boxId)
|
|
15552
|
+
return json(res, 403, { error: "this turn has no cloud computer" });
|
|
15553
|
+
const abort = new AbortController();
|
|
15554
|
+
res.once("close", () => { if (!res.writableEnded)
|
|
15555
|
+
abort.abort(); });
|
|
15556
|
+
return json(res, 200, { result: await cloudComputerRpc(body, {
|
|
15557
|
+
cfg, boxId, signal: abort.signal,
|
|
15558
|
+
gate: () => computerCallGate(internalCapability),
|
|
15559
|
+
assertActive: requireActiveInternalCapability,
|
|
15560
|
+
}) });
|
|
15561
|
+
}
|
|
15406
15562
|
if (method === "POST" && path === "/api/internal/phone/claim") {
|
|
15407
15563
|
// Lazy phone exclusivity (issue #1663): the turn holds computer:phone
|
|
15408
15564
|
// from its first real tool call until it settles. The proxy answers a
|
|
@@ -15784,6 +15940,58 @@ const handleRequest = async (req, res) => {
|
|
|
15784
15940
|
});
|
|
15785
15941
|
return json(res, 201, proposed);
|
|
15786
15942
|
}
|
|
15943
|
+
if (method === "POST" && path === "/api/internal/team-memory") {
|
|
15944
|
+
const parsed = z.object({
|
|
15945
|
+
fromBotId: z.string().min(1).max(128),
|
|
15946
|
+
fromThreadId: z.string().min(1).max(128),
|
|
15947
|
+
kind: z.string(),
|
|
15948
|
+
name: z.string(),
|
|
15949
|
+
detail: z.string(),
|
|
15950
|
+
aliases: z.array(z.string()).optional(),
|
|
15951
|
+
}).strict().safeParse(await readInternalBody());
|
|
15952
|
+
if (!parsed.success)
|
|
15953
|
+
return json(res, 400, { error: "invalid team memory proposal" });
|
|
15954
|
+
const body = parsed.data;
|
|
15955
|
+
const from = store.bot(body.fromBotId);
|
|
15956
|
+
if (!from)
|
|
15957
|
+
return json(res, 403, { error: "unknown sender" });
|
|
15958
|
+
const owner = connectorThread(from.id, body.fromThreadId);
|
|
15959
|
+
if (!owner)
|
|
15960
|
+
return json(res, 403, { error: "source conversation does not belong to sender" });
|
|
15961
|
+
if (!sameAudience(from.visibility, undefined) || (owner.group && !roomFeedsBot(owner.group, from))) {
|
|
15962
|
+
return json(res, 403, { error: "restricted conversations cannot publish section-wide memory; ask an admin to add it" });
|
|
15963
|
+
}
|
|
15964
|
+
if (!TEAM_MEMORY_KINDS.includes(body.kind)) {
|
|
15965
|
+
return json(res, 400, { error: `kind must be one of ${TEAM_MEMORY_KINDS.join(", ")}` });
|
|
15966
|
+
}
|
|
15967
|
+
let proposed;
|
|
15968
|
+
try {
|
|
15969
|
+
proposed = teamMemory.propose(from.section, { kind: body.kind, name: body.name, detail: body.detail, aliases: body.aliases }, { botId: from.id, botName: from.name, threadId: body.fromThreadId, at: Date.now() });
|
|
15970
|
+
}
|
|
15971
|
+
catch (error) {
|
|
15972
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
15973
|
+
}
|
|
15974
|
+
const summary = `${proposed.entry.kind[0].toUpperCase()}${proposed.entry.kind.slice(1)}: ${proposed.entry.name}${proposed.entry.aliases.length ? ` (also ${proposed.entry.aliases.join(", ")})` : ""} — ${proposed.entry.detail}`;
|
|
15975
|
+
const section = sectionContextKey(from.section);
|
|
15976
|
+
const card = store.appendMessage(body.fromThreadId, {
|
|
15977
|
+
role: "bot",
|
|
15978
|
+
kind: "options",
|
|
15979
|
+
...(owner.group ? { from: { botId: from.id, name: from.name, color: from.color } } : {}),
|
|
15980
|
+
card: {
|
|
15981
|
+
title: "Remember this for the team?",
|
|
15982
|
+
subtitle: summary,
|
|
15983
|
+
options: ["Remember", "Skip"],
|
|
15984
|
+
requestId: proposed.entry.id,
|
|
15985
|
+
tool: "propose_team_memory",
|
|
15986
|
+
teamMemoryRequest: { section, entryId: proposed.entry.id, kind: proposed.entry.kind },
|
|
15987
|
+
},
|
|
15988
|
+
});
|
|
15989
|
+
appendDecision(DATA_DIR, {
|
|
15990
|
+
threadId: body.fromThreadId, requestId: proposed.entry.id, botId: from.id, botName: from.name,
|
|
15991
|
+
tool: "propose_team_memory", summary, decision: "card-shown", source: "team-memory",
|
|
15992
|
+
});
|
|
15993
|
+
return json(res, 201, { status: "proposed", requestId: proposed.entry.id, messageId: card.id, summary });
|
|
15994
|
+
}
|
|
15787
15995
|
if (method === "POST" && path === "/api/internal/profile-requests") {
|
|
15788
15996
|
const parsed = z.object({
|
|
15789
15997
|
fromBotId: z.string().min(1).max(128),
|
|
@@ -16648,20 +16856,17 @@ const handleRequest = async (req, res) => {
|
|
|
16648
16856
|
})).filter(g => g.members.length);
|
|
16649
16857
|
return json(res, 200, { currentRoom: source ? { id: source.id, name: source.name, workingFolder: source.cwd || null } : null,
|
|
16650
16858
|
bots: reachablePeers(store.bots, internalSender).map(bot => ({ id: bot.id, name: bot.name, title: bot.title, section: bot.section, busy: bot.busy })),
|
|
16651
|
-
rooms, note: "
|
|
16859
|
+
rooms, note: "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." });
|
|
16652
16860
|
}
|
|
16653
16861
|
if (method === "POST" && path === "/api/internal/coordinate-bots") {
|
|
16654
16862
|
const parsed = z.object({
|
|
16655
16863
|
groupId: z.string().min(1).max(128).optional(),
|
|
16656
16864
|
botIds: z.array(z.string().min(1).max(128)).min(1).max(4).refine(ids => new Set(ids).size === ids.length),
|
|
16657
|
-
message: z.string().trim().min(1).max(4000),
|
|
16865
|
+
message: z.string().trim().min(1).max(4000),
|
|
16658
16866
|
rework: z.boolean().default(false),
|
|
16659
|
-
// Only ever a name for a thread, so it travels under the same
|
|
16660
|
-
// one-line rule as a peer thread title.
|
|
16661
|
-
label: z.string().trim().min(1).max(60).refine(fitsOnOneLine).optional(),
|
|
16662
16867
|
}).safeParse(await readInternalBody());
|
|
16663
16868
|
if (!parsed.success)
|
|
16664
|
-
return json(res, 400, { error: "Provide 1-4 distinct botIds
|
|
16869
|
+
return json(res, 400, { error: "Provide 1-4 distinct botIds and a message of 1-4000 characters." });
|
|
16665
16870
|
const groupId = parsed.data.groupId ?? source?.id;
|
|
16666
16871
|
const destination = groupId ? store.group(groupId) : undefined;
|
|
16667
16872
|
if (groupId && !destination)
|
|
@@ -16713,58 +16918,63 @@ const handleRequest = async (req, res) => {
|
|
|
16713
16918
|
});
|
|
16714
16919
|
approvalGranted = true;
|
|
16715
16920
|
}
|
|
16716
|
-
|
|
16717
|
-
const
|
|
16921
|
+
// A room shows one brief per call, addressed to the call's recipients.
|
|
16922
|
+
const requestBatchKey = destination ? randomUUID() : undefined;
|
|
16718
16923
|
// One receipt per recipient, in the order the caller addressed
|
|
16719
16924
|
// them. Dispatch is asynchronous (RoomHandoffs.tick), so a fresh
|
|
16720
|
-
// enqueue is honestly "queued"; a
|
|
16721
|
-
//
|
|
16925
|
+
// enqueue is honestly "queued"; a repeat reports the request it
|
|
16926
|
+
// repeats.
|
|
16722
16927
|
const receipts = [];
|
|
16928
|
+
// Why nothing was sent, in full: a receipt's detail is clipped.
|
|
16929
|
+
const refused = [];
|
|
16723
16930
|
for (const target of targets) {
|
|
16724
16931
|
let createdThread;
|
|
16725
16932
|
try {
|
|
16726
16933
|
requireActiveInternalCapability();
|
|
16934
|
+
// Outside a room this conversation has one thread with each
|
|
16935
|
+
// teammate: the first request opens it, everything after
|
|
16936
|
+
// continues it and runs once the work already there is done.
|
|
16937
|
+
const continued = destination ? undefined : store.workThread(target.botId, internalSender.id, address.threadId);
|
|
16727
16938
|
if (!destination) {
|
|
16728
|
-
const task = store.createTask(target.botId, `@${internalSender.name}
|
|
16939
|
+
const task = continued ?? store.createTask(target.botId, `@${internalSender.name} · work`, false, undefined, { botId: internalSender.id, name: internalSender.name, kind: "work", threadId: address.threadId, at: Date.now() });
|
|
16729
16940
|
if (!task)
|
|
16730
16941
|
throw new Error("The recipient no longer exists");
|
|
16731
|
-
target.threadId =
|
|
16942
|
+
target.threadId = task.threadId;
|
|
16943
|
+
if (!continued)
|
|
16944
|
+
createdThread = task.threadId;
|
|
16732
16945
|
applyDelegatedLevel(internalSender, internalCapability.threadId, store.bot(target.botId), target.threadId);
|
|
16733
16946
|
}
|
|
16734
|
-
const { node, duplicate } = roomHandoffs.enqueue(address, internalCapability.generation, internalCapability.roomHandoffId, target,
|
|
16735
|
-
// A
|
|
16736
|
-
|
|
16947
|
+
const { node, duplicate } = roomHandoffs.enqueue(address, internalCapability.generation, internalCapability.roomHandoffId, target, target.botId, parsed.data.message, approvalGranted, parsed.data.rework, [...store.messagesFor(address.threadId)].reverse().find(m => m.role === "user" && m.kind === "text")?.text ?? "", requestBatchKey);
|
|
16948
|
+
// A repeat lands on the request already made, possibly in a
|
|
16949
|
+
// thread the person archived meanwhile; a thread opened for it
|
|
16950
|
+
// is never used.
|
|
16951
|
+
if (duplicate && createdThread)
|
|
16737
16952
|
store.deleteTask(target.botId, createdThread);
|
|
16738
|
-
|
|
16739
|
-
if (!duplicate && createdThread)
|
|
16953
|
+
else if (createdThread)
|
|
16740
16954
|
threadStarters.set(createdThread, openerFrom(address.threadId));
|
|
16741
16955
|
createdThread = undefined; // The durable coordinator now owns this task.
|
|
16742
|
-
|
|
16743
|
-
//
|
|
16744
|
-
//
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16956
|
+
// Only a live request can still report: a finished one already
|
|
16957
|
+
// fired its report and tick() skips it, so a repeat of it sends
|
|
16958
|
+
// nothing and says how to run it again.
|
|
16959
|
+
const title = destination ? undefined : store.taskByThread(node.botId, node.threadId)?.title;
|
|
16960
|
+
const finished = {
|
|
16961
|
+
completed: "it already finished and its result stands; send it with rework=true to run it again",
|
|
16962
|
+
failed: "it failed; send it with rework=true to retry", cancelled: "it was cancelled; send it with rework=true to retry",
|
|
16963
|
+
};
|
|
16964
|
+
const notSent = duplicate && finished[node.status] ? `not sent again: ${finished[node.status]}` : undefined;
|
|
16965
|
+
if (notSent)
|
|
16966
|
+
refused.push(notSent);
|
|
16748
16967
|
receipts.push(peerDeliveryReceipt({
|
|
16749
16968
|
botId: node.botId,
|
|
16750
16969
|
botName: store.bot(node.botId)?.name,
|
|
16751
|
-
outcome: duplicate
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
: "queued",
|
|
16758
|
-
detail: duplicate
|
|
16759
|
-
? node.status === "running"
|
|
16760
|
-
? "duplicate request_key — this teammate is already running that exact assignment"
|
|
16761
|
-
: node.status === "completed"
|
|
16762
|
-
? "duplicate request_key — this teammate already completed that assignment; the earlier result stands"
|
|
16763
|
-
: node.status === "failed" || node.status === "cancelled"
|
|
16764
|
-
? `duplicate request_key — the earlier assignment ${node.status === "cancelled" ? "was cancelled" : "failed"} and will not rerun or resume you; resend with a new request_key to retry`
|
|
16765
|
-
: "duplicate request_key — already in flight; its result will resume you automatically"
|
|
16766
|
-
: "handed to the coordinator; the teammate's turn has not started yet",
|
|
16970
|
+
outcome: notSent ? "failed" : !duplicate || node.status === "queued" ? "queued" : "injected",
|
|
16971
|
+
detail: notSent ?? (duplicate
|
|
16972
|
+
? `already sent: it is ${node.status === "queued" ? "queued" : "under way"} and its result will resume you`
|
|
16973
|
+
: title
|
|
16974
|
+
? `sent to "${title}"; it runs after anything still running there`
|
|
16975
|
+
: "handed to the coordinator; the teammate's turn has not started yet"),
|
|
16767
16976
|
requestId: node.id,
|
|
16977
|
+
...(destination ? {} : { threadId: node.threadId }),
|
|
16768
16978
|
}));
|
|
16769
16979
|
if (!duplicate) {
|
|
16770
16980
|
const recipient = store.bot(target.botId);
|
|
@@ -16781,14 +16991,13 @@ const handleRequest = async (req, res) => {
|
|
|
16781
16991
|
store.deleteTask(target.botId, createdThread);
|
|
16782
16992
|
}
|
|
16783
16993
|
const said = error instanceof Error ? error.message : String(error);
|
|
16784
|
-
|
|
16785
|
-
receipts.push(peerDeliveryReceipt({
|
|
16786
|
-
botId: target.botId, botName: store.bot(target.botId)?.name, outcome: "failed", detail: said,
|
|
16787
|
-
}));
|
|
16994
|
+
refused.push(said);
|
|
16995
|
+
receipts.push(peerDeliveryReceipt({ botId: target.botId, botName: store.bot(target.botId)?.name, outcome: "failed", detail: said }));
|
|
16788
16996
|
}
|
|
16789
16997
|
}
|
|
16790
|
-
|
|
16791
|
-
|
|
16998
|
+
const sent = refused.length < receipts.length;
|
|
16999
|
+
return json(res, sent ? 200 : 409, { receipts,
|
|
17000
|
+
...(sent ? { message: "End your turn after sending all work. These actual teammates will reply and resume you automatically. Do not poll or wait." } : { error: refused.join("; ") }),
|
|
16792
17001
|
});
|
|
16793
17002
|
}
|
|
16794
17003
|
}
|
|
@@ -17386,7 +17595,7 @@ const handleRequest = async (req, res) => {
|
|
|
17386
17595
|
// Reading a streamed MCP body yields to ordinary settings requests.
|
|
17387
17596
|
// Re-read the live bot immediately before relay so turning Connected
|
|
17388
17597
|
// Apps off wins over a request that authenticated under the old value.
|
|
17389
|
-
|
|
17598
|
+
let currentSender = store.bot(internalCapability.botId);
|
|
17390
17599
|
if (!currentSender || currentSender.composio === false || !composio.configured(cfg)) {
|
|
17391
17600
|
return json(res, 403, { error: "connected apps are not enabled for this bot" });
|
|
17392
17601
|
}
|
|
@@ -17397,6 +17606,7 @@ const handleRequest = async (req, res) => {
|
|
|
17397
17606
|
// Rows are fire-and-forget: a log failure must never take the call
|
|
17398
17607
|
// (or its refusal) down with it.
|
|
17399
17608
|
const call = connectorCallFromFrame(body);
|
|
17609
|
+
let serviceSlugs = [];
|
|
17400
17610
|
// A bot with no grants record keeps the pre-grants relay: even an
|
|
17401
17611
|
// unreadable frame passes through exactly as it always did.
|
|
17402
17612
|
if (call.kind === "unrecognized" && currentSender.connectorTools !== undefined) {
|
|
@@ -17423,7 +17633,11 @@ const handleRequest = async (req, res) => {
|
|
|
17423
17633
|
// underscored service (bland_ai) keeps its own tools instead of
|
|
17424
17634
|
// a plain-prefix grant (bland) capturing them; an unreachable
|
|
17425
17635
|
// catalog reads as empty and the plain split stands.
|
|
17426
|
-
|
|
17636
|
+
serviceSlugs = await composio.connectedServiceSlugs(cfg);
|
|
17637
|
+
requireActiveInternalCapability();
|
|
17638
|
+
currentSender = store.bot(internalCapability.botId);
|
|
17639
|
+
if (!currentSender || currentSender.composio === false || !composio.configured(cfg))
|
|
17640
|
+
return json(res, 403, { error: "connected apps are no longer enabled for this bot" });
|
|
17427
17641
|
const verdict = evaluateConnectorTools(call.names, currentSender.connectorTools, serviceSlugs);
|
|
17428
17642
|
if (!verdict.allowed) {
|
|
17429
17643
|
for (const denial of verdict.denials) {
|
|
@@ -17463,9 +17677,174 @@ const handleRequest = async (req, res) => {
|
|
|
17463
17677
|
rule: verdict.rule,
|
|
17464
17678
|
});
|
|
17465
17679
|
}
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17680
|
+
// ── the outbound gate ──
|
|
17681
|
+
// A tools/call that sends something (shared/outbound.ts) is its own
|
|
17682
|
+
// confirmation in every approval mode, Full included: it waits on a
|
|
17683
|
+
// card, or spends the bot's daily allowance. Everything else relays
|
|
17684
|
+
// untouched. A refusal is an ordinary tool error so the bot reads
|
|
17685
|
+
// it and carries on, rather than a transport failure it retries.
|
|
17686
|
+
const rpc = body && typeof body === "object" && !Array.isArray(body)
|
|
17687
|
+
? body
|
|
17688
|
+
: null;
|
|
17689
|
+
const rpcParams = rpc?.params && typeof rpc.params === "object" && !Array.isArray(rpc.params)
|
|
17690
|
+
? rpc.params
|
|
17691
|
+
: null;
|
|
17692
|
+
// The app tools this call would actually run, seen through Composio's
|
|
17693
|
+
// meta tools (a multi-execute list, workbench code), and the ones
|
|
17694
|
+
// among them that send.
|
|
17695
|
+
const outboundCalls = rpc?.method === "tools/call" && typeof rpcParams?.name === "string"
|
|
17696
|
+
? outboundCallsIn(rpcParams.name, rpcParams.arguments)
|
|
17697
|
+
: [];
|
|
17698
|
+
const outboundTool = outboundCalls.length ? outboundCalls[0].slug : null;
|
|
17699
|
+
const refuseOutbound = (text) => {
|
|
17700
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
17701
|
+
return res.end(JSON.stringify({ jsonrpc: "2.0", id: rpc?.id ?? null, result: { content: [{ type: "text", text }], isError: true } }));
|
|
17702
|
+
};
|
|
17703
|
+
// ── connection scopes ──
|
|
17704
|
+
// Checked first: an app this bot may not touch is refused outright,
|
|
17705
|
+
// never turned into a question for the person.
|
|
17706
|
+
const connectorCalls = rpc?.method === "tools/call" && typeof rpcParams?.name === "string"
|
|
17707
|
+
? connectorCallsIn(rpcParams.name, rpcParams.arguments)
|
|
17708
|
+
: [];
|
|
17709
|
+
const access = connectorAccessDecision(currentSender.connectorScopes, connectorCalls, serviceSlugs);
|
|
17710
|
+
if (!access.ok) {
|
|
17711
|
+
const described = describeTool(access.slug);
|
|
17712
|
+
const appName = described.app ?? access.toolkit;
|
|
17713
|
+
appendDecision(DATA_DIR, {
|
|
17714
|
+
threadId: internalCapability.threadId, botId: currentSender.id, botName: currentSender.name,
|
|
17715
|
+
tool: access.slug, decision: "auto-denied", source: "connector-scope", rule: `scope:${access.toolkit}:${access.reason}`,
|
|
17716
|
+
});
|
|
17717
|
+
return refuseOutbound(access.reason === "app"
|
|
17718
|
+
? `OpenMausBot did not run this: ${currentSender.name} has not been given access to ${appName}. Ask the user to allow it under the bot's Access settings, and do not retry.`
|
|
17719
|
+
: `OpenMausBot did not run this: ${appName} is read-only for ${currentSender.name}, and ${described.label} would write to it. Ask the user before trying again.`);
|
|
17720
|
+
}
|
|
17721
|
+
const opaque = connectorCalls.some(call => call.slug === "COMPOSIO_PROXY_EXECUTE");
|
|
17722
|
+
if (opaque && currentSender.connectorTools !== undefined)
|
|
17723
|
+
return refuseOutbound("OpenMausBot cannot verify this code against this bot's tool grants. Use a direct app tool.");
|
|
17724
|
+
let outboundApproved = false;
|
|
17725
|
+
if (outboundTool) {
|
|
17726
|
+
const policy = currentSender.outbound ?? DEFAULT_OUTBOUND_POLICY;
|
|
17727
|
+
const threadId = internalCapability.threadId;
|
|
17728
|
+
const { app } = describeTool(outboundTool);
|
|
17729
|
+
const summary = outboundCalls.map((call) => {
|
|
17730
|
+
const described = describeTool(call.slug);
|
|
17731
|
+
const argsText = call.arguments === undefined ? "" : JSON.stringify(call.arguments);
|
|
17732
|
+
return `${described.app ? `${described.app} · ` : ""}${described.label}${argsText ? `\n${argsText.slice(0, 400)}${argsText.length > 400 ? "… [arguments truncated]" : ""}` : ""}`;
|
|
17733
|
+
}).join("\n\n");
|
|
17734
|
+
const capNote = (count) => `OpenMausBot did not send this. ${currentSender.name} has reached its daily limit of ${count} outbound action${count === 1 ? "" : "s"}. Ask the user to raise the limit under the bot's Permissions before trying again.`;
|
|
17735
|
+
if (policy.policy === "allow" && !opaque) {
|
|
17736
|
+
const today = outboundCounts.today(currentSender.id);
|
|
17737
|
+
if (today + outboundCalls.length > policy.dailyCap) {
|
|
17738
|
+
appendDecision(DATA_DIR, {
|
|
17739
|
+
threadId, botId: currentSender.id, botName: currentSender.name,
|
|
17740
|
+
tool: outboundTool, summary, decision: "auto-denied", source: "outbound", rule: `daily-cap:${policy.dailyCap}`,
|
|
17741
|
+
});
|
|
17742
|
+
return refuseOutbound(capNote(policy.dailyCap));
|
|
17743
|
+
}
|
|
17744
|
+
}
|
|
17745
|
+
else {
|
|
17746
|
+
const owner = connectorThread(currentSender.id, threadId);
|
|
17747
|
+
const held = outboundRequests.open({ botId: currentSender.id, threadId, tool: outboundTool, timeoutMs: OUTBOUND_HOLD_MS });
|
|
17748
|
+
const card = store.appendMessage(threadId, {
|
|
17749
|
+
role: "bot",
|
|
17750
|
+
kind: "options",
|
|
17751
|
+
...(owner?.group ? { from: { botId: currentSender.id, name: currentSender.name, color: currentSender.color } } : {}),
|
|
17752
|
+
card: {
|
|
17753
|
+
title: "Send on your behalf?",
|
|
17754
|
+
subtitle: summary,
|
|
17755
|
+
options: ["Allow", "Deny"],
|
|
17756
|
+
requestId: held.requestId,
|
|
17757
|
+
tool: outboundTool,
|
|
17758
|
+
held: HELD_NOTE["approval.held.outbound"],
|
|
17759
|
+
heldCode: "approval.held.outbound",
|
|
17760
|
+
outboundRequest: { tool: outboundTool, app },
|
|
17761
|
+
},
|
|
17762
|
+
});
|
|
17763
|
+
appendDecision(DATA_DIR, {
|
|
17764
|
+
threadId, requestId: held.requestId, botId: currentSender.id, botName: currentSender.name,
|
|
17765
|
+
tool: outboundTool, summary, decision: "card-shown", source: "outbound",
|
|
17766
|
+
});
|
|
17767
|
+
if (owner?.group) {
|
|
17768
|
+
if (activeGroupTurnForBot(currentSender.id)?.threadId === threadId)
|
|
17769
|
+
store.setActivity(currentSender.id, "waiting-on-you");
|
|
17770
|
+
}
|
|
17771
|
+
else if (store.taskByThread(currentSender.id, threadId)?.busy) {
|
|
17772
|
+
store.setTaskActivity(currentSender.id, threadId, "waiting-on-you");
|
|
17773
|
+
}
|
|
17774
|
+
notify(buildNotification("approval", currentSender, threadId, summary, { avatarUrl: currentSender.avatarUrl }));
|
|
17775
|
+
const answer = await held.answer;
|
|
17776
|
+
outboundRequests.forget(held.requestId);
|
|
17777
|
+
if (internalCapabilityIsActive(internalCapability)) {
|
|
17778
|
+
const waiting = store.bot(currentSender.id);
|
|
17779
|
+
if (owner?.group) {
|
|
17780
|
+
if (activeGroupTurnForBot(currentSender.id)?.threadId === threadId && waiting?.activity === "waiting-on-you")
|
|
17781
|
+
store.setActivity(waiting.id, "working");
|
|
17782
|
+
}
|
|
17783
|
+
else if (store.taskByThread(currentSender.id, threadId)?.activity === "waiting-on-you") {
|
|
17784
|
+
store.setTaskActivity(currentSender.id, threadId, "working");
|
|
17785
|
+
}
|
|
17786
|
+
}
|
|
17787
|
+
if (answer !== "allow") {
|
|
17788
|
+
if (answer === "timeout") {
|
|
17789
|
+
const stale = store.messagesFor(threadId).find((message) => message.id === card.id);
|
|
17790
|
+
if (stale?.card && !stale.card.answered) {
|
|
17791
|
+
store.patchMessage(threadId, stale.id, { card: { ...stale.card, answered: "unavailable", dismissed: true } });
|
|
17792
|
+
}
|
|
17793
|
+
}
|
|
17794
|
+
return refuseOutbound(answer === "timeout"
|
|
17795
|
+
? "OpenMausBot did not send this: nobody answered the approval in time. Ask the user before trying again."
|
|
17796
|
+
: "OpenMausBot did not send this: the user declined. Do not retry it; ask the user what they would like instead.");
|
|
17797
|
+
}
|
|
17798
|
+
outboundApproved = true;
|
|
17799
|
+
requireActiveInternalCapability();
|
|
17800
|
+
}
|
|
17801
|
+
}
|
|
17802
|
+
let dispatchRefusal;
|
|
17803
|
+
const refuseDispatch = (text) => {
|
|
17804
|
+
dispatchRefusal = text;
|
|
17805
|
+
throw new Error(text);
|
|
17806
|
+
};
|
|
17807
|
+
let upstream;
|
|
17808
|
+
try {
|
|
17809
|
+
upstream = await composio.relayMcp(cfg, body, Array.isArray(req.headers["mcp-session-id"])
|
|
17810
|
+
? req.headers["mcp-session-id"][0]
|
|
17811
|
+
: req.headers["mcp-session-id"], () => {
|
|
17812
|
+
requireActiveInternalCapability();
|
|
17813
|
+
const fresh = store.bot(internalCapability.botId);
|
|
17814
|
+
if (!fresh || fresh.composio === false || !composio.configured(cfg))
|
|
17815
|
+
refuseDispatch("Connected apps were disabled while this call waited.");
|
|
17816
|
+
const freshAccess = connectorAccessDecision(fresh.connectorScopes, connectorCalls, serviceSlugs);
|
|
17817
|
+
if (!freshAccess.ok || (opaque && fresh.connectorTools !== undefined) ||
|
|
17818
|
+
(call.kind === "tools" && !evaluateConnectorTools(call.names, fresh.connectorTools, serviceSlugs).allowed) ||
|
|
17819
|
+
(call.kind === "unrecognized" && fresh.connectorTools !== undefined)) {
|
|
17820
|
+
refuseDispatch("This bot's connected-app permissions changed. The call was not run.");
|
|
17821
|
+
}
|
|
17822
|
+
if (outboundTool) {
|
|
17823
|
+
const policy = fresh.outbound ?? DEFAULT_OUTBOUND_POLICY;
|
|
17824
|
+
if (policy.policy === "allow" && !opaque) {
|
|
17825
|
+
let reserved;
|
|
17826
|
+
try {
|
|
17827
|
+
reserved = outboundCounts.reserve(fresh.id, outboundCalls.length, policy.dailyCap);
|
|
17828
|
+
}
|
|
17829
|
+
catch {
|
|
17830
|
+
refuseDispatch("The daily allowance could not be saved. Nothing was sent.");
|
|
17831
|
+
}
|
|
17832
|
+
if (reserved === null)
|
|
17833
|
+
refuseDispatch("This bot has reached its daily outbound limit. Nothing was sent.");
|
|
17834
|
+
appendDecision(DATA_DIR, { threadId: internalCapability.threadId, botId: fresh.id, botName: fresh.name,
|
|
17835
|
+
tool: outboundTool, decision: "auto-approved", source: "outbound", rule: `daily-allowance:${reserved}/${policy.dailyCap}` });
|
|
17836
|
+
}
|
|
17837
|
+
else if (!outboundApproved) {
|
|
17838
|
+
refuseDispatch("The outbound policy changed. Ask for approval again before sending.");
|
|
17839
|
+
}
|
|
17840
|
+
}
|
|
17841
|
+
});
|
|
17842
|
+
}
|
|
17843
|
+
catch (error) {
|
|
17844
|
+
if (dispatchRefusal)
|
|
17845
|
+
return refuseOutbound(dispatchRefusal);
|
|
17846
|
+
throw error;
|
|
17847
|
+
}
|
|
17469
17848
|
const headers = {
|
|
17470
17849
|
"content-type": upstream.contentType,
|
|
17471
17850
|
"cache-control": "no-store",
|
|
@@ -17543,6 +17922,16 @@ const handleRequest = async (req, res) => {
|
|
|
17543
17922
|
if (!composio.configured(cfg) || owner.bot.composio === false) {
|
|
17544
17923
|
return json(res, 409, { error: "connected apps are not enabled for this bot" });
|
|
17545
17924
|
}
|
|
17925
|
+
if (owner.bot.connectorScopes) {
|
|
17926
|
+
const outside = slugs.find((slug) => !owner.bot.connectorScopes?.apps[slug]);
|
|
17927
|
+
if (outside) {
|
|
17928
|
+
appendDecision(DATA_DIR, {
|
|
17929
|
+
threadId, botId: owner.bot.id, botName: owner.bot.name,
|
|
17930
|
+
tool: "COMPOSIO_MANAGE_CONNECTIONS", summary: outside, decision: "auto-denied", source: "connector-scope", rule: `scope:${outside}:app`,
|
|
17931
|
+
});
|
|
17932
|
+
return json(res, 403, { error: `${owner.bot.name} has not been given access to ${outside}; allow it under the bot's Access settings first` });
|
|
17933
|
+
}
|
|
17934
|
+
}
|
|
17546
17935
|
const connectionState = await composio.connectionStatus(cfg, slugs).catch(() => ({}));
|
|
17547
17936
|
requireActiveInternalCapability();
|
|
17548
17937
|
const messageIds = [];
|
|
@@ -20045,6 +20434,43 @@ const handleRequest = async (req, res) => {
|
|
|
20045
20434
|
patch.section = section ?? undefined;
|
|
20046
20435
|
if (body.chiefOfStaff === false)
|
|
20047
20436
|
patch.chiefOfStaff = false;
|
|
20437
|
+
// where a task carries on when this engine is unavailable, in order
|
|
20438
|
+
if (body.fallback !== undefined) {
|
|
20439
|
+
if (!Array.isArray(body.fallback) || body.fallback.length > 5) {
|
|
20440
|
+
return json(res, 400, { error: "fallback must be a list of up to five { instanceId, model } entries" });
|
|
20441
|
+
}
|
|
20442
|
+
const chain = [];
|
|
20443
|
+
for (const entry of body.fallback) {
|
|
20444
|
+
const row = entry && typeof entry === "object" ? entry : null;
|
|
20445
|
+
if (!row || typeof row.instanceId !== "string" || !/^[\w.-]+$/.test(row.instanceId) || typeof row.model !== "string" || !row.model.trim() || row.model.length > 500) {
|
|
20446
|
+
return json(res, 400, { error: "fallback must be a list of up to five { instanceId, model } entries" });
|
|
20447
|
+
}
|
|
20448
|
+
if (!registry.get(row.instanceId))
|
|
20449
|
+
return json(res, 400, { error: `fallback engine "${row.instanceId}" is not available` });
|
|
20450
|
+
if (chain.some((seen) => seen.instanceId === row.instanceId))
|
|
20451
|
+
continue;
|
|
20452
|
+
chain.push({ instanceId: row.instanceId, model: row.model.trim() });
|
|
20453
|
+
}
|
|
20454
|
+
patch.fallback = chain;
|
|
20455
|
+
}
|
|
20456
|
+
// which connected apps this bot may use; null clears back to all of them
|
|
20457
|
+
if (body.connectorScopes !== undefined) {
|
|
20458
|
+
if (body.connectorScopes === null)
|
|
20459
|
+
patch.connectorScopes = undefined;
|
|
20460
|
+
else {
|
|
20461
|
+
const scopes = normalizeConnectorScopes(body.connectorScopes);
|
|
20462
|
+
if (!scopes)
|
|
20463
|
+
return json(res, 400, { error: "connectorScopes must be { apps: { <toolkit>: read | write } } or null" });
|
|
20464
|
+
patch.connectorScopes = scopes;
|
|
20465
|
+
}
|
|
20466
|
+
}
|
|
20467
|
+
// sending on the person's behalf: ask every time, or a daily allowance
|
|
20468
|
+
if (body.outbound !== undefined) {
|
|
20469
|
+
const policy = normalizeOutboundPolicy(body.outbound);
|
|
20470
|
+
if (!policy)
|
|
20471
|
+
return json(res, 400, { error: "outbound must be { policy: ask | allow, dailyCap: 1..1000 }" });
|
|
20472
|
+
patch.outbound = policy;
|
|
20473
|
+
}
|
|
20048
20474
|
// per-bot gate on the workspace's connected apps (Composio)
|
|
20049
20475
|
if (body.composio !== undefined) {
|
|
20050
20476
|
if (typeof body.composio !== "boolean")
|
|
@@ -20356,6 +20782,16 @@ const handleRequest = async (req, res) => {
|
|
|
20356
20782
|
// desktop capability or a paired session, which is what the refusal
|
|
20357
20783
|
// points at.
|
|
20358
20784
|
const loosened = [];
|
|
20785
|
+
// These three fields were validated above before joining the broad patch.
|
|
20786
|
+
const trustPatch = patch;
|
|
20787
|
+
if (body.outbound !== undefined && trustPatch.outbound?.policy === "allow" &&
|
|
20788
|
+
(existingBot?.outbound?.policy !== "allow" || trustPatch.outbound.dailyCap > existingBot.outbound.dailyCap))
|
|
20789
|
+
loosened.push("outbound");
|
|
20790
|
+
if (body.connectorScopes !== undefined && existingBot?.connectorScopes &&
|
|
20791
|
+
(!trustPatch.connectorScopes || Object.entries(trustPatch.connectorScopes.apps).some(([slug, scope]) => !existingBot.connectorScopes.apps[slug] || (scope === "write" && existingBot.connectorScopes.apps[slug] === "read"))))
|
|
20792
|
+
loosened.push("connectorScopes");
|
|
20793
|
+
if (body.fallback !== undefined && trustPatch.fallback?.some(candidate => !existingBot?.fallback?.some(previous => previous.instanceId === candidate.instanceId && previous.model === candidate.model)))
|
|
20794
|
+
loosened.push("fallback");
|
|
20359
20795
|
if (Array.isArray(patch.managedSections) && patch.managedSections.some(section => !(existingBot?.managedSections ?? []).includes(section))) {
|
|
20360
20796
|
loosened.push("managedSections");
|
|
20361
20797
|
}
|
|
@@ -20731,6 +21167,71 @@ const handleRequest = async (req, res) => {
|
|
|
20731
21167
|
// it. That keeps one bot from silently changing every teammate's future
|
|
20732
21168
|
// turns. The section query parameter is required even for General (""),
|
|
20733
21169
|
// so a malformed client cannot accidentally read or replace that brief.
|
|
21170
|
+
// ── team memory: the section's shared people, places, decisions, terms ──
|
|
21171
|
+
// The person's view and their edits. Same section rule as the brief:
|
|
21172
|
+
// the query parameter is required even for General.
|
|
21173
|
+
if (path === "/api/team-memory" && (method === "GET" || method === "POST")) {
|
|
21174
|
+
if (!url.searchParams.has("section"))
|
|
21175
|
+
return json(res, 400, { error: "section is required" });
|
|
21176
|
+
const section = sectionContextKey(url.searchParams.get("section") ?? "");
|
|
21177
|
+
if (section.length > 60)
|
|
21178
|
+
return json(res, 400, { error: "section must be at most 60 characters" });
|
|
21179
|
+
if (method === "GET") {
|
|
21180
|
+
return json(res, 200, { section, label: sectionContextLabel(section), entries: teamMemory.list(section) });
|
|
21181
|
+
}
|
|
21182
|
+
const body = await readBody(req);
|
|
21183
|
+
if (!body || typeof body !== "object" || !TEAM_MEMORY_KINDS.includes(String(body.kind))) {
|
|
21184
|
+
return json(res, 400, { error: `kind must be one of ${TEAM_MEMORY_KINDS.join(", ")}` });
|
|
21185
|
+
}
|
|
21186
|
+
try {
|
|
21187
|
+
const proposed = teamMemory.propose(section, { kind: body.kind, name: String(body.name ?? ""), detail: String(body.detail ?? ""), aliases: Array.isArray(body.aliases) ? body.aliases.map(String) : undefined }, { botId: "", botName: "you", threadId: "", at: Date.now() });
|
|
21188
|
+
// the person's own entry never waits on the person
|
|
21189
|
+
if (proposed.status === "proposed")
|
|
21190
|
+
teamMemory.resolve(section, proposed.entry.id, "accept");
|
|
21191
|
+
return json(res, 201, { entries: teamMemory.list(section) });
|
|
21192
|
+
}
|
|
21193
|
+
catch (error) {
|
|
21194
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
21195
|
+
}
|
|
21196
|
+
}
|
|
21197
|
+
m = path.match(/^\/api\/team-memory\/([\w-]+)$/);
|
|
21198
|
+
if (m && (method === "PATCH" || method === "DELETE")) {
|
|
21199
|
+
if (!url.searchParams.has("section"))
|
|
21200
|
+
return json(res, 400, { error: "section is required" });
|
|
21201
|
+
const section = sectionContextKey(url.searchParams.get("section") ?? "");
|
|
21202
|
+
if (method === "DELETE") {
|
|
21203
|
+
if (!teamMemory.remove(section, m[1]))
|
|
21204
|
+
return json(res, 404, { error: "no such entry" });
|
|
21205
|
+
return json(res, 200, { entries: teamMemory.list(section) });
|
|
21206
|
+
}
|
|
21207
|
+
const body = await readBody(req);
|
|
21208
|
+
if (!body || typeof body !== "object")
|
|
21209
|
+
return json(res, 400, { error: "body must be a JSON object" });
|
|
21210
|
+
if (body.accept === true) {
|
|
21211
|
+
const result = teamMemory.resolve(section, m[1], "accept");
|
|
21212
|
+
if (!result.claimed)
|
|
21213
|
+
return json(res, 404, { error: "no such entry" });
|
|
21214
|
+
return json(res, 200, { entries: teamMemory.list(section) });
|
|
21215
|
+
}
|
|
21216
|
+
const patch = {};
|
|
21217
|
+
if (typeof body.name === "string")
|
|
21218
|
+
patch.name = body.name;
|
|
21219
|
+
if (typeof body.detail === "string")
|
|
21220
|
+
patch.detail = body.detail;
|
|
21221
|
+
if (Array.isArray(body.aliases))
|
|
21222
|
+
patch.aliases = body.aliases.map(String);
|
|
21223
|
+
if (typeof body.kind === "string" && TEAM_MEMORY_KINDS.includes(body.kind))
|
|
21224
|
+
patch.kind = body.kind;
|
|
21225
|
+
try {
|
|
21226
|
+
const updated = teamMemory.update(section, m[1], patch);
|
|
21227
|
+
if (!updated)
|
|
21228
|
+
return json(res, 404, { error: "no such entry" });
|
|
21229
|
+
return json(res, 200, { entry: updated, entries: teamMemory.list(section) });
|
|
21230
|
+
}
|
|
21231
|
+
catch (error) {
|
|
21232
|
+
return json(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
21233
|
+
}
|
|
21234
|
+
}
|
|
20734
21235
|
if (path === "/api/section-context" && (method === "GET" || method === "PUT")) {
|
|
20735
21236
|
if (!url.searchParams.has("section"))
|
|
20736
21237
|
return json(res, 400, { error: "section is required" });
|
|
@@ -21332,6 +21833,8 @@ const handleRequest = async (req, res) => {
|
|
|
21332
21833
|
return json(res, 403, { error: refusal });
|
|
21333
21834
|
}
|
|
21334
21835
|
return await answeringCardAs(auth, bot.threadId, String(body.requestId), async () => {
|
|
21836
|
+
if (resolveAndSendTrust(res, bot.threadId, String(body.requestId), behavior))
|
|
21837
|
+
return;
|
|
21335
21838
|
if (await resolveAndSendTeamSetup(res, {
|
|
21336
21839
|
botId: bot.id, threadId: bot.threadId, requestId: String(body.requestId), behavior,
|
|
21337
21840
|
}, auth.kind === "loopback" ? DESKTOP_MANAGED || Boolean(req.headers.origin) && !store.bots.some((bot) => bot.busy || activeGroupTurnForBot(bot.id)) : auth.scopes.includes("admin")))
|
|
@@ -21434,6 +21937,8 @@ const handleRequest = async (req, res) => {
|
|
|
21434
21937
|
return json(res, 403, { error: refusal });
|
|
21435
21938
|
}
|
|
21436
21939
|
return await answeringCardAs(auth, threadId, requestId, async () => {
|
|
21940
|
+
if (resolveAndSendTrust(res, threadId, requestId, behavior))
|
|
21941
|
+
return;
|
|
21437
21942
|
const skillCard = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId && message.card.skillRequest);
|
|
21438
21943
|
if (skillCard?.card?.skillRequest) {
|
|
21439
21944
|
const skillBotId = skillCard.from?.botId ?? store.botByThread(threadId)?.id;
|
|
@@ -22663,6 +23168,34 @@ const handleRequest = async (req, res) => {
|
|
|
22663
23168
|
res.end(decisionsCsv(readDecisionRange(DATA_DIR, range)));
|
|
22664
23169
|
return;
|
|
22665
23170
|
}
|
|
23171
|
+
// ── a bot's activity: what it did, with the outcome ──
|
|
23172
|
+
// Read-only over the same two logs as the inspector and the decision
|
|
23173
|
+
// route above. The bot's own threads (its DM and every task) carry the
|
|
23174
|
+
// tool runs; the decision log carries the bot's requests wherever they
|
|
23175
|
+
// happened, since those rows name the bot.
|
|
23176
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/activity$/);
|
|
23177
|
+
if (m && method === "GET") {
|
|
23178
|
+
const bot = store.bot(m[1]);
|
|
23179
|
+
if (!bot)
|
|
23180
|
+
return json(res, 404, { error: "no such bot" });
|
|
23181
|
+
const rawLimit = url.searchParams.get("limit");
|
|
23182
|
+
const parsedLimit = rawLimit === null ? undefined : Number(rawLimit);
|
|
23183
|
+
if (parsedLimit !== undefined && (!Number.isInteger(parsedLimit) || parsedLimit <= 0)) {
|
|
23184
|
+
return json(res, 400, { error: "limit must be a positive whole number" });
|
|
23185
|
+
}
|
|
23186
|
+
const threadIds = [...new Set([bot.threadId, ...store.tasks(bot.id).map((task) => task.threadId)])];
|
|
23187
|
+
return json(res, 200, {
|
|
23188
|
+
rows: readBotActivity({ dataDir: DATA_DIR, eventsDir: EVENTS_DIR, botId: bot.id, threadIds, limit: parsedLimit ?? 300 }),
|
|
23189
|
+
});
|
|
23190
|
+
}
|
|
23191
|
+
// ── a bot's outbound allowance: the policy, and how much of it is spent ──
|
|
23192
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/outbound$/);
|
|
23193
|
+
if (m && method === "GET") {
|
|
23194
|
+
const bot = store.bot(m[1]);
|
|
23195
|
+
if (!bot)
|
|
23196
|
+
return json(res, 404, { error: "no such bot" });
|
|
23197
|
+
return json(res, 200, { policy: bot.outbound ?? DEFAULT_OUTBOUND_POLICY, today: outboundCounts.today(bot.id) });
|
|
23198
|
+
}
|
|
22666
23199
|
// ── provider instances (model picker) ──
|
|
22667
23200
|
if (method === "GET" && path === "/api/instances") {
|
|
22668
23201
|
// Rescan PATH first: this endpoint is how the app answers "what can I
|
|
@@ -22947,7 +23480,7 @@ const handleRequest = async (req, res) => {
|
|
|
22947
23480
|
catch { /* Allow repairing an unused malformed account. */ }
|
|
22948
23481
|
entry.config = { ...entry.config, configDir: body.configDir };
|
|
22949
23482
|
if (previousDir !== configuredAccountDirectory(entry)) {
|
|
22950
|
-
const used = store.bots.some((bot) => bot.modelSelection.instanceId === instanceId || store.tasks(bot.id).some((task) => task.modelSelection?.instanceId === instanceId || task.resumeCursors[instanceId] || task.lastInstanceId === instanceId));
|
|
23483
|
+
const used = store.bots.some((bot) => bot.modelSelection.instanceId === instanceId || bot.fallback?.some(candidate => candidate.instanceId === instanceId) || store.tasks(bot.id).some((task) => task.modelSelection?.instanceId === instanceId || task.resumeCursors[instanceId] || task.lastInstanceId === instanceId));
|
|
22951
23484
|
if (used)
|
|
22952
23485
|
return json(res, 409, { error: "This account is used by bots or conversation history. Add another account and select it for the bot instead." });
|
|
22953
23486
|
assertSeparateClaudeAccount(instances, instanceId, entry);
|
|
@@ -22971,7 +23504,7 @@ const handleRequest = async (req, res) => {
|
|
|
22971
23504
|
if (instances[instanceId].driver !== "claudeAgent" || instanceId === "claude") {
|
|
22972
23505
|
return json(res, 400, { error: "Only added Claude accounts can be removed here." });
|
|
22973
23506
|
}
|
|
22974
|
-
if (cfg.defaultModelSelection?.instanceId === instanceId || store.bots.some((bot) => bot.modelSelection.instanceId === instanceId || store.tasks(bot.id).some((task) => task.modelSelection?.instanceId === instanceId)) ||
|
|
23507
|
+
if (cfg.defaultModelSelection?.instanceId === instanceId || store.bots.some((bot) => bot.modelSelection.instanceId === instanceId || bot.fallback?.some(candidate => candidate.instanceId === instanceId) || store.tasks(bot.id).some((task) => task.modelSelection?.instanceId === instanceId)) ||
|
|
22975
23508
|
busyProviderSelections().some((selection) => selection.instanceId === instanceId)) {
|
|
22976
23509
|
return json(res, 409, { error: "Choose another account for the bots and default model using this account before removing it." });
|
|
22977
23510
|
}
|