openmausbot 0.1.87 → 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 +2 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BTiuL79F.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-C88Blc2v.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-BZsJIZrb.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CVeMe9Ah.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Cedk3Bhx.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-D-VRqzTB.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BChov01Y.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-C-TfwvHy.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Db7CGj5Y.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CNvIfqKT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C_bEp5tq.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-Coi4BOls.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CzylEuL3.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-m7p7ztaD.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BMy6o9x2.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BDQ__0_J.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-Djv4ghUb.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BULrlGYH.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-D-jrT_hU.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-p2UhQmfL.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-DHvGWZ7q.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-ChhKDot-.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-B-T7A43R.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CJm8ENNY.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-510tlza8.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-PkvRPHel.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BVBVPoX3.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BgYMVm7L.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BGqsnc_3.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-D_-UeReH.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-D0uooONx.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-zRsJERGJ.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-B9e1z77f.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BB4vNwvD.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DpkosOTB.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DbDP4kUu.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-DBJGPmjT.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-DWPRi-ue.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-nszBbJmx.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CWykJWu_.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-g3Da-Vnv.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Wzrlpzgh.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-moHTFMC0.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CwfXdFH8.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-3ZROYvnq.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-E1TBebP8.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DHKEU5pu.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BguEj09D.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-C7IDYnhd.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DCW-F5PW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-xBMFtWYV.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-B5nIkuH8.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DYFAhdbr.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DeXwX6Cz.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +3 -1
- package/dist-server/drivers/agents-proxy.js +277 -22
- package/dist-server/index.js +23150 -19947
- package/dist-server/local-computer-proxy.js +2 -0
- 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 +540 -298
- package/dist-server/pair-cli.js +540 -298
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +43 -1
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/config.js +25 -3
- 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 +31 -4
- package/dist-server/server/container-computer.js +60 -0
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/drivers/acp/core.js +24 -6
- 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 +137 -10
- package/dist-server/server/drivers/agents-catalog.js +100 -6
- package/dist-server/server/drivers/boxagent.js +2 -16
- package/dist-server/server/drivers/claude.js +31 -17
- 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-release.js +48 -0
- package/dist-server/server/drivers/codex.js +7 -25
- package/dist-server/server/drivers/openai-chat.js +26 -2
- package/dist-server/server/drivers/pi.js +237 -30
- package/dist-server/server/drivers/prompt-split.js +32 -9
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/index.js +1037 -209
- 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/options-card.js +21 -0
- package/dist-server/server/org-library.js +15 -1
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +57 -0
- package/dist-server/server/presets.js +32 -32
- 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/routine-requests.js +29 -7
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +1 -0
- package/dist-server/server/store.js +32 -2
- package/dist-server/server/system-prompt.js +26 -2
- package/dist-server/server/team-setup-requests.js +124 -23
- package/dist-server/server/thread-events.js +3 -1
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/usage-ledger.js +7 -1
- package/dist-server/server/workspace-backup.js +40 -3
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/shared/ask-question.js +19 -0
- package/dist-server/shared/credential-request.js +5 -1
- 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/profile-request.js +5 -4
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +3 -1
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/package.json +1 -1
- package/dist/assets/channel-taCNRlUU.js +0 -1
- package/dist/assets/index-BwdMAQ6p.css +0 -1
- package/dist/assets/index-ByJennr7.js +0 -581
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.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}` : "")
|
|
@@ -33,7 +33,7 @@ export const MAX_THREAD_EVENT_LOG_BYTES = 4 * 1024 * 1024 * 1024;
|
|
|
33
33
|
export const DEFAULT_LOCAL_VM_MODE = "shared";
|
|
34
34
|
export const DEFAULT_LOCAL_VM_MAX_INSTANCES = 2;
|
|
35
35
|
export const MIN_LOCAL_VM_MAX_INSTANCES = 1;
|
|
36
|
-
export const MAX_LOCAL_VM_MAX_INSTANCES =
|
|
36
|
+
export const MAX_LOCAL_VM_MAX_INSTANCES = 8;
|
|
37
37
|
export function isValidSshAlias(value) {
|
|
38
38
|
return typeof value === "string" && SSH_ALIAS.test(value);
|
|
39
39
|
}
|
|
@@ -683,13 +683,35 @@ function migrateLegacyFeatureFlags() {
|
|
|
683
683
|
console.error(`[config] could not rename features.skillRecorder: ${error instanceof Error ? error.message : String(error)}`);
|
|
684
684
|
}
|
|
685
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 = "";
|
|
686
689
|
export function loadConfig() {
|
|
687
690
|
let cfg = {};
|
|
688
691
|
try {
|
|
689
692
|
cfg = parseStoredConfig(parseJson(readFileSync(join(DATA_DIR, "config.json"), "utf8")));
|
|
693
|
+
lastIgnoredConfigWarning = "";
|
|
690
694
|
}
|
|
691
|
-
catch {
|
|
692
|
-
|
|
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
|
+
}
|
|
693
715
|
}
|
|
694
716
|
// Env wins over the file for every credential. The desktop shell keeps
|
|
695
717
|
// these secrets OS-encrypted and hands them to this process as env at
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Advertisement filtering for the connector MCP bridge (issue #1737, part
|
|
2
|
+
// of the per-bot Composio tool grants umbrella #1734).
|
|
3
|
+
//
|
|
4
|
+
// Call-time enforcement (connector-verdict.ts, slice 2) stays the
|
|
5
|
+
// authority: it judges the real tool names on the harness relay endpoint,
|
|
6
|
+
// where the loopback token already names the bot. This module is surface
|
|
7
|
+
// only — it trims a tools/list response to the tools the bot was granted,
|
|
8
|
+
// so the model is never offered a call that would be refused a moment
|
|
9
|
+
// later. The allowlist reaches the bridge as JSON in the environment, the
|
|
10
|
+
// same channel every other bridge input uses; whenever filtering cannot
|
|
11
|
+
// run — no env var, malformed env, oversized payload — the bridge relays
|
|
12
|
+
// the list untouched and the grants stay enforced at call time.
|
|
13
|
+
import { CONNECTOR_SLUG_PATTERN, CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
|
|
14
|
+
import { serviceSlugFor, serviceSlugForCandidates } from "./connector-verdict.js";
|
|
15
|
+
/** The env var mcpIntegration sets and connector-proxy.ts reads. */
|
|
16
|
+
export const CONNECTOR_ALLOWED_TOOLS_ENV = "OMB_CONNECTOR_ALLOWED_TOOLS";
|
|
17
|
+
/** The env var carrying the connected-service slugs the filter resolves
|
|
18
|
+
* tool-name prefixes against. Without it the filter falls back to the
|
|
19
|
+
* plain first-segment split, exactly as when the catalog is unreachable. */
|
|
20
|
+
export const CONNECTOR_SERVICE_SLUGS_ENV = "OMB_CONNECTOR_SERVICE_SLUGS";
|
|
21
|
+
/** Upper bound for the serialized allowlist. Past it the env var is
|
|
22
|
+
* omitted entirely (mounting must never depend on catalog size) and the
|
|
23
|
+
* bot keeps the unfiltered list with call-time enforcement only. */
|
|
24
|
+
export const CONNECTOR_ALLOWED_TOOLS_MAX_BYTES = 32 * 1024;
|
|
25
|
+
/** Upper bound for the serialized slug list; past it the env var is
|
|
26
|
+
* omitted and the filter falls back to the plain split. */
|
|
27
|
+
export const CONNECTOR_SERVICE_SLUGS_MAX_BYTES = 32 * 1024;
|
|
28
|
+
/** The platform meta-tools. They name no connected-app tool themselves —
|
|
29
|
+
* search and schemas discover, the executor runs what it is told — so
|
|
30
|
+
* they stay advertised whenever any tool is granted; the executor's
|
|
31
|
+
* arguments are still judged call-time by connector-verdict.ts. */
|
|
32
|
+
export const CONNECTOR_META_TOOLS = [
|
|
33
|
+
"COMPOSIO_SEARCH_TOOLS",
|
|
34
|
+
"COMPOSIO_GET_TOOL_SCHEMAS",
|
|
35
|
+
"COMPOSIO_MULTI_EXECUTE_TOOL",
|
|
36
|
+
// Session-level connection meta-tools. COMPOSIO_MANAGE_CONNECTIONS ships
|
|
37
|
+
// in every Composio session; COMPOSIO_WAIT_FOR_CONNECTIONS is opt-in and
|
|
38
|
+
// off by default. They prefix as service "composio", which no grant
|
|
39
|
+
// names, so they ride this list instead of CONNECTION_TOOL_SUFFIXES —
|
|
40
|
+
// connector-proxy intercepts their calls to surface connection cards.
|
|
41
|
+
// The filter only keeps names upstream actually lists, so advertising an
|
|
42
|
+
// opted-out tool changes nothing.
|
|
43
|
+
"COMPOSIO_MANAGE_CONNECTIONS",
|
|
44
|
+
"COMPOSIO_WAIT_FOR_CONNECTIONS",
|
|
45
|
+
];
|
|
46
|
+
const CONNECTION_TOOL_SUFFIXES = ["_MANAGE_CONNECTIONS", "_WAIT_FOR_CONNECTIONS"];
|
|
47
|
+
/** Harness side: serialize a grants record for the bridge env, or report
|
|
48
|
+
* that it exceeded the cap (the caller warns and omits it). */
|
|
49
|
+
export function serializeConnectorAllowedTools(grants) {
|
|
50
|
+
const env = JSON.stringify(grants);
|
|
51
|
+
if (Buffer.byteLength(env, "utf8") > CONNECTOR_ALLOWED_TOOLS_MAX_BYTES)
|
|
52
|
+
return { oversized: true };
|
|
53
|
+
return { env, oversized: false };
|
|
54
|
+
}
|
|
55
|
+
/** Harness side: serialize the connected-service slugs for the bridge
|
|
56
|
+
* env, or report that they exceeded the cap. Duplicate or non-slug
|
|
57
|
+
* entries are dropped rather than failing the whole mount. */
|
|
58
|
+
export function serializeConnectorServiceSlugs(slugs) {
|
|
59
|
+
const unique = [...new Set(slugs.filter((slug) => CONNECTOR_SLUG_PATTERN.test(slug)))];
|
|
60
|
+
if (!unique.length)
|
|
61
|
+
return { oversized: false };
|
|
62
|
+
const env = JSON.stringify(unique);
|
|
63
|
+
if (Buffer.byteLength(env, "utf8") > CONNECTOR_SERVICE_SLUGS_MAX_BYTES)
|
|
64
|
+
return { oversized: true };
|
|
65
|
+
return { env, oversized: false };
|
|
66
|
+
}
|
|
67
|
+
/** Bridge side: decode and shape-check the allowlist env. Anything off —
|
|
68
|
+
* absent, unparseable, wrong shapes — reads as "no allowlist" so the
|
|
69
|
+
* bridge degrades to relaying the unfiltered list rather than guessing a
|
|
70
|
+
* narrower one. Call-time enforcement holds either way. */
|
|
71
|
+
export function parseConnectorAllowedToolsEnv(raw) {
|
|
72
|
+
if (!raw)
|
|
73
|
+
return null;
|
|
74
|
+
let value;
|
|
75
|
+
try {
|
|
76
|
+
value = JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
82
|
+
return null;
|
|
83
|
+
const grants = {};
|
|
84
|
+
for (const [slug, grant] of Object.entries(value)) {
|
|
85
|
+
if (!CONNECTOR_SLUG_PATTERN.test(slug))
|
|
86
|
+
return null;
|
|
87
|
+
if (!grant || typeof grant !== "object" || Array.isArray(grant))
|
|
88
|
+
return null;
|
|
89
|
+
const tools = grant.tools;
|
|
90
|
+
if (tools === "*") {
|
|
91
|
+
grants[slug] = { tools: "*" };
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
if (!tools.every((tool) => typeof tool === "string" && CONNECTOR_TOOL_NAME_PATTERN.test(tool))) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
grants[slug] = { tools };
|
|
100
|
+
}
|
|
101
|
+
return grants;
|
|
102
|
+
}
|
|
103
|
+
/** Bridge side: decode the connected-service slug list. Anything off —
|
|
104
|
+
* absent, unparseable, or one bad entry — reads as an empty list so the
|
|
105
|
+
* filter degrades to the plain split instead of trusting a partial
|
|
106
|
+
* inventory. */
|
|
107
|
+
export function parseConnectorServiceSlugsEnv(raw) {
|
|
108
|
+
if (!raw)
|
|
109
|
+
return [];
|
|
110
|
+
let value;
|
|
111
|
+
try {
|
|
112
|
+
value = JSON.parse(raw);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(value))
|
|
118
|
+
return [];
|
|
119
|
+
return value.every((slug) => typeof slug === "string" && CONNECTOR_SLUG_PATTERN.test(slug))
|
|
120
|
+
? value
|
|
121
|
+
: [];
|
|
122
|
+
}
|
|
123
|
+
/** Whether one advertised tool name survives the filter. A tool is
|
|
124
|
+
* offered when its service is granted with `"*`", when its exact name is
|
|
125
|
+
* granted, or when it is that service's connection flow — a card the
|
|
126
|
+
* person approves, which slice 2 passes through at call time and which a
|
|
127
|
+
* bot granted a service should still be able to offer for another
|
|
128
|
+
* account. */
|
|
129
|
+
export function connectorToolAdvertised(name, grants, candidates = []) {
|
|
130
|
+
if (Object.keys(grants).length > 0 && CONNECTOR_META_TOOLS.includes(name))
|
|
131
|
+
return true;
|
|
132
|
+
// The connected-service slugs are the resolver's candidates, so an
|
|
133
|
+
// underscored service (bland_ai) claims its own tools instead of a
|
|
134
|
+
// plain-prefix grant (bland) widening them; an unreachable catalog
|
|
135
|
+
// leaves the list empty and the plain split stands.
|
|
136
|
+
const service = serviceSlugForCandidates(name, candidates) ?? serviceSlugFor(name);
|
|
137
|
+
const grant = service === null ? undefined : grants[service];
|
|
138
|
+
if (!grant)
|
|
139
|
+
return false;
|
|
140
|
+
if (grant.tools === "*")
|
|
141
|
+
return true;
|
|
142
|
+
if (grant.tools.includes(name))
|
|
143
|
+
return true;
|
|
144
|
+
return CONNECTION_TOOL_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
145
|
+
}
|
|
146
|
+
/** Filter one upstream tools/list response — the bridge normalizes SSE and
|
|
147
|
+
* JSON upstreams to a single frame first, so both shapes flow through
|
|
148
|
+
* here. Returns a shallow copy with result.tools trimmed; a frame without
|
|
149
|
+
* a readable tools array passes through unchanged (degrade, never guess).
|
|
150
|
+
* Entries the filter cannot read a name from are kept as-is. */
|
|
151
|
+
export function filterToolsListFrame(frame, grants, candidates = []) {
|
|
152
|
+
const result = frame.result;
|
|
153
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
154
|
+
return frame;
|
|
155
|
+
const tools = result.tools;
|
|
156
|
+
if (!Array.isArray(tools))
|
|
157
|
+
return frame;
|
|
158
|
+
const kept = tools.filter((tool) => {
|
|
159
|
+
const name = tool && typeof tool === "object" && !Array.isArray(tool)
|
|
160
|
+
? tool.name
|
|
161
|
+
: undefined;
|
|
162
|
+
return typeof name === "string" ? connectorToolAdvertised(name, grants, candidates) : true;
|
|
163
|
+
});
|
|
164
|
+
return { ...frame, result: { ...result, tools: kept } };
|
|
165
|
+
}
|
|
@@ -4,15 +4,24 @@
|
|
|
4
4
|
// to the configured Composio Session, but connection requests are converted
|
|
5
5
|
// into first-class OpenMausBot chat cards. The agent never authors an auth
|
|
6
6
|
// URL and credentials never pass through its transcript.
|
|
7
|
+
// tools/list responses are trimmed to this bot's granted tools as a
|
|
8
|
+
// prompt-time hint; every call is still judged harness-side by the grants
|
|
9
|
+
// relay.
|
|
7
10
|
//
|
|
8
11
|
// stdout is the MCP transport. Never log there.
|
|
9
12
|
import readline from "node:readline";
|
|
10
13
|
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { CONNECTOR_ALLOWED_TOOLS_ENV, CONNECTOR_SERVICE_SLUGS_ENV, filterToolsListFrame, parseConnectorAllowedToolsEnv, parseConnectorServiceSlugsEnv, } from "./connector-advertisement.js";
|
|
11
15
|
const UPSTREAM = process.env.OMB_CONNECTOR_UPSTREAM_URL ?? "";
|
|
12
16
|
const HARNESS = process.env.OMB_HARNESS_URL ?? "http://127.0.0.1:8799";
|
|
13
17
|
const BOT_ID = process.env.OMB_BOT_ID ?? "";
|
|
14
18
|
const THREAD_ID = process.env.OMB_THREAD_ID ?? "";
|
|
15
19
|
const TOKEN = process.env.OMB_CONNECTOR_TOKEN ?? process.env.OMB_COMMS_TOKEN ?? "";
|
|
20
|
+
// The bot's effective tool grants, or null when the harness sent none —
|
|
21
|
+
// legacy bots, oversized allowlists, anything unreadable. null means the
|
|
22
|
+
// upstream list is relayed verbatim; the harness still judges every call.
|
|
23
|
+
const ALLOWED_TOOLS = parseConnectorAllowedToolsEnv(process.env[CONNECTOR_ALLOWED_TOOLS_ENV]);
|
|
24
|
+
const SERVICE_SLUGS = parseConnectorServiceSlugsEnv(process.env[CONNECTOR_SERVICE_SLUGS_ENV]);
|
|
16
25
|
const MAX_RESPONSE_BYTES = 20 * 1024 * 1024;
|
|
17
26
|
const INITIALIZE_RELAY_TIMEOUT_MS = 1_000;
|
|
18
27
|
const RELAY_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -210,8 +219,11 @@ async function handle(message) {
|
|
|
210
219
|
}
|
|
211
220
|
try {
|
|
212
221
|
const response = await relay(message);
|
|
213
|
-
if (response && id !== undefined)
|
|
214
|
-
send(
|
|
222
|
+
if (response && id !== undefined) {
|
|
223
|
+
send(method === "tools/list" && ALLOWED_TOOLS
|
|
224
|
+
? filterToolsListFrame(response, ALLOWED_TOOLS, SERVICE_SLUGS)
|
|
225
|
+
: response);
|
|
226
|
+
}
|
|
215
227
|
}
|
|
216
228
|
catch (error) {
|
|
217
229
|
if (id === undefined)
|
|
@@ -33,6 +33,22 @@ export function serviceSlugFor(tool) {
|
|
|
33
33
|
return null;
|
|
34
34
|
return tool.slice(0, underscore).toLowerCase();
|
|
35
35
|
}
|
|
36
|
+
/** The service a Composio tool name belongs to when the caller knows the
|
|
37
|
+
* real service slugs. Slugs may contain underscores (bland_ai), which the
|
|
38
|
+
* plain first-segment split above cannot see — BLAND_AI_MAKE_CALL splits
|
|
39
|
+
* as "bland". When a known candidate matches the name's prefix, the
|
|
40
|
+
* longest candidate wins (bland_ai over bland), so an underscored service
|
|
41
|
+
* keeps its own tools; with no matching candidate the caller falls back
|
|
42
|
+
* to serviceSlugFor. */
|
|
43
|
+
export function serviceSlugForCandidates(tool, candidates) {
|
|
44
|
+
let match = null;
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
if (tool.startsWith(candidate.toUpperCase() + "_") && (match === null || candidate.length > match.length)) {
|
|
47
|
+
match = candidate;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return match;
|
|
51
|
+
}
|
|
36
52
|
/** Classify one relayed JSON-RPC frame. Anything that is not a tools/call,
|
|
37
53
|
* or is a discovery/connection/platform meta-tool, passes through. */
|
|
38
54
|
export function connectorCallFromFrame(payload) {
|
|
@@ -99,16 +115,27 @@ function multiExecuteCall(invoked, args) {
|
|
|
99
115
|
}
|
|
100
116
|
/** Judge distinct target names against a bot's grants. grants undefined
|
|
101
117
|
* is the legacy all-tools bot and passes everything; an explicit record —
|
|
102
|
-
* including the empty one — allows only what it names.
|
|
103
|
-
|
|
118
|
+
* including the empty one — allows only what it names. Service keys are
|
|
119
|
+
* read as own properties only: a name like CONSTRUCTOR_X maps to the
|
|
120
|
+
* "constructor" service, and the inherited property must never pose as
|
|
121
|
+
* a grant.
|
|
122
|
+
* candidates are the caller's connected-service slugs: the real backend
|
|
123
|
+
* slugs, so an underscored service (bland_ai) keeps its own tools instead
|
|
124
|
+
* of a plain-prefix grant (bland) capturing them. An empty list means the
|
|
125
|
+
* connected-service catalog was unreachable, and the plain first-segment
|
|
126
|
+
* split stands as the fallback — call-time enforcement degrades open, the
|
|
127
|
+
* same way advertisement filtering does. */
|
|
128
|
+
export function evaluateConnectorTools(names, grants, candidates = []) {
|
|
104
129
|
if (grants === undefined) {
|
|
105
130
|
return { allowed: true, legacy: true, denials: [], rule: "composio" };
|
|
106
131
|
}
|
|
107
132
|
const denials = [];
|
|
108
133
|
let rule = "";
|
|
109
134
|
for (const tool of new Set(names)) {
|
|
110
|
-
const service = serviceSlugFor(tool);
|
|
111
|
-
const grant = service
|
|
135
|
+
const service = serviceSlugForCandidates(tool, candidates) ?? serviceSlugFor(tool);
|
|
136
|
+
const grant = service !== null && Object.hasOwn(grants, service)
|
|
137
|
+
? grants[service]
|
|
138
|
+
: undefined;
|
|
112
139
|
if (grant && (grant.tools === "*" || grant.tools.includes(tool))) {
|
|
113
140
|
if (!rule)
|
|
114
141
|
rule = "connectorTools." + service;
|