openmausbot 0.1.93 → 0.1.94

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.
Files changed (140) hide show
  1. package/dist/assets/{DesktopViewer-CKNR3SEW.js → DesktopViewer-bxM7NV3N.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
  3. package/dist/assets/{arc-BBdBuCgB.js → arc-B5itKDcx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
  7. package/dist/assets/channel-CdJX6i8k.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
  34. package/dist/assets/index-CPYrGsv1.js +610 -0
  35. package/dist/assets/{index-BkYwilkG.js → index-CPiqc_DN.js} +1 -1
  36. package/dist/assets/index-CzT42GR8.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
  41. package/dist/assets/{linear-bq1PTDTW.js → linear-BpKhuyLm.js} +1 -1
  42. package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-B_BKNCw5.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -0
  62. package/dist-server/companion/src/routes.js +21 -0
  63. package/dist-server/container-mcp.js +93 -76
  64. package/dist-server/drivers/agents-proxy.js +38 -1
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +1 -0
  67. package/dist-server/index.js +16942 -13191
  68. package/dist-server/local-computer.js +1 -0
  69. package/dist-server/mcp-gate.js +147 -19
  70. package/dist-server/mcp-remote-proxy.js +437 -0
  71. package/dist-server/openmausbot.js +8233 -7651
  72. package/dist-server/pair-cli.js +8233 -7651
  73. package/dist-server/permission-proxy.js +65 -1
  74. package/dist-server/proxy-paths.js +1 -0
  75. package/dist-server/server/auto-approve.js +33 -11
  76. package/dist-server/server/browser-live.js +14 -5
  77. package/dist-server/server/browser-runtime.js +6 -0
  78. package/dist-server/server/channel-queue.js +4 -1
  79. package/dist-server/server/cli.js +32 -12
  80. package/dist-server/server/cloud-move-http.js +9 -3
  81. package/dist-server/server/cloud-move.js +18 -0
  82. package/dist-server/server/computer-wait.js +33 -13
  83. package/dist-server/server/config.js +44 -10
  84. package/dist-server/server/decision-log.js +9 -6
  85. package/dist-server/server/delegations.js +47 -22
  86. package/dist-server/server/drivers/acp/core.js +57 -8
  87. package/dist-server/server/drivers/acp/grok.js +208 -3
  88. package/dist-server/server/drivers/agents-call.js +28 -0
  89. package/dist-server/server/drivers/agents-catalog.js +10 -1
  90. package/dist-server/server/drivers/boatagent.js +2 -0
  91. package/dist-server/server/drivers/chat-mcp-tools.js +27 -14
  92. package/dist-server/server/drivers/claude.js +18 -6
  93. package/dist-server/server/drivers/codex.js +84 -10
  94. package/dist-server/server/drivers/openai-chat.js +11 -3
  95. package/dist-server/server/drivers/pi.js +48 -4
  96. package/dist-server/server/environment.js +3 -0
  97. package/dist-server/server/guarded-requests.js +2 -1
  98. package/dist-server/server/index.js +1143 -254
  99. package/dist-server/server/live-call-controller.js +868 -0
  100. package/dist-server/server/live-call.js +216 -0
  101. package/dist-server/server/mcp-gate-config.js +43 -12
  102. package/dist-server/server/mcp-gate.js +127 -25
  103. package/dist-server/server/mcp-http.js +46 -8
  104. package/dist-server/server/mcp-remote-proxy.js +108 -0
  105. package/dist-server/server/new-bot-defaults.js +3 -0
  106. package/dist-server/server/peer-provenance.js +4 -2
  107. package/dist-server/server/permission-proxy.js +15 -1
  108. package/dist-server/server/plan-usage.js +797 -0
  109. package/dist-server/server/proxy-paths.js +1 -0
  110. package/dist-server/server/redact.js +2 -0
  111. package/dist-server/server/request-auth.js +3 -2
  112. package/dist-server/server/routes/live.js +98 -0
  113. package/dist-server/server/skill-library.js +207 -0
  114. package/dist-server/server/skills-library-migration.js +235 -0
  115. package/dist-server/server/skills.js +66 -3
  116. package/dist-server/server/steer-queue.js +13 -1
  117. package/dist-server/server/store.js +32 -4
  118. package/dist-server/server/surface.js +9 -3
  119. package/dist-server/server/team-backup.js +2 -1
  120. package/dist-server/server/thread-events.js +1 -1
  121. package/dist-server/server/tunnel.js +20 -1
  122. package/dist-server/server/turn-log.js +10 -0
  123. package/dist-server/server/turn-resources.js +124 -0
  124. package/dist-server/shared/ask-question.js +13 -0
  125. package/dist-server/shared/call-consent.js +28 -0
  126. package/dist-server/shared/live-approval.js +124 -0
  127. package/dist-server/shared/live-call.js +176 -0
  128. package/dist-server/shared/skill-md.js +10 -0
  129. package/dist-server/shared/team-backup.js +3 -0
  130. package/dist-server/shared/tool-scope-support.js +25 -0
  131. package/dist-server/shared/tool-scope.js +136 -0
  132. package/dist-server/src/lib/thread-refs.js +406 -0
  133. package/dist-server/vps-container-mcp.js +93 -76
  134. package/dist-server/workspace-backup.worker.js +89 -100
  135. package/enterprise/server/index.js +1 -0
  136. package/package.json +1 -1
  137. package/dist/assets/channel-BU94VpxE.js +0 -1
  138. package/dist/assets/index-Biji-bfL.css +0 -1
  139. package/dist/assets/index-DZlhoES0.js +0 -597
  140. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
@@ -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
- mountMcpServer(appServerArgs, env, "openmausbot_connectors", turn.integrations.composio);
724
+ mountSelected("composio", "openmausbot_connectors", turn.integrations.composio);
700
725
  }
701
726
  if (turn.integrations?.agents) {
702
- mountMcpServer(appServerArgs, env, "agents", turn.integrations.agents);
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
- mountMcpServer(appServerArgs, env, "computer", turn.integrations.localComputer);
732
+ mountSelected("computer", "computer", turn.integrations.localComputer);
708
733
  }
709
734
  if (turn.integrations?.browser) {
710
- mountMcpServer(appServerArgs, env, "browser", turn.integrations.browser);
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
- mountMcpServer(appServerArgs, env, mountName, server, false);
757
+ mountSelected(name, mountName, server, false);
729
758
  }
730
759
  if (turn.integrations?.phone) {
731
- const bridge = turn.integrations.phone;
732
- Object.assign(env, bridge.env);
733
- const prefix = "mcp_servers.openmausbot_phone";
734
- 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"`);
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";
@@ -397,14 +398,19 @@ export function createOpenAIChatRuntime(options) {
397
398
  const seenCalls = new Set();
398
399
  let nudged = false;
399
400
  try {
400
- tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse);
401
- let optionalQuestionOnly = options.tools !== false && tools.definitions.length === 0;
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 (options.tools !== false && !tools.definitions.some((definition) => definition.function.name === ASK_USER_TOOL)) {
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
@@ -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
- if (mcpServers) {
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 = mcpServers ? [...PI_ARGS, "-e", SPAWNED_PROXIES.piMcpExtension] : PI_ARGS;
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
- ...(mcpServers && mcpTempDir ? { OMB_MCP_CONFIG: join(mcpTempDir, "mcp.json") } : {}),
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
@@ -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
  }