openmausbot 0.1.86 → 0.1.88
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 +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -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-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- 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 +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -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 +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- 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 +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- 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/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -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-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- 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 +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- 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 +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- 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 +19 -2
- 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 +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -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 +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- 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
|
@@ -5,6 +5,9 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
7
7
|
import { managedConnectorUnavailableReason } from "../shared/connector-availability.js";
|
|
8
|
+
import { CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
|
|
9
|
+
import { CONNECTOR_ALLOWED_TOOLS_ENV, CONNECTOR_ALLOWED_TOOLS_MAX_BYTES, CONNECTOR_SERVICE_SLUGS_ENV, CONNECTOR_SERVICE_SLUGS_MAX_BYTES, serializeConnectorAllowedTools, serializeConnectorServiceSlugs, } from "./connector-advertisement.js";
|
|
10
|
+
import { serviceSlugFor, serviceSlugForCandidates } from "./connector-verdict.js";
|
|
8
11
|
const DEFAULT_BACKEND_ORIGIN = "https://backend.composio.dev";
|
|
9
12
|
function apiBase() {
|
|
10
13
|
return (process.env.OMB_COMPOSIO_API ?? `${DEFAULT_BACKEND_ORIGIN}/api/v3.1`).replace(/\/$/, "");
|
|
@@ -443,6 +446,30 @@ const NEEDS_AUTH_CONFIG = /does not manage auth|auth[_ ]?config/i;
|
|
|
443
446
|
export async function mcpIntegration(cfg, context) {
|
|
444
447
|
if (!configured(cfg))
|
|
445
448
|
return null;
|
|
449
|
+
// Connector grants 3/5: a grants record rides to the bridge as a capped
|
|
450
|
+
// JSON env var so tools/list is filtered down to what the bot may call.
|
|
451
|
+
// No record (legacy all-tools bots) or one past the cap mounts without
|
|
452
|
+
// the var — the bridge then relays the unfiltered list and every call is
|
|
453
|
+
// still judged by the slice-2 verdict on the relay endpoint.
|
|
454
|
+
const allowlist = context.connectorTools === undefined
|
|
455
|
+
? undefined
|
|
456
|
+
: serializeConnectorAllowedTools(context.connectorTools);
|
|
457
|
+
if (allowlist?.oversized) {
|
|
458
|
+
console.warn(`[composio] connector allowlist for bot ${context.botId} exceeds ${CONNECTOR_ALLOWED_TOOLS_MAX_BYTES} bytes;`
|
|
459
|
+
+ " tools/list stays unfiltered and grants are enforced per call");
|
|
460
|
+
}
|
|
461
|
+
// The connected-service slugs ride alongside the allowlist so the
|
|
462
|
+
// bridge resolves underscored services (bland_ai) instead of letting a
|
|
463
|
+
// plain-prefix grant (bland) widen them. An unreachable catalog omits
|
|
464
|
+
// the var and the filter keeps its plain-split fallback; legacy
|
|
465
|
+
// all-tools bots mount without it because their list is unfiltered.
|
|
466
|
+
const serviceSlugs = context.connectorTools === undefined
|
|
467
|
+
? undefined
|
|
468
|
+
: serializeConnectorServiceSlugs(await connectedServiceSlugs(cfg));
|
|
469
|
+
if (serviceSlugs?.oversized) {
|
|
470
|
+
console.warn(`[composio] connector service slugs for bot ${context.botId} exceed ${CONNECTOR_SERVICE_SLUGS_MAX_BYTES} bytes;`
|
|
471
|
+
+ " tools/list falls back to plain-prefix resolution");
|
|
472
|
+
}
|
|
446
473
|
return {
|
|
447
474
|
command: process.execPath,
|
|
448
475
|
args: [SPAWNED_PROXIES.connectors],
|
|
@@ -460,6 +487,8 @@ export async function mcpIntegration(cfg, context) {
|
|
|
460
487
|
OMB_CONNECTOR_TOKEN: context.commsToken,
|
|
461
488
|
OMB_BOT_ID: context.botId,
|
|
462
489
|
OMB_THREAD_ID: context.threadId,
|
|
490
|
+
...(allowlist?.env ? { [CONNECTOR_ALLOWED_TOOLS_ENV]: allowlist.env } : {}),
|
|
491
|
+
...(serviceSlugs?.env ? { [CONNECTOR_SERVICE_SLUGS_ENV]: serviceSlugs.env } : {}),
|
|
463
492
|
},
|
|
464
493
|
};
|
|
465
494
|
}
|
|
@@ -672,6 +701,266 @@ export async function connectedServices(cfg) {
|
|
|
672
701
|
]);
|
|
673
702
|
return allServiceStates(summarizeAccounts(accounts, []), toolkits);
|
|
674
703
|
}
|
|
704
|
+
/** Semantic validation for a connectorTools patch (issue #1737): slugs
|
|
705
|
+
* must name a connected service, and every listed tool name must carry its
|
|
706
|
+
* own service prefix (GMAIL_SEND_EMAIL under "gmail") — a mismatched name
|
|
707
|
+
* would be granted yet permanently refused at call time. The prefix check
|
|
708
|
+
* resolves against the connected-service slugs, so an underscored service
|
|
709
|
+
* (bland_ai) keeps its BLAND_AI_* names and a plain-prefix grant cannot
|
|
710
|
+
* capture them; it always runs, degrading to the plain split when the
|
|
711
|
+
* connection inventory is unreachable. Only the connection-dependent and
|
|
712
|
+
* catalog checks fail open — an unreachable connection inventory, the
|
|
713
|
+
* curated fallback, or a catalog walk that did not reach its end never
|
|
714
|
+
* block the patch, because call-time enforcement (slice 2) stays the
|
|
715
|
+
* authority either way. Returns the rejection message or null. */
|
|
716
|
+
export async function validateConnectorGrants(cfg, grants) {
|
|
717
|
+
const connected = await connectedServices(cfg).catch(() => null);
|
|
718
|
+
const candidates = connected ? Object.keys(connected) : [];
|
|
719
|
+
for (const [slug, grant] of Object.entries(grants)) {
|
|
720
|
+
if (grant.tools === "*")
|
|
721
|
+
continue;
|
|
722
|
+
// The prefix check runs against the connected-service slugs, so an
|
|
723
|
+
// underscored service (bland_ai) accepts its own BLAND_AI_* names —
|
|
724
|
+
// and a plain-prefix grant cannot capture them while the real service
|
|
725
|
+
// is connected.
|
|
726
|
+
const misplaced = grant.tools.filter((tool) => (serviceSlugForCandidates(tool, candidates) ?? serviceSlugFor(tool)) !== slug);
|
|
727
|
+
if (misplaced.length) {
|
|
728
|
+
return `connectorTools.${slug}.tools names tools that belong to another service: ${misplaced.join(", ")}`;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (!connected)
|
|
732
|
+
return null;
|
|
733
|
+
const unconnected = Object.keys(grants).filter((slug) => !connected[slug]?.connected);
|
|
734
|
+
if (unconnected.length) {
|
|
735
|
+
return `connectorTools names services that are not connected: ${unconnected.join(", ")}`;
|
|
736
|
+
}
|
|
737
|
+
const catalog = await listToolkits(cfg);
|
|
738
|
+
// source "curated" means the marketplace was unreachable, and a walk that
|
|
739
|
+
// never reached its natural end served only part of it — either way there
|
|
740
|
+
// is nothing trustworthy to cross-check against, and a grant for a service
|
|
741
|
+
// on a page the walk never saw must not be rejected.
|
|
742
|
+
if (catalog.source !== "api" || !catalog.pagination?.complete)
|
|
743
|
+
return null;
|
|
744
|
+
const known = new Set(catalog.cards.map((card) => card.slug.toLowerCase()));
|
|
745
|
+
const unknown = Object.keys(grants).filter((slug) => !known.has(slug));
|
|
746
|
+
if (unknown.length) {
|
|
747
|
+
return `connectorTools names services missing from the connected-apps catalog: ${unknown.join(", ")}`;
|
|
748
|
+
}
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
/** The grant editor's tool inventory, grouped by service slug. Sourced from
|
|
752
|
+
* the same MCP endpoint the bots' bridge relays to (initialize + tools/list
|
|
753
|
+
* through relayMcp), so what the picker offers is exactly what a granted bot
|
|
754
|
+
* would see — in self-hosted and managed modes alike. Platform meta-tools
|
|
755
|
+
* and per-service connection flows are not per-tool grants (they ride along
|
|
756
|
+
* whenever a service is granted), so they never appear in the picker. */
|
|
757
|
+
export async function listConnectorTools(cfg, options = {}) {
|
|
758
|
+
const identity = connectorToolsIdentity(cfg);
|
|
759
|
+
if (!options.force && connectorToolsCache?.identity === identity
|
|
760
|
+
&& Date.now() - connectorToolsCache.at < CONNECTOR_TOOLS_CACHE_MS) {
|
|
761
|
+
return connectorToolsCache.services;
|
|
762
|
+
}
|
|
763
|
+
// Concurrent misses share one MCP walk — two editors opening together
|
|
764
|
+
// must not run the initialize + tools/list sequences twice. force
|
|
765
|
+
// bypasses the settled cache but still joins a walk already running.
|
|
766
|
+
if (connectorToolsRequest?.identity === identity)
|
|
767
|
+
return connectorToolsRequest.services;
|
|
768
|
+
const walk = collectConnectorTools(cfg).then((services) => {
|
|
769
|
+
connectorToolsCache = { at: Date.now(), identity, services };
|
|
770
|
+
return services;
|
|
771
|
+
});
|
|
772
|
+
connectorToolsRequest = { identity, services: walk };
|
|
773
|
+
try {
|
|
774
|
+
return await walk;
|
|
775
|
+
}
|
|
776
|
+
finally {
|
|
777
|
+
if (connectorToolsRequest?.services === walk)
|
|
778
|
+
connectorToolsRequest = null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const CONNECTOR_TOOLS_CACHE_MS = 60_000;
|
|
782
|
+
let connectorToolsCache = null;
|
|
783
|
+
/** Which backend an inventory walk belongs to, mirroring the relay's own
|
|
784
|
+
* credential fingerprint: a project key or the managed broker. Cached
|
|
785
|
+
* inventories are keyed by it, so a config switch can never serve the
|
|
786
|
+
* previous backend's tools for another cache window. */
|
|
787
|
+
function connectorToolsIdentity(cfg) {
|
|
788
|
+
const apiKey = projectApiKey(cfg);
|
|
789
|
+
if (apiKey)
|
|
790
|
+
return backendFingerprint("project-tools", apiBase(), apiKey);
|
|
791
|
+
const broker = brokerAccess();
|
|
792
|
+
return broker ? backendFingerprint("managed-tools", broker.url, broker.token) : "none";
|
|
793
|
+
}
|
|
794
|
+
let connectorToolsRequest = null;
|
|
795
|
+
/** The service slugs grant enforcement resolves tool-name prefixes
|
|
796
|
+
* against: every key of the connection inventory, connected or not,
|
|
797
|
+
* because the real backend slugs are what separates an underscored
|
|
798
|
+
* service (bland_ai) from a plain-prefix grant (bland). Cached like the
|
|
799
|
+
* tool inventory — 60s, keyed by backend identity, one shared in-flight
|
|
800
|
+
* walk — and an unreachable inventory reads as an empty list without
|
|
801
|
+
* caching the failure, so callers fall back to the plain split and the
|
|
802
|
+
* next call retries. */
|
|
803
|
+
export async function connectedServiceSlugs(cfg) {
|
|
804
|
+
const identity = connectorToolsIdentity(cfg);
|
|
805
|
+
if (connectorServiceSlugsCache?.identity === identity
|
|
806
|
+
&& Date.now() - connectorServiceSlugsCache.at < CONNECTOR_SERVICE_SLUGS_CACHE_MS) {
|
|
807
|
+
return connectorServiceSlugsCache.slugs;
|
|
808
|
+
}
|
|
809
|
+
if (connectorServiceSlugsRequest?.identity === identity)
|
|
810
|
+
return connectorServiceSlugsRequest.slugs;
|
|
811
|
+
const walk = connectedServices(cfg).then((services) => {
|
|
812
|
+
const slugs = Object.keys(services);
|
|
813
|
+
connectorServiceSlugsCache = { at: Date.now(), identity, slugs };
|
|
814
|
+
return slugs;
|
|
815
|
+
}, () => []);
|
|
816
|
+
connectorServiceSlugsRequest = { identity, slugs: walk };
|
|
817
|
+
try {
|
|
818
|
+
return await walk;
|
|
819
|
+
}
|
|
820
|
+
finally {
|
|
821
|
+
if (connectorServiceSlugsRequest?.slugs === walk)
|
|
822
|
+
connectorServiceSlugsRequest = null;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const CONNECTOR_SERVICE_SLUGS_CACHE_MS = 60_000;
|
|
826
|
+
let connectorServiceSlugsCache = null;
|
|
827
|
+
let connectorServiceSlugsRequest = null;
|
|
828
|
+
/** Tool names longer than any Composio description worth searching. */
|
|
829
|
+
const TOOL_DESCRIPTION_MAX = 240;
|
|
830
|
+
const TOOLS_LIST_PAGES_MAX = 10;
|
|
831
|
+
function connectorToolListing(tool) {
|
|
832
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool))
|
|
833
|
+
return null;
|
|
834
|
+
const name = tool.name;
|
|
835
|
+
if (typeof name !== "string" || !CONNECTOR_TOOL_NAME_PATTERN.test(name))
|
|
836
|
+
return null;
|
|
837
|
+
if (name.startsWith("COMPOSIO_"))
|
|
838
|
+
return null;
|
|
839
|
+
if (name.endsWith("_MANAGE_CONNECTIONS") || name.endsWith("_WAIT_FOR_CONNECTIONS"))
|
|
840
|
+
return null;
|
|
841
|
+
const description = tool.description;
|
|
842
|
+
const trimmed = typeof description === "string"
|
|
843
|
+
? description.replace(/\s+/g, " ").trim().slice(0, TOOL_DESCRIPTION_MAX)
|
|
844
|
+
: "";
|
|
845
|
+
return trimmed ? { name, description: trimmed } : { name };
|
|
846
|
+
}
|
|
847
|
+
/** One JSON-RPC response frame from a JSON or SSE body, mirroring the
|
|
848
|
+
* bridge's parseUpstream: SSE data lines are tried in order and the frame
|
|
849
|
+
* carrying the request id wins. */
|
|
850
|
+
function parseMcpResponse(text, id) {
|
|
851
|
+
const trimmed = text.trim();
|
|
852
|
+
if (!trimmed)
|
|
853
|
+
return null;
|
|
854
|
+
const candidate = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
855
|
+
if (trimmed.startsWith("{")) {
|
|
856
|
+
const parsed = JSON.parse(trimmed);
|
|
857
|
+
return candidate(parsed) ? parsed : null;
|
|
858
|
+
}
|
|
859
|
+
const frames = trimmed
|
|
860
|
+
.split(/\r?\n/)
|
|
861
|
+
.filter((line) => line.startsWith("data:"))
|
|
862
|
+
.map((line) => line.slice(5).trim())
|
|
863
|
+
.filter((line) => line && line !== "[DONE]")
|
|
864
|
+
.flatMap((line) => {
|
|
865
|
+
try {
|
|
866
|
+
return [JSON.parse(line)];
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
return [];
|
|
870
|
+
}
|
|
871
|
+
})
|
|
872
|
+
.filter(candidate);
|
|
873
|
+
return frames.findLast((frame) => frame.id === id) ?? frames.at(-1) ?? null;
|
|
874
|
+
}
|
|
875
|
+
async function collectConnectorTools(cfg) {
|
|
876
|
+
// Connected services are the real slugs, so they resolve underscored
|
|
877
|
+
// services (bland_ai) the plain split would file under "bland"; an
|
|
878
|
+
// unreachable inventory degrades to that split.
|
|
879
|
+
const connected = await connectedServices(cfg).catch(() => null);
|
|
880
|
+
const candidates = connected ? Object.keys(connected) : [];
|
|
881
|
+
const initialize = await relayMcp(cfg, {
|
|
882
|
+
jsonrpc: "2.0",
|
|
883
|
+
id: "omb-grants-initialize",
|
|
884
|
+
method: "initialize",
|
|
885
|
+
params: {
|
|
886
|
+
protocolVersion: "2025-03-26",
|
|
887
|
+
capabilities: {},
|
|
888
|
+
clientInfo: { name: "openmausbot-grant-editor", version: "1" },
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
if (initialize.status !== 200) {
|
|
892
|
+
throw new Error(await responseErrorFromBytes(initialize.status, initialize.bytes));
|
|
893
|
+
}
|
|
894
|
+
const transportSessionId = initialize.transportSessionId;
|
|
895
|
+
// Streamable-HTTP servers expect the handshake notification before the
|
|
896
|
+
// first request; it carries no response, so failures are not fatal.
|
|
897
|
+
await relayMcp(cfg, {
|
|
898
|
+
jsonrpc: "2.0",
|
|
899
|
+
method: "notifications/initialized",
|
|
900
|
+
}, transportSessionId).catch(() => undefined);
|
|
901
|
+
const grouped = new Map();
|
|
902
|
+
let cursor;
|
|
903
|
+
for (let page = 0; page < TOOLS_LIST_PAGES_MAX; page += 1) {
|
|
904
|
+
const list = await relayMcp(cfg, {
|
|
905
|
+
jsonrpc: "2.0",
|
|
906
|
+
id: "omb-grants-tools",
|
|
907
|
+
method: "tools/list",
|
|
908
|
+
params: cursor ? { cursor } : {},
|
|
909
|
+
}, transportSessionId);
|
|
910
|
+
if (list.status !== 200) {
|
|
911
|
+
throw new Error(await responseErrorFromBytes(list.status, list.bytes));
|
|
912
|
+
}
|
|
913
|
+
const frame = parseMcpResponse(new TextDecoder().decode(list.bytes), "omb-grants-tools");
|
|
914
|
+
const error = frame && typeof frame.error === "object" && frame.error !== null
|
|
915
|
+
? frame.error.message
|
|
916
|
+
: undefined;
|
|
917
|
+
if (typeof error === "string" && error)
|
|
918
|
+
throw new Error(error);
|
|
919
|
+
const result = frame && typeof frame.result === "object" && frame.result !== null
|
|
920
|
+
? frame.result
|
|
921
|
+
: undefined;
|
|
922
|
+
if (!result || !Array.isArray(result.tools)) {
|
|
923
|
+
throw new Error("Composio returned a tools/list response without a tools array");
|
|
924
|
+
}
|
|
925
|
+
for (const tool of result.tools) {
|
|
926
|
+
const listing = connectorToolListing(tool);
|
|
927
|
+
if (!listing)
|
|
928
|
+
continue;
|
|
929
|
+
const slug = serviceSlugForCandidates(listing.name, candidates) ?? serviceSlugFor(listing.name);
|
|
930
|
+
if (!slug)
|
|
931
|
+
continue;
|
|
932
|
+
const bucket = grouped.get(slug) ?? [];
|
|
933
|
+
if (!bucket.some((existing) => existing.name === listing.name))
|
|
934
|
+
bucket.push(listing);
|
|
935
|
+
grouped.set(slug, bucket);
|
|
936
|
+
}
|
|
937
|
+
cursor = typeof result.nextCursor === "string" && result.nextCursor ? result.nextCursor : undefined;
|
|
938
|
+
if (!cursor)
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
for (const bucket of grouped.values())
|
|
942
|
+
bucket.sort((a, b) => a.name.localeCompare(b.name));
|
|
943
|
+
return Object.fromEntries([...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
944
|
+
}
|
|
945
|
+
async function responseErrorFromBytes(status, bytes) {
|
|
946
|
+
try {
|
|
947
|
+
const body = JSON.parse(new TextDecoder().decode(bytes));
|
|
948
|
+
const message = body && typeof body === "object" && !Array.isArray(body)
|
|
949
|
+
? body.error
|
|
950
|
+
: undefined;
|
|
951
|
+
if (message && typeof message === "object" && !Array.isArray(message)) {
|
|
952
|
+
const text = message.message;
|
|
953
|
+
if (typeof text === "string" && text)
|
|
954
|
+
return text;
|
|
955
|
+
}
|
|
956
|
+
if (typeof message === "string" && message)
|
|
957
|
+
return message;
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
// fall through to the generic status line
|
|
961
|
+
}
|
|
962
|
+
return `Composio tools/list: HTTP ${status}`;
|
|
963
|
+
}
|
|
675
964
|
export async function connectionStatus(cfg, slugs) {
|
|
676
965
|
const canonicalSlugs = [...new Set(slugs.map(canonicalToolkitSlug))];
|
|
677
966
|
const apiKey = projectApiKey(cfg);
|
|
@@ -989,9 +1278,16 @@ export async function listToolkits(cfg) {
|
|
|
989
1278
|
const pagingStoppedEarly = lastReportedPage !== undefined
|
|
990
1279
|
&& reportedTotalPages !== undefined
|
|
991
1280
|
&& lastReportedPage < reportedTotalPages;
|
|
992
|
-
const
|
|
1281
|
+
const stalled = shortOfReportedTotal || pagingStoppedEarly;
|
|
1282
|
+
// "end" and "total-reached" are the walk's natural finishes; every
|
|
1283
|
+
// other stop reason left pages unread even when the API reports no
|
|
1284
|
+
// totals for stalled to compare against.
|
|
1285
|
+
const complete = (stop === "end" || stop === "total-reached") && !stalled;
|
|
1286
|
+
const pagination = { items: uniqueCards.length, stalled, complete };
|
|
993
1287
|
if (reportedTotalItems !== undefined)
|
|
994
1288
|
pagination.totalItems = reportedTotalItems;
|
|
1289
|
+
if (pagination.stalled)
|
|
1290
|
+
pagination.reason = stop;
|
|
995
1291
|
if (pagination.stalled) {
|
|
996
1292
|
console.warn(`[composio] marketplace catalog paging stopped early (${stop}) after ${uniqueCards.length} toolkits`
|
|
997
1293
|
+ (reportedTotalItems !== undefined ? ` of ~${reportedTotalItems}` : "")
|
|
@@ -7,8 +7,17 @@
|
|
|
7
7
|
// that, name the holder as a bot running a thread, and keep the same words
|
|
8
8
|
// on every surface (chat chip, room chip, the gate's refusal) so a person
|
|
9
9
|
// who has read it once recognises it everywhere.
|
|
10
|
-
/** Whole minutes, or seconds under one minute,
|
|
11
|
-
const minutes = (ms) =>
|
|
10
|
+
/** Whole minutes, or seconds under one minute, singular at exactly one. */
|
|
11
|
+
const minutes = (ms) => {
|
|
12
|
+
const value = ms >= 60_000 ? Math.round(ms / 60_000) : Math.round(ms / 1000);
|
|
13
|
+
const unit = ms >= 60_000 ? "minute" : "second";
|
|
14
|
+
return `${value} ${unit}${value === 1 ? "" : "s"}`;
|
|
15
|
+
};
|
|
16
|
+
/** How long a wait lasted, for the resolution line: waits under a second
|
|
17
|
+
* are honest about being over in a blink instead of claiming "0 seconds". */
|
|
18
|
+
export function computerWaitDuration(ms) {
|
|
19
|
+
return ms < 1_000 ? "under a second" : minutes(ms);
|
|
20
|
+
}
|
|
12
21
|
const holderPhrase = (holder) => holder.task ? `${holder.name} is running ${holder.task}` : `${holder.name} is using it`;
|
|
13
22
|
/** The chip a turn shows while it is queued behind another turn's desktop. */
|
|
14
23
|
export function computerWaitingText(holder) {
|
|
@@ -16,14 +25,18 @@ export function computerWaitingText(holder) {
|
|
|
16
25
|
return "Waiting for its turn on this computer. Starts automatically when it is free.";
|
|
17
26
|
return `Waiting for its turn on this computer — ${holderPhrase(holder)}. Starts automatically when that finishes.`;
|
|
18
27
|
}
|
|
19
|
-
/** The
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
/** The resolution appended once the wait landed and this turn holds the
|
|
29
|
+
* desktop: how long it waited, and who held it. The waiting chip stays as
|
|
30
|
+
* written — this line is the history beside it, not its replacement. */
|
|
31
|
+
export function computerFreeAfterText(holder, waitedMs) {
|
|
32
|
+
const who = holder ? ` (${holder.task ? `${holder.name} · ${holder.task}` : holder.name} held it)` : "";
|
|
33
|
+
return `Computer free — continuing after waiting ${computerWaitDuration(waitedMs)}${who}`;
|
|
22
34
|
}
|
|
23
|
-
/** The
|
|
24
|
-
*
|
|
25
|
-
export function
|
|
26
|
-
|
|
35
|
+
/** The resolution appended when the wait ended because the turn was stopped
|
|
36
|
+
* or its claim was cancelled. */
|
|
37
|
+
export function computerStoppedWaitingText(holder, waitedMs) {
|
|
38
|
+
const who = holder ? ` — ${holderPhrase(holder)}` : "";
|
|
39
|
+
return `Stopped waiting for the computer after ${computerWaitDuration(waitedMs)}${who}.`;
|
|
27
40
|
}
|
|
28
41
|
/** The error after the wait ceiling: still names who holds it, and says
|
|
29
42
|
* what a person can do — stop that turn, or move this one. */
|
|
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { normalizeImageGenerationUrl } from "../shared/image-generation.js";
|
|
9
9
|
import { writeFileAtomic } from "./atomic.js";
|
|
10
|
+
import { newBotDefaultsSchema } from "./new-bot-defaults.js";
|
|
10
11
|
import { EFFORT_LEVELS } from "../shared/wire.js";
|
|
11
12
|
import { isModelVariant } from "./contracts.js";
|
|
12
13
|
import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-icon.js";
|
|
@@ -32,7 +33,7 @@ export const MAX_THREAD_EVENT_LOG_BYTES = 4 * 1024 * 1024 * 1024;
|
|
|
32
33
|
export const DEFAULT_LOCAL_VM_MODE = "shared";
|
|
33
34
|
export const DEFAULT_LOCAL_VM_MAX_INSTANCES = 2;
|
|
34
35
|
export const MIN_LOCAL_VM_MAX_INSTANCES = 1;
|
|
35
|
-
export const MAX_LOCAL_VM_MAX_INSTANCES =
|
|
36
|
+
export const MAX_LOCAL_VM_MAX_INSTANCES = 8;
|
|
36
37
|
export function isValidSshAlias(value) {
|
|
37
38
|
return typeof value === "string" && SSH_ALIAS.test(value);
|
|
38
39
|
}
|
|
@@ -289,8 +290,8 @@ const instanceConfigSchema = z.object({
|
|
|
289
290
|
});
|
|
290
291
|
const instanceConfigMapSchema = z.record(z.string(), instanceConfigSchema);
|
|
291
292
|
const defaultModelSelectionSchema = z.object({
|
|
292
|
-
instanceId: z.string().trim().min(1),
|
|
293
|
-
model: z.string().trim().min(1),
|
|
293
|
+
instanceId: z.string().trim().min(1).max(200),
|
|
294
|
+
model: z.string().trim().min(1).max(500),
|
|
294
295
|
effort: z.enum(EFFORT_LEVELS).optional(),
|
|
295
296
|
variant: z.string().refine(isModelVariant, "invalid model variant").optional(),
|
|
296
297
|
}).refine((selection) => selection.variant === undefined || selection.effort === undefined, "choose either a model variant or an effort level");
|
|
@@ -303,6 +304,15 @@ const threadsConfigSchema = z.object({
|
|
|
303
304
|
* Absent keeps them forever. */
|
|
304
305
|
eventLogRetentionDays: z.number().int().min(1).max(3650).optional(),
|
|
305
306
|
}).strict();
|
|
307
|
+
/** Workspace-wide defaults every new bot starts with (Store.createBot). */
|
|
308
|
+
const newBotsConfigSchema = z.object({
|
|
309
|
+
/** Effort for a new bot whose model selection names none. */
|
|
310
|
+
effort: z.enum(EFFORT_LEVELS).optional(),
|
|
311
|
+
}).strict();
|
|
312
|
+
/** PATCH newBots: null clears a default back to absent (no level is sent). */
|
|
313
|
+
const newBotsPatchSchema = z.object({
|
|
314
|
+
effort: newBotsConfigSchema.shape.effort.nullable(),
|
|
315
|
+
}).strict();
|
|
306
316
|
/** PATCH threads: every knob is independently patchable, and null clears an
|
|
307
317
|
* event-log knob back to its absent (off) default. */
|
|
308
318
|
const threadsPatchSchema = threadsConfigSchema.extend({
|
|
@@ -317,12 +327,15 @@ const appConfigSchema = z.object({
|
|
|
317
327
|
* addresses or `@domain` entries; admins get every scope, members chat only. */
|
|
318
328
|
signIn: z.object({ admins: z.array(z.string().max(320)).max(500).optional(), members: z.array(z.string().max(320)).max(5000).optional() }).optional(),
|
|
319
329
|
defaultModelSelection: defaultModelSelectionSchema.optional(),
|
|
330
|
+
newBotDefaults: newBotDefaultsSchema.optional(),
|
|
331
|
+
newBots: newBotsConfigSchema.optional(),
|
|
320
332
|
/** CLI-only launch preferences. Never enable remote access implicitly. */
|
|
321
333
|
cliStartup: z.object({
|
|
322
334
|
access: z.enum(["local", "tunnel", "tailscale", "public-url"]),
|
|
323
335
|
publicUrl: z.string().url().optional(),
|
|
324
336
|
phone: z.enum(["ios", "android"]).optional(),
|
|
325
337
|
}).optional(),
|
|
338
|
+
mistral: z.object({ key: optionalText }).optional(),
|
|
326
339
|
xai: z.object({ key: optionalText, url: optionalText }).optional(),
|
|
327
340
|
/** Anthropic API key for Claude Code billed per token, handed only to
|
|
328
341
|
* Claude instances; `url` only for a proxy or a test double. Never a
|
|
@@ -400,8 +413,8 @@ const appConfigSchema = z.object({
|
|
|
400
413
|
}).optional(),
|
|
401
414
|
customModel: z.string().trim().max(200).refine((value) => !["\r", "\n", "\0"].some((character) => value.includes(character)), "Use a model ID without control characters").optional(),
|
|
402
415
|
}).optional(),
|
|
403
|
-
/** Non-secret profile details
|
|
404
|
-
profile: z.object({ name: optionalText, email: optionalText }).optional(),
|
|
416
|
+
/** Non-secret profile details; aboutMe is shared with every bot. */
|
|
417
|
+
profile: z.object({ name: optionalText, email: optionalText, aboutMe: z.string().max(24_000).optional() }).optional(),
|
|
405
418
|
/** UI language override (BCP-47, lowercase). Empty/absent = follow the
|
|
406
419
|
* system language. Unknown tags degrade to English in the renderer. */
|
|
407
420
|
language: optionalText,
|
|
@@ -432,7 +445,7 @@ const storedAppConfigSchema = appConfigSchema.extend({
|
|
|
432
445
|
browserProfiles: storedBrowserProfilesSchema.optional(),
|
|
433
446
|
});
|
|
434
447
|
const appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true })
|
|
435
|
-
.extend({ threads: threadsPatchSchema.optional() });
|
|
448
|
+
.extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() });
|
|
436
449
|
const jsonObjectSchema = z.record(z.string(), z.json());
|
|
437
450
|
/** Resolve a canonical profile record to its exact durable Electron
|
|
438
451
|
* partition identity. Callers must never substitute the display/API id. */
|
|
@@ -670,13 +683,35 @@ function migrateLegacyFeatureFlags() {
|
|
|
670
683
|
console.error(`[config] could not rename features.skillRecorder: ${error instanceof Error ? error.message : String(error)}`);
|
|
671
684
|
}
|
|
672
685
|
}
|
|
686
|
+
/** The last "config.json is being ignored" warning, so a file that stays
|
|
687
|
+
* broken is reported once rather than on every loadConfig() call. */
|
|
688
|
+
let lastIgnoredConfigWarning = "";
|
|
673
689
|
export function loadConfig() {
|
|
674
690
|
let cfg = {};
|
|
675
691
|
try {
|
|
676
692
|
cfg = parseStoredConfig(parseJson(readFileSync(join(DATA_DIR, "config.json"), "utf8")));
|
|
693
|
+
lastIgnoredConfigWarning = "";
|
|
677
694
|
}
|
|
678
|
-
catch {
|
|
679
|
-
|
|
695
|
+
catch (error) {
|
|
696
|
+
// No file yet is a normal first run: env fallbacks below. Any other failure
|
|
697
|
+
// (unreadable file, invalid JSON, a schema error in one field) means the
|
|
698
|
+
// whole file is being ignored for this process — every instance, account
|
|
699
|
+
// and setting in it — so say why instead of silently running on defaults.
|
|
700
|
+
// saveConfig() still merges into the raw file, so nothing on disk is lost;
|
|
701
|
+
// the user just needs to know which field to fix.
|
|
702
|
+
if (error?.code !== "ENOENT") {
|
|
703
|
+
// JSON.parse includes a fragment of the input in some error messages.
|
|
704
|
+
// A malformed credential must never be copied into the server log.
|
|
705
|
+
const reason = error instanceof SyntaxError ? "invalid JSON"
|
|
706
|
+
: error instanceof Error ? error.message : "unable to read configuration";
|
|
707
|
+
const warning = `config: ignoring ${join(DATA_DIR, "config.json")} and using defaults: ${reason}`;
|
|
708
|
+
if (warning !== lastIgnoredConfigWarning)
|
|
709
|
+
console.warn(warning);
|
|
710
|
+
lastIgnoredConfigWarning = warning;
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
lastIgnoredConfigWarning = "";
|
|
714
|
+
}
|
|
680
715
|
}
|
|
681
716
|
// Env wins over the file for every credential. The desktop shell keeps
|
|
682
717
|
// these secrets OS-encrypted and hands them to this process as env at
|
|
@@ -685,6 +720,9 @@ export function loadConfig() {
|
|
|
685
720
|
// Anything that saves a credential mid-session must keep process.env in
|
|
686
721
|
// step (syncCredentialEnv below), or the value injected at boot would
|
|
687
722
|
// shadow the save until the next launch.
|
|
723
|
+
cfg.mistral = { ...cfg.mistral };
|
|
724
|
+
if (process.env.MISTRAL_API_KEY !== undefined)
|
|
725
|
+
cfg.mistral.key = process.env.MISTRAL_API_KEY;
|
|
688
726
|
cfg.xai = { ...cfg.xai };
|
|
689
727
|
if (process.env.XAI_API_KEY !== undefined)
|
|
690
728
|
cfg.xai.key = process.env.XAI_API_KEY;
|
|
@@ -746,6 +784,7 @@ export function loadConfig() {
|
|
|
746
784
|
export function syncCredentialEnv(patch) {
|
|
747
785
|
const secrets = [
|
|
748
786
|
[patch.xai?.key, "XAI_API_KEY"],
|
|
787
|
+
[patch.mistral?.key, "MISTRAL_API_KEY"],
|
|
749
788
|
[patch.anthropic?.key, "OMB_ANTHROPIC_API_KEY"],
|
|
750
789
|
[patch.openaiCompat?.key, "OPENAI_COMPAT_API_KEY"],
|
|
751
790
|
[patch.composio?.apiKey, "COMPOSIO_API_KEY"],
|
|
@@ -788,6 +827,7 @@ export function syncCredentialEnv(patch) {
|
|
|
788
827
|
* child these are someone else's keys riding along in `...process.env`. */
|
|
789
828
|
export const WORKSPACE_CREDENTIAL_ENV = [
|
|
790
829
|
"XAI_API_KEY",
|
|
830
|
+
"MISTRAL_API_KEY",
|
|
791
831
|
"OMB_ANTHROPIC_API_KEY",
|
|
792
832
|
"OMB_ANTHROPIC_API_URL",
|
|
793
833
|
"OMB_HOSTED_MODEL_TOKEN",
|
|
@@ -850,6 +890,7 @@ export const PROVIDER_CREDENTIAL_ENV = [
|
|
|
850
890
|
"OPENAI_API_KEY",
|
|
851
891
|
"OPENCODE_API_KEY",
|
|
852
892
|
"XAI_API_KEY",
|
|
893
|
+
"MISTRAL_API_KEY",
|
|
853
894
|
"CURSOR_API_KEY",
|
|
854
895
|
"CURSOR_AUTH_TOKEN",
|
|
855
896
|
];
|
|
@@ -874,7 +915,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
874
915
|
catch {
|
|
875
916
|
/* first write */
|
|
876
917
|
}
|
|
877
|
-
const checkedPatch = appConfigSchema.partial().extend({ threads: threadsPatchSchema.optional() }).parse(patch);
|
|
918
|
+
const checkedPatch = appConfigSchema.partial().extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() }).parse(patch);
|
|
878
919
|
const before = configSaveListeners.size ? structuredClone(disk) : disk;
|
|
879
920
|
// A write is the durable migration point. Preserve every other raw key in
|
|
880
921
|
// config.json, but never write #567's mixed-case or duplicate profile ids
|
|
@@ -882,7 +923,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
882
923
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
883
924
|
if (storedProfiles.success)
|
|
884
925
|
disk.browserProfiles = storedProfiles.data;
|
|
885
|
-
for (const key of ["xai", "anthropic", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "context", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine"]) {
|
|
926
|
+
for (const key of ["xai", "anthropic", "mistral", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "context", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
886
927
|
const section = checkedPatch[key];
|
|
887
928
|
if (!section)
|
|
888
929
|
continue;
|
|
@@ -894,8 +935,9 @@ export function saveConfig(patch, options = {}) {
|
|
|
894
935
|
if (key === "threads" && !current.success)
|
|
895
936
|
merged.maxConcurrentPerBot = DEFAULT_MAX_CONCURRENT_BOT_THREADS;
|
|
896
937
|
Object.assign(merged, section);
|
|
897
|
-
// null is the patch's explicit "remove this key" marker (
|
|
898
|
-
//
|
|
938
|
+
// null is the patch's explicit "remove this key" marker (the threads
|
|
939
|
+
// event-log knobs and newBots.effort use it); a key the patch omits
|
|
940
|
+
// keeps its value.
|
|
899
941
|
for (const [sectionKey, sectionValue] of Object.entries(section)) {
|
|
900
942
|
if (sectionValue === null)
|
|
901
943
|
delete merged[sectionKey];
|
|
@@ -915,6 +957,24 @@ export function saveConfig(patch, options = {}) {
|
|
|
915
957
|
// an effort level omitted from the new selection.
|
|
916
958
|
if (checkedPatch.defaultModelSelection !== undefined) {
|
|
917
959
|
disk.defaultModelSelection = checkedPatch.defaultModelSelection;
|
|
960
|
+
if (disk.newBotDefaults) {
|
|
961
|
+
const previousDefaults = newBotDefaultsSchema.parse(disk.newBotDefaults);
|
|
962
|
+
disk.newBotDefaults = newBotDefaultsSchema.parse({
|
|
963
|
+
...previousDefaults,
|
|
964
|
+
profile: { ...previousDefaults.profile, modelSelection: checkedPatch.defaultModelSelection },
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// Replace the complete template so clearing a field, file or routine
|
|
969
|
+
// cannot revive an old value through the general section merge above.
|
|
970
|
+
if (checkedPatch.newBotDefaults !== undefined) {
|
|
971
|
+
disk.newBotDefaults = checkedPatch.newBotDefaults;
|
|
972
|
+
if (checkedPatch.newBotDefaults.profile.modelSelection) {
|
|
973
|
+
disk.defaultModelSelection = checkedPatch.newBotDefaults.profile.modelSelection;
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
delete disk.defaultModelSelection;
|
|
977
|
+
}
|
|
918
978
|
}
|
|
919
979
|
if (checkedPatch.cliStartup !== undefined)
|
|
920
980
|
disk.cliStartup = checkedPatch.cliStartup;
|
|
@@ -938,6 +998,14 @@ export function saveConfig(patch, options = {}) {
|
|
|
938
998
|
if (routingConflict)
|
|
939
999
|
throw Object.assign(new Error(routingConflict), { status: 409 });
|
|
940
1000
|
disk.browserProfiles = nextProfiles;
|
|
1001
|
+
if (disk.newBotDefaults) {
|
|
1002
|
+
const defaults = newBotDefaultsSchema.parse(disk.newBotDefaults);
|
|
1003
|
+
const profileId = defaults.profile.browserProfile;
|
|
1004
|
+
if (profileId && profileId !== "guest" && !nextProfiles.some(profile => profile.id === profileId)) {
|
|
1005
|
+
delete defaults.profile.browserProfile;
|
|
1006
|
+
disk.newBotDefaults = defaults;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
941
1009
|
}
|
|
942
1010
|
if (checkedPatch.instances) {
|
|
943
1011
|
const currentInstances = jsonObjectSchema.safeParse(disk.instances);
|
|
@@ -1046,6 +1114,8 @@ export function persistableInstanceConfigs(cfg) {
|
|
|
1046
1114
|
* environment of an unrelated child process. */
|
|
1047
1115
|
function injectedEnvironment(cfg, driver) {
|
|
1048
1116
|
const environment = new Map();
|
|
1117
|
+
if (driver === "mistral" && cfg.mistral?.key)
|
|
1118
|
+
environment.set("MISTRAL_API_KEY", cfg.mistral.key);
|
|
1049
1119
|
if (driver === "grok" && cfg.xai?.key)
|
|
1050
1120
|
environment.set("XAI_API_KEY", cfg.xai.key);
|
|
1051
1121
|
// The workspace Anthropic key reaches Claude Code as the variable it
|
|
@@ -1096,6 +1166,7 @@ export function instanceConfigs(cfg) {
|
|
|
1096
1166
|
opencodeGo: { driver: "opencodeGo" },
|
|
1097
1167
|
computer: { driver: "boxAgent" },
|
|
1098
1168
|
openaiCompat: { driver: "openai-compat" },
|
|
1169
|
+
mistral: { driver: "mistral" },
|
|
1099
1170
|
qwen: { driver: "qwenAgent" },
|
|
1100
1171
|
hermes: { driver: "hermesAgent" },
|
|
1101
1172
|
pi: { driver: "piAgent" },
|
|
@@ -1111,6 +1182,7 @@ export function instanceConfigs(cfg) {
|
|
|
1111
1182
|
const PRODUCT_FLEET_ADDITIONS = {
|
|
1112
1183
|
cursor: { driver: "cursorAgent" },
|
|
1113
1184
|
openaiCompat: { driver: "openai-compat" },
|
|
1185
|
+
mistral: { driver: "mistral" },
|
|
1114
1186
|
...CUSTOM_ONLY,
|
|
1115
1187
|
};
|
|
1116
1188
|
const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;
|