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
|
@@ -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)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The harness-side verdict for connected-app tool calls (issue #1736, part
|
|
2
|
+
// of the per-bot Composio tool grants umbrella #1734).
|
|
3
|
+
//
|
|
4
|
+
// Every Composio tool call crosses POST /api/internal/connectors/mcp: the
|
|
5
|
+
// provider CLIs talk to the stdio bridge, and the bridge forwards each
|
|
6
|
+
// JSON-RPC frame there. So the grants verdict lives on the harness side,
|
|
7
|
+
// where the loopback token already names the bot and the decision log is
|
|
8
|
+
// native — the bridge never sees a key it could have to enforce with.
|
|
9
|
+
//
|
|
10
|
+
// Three shapes reach this module:
|
|
11
|
+
//
|
|
12
|
+
// * a direct per-toolkit call — params.name is the tool itself, e.g.
|
|
13
|
+
// GMAIL_SEND_EMAIL;
|
|
14
|
+
// * the executor meta-tool — COMPOSIO_MULTI_EXECUTE_TOOL, whose
|
|
15
|
+
// arguments name one or many target tools;
|
|
16
|
+
// * everything else — connection cards, discovery, notifications —
|
|
17
|
+
// which passes through untouched (advertisement filtering is slice 3).
|
|
18
|
+
//
|
|
19
|
+
// Unrecognized argument shapes are denied by default: a call whose target
|
|
20
|
+
// tools cannot be read is a call whose tools cannot be checked, and the
|
|
21
|
+
// grants promise — "this bot may call exactly these tools" — must hold
|
|
22
|
+
// even against a model that garbles the protocol.
|
|
23
|
+
import { CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
|
|
24
|
+
/** The executor meta-tool: the only meta-tool whose arguments name target
|
|
25
|
+
* tools. Discovery and connection meta-tools keep their existing flows. */
|
|
26
|
+
export const COMPOSIO_MULTI_EXECUTE_TOOL = "COMPOSIO_MULTI_EXECUTE_TOOL";
|
|
27
|
+
/** The service a Composio tool name belongs to: the upper-snake prefix
|
|
28
|
+
* before the first underscore, lowercased (GMAIL_SEND_EMAIL to gmail). A
|
|
29
|
+
* name with no underscore names no service, so no grant can cover it. */
|
|
30
|
+
export function serviceSlugFor(tool) {
|
|
31
|
+
const underscore = tool.indexOf("_");
|
|
32
|
+
if (underscore <= 0)
|
|
33
|
+
return null;
|
|
34
|
+
return tool.slice(0, underscore).toLowerCase();
|
|
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
|
+
}
|
|
52
|
+
/** Classify one relayed JSON-RPC frame. Anything that is not a tools/call,
|
|
53
|
+
* or is a discovery/connection/platform meta-tool, passes through. */
|
|
54
|
+
export function connectorCallFromFrame(payload) {
|
|
55
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
56
|
+
return { kind: "passthrough" };
|
|
57
|
+
const frame = payload;
|
|
58
|
+
if (frame.method !== "tools/call")
|
|
59
|
+
return { kind: "passthrough" };
|
|
60
|
+
const params = frame.params;
|
|
61
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
62
|
+
return { kind: "unrecognized", invoked: "tools/call", reason: "the call carried no params object" };
|
|
63
|
+
}
|
|
64
|
+
const name = params.name;
|
|
65
|
+
if (typeof name !== "string" || !name) {
|
|
66
|
+
return { kind: "unrecognized", invoked: "tools/call", reason: "the call named no tool" };
|
|
67
|
+
}
|
|
68
|
+
if (name === COMPOSIO_MULTI_EXECUTE_TOOL) {
|
|
69
|
+
return multiExecuteCall(name, params.arguments);
|
|
70
|
+
}
|
|
71
|
+
// Platform meta-tools (search, schemas, remote workbench) are not
|
|
72
|
+
// connected-app tools; grants are keyed by service slug, and slice 3
|
|
73
|
+
// owns what the model is even shown.
|
|
74
|
+
if (name.startsWith("COMPOSIO_"))
|
|
75
|
+
return { kind: "passthrough" };
|
|
76
|
+
// Per-service connection cards keep their card flow in any spelling.
|
|
77
|
+
if (name.endsWith("_MANAGE_CONNECTIONS") || name.endsWith("_WAIT_FOR_CONNECTIONS")) {
|
|
78
|
+
return { kind: "passthrough" };
|
|
79
|
+
}
|
|
80
|
+
if (!CONNECTOR_TOOL_NAME_PATTERN.test(name)) {
|
|
81
|
+
return { kind: "unrecognized", invoked: name, reason: "the tool name is not a Composio tool name" };
|
|
82
|
+
}
|
|
83
|
+
return { kind: "tools", invoked: name, names: [name] };
|
|
84
|
+
}
|
|
85
|
+
/** Read the target tools out of COMPOSIO_MULTI_EXECUTE_TOOL arguments:
|
|
86
|
+
* the upstream schema is a 1-50 item tools array of { tool_slug,
|
|
87
|
+
* arguments }; a legacy single { tool_slug } object is accepted too.
|
|
88
|
+
* Anything else cannot be checked, so it is denied. */
|
|
89
|
+
function multiExecuteCall(invoked, args) {
|
|
90
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
91
|
+
return { kind: "unrecognized", invoked, reason: "the arguments were not an object" };
|
|
92
|
+
}
|
|
93
|
+
const tools = args.tools;
|
|
94
|
+
const singleSlug = args.tool_slug;
|
|
95
|
+
if (Array.isArray(tools)) {
|
|
96
|
+
if (tools.length === 0) {
|
|
97
|
+
return { kind: "unrecognized", invoked, reason: "the tools list was empty" };
|
|
98
|
+
}
|
|
99
|
+
const names = [];
|
|
100
|
+
for (const item of tools) {
|
|
101
|
+
const slug = item && typeof item === "object" && !Array.isArray(item)
|
|
102
|
+
? item.tool_slug
|
|
103
|
+
: undefined;
|
|
104
|
+
if (typeof slug !== "string" || !slug) {
|
|
105
|
+
return { kind: "unrecognized", invoked, reason: "an entry in the tools list named no tool_slug" };
|
|
106
|
+
}
|
|
107
|
+
names.push(slug);
|
|
108
|
+
}
|
|
109
|
+
return { kind: "tools", invoked, names };
|
|
110
|
+
}
|
|
111
|
+
if (typeof singleSlug === "string" && singleSlug) {
|
|
112
|
+
return { kind: "tools", invoked, names: [singleSlug] };
|
|
113
|
+
}
|
|
114
|
+
return { kind: "unrecognized", invoked, reason: "the arguments named no tools to execute" };
|
|
115
|
+
}
|
|
116
|
+
/** Judge distinct target names against a bot's grants. grants undefined
|
|
117
|
+
* is the legacy all-tools bot and passes everything; an explicit record —
|
|
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 = []) {
|
|
129
|
+
if (grants === undefined) {
|
|
130
|
+
return { allowed: true, legacy: true, denials: [], rule: "composio" };
|
|
131
|
+
}
|
|
132
|
+
const denials = [];
|
|
133
|
+
let rule = "";
|
|
134
|
+
for (const tool of new Set(names)) {
|
|
135
|
+
const service = serviceSlugForCandidates(tool, candidates) ?? serviceSlugFor(tool);
|
|
136
|
+
const grant = service !== null && Object.hasOwn(grants, service)
|
|
137
|
+
? grants[service]
|
|
138
|
+
: undefined;
|
|
139
|
+
if (grant && (grant.tools === "*" || grant.tools.includes(tool))) {
|
|
140
|
+
if (!rule)
|
|
141
|
+
rule = "connectorTools." + service;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
denials.push({ tool, service, onGrantedService: Boolean(grant) });
|
|
145
|
+
}
|
|
146
|
+
return { allowed: denials.length === 0, legacy: false, denials, rule };
|
|
147
|
+
}
|
|
148
|
+
/** Safe to hand straight to the model: names the refused tool and says who
|
|
149
|
+
* can change it. It never lists what the bot could have called instead —
|
|
150
|
+
* a refusal that enumerates adjacent grants teaches the model to probe. */
|
|
151
|
+
export function connectorRefusalText(denials) {
|
|
152
|
+
const named = denials.map((denial) => '"' + denial.tool + '"').join(", ");
|
|
153
|
+
const one = denials.length === 1;
|
|
154
|
+
return [
|
|
155
|
+
named + " " + (one ? "is" : "are") + " not granted to this bot.",
|
|
156
|
+
"This call was not performed.",
|
|
157
|
+
"Ask the person to grant " + (one ? "it" : "these tools") + " in OpenMausBot if they want " + (one ? "it" : "them") + " run.",
|
|
158
|
+
].join(" ");
|
|
159
|
+
}
|
|
160
|
+
/** The unrecognized-shape refusal: names the meta-tool and what could not
|
|
161
|
+
* be read, and still points at the person rather than at any tool list. */
|
|
162
|
+
export function connectorUnrecognizedText(invoked, reason) {
|
|
163
|
+
return [
|
|
164
|
+
"A " + invoked + " call arrived in a shape OpenMausBot could not read (" + reason + "), so it was not performed.",
|
|
165
|
+
"Send a well-formed call that names the tools to execute.",
|
|
166
|
+
"Ask the person to grant any tool this bot needs.",
|
|
167
|
+
].join(" ");
|
|
168
|
+
}
|
|
@@ -310,7 +310,7 @@ function statusProblem(status) {
|
|
|
310
310
|
if (status.security === "unsafe")
|
|
311
311
|
return "The existing Local VM is missing safety limits; recreate it";
|
|
312
312
|
if (status.persistence === "unsafe")
|
|
313
|
-
return "The existing Local VM is missing its durable
|
|
313
|
+
return "The existing Local VM is missing its durable folder; recreate it";
|
|
314
314
|
if (status.container === "stopped")
|
|
315
315
|
return "This desktop image cannot safely resume; recreate the Local VM";
|
|
316
316
|
if (status.desktop_error)
|
|
@@ -808,6 +808,66 @@ export async function containerComputerScreenshot(runner = sh, platform = proces
|
|
|
808
808
|
}
|
|
809
809
|
const screenshotStatusCache = new Map();
|
|
810
810
|
const containerMcpPath = SPAWNED_PROXIES.containerMcp;
|
|
811
|
+
const EXEC_DEFAULT_SECONDS = 60;
|
|
812
|
+
const EXEC_MAX_SECONDS = 300;
|
|
813
|
+
const EXEC_OUTPUT_LIMIT = 20_000;
|
|
814
|
+
/** Keep the start and the end: a failure message is nearly always at the end. */
|
|
815
|
+
export function clipExecOutput(text, limit = EXEC_OUTPUT_LIMIT) {
|
|
816
|
+
if (text.length <= limit)
|
|
817
|
+
return text;
|
|
818
|
+
const head = Math.floor(limit / 5);
|
|
819
|
+
return `${text.slice(0, head)}\n… ${text.length - limit} characters omitted …\n${text.slice(text.length - (limit - head))}`;
|
|
820
|
+
}
|
|
821
|
+
const defaultExecRunner = async (command, args, options) => {
|
|
822
|
+
const resolved = resolveCliSpawn(command, args);
|
|
823
|
+
try {
|
|
824
|
+
const { stdout, stderr } = await run(resolved.command, resolved.args, {
|
|
825
|
+
timeout: options.timeout,
|
|
826
|
+
encoding: "utf8",
|
|
827
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
828
|
+
env: { ...process.env, PATH: augmentedPath() },
|
|
829
|
+
});
|
|
830
|
+
return { stdout, stderr, code: 0 };
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
const failure = error;
|
|
834
|
+
// A non-zero exit still carries the output; anything else (runtime missing,
|
|
835
|
+
// client killed) is a real failure to run at all.
|
|
836
|
+
if (typeof failure.code === "number") {
|
|
837
|
+
return { stdout: failure.stdout ?? "", stderr: failure.stderr ?? "", code: failure.code };
|
|
838
|
+
}
|
|
839
|
+
throw Object.assign(new Error(failure.killed ? "the command client timed out" : failure.message ?? "could not run the command"), { status: 502 });
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
/** Run one shell command inside a Local VM as the desktop user, in its durable
|
|
843
|
+
* workspace, and return the exit code and text output. Bots use this instead of
|
|
844
|
+
* typing into a terminal window and reading screenshots. The limit is enforced
|
|
845
|
+
* inside the container so a runaway process is really stopped. */
|
|
846
|
+
export async function containerExec(target, command, options = {}) {
|
|
847
|
+
if (!command.trim())
|
|
848
|
+
throw Object.assign(new Error("command is required"), { status: 400 });
|
|
849
|
+
if (command.length > 20_000)
|
|
850
|
+
throw Object.assign(new Error("command is too long"), { status: 400 });
|
|
851
|
+
if (options.timeoutSeconds !== undefined && !Number.isFinite(options.timeoutSeconds)) {
|
|
852
|
+
throw Object.assign(new Error("timeout_seconds must be finite"), { status: 400 });
|
|
853
|
+
}
|
|
854
|
+
const runtime = options.runtime ?? (await containerRuntimeStatus()).runtime;
|
|
855
|
+
if (!runtime)
|
|
856
|
+
throw Object.assign(new Error("No container runtime is available for the Local VM"), { status: 409 });
|
|
857
|
+
const seconds = Math.min(Math.max(Math.floor(options.timeoutSeconds ?? EXEC_DEFAULT_SECONDS), 1), EXEC_MAX_SECONDS);
|
|
858
|
+
const result = await (options.exec ?? defaultExecRunner)(runtime, [
|
|
859
|
+
"exec",
|
|
860
|
+
"-u", "cua",
|
|
861
|
+
"-w", VM_WORKSPACE_GUEST,
|
|
862
|
+
"-e", "HOME=/home/cua",
|
|
863
|
+
"-e", `DISPLAY=${DISPLAY}`,
|
|
864
|
+
target.containerName,
|
|
865
|
+
"timeout", "-k", "5", String(seconds),
|
|
866
|
+
"sh", "-lc", command,
|
|
867
|
+
], { timeout: (seconds + 20) * 1000 });
|
|
868
|
+
const timedOut = result.code === 124 || result.code === 137;
|
|
869
|
+
return { exitCode: result.code, stdout: clipExecOutput(result.stdout), stderr: clipExecOutput(result.stderr), timedOut };
|
|
870
|
+
}
|
|
811
871
|
export function containerComputerMcp(runtime, control, target = SHARED_LOCAL_VM_TARGET) {
|
|
812
872
|
return {
|
|
813
873
|
command: process.execPath,
|
|
@@ -17,6 +17,28 @@ export function isModelVariant(value) {
|
|
|
17
17
|
return typeof value === "string" && value.length > 0 && value.length <= 256 &&
|
|
18
18
|
value.trim() === value && !/\p{Cc}/u.test(value);
|
|
19
19
|
}
|
|
20
|
+
/** The picker label for a model id when the catalog row carries no display
|
|
21
|
+
* name: split on the word breaks the catalog treats as separators and
|
|
22
|
+
* capitalize each part ("gpt-5.4-mini" → "Gpt 5.4 Mini"). Each harness
|
|
23
|
+
* passes its own break class so existing labels stay byte-for-byte. */
|
|
24
|
+
export function titleCaseModelId(id, wordBreaks) {
|
|
25
|
+
return id
|
|
26
|
+
.split(wordBreaks)
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
29
|
+
.join(" ");
|
|
30
|
+
}
|
|
31
|
+
/** A picker label that carries a second facet — a display name, a provider
|
|
32
|
+
* host — beside the model id, unless the facet is already what the id says.
|
|
33
|
+
* `redundantWhen` decides what counts as "already said" and `decorate` how
|
|
34
|
+
* the facet is appended, so every harness's labels stay exactly what they
|
|
35
|
+
* were while the shape is written once. */
|
|
36
|
+
export function qualifiedModelLabel(id, qualifier, options = {}) {
|
|
37
|
+
const { redundantWhen = () => false, decorate = (facet) => ` — ${facet}` } = options;
|
|
38
|
+
if (!qualifier || redundantWhen(id, qualifier))
|
|
39
|
+
return id;
|
|
40
|
+
return `${id}${decorate(qualifier)}`;
|
|
41
|
+
}
|
|
20
42
|
let eventCounter = 0;
|
|
21
43
|
export const newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
|
|
22
44
|
export const newId = () => crypto.randomUUID();
|
|
@@ -37,3 +37,12 @@ export function selectDefaultModelSelection(instances, preferred, context = {})
|
|
|
37
37
|
}
|
|
38
38
|
return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
|
|
39
39
|
}
|
|
40
|
+
/** Complete a new bot's selection with the workspace's new-bot effort. An
|
|
41
|
+
* explicit effort or model variant is the caller's choice and wins; an engine
|
|
42
|
+
* that does not offer the level keeps sending none rather than failing turn 1. */
|
|
43
|
+
export function withNewBotEffort(selection, effort, offered) {
|
|
44
|
+
if (!effort || selection.effort !== undefined || selection.variant !== undefined || !offered?.includes(effort)) {
|
|
45
|
+
return selection;
|
|
46
|
+
}
|
|
47
|
+
return { ...selection, effort };
|
|
48
|
+
}
|