openmausbot 0.1.93 → 0.1.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{DesktopViewer-CKNR3SEW.js → DesktopViewer-BwWpc4RK.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-jsQkIqTn.js} +1 -1
- package/dist/assets/{arc-BBdBuCgB.js → arc-7kei4Hsu.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-CRWDFDMq.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-CTKW_IKs.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-BBTW7-6M.js} +1 -1
- package/dist/assets/channel-DS2NbT5x.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-M_Ox-MkD.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-D2YiN-Ud.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-BmVYJ0f8.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtiLVVnZ.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-DFYSb6J6.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-mknKBzgV.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-DgSYGiOu.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-C7ZvuguH.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-CPqxINpA.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-Cd6xfJaC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BSXY5qRh.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-D_lhEMj1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-Da9FrFwk.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-T77jMOXe.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-Dkj96X5E.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-CMi9zlD_.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-CXK6YUfS.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-BE7B1u9Q.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-CCMQphKr.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-iHJ_-tGH.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-DONsCWQ7.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-CU_WOaXk.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-BeYwM04G.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-Bdw9DqEl.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-C9iwq3qm.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-Dx-VWzUg.js} +1 -1
- package/dist/assets/{index-BkYwilkG.js → index-KygHRE3E.js} +1 -1
- package/dist/assets/index-NF7XCHAy.css +1 -0
- package/dist/assets/index-X_5ydXIE.js +610 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-BG7uaEd4.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-D72WAXnM.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-DKH1gjYT.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-3upm9JUT.js} +1 -1
- package/dist/assets/{linear-bq1PTDTW.js → linear-CcR4pc0-.js} +1 -1
- package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-C6yU5FSS.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DOI2xEuv.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BVWJSgLd.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-jtbYezhq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-g0u8mtPX.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-k8689Wny.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DsJ22PDt.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CneWBRgG.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-DWsFCWap.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-Bvf0YE3S.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-Bxb3Iei8.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DLkk7W4A.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-KR9bIHYC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-DfsOrj8t.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-DRSTzG3h.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-DtEdhPgT.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B-XhTxjG.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +3 -1
- package/dist-server/companion/src/routes.js +60 -0
- package/dist-server/container-mcp.js +94 -77
- package/dist-server/drivers/agents-proxy.js +96 -39
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +3 -1
- package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +35 -19
- package/dist-server/index.js +18242 -13645
- package/dist-server/local-computer.js +3 -1
- package/dist-server/mcp-gate.js +147 -19
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +9618 -10058
- package/dist-server/pair-cli.js +9618 -10058
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +3 -1
- package/dist-server/server/account-signin.js +0 -2
- package/dist-server/server/activity.js +201 -0
- package/dist-server/server/auto-approve.js +39 -11
- package/dist-server/server/automatic-recovery.js +1 -1
- package/dist-server/server/boat-create-idempotency.js +0 -7
- package/dist-server/server/boat-delete-journal.js +0 -4
- package/dist-server/server/bot-package.js +1 -19
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +7 -2
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/chief-of-staff.js +2 -3
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-backend.js +0 -4
- package/dist-server/server/cloud-computer-tools.js +110 -0
- package/dist-server/server/cloud-home.js +0 -1
- package/dist-server/server/cloud-lending.js +0 -3
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/composio.js +31 -2
- package/dist-server/server/computer-control.js +0 -15
- package/dist-server/server/computer-observation.js +0 -74
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +73 -11
- package/dist-server/server/container-computer.js +7 -19
- package/dist-server/server/contracts.js +9 -0
- package/dist-server/server/control-client.js +4 -10
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/drivers/acp/core.js +64 -13
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/agents-call.js +70 -29
- package/dist-server/server/drivers/agents-catalog.js +34 -12
- package/dist-server/server/drivers/agents-proxy.js +8 -30
- package/dist-server/server/drivers/antigravity.js +1 -4
- package/dist-server/server/drivers/boatagent.js +25 -4
- package/dist-server/server/drivers/cerebras.js +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +29 -19
- package/dist-server/server/drivers/claude.js +33 -19
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +14 -9
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/retry.js +8 -4
- package/dist-server/server/engine-install.js +1 -5
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/harness/registry.js +0 -1
- package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
- package/dist-server/server/index.js +1929 -507
- package/dist-server/server/lending-memory.js +12 -8
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/memory-entries.js +38 -8
- package/dist-server/server/memory-organize.js +1 -8
- package/dist-server/server/memory-topics.js +4 -4
- package/dist-server/server/memory-upkeep.js +13 -15
- package/dist-server/server/message-file.js +5 -9
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/outbound-counts.js +79 -0
- package/dist-server/server/outbound-requests.js +60 -0
- package/dist-server/server/package-import.js +1 -1
- package/dist-server/server/peer-delivery.js +1 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/proxy-paths.js +3 -1
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/room-handoffs.js +5 -13
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +5 -1
- package/dist-server/server/setup-mode.js +0 -2
- package/dist-server/server/skill-learn.js +0 -1
- package/dist-server/server/skill-library.js +195 -3
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +41 -5
- package/dist-server/server/surface.js +74 -23
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/team-memory.js +209 -0
- package/dist-server/server/thread-events.js +3 -3
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/server/vps-computer.js +4 -14
- package/dist-server/server/workspace.js +85 -47
- package/dist-server/shared/activity.js +3 -0
- package/dist-server/shared/ask-question.js +13 -0
- package/dist-server/shared/bot-avatar.js +8 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/connector-scopes.js +78 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/outbound.js +128 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/shared/tool-surface.js +3 -2
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +94 -77
- package/dist-server/workspace-backup.worker.js +89 -100
- package/enterprise/server/index.js +3 -1
- package/package.json +1 -1
- package/dist/assets/channel-BU94VpxE.js +0 -1
- package/dist/assets/index-Biji-bfL.css +0 -1
- package/dist/assets/index-DZlhoES0.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
- package/dist-server/server/drivers/chat-boat-tools.js +0 -90
- package/dist-server/server/drivers/grokagent.js +0 -4
- package/dist-server/server/openmaus-status-capsule.js +0 -329
|
@@ -38,6 +38,9 @@ import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-r
|
|
|
38
38
|
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
39
39
|
import { parseProtocolAskQuestions, questionAnswersById, questionChoices } from "../../shared/ask-question.js";
|
|
40
40
|
import { codexVersionBehind, readLatestCodexRelease } from "./codex-release.js";
|
|
41
|
+
import { canUseMcpServer } from "../../shared/tool-scope.js";
|
|
42
|
+
import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
|
|
43
|
+
import { gateServer } from "../mcp-gate-config.js";
|
|
41
44
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
42
45
|
const DRIVER_KIND = "codex";
|
|
43
46
|
export function codexUserError(value, chatgptPlan) {
|
|
@@ -621,6 +624,7 @@ export const CodexDriver = {
|
|
|
621
624
|
createdAt: new Date().toISOString(),
|
|
622
625
|
});
|
|
623
626
|
const sendTurn = async (turn) => {
|
|
627
|
+
turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
|
|
624
628
|
const generation = planGeneration;
|
|
625
629
|
const assertPlanCurrent = () => {
|
|
626
630
|
if (disposed)
|
|
@@ -691,48 +695,81 @@ export const CodexDriver = {
|
|
|
691
695
|
if (planToken)
|
|
692
696
|
env.OPENMAUSBOT_CHATGPT_TOKEN = planToken;
|
|
693
697
|
const appServerArgs = ["app-server", ...(plan ? chatgptPlanCodexArgs() : config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model)), ...codexToolSurfaceArgs(),
|
|
698
|
+
// Native snapshots can restore inherited variables after the shell
|
|
699
|
+
// policy has filtered them. Scoped MCP gate settings must stay private.
|
|
700
|
+
...(turn.toolScope === undefined ? [] : ["-c", "features.shell_snapshot=false"]),
|
|
694
701
|
// A guest-driven turn (SendTurnInput.guestConfined): no shell and
|
|
695
702
|
// no file reads, whatever the person's own config says (-c wins
|
|
696
703
|
// over config files). The turn also starts with no environment.
|
|
697
704
|
...(turn.guestConfined ? GUEST_CONFINED_CODEX_ARGS : [])];
|
|
705
|
+
const selectedMcp = new Map();
|
|
706
|
+
const selectedApprovals = new Map();
|
|
707
|
+
const scopedServer = (name, mountName, server) => {
|
|
708
|
+
if (!canUseMcpServer(turn.toolScope, name))
|
|
709
|
+
return null;
|
|
710
|
+
return turn.toolScope === undefined ? server : gateServer({ name, server, threadId, budget: 0, toolScope: turn.toolScope, nodeEnv: { ELECTRON_RUN_AS_NODE: "1" },
|
|
711
|
+
configEnvName: `OMB_GATE_CONFIG_${createHash("sha256").update(mountName).digest("hex")}` });
|
|
712
|
+
};
|
|
713
|
+
const mountSelected = (name, mountName, server, preApproved = true) => {
|
|
714
|
+
const selected = scopedServer(name, mountName, server);
|
|
715
|
+
if (selected) {
|
|
716
|
+
selectedMcp.set(mountName, selected);
|
|
717
|
+
selectedApprovals.set(mountName, preApproved);
|
|
718
|
+
mountMcpServer(appServerArgs, env, mountName, selected, preApproved);
|
|
719
|
+
if (turn.toolScope !== undefined && !preApproved)
|
|
720
|
+
appServerArgs.push("-c", `mcp_servers.${mountName}.default_tools_approval_mode="prompt"`);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
698
723
|
if (turn.integrations?.composio) {
|
|
699
|
-
|
|
724
|
+
mountSelected("composio", "openmausbot_connectors", turn.integrations.composio);
|
|
700
725
|
}
|
|
701
726
|
if (turn.integrations?.agents) {
|
|
702
|
-
|
|
727
|
+
mountSelected("agents", "agents", turn.integrations.agents);
|
|
703
728
|
}
|
|
704
729
|
if (turn.integrations?.localComputer) {
|
|
705
730
|
// The host daemon and isolated Local VM both arrive as a direct Cua
|
|
706
731
|
// Driver stdio MCP server. Codex sees the same computer tool surface.
|
|
707
|
-
|
|
732
|
+
mountSelected("computer", "computer", turn.integrations.localComputer);
|
|
708
733
|
}
|
|
709
734
|
if (turn.integrations?.browser) {
|
|
710
|
-
|
|
735
|
+
mountSelected("browser", "browser", turn.integrations.browser);
|
|
711
736
|
}
|
|
712
737
|
// A custom server named like one in the user's own config.toml would
|
|
713
738
|
// be merged with it by the `-c` override — a stdio command over a
|
|
714
739
|
// remote url is "invalid configuration" and kills the turn before the
|
|
715
740
|
// model is asked. Such a server mounts under a name of its own.
|
|
716
741
|
const declaredInCodexConfig = codexConfigMcpServerNames(env);
|
|
742
|
+
if (turn.toolScope !== undefined) {
|
|
743
|
+
for (const name of declaredInCodexConfig)
|
|
744
|
+
appServerArgs.push("-c", `mcp_servers.${name}.enabled=false`);
|
|
745
|
+
}
|
|
717
746
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
718
747
|
// codex speaks streamable HTTP to a remote server, not the older
|
|
719
748
|
// SSE transport: such an entry still reaches Claude bots, and is
|
|
720
749
|
// left out here rather than mounted as something it is not
|
|
721
|
-
if ("url" in server && server.type === "sse") {
|
|
750
|
+
if (turn.toolScope === undefined && "url" in server && server.type === "sse") {
|
|
722
751
|
noteSkippedSseServer(name);
|
|
723
752
|
continue;
|
|
724
753
|
}
|
|
725
754
|
const mountName = mountedMcpServerName(name, declaredInCodexConfig);
|
|
726
755
|
if (mountName !== name)
|
|
727
756
|
noteRenamedMcpServer(name, mountName);
|
|
728
|
-
|
|
757
|
+
mountSelected(name, mountName, server, false);
|
|
729
758
|
}
|
|
730
759
|
if (turn.integrations?.phone) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
760
|
+
if (turn.toolScope !== undefined) {
|
|
761
|
+
mountSelected("phone", "openmausbot_phone", turn.integrations.phone);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
const bridge = turn.integrations.phone;
|
|
765
|
+
Object.assign(env, bridge.env);
|
|
766
|
+
const prefix = "mcp_servers.openmausbot_phone";
|
|
767
|
+
appServerArgs.push("-c", `${prefix}.command=${JSON.stringify(bridge.command)}`, "-c", `${prefix}.args=${JSON.stringify(bridge.args)}`, "-c", `${prefix}.env_vars=${JSON.stringify(Object.keys(bridge.env))}`, "-c", `${prefix}.default_tools_approval_mode="auto"`);
|
|
768
|
+
}
|
|
735
769
|
}
|
|
770
|
+
const selectionConfig = turn.toolScope === undefined ? {} : { config: { mcp_servers: Object.fromEntries([...selectedMcp].map(([name, server]) => [name, "command" in server ? {
|
|
771
|
+
command: server.command, args: server.args, env_vars: Object.keys(server.env), env: {}, default_tools_approval_mode: selectedApprovals.get(name) ? "auto" : "prompt",
|
|
772
|
+
} : {}])) } };
|
|
736
773
|
const commandCwd = permissionLaunchCwd(turn.cwd ?? homedir());
|
|
737
774
|
const child = spawnCli(config.cli, appServerArgs, {
|
|
738
775
|
cwd: turn.cwd ?? homedir(),
|
|
@@ -1487,6 +1524,41 @@ export const CodexDriver = {
|
|
|
1487
1524
|
? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
|
|
1488
1525
|
: "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
|
|
1489
1526
|
}
|
|
1527
|
+
if (turn.toolScope !== undefined) {
|
|
1528
|
+
if (effectiveConfig?.features?.shell_snapshot !== false) {
|
|
1529
|
+
throw new Error("Codex could not disable inherited shell snapshots. No prompt was sent.");
|
|
1530
|
+
}
|
|
1531
|
+
// Gate descriptors belong to MCP children, never native shell tools.
|
|
1532
|
+
// Extend the effective policy for both new and resumed threads so
|
|
1533
|
+
// local, Company and ChatGPT turns retain the person's exclusions.
|
|
1534
|
+
const rawPolicy = effectiveConfig?.shell_environment_policy;
|
|
1535
|
+
if (rawPolicy !== undefined && (!rawPolicy || typeof rawPolicy !== "object" || Array.isArray(rawPolicy))) {
|
|
1536
|
+
throw new Error("Codex could not confirm its shell environment policy. No prompt was sent.");
|
|
1537
|
+
}
|
|
1538
|
+
const policy = (rawPolicy ?? {});
|
|
1539
|
+
const excluded = policy.exclude === undefined ? [] : policy.exclude;
|
|
1540
|
+
if (!Array.isArray(excluded) || excluded.some(name => typeof name !== "string")) {
|
|
1541
|
+
throw new Error("Codex could not confirm its shell environment exclusions. No prompt was sent.");
|
|
1542
|
+
}
|
|
1543
|
+
selectionConfig.config["shell_environment_policy.exclude"] = [...new Set([...excluded, "OMB_GATE_CONFIG_*"])];
|
|
1544
|
+
const catalog = effectiveConfig?.mcp_servers;
|
|
1545
|
+
if (!catalog || typeof catalog !== "object" || Array.isArray(catalog))
|
|
1546
|
+
throw new Error("Codex could not confirm its selected MCP configuration. No prompt was sent.");
|
|
1547
|
+
for (const [name, entry] of Object.entries(catalog)) {
|
|
1548
|
+
if (entry && typeof entry === "object" && entry.enabled === false)
|
|
1549
|
+
continue;
|
|
1550
|
+
const expected = selectedMcp.get(name);
|
|
1551
|
+
const actual = entry;
|
|
1552
|
+
if (!expected || !("command" in expected) || actual?.command !== expected.command
|
|
1553
|
+
|| JSON.stringify(actual?.args ?? []) !== JSON.stringify(expected.args ?? [])
|
|
1554
|
+
|| !Array.isArray(actual?.env_vars) || JSON.stringify([...actual.env_vars].sort()) !== JSON.stringify(Object.keys(expected.env).sort())
|
|
1555
|
+
|| actual.default_tools_approval_mode !== (selectedApprovals.get(name) ? "auto" : "prompt")
|
|
1556
|
+
|| (actual.env != null && (typeof actual.env !== "object" || Array.isArray(actual.env) || Object.keys(actual.env).length > 0))
|
|
1557
|
+
|| (actual.tools != null && (typeof actual.tools !== "object" || Array.isArray(actual.tools) || Object.keys(actual.tools).length > 0))) {
|
|
1558
|
+
throw new Error("Codex has an MCP server outside the selected configuration. Disable native MCP entries for this account before using tool selection. No prompt was sent.");
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1490
1562
|
// Proven before the turn starts: a Codex that did not take the
|
|
1491
1563
|
// overrides runs nothing for a guest.
|
|
1492
1564
|
if (turn.guestConfined && !codexShellDisabled(effectiveConfig)) {
|
|
@@ -1534,6 +1606,7 @@ export const CodexDriver = {
|
|
|
1534
1606
|
let promptText = turn.text;
|
|
1535
1607
|
if (cursor) {
|
|
1536
1608
|
const resumeThread = () => request("thread/resume", {
|
|
1609
|
+
...selectionConfig,
|
|
1537
1610
|
threadId: cursor,
|
|
1538
1611
|
developerInstructions,
|
|
1539
1612
|
model: selection.model,
|
|
@@ -1581,6 +1654,7 @@ export const CodexDriver = {
|
|
|
1581
1654
|
}
|
|
1582
1655
|
if (!codexThreadId) {
|
|
1583
1656
|
const startThread = () => request("thread/start", {
|
|
1657
|
+
...selectionConfig,
|
|
1584
1658
|
developerInstructions,
|
|
1585
1659
|
cwd: turn.cwd ?? homedir(),
|
|
1586
1660
|
model: selection.model,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { newEventId, newId } from "../contracts.js";
|
|
2
2
|
import { ASK_USER_TOOL, ASK_USER_TOOL_DEFINITION, askQuestionSummary, parseAskQuestions, questionChoices } from "../../shared/ask-question.js";
|
|
3
|
+
import { allowsTool, parseToolScope } from "../../shared/tool-scope.js";
|
|
3
4
|
import { redactSecretsInText } from "../redact.js";
|
|
4
5
|
import { toolDetailPreview } from "../tool-summary.js";
|
|
5
6
|
import { ChatToolSessionError, mountChatTools } from "./chat-mcp-tools.js";
|
|
@@ -324,7 +325,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
324
325
|
throw new Error(options.missingKeyError);
|
|
325
326
|
if (active.has(turn.threadId))
|
|
326
327
|
throw new Error("a turn is already running on this thread");
|
|
327
|
-
if (options.computerUse && (turn.images?.length || turn.integrations?.
|
|
328
|
+
if (options.computerUse && (turn.images?.length || turn.integrations?.localComputer || turn.integrations?.browser))
|
|
328
329
|
assertImageTransport(options.apiUrl);
|
|
329
330
|
const turnId = newId();
|
|
330
331
|
const abort = new AbortController();
|
|
@@ -333,7 +334,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
333
334
|
for (const message of messages)
|
|
334
335
|
retainImages(message.content);
|
|
335
336
|
const model = turn.model || options.models().default;
|
|
336
|
-
const secrets = [options.apiKey
|
|
337
|
+
const secrets = [options.apiKey].filter((value) => Boolean(value));
|
|
337
338
|
for (const integration of Object.values(turn.integrations ?? {})) {
|
|
338
339
|
const entries = object(integration);
|
|
339
340
|
const specs = entries && "command" in entries ? [entries] : Object.values(entries ?? {}).map(object);
|
|
@@ -397,14 +398,19 @@ export function createOpenAIChatRuntime(options) {
|
|
|
397
398
|
const seenCalls = new Set();
|
|
398
399
|
let nudged = false;
|
|
399
400
|
try {
|
|
400
|
-
|
|
401
|
-
|
|
401
|
+
const parsed = parseToolScope(turn.toolScope);
|
|
402
|
+
if (!parsed.ok)
|
|
403
|
+
throw new Error(parsed.error);
|
|
404
|
+
const scope = parsed.scope;
|
|
405
|
+
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse, scope);
|
|
406
|
+
const questionAllowed = options.tools !== false && allowsTool(scope, { kind: "native", name: ASK_USER_TOOL });
|
|
407
|
+
let optionalQuestionOnly = questionAllowed && tools.definitions.length === 0;
|
|
402
408
|
// The runtime's one built-in tool rides the same list: ask_user is
|
|
403
409
|
// how a chat-completions engine reaches a person. An MCP server that
|
|
404
410
|
// squats the name cannot shadow it — dispatch intercepts the name
|
|
405
411
|
// before validate — but the definition is then skipped so the list
|
|
406
412
|
// never advertises two.
|
|
407
|
-
if (
|
|
413
|
+
if (questionAllowed && !tools.definitions.some((definition) => definition.function.name === ASK_USER_TOOL)) {
|
|
408
414
|
tools.definitions.push(ASK_USER_TOOL_DEFINITION);
|
|
409
415
|
}
|
|
410
416
|
for (let round = 0; round < 16; round++) {
|
|
@@ -549,6 +555,8 @@ export function createOpenAIChatRuntime(options) {
|
|
|
549
555
|
throw new ChatProtocolError("tool arguments must be a JSON object");
|
|
550
556
|
const inputPreview = preview(args);
|
|
551
557
|
if (call.function.name === ASK_USER_TOOL) {
|
|
558
|
+
if (!questionAllowed || !allowsTool(scope, { kind: "native", name: ASK_USER_TOOL }))
|
|
559
|
+
throw new Error("Tool selection excludes this tool");
|
|
552
560
|
// A question is the person's card, not a permission, so it is
|
|
553
561
|
// handled before the gate below: under Full access that gate
|
|
554
562
|
// would auto-run an unanswered ask, and under Ask it would
|
|
@@ -687,10 +695,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
687
695
|
adapter: {
|
|
688
696
|
provider: options.driverKind,
|
|
689
697
|
capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
|
|
690
|
-
|
|
691
|
-
// mount the leased Boat descriptor either. The fleet invariant test
|
|
692
|
-
// pins usesCloudComputer === (remoteAgent || cloudComputerMcp).
|
|
693
|
-
usesCloudComputer: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
|
|
698
|
+
localComputerMcp: options.tools !== false,
|
|
694
699
|
browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
|
|
695
700
|
sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
|
|
696
701
|
// The runtime owns the whole tool loop, so it can always take a
|
|
@@ -34,6 +34,8 @@ import { newEventId, newId } from "../contracts.js";
|
|
|
34
34
|
import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../shared/ask-question.js";
|
|
35
35
|
import { decodeInjectId, encodeInjectId, hostApiKey, localHost, mergeLocalInject, } from "./local-inject.js";
|
|
36
36
|
import { appendNative } from "./native.js";
|
|
37
|
+
import { canUseMcpServer, parseToolScope } from "../../shared/tool-scope.js";
|
|
38
|
+
import { gateServer, mcpStdioServer, resultBudget } from "../mcp-gate-config.js";
|
|
37
39
|
const DRIVER_KIND = "piAgent";
|
|
38
40
|
const PI_ARGS = ["--mode", "rpc", "--no-session"];
|
|
39
41
|
const PI_MODEL_UPDATE_ARGS = ["update", "--models", "--no-approve"];
|
|
@@ -87,6 +89,9 @@ export function piThinkingLevel(effort) {
|
|
|
87
89
|
* a JSON-RPC 2.0 stdio server the pi-mcp-extension consumes. Returns null when
|
|
88
90
|
* there is nothing to mount (the common case). */
|
|
89
91
|
export function buildMcpServers(turn) {
|
|
92
|
+
const parsed = parseToolScope(turn.toolScope);
|
|
93
|
+
if (!parsed.ok)
|
|
94
|
+
throw new Error(parsed.error);
|
|
90
95
|
const servers = {};
|
|
91
96
|
if (turn.integrations?.composio)
|
|
92
97
|
servers.composio = { ...turn.integrations.composio };
|
|
@@ -112,6 +117,22 @@ export function buildMcpServers(turn) {
|
|
|
112
117
|
env: { ...NODE_ENV_FLAG, DWEB_URL: turn.integrations.dweb.url },
|
|
113
118
|
};
|
|
114
119
|
}
|
|
120
|
+
if (turn.integrations?.browser)
|
|
121
|
+
servers.browser = { ...turn.integrations.browser };
|
|
122
|
+
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {}))
|
|
123
|
+
servers[name] = { ...server, scope: "custom" };
|
|
124
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
125
|
+
if (parsed.scope !== undefined && !canUseMcpServer(parsed.scope, name)) {
|
|
126
|
+
delete servers[name];
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const gated = gateServer({ name, server, toolScope: parsed.scope, threadId: turn.threadId, budget: name in (turn.integrations?.custom ?? {}) ? resultBudget() : 0, nodeEnv: NODE_ENV_FLAG });
|
|
130
|
+
const stdio = gated ?? mcpStdioServer(server, { nodeEnv: NODE_ENV_FLAG });
|
|
131
|
+
if (!stdio)
|
|
132
|
+
throw new Error("Pi MCP server configuration is invalid");
|
|
133
|
+
const original = server;
|
|
134
|
+
servers[name] = { ...stdio, ...(original.scope ? { scope: original.scope } : {}) };
|
|
135
|
+
}
|
|
115
136
|
return Object.keys(servers).length ? servers : null;
|
|
116
137
|
}
|
|
117
138
|
/** Pure parser: turn a `get_available_models` stdout blob into a catalog.
|
|
@@ -428,6 +449,10 @@ export const PiDriver = {
|
|
|
428
449
|
});
|
|
429
450
|
const sendTurn = async (turn) => {
|
|
430
451
|
const { threadId } = turn;
|
|
452
|
+
const selection = parseToolScope(turn.toolScope);
|
|
453
|
+
if (!selection.ok)
|
|
454
|
+
throw new Error(selection.error);
|
|
455
|
+
const toolScope = selection.scope;
|
|
431
456
|
if (active.has(threadId))
|
|
432
457
|
throw new Error("a turn is already running on this thread");
|
|
433
458
|
// Per-bot Ask/Auto is authoritative for harness turns. Preserve the
|
|
@@ -469,10 +494,13 @@ export const PiDriver = {
|
|
|
469
494
|
// into a 0600 temp file removed when the turn settles — never on argv.
|
|
470
495
|
const mcpServers = buildMcpServers(turn);
|
|
471
496
|
let mcpTempDir = null;
|
|
472
|
-
|
|
497
|
+
let scopeReadyPath;
|
|
498
|
+
if (mcpServers || toolScope !== undefined) {
|
|
473
499
|
mcpTempDir = mkdtempSync(join(tmpdir(), "omb-pi-mcp-"));
|
|
500
|
+
if (toolScope !== undefined)
|
|
501
|
+
scopeReadyPath = join(mcpTempDir, "scope-ready.json");
|
|
474
502
|
try {
|
|
475
|
-
writeFileSync(join(mcpTempDir, "mcp.json"), JSON.stringify({ mcpServers }), { mode: 0o600 });
|
|
503
|
+
writeFileSync(join(mcpTempDir, "mcp.json"), JSON.stringify({ mcpServers: mcpServers ?? {}, toolScope, scopeReadyPath, approvalMode: turn.approvalMode }), { mode: 0o600 });
|
|
476
504
|
}
|
|
477
505
|
catch (err) {
|
|
478
506
|
// A failed write must not leave the temp dir behind — a partial file
|
|
@@ -486,7 +514,7 @@ export const PiDriver = {
|
|
|
486
514
|
throw err;
|
|
487
515
|
}
|
|
488
516
|
}
|
|
489
|
-
const childArgs =
|
|
517
|
+
const childArgs = mcpTempDir ? [...PI_ARGS, "-e", SPAWNED_PROXIES.piMcpExtension] : PI_ARGS;
|
|
490
518
|
// spawnCli can throw synchronously (unresolvable CLI); if it does, the
|
|
491
519
|
// 0600 temp file with the boat token / composio key / comms token must
|
|
492
520
|
// not be left on disk — settle() never runs because no child existed.
|
|
@@ -498,7 +526,7 @@ export const PiDriver = {
|
|
|
498
526
|
env: piEnvironment({
|
|
499
527
|
...process.env,
|
|
500
528
|
...input.environment,
|
|
501
|
-
...(
|
|
529
|
+
...(mcpTempDir ? { OMB_MCP_CONFIG: join(mcpTempDir, "mcp.json") } : {}),
|
|
502
530
|
}),
|
|
503
531
|
});
|
|
504
532
|
}
|
|
@@ -962,6 +990,20 @@ export const PiDriver = {
|
|
|
962
990
|
/* keep going on the engine default */
|
|
963
991
|
}
|
|
964
992
|
}
|
|
993
|
+
if (scopeReadyPath) {
|
|
994
|
+
let ready = false;
|
|
995
|
+
try {
|
|
996
|
+
const receipt = JSON.parse(readFileSync(scopeReadyPath, "utf8"));
|
|
997
|
+
ready = receipt.ok === true && JSON.stringify(receipt.toolScope) === JSON.stringify(toolScope);
|
|
998
|
+
}
|
|
999
|
+
catch { /* Missing or malformed readiness is never a grant. */ }
|
|
1000
|
+
if (!ready) {
|
|
1001
|
+
const message = "Pi tool selection enforcement is unavailable. Update Pi and check the OpenMausBot extension before retrying.";
|
|
1002
|
+
emit({ ...base(threadId, turnId), type: "runtime.error", message });
|
|
1003
|
+
settle(false);
|
|
1004
|
+
throw new Error(message);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
965
1007
|
// The stable/volatile split: the full prompt rides only the turn that
|
|
966
1008
|
// establishes - or re-instructs, after a soul edit - this pi session,
|
|
967
1009
|
// which previously re-sent the whole system prompt on every turn and
|
|
@@ -1086,6 +1128,8 @@ export const PiDriver = {
|
|
|
1086
1128
|
computerMcp: true,
|
|
1087
1129
|
composioMcp: true,
|
|
1088
1130
|
phoneMcp: true,
|
|
1131
|
+
customMcp: true,
|
|
1132
|
+
browserMcp: true,
|
|
1089
1133
|
// Host control (the user's real Mac) rides the pi-native permission
|
|
1090
1134
|
// card (`ctx.ui.confirm` → extension_ui_request) gated in the
|
|
1091
1135
|
// extension, so it is offered exactly when the other engines offer
|
|
@@ -17,6 +17,10 @@ const TERMINAL_PATTERNS = [
|
|
|
17
17
|
pattern: /\b(?:40[13]|unauthorized|forbidden|invalid api key|missing bearer|authentication required|not logged in|logged out)\b/i,
|
|
18
18
|
reason: "auth",
|
|
19
19
|
},
|
|
20
|
+
// A subscription's usage window is hours away, so its limit is terminal
|
|
21
|
+
// for this engine even when the provider phrases it as a rate limit;
|
|
22
|
+
// checked before the transient 429 pattern for that reason.
|
|
23
|
+
{ pattern: /\busage limit\b|\bhit your (?:usage )?limit\b|\b(?:weekly|daily|monthly|subscription|usage) limit reached\b|\bout of credits\b/i, reason: "quota" },
|
|
20
24
|
{ pattern: /\b402\b|\bquota\b|\bbilling\b|\bsubscription\b/i, reason: "quota" },
|
|
21
25
|
{ pattern: /\bmodel not found\b|\bunknown model\b|\bdoes not exist for model\b|\bunsupported model\b/i, reason: "unknown_model" },
|
|
22
26
|
{ pattern: /\b400\b|\b422\b|\binvalid request\b|\bmalformed\b|\bunexpected status\b/i, reason: "invalid_request" },
|
|
@@ -50,14 +54,14 @@ export function classifyError(err) {
|
|
|
50
54
|
const { exitCode: code } = err;
|
|
51
55
|
if (code !== null && code < 0)
|
|
52
56
|
return { transient: false, reason: "interrupted" };
|
|
53
|
-
for (const { pattern, reason } of TRANSIENT_PATTERNS) {
|
|
54
|
-
if (pattern.test(text))
|
|
55
|
-
return { transient: true, reason };
|
|
56
|
-
}
|
|
57
57
|
for (const { pattern, reason } of TERMINAL_PATTERNS) {
|
|
58
58
|
if (pattern.test(text))
|
|
59
59
|
return { transient: false, reason };
|
|
60
60
|
}
|
|
61
|
+
for (const { pattern, reason } of TRANSIENT_PATTERNS) {
|
|
62
|
+
if (pattern.test(text))
|
|
63
|
+
return { transient: true, reason };
|
|
64
|
+
}
|
|
61
65
|
return { transient: false, reason: "terminal_exit" };
|
|
62
66
|
}
|
|
63
67
|
for (const { pattern, reason } of TERMINAL_PATTERNS) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// driver's own install descriptor and never from a request, npm runs with
|
|
5
5
|
// a fixed argument list, and the binary is found on the engines' PATH
|
|
6
6
|
// afterwards because that directory is registered ahead of everything else.
|
|
7
|
-
import {
|
|
7
|
+
import { mkdirSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { stripVTControlCharacters } from "node:util";
|
|
10
10
|
import { DATA_DIR, stripWorkspaceCredentialEnv } from "./config.js";
|
|
@@ -169,7 +169,3 @@ function runNpm(args, env, cwd, timeoutMs) {
|
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
171
|
}
|
|
172
|
-
/** True once something has been installed here, for status pages. */
|
|
173
|
-
export function enginesInstalled(baseDir = DATA_DIR) {
|
|
174
|
-
return existsSync(enginesBinDir(baseDir));
|
|
175
|
-
}
|
|
@@ -102,6 +102,9 @@ export function environmentDescriptor(input) {
|
|
|
102
102
|
// Never advertise a protocol this server would refuse: the routes are
|
|
103
103
|
// gone unless features.sharedComputers is on, so the capability is too.
|
|
104
104
|
...(input.sharedComputers === true ? { sharedComputers: true } : {}),
|
|
105
|
+
// An OMB Cloud home: its pairing page says "Opening your Cloud…" and
|
|
106
|
+
// where to connect from, not "the code shown on the server".
|
|
107
|
+
...(input.cloudHome === true ? { cloudHome: true } : {}),
|
|
105
108
|
selfUpdate: input.desktopManaged ? "desktop-managed" : "operator",
|
|
106
109
|
emailSignIn: input.emailSignIn === true,
|
|
107
110
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPersistentQuestionCard } from "../shared/ask-question.js";
|
|
1
2
|
export function requestConflict(message = "The request no longer owns this conversation") {
|
|
2
3
|
return Object.assign(new Error(message), { status: 409, code: "guarded_request_changed" });
|
|
3
4
|
}
|
|
@@ -54,7 +55,7 @@ export function requestSourceForCard(messages, messageId) {
|
|
|
54
55
|
return undefined;
|
|
55
56
|
}
|
|
56
57
|
export function requestNeedsInput(message) {
|
|
57
|
-
return Boolean((message.card?.requestId && !message.card.answered && !message.card.dismissed && !message.card.expired) ||
|
|
58
|
+
return Boolean((message.card?.requestId && !message.card.answered && !message.card.dismissed && !message.card.expired && !isPersistentQuestionCard(message.card)) ||
|
|
58
59
|
(message.connector && !message.connector.dismissed && !message.connector.resumed && message.connector.status !== "connected") ||
|
|
59
60
|
(message.secret && !message.secret.provided && !message.secret.dismissed && !message.secret.superseded));
|
|
60
61
|
}
|
|
@@ -228,7 +228,6 @@ export class ProviderRegistry {
|
|
|
228
228
|
modelVariants: inst.adapter.capabilities.modelVariants === true,
|
|
229
229
|
queueing: inst.adapter.capabilities.queueing === true,
|
|
230
230
|
localComputerMcp: inst.adapter.capabilities.localComputerMcp === true,
|
|
231
|
-
cloudComputerMcp: inst.adapter.capabilities.cloudComputerMcp === true,
|
|
232
231
|
approvalReview: inst.reviewPermission !== undefined,
|
|
233
232
|
},
|
|
234
233
|
access: driver?.metadata.access ?? "subscription",
|
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
// Per-turn MCP entry point
|
|
2
|
-
//
|
|
1
|
+
// Per-turn MCP entry point for the tools the harness serves itself: the
|
|
2
|
+
// built-in browser and the cloud computer. argv[2] names which one. Only a
|
|
3
|
+
// turn-scoped capability crosses into the agent process; engine commands,
|
|
4
|
+
// session names, and provider credentials (the Boat token) stay in OMB.
|
|
3
5
|
import { pathToFileURL } from "node:url";
|
|
4
6
|
const MAX_INPUT_BYTES = 1_048_576;
|
|
5
7
|
const MAX_OUTPUT_BYTES = 16_777_216;
|
|
6
8
|
const MAX_PENDING = 16;
|
|
9
|
+
const NAMES = { browser: "Browser", computer: "Cloud computer" };
|
|
10
|
+
/** What a model needs after a failed call, per tool family: the failure is
|
|
11
|
+
* about this one connection, and an uncertain action must be checked first. */
|
|
12
|
+
const RECOVERY = {
|
|
13
|
+
browser: " This failure does not establish that all browsers are unavailable. Inspect available connections with select_computer if that tool is present. Keep the requested computer and account; do not bypass a permission refusal or human takeover, and do not repeat an uncertain action without checking its result.",
|
|
14
|
+
computer: " Keep working on the assigned cloud computer only; do not bypass a person's control of it, and take a fresh screenshot before repeating an action whose result is uncertain.",
|
|
15
|
+
};
|
|
16
|
+
export function parseHarnessMcpKind(value) {
|
|
17
|
+
return value === "browser" || value === "computer" ? value : null;
|
|
18
|
+
}
|
|
7
19
|
function failure(id, method, message, code = -32603) {
|
|
8
20
|
return method === "tools/call"
|
|
9
21
|
? { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: message }], isError: true } }
|
|
10
22
|
: { jsonrpc: "2.0", id, error: { code, message } };
|
|
11
23
|
}
|
|
12
|
-
async function boundedJson(response) {
|
|
24
|
+
async function boundedJson(response, name) {
|
|
13
25
|
if (!response.body)
|
|
14
|
-
throw new Error(
|
|
26
|
+
throw new Error(`${name} server returned an empty response.`);
|
|
15
27
|
const reader = response.body.getReader();
|
|
16
28
|
const chunks = [];
|
|
17
29
|
let size = 0;
|
|
@@ -22,7 +34,7 @@ async function boundedJson(response) {
|
|
|
22
34
|
break;
|
|
23
35
|
size += value.byteLength;
|
|
24
36
|
if (size > MAX_OUTPUT_BYTES)
|
|
25
|
-
throw new Error(
|
|
37
|
+
throw new Error(`${name} response exceeded the size limit.`);
|
|
26
38
|
chunks.push(Buffer.from(value));
|
|
27
39
|
}
|
|
28
40
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
@@ -31,7 +43,7 @@ async function boundedJson(response) {
|
|
|
31
43
|
await reader.cancel().catch(() => { });
|
|
32
44
|
}
|
|
33
45
|
}
|
|
34
|
-
export async function
|
|
46
|
+
export async function harnessMcpRequest(frame, connection) {
|
|
35
47
|
if (!frame || typeof frame !== "object" || Array.isArray(frame))
|
|
36
48
|
return failure(null, null, "Invalid request.", -32600);
|
|
37
49
|
const message = frame;
|
|
@@ -40,10 +52,11 @@ export async function browserProxyRequest(frame, connection) {
|
|
|
40
52
|
return failure(null, null, "Invalid request.", -32600);
|
|
41
53
|
if (!Object.hasOwn(message, "id"))
|
|
42
54
|
return undefined;
|
|
55
|
+
const name = NAMES[connection.kind];
|
|
43
56
|
if (message.method === "initialize")
|
|
44
57
|
return {
|
|
45
58
|
jsonrpc: "2.0", id,
|
|
46
|
-
result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name:
|
|
59
|
+
result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: `openmausbot-${connection.kind}`, version: "1" } },
|
|
47
60
|
};
|
|
48
61
|
if (message.method === "ping")
|
|
49
62
|
return { jsonrpc: "2.0", id, result: {} };
|
|
@@ -52,30 +65,30 @@ export async function browserProxyRequest(frame, connection) {
|
|
|
52
65
|
try {
|
|
53
66
|
const url = new URL(connection.url);
|
|
54
67
|
if (!connection.token || url.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
55
|
-
throw new Error(
|
|
68
|
+
throw new Error(`${name} connection is not configured. Start a new bot turn from OpenMausBot.`);
|
|
56
69
|
}
|
|
57
70
|
const body = JSON.stringify({ method: message.method, params: message.params ?? {} });
|
|
58
71
|
if (Buffer.byteLength(body) > MAX_INPUT_BYTES)
|
|
59
|
-
throw new Error(
|
|
60
|
-
const response = await fetch(new URL(
|
|
72
|
+
throw new Error(`${name} request exceeded the size limit.`);
|
|
73
|
+
const response = await fetch(new URL(`/api/internal/${connection.kind}/mcp`, url), {
|
|
61
74
|
method: "POST", redirect: "error",
|
|
62
75
|
headers: { "content-type": "application/json", authorization: `Bearer ${connection.token}` },
|
|
63
76
|
body, signal: AbortSignal.timeout(130_000),
|
|
64
77
|
});
|
|
65
|
-
const payload = await boundedJson(response);
|
|
78
|
+
const payload = await boundedJson(response, name);
|
|
66
79
|
if (!response.ok || !payload || typeof payload !== "object" || !Object.hasOwn(payload, "result")) {
|
|
67
|
-
throw new Error(typeof payload?.error === "string" ? payload.error.slice(0, 2_000) :
|
|
80
|
+
throw new Error(typeof payload?.error === "string" ? payload.error.slice(0, 2_000) : `${name} server is unavailable. Start a new bot turn or try again.`);
|
|
68
81
|
}
|
|
69
82
|
return { jsonrpc: "2.0", id, result: payload.result };
|
|
70
83
|
}
|
|
71
84
|
catch (error) {
|
|
72
85
|
const detail = error instanceof Error && !/fetch failed|abort|timeout/i.test(error.message)
|
|
73
|
-
? error.message :
|
|
74
|
-
return failure(id, message.method, detail +
|
|
86
|
+
? error.message : `${name} connection was interrupted. Do not repeat the action until its state is checked.`;
|
|
87
|
+
return failure(id, message.method, detail + RECOVERY[connection.kind]);
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
|
-
function run() {
|
|
78
|
-
const connection = { url: process.env.OMB_HARNESS_URL ?? "", token: process.env.
|
|
90
|
+
function run(kind) {
|
|
91
|
+
const connection = { url: process.env.OMB_HARNESS_URL ?? "", token: process.env.OMB_MCP_TOKEN ?? "", kind };
|
|
79
92
|
let input = Buffer.alloc(0);
|
|
80
93
|
let pending = 0;
|
|
81
94
|
const output = (message) => {
|
|
@@ -114,11 +127,11 @@ function run() {
|
|
|
114
127
|
if (pending >= MAX_PENDING) {
|
|
115
128
|
const request = frame;
|
|
116
129
|
if (request && Object.hasOwn(request, "id"))
|
|
117
|
-
output(failure(request.id ?? null, request.method,
|
|
130
|
+
output(failure(request.id ?? null, request.method, `Too many pending ${kind} requests.`));
|
|
118
131
|
continue;
|
|
119
132
|
}
|
|
120
133
|
pending++;
|
|
121
|
-
void
|
|
134
|
+
void harnessMcpRequest(frame, connection).then(output).finally(() => { pending--; });
|
|
122
135
|
}
|
|
123
136
|
if (input.length > MAX_INPUT_BYTES) {
|
|
124
137
|
process.stdin.destroy();
|
|
@@ -126,5 +139,12 @@ function run() {
|
|
|
126
139
|
}
|
|
127
140
|
});
|
|
128
141
|
}
|
|
129
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
130
|
-
|
|
142
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
143
|
+
const kind = parseHarnessMcpKind(process.argv[2]);
|
|
144
|
+
if (!kind) {
|
|
145
|
+
process.stderr.write("usage: harness-mcp-proxy browser|computer\n");
|
|
146
|
+
process.exitCode = 2;
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
run(kind);
|
|
150
|
+
}
|