openmausbot 0.1.86 → 0.1.87
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/README.md +2 -2
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1029 -164
- package/dist-server/drivers/agents-proxy.js +64 -10
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +14577 -9001
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/openmausbot.js +3981 -1165
- package/dist-server/pair-cli.js +3981 -1165
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +59 -9
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +57 -15
- package/dist-server/server/drivers/agents-call.js +42 -3
- package/dist-server/server/drivers/agents-catalog.js +34 -4
- package/dist-server/server/drivers/boxagent.js +174 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +7 -2
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +95 -30
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +1385 -322
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/org-library.js +1051 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-roster.js +10 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +6 -3
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +259 -15
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +16 -5
- package/dist-server/server/thread-events.js +17 -1
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +12 -1
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +2 -2
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/vps-container-mcp.js +1129 -170
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// folds one SSE event stream; every provider process runs here.
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
|
-
import { existsSync, readFileSync, rmSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { existsSync, readFileSync, realpathSync, rmSync, mkdirSync } from "node:fs";
|
|
7
7
|
import { writeFileAtomic } from "./atomic.js";
|
|
8
8
|
import { rm as removeDirectory } from "node:fs/promises";
|
|
9
9
|
import { createServer } from "node:http";
|
|
@@ -26,6 +26,7 @@ import { approvalModeFor, supportsApprovalMode, modelSwitchNeedsAsk, isEmergency
|
|
|
26
26
|
import { escapeAttribute } from "../src/lib/composer-attachments.js";
|
|
27
27
|
import { CREDENTIAL_TARGETS, credentialResumeOutcome, credentialIsConfigured, isReusableCredentialRequest, isCredentialTargetId, } from "../shared/credential-request.js";
|
|
28
28
|
import { approvalHeldNote, approvalHeldReason, approvalModeForOrigin, autoVerdict, deliverFullAccessApproval, delegationInheritsFullAccess } from "./auto-approve.js";
|
|
29
|
+
import { CommandAllowlistStore, commandAllowlistCandidate } from "./command-allowlist.js";
|
|
29
30
|
import { updateClaudeCli } from "./claude-update.js";
|
|
30
31
|
import { configuredAccountDirectory, assertSeparateClaudeAccount, claudeAccountInfo, createClaudeAccountSchema, instanceSettingsSchema, newClaudeAccount } from "./claude-accounts.js";
|
|
31
32
|
import { providerIconPatchSchema, withInstanceIcon } from "./provider-icon.js";
|
|
@@ -36,7 +37,7 @@ import { commandReceipt } from "./commands.js";
|
|
|
36
37
|
import { buildTurnDigest, coverageForDriver, digestPromptLine, renderDigest, toolEvidence } from "./digest.js";
|
|
37
38
|
import { appendDecision, bindDecisionRetention, boundRetentionDays, decisionRetentionDays, decisionsCsv, flushDecisionLog, pruneDecisions, readDecisionRange, readDecisions, withDecisionActor } from "./decision-log.js";
|
|
38
39
|
import { validateBotCwd } from "./bot-cwd.js";
|
|
39
|
-
import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
40
|
+
import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveAudio, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
40
41
|
import { messageFileDisposition, messageFileDownloadName, messageAttachmentName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile, } from "./message-file.js";
|
|
41
42
|
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAvatarImage, avatarImageStatus, snapshotAvatarGenerationState, } from "./avatar-image.js";
|
|
42
43
|
import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
@@ -49,8 +50,9 @@ import { boxCreateRecoverySnapshot, retireDeletedBoxCreate } from "./box-create-
|
|
|
49
50
|
import { boxDeletionSnapshot } from "./box-delete-journal.js";
|
|
50
51
|
import { boxAccountResourceChangeError, cloudBackendChangeError, vpsAliasResourceChangeError, } from "./cloud-backend.js";
|
|
51
52
|
import * as composio from "./composio.js";
|
|
53
|
+
import { connectorCallFromFrame, connectorRefusalText, connectorUnrecognizedText, evaluateConnectorTools } from "./connector-verdict.js";
|
|
52
54
|
import { chiefOfStaffSystemPrompt } from "./chief-of-staff.js";
|
|
53
|
-
import { canAccessTeam, canReachPeer, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, resolveTeammate, roomPeerRosterSystemPrompt, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
|
|
55
|
+
import { canAccessTeam, canReachPeer, coordinatorSupervises, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, resolveTeammate, roomPeerRosterSystemPrompt, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
|
|
54
56
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
55
57
|
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerRuntimeStatus, localVmRecreatableOnDemand, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
|
|
56
58
|
import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, claudeUserMcpEnabled, skillAuthoringEnabled, sharedComputersEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, } from "./config.js";
|
|
@@ -59,6 +61,7 @@ import { ComputerControl } from "./computer-control.js";
|
|
|
59
61
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
60
62
|
import { registerEnginesBinDir } from "./engine-install.js";
|
|
61
63
|
import { appendUsage, parseUsageRange, readUsage, summarizeUsage, usageCsv, USAGE_GROUPINGS, flushUsageLedger } from "./usage-ledger.js";
|
|
64
|
+
import { GroupUsageReader } from "./group-thread-usage.js";
|
|
62
65
|
import { ledgerCost } from "./model-prices.js";
|
|
63
66
|
import { checkProviderKey, PROVIDER_KEY_KINDS } from "./provider-key-check.js";
|
|
64
67
|
import { assertWithinBudget, noteSpend, spendAlertText, spendState, takeSpendAlert } from "./spend.js";
|
|
@@ -92,30 +95,32 @@ import { ProviderRegistry } from "./harness/registry.js";
|
|
|
92
95
|
import { ManagedDesktopProviders } from "./managed-desktop.js";
|
|
93
96
|
import { computerKindForResource, ManagedDesktopPolicy } from "./managed-policy.js";
|
|
94
97
|
import { hostedModelPolicy, HOSTED_MODEL_POLICY_HEADER, HOSTED_PROVIDER_SETTINGS_ERROR } from "./hosted-models.js";
|
|
95
|
-
import { selectDefaultModelSelection } from "./default-model-selection.js";
|
|
96
|
-
import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
|
|
98
|
+
import { selectDefaultModelSelection, withNewBotEffort } from "./default-model-selection.js";
|
|
99
|
+
import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, peerApprovalFailure, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
|
|
97
100
|
import { peerProvenanceNote, withPeerProvenance } from "./peer-provenance.js";
|
|
98
101
|
import { decideRoomPost, emptyRoomPostBudget } from "./room-post-budget.js";
|
|
99
|
-
import { isProjectEmoji, mentionedBots, roomResponders, sectionKey, Store, titleFromLlm, toWireTask, } from "./store.js";
|
|
102
|
+
import { isProjectEmoji, mentionedBots, parseConnectorTools, roomResponders, sectionKey, Store, titleFromLlm, toWireTask, } from "./store.js";
|
|
100
103
|
import * as tts from "./tts/index.js";
|
|
101
104
|
import { narrateTool, toUtterances } from "./tts/speech-text.js";
|
|
102
|
-
import { buildRecoveryText, buildTurnContext, engineIsFresh, peerMessageText } from "./turn-context.js";
|
|
105
|
+
import { buildRecoveryText, buildTurnContext, engineIsFresh, NATIVELY_REPLAYING_DRIVER_KINDS, peerMessageText } from "./turn-context.js";
|
|
103
106
|
import { Handoffs, handedStateUsable, recordHanded, renderUnseen, sessionStart, unseenMessages, withUnseenMessages } from "./delta-context.js";
|
|
104
107
|
import { extractTurnImages } from "./turn-images.js";
|
|
105
108
|
import { TurnWatchdog } from "./turn-watchdog.js";
|
|
106
109
|
import { TurnResources, workspaceResource } from "./turn-resources.js";
|
|
107
110
|
import { ensureWorkspace, ensureTaskWorkspace, workspaceLocationsPrompt, supportsWorkspaceFiles, updateMemory, appendMemoryLog, isMemoryTopicName, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, workspaceDir, } from "./workspace.js";
|
|
108
|
-
import { readMemoryTopic } from "./workspace.js";
|
|
111
|
+
import { listMemoryTopics, readMemoryFile, readMemoryTopic, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
|
|
109
112
|
import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openMemoryLocation, readMemoryDoc, } from "./memory-store.js";
|
|
110
113
|
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "./memory-journal.js";
|
|
111
114
|
import { readSectionContext, readSections, sectionContextKey, sectionContextLabel, sectionContextSystemPrompt, writeSectionContext, SECTION_CONTEXT_MAX_BYTES, } from "./section-context.js";
|
|
112
|
-
import { applyStagedSkillWrite, applySkillWriteWithReceipt, getStagedSkillWrite, installSkill, listSkills, listStagedSkillWrites, isSkillName, readSkillFile, rejectStagedSkillWrite, removeSkill, setSkillEnabled, skillsSystemPrompt, stageSkillWrite, } from "./skills.js";
|
|
115
|
+
import { applyStagedSkillWrite, applySkillWriteWithReceipt, getStagedSkillWrite, installOrgSkill, installSkill, parseSkillMd, scanSkillText, listSkills, listStagedSkillWrites, isSkillName, readSkillFile, rejectStagedSkillWrite, removeSkill, setSkillEnabled, skillPackageStamps, skillsSystemPrompt, stageSkillWrite, } from "./skills.js";
|
|
113
116
|
import { fetchSkillFromSource } from "./skill-fetch.js";
|
|
117
|
+
import { pickBotName } from "./names.js";
|
|
118
|
+
import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefaults } from "./new-bot-defaults.js";
|
|
114
119
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
115
120
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
116
121
|
import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-folder.js";
|
|
117
|
-
import { buildSystemPrompt, computerPrompt, COMPOSIO_PROMPT, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, } from "./system-prompt.js";
|
|
118
|
-
import { readCuaConnection, gatedLocalComputer } from "./local-computer.js";
|
|
122
|
+
import { buildSystemPrompt, userProfileSystemPrompt, computerPrompt, COMPOSIO_PROMPT, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, } from "./system-prompt.js";
|
|
123
|
+
import { readCuaConnection, readCuaUnavailableReason, gatedLocalComputer } from "./local-computer.js";
|
|
119
124
|
import { discoverExistingPerBotLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
120
125
|
import { LocalVmIdleTimer } from "./local-vm-idle.js";
|
|
121
126
|
import { LocalVmLeasePool } from "./local-vm-lease.js";
|
|
@@ -139,9 +144,8 @@ import { flushAllProfileHistory, flushProfileHistory, readHistory, recordProfile
|
|
|
139
144
|
import { fetchBotDirectory, matchDirectoryBots } from "./bot-directory.js";
|
|
140
145
|
import { scoutProject, suggestTeam } from "./project-scout.js";
|
|
141
146
|
import { fetchGithubTeam, fetchLibraryTeam, fetchTeamCatalog } from "./team-library.js";
|
|
142
|
-
import { BOT_PACKAGE_MAX_SKILLS, isBotPackage,
|
|
143
|
-
import { createTeamManifest,
|
|
144
|
-
import { takeImportName } from "../shared/import-name.js";
|
|
147
|
+
import { BOT_PACKAGE_MAX_SKILLS, isBotPackage, packageSummary, parsePackageDocument, renderBotPackageMarkdown } from "./bot-package.js";
|
|
148
|
+
import { createTeamManifest, parseTeamManifest } from "./team-manifest.js";
|
|
145
149
|
import { readThreadEvents } from "./thread-events.js";
|
|
146
150
|
import { bindThreadLogCapProvider } from "./thread-log-rotation.js";
|
|
147
151
|
import { listenWebhookIngress, webhookCredential } from "./webhook-ingress.js";
|
|
@@ -150,13 +154,17 @@ import { WebhookManager } from "./webhooks.js";
|
|
|
150
154
|
import { SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
151
155
|
import { loadBundledSkills, loadUserSkills, mergeSkills, renderSkillInstructions, selectBundledSkills } from "./skill-library.js";
|
|
152
156
|
import { installedPlaybookInstructions } from "./installed-playbooks.js";
|
|
153
|
-
import { createBotPackageExport } from "./package-export.js";
|
|
157
|
+
import { createBotPackageExport, createLibraryPackageExport, createTeamPackageExport, picture as sharedPicture, TeamExportError } from "./package-export.js";
|
|
158
|
+
import { importPackageDocument, importTeamManifest, PackageImportError } from "./package-import.js";
|
|
159
|
+
import { applyPresetToBot, createPresetStore, DEFAULTS_PRESET_KEY, PRESET_UNAVAILABLE_MESSAGE, presetFromDefaults, presetOffered, readOrgInstallStatuses, readPublishedLibrary, writePublishedLibrary, } from "./presets.js";
|
|
160
|
+
import { readPublishedTeam, writePublishedTeam } from "./published-teams.js";
|
|
161
|
+
import { OrgLibrary } from "./org-library.js";
|
|
154
162
|
import { createTeamBackup, importTeamBackup } from "./team-backup.js";
|
|
155
163
|
import { MAX_TEAM_BACKUP_BYTES } from "../shared/team-backup.js";
|
|
156
164
|
import { shouldMountLocalComputer } from "./local-routing.js";
|
|
157
165
|
import { autoLocalVmAttachable } from "./container-computer.js";
|
|
158
166
|
import { startAutoVmClaim } from "./auto-vm-claims.js";
|
|
159
|
-
import {
|
|
167
|
+
import { computerFreeAfterText, computerStillBusyText, computerStoppedWaitingText, computerWaitingText } from "./computer-wait.js";
|
|
160
168
|
import { modelContextWindow } from "./model-context-window.js";
|
|
161
169
|
import { parseSurface, resolveSurface, surfaceLabel, surfaceOfComputerKind, surfacePrompt } from "./surface.js";
|
|
162
170
|
import { PendingTurnCancellations, ProviderTurnGenerationRegistry, RetiredTurnRegistry, guardTurnDispatch, isTurnAdmissionBlocked, isTurnEventQuarantined, } from "./turn-dispatch-guard.js";
|
|
@@ -183,6 +191,7 @@ import { PHONE_SECRET_PROTOCOL_VERSION, PhoneSecretBridge, PhoneSecretError, Pho
|
|
|
183
191
|
import { json, onJsonBody, parsedBodyOf, readBody } from "./harness/http.js";
|
|
184
192
|
import { ROUTES, dispatchRoutes } from "./routes/table.js";
|
|
185
193
|
import { createHostedSlackRoutes } from "./routes/hosted-slack.js";
|
|
194
|
+
import { createBotPresetRoutes } from "./routes/bot-presets.js";
|
|
186
195
|
const PORT = Number(process.env.OMB_PORT || process.env.OGB_PORT || 8799);
|
|
187
196
|
const WEBHOOK_PORT = Number(process.env.OMB_WEBHOOK_PORT || PORT + 1);
|
|
188
197
|
// Behind a proxy or tunnel, the base URL senders should use (docs/self-hosting.md).
|
|
@@ -248,6 +257,7 @@ const sessions = new SessionRegistry({
|
|
|
248
257
|
const sharedComputers = new SharedComputers(id => sessions.isLive(id));
|
|
249
258
|
// Who each thread is for, when a signed-in person can be named (server-private).
|
|
250
259
|
const threadStarters = new ThreadStarters(join(DATA_DIR, "thread-starters.json"));
|
|
260
|
+
const commandAllowlist = new CommandAllowlistStore(join(DATA_DIR, "command-allowlist.json"));
|
|
251
261
|
const SESSION_COOKIE = sessionCookieName(PORT, ENVIRONMENT_ID);
|
|
252
262
|
const HOSTED_WORKSPACE = hostedWorkspaceConfigured();
|
|
253
263
|
let workspaceAccess = null;
|
|
@@ -416,6 +426,28 @@ function cardAnswerRefusal(auth, threadId, requestId, behavior) {
|
|
|
416
426
|
return null;
|
|
417
427
|
return "Only the person who started this conversation or sent this request, or a workspace admin, can answer this card.";
|
|
418
428
|
}
|
|
429
|
+
function canManageCommandAllowlist(auth) {
|
|
430
|
+
return auth.kind === "loopback" ? auth.trust !== "service" : auth.scopes.includes("admin");
|
|
431
|
+
}
|
|
432
|
+
const COMMAND_ALLOWLIST_DRIVERS = new Set([
|
|
433
|
+
"claudeAgent", "codex", "antigravityAgent", "grokAgent", "opencodeGo", "kimiAgent",
|
|
434
|
+
"droidAgent", "cursorAgent", "qwenAgent", "geminiAgent", "hermesAgent", "customAcp",
|
|
435
|
+
]);
|
|
436
|
+
function commandAllowlistResponse(bot) {
|
|
437
|
+
const instance = registry.get(bot.modelSelection.instanceId);
|
|
438
|
+
const task = store.taskByThread(bot.id, bot.threadId);
|
|
439
|
+
let cwd = task?.cwd !== undefined ? task.cwd : bot.cwd ?? join(realpathSync(DATA_DIR), "task-workspaces", bot.id, bot.threadId);
|
|
440
|
+
if (cwd && existsSync(cwd))
|
|
441
|
+
cwd = realpathSync(cwd);
|
|
442
|
+
return {
|
|
443
|
+
rules: commandAllowlist.list(bot.id),
|
|
444
|
+
supported: COMMAND_ALLOWLIST_DRIVERS.has(instance?.driverKind ?? ""),
|
|
445
|
+
context: {
|
|
446
|
+
providerInstanceId: bot.modelSelection.instanceId,
|
|
447
|
+
cwd,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
419
451
|
function decisionActorFor(auth) {
|
|
420
452
|
if (auth.kind === "loopback")
|
|
421
453
|
return auth.trust === "service" ? { kind: "worker" } : { kind: "loopback" };
|
|
@@ -655,7 +687,7 @@ function adminAuditPlan(method, path) {
|
|
|
655
687
|
return null;
|
|
656
688
|
const plan = {
|
|
657
689
|
config: requiredScope(method, path, { sharedComputers: sharedComputersEnabled(cfg) }) === "admin",
|
|
658
|
-
createsBots: method === "POST" && (path === "/api/bots" || path === "/api/teams/import"),
|
|
690
|
+
createsBots: method === "POST" && (path === "/api/bots" || path === "/api/teams/import" || path === "/api/org-library/add"),
|
|
659
691
|
createsWebhook: method === "POST" && path === "/api/webhooks",
|
|
660
692
|
pairing: method === "POST" && path === "/api/auth/pairing",
|
|
661
693
|
};
|
|
@@ -994,6 +1026,24 @@ utilityParentPort?.on("message", event => {
|
|
|
994
1026
|
}
|
|
995
1027
|
utilityParentPort.postMessage({ type: "openmausbot:managed-desktop-result", requestId, ok });
|
|
996
1028
|
});
|
|
1029
|
+
// The organization library (server/org-library.ts): Electron relays the
|
|
1030
|
+
// verified catalog; the runtime swaps it and acknowledges at once, then
|
|
1031
|
+
// reconciles and reports on its own. A relay that arrives before the library
|
|
1032
|
+
// exists waits for it (the acknowledgement does not).
|
|
1033
|
+
let orgLibrary = null;
|
|
1034
|
+
let pendingOrgLibraryRelay = null;
|
|
1035
|
+
utilityParentPort?.on("message", event => {
|
|
1036
|
+
const message = event.data;
|
|
1037
|
+
if (message?.type !== "openmausbot:managed-library")
|
|
1038
|
+
return;
|
|
1039
|
+
const requestId = typeof message.requestId === "string" && message.requestId.length <= 100 ? message.requestId : undefined;
|
|
1040
|
+
let ok = true;
|
|
1041
|
+
if (orgLibrary)
|
|
1042
|
+
ok = orgLibrary.applyRelay(message.library ?? null).ok;
|
|
1043
|
+
else
|
|
1044
|
+
pendingOrgLibraryRelay = { library: message.library ?? null };
|
|
1045
|
+
utilityParentPort.postMessage({ type: "openmausbot:managed-desktop-result", requestId, ok });
|
|
1046
|
+
});
|
|
997
1047
|
// Only Electron owns this port. There is deliberately no HTTP equivalent or
|
|
998
1048
|
// config patch for its organization identity, endpoint, or model capability.
|
|
999
1049
|
utilityParentPort?.on("message", event => {
|
|
@@ -1215,6 +1265,9 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation, r
|
|
|
1215
1265
|
// The shared-computer tools are advertised only while the workspace
|
|
1216
1266
|
// gate is on; the routes behind them refuse regardless.
|
|
1217
1267
|
OMB_SHARED_COMPUTERS_ENABLED: sharedComputersEnabled(cfg) ? "1" : "0",
|
|
1268
|
+
// Same capability rule for voice: the tool is offered only when this
|
|
1269
|
+
// bot can actually speak, and the route re-checks on every call.
|
|
1270
|
+
OMB_VOICE_NOTES: tts.voiceReady(cfg, (botForThread(botId, threadId) ?? store.bot(botId))?.voice) ? "1" : "0",
|
|
1218
1271
|
},
|
|
1219
1272
|
};
|
|
1220
1273
|
}
|
|
@@ -1236,6 +1289,8 @@ function hooksIntegration(botId, threadId, generation) {
|
|
|
1236
1289
|
}
|
|
1237
1290
|
class DirectTurnSetupCancelled extends Error {
|
|
1238
1291
|
}
|
|
1292
|
+
class ComputerWaitGaveUp extends Error {
|
|
1293
|
+
}
|
|
1239
1294
|
const directTurnDispatchClaims = new Map();
|
|
1240
1295
|
const directTurnGenerationByThread = new Map();
|
|
1241
1296
|
// Only the latest direct request per thread is retained. A fresh user turn
|
|
@@ -1269,6 +1324,10 @@ function settleDirectFollowup(generation) {
|
|
|
1269
1324
|
// Keep the exact provider/profile settings that own a running conversation.
|
|
1270
1325
|
// Selecting another thread or changing a default must not retarget its tools.
|
|
1271
1326
|
const directTurnBots = new Map();
|
|
1327
|
+
/** Prompt bytes for the turn in flight, by thread, until the settled turn
|
|
1328
|
+
* books them next to its token usage: stable bytes ride the cacheable
|
|
1329
|
+
* prefix, volatile bytes are the part that legitimately changes. */
|
|
1330
|
+
const turnPromptBytes = new Map();
|
|
1272
1331
|
let providerFleetReloading = false;
|
|
1273
1332
|
const turnResources = new TurnResources();
|
|
1274
1333
|
const sharedComputerControl = new SharedComputerControl(turnResources, () => store.bots.some(bot => botComputerControlSnapshot(bot.id).held));
|
|
@@ -1312,7 +1371,49 @@ async function bindTurnComputer(owner, resource, exclusive = false) {
|
|
|
1312
1371
|
// Who holds the desktop, as the chip and the give-up error name them: a
|
|
1313
1372
|
// bot running a titled thread, or a room. Read once, when the wait begins.
|
|
1314
1373
|
let holder;
|
|
1374
|
+
let holderThreadRef;
|
|
1315
1375
|
const deadline = Date.now() + GROUP_GOAL_WAIT_MAX_MS;
|
|
1376
|
+
// The wait is history the transcript keeps (#1647): the waiting chip stays
|
|
1377
|
+
// exactly as written, a resolution line is appended beside it, and the
|
|
1378
|
+
// turn.wait_* events carry the same facts for the inspector log.
|
|
1379
|
+
let waitedSince;
|
|
1380
|
+
let waitEnded = false;
|
|
1381
|
+
const waitEventBase = () => {
|
|
1382
|
+
const selection = store.botByThread(owner.threadId)?.modelSelection;
|
|
1383
|
+
const instance = selection ? registry.get(selection.instanceId) : null;
|
|
1384
|
+
return {
|
|
1385
|
+
eventId: newId(),
|
|
1386
|
+
provider: instance?.driverKind ?? "harness",
|
|
1387
|
+
...(instance && selection ? { providerInstanceId: selection.instanceId } : {}),
|
|
1388
|
+
threadId: owner.threadId,
|
|
1389
|
+
createdAt: new Date().toISOString(),
|
|
1390
|
+
};
|
|
1391
|
+
};
|
|
1392
|
+
const endWait = (outcome) => {
|
|
1393
|
+
if (!waitingMessage || waitEnded)
|
|
1394
|
+
return;
|
|
1395
|
+
waitEnded = true;
|
|
1396
|
+
const waitedMs = waitedSince === undefined ? 0 : Date.now() - waitedSince;
|
|
1397
|
+
const text = outcome === "acquired"
|
|
1398
|
+
? computerFreeAfterText(holder, waitedMs)
|
|
1399
|
+
: outcome === "gave_up"
|
|
1400
|
+
? computerStillBusyText(holder, waitedMs)
|
|
1401
|
+
: computerStoppedWaitingText(holder, waitedMs);
|
|
1402
|
+
store.appendMessage(owner.threadId, {
|
|
1403
|
+
role: "bot", kind: "activity",
|
|
1404
|
+
...(outcome === "gave_up" ? { turnSucceeded: false } : {}),
|
|
1405
|
+
tool: { name: text, ok: outcome !== "gave_up" },
|
|
1406
|
+
...(holderThreadRef ? { threadRef: holderThreadRef } : {}),
|
|
1407
|
+
});
|
|
1408
|
+
bus.publish({
|
|
1409
|
+
...waitEventBase(),
|
|
1410
|
+
type: "turn.wait_ended",
|
|
1411
|
+
resource,
|
|
1412
|
+
...(holder ? { holder } : {}),
|
|
1413
|
+
waitedMs,
|
|
1414
|
+
outcome,
|
|
1415
|
+
});
|
|
1416
|
+
};
|
|
1316
1417
|
try {
|
|
1317
1418
|
while (true) {
|
|
1318
1419
|
if (!active())
|
|
@@ -1327,22 +1428,33 @@ async function bindTurnComputer(owner, resource, exclusive = false) {
|
|
|
1327
1428
|
holder = holderBot
|
|
1328
1429
|
? { name: holderBot.name, ...(holderTask?.title ? { task: holderTask.title } : {}) }
|
|
1329
1430
|
: holderRoom ? { name: holderRoom.name } : undefined;
|
|
1431
|
+
holderThreadRef = holderBot && holderTask?.title
|
|
1432
|
+
? { botId: holderBot.id, threadId: holderTask.threadId, title: holderTask.title }
|
|
1433
|
+
: undefined;
|
|
1330
1434
|
waitingMessage = store.appendMessage(owner.threadId, {
|
|
1331
1435
|
role: "bot", kind: "activity",
|
|
1332
1436
|
tool: { name: computerWaitingText(holder) },
|
|
1333
|
-
...(
|
|
1437
|
+
...(holderThreadRef ? { threadRef: holderThreadRef } : {}),
|
|
1334
1438
|
});
|
|
1439
|
+
waitedSince = Date.now();
|
|
1440
|
+
bus.publish({
|
|
1441
|
+
...waitEventBase(),
|
|
1442
|
+
type: "turn.wait_started",
|
|
1443
|
+
resource,
|
|
1444
|
+
...(holder ? { holder } : {}),
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
if (Date.now() >= deadline) {
|
|
1448
|
+
endWait("gave_up");
|
|
1449
|
+
throw new ComputerWaitGaveUp(computerStillBusyText(holder, GROUP_GOAL_WAIT_MAX_MS));
|
|
1335
1450
|
}
|
|
1336
|
-
if (Date.now() >= deadline)
|
|
1337
|
-
throw new Error(computerStillBusyText(holder, GROUP_GOAL_WAIT_MAX_MS));
|
|
1338
1451
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1339
1452
|
}
|
|
1340
1453
|
}
|
|
1341
1454
|
finally {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
});
|
|
1455
|
+
// Never patch the waiting chip over: the queue position this turn held
|
|
1456
|
+
// stays visible beside what the wait came to.
|
|
1457
|
+
endWait(active() && turnResources.owns(resource, owner) ? "acquired" : "stopped");
|
|
1346
1458
|
}
|
|
1347
1459
|
turnResourceOwners.set(owner.threadId, owner);
|
|
1348
1460
|
turnComputerResources.set(owner.threadId, { owner, resource });
|
|
@@ -1457,15 +1569,15 @@ async function interruptAllDirectThreads(botId) {
|
|
|
1457
1569
|
}
|
|
1458
1570
|
const retiredProviderTurns = new RetiredTurnRegistry();
|
|
1459
1571
|
const pendingCancelledProviderHandshakes = new PendingTurnCancellations();
|
|
1460
|
-
const
|
|
1461
|
-
function
|
|
1572
|
+
const turnAttachmentsByTurn = new Map();
|
|
1573
|
+
function turnAttachmentKey(threadId, turnId) {
|
|
1462
1574
|
return `${threadId}:${turnId ?? "active"}`;
|
|
1463
1575
|
}
|
|
1464
|
-
function
|
|
1465
|
-
for (const [key, attachments] of
|
|
1576
|
+
function purgeTurnAttachmentsForThread(threadId) {
|
|
1577
|
+
for (const [key, attachments] of turnAttachmentsByTurn) {
|
|
1466
1578
|
if (!key.startsWith(`${threadId}:`))
|
|
1467
1579
|
continue;
|
|
1468
|
-
|
|
1580
|
+
turnAttachmentsByTurn.delete(key);
|
|
1469
1581
|
for (const attachment of attachments) {
|
|
1470
1582
|
deleteAttachment(attachment.path);
|
|
1471
1583
|
}
|
|
@@ -1479,13 +1591,13 @@ function clearCancelledProviderHandshake(threadId, ownerId) {
|
|
|
1479
1591
|
}
|
|
1480
1592
|
function retireProviderTurn(turnId) {
|
|
1481
1593
|
retiredProviderTurns.retire(turnId);
|
|
1482
|
-
// A stopped/replaced turn is never folded again. Delete only
|
|
1483
|
-
// that were
|
|
1484
|
-
// not accumulate invisibly on disk.
|
|
1485
|
-
for (const [key, attachments] of
|
|
1594
|
+
// A stopped/replaced turn is never folded again. Delete only staged files
|
|
1595
|
+
// (images, voice notes) that were parked for that exact provider turn so
|
|
1596
|
+
// unattached output does not accumulate invisibly on disk.
|
|
1597
|
+
for (const [key, attachments] of turnAttachmentsByTurn) {
|
|
1486
1598
|
if (!key.endsWith(`:${turnId}`))
|
|
1487
1599
|
continue;
|
|
1488
|
-
|
|
1600
|
+
turnAttachmentsByTurn.delete(key);
|
|
1489
1601
|
for (const attachment of attachments) {
|
|
1490
1602
|
deleteAttachment(attachment.path);
|
|
1491
1603
|
}
|
|
@@ -1636,8 +1748,26 @@ export function browserEngineSummary() {
|
|
|
1636
1748
|
? { kind: "engine", version: status.version, ...progress }
|
|
1637
1749
|
: { kind: "unavailable", reason: status.reason, installable: status.installable, ...progress };
|
|
1638
1750
|
}
|
|
1639
|
-
function phoneIntegration() {
|
|
1640
|
-
|
|
1751
|
+
function phoneIntegration(botId, threadId, generation) {
|
|
1752
|
+
// The phone is claimed lazily, at the first tools/call the engine makes
|
|
1753
|
+
// through this proxy (/api/internal/phone/claim): trigger-term matching
|
|
1754
|
+
// only decides whether the tools are mounted, so a conversation that
|
|
1755
|
+
// merely mentions "android" holds nothing while it runs.
|
|
1756
|
+
const token = mintInternalCapability({
|
|
1757
|
+
botId,
|
|
1758
|
+
threadId,
|
|
1759
|
+
generation,
|
|
1760
|
+
depth: 0,
|
|
1761
|
+
kind: "phone",
|
|
1762
|
+
skillAuthoring: false,
|
|
1763
|
+
createdBots: 0,
|
|
1764
|
+
openedThreads: 0,
|
|
1765
|
+
});
|
|
1766
|
+
const env = {
|
|
1767
|
+
...AGENTS_NODE_FLAG,
|
|
1768
|
+
OMB_PHONE_TOKEN: token,
|
|
1769
|
+
OMB_HARNESS_URL: `http://127.0.0.1:${PORT}`,
|
|
1770
|
+
};
|
|
1641
1771
|
if (process.env.OMB_ADB_PATH)
|
|
1642
1772
|
env.OMB_ADB_PATH = process.env.OMB_ADB_PATH;
|
|
1643
1773
|
if (process.env.OMB_RESOURCES_PATH)
|
|
@@ -1933,6 +2063,113 @@ function collectExportSkills(bots, names) {
|
|
|
1933
2063
|
}
|
|
1934
2064
|
return byBot;
|
|
1935
2065
|
}
|
|
2066
|
+
/** Skills on one team's bots for a whole-team share. "all" skips a skill
|
|
2067
|
+
* whose stored SKILL.md changed (listed to the person); an explicit list
|
|
2068
|
+
* refuses instead, because the person asked for that exact skill. */
|
|
2069
|
+
/** The team's skills for a whole-team export. `available` (every skill name
|
|
2070
|
+
* on the team's bots) comes back on refusals too: the Share dialog draws its
|
|
2071
|
+
* skill choices from it, so a refused choice can always be changed. */
|
|
2072
|
+
function collectTeamSkills(bots, selection) {
|
|
2073
|
+
const all = selection === undefined || selection === "all";
|
|
2074
|
+
const available = [...new Set(bots.flatMap((bot) => listSkills(bot.id).map((skill) => skill.name)))].sort();
|
|
2075
|
+
if (!all && (!Array.isArray(selection) || selection.some((name) => typeof name !== "string" || !isSkillName(name)))) {
|
|
2076
|
+
return { ok: false, error: "skills must be \"all\" or a list of skill names", available };
|
|
2077
|
+
}
|
|
2078
|
+
const chosen = new Set(all ? available : selection);
|
|
2079
|
+
const unknown = [...chosen].find((name) => !available.includes(name));
|
|
2080
|
+
if (unknown)
|
|
2081
|
+
return { ok: false, error: `This team has no skill named "${unknown}"`, available };
|
|
2082
|
+
const skillsByBot = new Map();
|
|
2083
|
+
const skipped = [];
|
|
2084
|
+
for (const bot of bots) {
|
|
2085
|
+
const assigned = [];
|
|
2086
|
+
for (const listing of listSkills(bot.id)) {
|
|
2087
|
+
if (!chosen.has(listing.name))
|
|
2088
|
+
continue;
|
|
2089
|
+
const instructions = readSkillFile(bot.id, listing.name);
|
|
2090
|
+
if (instructions === null) {
|
|
2091
|
+
if (!all)
|
|
2092
|
+
return { ok: false, error: `Skill "${listing.name}" changed or is unavailable and cannot be shared safely`, available };
|
|
2093
|
+
const part = `skills[${listing.name}]`;
|
|
2094
|
+
if (!skipped.some((skip) => skip.part === part))
|
|
2095
|
+
skipped.push({ part, reason: "skill_changed" });
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
assigned.push({
|
|
2099
|
+
name: listing.name,
|
|
2100
|
+
description: listing.description,
|
|
2101
|
+
...(listing.source ? { source: listing.source } : {}),
|
|
2102
|
+
...(listing.license ? { license: listing.license } : {}),
|
|
2103
|
+
...(listing.compatibility ? { compatibility: listing.compatibility } : {}),
|
|
2104
|
+
instructions,
|
|
2105
|
+
enabled: listing.enabled,
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
if (assigned.length)
|
|
2109
|
+
skillsByBot.set(bot.id, assigned);
|
|
2110
|
+
}
|
|
2111
|
+
return { ok: true, all, skillsByBot, skipped, available };
|
|
2112
|
+
}
|
|
2113
|
+
/** A bot's starter notes: MEMORY.md and its topic files. Daily logs never
|
|
2114
|
+
* travel (they are closer to chat history than to notes). */
|
|
2115
|
+
function starterNotes(botId) {
|
|
2116
|
+
const notes = [];
|
|
2117
|
+
const index = readMemoryFile(botId).text;
|
|
2118
|
+
if (index.trim())
|
|
2119
|
+
notes.push({ path: "MEMORY.md", text: index });
|
|
2120
|
+
for (const topic of listMemoryTopics(botId).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2121
|
+
const text = readMemoryTopic(botId, topic.name);
|
|
2122
|
+
if (text?.trim())
|
|
2123
|
+
notes.push({ path: `memory/${topic.name}`, text });
|
|
2124
|
+
}
|
|
2125
|
+
return notes;
|
|
2126
|
+
}
|
|
2127
|
+
/** The importer's view of this installation. Pictures become ordinary local
|
|
2128
|
+
* avatars, connection slots ordinary (switched-off) MCP servers, starter
|
|
2129
|
+
* notes ordinary memory writes. */
|
|
2130
|
+
function packageImportDeps(selection) {
|
|
2131
|
+
return {
|
|
2132
|
+
store,
|
|
2133
|
+
routines: routines,
|
|
2134
|
+
skills: { install: installSkill, setEnabled: setSkillEnabled, installOrg: installOrgSkill },
|
|
2135
|
+
memory: { writeIndex: writeMemoryFile, writeTopic: writeMemoryTopic },
|
|
2136
|
+
mcp: { servers: () => cfg.mcpServers ?? {}, refusal: mcpPolicyRefusal, persist: persistMcpServers },
|
|
2137
|
+
sections: { writeBrief: (section, text) => void writeSectionContext(section, text) },
|
|
2138
|
+
images: {
|
|
2139
|
+
save: (bytes, mime) => {
|
|
2140
|
+
const avatarUrl = botAvatarUrlFromStoredPath(saveImage(Buffer.from(bytes), mime).path);
|
|
2141
|
+
if (!avatarUrl)
|
|
2142
|
+
throw new Error("A picture in this package could not be stored");
|
|
2143
|
+
return avatarUrl;
|
|
2144
|
+
},
|
|
2145
|
+
},
|
|
2146
|
+
broadcast: (event) => event.kind === "bot"
|
|
2147
|
+
? broadcast({ kind: "bot", bot: publicBot(event.bot) })
|
|
2148
|
+
: broadcast({ kind: "group", group: publicGroupState(event.group) }),
|
|
2149
|
+
defaultSelection: () => selection,
|
|
2150
|
+
presets: presetStore,
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
/** "Include my New bot defaults as a preset" from an export body: the saved
|
|
2154
|
+
* defaults through the allowlist, with the picture the dialog prepared. */
|
|
2155
|
+
function defaultsPresetFor(body, includeNotes) {
|
|
2156
|
+
const skipped = [];
|
|
2157
|
+
let avatar = null;
|
|
2158
|
+
if (body.presetAvatar !== undefined && body.presetAvatar !== null && body.presetAvatar !== "") {
|
|
2159
|
+
const offered = sharedPicture(body.presetAvatar);
|
|
2160
|
+
if (typeof offered === "string")
|
|
2161
|
+
skipped.push({ part: `presets[${DEFAULTS_PRESET_KEY}].bot.appearance.avatar`, reason: offered });
|
|
2162
|
+
else
|
|
2163
|
+
avatar = offered;
|
|
2164
|
+
}
|
|
2165
|
+
const built = presetFromDefaults(cfg.newBotDefaults, {
|
|
2166
|
+
name: typeof body.presetName === "string" ? body.presetName : undefined,
|
|
2167
|
+
description: typeof body.presetDescription === "string" ? body.presetDescription : undefined,
|
|
2168
|
+
includeNotes,
|
|
2169
|
+
avatar,
|
|
2170
|
+
});
|
|
2171
|
+
return { preset: built.value, skipped: [...skipped, ...built.skipped] };
|
|
2172
|
+
}
|
|
1936
2173
|
function checkedGroupResponder(value, memberIds) {
|
|
1937
2174
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1938
2175
|
return null;
|
|
@@ -1970,7 +2207,9 @@ function checkedMemberIds(value) {
|
|
|
1970
2207
|
return { ok: true, memberIds };
|
|
1971
2208
|
}
|
|
1972
2209
|
let bootSelection = { instanceId: "", model: "" };
|
|
1973
|
-
|
|
2210
|
+
/** Preset bots for New bot, from shared files and the organization (presets.ts). */
|
|
2211
|
+
const presetStore = createPresetStore();
|
|
2212
|
+
const store = new Store(() => bootSelection, (selection) => withNewBotEffort(selection, cfg.newBots?.effort, registry.get(selection.instanceId)?.adapter.capabilities.effortLevels));
|
|
1974
2213
|
const teamComputers = new TeamComputers(join(DATA_DIR, "team-computers.json"), ENVIRONMENT_ID);
|
|
1975
2214
|
let followupsReady = false;
|
|
1976
2215
|
const sendSequencer = new SendSequencer();
|
|
@@ -2008,9 +2247,17 @@ if (browserCleanupReferencesReconciled)
|
|
|
2008
2247
|
* than the desktop window did. Stripped here rather than at each call site
|
|
2009
2248
|
* so a new broadcast cannot forget. */
|
|
2010
2249
|
let activeCoordinationForThread = (_threadId) => false;
|
|
2011
|
-
const wireTask = (task) =>
|
|
2250
|
+
const wireTask = (task) => {
|
|
2251
|
+
// Time-based snoozes heal on read against the server clock — no client
|
|
2252
|
+
// timer, no device skew. The 0 sentinel ("until new activity") is not a
|
|
2253
|
+
// time and survives reads; only a wake event in the store clears it.
|
|
2254
|
+
const { snoozedUntil, ...base } = toWireTask(task);
|
|
2255
|
+
const coordinated = { ...base, waitingForTeammates: activeCoordinationForThread(task.threadId) && !task.busy };
|
|
2256
|
+
const asleep = snoozedUntil === 0 || (snoozedUntil !== undefined && snoozedUntil > Date.now());
|
|
2257
|
+
return asleep ? { ...coordinated, snoozedUntil } : coordinated;
|
|
2258
|
+
};
|
|
2012
2259
|
const wireBot = (bot) => {
|
|
2013
|
-
const { resumeCursors: _resumeCursors, tasks, approvalGrant, lastProfileRequestId: _lastProfileRequestId, lastTeamSetupReceipt: _lastTeamSetupReceipt, ...rest } = bot;
|
|
2260
|
+
const { resumeCursors: _resumeCursors, tasks, approvalGrant, lastProfileRequestId: _lastProfileRequestId, lastTeamSetupReceipt: _lastTeamSetupReceipt, packageBase: _packageBase, ...rest } = bot;
|
|
2014
2261
|
// An elevated selection is inert until the desktop confirms its exact
|
|
2015
2262
|
// private reply. Every ordinary client sees the effective Ask state during
|
|
2016
2263
|
// that two-phase window, never a grant that may still roll back.
|
|
@@ -2023,7 +2270,7 @@ const wireBot = (bot) => {
|
|
|
2023
2270
|
/** The correlated private response carries the requested value so Electron
|
|
2024
2271
|
* can validate it before sending the confirmation that makes it effective. */
|
|
2025
2272
|
const wireTrustedApprovalBot = (bot) => {
|
|
2026
|
-
const { resumeCursors: _resumeCursors, tasks, approvalGrant: _approvalGrant, lastProfileRequestId: _lastProfileRequestId, lastTeamSetupReceipt: _lastTeamSetupReceipt, ...rest } = bot;
|
|
2273
|
+
const { resumeCursors: _resumeCursors, tasks, approvalGrant: _approvalGrant, lastProfileRequestId: _lastProfileRequestId, lastTeamSetupReceipt: _lastTeamSetupReceipt, packageBase: _packageBase, ...rest } = bot;
|
|
2027
2274
|
return { ...rest, approvalMode: approvalModeFor(rest), avatarUrl: rest.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map(wireTask) } : {}) };
|
|
2028
2275
|
};
|
|
2029
2276
|
/** A settings-based preview, not a receipt of a dispatched turn. No
|
|
@@ -2046,7 +2293,7 @@ function previewSystemPrompt(bot) {
|
|
|
2046
2293
|
const computerPromptKind = previewComputer === "vm"
|
|
2047
2294
|
? caps?.computerMcp ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : null
|
|
2048
2295
|
: previewComputer === "cloud"
|
|
2049
|
-
? instance?.driverKind === "boxAgent" ? "box-agent" : caps?.computerMcp ? bot.cloudBackend === "vps" ? "vps" : "box" : null
|
|
2296
|
+
? instance?.driverKind === "boxAgent" ? "box-agent" : caps?.computerMcp ? bot.cloudBackend === "vps" ? "vps" : caps.cloudComputerMcp ? "box-chat" : "box" : null
|
|
2050
2297
|
: previewComputer === "local"
|
|
2051
2298
|
? caps?.localComputerMcp ? "local" : null
|
|
2052
2299
|
: null;
|
|
@@ -2070,6 +2317,7 @@ function previewSystemPrompt(bot) {
|
|
|
2070
2317
|
});
|
|
2071
2318
|
const privateWorkspace = instance && supportsWorkspaceFiles(instance.driverKind);
|
|
2072
2319
|
const built = buildSystemPrompt(persona, bot.soul ?? "", [
|
|
2320
|
+
{ id: "user-profile", label: "About the user", text: userProfileSystemPrompt(cfg.profile) },
|
|
2073
2321
|
{
|
|
2074
2322
|
id: "setup",
|
|
2075
2323
|
label: "Setup",
|
|
@@ -2889,7 +3137,7 @@ function roomHandoffProblem(node, parent) {
|
|
|
2889
3137
|
// cannot silently give a specialist access to another team's transcript.
|
|
2890
3138
|
const outsideSection = (room, speaker) => room.memberIds.some(id => {
|
|
2891
3139
|
const member = store.bot(id);
|
|
2892
|
-
return member && !canAccessTeam(speaker, member.section);
|
|
3140
|
+
return member && !canAccessTeam(speaker, member.section) && !coordinatorSupervises(member, speaker);
|
|
2893
3141
|
});
|
|
2894
3142
|
if (group && outsideSection(group, bot))
|
|
2895
3143
|
return "Destination room includes a member outside the agent's section";
|
|
@@ -2900,7 +3148,10 @@ function roomHandoffProblem(node, parent) {
|
|
|
2900
3148
|
const source = parent.groupId ? store.group(parent.groupId) : undefined;
|
|
2901
3149
|
if (!from || from.hidden || (parent.groupId ? !source || !source.memberIds.includes(from.id) || !store.groupTaskByThread(source.id, parent.threadId) : !store.taskByThread(from.id, parent.threadId)))
|
|
2902
3150
|
return "Source membership or task was removed";
|
|
2903
|
-
|
|
3151
|
+
// A specialist may address its supervisor in their shared conversation;
|
|
3152
|
+
// supervision alone never grants a direct chat or another room's access.
|
|
3153
|
+
const sharedSupervisor = source && group && source.id === group.id && parent.threadId === node.threadId && coordinatorSupervises(bot, from);
|
|
3154
|
+
if ((!canAccessTeam(from, bot.section) && !sharedSupervisor) || (source && outsideSection(source, from)))
|
|
2904
3155
|
return "Room work cannot cross the sender's section boundary";
|
|
2905
3156
|
if (source && group && source.id === group.id && parent.threadId !== node.threadId)
|
|
2906
3157
|
return "Same-room work must stay in the originating conversation";
|
|
@@ -3027,11 +3278,12 @@ const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
|
|
|
3027
3278
|
const result = {};
|
|
3028
3279
|
const turnText = coordinationTurnText(node, resumed);
|
|
3029
3280
|
const systemInstructions = coordinationSystemInstructions();
|
|
3030
|
-
|
|
3281
|
+
const request = roomHandoffs.sharedRequest(node);
|
|
3282
|
+
if (!resumed && !store.messagesFor(node.threadId).some(m => m.roomRequest?.id === request.id && m.roomRequest.phase === "request")) {
|
|
3031
3283
|
store.appendMessage(node.threadId, { role: "bot", kind: "text",
|
|
3032
|
-
roomRequest: { id:
|
|
3284
|
+
roomRequest: { id: request.id, phase: "request" },
|
|
3033
3285
|
from: sender ? { botId: sender.id, name: sender.name, color: sender.color } : undefined,
|
|
3034
|
-
text: `@${bot
|
|
3286
|
+
text: `${request.botIds.map(id => `@${store.bot(id)?.name ?? "Teammate"}`).join(" ")} ${node.text}`,
|
|
3035
3287
|
});
|
|
3036
3288
|
}
|
|
3037
3289
|
markInternalTurn(node.threadId);
|
|
@@ -3108,8 +3360,20 @@ const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
|
|
|
3108
3360
|
},
|
|
3109
3361
|
}, Date.now, roomHandoffLimits(cfg));
|
|
3110
3362
|
activeCoordinationForThread = threadId => roomHandoffs.activeDirect(threadId);
|
|
3111
|
-
|
|
3112
|
-
|
|
3363
|
+
const groupUsageReader = new GroupUsageReader(DATA_DIR);
|
|
3364
|
+
try {
|
|
3365
|
+
groupUsageReader.refresh();
|
|
3366
|
+
}
|
|
3367
|
+
catch { /* accounting must not block startup */ }
|
|
3368
|
+
function publicGroupState(record) {
|
|
3369
|
+
// The organization library's part hashes stay server-side.
|
|
3370
|
+
const { installedPackage: _installedPackage, ...group } = record;
|
|
3371
|
+
let usage;
|
|
3372
|
+
try {
|
|
3373
|
+
usage = groupUsageReader.forThread(group.threadId);
|
|
3374
|
+
}
|
|
3375
|
+
catch { /* accounting must not block chat */ }
|
|
3376
|
+
return { ...group, usage: usage ?? null, working: groupIsWorking(group) || [...roomHandoffs.nodes.values()].some(n => n.groupId === group.id && !["completed", "failed", "cancelled"].includes(n.status)) };
|
|
3113
3377
|
}
|
|
3114
3378
|
function beginGroupTurnOperation(groupId, threadId, botIds = []) {
|
|
3115
3379
|
const operation = {
|
|
@@ -3140,6 +3404,13 @@ function finishGroupTurnOperation(groupId, operation) {
|
|
|
3140
3404
|
const group = store.group(groupId);
|
|
3141
3405
|
if (group)
|
|
3142
3406
|
broadcast({ kind: "group", group: publicGroupState(group) });
|
|
3407
|
+
// The operation can outlive the last member's turn.completed: the fold
|
|
3408
|
+
// releases the speaker, but this map entry is what holds a 1:1 message
|
|
3409
|
+
// queued behind the room (reason "group-turn"), and no later event fires
|
|
3410
|
+
// once the run settles. Drain here, at the true end — the drain's own
|
|
3411
|
+
// block check re-reads activeGroupTurnForBot, so a replacement operation
|
|
3412
|
+
// begun in the interim still keeps its ground.
|
|
3413
|
+
drainQueuedSends();
|
|
3143
3414
|
// A follow-up sent while this operation was running belongs to the
|
|
3144
3415
|
// harness, not whichever composer happened to be mounted. Hand the next
|
|
3145
3416
|
// one to the ordinary channel runner as soon as the channel is truly idle.
|
|
@@ -3448,6 +3719,12 @@ store.onChange((change) => {
|
|
|
3448
3719
|
break;
|
|
3449
3720
|
}
|
|
3450
3721
|
case "bot.deleted":
|
|
3722
|
+
try {
|
|
3723
|
+
commandAllowlist.clear(change.botId);
|
|
3724
|
+
}
|
|
3725
|
+
catch {
|
|
3726
|
+
console.error("[command-allowlist] Could not remove deleted bot's saved rules.");
|
|
3727
|
+
}
|
|
3451
3728
|
broadcast({ kind: "bot.deleted", botId: change.botId });
|
|
3452
3729
|
break;
|
|
3453
3730
|
case "group": {
|
|
@@ -3718,6 +3995,17 @@ onSteeredQueueChange(() => broadcast({ kind: "bot.queued", queues: publicBotQueu
|
|
|
3718
3995
|
// once can collide on a bare id and patch each other's messages.
|
|
3719
3996
|
const toolMessageByItem = new Map(); // threadId:itemId -> messageId
|
|
3720
3997
|
const askMessageByRequest = new Map(); // threadId:requestId -> messageId
|
|
3998
|
+
// Only native, live permission metadata can become a remembered grant. Never
|
|
3999
|
+
// rebuild executable authority from an imported transcript or display summary.
|
|
4000
|
+
const pendingCommandRules = new Map();
|
|
4001
|
+
function commandRememberRefusal(auth, threadId, requestId, behavior) {
|
|
4002
|
+
if (!canManageCommandAllowlist(auth))
|
|
4003
|
+
return "Only the workspace owner or an admin can save bot-wide command permissions.";
|
|
4004
|
+
if (behavior !== "allow" || !pendingCommandRules.has(`${threadId}:${requestId}`)) {
|
|
4005
|
+
return "This live request does not contain a command that can be saved. Allow it once instead.";
|
|
4006
|
+
}
|
|
4007
|
+
return null;
|
|
4008
|
+
}
|
|
3721
4009
|
/** Deliver a person's answer to the engine that asked, and tell the truth
|
|
3722
4010
|
* about what happened. `unavailable` — the turn ended, the ask timed out,
|
|
3723
4011
|
* the engine has no asks — is fail-closed: the action was never run. The
|
|
@@ -3725,7 +4013,7 @@ const askMessageByRequest = new Map(); // threadId:requestId -> messageId
|
|
|
3725
4013
|
* a 500 while the card sits open forever. */
|
|
3726
4014
|
async function answerRequest(threadId, instanceId, requestId, behavior, message, decidedFor,
|
|
3727
4015
|
/** "Always allow this session": the provider keeps the allow, not the app */
|
|
3728
|
-
always) {
|
|
4016
|
+
always, rememberCommand) {
|
|
3729
4017
|
// Snapshot the card BEFORE delivering the answer: a delivered answer
|
|
3730
4018
|
// resolves the request synchronously through the fold, which consumes
|
|
3731
4019
|
// the askMessageByRequest entry — by the time the await returns, nobody
|
|
@@ -3738,7 +4026,10 @@ always) {
|
|
|
3738
4026
|
? thread.find((m) => m.id === cardMessageId)
|
|
3739
4027
|
: thread.find((m) => m.card?.requestId === requestId);
|
|
3740
4028
|
const card = cardMessage?.card;
|
|
3741
|
-
const
|
|
4029
|
+
const remembered = rememberCommand ? pendingCommandRules.get(`${threadId}:${requestId}`) : undefined;
|
|
4030
|
+
// Cloud routing and a model change can differ from the bot's current
|
|
4031
|
+
// settings. The live turn owns this question, just as it owns Stop/steer.
|
|
4032
|
+
const instance = runningTurnEngines.get(threadId) ?? registry.get(instanceId);
|
|
3742
4033
|
let outcome = "unavailable";
|
|
3743
4034
|
if (instance) {
|
|
3744
4035
|
try {
|
|
@@ -3748,6 +4039,18 @@ always) {
|
|
|
3748
4039
|
outcome = "unavailable";
|
|
3749
4040
|
}
|
|
3750
4041
|
}
|
|
4042
|
+
if (remembered && outcome === "allowed-once" && behavior === "allow" && store.bot(remembered.botId)) {
|
|
4043
|
+
// Persist only after the provider accepted this exact live request. A dead
|
|
4044
|
+
// card, denied request, failed delivery or process restart saves no grant.
|
|
4045
|
+
try {
|
|
4046
|
+
commandAllowlist.add(remembered.botId, remembered.candidate);
|
|
4047
|
+
}
|
|
4048
|
+
catch {
|
|
4049
|
+
store.appendMessage(threadId, { role: "bot", kind: "activity", tool: {
|
|
4050
|
+
name: "Command allowed once, but its allowlist rule could not be saved. Try again from the permissions menu.", ok: false,
|
|
4051
|
+
} });
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
3751
4054
|
// An answered question keeps its words. `request.resolved` only records
|
|
3752
4055
|
// the behavior, so without this the card reads "answer" forever and the
|
|
3753
4056
|
// person can no longer see what they told the bot.
|
|
@@ -3801,6 +4104,9 @@ always) {
|
|
|
3801
4104
|
* be answered. Routine proposals are harness-owned and durable, so they stay
|
|
3802
4105
|
* actionable even after the proposing turn has stopped. */
|
|
3803
4106
|
function closeOpenApprovals(threadId) {
|
|
4107
|
+
for (const key of pendingCommandRules.keys())
|
|
4108
|
+
if (key.startsWith(`${threadId}:`))
|
|
4109
|
+
pendingCommandRules.delete(key);
|
|
3804
4110
|
// Peer approvals also hold an in-memory promise. Resolve those first; merely
|
|
3805
4111
|
// patching their cards would leave the delegation queue waiting 15 minutes.
|
|
3806
4112
|
cancelPeerApprovalsForThread(threadId);
|
|
@@ -3970,6 +4276,10 @@ bus.subscribe((event) => {
|
|
|
3970
4276
|
watchdog.setWaitingOnHuman(event.threadId, true);
|
|
3971
4277
|
else if (event.type === "request.resolved")
|
|
3972
4278
|
watchdog.setWaitingOnHuman(event.threadId, false);
|
|
4279
|
+
else if (event.type === "turn.wait_started")
|
|
4280
|
+
watchdog.setWaitingOnComputer(event.threadId, true);
|
|
4281
|
+
else if (event.type === "turn.wait_ended")
|
|
4282
|
+
watchdog.setWaitingOnComputer(event.threadId, false);
|
|
3973
4283
|
else if (event.type === "turn.completed") {
|
|
3974
4284
|
watchdog.settle(event.threadId);
|
|
3975
4285
|
revokeInternalCapabilityForProviderEvent(event);
|
|
@@ -4444,8 +4754,12 @@ async function mountHostComputer(owner, botId, providerSupportsLocal) {
|
|
|
4444
4754
|
: "this model engine cannot control this computer — choose Claude or an ACP engine, or select another destination");
|
|
4445
4755
|
}
|
|
4446
4756
|
const cua = readCuaConnection();
|
|
4447
|
-
if (!cua)
|
|
4448
|
-
|
|
4757
|
+
if (!cua) {
|
|
4758
|
+
const reason = readCuaUnavailableReason();
|
|
4759
|
+
throw new Error(reason
|
|
4760
|
+
? `CUA Driver is not ready for this computer — ${reason}${process.platform === "darwin" && /(?:Screen Recording|Accessibility).*required/i.test(reason) ? ". Relaunch OpenMausBot after granting the missing macOS permission." : ""}`
|
|
4761
|
+
: "CUA Driver is not ready for this computer — check permissions and restart OpenMausBot");
|
|
4762
|
+
}
|
|
4449
4763
|
await bindTurnComputer(owner, "computer:host");
|
|
4450
4764
|
return gatedLocalComputer(cua, controlIntegration(botId, owner.threadId, owner.generation));
|
|
4451
4765
|
}
|
|
@@ -4472,6 +4786,7 @@ async function mountBotVps(bot, owner, opts) {
|
|
|
4472
4786
|
owner,
|
|
4473
4787
|
lazy: true,
|
|
4474
4788
|
label: "the VPS computer",
|
|
4789
|
+
onRejected: opts.onRejected,
|
|
4475
4790
|
claim: async () => {
|
|
4476
4791
|
await bindTurnComputer(owner, vpsResource, true);
|
|
4477
4792
|
opts.onClaimed(vpsCapture);
|
|
@@ -4608,11 +4923,12 @@ function turnProvider(bot, runOn, threadId) {
|
|
|
4608
4923
|
return "box";
|
|
4609
4924
|
return bot.cloudBackend === "vps" ? "vps" : wants === "cloud" ? "box" : null;
|
|
4610
4925
|
}
|
|
4611
|
-
/** A
|
|
4612
|
-
*
|
|
4613
|
-
* machine relays clicks and screenshots. Every start/interrupt of a turn asks
|
|
4614
|
-
* here which engine owns it. */
|
|
4926
|
+
/** A driver with a Box bridge keeps the selected model. Other engines use
|
|
4927
|
+
* Box's native runner. Start and interrupt must resolve the same owner. */
|
|
4615
4928
|
function turnInstance(bot, runOn, threadId) {
|
|
4929
|
+
const selected = registry.get(bot.modelSelection.instanceId);
|
|
4930
|
+
if (selected?.adapter.capabilities.cloudComputerMcp)
|
|
4931
|
+
return selected;
|
|
4616
4932
|
const onBox = turnProvider(bot, runOn, threadId) === "box";
|
|
4617
4933
|
return onBox
|
|
4618
4934
|
? registry.instances().find((candidate) => candidate.driverKind === "boxAgent") ?? null
|
|
@@ -4677,7 +4993,7 @@ async function selectableComputers(bot) {
|
|
|
4677
4993
|
canCreate = Boolean(status?.configured && status.daemonUp && status.container === "missing");
|
|
4678
4994
|
reason = status?.problem ?? reason;
|
|
4679
4995
|
}
|
|
4680
|
-
else if (box.boxConfigured(cfg) && registry.instances().some(instance => instance.driverKind === "boxAgent")) {
|
|
4996
|
+
else if (box.boxConfigured(cfg) && (caps?.cloudComputerMcp || registry.instances().some(instance => instance.driverKind === "boxAgent"))) {
|
|
4681
4997
|
const status = await box.boxStatus(cfg, bot.id);
|
|
4682
4998
|
const lifecycle = box.boxTurnLifecycleAction({ explicitCloud: true, canMount: true, state: status.box?.state ?? null });
|
|
4683
4999
|
ready = lifecycle === "attach";
|
|
@@ -4740,7 +5056,10 @@ function continueComputerSelection(threadId, generation, succeeded) {
|
|
|
4740
5056
|
}
|
|
4741
5057
|
if (store.activePath(threadId).findLast(message => message.role === "user" && message.kind === "text")?.id !== selection.source.id)
|
|
4742
5058
|
return;
|
|
4743
|
-
|
|
5059
|
+
// The model picked this surface through select_computer; the person
|
|
5060
|
+
// never chose it in the composer or thread settings. It is the
|
|
5061
|
+
// machine's record, so a later Works on change may sweep it.
|
|
5062
|
+
store.patchTask(bot.id, threadId, { surface, surfaceSource: "auto" });
|
|
4744
5063
|
const text = `The computer selection is now ${surfaceLabel(surface)}. Continue the user's original request using the tools mounted for this turn; verify the result before claiming success.\n\n${selection.text}`;
|
|
4745
5064
|
void startTurn(bot.id, text, { threadId, userMessage: selection.source, computerSelectionContinuation: true }).catch(error => {
|
|
4746
5065
|
if (store.taskByThread(bot.id, threadId))
|
|
@@ -4965,8 +5284,12 @@ bus.subscribe((event) => {
|
|
|
4965
5284
|
const privateImageEvent = event.type === "item.completed" && event.itemType === "assistant_image";
|
|
4966
5285
|
// The durable message patch below is the public frame. Sending raw base64
|
|
4967
5286
|
// through runtime SSE would multiply large bytes across every app window.
|
|
4968
|
-
if (!privateImageEvent)
|
|
4969
|
-
|
|
5287
|
+
if (!privateImageEvent) {
|
|
5288
|
+
// Exact executable input is internal matching metadata, not inspector UI.
|
|
5289
|
+
// The event bus's persistence redactor does not scrub its live event.
|
|
5290
|
+
const publicEvent = event.type === "request.opened" ? { ...event, command: undefined } : event;
|
|
5291
|
+
broadcast({ kind: "runtime", event: publicEvent });
|
|
5292
|
+
}
|
|
4970
5293
|
const routineRun = privateImageEvent ? null : (routines?.handleRuntimeEvent(event) ?? null);
|
|
4971
5294
|
const ownerBot = store.botByThread(event.threadId);
|
|
4972
5295
|
const bot = ownerBot ? botForThread(ownerBot.id, event.threadId) ?? undefined : undefined;
|
|
@@ -5018,10 +5341,10 @@ bus.subscribe((event) => {
|
|
|
5018
5341
|
try {
|
|
5019
5342
|
const decoded = decodeGeneratedImage(event.data);
|
|
5020
5343
|
const saved = saveImage(decoded.bytes, decoded.mime);
|
|
5021
|
-
const key =
|
|
5022
|
-
const current =
|
|
5344
|
+
const key = turnAttachmentKey(event.threadId, event.turnId);
|
|
5345
|
+
const current = turnAttachmentsByTurn.get(key) ?? [];
|
|
5023
5346
|
current.push({ kind: "image", path: saved.path, mime: saved.mime });
|
|
5024
|
-
|
|
5347
|
+
turnAttachmentsByTurn.set(key, current);
|
|
5025
5348
|
}
|
|
5026
5349
|
catch (error) {
|
|
5027
5350
|
pushMessage({
|
|
@@ -5092,14 +5415,22 @@ bus.subscribe((event) => {
|
|
|
5092
5415
|
const permission = event.requestType === "permission" && !event.questions?.length;
|
|
5093
5416
|
// A permission request here is one the provider left for a person: its
|
|
5094
5417
|
// own mode already ran (Ask, Edits, Auto's reviewer, Custom's config).
|
|
5095
|
-
//
|
|
5096
|
-
//
|
|
5418
|
+
// Only the person's explicit Full access or exact saved command answers.
|
|
5419
|
+
// The app does not guess whether the action is safe. A QUESTION
|
|
5097
5420
|
// always reaches the human — even Full access never invents an answer.
|
|
5098
5421
|
const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
5099
5422
|
const unattended = permission && asker && event.requestId ? isUnattended(asker.id, event.threadId) : false;
|
|
5100
5423
|
const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)) : "ask";
|
|
5424
|
+
// Native shell descriptors are distinct from computer/MCP permissions;
|
|
5425
|
+
// choosing a local desktop must not disable an exact shell grant.
|
|
5426
|
+
const command = permission && asker && event.requestId && !event.requiresExplicitApproval && event.command
|
|
5427
|
+
? commandAllowlistCandidate({ ...event.command, providerInstanceId: event.providerInstanceId ?? asker.modelSelection.instanceId })
|
|
5428
|
+
: null;
|
|
5101
5429
|
const verdict = permission && asker && event.requestId
|
|
5102
|
-
? autoVerdict(effectiveApprovalMode, event.tool, {
|
|
5430
|
+
? autoVerdict(effectiveApprovalMode, event.tool, {
|
|
5431
|
+
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
5432
|
+
commandAllowed: Boolean(command && commandAllowlist.matches(asker.id, command)),
|
|
5433
|
+
})
|
|
5103
5434
|
: null;
|
|
5104
5435
|
// Auto's reviewer is the engine's own. Claude accepts `--permission-mode
|
|
5105
5436
|
// auto` for any model and starts in Manual without a word when auto is
|
|
@@ -5146,8 +5477,8 @@ bus.subscribe((event) => {
|
|
|
5146
5477
|
pushMessage({
|
|
5147
5478
|
role: "bot", kind: "activity",
|
|
5148
5479
|
tool: { name: outcome === "rejected"
|
|
5149
|
-
?
|
|
5150
|
-
: "error: could not deliver
|
|
5480
|
+
? `The provider rejected this action despite ${verdict.source === "command-allowlist" ? "the saved command permission" : "Full access"}.`
|
|
5481
|
+
: "error: could not deliver approval to the provider; retry the task after reconnecting.", ok: false },
|
|
5151
5482
|
});
|
|
5152
5483
|
return;
|
|
5153
5484
|
}
|
|
@@ -5170,7 +5501,7 @@ bus.subscribe((event) => {
|
|
|
5170
5501
|
})().catch(() => {
|
|
5171
5502
|
// A receipt failure must neither crash the server nor manufacture
|
|
5172
5503
|
// a new permission request after the provider took our answer.
|
|
5173
|
-
console.error("[
|
|
5504
|
+
console.error("[approval] Could not record the provider approval result.");
|
|
5174
5505
|
});
|
|
5175
5506
|
break;
|
|
5176
5507
|
}
|
|
@@ -5192,9 +5523,11 @@ bus.subscribe((event) => {
|
|
|
5192
5523
|
options: event.choices?.length ? event.choices : permission ? ["Allow", "Deny"] : [],
|
|
5193
5524
|
requestId: event.requestId,
|
|
5194
5525
|
tool: permission ? event.tool : undefined,
|
|
5195
|
-
questionRequest: questions
|
|
5196
|
-
|
|
5197
|
-
|
|
5526
|
+
questionRequest: questions
|
|
5527
|
+
? { version: 1, questions, ...(event.origin === "output" ? { origin: "output" } : {}) }
|
|
5528
|
+
: undefined,
|
|
5529
|
+
commandAllowlist: command ?? undefined,
|
|
5530
|
+
// Provider-owned session grants remain separate from exact commands.
|
|
5198
5531
|
allowSession: permission && event.allowSession && !event.requiresExplicitApproval ? true : undefined,
|
|
5199
5532
|
// The text stays for cards saved before heldCode existed, and for
|
|
5200
5533
|
// clients that do not know the key yet.
|
|
@@ -5205,6 +5538,8 @@ bus.subscribe((event) => {
|
|
|
5205
5538
|
});
|
|
5206
5539
|
if (event.requestId)
|
|
5207
5540
|
askMessageByRequest.set(`${event.threadId}:${event.requestId}`, message.id);
|
|
5541
|
+
if (command && asker && event.requestId)
|
|
5542
|
+
pendingCommandRules.set(`${event.threadId}:${event.requestId}`, { botId: asker.id, candidate: command });
|
|
5208
5543
|
// Every card that reaches a human is a decision too: "the provider
|
|
5209
5544
|
// left this for you, in this mode". `question` marks the cards no
|
|
5210
5545
|
// rule may ever answer; a permission card without a verdict (no known
|
|
@@ -5219,6 +5554,7 @@ bus.subscribe((event) => {
|
|
|
5219
5554
|
summary: event.summary,
|
|
5220
5555
|
decision: "card-shown",
|
|
5221
5556
|
source: !permission ? "question" : verdict ? verdict.source : "no-grant",
|
|
5557
|
+
origin: !permission && event.origin === "output" ? "output" : undefined,
|
|
5222
5558
|
unattended: unattended || undefined,
|
|
5223
5559
|
});
|
|
5224
5560
|
// Notify from HERE, not from a separate subscriber on request.opened:
|
|
@@ -5239,6 +5575,8 @@ bus.subscribe((event) => {
|
|
|
5239
5575
|
break;
|
|
5240
5576
|
}
|
|
5241
5577
|
case "request.resolved": {
|
|
5578
|
+
if (event.requestId)
|
|
5579
|
+
pendingCommandRules.delete(`${event.threadId}:${event.requestId}`);
|
|
5242
5580
|
// answered (by whoever): the turn is working again, unless it settled
|
|
5243
5581
|
const waiting = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
5244
5582
|
if (bot && store.taskByThread(bot.id, event.threadId)?.activity === "waiting-on-you") {
|
|
@@ -5296,26 +5634,60 @@ bus.subscribe((event) => {
|
|
|
5296
5634
|
// thread nobody types in again (a deleted bot's) is not held forever.
|
|
5297
5635
|
const internal = isInternalTurn(event.threadId);
|
|
5298
5636
|
clearInternalTurn(event.threadId);
|
|
5299
|
-
const generatedKey =
|
|
5300
|
-
const generated =
|
|
5301
|
-
|
|
5302
|
-
|
|
5637
|
+
const generatedKey = turnAttachmentKey(event.threadId, event.turnId);
|
|
5638
|
+
const generated = turnAttachmentsByTurn.get(generatedKey) ?? [];
|
|
5639
|
+
turnAttachmentsByTurn.delete(generatedKey);
|
|
5640
|
+
// A turn that never completed never lands its half-said note: parked
|
|
5641
|
+
// audio is deleted when the settle is not ok (person stop, provider
|
|
5642
|
+
// failure) rather than posted as if the turn finished (#1742).
|
|
5643
|
+
// Generated images keep their long-standing settle-anyway behavior.
|
|
5644
|
+
for (const attachment of generated) {
|
|
5645
|
+
if (event.ok || attachment.kind !== "audio")
|
|
5646
|
+
continue;
|
|
5647
|
+
deleteAttachment(attachment.path);
|
|
5648
|
+
}
|
|
5649
|
+
const settled = event.ok || !generated.length
|
|
5650
|
+
? generated
|
|
5651
|
+
: generated.filter((attachment) => attachment.kind !== "audio");
|
|
5652
|
+
if (settled.length) {
|
|
5303
5653
|
const response = [...store.messagesFor(event.threadId)].reverse().find((message) => message.role === "bot" &&
|
|
5304
5654
|
message.kind === "text" &&
|
|
5305
5655
|
message.turnId === completedTurnId);
|
|
5656
|
+
// A voice note's text is its transcript: the visible caption search,
|
|
5657
|
+
// compaction and notifications read (#1740 decision 3).
|
|
5658
|
+
const transcript = settled
|
|
5659
|
+
.filter((attachment) => attachment.kind === "audio" && attachment.text)
|
|
5660
|
+
.map((attachment) => attachment.text.trim())
|
|
5661
|
+
.filter(Boolean)
|
|
5662
|
+
.join("\n\n");
|
|
5663
|
+
const staged = settled.map((attachment) => attachment.kind === "audio"
|
|
5664
|
+
? { kind: "audio", path: attachment.path, mime: attachment.mime }
|
|
5665
|
+
: attachment);
|
|
5306
5666
|
if (response) {
|
|
5307
5667
|
store.patchMessage(event.threadId, response.id, {
|
|
5308
|
-
attachments: [...(response.attachments ?? []), ...
|
|
5668
|
+
attachments: [...(response.attachments ?? []), ...staged],
|
|
5669
|
+
// The note's text is its transcript and stays visible beside a
|
|
5670
|
+
// written epilogue: the wire audio carries no text, so letting
|
|
5671
|
+
// the epilogue replace it would erase what was said (#1740
|
|
5672
|
+
// decision 3).
|
|
5673
|
+
...(transcript
|
|
5674
|
+
? {
|
|
5675
|
+
text: [response.text?.trim(), redactSecretsInText(transcript)]
|
|
5676
|
+
.filter(Boolean)
|
|
5677
|
+
.join("\n\n"),
|
|
5678
|
+
}
|
|
5679
|
+
: {}),
|
|
5309
5680
|
});
|
|
5310
5681
|
}
|
|
5311
5682
|
else {
|
|
5312
|
-
// Some
|
|
5313
|
-
// terminal assistant response instead of inventing model words
|
|
5683
|
+
// Some turns have no textual epilogue. Keep the attachment as the
|
|
5684
|
+
// terminal assistant response instead of inventing model words; a
|
|
5685
|
+
// voice note's own text is the visible transcript there.
|
|
5314
5686
|
pushMessage({
|
|
5315
5687
|
role: "bot",
|
|
5316
5688
|
kind: "text",
|
|
5317
|
-
text:
|
|
5318
|
-
attachments:
|
|
5689
|
+
text: transcript,
|
|
5690
|
+
attachments: staged,
|
|
5319
5691
|
turnId: completedTurnId,
|
|
5320
5692
|
});
|
|
5321
5693
|
}
|
|
@@ -5329,7 +5701,13 @@ bus.subscribe((event) => {
|
|
|
5329
5701
|
lastReply.delete(event.threadId);
|
|
5330
5702
|
// A run that broke — not one the person stopped, and not a routine's,
|
|
5331
5703
|
// which reports through its own failure path — is the Chief's to see.
|
|
5332
|
-
|
|
5704
|
+
// A lazy computer-claim rejection already reported its failure and
|
|
5705
|
+
// interrupted the turn; Claude settles that interrupt as
|
|
5706
|
+
// exit_before_result, not "interrupted", so this generation's marker
|
|
5707
|
+
// keeps the same failure from reporting a second incident.
|
|
5708
|
+
const lazyClaimAlreadyReported = event.stopReason === "exit_before_result" &&
|
|
5709
|
+
turnResourceOwners.get(event.threadId)?.lazyClaimFailureReported === true;
|
|
5710
|
+
if (!event.ok && event.stopReason !== "interrupted" && !lazyClaimAlreadyReported && !routines?.runForThread(event.threadId)) {
|
|
5333
5711
|
const broken = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
5334
5712
|
if (broken)
|
|
5335
5713
|
reportIncident({ kind: "failed", bot: broken, threadId: event.threadId, detail: event.stopReason?.trim() || "the run ended without a result" });
|
|
@@ -5400,6 +5778,8 @@ bus.subscribe((event) => {
|
|
|
5400
5778
|
const measuredSelection = directTurnBots.get(event.threadId)?.modelSelection ?? selection;
|
|
5401
5779
|
store.patchTask(bot.id, event.threadId, { lastContextModel: Number.isFinite(lastContext?.tokens) && (lastContext?.tokens ?? 0) > 0
|
|
5402
5780
|
? `${measuredSelection.instanceId}:${measuredSelection.model}` : undefined });
|
|
5781
|
+
const promptBytes = turnPromptBytes.get(event.threadId);
|
|
5782
|
+
turnPromptBytes.delete(event.threadId);
|
|
5403
5783
|
bookTurnUsage({
|
|
5404
5784
|
botId: bot.id,
|
|
5405
5785
|
botName: bot.name,
|
|
@@ -5410,6 +5790,7 @@ bus.subscribe((event) => {
|
|
|
5410
5790
|
input: tokens?.input ?? 0,
|
|
5411
5791
|
output: tokens?.output ?? 0,
|
|
5412
5792
|
...(typeof tokens?.cachedInput === "number" ? { cachedInput: tokens.cachedInput } : {}),
|
|
5793
|
+
...(promptBytes ? { promptBytes } : {}),
|
|
5413
5794
|
costUsd: event.cost ?? null,
|
|
5414
5795
|
trigger: routineRun
|
|
5415
5796
|
? { kind: "routine", routineId: routineRun.routineId, label: routineRun.routineName }
|
|
@@ -5485,6 +5866,8 @@ bus.subscribe((event) => {
|
|
|
5485
5866
|
const tokens = event.usage ?? lastReported;
|
|
5486
5867
|
const speakingBot = store.bot(speaker.botId);
|
|
5487
5868
|
const selection = speakingBot?.modelSelection;
|
|
5869
|
+
const promptBytes = turnPromptBytes.get(event.threadId);
|
|
5870
|
+
turnPromptBytes.delete(event.threadId);
|
|
5488
5871
|
bookTurnUsage({
|
|
5489
5872
|
botId: speaker.botId,
|
|
5490
5873
|
botName: speaker.name,
|
|
@@ -5495,11 +5878,20 @@ bus.subscribe((event) => {
|
|
|
5495
5878
|
input: tokens?.input ?? 0,
|
|
5496
5879
|
output: tokens?.output ?? 0,
|
|
5497
5880
|
...(typeof tokens?.cachedInput === "number" ? { cachedInput: tokens.cachedInput } : {}),
|
|
5881
|
+
...(promptBytes ? { promptBytes } : {}),
|
|
5498
5882
|
costUsd: event.cost ?? null,
|
|
5499
5883
|
trigger: routineRun
|
|
5500
5884
|
? { kind: "routine", routineId: routineRun.routineId, label: routineRun.routineName }
|
|
5501
5885
|
: turnTriggers.get(event.threadId) ?? { kind: "owner" },
|
|
5502
5886
|
});
|
|
5887
|
+
// Appends are asynchronous. Refresh after persistence so the settled
|
|
5888
|
+
// turn appears immediately, without waiting for another message.
|
|
5889
|
+
void flushUsageLedger(DATA_DIR).then(() => {
|
|
5890
|
+
groupUsageReader.refresh();
|
|
5891
|
+
const current = store.group(group.id);
|
|
5892
|
+
if (current)
|
|
5893
|
+
broadcast({ kind: "group", group: publicGroupState(current) });
|
|
5894
|
+
}).catch(() => { });
|
|
5503
5895
|
if (completedTurnId) {
|
|
5504
5896
|
void scheduleTurnDigest({
|
|
5505
5897
|
botId: speaker.botId, botName: speaker.name,
|
|
@@ -6207,8 +6599,12 @@ function drainQueuedSends() {
|
|
|
6207
6599
|
* available. Reuse the existing cancellable, idempotent composer queue. */
|
|
6208
6600
|
async function startOrQueueDirectMessage(botId, threadId, text, replyTo, sendId, sender, trigger) {
|
|
6209
6601
|
const capacity = botAtThreadCapacity(botId);
|
|
6210
|
-
|
|
6211
|
-
|
|
6602
|
+
// A room turn holds the bot exactly like the sibling opened-thread queue
|
|
6603
|
+
// below: the drain's own block check waits it out, so the words queue
|
|
6604
|
+
// here rather than bounce off startTurn's 409.
|
|
6605
|
+
const groupTurn = activeGroupTurnForBot(botId);
|
|
6606
|
+
if (capacity || threadBusy(botId, threadId) || groupTurn || parksBehindCoordination(botId, threadId)) {
|
|
6607
|
+
const reason = capacity ? "capacity" : groupTurn ? "group-turn" : undefined;
|
|
6212
6608
|
const queued = queueSteeredMessage(botId, threadId, text, {
|
|
6213
6609
|
replyToId: replyTo?.id,
|
|
6214
6610
|
sendId,
|
|
@@ -6235,8 +6631,14 @@ async function startOrQueueOpenedThread(botId, threadId, text, unattended) {
|
|
|
6235
6631
|
// A room turn holds the bot too (startTurn refuses a direct turn during
|
|
6236
6632
|
// one); the drain's own block check already waits for it, so the words
|
|
6237
6633
|
// queue here rather than bounce.
|
|
6238
|
-
|
|
6239
|
-
|
|
6634
|
+
const capacity = botAtThreadCapacity(botId);
|
|
6635
|
+
const groupTurn = activeGroupTurnForBot(botId);
|
|
6636
|
+
if (capacity || groupTurn) {
|
|
6637
|
+
queueSteeredMessage(botId, threadId, text, {
|
|
6638
|
+
reason: capacity ? "capacity" : "group-turn",
|
|
6639
|
+
unattended,
|
|
6640
|
+
peerAsk,
|
|
6641
|
+
});
|
|
6240
6642
|
return { state: "queued", position: queuedThreadPosition(botId, threadId) ?? 1 };
|
|
6241
6643
|
}
|
|
6242
6644
|
try {
|
|
@@ -6542,11 +6944,12 @@ async function startTurn(botId, text, opts) {
|
|
|
6542
6944
|
throw Object.assign(new Error("this provider account is being updated — try again shortly"), { status: 409 });
|
|
6543
6945
|
}
|
|
6544
6946
|
const switchedEngine = instance.instanceId !== bot.modelSelection.instanceId;
|
|
6545
|
-
const
|
|
6546
|
-
|
|
6547
|
-
//
|
|
6548
|
-
|
|
6549
|
-
const
|
|
6947
|
+
const useInstanceDefaults = switchedEngine || (opts?.runOn === "cloud" && !instance.adapter.capabilities.cloudComputerMcp);
|
|
6948
|
+
const model = useInstanceDefaults ? instance.models.default : bot.modelSelection.model;
|
|
6949
|
+
// A native cloud runner borrows its instance defaults; a Box bridge keeps
|
|
6950
|
+
// the bot's selected model, effort and variant.
|
|
6951
|
+
const effort = useInstanceDefaults ? undefined : bot.modelSelection.effort;
|
|
6952
|
+
const variant = useInstanceDefaults ? undefined : bot.modelSelection.variant;
|
|
6550
6953
|
assertModelVariantSupported({ variant, effort }, instance.adapter.capabilities);
|
|
6551
6954
|
// A selection can be persisted while its engine is offline. Re-check when
|
|
6552
6955
|
// the engine returns so an old or unsupported value never reaches a CLI.
|
|
@@ -6556,7 +6959,7 @@ async function startTurn(botId, text, opts) {
|
|
|
6556
6959
|
// an edit hands us its already-branched user message; a plain send appends
|
|
6557
6960
|
let userMessage = opts?.userMessage;
|
|
6558
6961
|
if (opts?.editedMessageId) {
|
|
6559
|
-
const edited = store.branchMessage(threadId, opts.editedMessageId, text);
|
|
6962
|
+
const edited = store.branchMessage(threadId, opts.editedMessageId, text, opts.sendId);
|
|
6560
6963
|
if (!edited)
|
|
6561
6964
|
throw Object.assign(new Error("only a user text message can be edited"), { status: 400 });
|
|
6562
6965
|
store.patchTask(bot.id, threadId, { rewound: true });
|
|
@@ -6632,6 +7035,12 @@ async function startTurn(botId, text, opts) {
|
|
|
6632
7035
|
computerSelectionTurns.set(threadId, { generation: dispatchClaimId, botId: bot.id, source, text });
|
|
6633
7036
|
}
|
|
6634
7037
|
store.setTaskActivity(bot.id, threadId, "working");
|
|
7038
|
+
// Watch from admission, not dispatch: a turn can wedge in setup — context
|
|
7039
|
+
// compaction that never returns, a hung browser or VM mount — long before
|
|
7040
|
+
// any provider event exists, and a watch armed only at dispatch never saw
|
|
7041
|
+
// those. Every setup exit below settles or throws into the catch, which
|
|
7042
|
+
// settles under the same generation ownership it already enforces.
|
|
7043
|
+
watchdog.watch(threadId, bot.id);
|
|
6635
7044
|
// A closed thread that gets a new turn is open again: the person (or the
|
|
6636
7045
|
// opener) picked it back up, so its row returns to the sidebar and
|
|
6637
7046
|
// list_threads stops calling it closed. No-op on an open thread.
|
|
@@ -6646,6 +7055,16 @@ async function startTurn(botId, text, opts) {
|
|
|
6646
7055
|
turnContext.delete(threadId);
|
|
6647
7056
|
void (async () => {
|
|
6648
7057
|
try {
|
|
7058
|
+
// Readiness can wait on the network. Admit the task first so its busy
|
|
7059
|
+
// state, Stop action and watchdog own that wait just like VM setup.
|
|
7060
|
+
if (opts?.runOn === "cloud") {
|
|
7061
|
+
const readiness = await cloudRoutineReadiness(botId, threadId);
|
|
7062
|
+
if (!directTurnClaimIsCurrent(bot.id, dispatchClaimId, threadId)) {
|
|
7063
|
+
throw new DirectTurnSetupCancelled("turn stopped during cloud readiness");
|
|
7064
|
+
}
|
|
7065
|
+
if (!readiness.ready)
|
|
7066
|
+
throw new Error(readiness.reason ?? "The cloud computer is not ready");
|
|
7067
|
+
}
|
|
6649
7068
|
await compactConversation({ bot, threadId, generation: dispatchClaimId, instance, model,
|
|
6650
7069
|
excludedIds: new Set([userMessage.id, ...(opts?.excludeMessageIds ?? [])]), manual: opts?.compactOnly === true });
|
|
6651
7070
|
if (!directTurnClaimIsCurrent(bot.id, dispatchClaimId, threadId))
|
|
@@ -6656,6 +7075,7 @@ async function startTurn(botId, text, opts) {
|
|
|
6656
7075
|
settleDirectFollowup(dispatchClaimId);
|
|
6657
7076
|
releaseTurnResources(resourceOwner);
|
|
6658
7077
|
store.setTaskActivity(bot.id, threadId, "idle");
|
|
7078
|
+
watchdog.settle(threadId);
|
|
6659
7079
|
directTurnBots.delete(threadId);
|
|
6660
7080
|
retryDelegationsWaitingOn(bot.id);
|
|
6661
7081
|
drainQueuedSends();
|
|
@@ -6752,7 +7172,7 @@ async function startTurn(botId, text, opts) {
|
|
|
6752
7172
|
rewound,
|
|
6753
7173
|
fresh: fresh || contextReset,
|
|
6754
7174
|
externallyUpdated: Boolean(externalContextMarker) || handedStale,
|
|
6755
|
-
replaysNatively: instance.driverKind
|
|
7175
|
+
replaysNatively: NATIVELY_REPLAYING_DRIVER_KINDS.includes(instance.driverKind),
|
|
6756
7176
|
});
|
|
6757
7177
|
// Snapshot the cursor alongside the context decision. An external result
|
|
6758
7178
|
// can arrive during async computer/setup work and clear the task cursor;
|
|
@@ -6808,9 +7228,7 @@ async function startTurn(botId, text, opts) {
|
|
|
6808
7228
|
...(skillAuthoring ? ["skillAuthoring"] : []),
|
|
6809
7229
|
], availableSkills());
|
|
6810
7230
|
if (selectedSkills.some((skill) => skill.manifest.requiredCapabilities.includes("phoneMcp"))) {
|
|
6811
|
-
|
|
6812
|
-
throw new Error("another thread is using the phone — wait for it to finish");
|
|
6813
|
-
integrations.phone = phoneIntegration();
|
|
7231
|
+
integrations.phone = phoneIntegration(bot.id, threadId, dispatchClaimId);
|
|
6814
7232
|
}
|
|
6815
7233
|
// the user's connected apps, but only to a driver that can mount
|
|
6816
7234
|
// them — a key in the config says the connections exist, not that
|
|
@@ -6875,9 +7293,7 @@ async function startTurn(botId, text, opts) {
|
|
|
6875
7293
|
const teamComputer = inheritedTeamComputer(bot);
|
|
6876
7294
|
const cloudBackend = teamComputer || opts?.runOn === "cloud" || bot.cloudBackend !== "vps" ? "box" : "vps";
|
|
6877
7295
|
const mountsComputerMcp = instance.adapter.capabilities.computerMcp === true;
|
|
6878
|
-
|
|
6879
|
-
// Local VM/VPS tools, but have no Box relay to execute this descriptor.
|
|
6880
|
-
const mountsCloudComputer = instance.driverKind === "boxAgent";
|
|
7296
|
+
const mountsCloudComputer = instance.driverKind === "boxAgent" || instance.adapter.capabilities.cloudComputerMcp === true;
|
|
6881
7297
|
const mountsLocalComputer = instance.adapter.capabilities.localComputerMcp === true;
|
|
6882
7298
|
// Where this turn's hands may land. The bot's "Works on" choice is
|
|
6883
7299
|
// strict; a browser-only bot gets no computer at all, and a bot whose
|
|
@@ -6988,6 +7404,32 @@ async function startTurn(botId, text, opts) {
|
|
|
6988
7404
|
previewCapture = localVmPreviewFor(localVmTarget, claimThreadId);
|
|
6989
7405
|
return { target: localVmTarget, runtime: localVm.runtime };
|
|
6990
7406
|
};
|
|
7407
|
+
// Issue #1369: a lazy claim that rejects must not leave its turn busy
|
|
7408
|
+
// behind a gate that can only refuse. Surface the terminal
|
|
7409
|
+
// computer-unavailable error, then end the turn. Fenced to this
|
|
7410
|
+
// dispatch generation and a still-busy thread, so a rejection landing
|
|
7411
|
+
// after settle — or after a newer turn owns the thread — never touches
|
|
7412
|
+
// the newer turn. The gate's refusals stay fail-closed for any screen
|
|
7413
|
+
// call racing this teardown; the turn-failed buzz and incident follow
|
|
7414
|
+
// the dispatch-failure rules (person-started turns only).
|
|
7415
|
+
const surfaceLazyClaimRejection = (label) => (failure) => {
|
|
7416
|
+
if (activeInternalGenerationByThread.get(threadId) !== resourceOwner.generation || !threadBusy(bot.id, threadId))
|
|
7417
|
+
return;
|
|
7418
|
+
const message = `computer unavailable — ${label} could not be claimed for this turn (${failure})`;
|
|
7419
|
+
store.appendMessage(threadId, {
|
|
7420
|
+
role: "bot",
|
|
7421
|
+
kind: "activity",
|
|
7422
|
+
tool: { name: `error: ${message.slice(0, 160)}`, ok: false },
|
|
7423
|
+
});
|
|
7424
|
+
if (opts?.automationSource === undefined && !opts?.commsDepth && !opts?.cardContinuation) {
|
|
7425
|
+
notify(buildNotification("turn-failed", bot, threadId, redactSecretsInText(message), { avatarUrl: bot.avatarUrl }));
|
|
7426
|
+
reportIncident({ kind: "could-not-start", bot, threadId, detail: message });
|
|
7427
|
+
// Claude settles the interrupt below as exit_before_result; the
|
|
7428
|
+
// completion fold must not report this failure a second time.
|
|
7429
|
+
resourceOwner.lazyClaimFailureReported = true;
|
|
7430
|
+
}
|
|
7431
|
+
void interruptDirectThread(bot.id, threadId).catch(() => { });
|
|
7432
|
+
};
|
|
6991
7433
|
// Explicit destinations are strict. In particular, Local VM must never
|
|
6992
7434
|
// fall through to host CUA and accidentally click on the user's Mac.
|
|
6993
7435
|
// The Local VM attach, shared by explicit "Local VM" and by Auto. Explicit
|
|
@@ -7030,6 +7472,7 @@ async function startTurn(botId, text, opts) {
|
|
|
7030
7472
|
owner: resourceOwner,
|
|
7031
7473
|
lazy: true,
|
|
7032
7474
|
label: "the Local VM",
|
|
7475
|
+
onRejected: surfaceLazyClaimRejection("the Local VM"),
|
|
7033
7476
|
claim: async () => {
|
|
7034
7477
|
await claimAutoLocalVm(threadId, localVmTarget);
|
|
7035
7478
|
// The dispatch-site poller start saw a null previewCapture
|
|
@@ -7108,6 +7551,7 @@ async function startTurn(botId, text, opts) {
|
|
|
7108
7551
|
startScreenPoller(bot.id, threadId, { computer: vpsCapture, ...(browserCapture ? { browser: browserCapture } : {}) }, { screenIsTheWork: touched });
|
|
7109
7552
|
}
|
|
7110
7553
|
},
|
|
7554
|
+
onRejected: surfaceLazyClaimRejection("the VPS computer"),
|
|
7111
7555
|
});
|
|
7112
7556
|
if ("integration" in mounted) {
|
|
7113
7557
|
integrations.localComputer = mounted.integration;
|
|
@@ -7122,8 +7566,8 @@ async function startTurn(botId, text, opts) {
|
|
|
7122
7566
|
}
|
|
7123
7567
|
}
|
|
7124
7568
|
}
|
|
7125
|
-
// Cloud is strict when selected.
|
|
7126
|
-
//
|
|
7569
|
+
// Cloud is strict when selected. Native Box and drivers with a Box
|
|
7570
|
+
// bridge may also reuse an already-ready Box on Auto.
|
|
7127
7571
|
if (teamComputer) {
|
|
7128
7572
|
const attached = await attachTeamBox(teamComputer, bot.id, resourceOwner, mountsCloudComputer, instance.driverKind === "boxAgent");
|
|
7129
7573
|
integrations.computer = attached.integration;
|
|
@@ -7303,11 +7747,12 @@ async function startTurn(botId, text, opts) {
|
|
|
7303
7747
|
}
|
|
7304
7748
|
// An Auto conversation remembers where its first turn landed, so later
|
|
7305
7749
|
// turns stay there and the composer can show it. Explicit settings are
|
|
7306
|
-
// not recorded:
|
|
7750
|
+
// not recorded: an auto pin yields to a later Works on change (which
|
|
7751
|
+
// sweeps it — store.clearAutoSurfacePins); a person's pin does not.
|
|
7307
7752
|
if (bot.computer === undefined && !teamComputer && opts?.runOn !== "cloud" && !plan.pinned) {
|
|
7308
7753
|
const used = mountedComputer ?? (integrations.browser ? "browser" : null);
|
|
7309
7754
|
if (used)
|
|
7310
|
-
store.patchTask(bot.id, threadId, { surface: used });
|
|
7755
|
+
store.patchTask(bot.id, threadId, { surface: used, surfaceSource: "auto" });
|
|
7311
7756
|
}
|
|
7312
7757
|
const computerSelection = computerSelectionTurns.get(threadId);
|
|
7313
7758
|
if (computerSelection)
|
|
@@ -7320,11 +7765,10 @@ async function startTurn(botId, text, opts) {
|
|
|
7320
7765
|
if (!markDirectTurnDispatching(bot.id, dispatchClaimId, threadId)) {
|
|
7321
7766
|
throw new DirectTurnSetupCancelled("turn stopped before dispatch");
|
|
7322
7767
|
}
|
|
7323
|
-
watchdog.watch(threadId, bot.id);
|
|
7324
7768
|
const computerPromptKind = computerKind === "vm"
|
|
7325
7769
|
? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared"
|
|
7326
7770
|
: computerKind === "box"
|
|
7327
|
-
? instance.driverKind === "boxAgent" ? "box-agent" : "box"
|
|
7771
|
+
? instance.driverKind === "boxAgent" ? "box-agent" : instance.adapter.capabilities.cloudComputerMcp ? "box-chat" : "box"
|
|
7328
7772
|
: computerKind === "vps"
|
|
7329
7773
|
? "vps"
|
|
7330
7774
|
: computerKind === "local"
|
|
@@ -7335,6 +7779,7 @@ async function startTurn(botId, text, opts) {
|
|
|
7335
7779
|
throw new DirectTurnSetupCancelled("Coordination access changed before dispatch");
|
|
7336
7780
|
}
|
|
7337
7781
|
const prompt = buildSystemPrompt(persona, liveBot?.soul ?? bot.soul ?? "", [
|
|
7782
|
+
{ id: "user-profile", label: "About the user", text: userProfileSystemPrompt(cfg.profile) },
|
|
7338
7783
|
// first after the soul: the block names agent tools, so it only goes
|
|
7339
7784
|
// to a turn whose engine actually mounted them (setupMode is already
|
|
7340
7785
|
// false when they are not — see agentsMounted above)
|
|
@@ -7368,6 +7813,7 @@ async function startTurn(botId, text, opts) {
|
|
|
7368
7813
|
{ id: "webhook", label: "Webhook provenance", text: opts?.automationSource === "webhook" ? WEBHOOK_PROMPT : "" },
|
|
7369
7814
|
{ id: "mentions", label: "Mentions", text: boundedCoordination && tagged.length ? `The user named these existing teammates: ${tagged.map(b => `${peerName(b.name)} (${b.id})`).join(", ")}. Use coordinate_bots when their contribution is needed; do not substitute native helper agents for these bots.` : mentionPrompt(tagged) },
|
|
7370
7815
|
]);
|
|
7816
|
+
turnPromptBytes.set(threadId, { stable: Buffer.byteLength(prompt.stable), volatile: Buffer.byteLength(prompt.volatile) });
|
|
7371
7817
|
runningTurnEngines.set(threadId, instance);
|
|
7372
7818
|
// The prompt carries the soul as saved now. If it changed during setup,
|
|
7373
7819
|
// decide again from what is actually sent — except on a continuation
|
|
@@ -7409,6 +7855,9 @@ async function startTurn(botId, text, opts) {
|
|
|
7409
7855
|
system: prompt.text,
|
|
7410
7856
|
systemStable: prompt.stable,
|
|
7411
7857
|
systemVolatile: prompt.volatile,
|
|
7858
|
+
// the mentions half describes this turn: identical consecutive tags
|
|
7859
|
+
// must still deliver their note (SendTurnInput.mentionTurn)
|
|
7860
|
+
mentionTurn: tagged.length > 0,
|
|
7412
7861
|
integrations,
|
|
7413
7862
|
mcpFromUserConfig: claudeUserMcpEnabled(cfg) && !managedPolicy.restrictsMcp(),
|
|
7414
7863
|
cwd,
|
|
@@ -7507,12 +7956,15 @@ async function startTurn(botId, text, opts) {
|
|
|
7507
7956
|
if (!ownsLatestGeneration)
|
|
7508
7957
|
return;
|
|
7509
7958
|
const message = e instanceof Error ? e.message : String(e);
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7959
|
+
// The wait already wrote its failure resolution; keep all dispatch
|
|
7960
|
+
// failure bookkeeping below without adding the same error twice.
|
|
7961
|
+
if (!(e instanceof ComputerWaitGaveUp))
|
|
7962
|
+
store.appendMessage(threadId, {
|
|
7963
|
+
role: "bot",
|
|
7964
|
+
kind: "activity",
|
|
7965
|
+
turnSucceeded: false,
|
|
7966
|
+
tool: { name: `error: ${message.slice(0, 160)}`, ok: false },
|
|
7967
|
+
});
|
|
7516
7968
|
// Worth a buzz for the same reason a routine failure is, and the rule
|
|
7517
7969
|
// notify.ts encodes: the bot is not working, and the cause is usually
|
|
7518
7970
|
// a setting only a person can change — an unattended user would
|
|
@@ -7791,8 +8243,9 @@ routines = new RoutineManager({
|
|
|
7791
8243
|
handoffs.forget(threadId);
|
|
7792
8244
|
}
|
|
7793
8245
|
},
|
|
7794
|
-
startTurn: (botId, threadId, prompt, runOn, triggerSource, onDispatchError) =>
|
|
7795
|
-
|
|
8246
|
+
startTurn: async (botId, threadId, prompt, runOn, triggerSource, onDispatchError) => {
|
|
8247
|
+
await startTurn(botId, prompt, { threadId, runOn, automationSource: triggerSource, onDispatchError });
|
|
8248
|
+
},
|
|
7796
8249
|
startGoal: async (groupId, threadId, prompt, coordinatorBotId, runId, _onDispatchError) => {
|
|
7797
8250
|
startGroupTurn(groupId, prompt, undefined, undefined, "goal", undefined, {
|
|
7798
8251
|
threadId,
|
|
@@ -7843,6 +8296,23 @@ routines = new RoutineManager({
|
|
|
7843
8296
|
notify(buildNotification("routine-deferred", notificationBot, routineSourceThread(run) ?? bot.threadId, detail));
|
|
7844
8297
|
},
|
|
7845
8298
|
});
|
|
8299
|
+
orgLibrary = new OrgLibrary({
|
|
8300
|
+
dataDir: DATA_DIR,
|
|
8301
|
+
store,
|
|
8302
|
+
routines: routines,
|
|
8303
|
+
skills: { list: listSkills, stamps: skillPackageStamps, setEnabled: setSkillEnabled, installOrg: installOrgSkill },
|
|
8304
|
+
presets: presetStore,
|
|
8305
|
+
// Runtime → Electron main, which posts the report to Admin. Plain Node
|
|
8306
|
+
// (no parentPort) keeps it in memory only.
|
|
8307
|
+
postState: (message) => utilityParentPort?.postMessage(message),
|
|
8308
|
+
});
|
|
8309
|
+
{
|
|
8310
|
+
// Set by the port listener above, which TypeScript cannot see run first.
|
|
8311
|
+
const early = pendingOrgLibraryRelay;
|
|
8312
|
+
if (early)
|
|
8313
|
+
orgLibrary.applyRelay(early.library);
|
|
8314
|
+
pendingOrgLibraryRelay = null;
|
|
8315
|
+
}
|
|
7846
8316
|
// The scheduler receipt and room transcript live in separate durable stores.
|
|
7847
8317
|
// If the process exited between those two writes, prefer the correlated
|
|
7848
8318
|
// RoutineRun's terminal truth; an uncorrelated manual goal is simply failed
|
|
@@ -7885,27 +8355,37 @@ if (recoveryOwners.length > 0) {
|
|
|
7885
8355
|
// Chat tools can prepare routine changes, but the harness applies them only
|
|
7886
8356
|
// after the user confirms a durable card. Keeping this beside the scheduler
|
|
7887
8357
|
// makes the card resolvable after an app restart without involving the model.
|
|
7888
|
-
async function cloudRoutineReadiness() {
|
|
8358
|
+
async function cloudRoutineReadiness(botId, threadId) {
|
|
8359
|
+
const bot = threadId ? store.projectBotForTask(botId, threadId) : store.bot(botId);
|
|
8360
|
+
if (!bot || bot.hidden)
|
|
8361
|
+
return { ready: false, reason: "The routine's target bot no longer exists." };
|
|
7889
8362
|
if (!box.boxConfigured(cfg)) {
|
|
7890
8363
|
return {
|
|
7891
8364
|
ready: false,
|
|
7892
|
-
reason: 'The Box
|
|
8365
|
+
reason: 'The Box cloud computer needs a working Box API key. For the bot’s configured computer, including a self-hosted VPS, set run_on="maus" instead.',
|
|
7893
8366
|
};
|
|
7894
8367
|
}
|
|
7895
|
-
const instance =
|
|
7896
|
-
if (!instance)
|
|
8368
|
+
const instance = turnInstance(bot, "cloud", threadId);
|
|
8369
|
+
if (!instance)
|
|
7897
8370
|
return { ready: false, reason: "The Cloud VM runner is unavailable. Restart OpenMausBot and try again." };
|
|
7898
|
-
}
|
|
7899
8371
|
try {
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
8372
|
+
if ((await instance.snapshot()).state !== "available") {
|
|
8373
|
+
return { ready: false, reason: "The target bot's model engine is not ready to use the Box cloud computer." };
|
|
8374
|
+
}
|
|
8375
|
+
const teamComputer = inheritedTeamComputer(bot);
|
|
8376
|
+
const ownerId = teamComputer ? teamComputerOwner(teamComputer.id) : bot.id;
|
|
8377
|
+
const machine = await box.findBox(cfg, ownerId);
|
|
8378
|
+
if (teamComputer && !machine) {
|
|
8379
|
+
return { ready: false, reason: "The team's Box computer is missing; explicitly create or retry it from the Team map." };
|
|
8380
|
+
}
|
|
8381
|
+
// Explicit Cloud may provision or wake the bot's own Box at dispatch.
|
|
8382
|
+
// This probe only checks its identity/account, without starting billing.
|
|
8383
|
+
return { ready: true };
|
|
7904
8384
|
}
|
|
7905
8385
|
catch (error) {
|
|
7906
8386
|
return {
|
|
7907
8387
|
ready: false,
|
|
7908
|
-
reason: `The
|
|
8388
|
+
reason: `The target bot's cloud computer could not be checked: ${redactSecretsInText(error instanceof Error ? error.message : String(error))}`,
|
|
7909
8389
|
};
|
|
7910
8390
|
}
|
|
7911
8391
|
}
|
|
@@ -7965,7 +8445,7 @@ async function deleteBotWithLifecycle(botId, revalidate = () => { }, setupReques
|
|
|
7965
8445
|
const botBoxDeletions = boxDeletionSnapshot().filter((entry) => entry.ownerBotId === bot.id);
|
|
7966
8446
|
if (botBoxRecovery.some((entry) => !entry.resolved)) {
|
|
7967
8447
|
return deletionResponse(409, {
|
|
7968
|
-
error: "finish reconciling this bot's pending cloud computer creation before deleting it — check
|
|
8448
|
+
error: "finish reconciling this bot's pending cloud computer creation before deleting it — check boat.dev, then retry Box setup",
|
|
7969
8449
|
});
|
|
7970
8450
|
}
|
|
7971
8451
|
// Bot deletion awaits VM/browser/provider cleanup. Claim the bot and
|
|
@@ -8083,7 +8563,7 @@ async function deleteBotWithLifecycle(botId, revalidate = () => { }, setupReques
|
|
|
8083
8563
|
// Deletion removes the thread before a late turn.completed can fold
|
|
8084
8564
|
// staged provider images into a message, so dispose them here.
|
|
8085
8565
|
for (const task of store.tasks(bot.id)) {
|
|
8086
|
-
|
|
8566
|
+
purgeTurnAttachmentsForThread(task.threadId);
|
|
8087
8567
|
clearTurnDigestState(task.threadId);
|
|
8088
8568
|
settleDirectFollowup(directTurnGenerationByThread.get(task.threadId));
|
|
8089
8569
|
directTurnGenerationByThread.delete(task.threadId);
|
|
@@ -8645,6 +9125,8 @@ setupRetry = 0) {
|
|
|
8645
9125
|
let retainRoomVmLease = false;
|
|
8646
9126
|
let roomSpeaker;
|
|
8647
9127
|
let providerDispatched = false;
|
|
9128
|
+
let setupStalled = false;
|
|
9129
|
+
let unregisterSetupStall = () => { };
|
|
8648
9130
|
// The speaker's own This computer / Cloud mount, and what it must give back.
|
|
8649
9131
|
let roomComputerKind = null;
|
|
8650
9132
|
let roomVpsBotId = null;
|
|
@@ -8696,9 +9178,7 @@ setupRetry = 0) {
|
|
|
8696
9178
|
const skills = availableSkills();
|
|
8697
9179
|
const selectedSkills = mergeSkills(selectBundledSkills(roomContext, instance.adapter.capabilities.phoneMcp === true ? ["phoneMcp"] : [], skills), selectBundledSkills(latestUser?.text ?? "", skillAuthoring ? ["skillAuthoring"] : [], skills));
|
|
8698
9180
|
if (selectedSkills.some((skill) => skill.manifest.requiredCapabilities.includes("phoneMcp"))) {
|
|
8699
|
-
|
|
8700
|
-
throw new Error("another thread is using the phone — wait for it to finish");
|
|
8701
|
-
integrations.phone = phoneIntegration();
|
|
9181
|
+
integrations.phone = phoneIntegration(bot.id, threadId, internalGeneration);
|
|
8702
9182
|
}
|
|
8703
9183
|
try {
|
|
8704
9184
|
if (bot.composio !== false && composio.configured(cfg) && instance.adapter.capabilities.composioMcp === true) {
|
|
@@ -8822,6 +9302,22 @@ setupRetry = 0) {
|
|
|
8822
9302
|
groupSpeakers.set(threadId, roomSpeaker);
|
|
8823
9303
|
store.patchGroup(readyGroup.id, { busyBotId: bot.id });
|
|
8824
9304
|
orchestration?.onClaimed?.();
|
|
9305
|
+
// The stall completion handler only exists once the provider turn runs
|
|
9306
|
+
// (registered at dispatch below), but this watch is armed at claim. Latch
|
|
9307
|
+
// a stall that fires during setup — browser, box, and VM mounts can all
|
|
9308
|
+
// wedge — so the dispatch site completes the turn instead of launching a
|
|
9309
|
+
// provider turn the person was already told was stopped. The finally
|
|
9310
|
+
// below unregisters the latch on every other exit.
|
|
9311
|
+
setupStalled = false;
|
|
9312
|
+
unregisterSetupStall = roomStallCompletions.register(threadId, () => {
|
|
9313
|
+
setupStalled = true;
|
|
9314
|
+
});
|
|
9315
|
+
// Watch from claim, not provider dispatch: room setup (connected-app
|
|
9316
|
+
// discovery, browser, box, and VM mounts) can wedge before any provider
|
|
9317
|
+
// event exists. The exits between here and dispatch either throw into the
|
|
9318
|
+
// finally's guarded cleanup or return through the revalidation block,
|
|
9319
|
+
// both of which settle the watch.
|
|
9320
|
+
watchdog.watch(threadId, bot.id);
|
|
8825
9321
|
// Connected-app discovery above can yield for a network round trip. A
|
|
8826
9322
|
// profile may be removed, or the browser feature switched off, during that
|
|
8827
9323
|
// window. Mint the capability only after this turn has synchronously
|
|
@@ -8867,7 +9363,7 @@ setupRetry = 0) {
|
|
|
8867
9363
|
return false;
|
|
8868
9364
|
}
|
|
8869
9365
|
if (roomTeamComputer) {
|
|
8870
|
-
const attached = await attachTeamBox(roomTeamComputer, readyBot.id, resourceOwner, instance.driverKind === "boxAgent", instance.driverKind === "boxAgent");
|
|
9366
|
+
const attached = await attachTeamBox(roomTeamComputer, readyBot.id, resourceOwner, instance.driverKind === "boxAgent" || instance.adapter.capabilities.cloudComputerMcp === true, instance.driverKind === "boxAgent");
|
|
8871
9367
|
if (isCancelled?.() || groupSpeakers.get(threadId) !== roomSpeaker ||
|
|
8872
9368
|
activeInternalGenerationByThread.get(threadId) !== internalGeneration)
|
|
8873
9369
|
return false;
|
|
@@ -8913,7 +9409,8 @@ setupRetry = 0) {
|
|
|
8913
9409
|
if (!box.boxConfigured(cfg))
|
|
8914
9410
|
throw new Error("Cloud box is not configured — add a Box API key or choose Local VM");
|
|
8915
9411
|
const remoteAgent = instance.driverKind === "boxAgent";
|
|
8916
|
-
const attached = await attachBotBox(readyBot, resourceOwner, { explicitCloud: true,
|
|
9412
|
+
const attached = await attachBotBox(readyBot, resourceOwner, { explicitCloud: true,
|
|
9413
|
+
canMount: remoteAgent || instance.adapter.capabilities.cloudComputerMcp === true, remoteAgent });
|
|
8917
9414
|
if (!roomSetupIsCurrent())
|
|
8918
9415
|
return false;
|
|
8919
9416
|
if (!attached?.integration)
|
|
@@ -9048,9 +9545,10 @@ setupRetry = 0) {
|
|
|
9048
9545
|
}
|
|
9049
9546
|
}
|
|
9050
9547
|
const roomSystem = buildSystemPrompt(system, store.bot(bot.id)?.soul ?? bot.soul ?? "", [
|
|
9548
|
+
{ id: "user-profile", label: "About the user", text: userProfileSystemPrompt(cfg.profile) },
|
|
9051
9549
|
{ id: "files", label: "File locations", text: workspace ? workspaceLocationsPrompt(bot.id, cwd, readyBot.cwd) : "" },
|
|
9052
9550
|
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
9053
|
-
{ id: "computer", label: "Computer", text: computerPrompt(roomTeamComputer || roomComputerKind === "box" ? instance.driverKind === "boxAgent" ? "box-agent" : "box" : roomVmTarget ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : roomComputerKind) },
|
|
9551
|
+
{ id: "computer", label: "Computer", text: computerPrompt(roomTeamComputer || roomComputerKind === "box" ? instance.driverKind === "boxAgent" ? "box-agent" : instance.adapter.capabilities.cloudComputerMcp ? "box-chat" : "box" : roomVmTarget ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : roomComputerKind) },
|
|
9054
9552
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(roomTeamComputer) },
|
|
9055
9553
|
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: roomTeamComputer ? "cloud" : roomVmTarget ? "vm" : surfaceOfComputerKind(roomComputerKind), browser: Boolean(integrations.browser) }, { note: roomPlan.note }) },
|
|
9056
9554
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
@@ -9089,6 +9587,7 @@ setupRetry = 0) {
|
|
|
9089
9587
|
store.setActivity(bot.id, "idle");
|
|
9090
9588
|
retryDelegationsWaitingOn(bot.id);
|
|
9091
9589
|
}
|
|
9590
|
+
watchdog.settle(threadId);
|
|
9092
9591
|
return false;
|
|
9093
9592
|
}
|
|
9094
9593
|
let replyText = "";
|
|
@@ -9157,13 +9656,27 @@ setupRetry = 0) {
|
|
|
9157
9656
|
deadline.setWaitingOnHuman(false);
|
|
9158
9657
|
});
|
|
9159
9658
|
deadline.start();
|
|
9659
|
+
// Swap the setup latch for the real completion handler. The swap is
|
|
9660
|
+
// synchronous with the dispatch below, so no stall can fall between
|
|
9661
|
+
// them. A stall latched during setup completes the turn here without a
|
|
9662
|
+
// provider turn: the stopped message is already on the thread, and the
|
|
9663
|
+
// watchdog's bounded cleanup owns releasing the claim.
|
|
9664
|
+
unregisterSetupStall();
|
|
9665
|
+
if (setupStalled) {
|
|
9666
|
+
// No provider turn was launched, so there is no ambiguous event
|
|
9667
|
+
// window to quarantine: marking a cancelled handshake here would
|
|
9668
|
+
// strand the next turn behind its TTL for no benefit. The readiness
|
|
9669
|
+
// stop path above skips the mark for the same reason.
|
|
9670
|
+
finish("stalled");
|
|
9671
|
+
return;
|
|
9672
|
+
}
|
|
9160
9673
|
unregisterStall = roomStallCompletions.register(threadId, () => {
|
|
9161
9674
|
abandonProviderTurn();
|
|
9162
9675
|
finish("stalled");
|
|
9163
9676
|
});
|
|
9164
|
-
watchdog.watch(threadId, bot.id);
|
|
9165
9677
|
onProviderHandshakeStarted?.();
|
|
9166
9678
|
providerDispatched = true;
|
|
9679
|
+
turnPromptBytes.set(threadId, { stable: Buffer.byteLength(roomSystem.stable), volatile: Buffer.byteLength(roomSystem.volatile) });
|
|
9167
9680
|
runningTurnEngines.set(threadId, instance);
|
|
9168
9681
|
guardTurnDispatch(instance.adapter.sendTurn({
|
|
9169
9682
|
threadId,
|
|
@@ -9379,11 +9892,12 @@ setupRetry = 0) {
|
|
|
9379
9892
|
if (!roomSpeaker && !isSpendCap && !isVariantError)
|
|
9380
9893
|
throw error;
|
|
9381
9894
|
const message = error instanceof Error ? error.message : "Local VM setup failed";
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9895
|
+
if (!(error instanceof ComputerWaitGaveUp))
|
|
9896
|
+
store.appendMessage(threadId, {
|
|
9897
|
+
role: "bot", kind: "activity",
|
|
9898
|
+
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
9899
|
+
tool: { name: `error: ${message}`, ok: false },
|
|
9900
|
+
});
|
|
9387
9901
|
onDispatchError?.(message);
|
|
9388
9902
|
if (orchestration) {
|
|
9389
9903
|
// A spend-cap refusal is deterministic: record it as its own
|
|
@@ -9398,12 +9912,14 @@ setupRetry = 0) {
|
|
|
9398
9912
|
finally {
|
|
9399
9913
|
// Covers connector/setup failures, cancellation before dispatch, and all
|
|
9400
9914
|
// other early returns that never produce a provider terminal event.
|
|
9915
|
+
unregisterSetupStall();
|
|
9401
9916
|
revokeInternalCapabilityGeneration(threadId, internalGeneration);
|
|
9402
9917
|
roomHandoffs.sourceSettled(internalGeneration, roomHandoffSourceSucceeded);
|
|
9403
9918
|
if (!retainRoomVmLease)
|
|
9404
9919
|
releaseRoomVmLease();
|
|
9405
9920
|
if (!providerDispatched && roomSpeaker && groupSpeakers.get(threadId) === roomSpeaker) {
|
|
9406
9921
|
groupSpeakers.delete(threadId);
|
|
9922
|
+
watchdog.settle(threadId);
|
|
9407
9923
|
if (store.group(group.id)?.busyBotId === bot.id)
|
|
9408
9924
|
store.patchGroup(group.id, { busyBotId: null });
|
|
9409
9925
|
if (store.bot(bot.id)?.busy) {
|
|
@@ -10015,7 +10531,7 @@ function roomPostEligibility(bot, group) {
|
|
|
10015
10531
|
}
|
|
10016
10532
|
const outsider = group.memberIds
|
|
10017
10533
|
.map((id) => store.bot(id))
|
|
10018
|
-
.find((member) => member && !canAccessTeam(bot, member.section));
|
|
10534
|
+
.find((member) => member && !canAccessTeam(bot, member.section) && !coordinatorSupervises(member, bot));
|
|
10019
10535
|
if (outsider) {
|
|
10020
10536
|
return {
|
|
10021
10537
|
ok: false,
|
|
@@ -10804,6 +11320,7 @@ function configStatus() {
|
|
|
10804
11320
|
const eventLogRetentionDays = threadEventLogRetentionDays(cfg);
|
|
10805
11321
|
return {
|
|
10806
11322
|
xai: { configured: Boolean(cfg.xai?.key) },
|
|
11323
|
+
mistral: { configured: Boolean(cfg.mistral?.key) },
|
|
10807
11324
|
anthropic: { configured: Boolean(cfg.anthropic?.key) },
|
|
10808
11325
|
// a fleet agent on this server means Settings → Workspaces has something to drive
|
|
10809
11326
|
fleet: { available: fleetAvailable(fleetSocketPath()) },
|
|
@@ -10831,12 +11348,14 @@ function configStatus() {
|
|
|
10831
11348
|
tts: tts.describeVoice(cfg),
|
|
10832
11349
|
imageGen: avatarImageStatus(cfg),
|
|
10833
11350
|
// not a secret — the sidebar shows it
|
|
10834
|
-
profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "" },
|
|
11351
|
+
profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "", aboutMe: cfg.profile?.aboutMe ?? "" },
|
|
10835
11352
|
// the enrolled organisation's read-only desktop policy; null when not enrolled
|
|
10836
11353
|
managedPolicy: managedPolicy.summary(),
|
|
10837
11354
|
// not a secret — the settings picker shows it; "" = follow the system
|
|
10838
11355
|
language: cfg.language ?? "",
|
|
10839
11356
|
rooms: { turnTimeoutMinutes: roomTurnTimeoutMinutes(cfg) },
|
|
11357
|
+
// absent effort = no level is sent, so clients can tell it from any level
|
|
11358
|
+
newBots: cfg.newBots?.effort ? { effort: cfg.newBots.effort } : {},
|
|
10840
11359
|
threads: {
|
|
10841
11360
|
maxConcurrentPerBot: maxConcurrentBotThreads(cfg),
|
|
10842
11361
|
...(eventLogMaxBytes !== null ? { eventLogMaxBytes } : {}),
|
|
@@ -11235,6 +11754,7 @@ const workspaceBackupRoutes = createWorkspaceBackupRoutes({
|
|
|
11235
11754
|
// Route modules (server/routes/README.md). `workspaceAccess` is assigned at
|
|
11236
11755
|
// boot, after this line, so the dependency reads it per request.
|
|
11237
11756
|
ROUTES.push(createHostedSlackRoutes({ bot: (id) => store.bot(id), hostedReady: () => Boolean(workspaceAccess) && entitled("admin") }));
|
|
11757
|
+
ROUTES.push(createBotPresetRoutes({ presets: presetStore, orgStatuses: readOrgInstallStatuses }));
|
|
11238
11758
|
const toolResults = new ToolResults();
|
|
11239
11759
|
const handleRequest = async (req, res) => {
|
|
11240
11760
|
let url;
|
|
@@ -11659,6 +12179,21 @@ const handleRequest = async (req, res) => {
|
|
|
11659
12179
|
// through a per-process high-entropy test key. The route does not exist
|
|
11660
12180
|
// unless the launcher explicitly sets that key; production builds never
|
|
11661
12181
|
// set it.
|
|
12182
|
+
// Isolated fixtures have no Electron parent to relay an organization's
|
|
12183
|
+
// library, so a test relays one here instead. Like the capability route
|
|
12184
|
+
// below, it exists only when the launcher sets its high-entropy key.
|
|
12185
|
+
if (method === "POST" && path === "/api/testing/org-library") {
|
|
12186
|
+
const expected = Buffer.from(process.env.OMB_TEST_ORG_LIBRARY_KEY ?? "");
|
|
12187
|
+
const header = req.headers["x-openmausbot-test-org-library"];
|
|
12188
|
+
const actual = Buffer.from(Array.isArray(header) ? "" : String(header ?? ""));
|
|
12189
|
+
if (expected.length < 32 || actual.length !== expected.length || !timingSafeEqual(actual, expected) || !orgLibrary) {
|
|
12190
|
+
return json(res, 404, { error: "not found" });
|
|
12191
|
+
}
|
|
12192
|
+
const body = await readBody(req);
|
|
12193
|
+
const applied = orgLibrary.applyRelay(body?.library ?? null);
|
|
12194
|
+
await orgLibrary.settled();
|
|
12195
|
+
return json(res, applied.ok ? 200 : 400, { ...applied, report: orgLibrary.lastReport() });
|
|
12196
|
+
}
|
|
11662
12197
|
if (method === "POST" && path === "/api/testing/internal-capability") {
|
|
11663
12198
|
const expected = process.env.OMB_TEST_INTERNAL_CAPABILITY_KEY ?? "";
|
|
11664
12199
|
const actual = Array.isArray(req.headers["x-openmausbot-test-capability"])
|
|
@@ -11708,11 +12243,13 @@ const handleRequest = async (req, res) => {
|
|
|
11708
12243
|
? "hooks"
|
|
11709
12244
|
: path === "/api/internal/browser/mcp"
|
|
11710
12245
|
? "browser"
|
|
11711
|
-
: path
|
|
11712
|
-
? "
|
|
11713
|
-
: path
|
|
11714
|
-
? "
|
|
11715
|
-
: "
|
|
12246
|
+
: path === "/api/internal/phone/claim"
|
|
12247
|
+
? "phone"
|
|
12248
|
+
: path.startsWith("/api/internal/connectors/")
|
|
12249
|
+
? "connectors"
|
|
12250
|
+
: path === "/api/internal/computer-control"
|
|
12251
|
+
? "computer"
|
|
12252
|
+
: "agents";
|
|
11716
12253
|
if (internalCapability.kind !== requiredCapabilityKind) {
|
|
11717
12254
|
return json(res, 403, { error: "this internal capability cannot access that service" });
|
|
11718
12255
|
}
|
|
@@ -11870,6 +12407,17 @@ const handleRequest = async (req, res) => {
|
|
|
11870
12407
|
requireActiveInternalCapability();
|
|
11871
12408
|
return json(res, 200, { result });
|
|
11872
12409
|
}
|
|
12410
|
+
if (method === "POST" && path === "/api/internal/phone/claim") {
|
|
12411
|
+
// Lazy phone exclusivity (issue #1663): the turn holds computer:phone
|
|
12412
|
+
// from its first real tool call until it settles. The proxy answers a
|
|
12413
|
+
// conflict as a blocked tool result, so a concurrent caller waits or
|
|
12414
|
+
// does other work instead of losing the whole turn.
|
|
12415
|
+
requireActiveInternalCapability();
|
|
12416
|
+
if (!claimTurnResource(internalCapability, "computer:phone")) {
|
|
12417
|
+
return json(res, 409, { error: "another thread is using the phone" });
|
|
12418
|
+
}
|
|
12419
|
+
return json(res, 200, { claimed: true });
|
|
12420
|
+
}
|
|
11873
12421
|
// Off by default: both fall through to the same "unknown internal
|
|
11874
12422
|
// endpoint" 404 a never-implemented route returns.
|
|
11875
12423
|
if (method === "GET" && path === "/api/internal/shared-computers" && sharedComputersEnabled(cfg))
|
|
@@ -12157,7 +12705,7 @@ const handleRequest = async (req, res) => {
|
|
|
12157
12705
|
return json(res, 200, {
|
|
12158
12706
|
teams: teamSetupTeams().filter((name) => canAccessTeam(chief, name)),
|
|
12159
12707
|
bots: store.bots.filter((bot) => !bot.hidden && canAccessTeam(chief, bot.section) && (bot.id === chief.id || peerAllowed(chief, bot)))
|
|
12160
|
-
.map((bot) => ({ id: bot.id, name: bot.name, title: bot.title, section: bot.section ?? "", modelSelection: bot.modelSelection })),
|
|
12708
|
+
.map((bot) => ({ id: bot.id, name: bot.name, title: bot.title, section: bot.section ?? "", modelSelection: bot.modelSelection, chiefOfStaff: Boolean(bot.chiefOfStaff) })),
|
|
12161
12709
|
instances: instances.map((instance) => ({ instanceId: instance.instanceId, driverKind: instance.driverKind, displayName: instance.displayName,
|
|
12162
12710
|
state: instance.snapshot.state, models: instance.models, effortLevels: instance.capabilities?.effortLevels ?? [] })),
|
|
12163
12711
|
scope: "Bot model defaults apply to groups and new threads; existing threads retain their models. Full Access applies requested team setup immediately; other modes return a review card. Existing unauthorized teams remain outside this Chief's scope.",
|
|
@@ -12497,14 +13045,14 @@ const handleRequest = async (req, res) => {
|
|
|
12497
13045
|
//
|
|
12498
13046
|
// per-bot approval gate: a chief-of-staff bot without this on is
|
|
12499
13047
|
// free to coordinate; one with it on must wait for a human card
|
|
12500
|
-
// (15-min timeout →
|
|
13048
|
+
// (15-min timeout → expired) before its peer turn starts. The channel
|
|
12501
13049
|
// and the chips are created only AFTER the verdict, so a denied
|
|
12502
13050
|
// contact leaves no trace of an exchange that never happened.
|
|
12503
13051
|
if (peerReviewRequired(from, fromThreadId)) {
|
|
12504
13052
|
const verdict = await requestPeerApproval(approvalBus, from, target, message, "ask_bot", fromThreadId);
|
|
12505
13053
|
requireActiveInternalCapability();
|
|
12506
13054
|
if (verdict !== "allow")
|
|
12507
|
-
return json(res, 200,
|
|
13055
|
+
return json(res, 200, peerApprovalFailure(verdict));
|
|
12508
13056
|
// The card may have been open for minutes. Re-read both records so
|
|
12509
13057
|
// deleted bots cannot recreate transcripts through stale objects.
|
|
12510
13058
|
const freshFrom = store.bot(fromBotId);
|
|
@@ -12614,7 +13162,12 @@ const handleRequest = async (req, res) => {
|
|
|
12614
13162
|
if (!sender || (target && !canReachPeer(sender, target))) {
|
|
12615
13163
|
return json(res, 403, { error: "Result withheld: team access changed while the teammate was working" });
|
|
12616
13164
|
}
|
|
12617
|
-
return json(res, 200, {
|
|
13165
|
+
return json(res, 200, {
|
|
13166
|
+
status: receipt.status, toBotName: receipt.toBotName, result: receipt.result ?? "",
|
|
13167
|
+
...(receipt.approvalOutcome ? {
|
|
13168
|
+
approvalOutcome: receipt.approvalOutcome, approvalSource: receipt.approvalSource,
|
|
13169
|
+
} : {}),
|
|
13170
|
+
});
|
|
12618
13171
|
}
|
|
12619
13172
|
const stillQueued = pendingDelegationInfo(taskId);
|
|
12620
13173
|
const runningEntry = [...delegationWatch.entries()].find(([, watch]) => watch.taskId === taskId);
|
|
@@ -12841,8 +13394,15 @@ const handleRequest = async (req, res) => {
|
|
|
12841
13394
|
if (peerReviewRequired(internalSender, address.threadId)) {
|
|
12842
13395
|
const verdicts = await Promise.all(targets.map(target => requestPeerApproval(approvalBus, internalSender, store.bot(target.botId), parsed.data.message, "delegate_bot", address.threadId)));
|
|
12843
13396
|
requireActiveInternalCapability();
|
|
12844
|
-
|
|
12845
|
-
|
|
13397
|
+
const blocked = verdicts.flatMap((verdict, index) => verdict === "allow" ? []
|
|
13398
|
+
: [{ botId: targets[index].botId, ...peerApprovalFailure(verdict) }]);
|
|
13399
|
+
if (blocked.length)
|
|
13400
|
+
return json(res, 403, {
|
|
13401
|
+
error: blocked.every(failure => failure.approvalOutcome === "deny")
|
|
13402
|
+
? "Denied by user; no work sent."
|
|
13403
|
+
: `${blocked.map(failure => failure.error).join("; ")}; no work sent.`,
|
|
13404
|
+
approvals: blocked,
|
|
13405
|
+
});
|
|
12846
13406
|
approvalGranted = true;
|
|
12847
13407
|
}
|
|
12848
13408
|
const accepted = [];
|
|
@@ -12878,7 +13438,7 @@ const handleRequest = async (req, res) => {
|
|
|
12878
13438
|
grantDelegatedFullAccess(internalSender, store.bot(target.botId), target.threadId);
|
|
12879
13439
|
}
|
|
12880
13440
|
}
|
|
12881
|
-
const { node, duplicate } = roomHandoffs.enqueue(address, internalCapability.generation, internalCapability.roomHandoffId, target, parsed.data.requestKey + ":" + target.botId, parsed.data.message, approvalGranted, parsed.data.rework, [...store.messagesFor(address.threadId)].reverse().find(m => m.role === "user" && m.kind === "text")?.text ?? "");
|
|
13441
|
+
const { node, duplicate } = roomHandoffs.enqueue(address, internalCapability.generation, internalCapability.roomHandoffId, target, parsed.data.requestKey + ":" + target.botId, parsed.data.message, approvalGranted, parsed.data.rework, [...store.messagesFor(address.threadId)].reverse().find(m => m.role === "user" && m.kind === "text")?.text ?? "", destination ? parsed.data.requestKey : undefined);
|
|
12882
13442
|
// A re-dispatched request_key is answered by the request it
|
|
12883
13443
|
// already made, so a thread resolved for the retry (the pair
|
|
12884
13444
|
// conversation was busy with that very request) goes back
|
|
@@ -12998,7 +13558,7 @@ const handleRequest = async (req, res) => {
|
|
|
12998
13558
|
const verdict = await requestPeerApproval(approvalBus, from, { id: room.id, name: room.name }, message, "post_to_room", fromThreadId);
|
|
12999
13559
|
requireActiveInternalCapability();
|
|
13000
13560
|
if (verdict !== "allow")
|
|
13001
|
-
return json(res, 200,
|
|
13561
|
+
return json(res, 200, peerApprovalFailure(verdict));
|
|
13002
13562
|
// The card may have been open for minutes. Re-read both records so a
|
|
13003
13563
|
// roster change, a section move, or a deletion during that window
|
|
13004
13564
|
// cannot be posted through on a stale decision.
|
|
@@ -13219,6 +13779,26 @@ const handleRequest = async (req, res) => {
|
|
|
13219
13779
|
if (instructions.length > 1_000) {
|
|
13220
13780
|
return json(res, 400, { error: "instructions must be at most 1000 characters" });
|
|
13221
13781
|
}
|
|
13782
|
+
let selection;
|
|
13783
|
+
if (body.modelSelection === undefined) {
|
|
13784
|
+
selection = await defaultSelection();
|
|
13785
|
+
}
|
|
13786
|
+
else {
|
|
13787
|
+
const checked = checkedModelSelection(body.modelSelection, undefined, true);
|
|
13788
|
+
if (!checked.ok)
|
|
13789
|
+
return json(res, checked.status, { error: checked.error });
|
|
13790
|
+
selection = checked.selection;
|
|
13791
|
+
}
|
|
13792
|
+
if (hostedModels && !hostedModels.allows(selection))
|
|
13793
|
+
return json(res, 400, { error: hostedModels.error() });
|
|
13794
|
+
// Discovery can yield; check current authority and capacity again before writing.
|
|
13795
|
+
if (store.bot(chief.id) !== chief || chief.hidden || !chief.chiefOfStaff || !connectorThread(chief.id, fromThreadId)) {
|
|
13796
|
+
return json(res, 403, { error: "only an active Chief of Staff can create operator bots" });
|
|
13797
|
+
}
|
|
13798
|
+
if (internalCapability.createdBots >= 4)
|
|
13799
|
+
return json(res, 429, { error: "you can create at most 4 bots in one turn" });
|
|
13800
|
+
if (store.bots.length >= MAX_WORKSPACE_BOTS)
|
|
13801
|
+
return json(res, 409, { error: `this workspace is limited to ${MAX_WORKSPACE_BOTS} bots` });
|
|
13222
13802
|
const duplicate = store.bots.find((candidate) => !candidate.hidden &&
|
|
13223
13803
|
sectionKey(candidate.section) === sectionKey(chief.section) &&
|
|
13224
13804
|
candidate.name.trim().toLowerCase() === name.toLowerCase());
|
|
@@ -13229,13 +13809,14 @@ const handleRequest = async (req, res) => {
|
|
|
13229
13809
|
name,
|
|
13230
13810
|
title: role,
|
|
13231
13811
|
description: instructions,
|
|
13232
|
-
modelSelection:
|
|
13812
|
+
modelSelection: selection,
|
|
13233
13813
|
section: chief.section,
|
|
13234
13814
|
// exactly the Chief's audience: a restricted Chief never makes a bot everyone sees
|
|
13235
13815
|
...(chief.visibility ? { visibility: chief.visibility } : {}),
|
|
13236
13816
|
}, { seedMessages: false });
|
|
13237
13817
|
const safeBot = store.patchBot(created.id, {
|
|
13238
13818
|
composio: false,
|
|
13819
|
+
connectorTools: {},
|
|
13239
13820
|
autoApprove: false,
|
|
13240
13821
|
approvePeerComms: false,
|
|
13241
13822
|
});
|
|
@@ -13246,6 +13827,7 @@ const handleRequest = async (req, res) => {
|
|
|
13246
13827
|
title: safeBot.title,
|
|
13247
13828
|
section: safeBot.section || "General",
|
|
13248
13829
|
model: safeBot.modelSelection.model,
|
|
13830
|
+
modelSelection: safeBot.modelSelection,
|
|
13249
13831
|
});
|
|
13250
13832
|
}
|
|
13251
13833
|
if (method === "POST" && (path === "/api/internal/create-room" || path === "/api/internal/manage-room")) {
|
|
@@ -13369,6 +13951,47 @@ const handleRequest = async (req, res) => {
|
|
|
13369
13951
|
});
|
|
13370
13952
|
return json(res, 201, { messageId: message.id, label: target.label });
|
|
13371
13953
|
}
|
|
13954
|
+
if (method === "POST" && path === "/api/internal/voice-note") {
|
|
13955
|
+
const body = await readInternalBody();
|
|
13956
|
+
const text = typeof body.text === "string" ? body.text.trim() : "";
|
|
13957
|
+
if (!text)
|
|
13958
|
+
return json(res, 400, { error: "text required" });
|
|
13959
|
+
if (text.length > 1000)
|
|
13960
|
+
return json(res, 413, { error: "voice notes are limited to 1000 characters" });
|
|
13961
|
+
// Advertisement is presentation only; the capability is re-checked
|
|
13962
|
+
// here so a call that reaches an unconfigured workspace answers with
|
|
13963
|
+
// the setup guidance instead of a provider failure.
|
|
13964
|
+
const voiceBot = botForThread(internalSender.id, internalCapability.threadId) ?? internalSender;
|
|
13965
|
+
try {
|
|
13966
|
+
const audio = await tts.speak(cfg, text, voiceBot.voice);
|
|
13967
|
+
// Synthesis can outlive its turn: a stop or settle during the await
|
|
13968
|
+
// revokes the capability and clears the live turn. Re-check both
|
|
13969
|
+
// before saving so the clip cannot park under the :active fallback
|
|
13970
|
+
// or onto a later turn's key (#1742).
|
|
13971
|
+
requireActiveInternalCapability();
|
|
13972
|
+
const turnId = liveTurnByThread.get(internalCapability.threadId);
|
|
13973
|
+
if (!turnId) {
|
|
13974
|
+
return json(res, 409, { error: "this turn has not started yet or already ended" });
|
|
13975
|
+
}
|
|
13976
|
+
const saved = saveAudio(Buffer.from(audio.bytes), audio.mime);
|
|
13977
|
+
const key = turnAttachmentKey(internalCapability.threadId, turnId);
|
|
13978
|
+
const current = turnAttachmentsByTurn.get(key) ?? [];
|
|
13979
|
+
current.push({ kind: "audio", path: saved.path, mime: saved.mime, text });
|
|
13980
|
+
turnAttachmentsByTurn.set(key, current);
|
|
13981
|
+
return json(res, 201, { attached: true });
|
|
13982
|
+
}
|
|
13983
|
+
catch (error) {
|
|
13984
|
+
// "no voice configured" is a setup state, not a provider failure —
|
|
13985
|
+
// 409 with the hint, exactly like /api/tts/speak.
|
|
13986
|
+
if (error instanceof tts.NoVoiceConfigured)
|
|
13987
|
+
return json(res, 409, { error: error.message });
|
|
13988
|
+
const status = error.status;
|
|
13989
|
+
if (typeof status === "number" && status >= 400 && status < 500) {
|
|
13990
|
+
return json(res, status, { error: error instanceof Error ? error.message : "voice note failed" });
|
|
13991
|
+
}
|
|
13992
|
+
throw error;
|
|
13993
|
+
}
|
|
13994
|
+
}
|
|
13372
13995
|
if (method === "POST" && path === "/api/internal/connectors/mcp") {
|
|
13373
13996
|
const body = await readInternalBody();
|
|
13374
13997
|
// Reading a streamed MCP body yields to ordinary settings requests.
|
|
@@ -13378,6 +14001,72 @@ const handleRequest = async (req, res) => {
|
|
|
13378
14001
|
if (!currentSender || currentSender.composio === false || !composio.configured(cfg)) {
|
|
13379
14002
|
return json(res, 403, { error: "connected apps are not enabled for this bot" });
|
|
13380
14003
|
}
|
|
14004
|
+
// Per-bot tool grants (issue #1736): verdict every tools/call frame
|
|
14005
|
+
// against the calling bot's connectorTools before it reaches
|
|
14006
|
+
// Composio. A bot with no grants record keeps the legacy all-tools
|
|
14007
|
+
// behavior; a grants record makes every unrecognized shape a deny.
|
|
14008
|
+
// Rows are fire-and-forget: a log failure must never take the call
|
|
14009
|
+
// (or its refusal) down with it.
|
|
14010
|
+
const call = connectorCallFromFrame(body);
|
|
14011
|
+
if (call.kind === "unrecognized") {
|
|
14012
|
+
appendDecision(DATA_DIR, {
|
|
14013
|
+
threadId: internalCapability.threadId,
|
|
14014
|
+
botId: currentSender.id,
|
|
14015
|
+
botName: currentSender.name,
|
|
14016
|
+
tool: call.invoked,
|
|
14017
|
+
summary: call.reason,
|
|
14018
|
+
decision: "user-denied",
|
|
14019
|
+
source: "connector-scope",
|
|
14020
|
+
rule: "connectorTools",
|
|
14021
|
+
});
|
|
14022
|
+
const refusal = connectorUnrecognizedText(call.invoked, call.reason);
|
|
14023
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
14024
|
+
return res.end(JSON.stringify({
|
|
14025
|
+
jsonrpc: "2.0",
|
|
14026
|
+
id: body.id ?? null,
|
|
14027
|
+
result: { content: [{ type: "text", text: refusal }], isError: true },
|
|
14028
|
+
}));
|
|
14029
|
+
}
|
|
14030
|
+
if (call.kind === "tools") {
|
|
14031
|
+
const verdict = evaluateConnectorTools(call.names, currentSender.connectorTools);
|
|
14032
|
+
if (!verdict.allowed) {
|
|
14033
|
+
for (const denial of verdict.denials) {
|
|
14034
|
+
appendDecision(DATA_DIR, {
|
|
14035
|
+
threadId: internalCapability.threadId,
|
|
14036
|
+
botId: currentSender.id,
|
|
14037
|
+
botName: currentSender.name,
|
|
14038
|
+
tool: denial.tool,
|
|
14039
|
+
summary: denial.service === null
|
|
14040
|
+
? "tool name does not name a service"
|
|
14041
|
+
: denial.onGrantedService
|
|
14042
|
+
? "tool is not in this service's grant"
|
|
14043
|
+
: "service is not granted",
|
|
14044
|
+
decision: "user-denied",
|
|
14045
|
+
source: "connector-scope",
|
|
14046
|
+
rule: denial.service ? "connectorTools." + denial.service : "connectorTools",
|
|
14047
|
+
});
|
|
14048
|
+
}
|
|
14049
|
+
const refusal = connectorRefusalText(verdict.denials);
|
|
14050
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
14051
|
+
return res.end(JSON.stringify({
|
|
14052
|
+
jsonrpc: "2.0",
|
|
14053
|
+
id: body.id ?? null,
|
|
14054
|
+
result: { content: [{ type: "text", text: refusal }], isError: true },
|
|
14055
|
+
}));
|
|
14056
|
+
}
|
|
14057
|
+
// One row per allowed call, naming the first target: the audit
|
|
14058
|
+
// trail reads "which bot ran what", not one row per tool.
|
|
14059
|
+
appendDecision(DATA_DIR, {
|
|
14060
|
+
threadId: internalCapability.threadId,
|
|
14061
|
+
botId: currentSender.id,
|
|
14062
|
+
botName: currentSender.name,
|
|
14063
|
+
tool: call.names[0],
|
|
14064
|
+
summary: ("allowed " + call.names.join(", ")).slice(0, 240),
|
|
14065
|
+
decision: "user-approved",
|
|
14066
|
+
source: "connector-scope",
|
|
14067
|
+
rule: verdict.rule,
|
|
14068
|
+
});
|
|
14069
|
+
}
|
|
13381
14070
|
const upstream = await composio.relayMcp(cfg, body, Array.isArray(req.headers["mcp-session-id"])
|
|
13382
14071
|
? req.headers["mcp-session-id"][0]
|
|
13383
14072
|
: req.headers["mcp-session-id"]);
|
|
@@ -13873,6 +14562,10 @@ const handleRequest = async (req, res) => {
|
|
|
13873
14562
|
}
|
|
13874
14563
|
};
|
|
13875
14564
|
if (method === "GET" && path === "/api/bots") {
|
|
14565
|
+
try {
|
|
14566
|
+
groupUsageReader.refresh();
|
|
14567
|
+
}
|
|
14568
|
+
catch { /* accounting must not block a snapshot */ }
|
|
13876
14569
|
const limit = pageSize(url.searchParams.get("messages"));
|
|
13877
14570
|
if (limit === null)
|
|
13878
14571
|
return json(res, 400, { error: "messages must be a non-negative whole number" });
|
|
@@ -14304,8 +14997,102 @@ const handleRequest = async (req, res) => {
|
|
|
14304
14997
|
return json(res, 201, { group: { ...publicGroupState(group), messages: [] } });
|
|
14305
14998
|
}
|
|
14306
14999
|
if (method === "POST" && path === "/api/teams/export") {
|
|
14307
|
-
|
|
15000
|
+
// Pictures the Share dialog prepared ride this body, so it is larger
|
|
15001
|
+
// than the default JSON limit (the document itself is capped at 4 MB).
|
|
15002
|
+
const body = await readBody(req, 8 * 1024 * 1024);
|
|
14308
15003
|
const profileName = cfg.profile?.name?.trim();
|
|
15004
|
+
if (body.format === "package" && body.version === 2 && body.kind === "library") {
|
|
15005
|
+
// Preset bots and their skills, no team: my New bot defaults as a
|
|
15006
|
+
// preset someone else picks in New bot (presets.ts has the allowlist).
|
|
15007
|
+
const optional = (value) => (typeof value === "string" ? value : undefined);
|
|
15008
|
+
const built = defaultsPresetFor(body, body.includeMemory === true);
|
|
15009
|
+
try {
|
|
15010
|
+
const exported = createLibraryPackageExport({
|
|
15011
|
+
name: optional(body.name),
|
|
15012
|
+
tagline: optional(body.tagline),
|
|
15013
|
+
summary: optional(body.summary),
|
|
15014
|
+
release: optional(body.release),
|
|
15015
|
+
notes: optional(body.notes),
|
|
15016
|
+
authorName: profileName,
|
|
15017
|
+
published: readPublishedLibrary(),
|
|
15018
|
+
preset: built.preset,
|
|
15019
|
+
skipped: built.skipped,
|
|
15020
|
+
});
|
|
15021
|
+
if (body.dryRun !== true)
|
|
15022
|
+
writePublishedLibrary(exported.published);
|
|
15023
|
+
return json(res, 200, {
|
|
15024
|
+
document: exported.document,
|
|
15025
|
+
filename: exported.filename,
|
|
15026
|
+
redacted: exported.redacted,
|
|
15027
|
+
skipped: exported.skipped,
|
|
15028
|
+
summary: packageSummary(exported.document),
|
|
15029
|
+
});
|
|
15030
|
+
}
|
|
15031
|
+
catch (error) {
|
|
15032
|
+
if (error instanceof TeamExportError)
|
|
15033
|
+
return json(res, error.status, { error: error.message });
|
|
15034
|
+
throw error;
|
|
15035
|
+
}
|
|
15036
|
+
}
|
|
15037
|
+
if (body.format === "package" && body.version === 2) {
|
|
15038
|
+
// One team, whole, except its chat history (package format v2).
|
|
15039
|
+
if (typeof body.team !== "string")
|
|
15040
|
+
return json(res, 400, { error: "Choose a team to share." });
|
|
15041
|
+
const team = body.team.trim();
|
|
15042
|
+
if (team && !store.sections.includes(team) && !store.bots.some((bot) => sectionKey(bot.section) === team)) {
|
|
15043
|
+
return json(res, 404, { error: "That team no longer exists." });
|
|
15044
|
+
}
|
|
15045
|
+
const optional = (value) => (typeof value === "string" ? value : undefined);
|
|
15046
|
+
const teamBots = store.bots.filter((bot) => !bot.hidden && sectionKey(bot.section) === team);
|
|
15047
|
+
const skills = collectTeamSkills(teamBots, body.skills);
|
|
15048
|
+
if (!skills.ok)
|
|
15049
|
+
return json(res, 400, { error: skills.error, choices: { skills: skills.available } });
|
|
15050
|
+
const avatars = body.avatars && typeof body.avatars === "object" && !Array.isArray(body.avatars)
|
|
15051
|
+
? body.avatars
|
|
15052
|
+
: undefined;
|
|
15053
|
+
// Off unless asked: New bot defaults are personal until shared.
|
|
15054
|
+
const defaults = body.includeDefaultsPreset === true ? defaultsPresetFor(body, body.includeMemory === true) : null;
|
|
15055
|
+
try {
|
|
15056
|
+
const exported = createTeamPackageExport({
|
|
15057
|
+
team,
|
|
15058
|
+
name: optional(body.name),
|
|
15059
|
+
tagline: optional(body.tagline),
|
|
15060
|
+
summary: optional(body.summary),
|
|
15061
|
+
release: optional(body.release),
|
|
15062
|
+
notes: optional(body.notes),
|
|
15063
|
+
authorName: profileName,
|
|
15064
|
+
bots: store.bots,
|
|
15065
|
+
groups: store.groups,
|
|
15066
|
+
routines: routines.listRoutines(),
|
|
15067
|
+
brief: readSectionContext(team)?.text,
|
|
15068
|
+
published: readPublishedTeam(team),
|
|
15069
|
+
skillsByBot: skills.skillsByBot,
|
|
15070
|
+
skillSelection: skills.all ? "all" : "chosen",
|
|
15071
|
+
memoryByBot: body.includeMemory === true ? new Map(teamBots.map((bot) => [bot.id, starterNotes(bot.id)])) : undefined,
|
|
15072
|
+
avatars,
|
|
15073
|
+
mcpServers: cfg.mcpServers ?? {},
|
|
15074
|
+
skipped: [...skills.skipped, ...(defaults?.skipped ?? [])],
|
|
15075
|
+
preset: defaults?.preset ?? null,
|
|
15076
|
+
});
|
|
15077
|
+
// A preview (the dialog's live counts) never records keys; saving
|
|
15078
|
+
// the file does, so renaming anything later keeps its identity.
|
|
15079
|
+
if (body.dryRun !== true)
|
|
15080
|
+
writePublishedTeam(team, exported.published);
|
|
15081
|
+
return json(res, 200, {
|
|
15082
|
+
document: exported.document,
|
|
15083
|
+
filename: exported.filename,
|
|
15084
|
+
redacted: exported.redacted,
|
|
15085
|
+
skipped: exported.skipped,
|
|
15086
|
+
summary: packageSummary(exported.document),
|
|
15087
|
+
choices: { skills: skills.available },
|
|
15088
|
+
});
|
|
15089
|
+
}
|
|
15090
|
+
catch (error) {
|
|
15091
|
+
if (error instanceof TeamExportError)
|
|
15092
|
+
return json(res, error.status, { error: error.message, choices: { skills: skills.available } });
|
|
15093
|
+
throw error;
|
|
15094
|
+
}
|
|
15095
|
+
}
|
|
14309
15096
|
const name = typeof body.name === "string" && body.name.trim()
|
|
14310
15097
|
? body.name.trim()
|
|
14311
15098
|
: profileName
|
|
@@ -14414,6 +15201,64 @@ const handleRequest = async (req, res) => {
|
|
|
14414
15201
|
}
|
|
14415
15202
|
return json(res, 200, { directory });
|
|
14416
15203
|
}
|
|
15204
|
+
// ── the organization library (server/org-library.ts) ─────────────────
|
|
15205
|
+
// Admin scope, as import is. With no organization every read answers
|
|
15206
|
+
// {organization: null} and nothing else changes.
|
|
15207
|
+
if (method === "GET" && path === "/api/org-library") {
|
|
15208
|
+
return json(res, 200, orgLibrary?.list() ?? { organization: null, packages: [] });
|
|
15209
|
+
}
|
|
15210
|
+
m = path.match(/^\/api\/org-library\/packages\/([a-f0-9-]{36})$/);
|
|
15211
|
+
if (m && method === "GET") {
|
|
15212
|
+
const outcome = orgLibrary?.preview(m[1]);
|
|
15213
|
+
if (!outcome)
|
|
15214
|
+
return json(res, 404, { error: "This package is not in your organization's library.", code: "not_listed" });
|
|
15215
|
+
return outcome.ok ? json(res, 200, outcome.value) : json(res, outcome.status, { error: outcome.error, code: outcome.code });
|
|
15216
|
+
}
|
|
15217
|
+
if (method === "POST" && path === "/api/org-library/add") {
|
|
15218
|
+
// One click, no confirmation: the person's click is the decision.
|
|
15219
|
+
const body = await readBody(req);
|
|
15220
|
+
const packageId = typeof body?.packageId === "string" && /^[a-f0-9-]{36}$/.test(body.packageId) ? body.packageId : null;
|
|
15221
|
+
if (!packageId)
|
|
15222
|
+
return json(res, 400, { error: "Choose a package from your organization's library." });
|
|
15223
|
+
if (!orgLibrary)
|
|
15224
|
+
return json(res, 404, { error: "This package is not in your organization's library.", code: "not_listed" });
|
|
15225
|
+
const outcome = orgLibrary.add(packageId, packageImportDeps(await defaultSelection()));
|
|
15226
|
+
if (!outcome.ok)
|
|
15227
|
+
return json(res, outcome.status, { error: outcome.error, code: outcome.code });
|
|
15228
|
+
if (outcome.value.alreadyAdded)
|
|
15229
|
+
return json(res, 200, outcome.value);
|
|
15230
|
+
const imported = outcome.value.result;
|
|
15231
|
+
return json(res, 201, {
|
|
15232
|
+
alreadyAdded: false,
|
|
15233
|
+
installId: imported.installId,
|
|
15234
|
+
name: imported.name,
|
|
15235
|
+
section: imported.section,
|
|
15236
|
+
bots: imported.bots.map((bot) => publicBot(store.bot(bot.id) ?? bot)),
|
|
15237
|
+
groups: imported.groups.map((created) => ({ ...publicGroupState(store.group(created.id) ?? created), messages: [] })),
|
|
15238
|
+
routines: imported.routines,
|
|
15239
|
+
offeredSkills: imported.offeredSkills,
|
|
15240
|
+
connections: imported.connections,
|
|
15241
|
+
skipped: imported.skipped,
|
|
15242
|
+
brief: imported.brief,
|
|
15243
|
+
notes: imported.notes,
|
|
15244
|
+
presets: imported.presets ?? [],
|
|
15245
|
+
});
|
|
15246
|
+
}
|
|
15247
|
+
if (method === "GET" && path === "/api/org-library/skills") {
|
|
15248
|
+
const botId = url.searchParams.get("botId") ?? undefined;
|
|
15249
|
+
return json(res, 200, orgLibrary?.offeredSkills(botId) ?? { organization: null, skills: [] });
|
|
15250
|
+
}
|
|
15251
|
+
if (method === "POST" && path === "/api/org-library/skills") {
|
|
15252
|
+
const body = await readBody(req);
|
|
15253
|
+
const parsed = z.object({ botId: z.string().min(1).max(128), installId: z.string().regex(/^[a-f0-9]{32}$/), name: z.string().min(1).max(64) })
|
|
15254
|
+
.safeParse(body);
|
|
15255
|
+
if (!parsed.success)
|
|
15256
|
+
return json(res, 400, { error: "Choose a bot and a skill from your organization's library." });
|
|
15257
|
+
if (!orgLibrary)
|
|
15258
|
+
return json(res, 404, { error: "This package is not in your organization's library.", code: "not_listed" });
|
|
15259
|
+
const outcome = orgLibrary.addOfferedSkill(parsed.data.botId, parsed.data.installId, parsed.data.name);
|
|
15260
|
+
return outcome.ok ? json(res, 201, { skill: outcome.value }) : json(res, outcome.status, { error: outcome.error, code: outcome.code });
|
|
15261
|
+
}
|
|
14417
15262
|
if (method === "POST" && path === "/api/teams/import") {
|
|
14418
15263
|
// Import is additive-only. A manifest is untrusted input (catalog,
|
|
14419
15264
|
// GitHub, a shared file), so it must be structurally unable to reach
|
|
@@ -14479,163 +15324,59 @@ const handleRequest = async (req, res) => {
|
|
|
14479
15324
|
return json(res, 400, { error: error instanceof Error ? error.message : "Backup could not be imported" });
|
|
14480
15325
|
}
|
|
14481
15326
|
}
|
|
15327
|
+
// Every package (v1 JSON, BotMRR Markdown, v2) is read by the one shared
|
|
15328
|
+
// parser as a FILE: it cannot claim a publisher, and its skills and
|
|
15329
|
+
// routines arrive off. A body field such as `trust` is just an unknown
|
|
15330
|
+
// field of the document and is stripped; only the organization library
|
|
15331
|
+
// module ever imports with trust "org".
|
|
14482
15332
|
let packageDocument = null;
|
|
14483
15333
|
let manifest = null;
|
|
14484
15334
|
try {
|
|
14485
15335
|
if (isBotPackage(body))
|
|
14486
|
-
packageDocument =
|
|
15336
|
+
packageDocument = parsePackageDocument(body, { trust: "file" });
|
|
14487
15337
|
else
|
|
14488
15338
|
manifest = parseTeamManifest(body);
|
|
14489
15339
|
}
|
|
14490
15340
|
catch (error) {
|
|
14491
15341
|
return json(res, 400, { error: error instanceof Error ? error.message : "Invalid bot package" });
|
|
14492
15342
|
}
|
|
14493
|
-
const
|
|
14494
|
-
const
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
:
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
// Names already in use, hidden bots included: an archived bot can be
|
|
14502
|
-
// un-archived later, and a revived duplicate would be just as
|
|
14503
|
-
// ambiguous then.
|
|
14504
|
-
const takenNames = new Set(store.bots.map((bot) => bot.name.trim().toLowerCase()));
|
|
14505
|
-
const memberIds = new Map();
|
|
14506
|
-
let group;
|
|
14507
|
-
let importSection;
|
|
15343
|
+
const deps = packageImportDeps(await defaultSelection());
|
|
15344
|
+
const options = {
|
|
15345
|
+
mode: importMode,
|
|
15346
|
+
cwd: projectCwd,
|
|
15347
|
+
room: url.searchParams.get("room") ?? undefined,
|
|
15348
|
+
...(importVisibility ? { visibility: importVisibility } : {}),
|
|
15349
|
+
};
|
|
15350
|
+
let imported;
|
|
14508
15351
|
try {
|
|
14509
|
-
const
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
//
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
const playbookByKey = new Map((pkg?.playbooks ?? []).map((playbook) => [playbook.key, playbook]));
|
|
14518
|
-
const packageSkillByName = new Map((pkg?.skills?.entries ?? []).map((skill) => [skill.name, skill]));
|
|
14519
|
-
for (const source of sourceMembers) {
|
|
14520
|
-
const member = source.member;
|
|
14521
|
-
// importedMemberProfile is the authority boundary: persona fields
|
|
14522
|
-
// only, colliding names numbered. seedMessages: false — an
|
|
14523
|
-
// imported bot must not open by greeting the user as though it
|
|
14524
|
-
// were new. composio: false — a shared persona never starts with
|
|
14525
|
-
// reach into the user's connected apps (absence would mean
|
|
14526
|
-
// allowed); the user can switch it on per bot after reading who
|
|
14527
|
-
// they got.
|
|
14528
|
-
const created = store.createBot({
|
|
14529
|
-
...importedMemberProfile(member, takenNames),
|
|
14530
|
-
modelSelection: selection,
|
|
14531
|
-
section: importSection,
|
|
14532
|
-
...(importVisibility ? { visibility: importVisibility } : {}),
|
|
14533
|
-
}, { seedMessages: false });
|
|
14534
|
-
importedBots.push(created);
|
|
14535
|
-
const installedPlaybooks = source.playbookKeys.flatMap((key) => {
|
|
14536
|
-
const playbook = playbookByKey.get(key);
|
|
14537
|
-
return playbook ? [{ ...playbook }] : [];
|
|
14538
|
-
});
|
|
14539
|
-
store.patchBot(created.id, {
|
|
14540
|
-
composio: false,
|
|
14541
|
-
...(installedPlaybooks.length ? { playbooks: installedPlaybooks } : {}),
|
|
14542
|
-
...(pkg
|
|
14543
|
-
? {
|
|
14544
|
-
installedPackage: {
|
|
14545
|
-
id: pkg.id,
|
|
14546
|
-
name: pkg.name,
|
|
14547
|
-
release: pkg.release,
|
|
14548
|
-
requiredApps: pkg.requirements.apps.map((app) => ({ ...app })),
|
|
14549
|
-
},
|
|
14550
|
-
}
|
|
14551
|
-
: {}),
|
|
14552
|
-
});
|
|
14553
|
-
for (const skillName of source.skillNames) {
|
|
14554
|
-
const skill = packageSkillByName.get(skillName);
|
|
14555
|
-
if (!skill)
|
|
14556
|
-
throw new Error(`Package skill "${skillName}" is unavailable`);
|
|
14557
|
-
const installed = installSkill(created.id, skill.source ?? `package:${pkg.id}`, [
|
|
14558
|
-
{ path: "SKILL.md", content: skill.instructions },
|
|
14559
|
-
]);
|
|
14560
|
-
if ("error" in installed) {
|
|
14561
|
-
throw new Error(`Package skill "${skillName}" could not be imported: ${installed.error}`);
|
|
14562
|
-
}
|
|
14563
|
-
}
|
|
14564
|
-
memberIds.set(member.key, created.id);
|
|
14565
|
-
}
|
|
14566
|
-
// A package is an explicit structure import: its rooms are created
|
|
14567
|
-
// from package-local keys only, then normalized to fresh bot ids.
|
|
14568
|
-
for (const room of pkg?.rooms ?? []) {
|
|
14569
|
-
const ids = room.members.map((key) => memberIds.get(key));
|
|
14570
|
-
let created = store.createGroup(room.name, ids, false, importSection);
|
|
14571
|
-
createdGroups.push(created);
|
|
14572
|
-
const defaultResponder = room.defaultResponder.kind === "agent"
|
|
14573
|
-
? { kind: "member", botId: memberIds.get(room.defaultResponder.agent) }
|
|
14574
|
-
: { kind: room.defaultResponder.kind };
|
|
14575
|
-
created = store.patchGroup(created.id, {
|
|
14576
|
-
bulletin: room.bulletin ?? "",
|
|
14577
|
-
defaultResponder,
|
|
14578
|
-
setupCompletedAt: Date.now(),
|
|
14579
|
-
}) ?? created;
|
|
14580
|
-
}
|
|
14581
|
-
for (const routine of pkg?.routines ?? []) {
|
|
14582
|
-
const created = routines.create({
|
|
14583
|
-
name: routine.name,
|
|
14584
|
-
prompt: routine.prompt,
|
|
14585
|
-
botId: memberIds.get(routine.agent),
|
|
14586
|
-
runOn: routine.runOn,
|
|
14587
|
-
enabled: false,
|
|
14588
|
-
schedule: routine.schedule,
|
|
14589
|
-
durationMinutes: routine.durationMinutes,
|
|
14590
|
-
...(routine.timeoutMinutes === undefined ? {} : { timeoutMinutes: routine.timeoutMinutes }),
|
|
14591
|
-
...(routine.overlap ? { overlap: routine.overlap } : {}),
|
|
14592
|
-
});
|
|
14593
|
-
createdRoutineIds.push(created.id);
|
|
14594
|
-
}
|
|
14595
|
-
if (pkg?.chiefOfStaff) {
|
|
14596
|
-
store.setChiefOfStaff(memberIds.get(pkg.chiefOfStaff));
|
|
14597
|
-
}
|
|
14598
|
-
// The room is created last, so a failure anywhere above leaves no
|
|
14599
|
-
// half-built project behind — the catch below deletes the bots and
|
|
14600
|
-
// there is no room pointing at them.
|
|
14601
|
-
if (!pkg && importMode === "project" && importedBots.length > 0) {
|
|
14602
|
-
const roomName = url.searchParams.get("room")?.trim() || manifest.team.name;
|
|
14603
|
-
group = store.createGroup(roomName, importedBots.map((bot) => bot.id), false, importSection);
|
|
14604
|
-
createdGroups.push(group);
|
|
14605
|
-
if (projectCwd) {
|
|
14606
|
-
// `cwd` is the folder the room WANTS; the store pins it on the
|
|
14607
|
-
// first turn (pinGroupCwd). Setting the pin here would decide it
|
|
14608
|
-
// before anyone has worked, which is the store's call, not ours.
|
|
14609
|
-
group = store.patchGroup(group.id, { cwd: projectCwd }) ?? group;
|
|
14610
|
-
}
|
|
14611
|
-
broadcast({ kind: "group", group: publicGroupState(group) });
|
|
14612
|
-
}
|
|
14613
|
-
const publicBots = importedBots.map((bot) => publicBot(store.bot(bot.id)));
|
|
14614
|
-
for (const bot of publicBots)
|
|
14615
|
-
broadcast({ kind: "bot", bot });
|
|
14616
|
-
return json(res, 201, {
|
|
14617
|
-
name: importName,
|
|
14618
|
-
bots: publicBots,
|
|
14619
|
-
group,
|
|
14620
|
-
groups: createdGroups.map((created) => ({ ...created, messages: [] })),
|
|
14621
|
-
routines: createdRoutineIds.flatMap((id) => routines.listRoutines().filter((routine) => routine.id === id)),
|
|
14622
|
-
});
|
|
15352
|
+
const result = packageDocument
|
|
15353
|
+
? importPackageDocument(packageDocument, { ...options, trust: "file" }, deps)
|
|
15354
|
+
: importTeamManifest(manifest, options, deps);
|
|
15355
|
+
// A file import always gets a fresh install id, so it never reports
|
|
15356
|
+
// "already added"; that answer belongs to the organization library.
|
|
15357
|
+
if (result.alreadyAdded)
|
|
15358
|
+
return json(res, 200, result);
|
|
15359
|
+
imported = result;
|
|
14623
15360
|
}
|
|
14624
15361
|
catch (error) {
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
for (const routineId of createdRoutineIds)
|
|
14628
|
-
routines.remove(routineId);
|
|
14629
|
-
for (const created of createdGroups)
|
|
14630
|
-
store.deleteGroup(created.id);
|
|
14631
|
-
for (const bot of importedBots)
|
|
14632
|
-
store.deleteBot(bot.id);
|
|
14633
|
-
// Empty teams are now durable too. This import allocated a fresh
|
|
14634
|
-
// identity, so its failed installation must retire that identity.
|
|
14635
|
-
if (importSection && store.sections.includes(importSection))
|
|
14636
|
-
store.changeEmptySection(importSection, null);
|
|
15362
|
+
if (error instanceof PackageImportError)
|
|
15363
|
+
return json(res, error.status, { error: error.message, code: error.code });
|
|
14637
15364
|
throw error;
|
|
14638
15365
|
}
|
|
15366
|
+
return json(res, 201, {
|
|
15367
|
+
name: imported.name,
|
|
15368
|
+
section: imported.section,
|
|
15369
|
+
bots: imported.bots.map((bot) => publicBot(store.bot(bot.id) ?? bot)),
|
|
15370
|
+
group: imported.group,
|
|
15371
|
+
groups: imported.groups.map((created) => ({ ...created, messages: [] })),
|
|
15372
|
+
routines: imported.routines,
|
|
15373
|
+
offeredSkills: imported.offeredSkills,
|
|
15374
|
+
connections: imported.connections,
|
|
15375
|
+
skipped: imported.skipped,
|
|
15376
|
+
brief: imported.brief,
|
|
15377
|
+
notes: imported.notes,
|
|
15378
|
+
presets: imported.presets ?? [],
|
|
15379
|
+
});
|
|
14639
15380
|
}
|
|
14640
15381
|
m = path.match(/^\/api\/groups\/([\w-]+)\/setup$/);
|
|
14641
15382
|
if (m && method === "PATCH") {
|
|
@@ -15187,8 +15928,8 @@ const handleRequest = async (req, res) => {
|
|
|
15187
15928
|
const section = url.searchParams.get("section")?.trim();
|
|
15188
15929
|
if (!section)
|
|
15189
15930
|
return json(res, 400, { error: "Choose a named team" });
|
|
15190
|
-
if (teamComputers.forSection(section))
|
|
15191
|
-
return json(res, 409, { error: "Unassign this team's computer before
|
|
15931
|
+
if (method === "DELETE" && teamComputers.forSection(section))
|
|
15932
|
+
return json(res, 409, { error: "Unassign this team's computer before deleting the team" });
|
|
15192
15933
|
let nextName = null;
|
|
15193
15934
|
if (method === "PATCH") {
|
|
15194
15935
|
const parsed = z.object({ name: z.string().trim().min(1).max(60) }).strict().safeParse(await readBody(req));
|
|
@@ -15196,11 +15937,33 @@ const handleRequest = async (req, res) => {
|
|
|
15196
15937
|
return json(res, 400, { error: "Team name must be 1 to 60 characters" });
|
|
15197
15938
|
nextName = parsed.data.name;
|
|
15198
15939
|
}
|
|
15199
|
-
const error = store.
|
|
15940
|
+
const error = nextName === null ? store.deleteSection(section) : store.renameSection(section, nextName, teamComputers);
|
|
15200
15941
|
if (error)
|
|
15201
15942
|
return json(res, error === "No such team" ? 404 : 409, { error });
|
|
15202
15943
|
return json(res, 200, { sections: store.sections });
|
|
15203
15944
|
}
|
|
15945
|
+
if (method === "PUT" && path === "/api/sidebar-sections") {
|
|
15946
|
+
const section = url.searchParams.get("section")?.trim();
|
|
15947
|
+
if (!section || !store.sections.includes(section))
|
|
15948
|
+
return json(res, 404, { error: "No such team" });
|
|
15949
|
+
const ids = z.array(z.string().regex(/^[\w-]+$/)).max(MAX_WORKSPACE_BOTS);
|
|
15950
|
+
const parsed = z.object({ addBotIds: ids, removeBotIds: ids }).strict().safeParse(await readBody(req));
|
|
15951
|
+
if (!parsed.success)
|
|
15952
|
+
return json(res, 400, { error: "Provide addBotIds and removeBotIds" });
|
|
15953
|
+
for (const [botIds, destination] of [[parsed.data.addBotIds, section], [parsed.data.removeBotIds, undefined]]) {
|
|
15954
|
+
for (const id of botIds) {
|
|
15955
|
+
const bot = store.bot(id);
|
|
15956
|
+
if (bot)
|
|
15957
|
+
assertTeamComputerChangeIdle(bot, { ...bot, section: destination });
|
|
15958
|
+
}
|
|
15959
|
+
}
|
|
15960
|
+
const result = store.updateTeamMembers(section, parsed.data.addBotIds, parsed.data.removeBotIds);
|
|
15961
|
+
if (!result.ok)
|
|
15962
|
+
return json(res, result.reason === "unavailable" ? 404 : 409, { error: result.reason === "chief-conflict" ? "A team can have only one Chief of Staff. Change the Chief before moving this bot."
|
|
15963
|
+
: result.reason === "membership-changed" ? "Team membership changed. Reopen the dialog and try again."
|
|
15964
|
+
: "One or more bots are unavailable" });
|
|
15965
|
+
return json(res, 200, { sections: store.sections, bots: result.bots.map(wireBot) });
|
|
15966
|
+
}
|
|
15204
15967
|
if (method === "POST" && path === "/api/sidebar-sections") {
|
|
15205
15968
|
const parsed = createSidebarSectionSchema.safeParse(await readBody(req));
|
|
15206
15969
|
if (!parsed.success) {
|
|
@@ -15229,11 +15992,108 @@ const handleRequest = async (req, res) => {
|
|
|
15229
15992
|
}
|
|
15230
15993
|
return json(res, 200, { section: name, sections: store.sections, bots: result.bots.map(wireBot) });
|
|
15231
15994
|
}
|
|
15232
|
-
if (method === "
|
|
15995
|
+
if (method === "GET" && path === "/api/bot-defaults") {
|
|
15996
|
+
return json(res, 200, {
|
|
15997
|
+
defaults: structuredClone(cfg.newBotDefaults ?? newBotDefaultsSchema.parse({})),
|
|
15998
|
+
modelSelection: await defaultSelection(),
|
|
15999
|
+
suggestedName: pickBotName(store.bots.map(bot => bot.name)),
|
|
16000
|
+
});
|
|
16001
|
+
}
|
|
16002
|
+
if (method === "POST" && path === "/api/bot-defaults/skills/preview") {
|
|
16003
|
+
const checked = z.object({ source: z.string().min(1).max(2000) }).strict().safeParse(await readBody(req));
|
|
16004
|
+
if (!checked.success)
|
|
16005
|
+
return json(res, 400, { error: checked.error.message });
|
|
16006
|
+
const input = checked.data;
|
|
16007
|
+
const fetched = await fetchSkillFromSource(input.source);
|
|
16008
|
+
if ("error" in fetched)
|
|
16009
|
+
return json(res, 422, { error: fetched.error });
|
|
16010
|
+
const skills = [];
|
|
16011
|
+
for (const skill of fetched.skills) {
|
|
16012
|
+
const file = skill.files.find(file => file.path === "SKILL.md" || file.path.endsWith("/SKILL.md"));
|
|
16013
|
+
if (!file)
|
|
16014
|
+
continue;
|
|
16015
|
+
const parsed = parseSkillMd(file.content);
|
|
16016
|
+
if ("error" in parsed)
|
|
16017
|
+
return json(res, 422, { error: parsed.error });
|
|
16018
|
+
const skippedWarnings = skill.files.filter(candidate => candidate !== file)
|
|
16019
|
+
.map(candidate => `skipped supporting file "${candidate.path}" — v1 imports only SKILL.md`);
|
|
16020
|
+
const template = botSkillTemplateSchema.safeParse({ name: parsed.name, description: parsed.description, source: skill.source, text: file.content,
|
|
16021
|
+
enabled: false, warnings: [...scanSkillText(file.content), ...skippedWarnings] });
|
|
16022
|
+
if (!template.success)
|
|
16023
|
+
return json(res, 422, { error: template.error.message });
|
|
16024
|
+
skills.push(template.data);
|
|
16025
|
+
}
|
|
16026
|
+
return json(res, 200, { skills });
|
|
16027
|
+
}
|
|
16028
|
+
if (method === "POST" && path === "/api/bot-defaults/avatar") {
|
|
15233
16029
|
const body = await readBody(req);
|
|
16030
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
16031
|
+
return json(res, 400, { error: "avatar preview must be a JSON object" });
|
|
16032
|
+
const profile = parseBotProfilePatch(body.profile, true);
|
|
16033
|
+
if (!profile.ok)
|
|
16034
|
+
return json(res, 400, { error: profile.error });
|
|
16035
|
+
const checked = avatarGenerationRequestSchema.safeParse({ prompt: body.prompt });
|
|
16036
|
+
if (!checked.success)
|
|
16037
|
+
return json(res, 400, { error: checked.error.message });
|
|
16038
|
+
const { prompt } = checked.data;
|
|
16039
|
+
const generated = await generateAvatarImage(cfg, {
|
|
16040
|
+
name: profile.patch.name ?? "", title: profile.patch.title ?? "", description: profile.patch.description ?? "",
|
|
16041
|
+
}, prompt);
|
|
16042
|
+
// A preview belongs to the draft. Upload it through the existing image
|
|
16043
|
+
// endpoint only when the user commits the new bot or its defaults.
|
|
16044
|
+
return json(res, 200, { dataUrl: `data:${generated.mime};base64,${generated.bytes.toString("base64")}` });
|
|
16045
|
+
}
|
|
16046
|
+
if (method === "POST" && path === "/api/bots") {
|
|
16047
|
+
let body = await readBody(req);
|
|
15234
16048
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
15235
16049
|
return json(res, 400, { error: "bot must be a JSON object" });
|
|
15236
16050
|
}
|
|
16051
|
+
const template = resolveBotCreationDefaults(cfg.newBotDefaults, body);
|
|
16052
|
+
const settings = template.profile;
|
|
16053
|
+
if (auth.kind === "session" && !auth.scopes.includes("admin")) {
|
|
16054
|
+
const field = clientBotPatchViolation(settings);
|
|
16055
|
+
if (field || body.preset !== undefined || template.skills.length || template.routines.length || Object.keys(template.memory).length) {
|
|
16056
|
+
return json(res, 403, { error: "Creating a bot with these defaults needs the admin scope" });
|
|
16057
|
+
}
|
|
16058
|
+
}
|
|
16059
|
+
// A preset (presets.ts) supplies content: playbooks, skills and
|
|
16060
|
+
// starter notes. Name, look and instructions stay the request's own.
|
|
16061
|
+
if (body.preset !== undefined && (typeof body.preset !== "string" || !/^[\w-]{1,64}$/.test(body.preset))) {
|
|
16062
|
+
return json(res, 400, { error: "preset must be a preset id from New bot" });
|
|
16063
|
+
}
|
|
16064
|
+
const resolvedPreset = typeof body.preset === "string" ? presetStore.resolve(body.preset) : null;
|
|
16065
|
+
const preset = resolvedPreset && presetOffered(resolvedPreset.row, readOrgInstallStatuses()) ? resolvedPreset : null;
|
|
16066
|
+
if (body.preset !== undefined && !preset)
|
|
16067
|
+
return json(res, 404, { error: PRESET_UNAVAILABLE_MESSAGE });
|
|
16068
|
+
body = { ...body, ...settings, name: settings.name };
|
|
16069
|
+
if (settings.approvalMode === "full" || settings.approvalMode === "custom") {
|
|
16070
|
+
return json(res, 403, { error: "This approval level requires the desktop permission flow. Use the bot creation dialog or explicitly choose Ask/Auto for API creation." });
|
|
16071
|
+
}
|
|
16072
|
+
if (settings.computer === "local" && settings.approvalMode === "auto" && body.acknowledgeLocalAuto !== true) {
|
|
16073
|
+
return json(res, 400, { error: "Auto mode on this computer requires confirming the warning first (acknowledgeLocalAuto)" });
|
|
16074
|
+
}
|
|
16075
|
+
if (settings.avatarUrl && !storedAvatarExists(settings.avatarUrl)) {
|
|
16076
|
+
return json(res, 400, { error: "avatarUrl must reference an existing stored image" });
|
|
16077
|
+
}
|
|
16078
|
+
if (settings.browserProfile && settings.browserProfile !== "guest" && !(cfg.browserProfiles ?? []).some(profile => profile.id === settings.browserProfile)) {
|
|
16079
|
+
return json(res, 400, { error: "browserProfile must name an existing browser profile" });
|
|
16080
|
+
}
|
|
16081
|
+
if (settings.mcpServers?.some(name => mcpServerNameError(name) !== null) || (settings.mcpServers?.length ?? 0) > MAX_MCP_SERVERS) {
|
|
16082
|
+
return json(res, 400, { error: "Invalid MCP server selection" });
|
|
16083
|
+
}
|
|
16084
|
+
if (settings.managedSections?.length) {
|
|
16085
|
+
if (!settings.chiefOfStaff || settings.managedSections.some(name => name !== "" && !store.sections.includes(name))) {
|
|
16086
|
+
return json(res, 400, { error: "Only a Chief may manage existing additional teams" });
|
|
16087
|
+
}
|
|
16088
|
+
if (body.acknowledgePeerScope !== true)
|
|
16089
|
+
return json(res, 400, { error: "Confirm additional team access (acknowledgePeerScope)" });
|
|
16090
|
+
if (auth.kind === "loopback" && !DESKTOP_MANAGED && store.bots.some(bot => bot.busy)) {
|
|
16091
|
+
return json(res, 409, { error: "Stop running bots before granting access to another team" });
|
|
16092
|
+
}
|
|
16093
|
+
}
|
|
16094
|
+
const checkedCwd = validateBotCwd(settings.cwd ?? null);
|
|
16095
|
+
if (!checkedCwd.ok)
|
|
16096
|
+
return json(res, 400, { error: checkedCwd.error });
|
|
15237
16097
|
if (body.requireAvailableModel !== undefined && typeof body.requireAvailableModel !== "boolean") {
|
|
15238
16098
|
return json(res, 400, { error: "requireAvailableModel must be true or false" });
|
|
15239
16099
|
}
|
|
@@ -15272,14 +16132,91 @@ const handleRequest = async (req, res) => {
|
|
|
15272
16132
|
if (store.bots.length >= MAX_WORKSPACE_BOTS) {
|
|
15273
16133
|
return json(res, 409, { error: `this workspace is limited to ${MAX_WORKSPACE_BOTS} bots` });
|
|
15274
16134
|
}
|
|
15275
|
-
//
|
|
15276
|
-
// create bots). A bot named for a sensitive job must never be
|
|
15277
|
-
// announced to everyone before it is restricted.
|
|
16135
|
+
// Apply the audience before creation can announce the bot to members.
|
|
15278
16136
|
const created = body.visibility === undefined ? null : parseVisibility(body.visibility);
|
|
15279
16137
|
if (created && !created.ok)
|
|
15280
16138
|
return json(res, 400, { error: created.error });
|
|
15281
|
-
const bot = store.createBot({ ...profile.patch, section, modelSelection: selection,
|
|
16139
|
+
const bot = store.createBot({ ...profile.patch, soul: settings.soul, section, modelSelection: selection,
|
|
16140
|
+
...(created?.ok ? { visibility: created.visibility } : {}) });
|
|
16141
|
+
const createdRoutines = [];
|
|
16142
|
+
try {
|
|
16143
|
+
const { chiefOfStaff: _chief, managedSections: _managed, ...ordinary } = settings;
|
|
16144
|
+
store.patchBot(bot.id, {
|
|
16145
|
+
// Store creation completes workspace defaults (including effort).
|
|
16146
|
+
// Applying the rest of the template must not undo that selection.
|
|
16147
|
+
...ordinary, name: bot.name, section, modelSelection: bot.modelSelection,
|
|
16148
|
+
mascotExpression: ordinary.mascotExpression ?? undefined,
|
|
16149
|
+
avatarUrl: ordinary.avatarUrl || undefined,
|
|
16150
|
+
computer: ordinary.computer ?? undefined, cwd: checkedCwd.cwd ?? undefined,
|
|
16151
|
+
peers: ordinary.peers ?? undefined, mcpServers: ordinary.mcpServers ?? undefined,
|
|
16152
|
+
browserProfile: ordinary.browserProfile || undefined,
|
|
16153
|
+
autoApprove: ordinary.approvalMode === "auto",
|
|
16154
|
+
});
|
|
16155
|
+
// What the preset brings wins over the saved defaults for the same
|
|
16156
|
+
// skill name or note file.
|
|
16157
|
+
const presetSkills = new Set(preset?.preset.skills ?? []);
|
|
16158
|
+
const presetNotes = new Set(Object.keys(preset?.preset.seed?.memory ?? {}));
|
|
16159
|
+
for (const [file, text] of Object.entries(template.memory)) {
|
|
16160
|
+
if (presetNotes.has(file))
|
|
16161
|
+
continue;
|
|
16162
|
+
journalMemoryWrite(bot.id, file, text, { actor: "person", via: "api" });
|
|
16163
|
+
}
|
|
16164
|
+
if (preset) {
|
|
16165
|
+
applyPresetToBot(bot.id, preset, {
|
|
16166
|
+
store,
|
|
16167
|
+
skills: { install: installSkill, setEnabled: setSkillEnabled },
|
|
16168
|
+
memory: { writeIndex: writeMemoryFile, writeTopic: writeMemoryTopic },
|
|
16169
|
+
});
|
|
16170
|
+
}
|
|
16171
|
+
for (const skill of template.skills) {
|
|
16172
|
+
if (presetSkills.has(skill.name))
|
|
16173
|
+
continue;
|
|
16174
|
+
const parsed = parseSkillMd(skill.text);
|
|
16175
|
+
if ("error" in parsed || parsed.name !== skill.name)
|
|
16176
|
+
throw Object.assign(new Error("Skill name does not match its contents"), { status: 400 });
|
|
16177
|
+
const installed = installSkill(bot.id, skill.source, [{ path: "SKILL.md", content: skill.text }]);
|
|
16178
|
+
if ("error" in installed)
|
|
16179
|
+
throw Object.assign(new Error(installed.error), { status: 422 });
|
|
16180
|
+
if (skill.enabled) {
|
|
16181
|
+
const enabled = setSkillEnabled(bot.id, skill.name, true);
|
|
16182
|
+
if ("error" in enabled)
|
|
16183
|
+
throw Object.assign(new Error(enabled.error), { status: 422 });
|
|
16184
|
+
}
|
|
16185
|
+
}
|
|
16186
|
+
for (const routine of template.routines) {
|
|
16187
|
+
const created = routines.create({ ...routine, botId: bot.id, enabled: false });
|
|
16188
|
+
createdRoutines.push({ id: created.id, enabled: routine.enabled !== false });
|
|
16189
|
+
}
|
|
16190
|
+
}
|
|
16191
|
+
catch (error) {
|
|
16192
|
+
for (const routine of createdRoutines)
|
|
16193
|
+
routines.remove(routine.id);
|
|
16194
|
+
store.deleteBot(bot.id);
|
|
16195
|
+
throw error;
|
|
16196
|
+
}
|
|
16197
|
+
// Creation is complete. Activation errors must not invite a duplicate
|
|
16198
|
+
// creation retry or delete a bot whose routine may already have run.
|
|
16199
|
+
const warnings = [];
|
|
16200
|
+
if (settings.chiefOfStaff) {
|
|
16201
|
+
try {
|
|
16202
|
+
store.patchBot(bot.id, { managedSections: settings.managedSections ?? [] });
|
|
16203
|
+
store.setChiefOfStaff(bot.id);
|
|
16204
|
+
}
|
|
16205
|
+
catch {
|
|
16206
|
+
warnings.push("Bot created; review its Chief of Staff setting before delegating work.");
|
|
16207
|
+
}
|
|
16208
|
+
}
|
|
16209
|
+
for (const routine of createdRoutines)
|
|
16210
|
+
if (routine.enabled) {
|
|
16211
|
+
try {
|
|
16212
|
+
routines.update(routine.id, { enabled: true });
|
|
16213
|
+
}
|
|
16214
|
+
catch {
|
|
16215
|
+
warnings.push(`Bot created; routine ${routine.id} could not be activated.`);
|
|
16216
|
+
}
|
|
16217
|
+
}
|
|
15282
16218
|
return json(res, 201, {
|
|
16219
|
+
...(warnings.length ? { warnings } : {}),
|
|
15283
16220
|
bot: {
|
|
15284
16221
|
...wireBot(bot),
|
|
15285
16222
|
messages: store.messagesFor(bot.threadId),
|
|
@@ -15398,6 +16335,30 @@ const handleRequest = async (req, res) => {
|
|
|
15398
16335
|
broadcast({ kind: "bot", bot: visible });
|
|
15399
16336
|
return json(res, 200, { bot: visible });
|
|
15400
16337
|
}
|
|
16338
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/command-allowlist(?:\/([\w-]+))?$/);
|
|
16339
|
+
if (m && ["GET", "POST", "DELETE"].includes(method)) {
|
|
16340
|
+
if (!canManageCommandAllowlist(auth))
|
|
16341
|
+
return json(res, 403, { error: "Only the workspace owner or an admin can manage command permissions." });
|
|
16342
|
+
const bot = requestedTaskBot(m[1], url.searchParams.get("threadId") ?? undefined);
|
|
16343
|
+
if (method === "GET" && !m[2])
|
|
16344
|
+
return json(res, 200, commandAllowlistResponse(bot));
|
|
16345
|
+
if (method === "POST" && !m[2]) {
|
|
16346
|
+
const candidate = commandAllowlistCandidate(await readBody(req));
|
|
16347
|
+
if (!candidate)
|
|
16348
|
+
return json(res, 400, { error: "Enter an exact command without secret values, an absolute working folder, and a provider." });
|
|
16349
|
+
const instance = registry.get(candidate.providerInstanceId);
|
|
16350
|
+
if (!instance || !COMMAND_ALLOWLIST_DRIVERS.has(instance.driverKind)) {
|
|
16351
|
+
return json(res, 400, { error: "This provider does not supply structured command approvals." });
|
|
16352
|
+
}
|
|
16353
|
+
commandAllowlist.add(bot.id, candidate);
|
|
16354
|
+
return json(res, 200, commandAllowlistResponse(bot));
|
|
16355
|
+
}
|
|
16356
|
+
if (method === "DELETE" && m[2]) {
|
|
16357
|
+
commandAllowlist.remove(bot.id, m[2]);
|
|
16358
|
+
return json(res, 200, commandAllowlistResponse(bot));
|
|
16359
|
+
}
|
|
16360
|
+
return json(res, 405, { error: "Unsupported command allowlist operation" });
|
|
16361
|
+
}
|
|
15401
16362
|
m = path.match(/^\/api\/bots\/([\w-]+)\/always-allow$/);
|
|
15402
16363
|
if (m && method === "POST") {
|
|
15403
16364
|
const body = await readBody(req);
|
|
@@ -15555,6 +16516,20 @@ const handleRequest = async (req, res) => {
|
|
|
15555
16516
|
return json(res, 400, { error: "composio must be true or false" });
|
|
15556
16517
|
patch.composio = body.composio;
|
|
15557
16518
|
}
|
|
16519
|
+
// which of those apps' tools this bot may call (connector grants 1/5:
|
|
16520
|
+
// data model only — enforcement lands with slice 2). null returns the
|
|
16521
|
+
// bot to boolean-only legacy behavior; {} means no tools.
|
|
16522
|
+
if (body.connectorTools !== undefined) {
|
|
16523
|
+
if (body.connectorTools === null) {
|
|
16524
|
+
patch.connectorTools = undefined;
|
|
16525
|
+
}
|
|
16526
|
+
else {
|
|
16527
|
+
const parsed = parseConnectorTools(body.connectorTools);
|
|
16528
|
+
if (!parsed.ok)
|
|
16529
|
+
return json(res, 400, { error: parsed.error });
|
|
16530
|
+
patch.connectorTools = parsed.grants;
|
|
16531
|
+
}
|
|
16532
|
+
}
|
|
15558
16533
|
// Queue this bot's direct messages behind outstanding delegated work
|
|
15559
16534
|
// instead of steering the conversation immediately (#1194).
|
|
15560
16535
|
if (body.parkDirectMessages !== undefined) {
|
|
@@ -15920,6 +16895,12 @@ const handleRequest = async (req, res) => {
|
|
|
15920
16895
|
}
|
|
15921
16896
|
if (!bot)
|
|
15922
16897
|
return json(res, 404, { error: "no such bot" });
|
|
16898
|
+
// A defined Works on is the newest explicit choice: this bot's
|
|
16899
|
+
// auto-recorded pins that now point elsewhere give way immediately, so
|
|
16900
|
+
// the next turn on each thread follows the new setting. Person-set pins
|
|
16901
|
+
// and pins that already match stay; returning to Auto sweeps nothing.
|
|
16902
|
+
if (computerSpecified && requestedComputer !== undefined)
|
|
16903
|
+
store.clearAutoSurfacePins(m[1], requestedComputer);
|
|
15923
16904
|
if (normalizedSelection && selectedTask)
|
|
15924
16905
|
store.patchTask(bot.id, selectedTask.threadId, { modelSelection: normalizedSelection,
|
|
15925
16906
|
...hostedModels?.resetTask(selectedTask.modelSelection, normalizedSelection) });
|
|
@@ -15983,6 +16964,27 @@ const handleRequest = async (req, res) => {
|
|
|
15983
16964
|
// ── bot skills: imported Agent Skills (SKILL.md) ────────────────────
|
|
15984
16965
|
// Import lands DISABLED; the UI shows SKILL.md + scan warnings and a
|
|
15985
16966
|
// person enables after reading. See server/skills.ts for the policy.
|
|
16967
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/skill-template$/);
|
|
16968
|
+
if (m && method === "POST") {
|
|
16969
|
+
if (!store.bot(m[1]))
|
|
16970
|
+
return json(res, 404, { error: "no such bot" });
|
|
16971
|
+
const checked = botSkillTemplateSchema.safeParse(await readBody(req));
|
|
16972
|
+
if (!checked.success)
|
|
16973
|
+
return json(res, 400, { error: checked.error.message });
|
|
16974
|
+
const input = checked.data;
|
|
16975
|
+
const parsed = parseSkillMd(input.text);
|
|
16976
|
+
if ("error" in parsed || parsed.name !== input.name)
|
|
16977
|
+
return json(res, 400, { error: "Skill name does not match its contents" });
|
|
16978
|
+
const installed = installSkill(m[1], input.source, [{ path: "SKILL.md", content: input.text }]);
|
|
16979
|
+
if ("error" in installed)
|
|
16980
|
+
return json(res, 422, { error: installed.error });
|
|
16981
|
+
if (input.enabled) {
|
|
16982
|
+
const enabled = setSkillEnabled(m[1], input.name, true);
|
|
16983
|
+
if ("error" in enabled)
|
|
16984
|
+
return json(res, 422, { error: enabled.error });
|
|
16985
|
+
}
|
|
16986
|
+
return json(res, 201, { installed });
|
|
16987
|
+
}
|
|
15986
16988
|
m = path.match(/^\/api\/bots\/([\w-]+)\/skills$/);
|
|
15987
16989
|
if (m && method === "GET") {
|
|
15988
16990
|
if (!store.bot(m[1]))
|
|
@@ -16766,6 +17768,23 @@ const handleRequest = async (req, res) => {
|
|
|
16766
17768
|
const text = String(body.text ?? "").trim();
|
|
16767
17769
|
if (!text)
|
|
16768
17770
|
return json(res, 400, { error: "text required" });
|
|
17771
|
+
const threadMessages = store.messagesFor(bot.threadId);
|
|
17772
|
+
const source = threadMessages.find((msg) => msg.id === messageId);
|
|
17773
|
+
if (!source || source.role !== "user" || source.kind !== "text") {
|
|
17774
|
+
return json(res, 404, { error: "only user messages can be edited" });
|
|
17775
|
+
}
|
|
17776
|
+
// The client's id for this edit. A retry of an edit the server already
|
|
17777
|
+
// forked answers with that fork instead of forking (and rerunning) again.
|
|
17778
|
+
// Checked before the busy guard: that fork's own turn is what is busy.
|
|
17779
|
+
const sendId = parseSendId(body.sendId);
|
|
17780
|
+
if (sendId) {
|
|
17781
|
+
const accepted = acceptedSendMatch(threadMessages, sendId, text, source.replyToId);
|
|
17782
|
+
if (accepted.kind === "conflict" || (accepted.kind === "match" && (accepted.message.parentId ?? null) !== (source.parentId ?? null))) {
|
|
17783
|
+
return json(res, 409, { error: "sendId already belongs to another message" });
|
|
17784
|
+
}
|
|
17785
|
+
if (accepted.kind === "match")
|
|
17786
|
+
return json(res, 202, { ok: true, message: accepted.message });
|
|
17787
|
+
}
|
|
16769
17788
|
// everything from here down is synchronous, so two racing edits can
|
|
16770
17789
|
// never both get past this check: startTurn flips busy before the
|
|
16771
17790
|
// next request is handled
|
|
@@ -16774,10 +17793,6 @@ const handleRequest = async (req, res) => {
|
|
|
16774
17793
|
if (phoneSecretSubmissions.hasThread(bot.threadId)) {
|
|
16775
17794
|
return json(res, 409, { error: "this task is securely saving a credential — try again when it finishes" });
|
|
16776
17795
|
}
|
|
16777
|
-
const source = store.messagesFor(bot.threadId).find((msg) => msg.id === messageId);
|
|
16778
|
-
if (!source || source.role !== "user" || source.kind !== "text") {
|
|
16779
|
-
return json(res, 404, { error: "only user messages can be edited" });
|
|
16780
|
-
}
|
|
16781
17796
|
if (!registry.get(bot.modelSelection.instanceId)) {
|
|
16782
17797
|
return json(res, 409, {
|
|
16783
17798
|
error: `provider instance "${bot.modelSelection.instanceId}" is unavailable — pick another model in settings`,
|
|
@@ -16790,7 +17805,7 @@ const handleRequest = async (req, res) => {
|
|
|
16790
17805
|
// startTurn admits the rerun before branching. A shared-resource or
|
|
16791
17806
|
// concurrency-limit refusal must leave the original transcript intact.
|
|
16792
17807
|
const replyTo = source.replyToId ? resolveReplyTarget(bot.threadId, source.replyToId) : undefined;
|
|
16793
|
-
const message = await startTurn(bot.id, text, { threadId: bot.threadId, editedMessageId: messageId, replyTo, trigger: usageTriggerFor(auth) });
|
|
17808
|
+
const message = await startTurn(bot.id, text, { threadId: bot.threadId, editedMessageId: messageId, replyTo, sendId, trigger: usageTriggerFor(auth) });
|
|
16794
17809
|
return json(res, 202, { ok: true, message });
|
|
16795
17810
|
}
|
|
16796
17811
|
// switch which fork of the conversation is visible (no new turn)
|
|
@@ -16823,6 +17838,11 @@ const handleRequest = async (req, res) => {
|
|
|
16823
17838
|
const refusal = cardAnswerRefusal(auth, bot.threadId, String(body.requestId), behavior);
|
|
16824
17839
|
if (refusal)
|
|
16825
17840
|
return json(res, 403, { error: refusal });
|
|
17841
|
+
if (body.rememberCommand === true) {
|
|
17842
|
+
const refusal = commandRememberRefusal(auth, bot.threadId, String(body.requestId), behavior);
|
|
17843
|
+
if (refusal)
|
|
17844
|
+
return json(res, 403, { error: refusal });
|
|
17845
|
+
}
|
|
16826
17846
|
return await answeringCardAs(auth, bot.threadId, String(body.requestId), async () => {
|
|
16827
17847
|
if (await resolveAndSendTeamSetup(res, {
|
|
16828
17848
|
botId: bot.id, threadId: bot.threadId, requestId: String(body.requestId), behavior,
|
|
@@ -16860,7 +17880,7 @@ const handleRequest = async (req, res) => {
|
|
|
16860
17880
|
resolvePeerComms(approvalBus, String(body.requestId), behavior)) {
|
|
16861
17881
|
return json(res, 200, { ok: true, outcome: behavior === "allow" ? "allowed-once" : "rejected" });
|
|
16862
17882
|
}
|
|
16863
|
-
const outcome = await answerRequest(bot.threadId, bot.modelSelection.instanceId, String(body.requestId), behavior, body.message, { id: bot.id, name: bot.name }, body.always === true);
|
|
17883
|
+
const outcome = await answerRequest(bot.threadId, bot.modelSelection.instanceId, String(body.requestId), behavior, body.message, { id: bot.id, name: bot.name }, body.always === true, body.rememberCommand === true);
|
|
16864
17884
|
return json(res, 200, { ok: true, outcome });
|
|
16865
17885
|
});
|
|
16866
17886
|
}
|
|
@@ -16879,6 +17899,11 @@ const handleRequest = async (req, res) => {
|
|
|
16879
17899
|
const refusal = cardAnswerRefusal(auth, threadId, requestId, behavior);
|
|
16880
17900
|
if (refusal)
|
|
16881
17901
|
return json(res, 403, { error: refusal });
|
|
17902
|
+
if (body.rememberCommand === true) {
|
|
17903
|
+
const refusal = commandRememberRefusal(auth, threadId, requestId, behavior);
|
|
17904
|
+
if (refusal)
|
|
17905
|
+
return json(res, 403, { error: refusal });
|
|
17906
|
+
}
|
|
16882
17907
|
return await answeringCardAs(auth, threadId, requestId, async () => {
|
|
16883
17908
|
const skillCard = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId && message.card.skillRequest);
|
|
16884
17909
|
if (skillCard?.card?.skillRequest) {
|
|
@@ -16958,7 +17983,7 @@ const handleRequest = async (req, res) => {
|
|
|
16958
17983
|
if (!owner && !pending)
|
|
16959
17984
|
return json(res, 404, { error: "nothing is waiting on an answer in this conversation" });
|
|
16960
17985
|
const requestOwner = owner ? botForThread(owner.id, threadId) : null;
|
|
16961
|
-
const outcome = await answerRequest(threadId, requestOwner?.modelSelection.instanceId ?? "", requestId, behavior, body.message, owner ? { id: owner.id, name: owner.name } : undefined, body.always === true);
|
|
17986
|
+
const outcome = await answerRequest(threadId, requestOwner?.modelSelection.instanceId ?? "", requestId, behavior, body.message, owner ? { id: owner.id, name: owner.name } : undefined, body.always === true, body.rememberCommand === true);
|
|
16962
17987
|
return json(res, 200, { ok: true, outcome });
|
|
16963
17988
|
});
|
|
16964
17989
|
}
|
|
@@ -17163,7 +18188,7 @@ const handleRequest = async (req, res) => {
|
|
|
17163
18188
|
const current = store.projectBotForTask(m[1], m[2]);
|
|
17164
18189
|
if (!current)
|
|
17165
18190
|
return json(res, 404, { error: "no such task" });
|
|
17166
|
-
const allowed = new Set(["title", "projectId", "modelSelection", "updateBotDefault", "resetApprovalToAsk", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto", "archivedAt", "pinned", "surface"]);
|
|
18191
|
+
const allowed = new Set(["title", "projectId", "modelSelection", "updateBotDefault", "resetApprovalToAsk", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto", "archivedAt", "pinned", "snoozedUntil", "surface"]);
|
|
17167
18192
|
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
17168
18193
|
return json(res, 400, { error: "unsupported thread setting" });
|
|
17169
18194
|
for (const key of ["requireAvailableModel", "acknowledgeLocalAuto", "updateBotDefault", "resetApprovalToAsk"]) {
|
|
@@ -17205,15 +18230,28 @@ const handleRequest = async (req, res) => {
|
|
|
17205
18230
|
return json(res, 400, { error: "pinned must be a boolean" });
|
|
17206
18231
|
patch.pinned = body.pinned ? true : undefined;
|
|
17207
18232
|
}
|
|
18233
|
+
if (body.snoozedUntil !== undefined) {
|
|
18234
|
+
if (body.snoozedUntil === null)
|
|
18235
|
+
patch.snoozedUntil = undefined;
|
|
18236
|
+
else if (typeof body.snoozedUntil === "number" && Number.isFinite(body.snoozedUntil) && body.snoozedUntil >= 0)
|
|
18237
|
+
patch.snoozedUntil = body.snoozedUntil;
|
|
18238
|
+
else
|
|
18239
|
+
return json(res, 400, { error: "snoozedUntil must be a timestamp, 0 to snooze until activity, or null to wake now" });
|
|
18240
|
+
}
|
|
17208
18241
|
if (body.surface !== undefined) {
|
|
17209
18242
|
if (threadBusy(current.id, current.threadId))
|
|
17210
18243
|
return json(res, 409, { error: "Stop this thread before changing its computer destination." });
|
|
17211
|
-
// Where this conversation works, chosen
|
|
17212
|
-
//
|
|
17213
|
-
|
|
18244
|
+
// Where this conversation works, chosen by the person and recorded as
|
|
18245
|
+
// theirs, so a later Works on change never sweeps it. Null follows the
|
|
18246
|
+
// bot's Works on again. Reachability is the turn's to judge.
|
|
18247
|
+
if (body.surface === null) {
|
|
17214
18248
|
patch.surface = undefined;
|
|
17215
|
-
|
|
18249
|
+
patch.surfaceSource = undefined;
|
|
18250
|
+
}
|
|
18251
|
+
else if (parseSurface(body.surface)) {
|
|
17216
18252
|
patch.surface = parseSurface(body.surface);
|
|
18253
|
+
patch.surfaceSource = "user";
|
|
18254
|
+
}
|
|
17217
18255
|
else
|
|
17218
18256
|
return json(res, 400, { error: "surface must be cloud, vm, local, browser, or null to follow the bot" });
|
|
17219
18257
|
}
|
|
@@ -17783,7 +18821,7 @@ const handleRequest = async (req, res) => {
|
|
|
17783
18821
|
return json(res, 400, { error: `provider must be one of ${PROVIDER_KEY_KINDS.join(", ")}` });
|
|
17784
18822
|
}
|
|
17785
18823
|
const kind = provider;
|
|
17786
|
-
const saved = kind === "anthropic" ? cfg.anthropic : kind === "openaiCompat" ? cfg.openaiCompat : cfg.xai;
|
|
18824
|
+
const saved = kind === "anthropic" ? cfg.anthropic : kind === "openaiCompat" ? cfg.openaiCompat : kind === "mistral" ? cfg.mistral : cfg.xai;
|
|
17787
18825
|
if (body?.key !== undefined && typeof body.key !== "string") {
|
|
17788
18826
|
return json(res, 400, { error: "key must be a string" });
|
|
17789
18827
|
}
|
|
@@ -17792,7 +18830,7 @@ const handleRequest = async (req, res) => {
|
|
|
17792
18830
|
return json(res, 400, { error: "No key to test. Paste one or save one first." });
|
|
17793
18831
|
if (key.length > 512)
|
|
17794
18832
|
return json(res, 400, { error: "That does not look like an API key." });
|
|
17795
|
-
const url = typeof body?.url === "string" && body.url.trim() ? body.url.trim() : saved
|
|
18833
|
+
const url = typeof body?.url === "string" && body.url.trim() ? body.url.trim() : (saved && "url" in saved ? saved.url : undefined);
|
|
17796
18834
|
res.setHeader("cache-control", "no-store");
|
|
17797
18835
|
return json(res, 200, await checkProviderKey({ provider: kind, key, url }));
|
|
17798
18836
|
}
|
|
@@ -18095,8 +19133,8 @@ const handleRequest = async (req, res) => {
|
|
|
18095
19133
|
return json(res, 400, { error: "Account settings are currently available for Claude only." });
|
|
18096
19134
|
}
|
|
18097
19135
|
if (body.tools !== undefined) {
|
|
18098
|
-
if (!["openai-compat", "grok", "minimax"].includes(entry.driver)) {
|
|
18099
|
-
return json(res, 400, { error: "The tools setting is available for OpenAI-compatible, Grok API and
|
|
19136
|
+
if (!["openai-compat", "grok", "minimax", "mistral"].includes(entry.driver)) {
|
|
19137
|
+
return json(res, 400, { error: "The tools setting is available for OpenAI-compatible, Grok API, MiniMax API and Mistral API instances only." });
|
|
18100
19138
|
}
|
|
18101
19139
|
entry.config = { ...entry.config, tools: body.tools };
|
|
18102
19140
|
}
|
|
@@ -18298,9 +19336,22 @@ const handleRequest = async (req, res) => {
|
|
|
18298
19336
|
}
|
|
18299
19337
|
if ((method === "PUT" || method === "PATCH") && path === "/api/config") {
|
|
18300
19338
|
const body = await readBody(req);
|
|
18301
|
-
if (hostedModels && ["instances", "anthropic", "openaiCompat", "xai", "opencodeGo"].some(key => Object.hasOwn(body, key)))
|
|
19339
|
+
if (hostedModels && ["instances", "anthropic", "openaiCompat", "xai", "mistral", "opencodeGo"].some(key => Object.hasOwn(body, key)))
|
|
18302
19340
|
return json(res, 403, { error: HOSTED_PROVIDER_SETTINGS_ERROR });
|
|
18303
19341
|
const patch = parseConfigPatch(body);
|
|
19342
|
+
if (patch.newBotDefaults) {
|
|
19343
|
+
if (patch.newBotDefaults.profile.modelSelection) {
|
|
19344
|
+
const checked = checkedModelSelection(patch.newBotDefaults.profile.modelSelection);
|
|
19345
|
+
if (!checked.ok)
|
|
19346
|
+
return json(res, checked.status, { error: checked.error });
|
|
19347
|
+
patch.newBotDefaults.profile.modelSelection = checked.selection;
|
|
19348
|
+
}
|
|
19349
|
+
for (const skill of patch.newBotDefaults.skills) {
|
|
19350
|
+
const parsed = parseSkillMd(skill.text);
|
|
19351
|
+
if ("error" in parsed || parsed.name !== skill.name)
|
|
19352
|
+
return json(res, 400, { error: "Skill name does not match its contents" });
|
|
19353
|
+
}
|
|
19354
|
+
}
|
|
18304
19355
|
if (hostedModels && patch.defaultModelSelection) {
|
|
18305
19356
|
const checked = checkedModelSelection(patch.defaultModelSelection);
|
|
18306
19357
|
if (!checked.ok)
|
|
@@ -18470,7 +19521,7 @@ const handleRequest = async (req, res) => {
|
|
|
18470
19521
|
}
|
|
18471
19522
|
const listed = currentById.get(inspected.identity.boxId);
|
|
18472
19523
|
if (listed && listed.name !== inspected.identity.name) {
|
|
18473
|
-
return json(res, 503, { error: "
|
|
19524
|
+
return json(res, 503, { error: "boat.dev returned conflicting cloud computer identities; keep the current Box account and retry" });
|
|
18474
19525
|
}
|
|
18475
19526
|
currentById.set(inspected.identity.boxId, inspected.identity);
|
|
18476
19527
|
journalBoxResources.push(inspected.identity);
|
|
@@ -19381,6 +20432,18 @@ for (const row of chatFollowups()) {
|
|
|
19381
20432
|
}
|
|
19382
20433
|
restoreSteeredMessages();
|
|
19383
20434
|
restoreChannelMessages();
|
|
20435
|
+
// Repair known auto pins that conflict with Works on before dispatch starts.
|
|
20436
|
+
// Legacy pins have no provenance and may be deliberate person selections:
|
|
20437
|
+
// preserve them rather than guessing. The sweep is idempotent.
|
|
20438
|
+
{
|
|
20439
|
+
let movedAutoPins = 0;
|
|
20440
|
+
for (const bot of store.bots) {
|
|
20441
|
+
if (bot.computer !== undefined)
|
|
20442
|
+
movedAutoPins += store.clearAutoSurfacePins(bot.id, bot.computer);
|
|
20443
|
+
}
|
|
20444
|
+
if (movedAutoPins)
|
|
20445
|
+
console.log(`Works on: moved ${movedAutoPins} auto-pinned thread(s) to their bot's current Works on`);
|
|
20446
|
+
}
|
|
19384
20447
|
server.listen(PORT, "127.0.0.1", () => {
|
|
19385
20448
|
companyRuntimeReady();
|
|
19386
20449
|
console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
|