@chibitek/chibi 0.1.169 → 0.1.171
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{abnfDiagram-O67JEVCF-aQkRN0ee.js → abnfDiagram-O67JEVCF-DrPBKmVh.js} +1 -1
- package/dist/assets/{arc-BT7qPVAa.js → arc-BEIChVSB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-oMlARqKW.js → architectureDiagram-NJMV4G6O-Da60nTzs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CKUrb4js.js → blockDiagram-BEXU5L5S-_NelwIQj.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BkL5cRIA.js → c4Diagram-YGBWAQC7-DUdkQ3dy.js} +1 -1
- package/dist/assets/channel-DEJSSlVQ.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BMitkm2s.js → chunk-2Q5K7J3B-BQgYAptM.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-DOhVN2ed.js → chunk-3FUC2YCW-XXOXwcoW.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BFkFo_ng.js → chunk-5VM5RSS4-CUyMXCSt.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-QbxYUf3Q.js → chunk-GTNCS2PH-BwctbINm.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BZChfTYw.js → chunk-GWA4HPMP-DQ0bZK59.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BXpmYglA.js → chunk-JWPE2WC7-BADCk9AK.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CIZTrz1i.js → chunk-NETBCI7D-CoJxabSR.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-FhqQ0sO0.js → chunk-WEXAMYUT-fupByVJn.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-5ThtmFbP.js → chunk-XXDRQBXY-BavbGHGi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BzObC5Qw.js → classDiagram-v2-NBCMYWYE-B5wfUNKJ.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BASV9Okv.js → cose-bilkent-JH36ORCC-DlYFmKDe.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B0zQYMFh.js → cynefin-OW5HDTMX-Y0U-21MH.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DMEO1cHA.js → cynefinDiagram-VND7K2PF-D1Fh1sU0.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BG04TSi_.js → dagre-6A5THRUB-BNU-Trog.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CfpVNMhg.js → diagram-22UHCM2B-Bjie4nDQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-DG9L4I1L.js → diagram-3UASUU5V-Qnbrq1T5.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Db7Bf63S.js → diagram-ATOU4E4O-CNJasPCN.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-Cdzv1tJn.js → diagram-CDSNMT55-BAzGZBKX.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BBMeXGKE.js → diagram-MLGK6HIB-I8ACRaw-.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BAQ22zk3.js → diagram-MPIPVDR6-nLlrAEPz.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BDZgSFSF.js → ebnfDiagram-ZINNZB2B-DF9BG3pH.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Bx4-nN8N.js → elk-276RUBZZ-C1AJYKMV.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-D2Pn-cqm.js → erDiagram-OPXOYQCR-MNNcRrPy.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-hsbvL3Ow.js → flowDiagram-KWPJA3E3-C7mfBfgt.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DBUgYe4p.js → ganttDiagram-FUAMR5RP-a9zNyDbb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-ChGeSINg.js → gitGraphDiagram-X574FWY7-OL8NhUsO.js} +1 -1
- package/dist/assets/{index-ByXr4FRl.js → index-DojGBgbN.js} +167 -164
- package/dist/assets/{index-Ch93Ygtq.js → index-xypOVpVT.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-DY2EeC-A.js → infoDiagram-VRGFBTTK-CF-3pzUZ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BznYuY04.js → ishikawaDiagram-OU5B5YK6-B2HqD-sv.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CX74ZLbW.js → journeyDiagram-ZHPQQLJL-84gzOTb9.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-0DPavB-t.js → kanban-definition-PNTS6WVX-D7Ee23QC.js} +1 -1
- package/dist/assets/{linear-BHeqbvM8.js → linear-CdAXsmfc.js} +1 -1
- package/dist/assets/{mermaid.core-Dm-Ny6a0.js → mermaid.core-CE_M1v1e.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-BPrMElav.js → mindmap-definition-NLK3R4M7-KSTIET9y.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CaD7wyyX.js → pegDiagram-GJSIUBJH-C7ogvddb.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-lbSp8R4K.js → pieDiagram-5QR66LMP-Bp1CgCrM.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Bs3Sn2of.js → quadrantDiagram-O4NWA36T-Bw7aN9Lh.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-C88ykXCV.js → railroadDiagram-XR7U4H2S-Dij4U4NY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-mgIQnORW.js → requirementDiagram-PLB6GJNP-DOxTQvCl.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-K5esEw-m.js → sankeyDiagram-IPEJSGJF-DPCUmNbV.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-D5v8PJqQ.js → sequenceDiagram-PO4LG4MO-DPjC8zGe.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BHBBDTEI.js → sizeCapture-INFHLROL-WlmsNlh-.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CeRdNmKQ.js → stateDiagram-v2-GCMORJYK-BwMPiapI.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D8CbMCI8.js → swimlanes-2SLR337P-Cyja7ZPb.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Dh25zxzq.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-CMG02UV1.js → timeline-definition-EJHVYXUP-BehkSauz.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Bm8h6ijK.js → usecaseDiagram-POWQR4AR-koUjcmpW.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-Dk9tFjlM.js → vennDiagram-UO4OBE2U-Ch8TXIpy.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-61vrb2Ft.js → wardleyDiagram-VNRHLVJA-Dm5kC4c2.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-C7uO44ng.js → xychartDiagram-PMCCYNJV-YA-1zyQz.js} +1 -1
- package/dist/handbook/atomic-handbook.html +1 -1
- package/dist/index.html +1 -1
- package/dist-server/companion/src/routes.js +28 -1
- package/dist-server/container-mcp.js +1 -0
- package/dist-server/hooks/plan-hook.js +22 -0
- package/dist-server/index.js +38538 -37905
- package/dist-server/local-computer.js +1 -0
- package/dist-server/openmausbot.js +389 -303
- package/dist-server/pair-cli.js +389 -303
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/atomic-engine-release.js +13 -13
- package/dist-server/server/bot-work-modes.js +212 -0
- package/dist-server/server/config.js +6 -5
- package/dist-server/server/drivers/acp/core.js +6 -4
- package/dist-server/server/drivers/acp/custom.js +51 -37
- package/dist-server/server/drivers/builtIn.js +2 -1
- package/dist-server/server/drivers/chat-mcp-tools.js +1 -0
- package/dist-server/server/drivers/claude.js +28 -25
- package/dist-server/server/drivers/codex-approval-presets.js +34 -0
- package/dist-server/server/drivers/codex.js +11 -35
- package/dist-server/server/drivers/openai-chat.js +4 -3
- package/dist-server/server/hooks/plan-hook.js +17 -0
- package/dist-server/server/index.js +92 -175
- package/dist-server/server/mcp-interactive-probe.js +60 -0
- package/dist-server/server/mcp-remote-bridge.js +13 -6
- package/dist-server/server/message-file.js +28 -11
- package/dist-server/server/plan-policy.js +14 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/remote-desktop.js +142 -0
- package/dist-server/server/routes/message-file.js +115 -0
- package/dist-server/server/shared-deliverables.js +109 -0
- package/dist-server/server/skill-learn.js +4 -0
- package/dist-server/server/store.js +4 -4
- package/dist-server/server/subscription-bots.js +3 -2
- package/dist-server/shared/approval-mode.js +3 -1
- package/dist-server/shared/engine-choices.js +1 -1
- package/dist-server/shared/provider-accounts.js +5 -5
- package/dist-server/shared/skill-command.js +16 -0
- package/dist-server/vps-container-mcp.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-BZDQCBTd.js +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CQHcTGnI.js +0 -8
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createMessageFileRoutes } from "./routes/message-file.js";
|
|
2
|
+
import { SharedDeliverables, createSharedDeliverablesRoutes, sharedDeliverableAccess } from "./shared-deliverables.js";
|
|
1
3
|
import { connectionCredentialServer } from "./connection-credential.js";
|
|
2
4
|
import { requestCredentialCard } from "./credential-card-request.js";
|
|
3
5
|
import { invalidateBrokenCheckpoint } from "./checkpoint-recovery.js";
|
|
@@ -34,7 +36,7 @@ import { CLOUD_COMPUTER_BUSY_ERROR } from "../shared/computer-contention.js";
|
|
|
34
36
|
import { approvalModeFor, supportsApprovalMode, modelSwitchNeedsAsk, isEmergencyApprovalDowngrade, isApprovalMode, } from "../shared/approval-mode.js";
|
|
35
37
|
import { escapeAttribute } from "../src/lib/composer-attachments.js";
|
|
36
38
|
import { credentialResumeOutcome, credentialIsConfigured, isCredentialTargetId, } from "../shared/credential-request.js";
|
|
37
|
-
import { approvalHeldNote, approvalHeldReason,
|
|
39
|
+
import { approvalHeldNote, approvalHeldReason, autoVerdict, deliverFullAccessApproval, delegationInheritsFullAccess } from "./auto-approve.js";
|
|
38
40
|
import { updateClaudeCli } from "./claude-update.js";
|
|
39
41
|
import { failedRunDetail } from "./turn-failure.js";
|
|
40
42
|
import { configuredAccountDirectory, assertSeparateClaudeAccount, claudeAccountInfo, createClaudeAccountSchema, instanceSettingsSchema, newClaudeAccount } from "./claude-accounts.js";
|
|
@@ -50,7 +52,6 @@ import { ApprovalGrantStore, describeScope, matchGrant } from "./approval-grants
|
|
|
50
52
|
import { validateBotCwd } from "./bot-cwd.js";
|
|
51
53
|
import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
52
54
|
import { companyCheck, companyContext } from "./company-document-check.js";
|
|
53
|
-
import { messageFileDisposition, messageFileDownloadName, messageAttachmentName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile, } from "./message-file.js";
|
|
54
55
|
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, avatarImageStatus, snapshotAvatarGenerationState } from "./avatar-image.js";
|
|
55
56
|
import { generateRoutedAvatar, installAvatarSubscriptionRoute } from "./avatar-subscription.js";
|
|
56
57
|
import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
@@ -99,6 +100,7 @@ import { decodeGeneratedImage } from "./generated-image.js";
|
|
|
99
100
|
import { MAX_MCP_SERVERS, listMcpServers, mcpServerNameError, parseMcpServerMutation, parseMcpServersImport, parseStoredMcpServer, isRemoteMcpServer, mcpReadOnlyAutoAllow, } from "./mcp-registry.js";
|
|
100
101
|
import { mcpLaunchBlocker, mcpLaunchBlockerReason, mcpSecretStorage, mcpServerSecretView, moveParkedArgToEnv, receiveMcpSecretsSaveResult, setMcpSecretSender, storeMcpServerSecrets, } from "./mcp-secrets.js";
|
|
101
102
|
import { probeMcpServer } from "./mcp-probe.js";
|
|
103
|
+
import { createInteractiveMcpProbe } from "./mcp-interactive-probe.js";
|
|
102
104
|
import { McpToolHintStore, readOnlyToolAllowed } from "./mcp-tool-hints.js";
|
|
103
105
|
import { McpAuthStateStore } from "./mcp-auth-state.js";
|
|
104
106
|
import { McpHealthStore } from "./mcp-health.js";
|
|
@@ -156,11 +158,12 @@ import { applyStagedSkillWrite, applySkillWriteWithReceipt, getStagedSkillWrite,
|
|
|
156
158
|
import { resolveSkillImport, skillImportSchema } from "./skill-fetch.js";
|
|
157
159
|
import { pickBotName } from "./names.js";
|
|
158
160
|
import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefaults } from "./new-bot-defaults.js";
|
|
159
|
-
import {
|
|
161
|
+
import { expandSkillTurnText, learnSource } from "./skill-learn.js";
|
|
160
162
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
161
163
|
import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-folder.js";
|
|
162
164
|
import { buildSystemPrompt, userProfileSystemPrompt, computerPrompt, accountsPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, } from "./system-prompt.js";
|
|
163
165
|
import { readCuaConnection, readCuaUnavailableReason, gatedLocalComputer } from "./local-computer.js";
|
|
166
|
+
import { remoteDesktopRoute } from "./remote-desktop.js";
|
|
164
167
|
import { discoverExistingPerBotLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
165
168
|
import { ATOMIC_BACKUP_DIR, BROWSER_BACKUP_INTERVAL_MS, backupBrowserProfiles, localVmWorkspaces } from "./atomic-backup.js";
|
|
166
169
|
import { LocalVmIdleTimer } from "./local-vm-idle.js";
|
|
@@ -251,6 +254,7 @@ import { PHONE_SECRET_PROTOCOL_VERSION, PhoneSecretBridge, PhoneSecretError, Pho
|
|
|
251
254
|
// Keep these two last: a route module may import any server module, and
|
|
252
255
|
// loading the table after everything above leaves module start-up order as is.
|
|
253
256
|
import { json, onJsonBody, parsedBodyOf, readBody } from "./harness/http.js";
|
|
257
|
+
import { BotWorkModes, createBotWorkModeRoutes, ownBotApprovalMode, legacyGodModeCovers, dispatchWorkMode, trustedPlanChangeForStore, invalidAllThreadScope } from "./bot-work-modes.js";
|
|
254
258
|
import { ROUTES, dispatchRoutes } from "./routes/table.js";
|
|
255
259
|
import { createThreadStopRoutes } from "./routes/thread-stop.js";
|
|
256
260
|
import { createThreadImageRoutes } from "./routes/thread-images.js";
|
|
@@ -2662,12 +2666,13 @@ function subscriptionModel(bot, candidate, wanted) {
|
|
|
2662
2666
|
const provider = subscriptionProvider(bot.subscription?.provider);
|
|
2663
2667
|
if (!provider)
|
|
2664
2668
|
return undefined;
|
|
2669
|
+
const usable = (model) => !engineResting(candidate, model);
|
|
2665
2670
|
const flagship = wanted !== undefined ? undefined : provider.host ? hostDefaultModel(provider, candidate) : frontierChoice(bot, candidate)?.model;
|
|
2666
|
-
if (flagship)
|
|
2671
|
+
if (flagship && usable(flagship))
|
|
2667
2672
|
return flagship;
|
|
2668
2673
|
return bestProviderModel({
|
|
2669
2674
|
provider, candidate, wanted: wanted ?? engineTierClasses(cfg.engineTiers)[botTier(bot)],
|
|
2670
|
-
classOf: (model) => modelClassOf(candidate.driverKind, model), ranked: rankedModelIds(candidate.driverKind),
|
|
2675
|
+
classOf: (model) => modelClassOf(candidate.driverKind, model), ranked: rankedModelIds(candidate.driverKind), usable,
|
|
2671
2676
|
});
|
|
2672
2677
|
}
|
|
2673
2678
|
/** Thrown when a subscription bot's turn has no account of its provider
|
|
@@ -4171,8 +4176,8 @@ function checkedTaskModelSwitch(current, raw, updateBotDefault, resetApprovalToA
|
|
|
4171
4176
|
}
|
|
4172
4177
|
for (const target of updateBotDefault ? [current, profile] : [current]) {
|
|
4173
4178
|
const mode = approvalModeFor(target);
|
|
4174
|
-
if (resetApprovalToAsk && mode === "custom" && !trusted) {
|
|
4175
|
-
return { ok: false, status: 403, error: "
|
|
4179
|
+
if (resetApprovalToAsk && (mode === "plan" || (mode === "custom" && !trusted))) {
|
|
4180
|
+
return { ok: false, status: 403, error: "Approve the plan or change Custom permissions in the app before switching models" };
|
|
4176
4181
|
}
|
|
4177
4182
|
if (!resetApprovalToAsk && modelSwitchNeedsAsk(mode, registry.cliTarget(target.modelSelection.instanceId)?.driverKind, registry.cliTarget(checked.selection.instanceId)?.driverKind)) {
|
|
4178
4183
|
return { ok: false, status: 400, error: "Confirm switching this model with Ask permissions first (resetApprovalToAsk)" };
|
|
@@ -4324,7 +4329,7 @@ const wireBot = (bot) => {
|
|
|
4324
4329
|
const visible = approvalGrant && !approvalGrant.threadOnly
|
|
4325
4330
|
? { ...rest, approvalMode: "ask", autoApprove: false }
|
|
4326
4331
|
: rest;
|
|
4327
|
-
return { ...visible, waitingForTeammates: activeCoordinationForThread(bot.threadId) && !threadBusy(bot.id, bot.threadId),
|
|
4332
|
+
return { ...visible, workMode: godModeCovers(bot) ? "full" : botWorkModes.permits(approvalModeFor(visible)) ? approvalModeFor(visible) : "ask", workModeWorkspaceOverride: godModeCovers(bot), waitingForTeammates: activeCoordinationForThread(bot.threadId) && !threadBusy(bot.id, bot.threadId),
|
|
4328
4333
|
screenLive: screenLiveForBot(bot.id), avatarUrl: visible.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map(wireTask) } : {}) };
|
|
4329
4334
|
};
|
|
4330
4335
|
/** The correlated private response carries the requested value so Electron
|
|
@@ -4450,7 +4455,7 @@ async function botOverview(bot) {
|
|
|
4450
4455
|
autoApprove: bot.autoApprove,
|
|
4451
4456
|
approvalMode: approvalModeForTurn(bot),
|
|
4452
4457
|
// GOD MODE skips the peer check for every bot, whatever its engine
|
|
4453
|
-
approvePeerComms: bot.approvePeerComms && !
|
|
4458
|
+
approvePeerComms: bot.approvePeerComms && !godModeCovers(bot),
|
|
4454
4459
|
peers: bot.peers,
|
|
4455
4460
|
browser: bot.browser,
|
|
4456
4461
|
chiefOfStaff: bot.chiefOfStaff,
|
|
@@ -4489,36 +4494,15 @@ async function botOverview(bot) {
|
|
|
4489
4494
|
recent,
|
|
4490
4495
|
});
|
|
4491
4496
|
}
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
* uses the receiving bot's grant, never the sender's (approvalModeForOrigin) —
|
|
4497
|
-
* with one deliberate exception: a Chief of Staff with Full access makes the
|
|
4498
|
-
* threads it delegates Full too (delegatedFullAccess), so the grant the
|
|
4499
|
-
* person gave the Chief covers the work the Chief hands out. */
|
|
4497
|
+
// Enforce engine permissions on every turn, including delegated work.
|
|
4498
|
+
const botWorkModes = new BotWorkModes(join(DATA_DIR, "bot-work-modes.json"));
|
|
4499
|
+
for (const id of botWorkModes.expired(id => sessions.isLive(id)))
|
|
4500
|
+
store.setAllThreadApprovalMode(id, "ask", true);
|
|
4500
4501
|
const approvalModeForTurn = (bot, peerInitiated = false) => godModeElevates(bot) ? "full" : ownApprovalModeForTurn(bot, peerInitiated);
|
|
4501
4502
|
/** The bot's (or thread's) own level for a turn, GOD MODE aside. */
|
|
4502
|
-
const ownApprovalModeForTurn = (bot, peerInitiated = false) =>
|
|
4503
|
-
const mode = approvalModeForOrigin(approvalModeFor(bot), { peerInitiated });
|
|
4504
|
-
if (!supportsApprovalMode(registry.cliTarget(bot.modelSelection.instanceId)?.driverKind, mode)) {
|
|
4505
|
-
return "ask";
|
|
4506
|
-
}
|
|
4507
|
-
return mode;
|
|
4508
|
-
};
|
|
4509
|
-
/** GOD MODE covers this bot or thread: the switch is on, this server allows
|
|
4510
|
-
* it, and no approval change of this bot's own is still being confirmed
|
|
4511
|
-
* (approvalModeFor keeps that inert as Ask, and so does GOD MODE). It is
|
|
4512
|
-
* decided per request and never written to a bot record, so turning it off
|
|
4513
|
-
* returns every bot to its own level. Every engine is covered: one that
|
|
4514
|
-
* implements Full runs its turns at Full (godModeElevates); on any other the
|
|
4515
|
-
* harness answers each tool permission request the engine sends
|
|
4516
|
-
* (request.opened), so no engine keeps asking under GOD MODE. */
|
|
4503
|
+
const ownApprovalModeForTurn = (bot, peerInitiated = false) => ownBotApprovalMode(bot, registry.cliTarget(bot.modelSelection.instanceId)?.driverKind, peerInitiated, botWorkModes.permits(approvalModeFor(bot)));
|
|
4517
4504
|
function godModeCovers(bot) {
|
|
4518
|
-
|
|
4519
|
-
return false;
|
|
4520
|
-
const grant = bot.approvalGrant;
|
|
4521
|
-
return !(grant && !(grant.threadOnly && grant.threadId !== bot.threadId));
|
|
4505
|
+
return legacyGodModeCovers(bot, godModeActive() && botWorkModes.permits("full"), botWorkModes.selected(bot.id) !== undefined);
|
|
4522
4506
|
}
|
|
4523
4507
|
/** GOD MODE runs this bot or thread at Full, whatever its own level says:
|
|
4524
4508
|
* it is covered (godModeCovers) and its engine implements Full. */
|
|
@@ -4563,7 +4547,7 @@ function peerReviewRequired(bot, threadId) {
|
|
|
4563
4547
|
/** "Ask me before contacting other bots" is skipped with Full access on the
|
|
4564
4548
|
* source conversation, and for every bot while GOD MODE is on. */
|
|
4565
4549
|
function peerApprovalWaived(botId, threadId) {
|
|
4566
|
-
return
|
|
4550
|
+
return fullAccessForSource(botId, threadId) || Boolean(store.projectBotForTask(botId, threadId) && godModeCovers(store.projectBotForTask(botId, threadId)));
|
|
4567
4551
|
}
|
|
4568
4552
|
/** Full access flows down a Chief of Staff's delegation. The person gave the
|
|
4569
4553
|
* Chief Full access so its work runs without prompts; a teammate stopping
|
|
@@ -4577,7 +4561,7 @@ function peerApprovalWaived(botId, threadId) {
|
|
|
4577
4561
|
function delegatedFullAccess(from, fromThreadId, target) {
|
|
4578
4562
|
// Chibi's GOD MODE never reaches the Private Bot, and the Private Bot's
|
|
4579
4563
|
// never reaches Chibi: each runs a linked handoff at its own level.
|
|
4580
|
-
if (isPersonalBot(from) !== isPersonalBot(target))
|
|
4564
|
+
if (isPersonalBot(from) !== isPersonalBot(target) || approvalModeFor(target) === "plan")
|
|
4581
4565
|
return false;
|
|
4582
4566
|
return delegationInheritsFullAccess({
|
|
4583
4567
|
senderIsChief: Boolean(from.chiefOfStaff),
|
|
@@ -4681,7 +4665,7 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
4681
4665
|
const grantBusy = (bot) => bot.approvalGrant?.threadOnly
|
|
4682
4666
|
? threadBusy(bot.id, bot.approvalGrant.threadId) : bot.busy ||
|
|
4683
4667
|
Boolean(bot.approvalGrant?.allThreads && store.tasks(bot.id).some(task => threadBusy(bot.id, task.threadId)));
|
|
4684
|
-
const grantSupported = (bot, mode) => supportsApprovalMode(registry.cliTarget(grantTarget(bot)?.modelSelection.instanceId ?? "")?.driverKind, mode) &&
|
|
4668
|
+
const grantSupported = (bot, mode) => botWorkModes.permits(mode) && supportsApprovalMode(registry.cliTarget(grantTarget(bot)?.modelSelection.instanceId ?? "")?.driverKind, mode) &&
|
|
4685
4669
|
(!bot.approvalGrant?.allThreads || store.tasks(bot.id).every(task => supportsApprovalMode(registry.cliTarget((task.modelSelection ?? bot.modelSelection).instanceId)?.driverKind, mode)));
|
|
4686
4670
|
const clearGrant = (bot) => store.patchBot(bot.id, {
|
|
4687
4671
|
...(!bot.approvalGrant?.threadOnly ? { approvalMode: "ask", autoApprove: false } : {}),
|
|
@@ -4896,6 +4880,10 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
4896
4880
|
respond({ ok: false, error: "The approval mode is invalid" });
|
|
4897
4881
|
return true;
|
|
4898
4882
|
}
|
|
4883
|
+
if (!botWorkModes.permits(mode)) {
|
|
4884
|
+
respond({ ok: false, error: "This mode is disabled in Settings" });
|
|
4885
|
+
return true;
|
|
4886
|
+
}
|
|
4899
4887
|
const existing = store.bot(botId);
|
|
4900
4888
|
if (!existing) {
|
|
4901
4889
|
respond({ ok: false, error: "No such bot" });
|
|
@@ -4903,15 +4891,16 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
4903
4891
|
}
|
|
4904
4892
|
const currentMode = approvalModeFor(existing);
|
|
4905
4893
|
const threadId = message.threadId;
|
|
4906
|
-
if (
|
|
4907
|
-
(
|
|
4908
|
-
|
|
4909
|
-
|
|
4894
|
+
if (!trustedPlanChangeForStore(store, botId, mode, message)) {
|
|
4895
|
+
respond({ ok: false, error: "Approve the plan in the app before leaving Plan" });
|
|
4896
|
+
return true;
|
|
4897
|
+
}
|
|
4898
|
+
if (invalidAllThreadScope(mode, message)) {
|
|
4910
4899
|
respond({ ok: false, error: "Invalid all-threads approval scope" });
|
|
4911
4900
|
return true;
|
|
4912
4901
|
}
|
|
4913
4902
|
if (message.allThreads === true && mode === "ask") {
|
|
4914
|
-
const updated = store.setAllThreadApprovalMode(botId, "ask");
|
|
4903
|
+
const updated = store.setAllThreadApprovalMode(botId, "ask", message.approvePlan !== true);
|
|
4915
4904
|
void stopBotForEmergencyApprovalDowngrade(botId).then(() => respond({ ok: true, bot: wireBot(store.bot(botId) ?? updated) }), () => respond({ ok: false, error: "Approval was reset to Ask, but an active turn could not be stopped" }));
|
|
4916
4905
|
return true;
|
|
4917
4906
|
}
|
|
@@ -6048,6 +6037,10 @@ function closeSessionStreams(sessionId) {
|
|
|
6048
6037
|
}
|
|
6049
6038
|
sessions.onSessionRevoked((sessionId) => {
|
|
6050
6039
|
providerAuthSessions.revokeOwner(sessionId);
|
|
6040
|
+
for (const id of botWorkModes.revoke(sessionId)) {
|
|
6041
|
+
store.setAllThreadApprovalMode(id, "ask", true);
|
|
6042
|
+
void interruptBotWork(id);
|
|
6043
|
+
}
|
|
6051
6044
|
closeSessionStreams(sessionId);
|
|
6052
6045
|
});
|
|
6053
6046
|
/** Every frame is numbered, and the last few hundred are kept, so a client
|
|
@@ -10051,7 +10044,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10051
10044
|
// the bot's first tool call (desktop attachments are host paths).
|
|
10052
10045
|
const attachedText = commsDepth === 0 ? await withZipListings(providerText) : providerText;
|
|
10053
10046
|
const setupText = agentsMounted ? expandSetupTurnText(attachedText) : attachedText;
|
|
10054
|
-
const userTurnText = withAssistantProvenance(promptWithReply(
|
|
10047
|
+
const userTurnText = withAssistantProvenance(promptWithReply(expandSkillTurnText(setupText, skillAuthoring), opts?.replyTo, cfg.profile?.name?.trim() || "User"), userMessage.sender, cfg.profile?.name?.trim() || "the person");
|
|
10055
10048
|
// Decided again at dispatch when setup outlasted a soul edit (config).
|
|
10056
10049
|
const decideContext = (config) => {
|
|
10057
10050
|
const handedStale = Boolean(handed && (!unseen || (externalUpdate && handed.config !== config)));
|
|
@@ -10817,7 +10810,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10817
10810
|
text: dispatchContext.turnText,
|
|
10818
10811
|
refreshSystemPrompt: true,
|
|
10819
10812
|
images: turnImages,
|
|
10820
|
-
approvalMode: approvalModeForTurn(bot, commsDepth > 0),
|
|
10813
|
+
approvalMode: dispatchWorkMode(approvalModeForTurn(bot, commsDepth > 0), registry.cliTarget(instance.instanceId)?.driverKind),
|
|
10821
10814
|
model,
|
|
10822
10815
|
effort,
|
|
10823
10816
|
variant,
|
|
@@ -12546,7 +12539,7 @@ failover) {
|
|
|
12546
12539
|
.filter(Boolean)
|
|
12547
12540
|
.join("\n");
|
|
12548
12541
|
const latestUserText = usesNativeImageInput ? resolvedLatestImages.text : latestUser?.text;
|
|
12549
|
-
const learnTurn =
|
|
12542
|
+
const learnTurn = latestUserText ? expandSkillTurnText(latestUserText, skillAuthoring) : "";
|
|
12550
12543
|
const learnBlock = learnTurn && learnTurn !== latestUserText ? `\n\n${learnTurn}` : "";
|
|
12551
12544
|
const addressedRequest = orchestration?.roomHandoffId ? roomHandoffs.nodes.get(orchestration.roomHandoffId) : undefined;
|
|
12552
12545
|
// The room transcript already carries recent requests and reports. Repeat
|
|
@@ -12769,7 +12762,7 @@ failover) {
|
|
|
12769
12762
|
text,
|
|
12770
12763
|
refreshSystemPrompt: true,
|
|
12771
12764
|
images: turnImages,
|
|
12772
|
-
approvalMode: roomTurnApprovalMode(readyBot, orchestration),
|
|
12765
|
+
approvalMode: dispatchWorkMode(roomTurnApprovalMode(readyBot, orchestration), registry.cliTarget(instance.instanceId)?.driverKind),
|
|
12773
12766
|
system: roomSystem.text,
|
|
12774
12767
|
systemStable: roomSystem.stable,
|
|
12775
12768
|
systemVolatile: roomSystem.volatile,
|
|
@@ -13610,8 +13603,9 @@ function handoffAuthorizations(from, sourceThreadId, raw) {
|
|
|
13610
13603
|
conversationBotName: from.name,
|
|
13611
13604
|
});
|
|
13612
13605
|
}
|
|
13613
|
-
/**
|
|
13614
|
-
|
|
13606
|
+
/** Test and Reconnect share a single browser-allowed probe per connection.
|
|
13607
|
+
* Bot MCP sessions remain independent to preserve request routing. */
|
|
13608
|
+
const interactiveMcpProbe = createInteractiveMcpProbe();
|
|
13615
13609
|
/** The interview's view of this computer (GET /api/bot-interview/context):
|
|
13616
13610
|
* only tools that exist here, each with a plain status. The Personal Bot
|
|
13617
13611
|
* sees only connections marked Personal; a company bot never sees those. */
|
|
@@ -16043,6 +16037,24 @@ ROUTES.push(createMcpMarketplaceRoutes({
|
|
|
16043
16037
|
fromBot: (req) => authorizedInternalCapability(req.headers.authorization) !== null,
|
|
16044
16038
|
sessionExpired: (auth) => auth.kind === "session" && !sessions.isLive(auth.session.id),
|
|
16045
16039
|
}));
|
|
16040
|
+
ROUTES.push(createBotWorkModeRoutes({
|
|
16041
|
+
modes: botWorkModes, desktopManaged: DESKTOP_MANAGED, live: id => sessions.isLive(id),
|
|
16042
|
+
bot: id => {
|
|
16043
|
+
const bot = store.bot(id);
|
|
16044
|
+
if (!bot)
|
|
16045
|
+
return undefined;
|
|
16046
|
+
const targets = [bot, ...store.tasks(id).map(task => store.projectBotForTask(id, task.threadId)).filter(Boolean)];
|
|
16047
|
+
return { busy: targets.some(target => threadBusy(id, target.threadId)),
|
|
16048
|
+
revision: JSON.stringify(targets.map(target => [target.threadId, target.modelSelection, target.approvalMode, target.approvalGrant, store.messagesFor(target.threadId).at(-1)?.id])),
|
|
16049
|
+
drivers: targets.map(target => registry.cliTarget(target.modelSelection.instanceId)?.driverKind),
|
|
16050
|
+
current: targets.some(target => approvalModeFor(target) === "plan") ? "plan" : wireBot(bot).workMode ?? "ask" };
|
|
16051
|
+
},
|
|
16052
|
+
stop: interruptBotWork,
|
|
16053
|
+
apply: (id, mode, preservePlan) => { store.setAllThreadApprovalMode(id, mode, preservePlan); },
|
|
16054
|
+
affected: allowed => store.bots.filter(bot => [bot, ...store.tasks(bot.id).map(task => store.projectBotForTask(bot.id, task.threadId)).filter(Boolean)].some(target => ![...allowed, "auto", "custom", "plan"].includes(godModeCovers(target) ? "full" : approvalModeFor(target)))).map(bot => bot.id),
|
|
16055
|
+
changed: id => { const bot = store.bot(id); if (bot)
|
|
16056
|
+
broadcast({ kind: "bot", bot: wireBot(bot) }); },
|
|
16057
|
+
}));
|
|
16046
16058
|
// Settings → AI & accounts → Local models: Ollama's models, pulls and removals.
|
|
16047
16059
|
ROUTES.push(createLocalModelRoutes({
|
|
16048
16060
|
ollamaUrl: process.env.OMB_OLLAMA_URL || undefined,
|
|
@@ -16158,6 +16170,14 @@ ROUTES.push(createThreadStopRoutes({
|
|
|
16158
16170
|
routines?.activeBotRunForBot(botId)?.threadId === threadId,
|
|
16159
16171
|
stop: stopBotThread,
|
|
16160
16172
|
}));
|
|
16173
|
+
const sharedDeliverables = new SharedDeliverables(DATA_DIR);
|
|
16174
|
+
const deliverableAccess = (senderId, auth) => sharedDeliverableAccess(sharedDeliverables, {
|
|
16175
|
+
dataDir: DATA_DIR, pawRoot: paw.root, store, senderId,
|
|
16176
|
+
canSeeBot: auth ? (id) => visibleTo(viewerFor(auth)).bot(id) : () => true,
|
|
16177
|
+
canSeeGroup: auth ? (id) => visibleTo(viewerFor(auth)).group(id) : () => true,
|
|
16178
|
+
});
|
|
16179
|
+
ROUTES.push(createSharedDeliverablesRoutes(sharedDeliverables, () => deliverableAccess(undefined).sharedBlockedRoots ?? []));
|
|
16180
|
+
ROUTES.push(createMessageFileRoutes({ store, attachments: ATTACHMENTS_DIR, workspaceDir, access: deliverableAccess }));
|
|
16161
16181
|
ROUTES.push(createPinOrderRoutes({ store, visible: (auth) => visibleTo(viewerFor(auth)), personalHidden: personalBotHidden, fromBot: (req) => authorizedInternalCapability(req.headers.authorization) !== null }));
|
|
16162
16182
|
// Linked repos: Settings > Repos, per-thread links and worktrees. See
|
|
16163
16183
|
// server/routes/repos.ts for the routes and docs/chibitek/README.md.
|
|
@@ -19204,118 +19224,6 @@ const handleRequest = async (req, res) => {
|
|
|
19204
19224
|
});
|
|
19205
19225
|
return res.end(bytes);
|
|
19206
19226
|
}
|
|
19207
|
-
// Download one local file only when this exact stored message grants it:
|
|
19208
|
-
// a bot must render a Markdown link or carry a generated-image attachment,
|
|
19209
|
-
// while a user message must carry the standalone composer tag. The bot
|
|
19210
|
-
// branch derives conversation/workspace roots; the user branch is limited
|
|
19211
|
-
// to OpenMausBot's private attachment directory. This is deliberately not
|
|
19212
|
-
// a general path reader.
|
|
19213
|
-
m = path.match(/^\/api\/threads\/([\w-]+)\/messages\/([\w-]+)\/file$/);
|
|
19214
|
-
const streamsMessageImage = Boolean(m && method === "GET" && url.searchParams.get("preview") === "1");
|
|
19215
|
-
if (m && (method === "POST" || streamsMessageImage)) {
|
|
19216
|
-
if (method === "POST" && !String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
19217
|
-
return json(res, 415, { error: "content-type must be application/json" });
|
|
19218
|
-
}
|
|
19219
|
-
const threadId = m[1];
|
|
19220
|
-
const directBot = store.botByThread(threadId);
|
|
19221
|
-
const group = directBot ? undefined : store.groupByThread(threadId);
|
|
19222
|
-
if (!directBot && !group)
|
|
19223
|
-
return json(res, 404, { error: "no such conversation" });
|
|
19224
|
-
const message = store.messagesFor(threadId).find((candidate) => candidate.id === m[2]);
|
|
19225
|
-
if (!message)
|
|
19226
|
-
return json(res, 404, { error: "no such message" });
|
|
19227
|
-
if (message.kind !== "text") {
|
|
19228
|
-
return json(res, 403, { error: "that message does not share this file" });
|
|
19229
|
-
}
|
|
19230
|
-
const body = method === "POST" ? await readBody(req) : null;
|
|
19231
|
-
const rawReference = streamsMessageImage ? url.searchParams.get("ref") : null;
|
|
19232
|
-
if (streamsMessageImage && (!rawReference || !/^\d+$/.test(rawReference))) {
|
|
19233
|
-
return json(res, 400, { error: "ref must identify a rendered image" });
|
|
19234
|
-
}
|
|
19235
|
-
const href = method === "POST"
|
|
19236
|
-
? (typeof body.path === "string" ? body.path : "")
|
|
19237
|
-
: messageImageTargetAt(message.text ?? "", Number(rawReference));
|
|
19238
|
-
if (!href)
|
|
19239
|
-
return json(res, 400, { error: "path is required" });
|
|
19240
|
-
// Generated image paths are durable capabilities on this exact message.
|
|
19241
|
-
// They do not grant access to arbitrary workspace files or Markdown refs.
|
|
19242
|
-
const generatedImage = method === "POST" && message.role === "bot" &&
|
|
19243
|
-
message.attachments?.some((attachment) => attachment.kind === "image" && attachment.path === href) === true;
|
|
19244
|
-
let roots;
|
|
19245
|
-
let downloadName;
|
|
19246
|
-
if (generatedImage) {
|
|
19247
|
-
roots = [ATTACHMENTS_DIR];
|
|
19248
|
-
}
|
|
19249
|
-
else if (message.role === "user") {
|
|
19250
|
-
downloadName = messageAttachmentName(message.text ?? "", href) ?? undefined;
|
|
19251
|
-
if (!downloadName) {
|
|
19252
|
-
return json(res, 403, { error: "that message does not share this file" });
|
|
19253
|
-
}
|
|
19254
|
-
roots = [ATTACHMENTS_DIR];
|
|
19255
|
-
}
|
|
19256
|
-
else {
|
|
19257
|
-
if (!messageReferencesFile(message.text ?? "", href)) {
|
|
19258
|
-
return json(res, 403, { error: "that bot message does not link to this file" });
|
|
19259
|
-
}
|
|
19260
|
-
const senderId = directBot?.id ?? message.from?.botId;
|
|
19261
|
-
// The persisted bot-role message is the author record. Membership is
|
|
19262
|
-
// intentionally not consulted: removing a bot must not break files it
|
|
19263
|
-
// already shared in channel history.
|
|
19264
|
-
if (!senderId) {
|
|
19265
|
-
return json(res, 403, { error: "the file's bot author could not be verified" });
|
|
19266
|
-
}
|
|
19267
|
-
let pinnedCwd;
|
|
19268
|
-
let configuredCwd;
|
|
19269
|
-
if (directBot) {
|
|
19270
|
-
pinnedCwd = store.taskByThread(directBot.id, threadId)?.cwd;
|
|
19271
|
-
configuredCwd = directBot.cwd;
|
|
19272
|
-
}
|
|
19273
|
-
else if (group) {
|
|
19274
|
-
const task = store.groupTaskByThread(group.id, threadId);
|
|
19275
|
-
pinnedCwd = task ? task.pinnedCwd : group.threadId === threadId ? group.pinnedCwd : undefined;
|
|
19276
|
-
configuredCwd = group.cwd;
|
|
19277
|
-
}
|
|
19278
|
-
roots = messageFileRoots({
|
|
19279
|
-
senderWorkspace: workspaceDir(senderId),
|
|
19280
|
-
attachments: ATTACHMENTS_DIR,
|
|
19281
|
-
pinnedCwd,
|
|
19282
|
-
configuredCwd,
|
|
19283
|
-
});
|
|
19284
|
-
}
|
|
19285
|
-
const file = await openMessageFile(href, roots);
|
|
19286
|
-
if ((streamsMessageImage || generatedImage) && !file.mime.startsWith("image/")) {
|
|
19287
|
-
await file.handle.close();
|
|
19288
|
-
return json(res, 415, { error: "only images can be previewed here" });
|
|
19289
|
-
}
|
|
19290
|
-
res.writeHead(200, {
|
|
19291
|
-
"content-type": file.mime,
|
|
19292
|
-
"content-length": String(file.bytes),
|
|
19293
|
-
...(streamsMessageImage
|
|
19294
|
-
? { "content-disposition": "inline" }
|
|
19295
|
-
: { "content-disposition": messageFileDisposition(messageFileDownloadName(downloadName, file.name)) }),
|
|
19296
|
-
"cache-control": streamsMessageImage ? "private, max-age=3600" : "private, no-store",
|
|
19297
|
-
"cdn-cache-control": "no-store",
|
|
19298
|
-
"cloudflare-cdn-cache-control": "no-store",
|
|
19299
|
-
pragma: "no-cache",
|
|
19300
|
-
vary: "Authorization",
|
|
19301
|
-
"x-content-type-options": "nosniff",
|
|
19302
|
-
...(streamsMessageImage
|
|
19303
|
-
? {
|
|
19304
|
-
"cross-origin-resource-policy": "same-origin",
|
|
19305
|
-
"referrer-policy": "no-referrer",
|
|
19306
|
-
}
|
|
19307
|
-
: {}),
|
|
19308
|
-
});
|
|
19309
|
-
if (file.bytes === 0) {
|
|
19310
|
-
await file.handle.close();
|
|
19311
|
-
return res.end();
|
|
19312
|
-
}
|
|
19313
|
-
const stream = file.handle.createReadStream({ start: 0, end: file.bytes - 1, autoClose: true });
|
|
19314
|
-
stream.on("error", () => res.destroy());
|
|
19315
|
-
res.on("close", () => stream.destroy());
|
|
19316
|
-
stream.pipe(res);
|
|
19317
|
-
return;
|
|
19318
|
-
}
|
|
19319
19227
|
// ── image attachments ────────────────────────────────────────────────
|
|
19320
19228
|
// Pasted/dropped images are stored as files and referenced by path in
|
|
19321
19229
|
// the prompt (<attached-image path="…"/>); this pair of routes is the
|
|
@@ -21283,8 +21191,12 @@ const handleRequest = async (req, res) => {
|
|
|
21283
21191
|
else {
|
|
21284
21192
|
requestedApprovalMode = approvalModeFor(existingBot ?? {});
|
|
21285
21193
|
}
|
|
21194
|
+
if (!botWorkModes.permits(requestedApprovalMode))
|
|
21195
|
+
return json(res, 403, { error: "This mode is disabled in Settings" });
|
|
21286
21196
|
const currentApprovalMode = approvalModeFor(existingBot ?? {});
|
|
21287
21197
|
const approvalChangeRequested = body.approvalMode !== undefined || body.autoApprove !== undefined;
|
|
21198
|
+
if (currentApprovalMode === "plan" && requestedApprovalMode !== "plan")
|
|
21199
|
+
return json(res, 403, { error: "Approve the plan in the app before leaving Plan" });
|
|
21288
21200
|
if (existingBot?.busy && approvalChangeRequested && requestedApprovalMode !== currentApprovalMode) {
|
|
21289
21201
|
return json(res, 409, {
|
|
21290
21202
|
error: "stop this bot's turn before changing its approval level",
|
|
@@ -21299,13 +21211,13 @@ const handleRequest = async (req, res) => {
|
|
|
21299
21211
|
const targetSelection = normalizedSelection ?? existingBot?.modelSelection;
|
|
21300
21212
|
if (normalizedSelection && selectedTask) {
|
|
21301
21213
|
const mode = approvalModeFor(selectedTask);
|
|
21302
|
-
if ((mode === "full" || mode === "custom") &&
|
|
21214
|
+
if ((mode === "full" || mode === "custom" || mode === "plan") &&
|
|
21303
21215
|
(!supportsApprovalMode(registry.cliTarget(normalizedSelection.instanceId)?.driverKind, mode) ||
|
|
21304
21216
|
registry.cliTarget(normalizedSelection.instanceId)?.driverKind !== registry.cliTarget(selectedTask.modelSelection.instanceId)?.driverKind)) {
|
|
21305
21217
|
return json(res, 400, { error: "Choose Ask for the selected thread before changing providers with elevated permissions" });
|
|
21306
21218
|
}
|
|
21307
21219
|
}
|
|
21308
|
-
if ((requestedApprovalMode === "full" || requestedApprovalMode === "custom") &&
|
|
21220
|
+
if ((requestedApprovalMode === "full" || requestedApprovalMode === "custom" || requestedApprovalMode === "plan") &&
|
|
21309
21221
|
(body.approvalMode !== undefined || normalizedSelection !== undefined) &&
|
|
21310
21222
|
(!targetSelection || !supportsApprovalMode(registry.cliTarget(targetSelection.instanceId)?.driverKind, requestedApprovalMode) ||
|
|
21311
21223
|
(existingBot && normalizedSelection && registry.cliTarget(normalizedSelection.instanceId)?.driverKind !== registry.cliTarget(existingBot.modelSelection.instanceId)?.driverKind))) {
|
|
@@ -23009,14 +22921,19 @@ const handleRequest = async (req, res) => {
|
|
|
23009
22921
|
if (!checked.ok)
|
|
23010
22922
|
return json(res, checked.status, { error: checked.error });
|
|
23011
22923
|
patch.modelSelection = checked.selection;
|
|
22924
|
+
if (approvalModeFor(current) === "plan" && !supportsApprovalMode(registry.cliTarget(checked.selection.instanceId)?.driverKind, "plan"))
|
|
22925
|
+
return json(res, 400, { error: "Choose Ask before switching from Plan to this engine" });
|
|
23012
22926
|
}
|
|
23013
22927
|
if (body.approvalMode !== undefined || body.autoApprove !== undefined) {
|
|
23014
22928
|
if (body.autoApprove !== undefined && typeof body.autoApprove !== "boolean")
|
|
23015
22929
|
return json(res, 400, { error: "autoApprove must be a boolean" });
|
|
23016
22930
|
const mode = body.approvalMode ?? (body.autoApprove ? "auto" : "ask");
|
|
23017
|
-
|
|
23018
|
-
|
|
23019
|
-
if (
|
|
22931
|
+
if (!botWorkModes.permits(mode))
|
|
22932
|
+
return json(res, 403, { error: "This mode is disabled in Settings" });
|
|
22933
|
+
if (approvalModeFor(current) === "plan" && mode !== "plan")
|
|
22934
|
+
return json(res, 403, { error: "Approve the plan in the app before leaving Plan" });
|
|
22935
|
+
// A settings PATCH cannot manufacture an elevated grant.
|
|
22936
|
+
if (mode !== "ask" && mode !== "auto" && mode !== "edits" && mode !== "plan")
|
|
23020
22937
|
return json(res, 403, { error: "Full and Custom access require trusted desktop confirmation" });
|
|
23021
22938
|
if (approvalModeFor(current) === "custom")
|
|
23022
22939
|
return json(res, 403, { error: "Leaving Custom approval requires confirmation in the packaged desktop app" });
|
|
@@ -23995,7 +23912,7 @@ const handleRequest = async (req, res) => {
|
|
|
23995
23912
|
const testBlocked = mcpLaunchBlocker(parsed.server);
|
|
23996
23913
|
if (testBlocked)
|
|
23997
23914
|
return json(res, 409, { error: `${mcpTest[1]} cannot start yet: ${mcpLaunchBlockerReason(testBlocked)}`, code: testBlocked });
|
|
23998
|
-
if (mcpProbesInFlight >= MAX_CONCURRENT_MCP_PROBES) {
|
|
23915
|
+
if (mcpProbesInFlight >= MAX_CONCURRENT_MCP_PROBES && !interactiveMcpProbe.has(bridgedMcpServer(parsed.server, true))) {
|
|
23999
23916
|
return json(res, 429, { error: "Two MCP connection tests are already running." });
|
|
24000
23917
|
}
|
|
24001
23918
|
const controller = new AbortController();
|
|
@@ -24006,11 +23923,9 @@ const handleRequest = async (req, res) => {
|
|
|
24006
23923
|
res.once("close", disconnect);
|
|
24007
23924
|
mcpProbesInFlight += 1;
|
|
24008
23925
|
try {
|
|
24009
|
-
//
|
|
24010
|
-
//
|
|
24011
|
-
const
|
|
24012
|
-
// A person's click: the one start besides Reconnect that may open a browser.
|
|
24013
|
-
const probed = await probeMcpServer(bridgedMcpServer(parsed.server, true), connecting ? MCP_SIGN_IN_TIMEOUT_MS : undefined, controller.signal);
|
|
23926
|
+
// Test may open a browser even without ?connect=1. Keep its callback
|
|
23927
|
+
// listener alive for the same MFA window as Reconnect.
|
|
23928
|
+
const probed = await interactiveMcpProbe(bridgedMcpServer(parsed.server, true), controller.signal);
|
|
24014
23929
|
if (probed.ok) {
|
|
24015
23930
|
mcpAuthState.noteOk(mcpTest[1]);
|
|
24016
23931
|
mcpHealth.noteOk(mcpTest[1]);
|
|
@@ -24049,7 +23964,7 @@ const handleRequest = async (req, res) => {
|
|
|
24049
23964
|
const reconnectBlocked = mcpLaunchBlocker(parsed.server);
|
|
24050
23965
|
if (reconnectBlocked)
|
|
24051
23966
|
return json(res, 409, { error: `${name} cannot start yet: ${mcpLaunchBlockerReason(reconnectBlocked)}`, code: reconnectBlocked });
|
|
24052
|
-
if (mcpProbesInFlight >= MAX_CONCURRENT_MCP_PROBES) {
|
|
23967
|
+
if (mcpProbesInFlight >= MAX_CONCURRENT_MCP_PROBES && !interactiveMcpProbe.has(bridgedMcpServer(parsed.server, true))) {
|
|
24053
23968
|
return json(res, 429, { error: "Two MCP connection tests are already running." });
|
|
24054
23969
|
}
|
|
24055
23970
|
const controller = new AbortController();
|
|
@@ -24060,7 +23975,7 @@ const handleRequest = async (req, res) => {
|
|
|
24060
23975
|
res.once("close", disconnect);
|
|
24061
23976
|
mcpProbesInFlight += 1;
|
|
24062
23977
|
try {
|
|
24063
|
-
const probed = await
|
|
23978
|
+
const probed = await interactiveMcpProbe(bridgedMcpServer(parsed.server, true), controller.signal);
|
|
24064
23979
|
if (probed.ok) {
|
|
24065
23980
|
mcpAuthState.noteOk(name);
|
|
24066
23981
|
mcpHealth.noteOk(name);
|
|
@@ -25120,6 +25035,8 @@ const handleRequest = async (req, res) => {
|
|
|
25120
25035
|
}
|
|
25121
25036
|
return json(res, 405, { error: "method not allowed" });
|
|
25122
25037
|
}
|
|
25038
|
+
if (await remoteDesktopRoute(path, method, req, res, { store, surface: computerPreviewSurface, controlHeld: (id) => botComputerControlSnapshot(id).held }))
|
|
25039
|
+
return;
|
|
25123
25040
|
m = path.match(/^\/api\/bots\/([\w-]+)\/computer\/viewer-close$/);
|
|
25124
25041
|
if (m && method === "POST") {
|
|
25125
25042
|
const bot = computerPreviewBot(m[1], url);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// One browser-allowed probe per resolved connection. Bot MCP sessions remain
|
|
2
|
+
// independent: sharing those would misroute server-initiated requests.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { probeMcpServer } from "./mcp-probe.js";
|
|
5
|
+
import { MCP_SIGN_IN_TIMEOUT_MS } from "./mcp-remote-bridge.js";
|
|
6
|
+
const cancelled = () => ({ ok: false, error: "Connection test was cancelled." });
|
|
7
|
+
function connectionKey(spec) {
|
|
8
|
+
// Include every resolved argument and credential value, never just the
|
|
9
|
+
// display name/URL. Only a digest is retained as the map key, never logged.
|
|
10
|
+
return createHash("sha256").update(JSON.stringify(spec, (_key, value) => {
|
|
11
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
12
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
})).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
export function createInteractiveMcpProbe(probe = probeMcpServer) {
|
|
18
|
+
const pending = new Map();
|
|
19
|
+
const run = (spec, signal) => {
|
|
20
|
+
if (signal?.aborted)
|
|
21
|
+
return Promise.resolve(cancelled());
|
|
22
|
+
const key = connectionKey(spec);
|
|
23
|
+
let entry = pending.get(key);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
entry = { controller, result: Promise.resolve().then(() => probe(spec, MCP_SIGN_IN_TIMEOUT_MS, controller.signal)), callers: 0 };
|
|
27
|
+
pending.set(key, entry);
|
|
28
|
+
const current = entry;
|
|
29
|
+
const clear = () => { if (pending.get(key) === current)
|
|
30
|
+
pending.delete(key); };
|
|
31
|
+
void current.result.then(clear, clear);
|
|
32
|
+
}
|
|
33
|
+
const current = entry;
|
|
34
|
+
current.callers += 1;
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let settled = false;
|
|
37
|
+
const release = () => {
|
|
38
|
+
if (settled)
|
|
39
|
+
return false;
|
|
40
|
+
settled = true;
|
|
41
|
+
signal?.removeEventListener("abort", abort);
|
|
42
|
+
current.callers -= 1;
|
|
43
|
+
if (!current.callers && pending.get(key) === current) {
|
|
44
|
+
pending.delete(key);
|
|
45
|
+
current.controller.abort();
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
const abort = () => { if (release())
|
|
50
|
+
resolve(cancelled()); };
|
|
51
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
52
|
+
void current.result.then((result) => { if (release())
|
|
53
|
+
resolve(result); }, (error) => { if (release())
|
|
54
|
+
reject(error); });
|
|
55
|
+
if (signal?.aborted)
|
|
56
|
+
abort();
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
return Object.assign(run, { has: (spec) => pending.has(connectionKey(spec)) });
|
|
60
|
+
}
|
|
@@ -19,6 +19,7 @@ import { basename, dirname } from "node:path";
|
|
|
19
19
|
/** The bundled bridge's exit code when it needed a browser sign-in and was
|
|
20
20
|
* told not to open one (EX_NOPERM). */
|
|
21
21
|
export const MCP_SIGN_IN_REQUIRED_EXIT = 77;
|
|
22
|
+
export const MCP_SIGN_IN_TIMEOUT_MS = 300_000;
|
|
22
23
|
/** Set on a bridge's environment: never open a browser, exit instead. */
|
|
23
24
|
export const MCP_NO_BROWSER_ENV = "ATOMIC_MCP_NO_BROWSER";
|
|
24
25
|
/** The line the bundled bridge writes to stderr before that exit. */
|
|
@@ -63,6 +64,13 @@ export function bridgeMcpRemote(spec, options) {
|
|
|
63
64
|
const target = mcpRemoteTarget(spec);
|
|
64
65
|
if (!target)
|
|
65
66
|
return spec;
|
|
67
|
+
// A stable redirect host prevents saved OAuth clients becoming invalid when
|
|
68
|
+
// an upstream/platform default changes. Preserve an explicitly configured host.
|
|
69
|
+
const extraArgs = target.extraArgs.includes("--host")
|
|
70
|
+
? [...target.extraArgs]
|
|
71
|
+
: [...target.extraArgs, "--host", "localhost"];
|
|
72
|
+
if (!extraArgs.includes("--auth-timeout"))
|
|
73
|
+
extraArgs.push("--auth-timeout", String(MCP_SIGN_IN_TIMEOUT_MS / 1_000));
|
|
66
74
|
const noBrowser = options.allowBrowser ? {} : { [MCP_NO_BROWSER_ENV]: "1" };
|
|
67
75
|
const env = { ...spec.env };
|
|
68
76
|
// A stale flag from an older start must not outlive a person's click.
|
|
@@ -71,16 +79,15 @@ export function bridgeMcpRemote(spec, options) {
|
|
|
71
79
|
// A bundled entry keeps its own script: the desktop app already repoints
|
|
72
80
|
// it at the current Resources copy on launch (electron/chibitek-defaults.mjs).
|
|
73
81
|
if (target.bundled)
|
|
74
|
-
return { ...spec, env: { ...env, ...noBrowser } };
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// drop a server the person added.
|
|
82
|
+
return { ...spec, args: [spec.args[0], target.url, ...extraArgs], env: { ...env, ...noBrowser } };
|
|
83
|
+
// Keep the redirect stable even in a dev checkout without a built bridge.
|
|
84
|
+
// The unbundled command retains its existing browser behavior.
|
|
78
85
|
if (!options.bridge)
|
|
79
|
-
return spec;
|
|
86
|
+
return { ...spec, args: [...spec.args.slice(0, spec.args.length - target.extraArgs.length), ...extraArgs] };
|
|
80
87
|
return {
|
|
81
88
|
...spec,
|
|
82
89
|
command: options.execPath,
|
|
83
|
-
args: [options.bridge, target.url, ...
|
|
90
|
+
args: [options.bridge, target.url, ...extraArgs],
|
|
84
91
|
env: { ...env, ELECTRON_RUN_AS_NODE: "1", ...noBrowser },
|
|
85
92
|
};
|
|
86
93
|
}
|