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
@@ -4,10 +4,13 @@
4
4
  // The generic protocol runtime lives in acp/core.ts; this file is only the
5
5
  // per-harness quirks. Verified against grok 1.0.0.
6
6
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
- import { join } from "node:path";
8
- import { harnessHome } from "../../env-path.js";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { setTimeout as delay } from "node:timers/promises";
9
+ import { parse as parseYaml } from "yaml";
10
+ import { harnessHome, splitCliString } from "../../env-path.js";
9
11
  import { decodeInjectId, hostApiKey, localHost, mergeLocalInject } from "../local-inject.js";
10
12
  import { createAcpDriver } from "./core.js";
13
+ import { allowsTool, canUseMcpServer, narrowsNativeTools, parseToolScope } from "../../../shared/tool-scope.js";
11
14
  export const STATIC_GROK_MODELS = {
12
15
  default: "grok-4.7",
13
16
  options: [
@@ -181,6 +184,140 @@ export function grokAcceptsUnadvertisedImages(init) {
181
184
  const version = /^1\.0\.(\d+)$/.exec(meta.agentVersion);
182
185
  return Boolean(version && Number(version[1]) >= 25);
183
186
  }
187
+ // Original names in the official 1.0.41 default registry, not arbitrary
188
+ // owner strings forwarded into Grok's inherit-all profile allowlist.
189
+ const GROK_NATIVE_TOOLS = [
190
+ ["read_file", "GrokBuild:read_file"], ["search_replace", "GrokBuild:search_replace"], ["write", "OpenCode:write"],
191
+ ["run_terminal_command", "GrokBuild:run_terminal_cmd"], ["list_dir", "GrokBuild:list_dir"], ["grep", "GrokBuild:grep"],
192
+ ["kill_command_or_subagent", "GrokBuild:kill_task"], ["todo_write", "GrokBuild:todo_write"],
193
+ ["get_command_or_subagent_output", "GrokBuild:get_task_output"], ["spawn_subagent", "GrokBuild:task"],
194
+ ["scheduler_create", "GrokBuild:scheduler_create"], ["scheduler_delete", "GrokBuild:scheduler_delete"], ["scheduler_list", "GrokBuild:scheduler_list"],
195
+ ["monitor", "GrokBuild:monitor"], ["search_tool", "GrokBuild:search_tool"], ["use_tool", "GrokBuild:use_tool"],
196
+ ["workflow", "GrokBuild:workflow"], ["enter_plan_mode", "GrokBuild:enter_plan_mode"], ["exit_plan_mode", "GrokBuild:exit_plan_mode"],
197
+ ["ask_user_question", "GrokBuild:ask_user_question"], ["send_feedback", "GrokBuild:send_feedback"],
198
+ ["image_gen", "GrokBuild:image_gen"], ["image_edit", "GrokBuild:image_edit"],
199
+ ["image_to_video", "GrokBuild:image_to_video"], ["reference_to_video", "GrokBuild:reference_to_video"],
200
+ ];
201
+ /** Read a supported on-disk profile rather than overriding its restriction. */
202
+ export function grokInheritedProfile(cli, env, cwd) {
203
+ const unsupported = () => { throw new Error("Grok's existing agent profile cannot be safely intersected with tool selection. Use a profile file with explicit tools or a separate default Grok account."); };
204
+ if ((env.GROK_AGENT && env.GROK_AGENT !== "grok-build") || env.GROK_CONFIG || env.GROK_CONFIG_PATH)
205
+ return unsupported();
206
+ const args = splitCliString(cli);
207
+ // Operator filters are applied after ACP's profile by the CLI. Refuse
208
+ // unverified overrides rather than replacing an inherited restriction.
209
+ if (args.some(arg => /^(--tools|--disallowed-tools|--disallowedTools|--agent)(=|$)/.test(arg)))
210
+ return unsupported();
211
+ const flags = args.flatMap((arg, index) => arg === "--agent-profile" || arg.startsWith("--agent-profile=") ? [index] : []);
212
+ if (flags.length > 1)
213
+ return unsupported();
214
+ const flag = flags[0] ?? -1;
215
+ let path = flag >= 0 ? (args[flag].includes("=") ? args[flag].slice(args[flag].indexOf("=") + 1) : args[flag + 1]) : undefined;
216
+ if (flag >= 0 && (!path || path.startsWith("-")))
217
+ return unsupported();
218
+ const files = [join(env.GROK_HOME || harnessHome("grok", env), "config.toml")];
219
+ for (let directory = resolve(cwd);;) {
220
+ const file = join(directory, ".grok", "config.toml");
221
+ if (!files.includes(file))
222
+ files.push(file);
223
+ const parent = dirname(directory);
224
+ if (parent === directory)
225
+ break;
226
+ directory = parent;
227
+ }
228
+ for (const file of files) {
229
+ if (!existsSync(file))
230
+ continue;
231
+ const text = readFileSync(file, "utf8");
232
+ if (text.length > 262_144)
233
+ return unsupported();
234
+ // Support a deliberately small, unambiguous TOML subset. Scan the whole
235
+ // file: quoted/dotted keys, inline agent tables and multiline constructs
236
+ // must never be silently missed by a section regex. Unsupported syntax
237
+ // refuses this scoped turn; ordinary unrestricted turns are unaffected.
238
+ let section = "";
239
+ for (const line of text.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith("#"))) {
240
+ const header = /^\[([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\]\s*(?:#.*)?$/.exec(line);
241
+ if (header) {
242
+ section = header[1];
243
+ if (section.startsWith("agent."))
244
+ return unsupported();
245
+ continue;
246
+ }
247
+ const assignment = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
248
+ if (!assignment || /'''|"""/.test(assignment[2]) || (!section && assignment[1] === "agent"))
249
+ return unsupported();
250
+ if (section !== "agent")
251
+ continue;
252
+ const match = /^(name|definition)\s*=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(?:#.*)?$/.exec(line);
253
+ if (!match)
254
+ return unsupported();
255
+ let value;
256
+ try {
257
+ value = match[2].startsWith('"') ? JSON.parse(match[2]) : match[2].slice(1, -1);
258
+ }
259
+ catch {
260
+ return unsupported();
261
+ }
262
+ // Grok expands environment variables in paths. Do not resolve a
263
+ // different literal file or fall back when that contract is unknown.
264
+ if (value.includes("$") || !value)
265
+ return unsupported();
266
+ if (match[1] === "name" && value !== "grok-build")
267
+ return unsupported();
268
+ if (match[1] === "definition" && flag < 0) {
269
+ if (path !== undefined && path !== value)
270
+ return unsupported();
271
+ path = value;
272
+ }
273
+ }
274
+ }
275
+ if (!path)
276
+ return;
277
+ const text = readFileSync(resolve(cwd, path), "utf8");
278
+ if (text.length > 262_144)
279
+ return unsupported();
280
+ const match = /^\s*---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/.exec(text);
281
+ if (!match)
282
+ return unsupported();
283
+ const profile = parseYaml(match[1]);
284
+ if (!profile || typeof profile.name !== "string" || typeof profile.description !== "string"
285
+ || (profile.tools !== undefined && (!Array.isArray(profile.tools) || !profile.tools.every((tool) => typeof tool === "string")))
286
+ || (profile.disallowedTools !== undefined && (!Array.isArray(profile.disallowedTools) || !profile.disallowedTools.every((tool) => typeof tool === "string"))))
287
+ return unsupported();
288
+ const toolConfig = profile.toolConfig ?? { tools: GROK_NATIVE_TOOLS.map(([name, id]) => ({ id, name_override: name })) };
289
+ if (!Array.isArray(toolConfig.tools) || !toolConfig.tools.every((tool) => typeof tool?.id === "string" && (tool.name_override === undefined || typeof tool.name_override === "string")))
290
+ return unsupported();
291
+ return { ...profile, name: profile.name, description: profile.description, toolConfig, ...(match[2]?.trim() ? { promptBody: match[2].trim() } : {}) };
292
+ }
293
+ /** A curated registry: empty/unknown selections never inherit Grok's defaults. */
294
+ export function grokToolScopeProfile(scope, init, inherited, hasMcp = false) {
295
+ const parsed = parseToolScope(scope);
296
+ if (!parsed.ok)
297
+ throw new Error(parsed.error);
298
+ const meta = init?._meta;
299
+ if (meta?.grokShell !== true || meta.agentVersion !== "1.0.41") {
300
+ throw new Error("This Grok runtime does not have verified native tool selection. Use Grok 1.0.41 or Pi; update Grok support before using another version.");
301
+ }
302
+ if (hasMcp && (!allowsTool(scope, { kind: "native", name: "search_tool" }) || !allowsTool(scope, { kind: "native", name: "use_tool" }))) {
303
+ throw new Error("Grok requires native:search_tool and native:use_tool to find and call selected MCP tools. Allow both explicitly or use Pi.");
304
+ }
305
+ const candidates = inherited?.toolConfig.tools ?? GROK_NATIVE_TOOLS.map(([name, id]) => ({ id, name_override: name }));
306
+ const selected = candidates.filter((tool) => {
307
+ const name = tool.name_override ?? GROK_NATIVE_TOOLS.find(([, id]) => id === tool.id)?.[0];
308
+ return name !== undefined && allowsTool(scope, { kind: "native", name })
309
+ && (!inherited?.tools?.length || inherited.tools.includes(name) || inherited.tools.includes(tool.id))
310
+ && !inherited?.disallowedTools?.some((denied) => denied === name || denied === tool.id);
311
+ });
312
+ // Grok rejects an empty curated construction. A real read entry explicitly
313
+ // disabled by its exact native name constructs an empty final registry.
314
+ return {
315
+ ...inherited, name: inherited?.name ?? "openmausbot-tools", description: inherited?.description ?? "Owner-selected tools",
316
+ injectDefaultTools: false, discoverSkills: false,
317
+ toolConfig: { ...inherited?.toolConfig, tools: selected.length ? selected : [{ id: "GrokBuild:read_file", name_override: "read_file" }] },
318
+ tools: [], disallowedTools: [...new Set([...(inherited?.disallowedTools ?? []), ...(selected.length ? [] : ["read_file"])])],
319
+ };
320
+ }
184
321
  const support = {
185
322
  driverKind: "grokAgent",
186
323
  displayName: "Grok",
@@ -225,17 +362,69 @@ const support = {
225
362
  ? "bypassPermissions"
226
363
  : turn.approvalMode === "auto" ? "auto" : turn.approvalMode === "edits" ? "acceptEdits" : "default",
227
364
  "agent",
365
+ ...(turn.toolScope !== undefined ? ["--no-leader"] : []),
228
366
  ...(turn.model ? ["-m", turn.model] : []),
229
367
  // long form on purpose: `--effort` is documented as an alias, and an
230
368
  // alias is the part a CLI is free to rename
231
369
  ...(turn.effort ? ["--reasoning-effort", turn.effort] : []),
232
370
  "stdio",
233
371
  ],
372
+ toolScopeSessionParams: (turn, init, hasMcp, { config, env, cwd }) => {
373
+ if (!narrowsNativeTools(turn.toolScope))
374
+ return {};
375
+ const profile = grokToolScopeProfile(turn.toolScope, init, grokInheritedProfile(config.cli, env, cwd), hasMcp);
376
+ // Establish on the selected model. Changing harnesses after session/new
377
+ // can discard an ACP profile and restore the model's default tools.
378
+ if (turn.model)
379
+ profile.model = ensureGrokInjectSlug(turn.model, env);
380
+ return { _meta: { agentProfile: profile } };
381
+ },
382
+ toolScopeCacheKey: ({ config, env, cwd }) => JSON.stringify(grokInheritedProfile(config.cli, env, cwd) ?? null),
234
383
  // -m on argv is necessary but not sufficient: session/new still starts on
235
384
  // [models].default. Pin the slug over the wire, same as Hermes/Droid.
236
- async configureSession({ request, sessionId, turn }) {
385
+ async configureSession({ request, sessionId, turn, currentModelId }) {
386
+ if (turn.toolScope !== undefined) {
387
+ const available = new Set([
388
+ ...(turn.integrations?.agents ? ["agents"] : []), ...(turn.integrations?.composio ? ["composio"] : []),
389
+ ...(turn.integrations?.browser ? ["browser"] : []), ...(turn.integrations?.localComputer ? ["computer"] : []),
390
+ ...Object.keys(turn.integrations?.custom ?? {}),
391
+ ].filter((name) => canUseMcpServer(turn.toolScope, name)));
392
+ const deadline = Date.now() + 10_000;
393
+ for (;;) {
394
+ const reply = await request("_x.ai/mcp/list", { sessionId, cache: true }, 10_000);
395
+ const catalog = reply?.result;
396
+ if (!Array.isArray(catalog?.servers))
397
+ throw new Error("Grok could not confirm its MCP catalog. No prompt was sent.");
398
+ for (const server of catalog.servers) {
399
+ if (server?.session?.enabled === false)
400
+ continue;
401
+ if (!available.has(server?.name))
402
+ throw new Error("Grok has an MCP connection outside the bot's selection. Disable native MCP connections before using tool selection. No prompt was sent.");
403
+ if (Array.isArray(server?.session?.tools) && server.session.tools.some((tool) => typeof tool.name !== "string" || !allowsTool(turn.toolScope, { kind: "mcp", server: server.name, name: tool.name }))) {
404
+ throw new Error("Grok could not enforce the selected MCP catalog. No prompt was sent.");
405
+ }
406
+ }
407
+ if (catalog.sessionMcpResolved === true) {
408
+ if ([...available].some((name) => !catalog.servers.some((server) => server.name === name && server.session?.enabled === true && server.session.status === "ready"))) {
409
+ throw new Error("Grok could not connect a selected MCP server. No prompt was sent.");
410
+ }
411
+ break;
412
+ }
413
+ if (Date.now() >= deadline)
414
+ throw new Error("Grok did not confirm its selected MCP catalog in time. No prompt was sent.");
415
+ await delay(50);
416
+ }
417
+ }
237
418
  if (!turn.model)
238
419
  return;
420
+ if (narrowsNativeTools(turn.toolScope)) {
421
+ if (currentModelId !== turn.model) {
422
+ throw new Error("Grok could not confirm the selected model without replacing its tool profile. Start a new conversation or choose a model with a supported Grok profile. No prompt was sent.");
423
+ }
424
+ // The inline profile already pins this model. Repeating set_model can
425
+ // rebuild the harness, or reject an otherwise valid resumed profile.
426
+ return;
427
+ }
239
428
  try {
240
429
  await request("session/set_model", { sessionId, modelId: turn.model });
241
430
  }
@@ -249,6 +438,22 @@ const support = {
249
438
  transformEnv: (env) => {
250
439
  delete env.XAI_API_KEY;
251
440
  },
441
+ applyTurnEnv: (env, { toolScope }) => {
442
+ if (toolScope !== undefined) {
443
+ // The verified runtime gives these local env switches priority over
444
+ // account defaults, preventing an unfiltered managed gateway fallback.
445
+ env.GROK_MANAGED_MCPS_ENABLED = "false";
446
+ env.GROK_MANAGED_MCP_GATEWAY_TOOLS_ENABLED = "false";
447
+ if (narrowsNativeTools(toolScope)) {
448
+ if (env.GROK_AGENT && env.GROK_AGENT !== "grok-build") {
449
+ throw new Error("Grok's existing agent profile cannot be safely intersected with tool selection. Use a separate default Grok account.");
450
+ }
451
+ // A model's agent_type otherwise takes priority over ACP profiles.
452
+ // Preserve profile-file restrictions when intersecting them below.
453
+ env.GROK_AGENT = "grok-build";
454
+ }
455
+ }
456
+ },
252
457
  // Bind the grok.com subscription login. No API-key fallback by design —
253
458
  // an unauthenticated CLI is a user action, not something to paper over.
254
459
  pickAuthMethod: (methods) => (methods.some((m) => m.id === "cached_token") ? "cached_token" : null),
@@ -507,6 +507,34 @@ export async function callTool(name, args, context) {
507
507
  }) });
508
508
  return { text: JSON.stringify(r), ...(r.error ? { isError: true } : {}) };
509
509
  }
510
+ if (name === "send_to_bot") {
511
+ const botId = String(args.bot_id ?? "").trim();
512
+ const title = String(args.title ?? "").trim();
513
+ const message = String(args.message ?? "").trim();
514
+ if (!botId || !title || !message) {
515
+ return { text: "send_to_bot needs bot_id, title and message.", isError: true };
516
+ }
517
+ if (botId === context.botId) {
518
+ return { text: "send_to_bot is cross-bot only. Use start_thread to send independent work to yourself.", isError: true };
519
+ }
520
+ if (turn.threadsOpenedThisTurn >= MAX_THREADS_PER_TURN) {
521
+ return { text: `You have already opened ${MAX_THREADS_PER_TURN} threads this turn, which is the limit.`, isError: true };
522
+ }
523
+ const r = await api("/api/internal/threads", { method: "POST", body: JSON.stringify({
524
+ fromBotId: context.botId, fromThreadId: context.threadId, toBotId: botId,
525
+ title, message, depth: context.depth, oneWay: true,
526
+ }) });
527
+ if (r.error)
528
+ return { text: `Couldn't send to that bot: ${String(r.error)}`, isError: true };
529
+ turn.threadsOpenedThisTurn += 1;
530
+ const destination = `@${String(r.botName ?? "that bot")} in #${String(r.title ?? title)} [thread id: ${String(r.threadId ?? "")}]`;
531
+ const state = r.approvalRequired === true
532
+ ? `Send to ${destination} is pending approval. The person's approval card appears after this turn ends; dispatch starts once approved.`
533
+ : r.state === "queued"
534
+ ? `Send to ${destination} is queued ${ordinal(Number(r.position) || 1)} for a free slot and can dispatch after this turn ends.`
535
+ : `Send to ${destination} is pending until this turn ends, then dispatch starts.`;
536
+ return { text: `${state} Its result stays in that thread; nothing there will resume you.` };
537
+ }
510
538
  if (name === "list_bots") {
511
539
  const r = await api(`/api/internal/agents?self=${encodeURIComponent(BOT_ID)}`);
512
540
  const bots = r.bots ?? [];
@@ -221,6 +221,15 @@ const toolDefinitions = (externalRuntime) => [
221
221
  label: { type: "string", description: "Optional short name (one line, at most 60 characters) for this job. Names the new direct assignment thread." },
222
222
  }, required: ["bot_ids", "message", "request_key"] },
223
223
  },
224
+ {
225
+ name: "send_to_bot",
226
+ description: "Send work one way to another bot in a fresh thread. This is cross-bot only: use start_thread to send independent work to yourself. The recipient owns the new thread; its results, failures and questions stay there and never resume you. Use coordinate_bots when you need the result returned here.",
227
+ inputSchema: { type: "object", additionalProperties: false, properties: {
228
+ bot_id: { type: "string", description: "One reachable teammate id from list_bots or list_room_targets." },
229
+ title: { type: "string", description: "A short one-line title, at most 80 characters." },
230
+ message: { type: "string", description: "Complete instructions; the recipient does not inherit this conversation." },
231
+ }, required: ["bot_id", "title", "message"] },
232
+ },
224
233
  {
225
234
  name: "list_bots",
226
235
  description: "List the other bots (agents) you may contact in your own team and any additional teams the owner has explicitly allowed you to coordinate, with their team, model and current status. Call this to discover exact teammate IDs before assigning work or requesting advice through your available coordination tools.",
@@ -757,7 +766,7 @@ const VOICE_TOOL_NAMES = new Set(["send_voice_note"]);
757
766
  // One teamwork path in room turns; keep all unrelated integrations available.
758
767
  // Ordinary direct chats use this same bounded coordinator. Goal-owned turns
759
768
  // retain their independent loop and cannot start a second coordinator.
760
- const ROOM_ONLY_TOOLS = new Set(["list_room_targets", "coordinate_bots"]);
769
+ const ROOM_ONLY_TOOLS = new Set(["list_room_targets", "coordinate_bots", "send_to_bot"]);
761
770
  const ROOM_REPLACED_TOOLS = new Set(["ask_bot", "delegate_bot", "check_delegation", "wait_delegation", "start_thread", "send_to_thread", "wait_thread"]);
762
771
  const EXTERNAL_TOOL_NAMES = new Set(["list_bots", "ask_bot", "delegate_bot", "check_delegation", "wait_delegation"]);
763
772
  const WATCHER_TOOL_NAMES = new Set(["create_options_card"]);
@@ -1,6 +1,7 @@
1
1
  import { newEventId, newId } from "../contracts.js";
2
2
  import { boatCredential, boatProviderApi } from "../included-services.js";
3
3
  import { appendNative } from "./native.js";
4
+ import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
4
5
  import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
5
6
  // "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
6
7
  const DRIVER_KIND = "boxAgent";
@@ -96,6 +97,7 @@ export const BoatAgentDriver = {
96
97
  return body;
97
98
  };
98
99
  const sendTurn = async (turn) => {
100
+ turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
99
101
  const { threadId } = turn;
100
102
  const computer = turn.integrations?.computer;
101
103
  const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
@@ -8,6 +8,8 @@ import { augmentedPath } from "../env-path.js";
8
8
  import { killCliTree, spawnCli } from "../procs.js";
9
9
  import { chatImage } from "./chat-images.js";
10
10
  import { ChatBoatClient } from "./chat-boat-tools.js";
11
+ import { mcpStdioServer } from "../mcp-gate-config.js";
12
+ import { allowsTool, canUseMcpServer, parseToolScope } from "../../shared/tool-scope.js";
11
13
  /** The transport cannot safely continue this turn. A dispatched operation may
12
14
  * already have taken effect, so callers must not retry it through a new round. */
13
15
  export class ChatToolSessionError extends Error {
@@ -190,7 +192,7 @@ class ChatMcpClient {
190
192
  throw error;
191
193
  }
192
194
  }
193
- async tools(signal) {
195
+ async tools(signal, include = () => true) {
194
196
  const deadline = Date.now() + STARTUP_MS;
195
197
  const remaining = () => {
196
198
  if (Date.now() >= deadline)
@@ -212,7 +214,7 @@ class ChatMcpClient {
212
214
  const result = await this.call("tools/list", cursor ? { cursor } : {}, signal, remaining());
213
215
  if (!object(result) || !Array.isArray(result.tools))
214
216
  throw new Error("MCP tools/list returned an invalid result");
215
- tools.push(...result.tools);
217
+ tools.push(...result.tools.filter(include));
216
218
  if (tools.length > TOOL_COUNT)
217
219
  throw new Error("MCP tool count exceeds the 128-tool limit");
218
220
  if (result.nextCursor === undefined)
@@ -282,23 +284,30 @@ function omitBlankDefaults(builtInBrowser, tool, args) {
282
284
  delete args[field];
283
285
  }
284
286
  }
285
- export async function mountChatTools(integrations, signal, computerUse = false) {
287
+ export async function mountChatTools(integrations, signal, computerUse = false, toolScope) {
288
+ const parsed = parseToolScope(toolScope);
289
+ if (!parsed.ok)
290
+ throw new Error(parsed.error);
291
+ const scope = parsed.scope;
286
292
  const servers = [];
287
- if (computerUse && integrations?.computer)
293
+ const eligible = (server) => scope === undefined || canUseMcpServer(scope, server);
294
+ if (computerUse && integrations?.computer && eligible("computer"))
288
295
  servers.push(["computer", integrations.computer]);
289
- if (computerUse && integrations?.localComputer)
296
+ if (computerUse && integrations?.localComputer && eligible("computer"))
290
297
  servers.push(["computer", integrations.localComputer]);
291
- if (computerUse && integrations?.browser)
298
+ if (computerUse && integrations?.browser && eligible("browser"))
292
299
  servers.push(["browser", integrations.browser]);
293
- if (integrations?.agents)
300
+ if (integrations?.agents && eligible("agents"))
294
301
  servers.push(["agents", integrations.agents]);
295
- if (integrations?.composio)
302
+ if (integrations?.composio && eligible("composio"))
296
303
  servers.push(["composio", integrations.composio]);
297
- // this client starts its servers and talks over stdio; a remote (url)
298
- // entry is skipped here and reaches Claude and Codex bots
299
304
  for (const [name, server] of Object.entries(integrations?.custom ?? {})) {
300
- if ("command" in server)
301
- servers.push([name, server]);
305
+ if (!eligible(name))
306
+ continue;
307
+ const stdio = mcpStdioServer(server, { nodeEnv: { ELECTRON_RUN_AS_NODE: "1" } });
308
+ if (!stdio)
309
+ throw new Error("MCP server configuration is invalid");
310
+ servers.push([name, { command: stdio.command, args: stdio.args ?? [], env: stdio.env ?? {} }]);
302
311
  }
303
312
  if (servers.length > 32)
304
313
  throw new Error("MCP server count exceeds the 32-server limit");
@@ -332,7 +341,9 @@ export async function mountChatTools(integrations, signal, computerUse = false)
332
341
  // image delivery, including custom servers. Text stays bounded below.
333
342
  const client = "boxId" in descriptor ? new ChatBoatClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
334
343
  clients.push(client);
335
- return { name, client, builtInBrowser: descriptor === integrations?.browser, tools: await client.tools(signal) };
344
+ const include = (tool) => scope === undefined || (object(tool) && typeof tool.name === "string" && allowsTool(scope, { kind: "mcp", server: name, name: tool.name }));
345
+ const tools = client instanceof ChatMcpClient ? await client.tools(signal, include) : (await client.tools()).filter(include);
346
+ return { name, client, builtInBrowser: descriptor === integrations?.browser, tools };
336
347
  }));
337
348
  for (const mount of mounts) {
338
349
  if (mount.status === "rejected")
@@ -368,7 +379,7 @@ export async function mountChatTools(integrations, signal, computerUse = false)
368
379
  const suffix = `_${index}`;
369
380
  name = base.slice(0, 64 - suffix.length) + suffix;
370
381
  }
371
- registered.set(name, { client, builtInBrowser, name: tool.name, schema });
382
+ registered.set(name, { client, server, builtInBrowser, name: tool.name, schema });
372
383
  definitions.push({ type: "function", function: { name, description, parameters } });
373
384
  if (Buffer.byteLength(JSON.stringify(definitions)) > CATALOG_BYTES)
374
385
  throw new Error("MCP tool catalog exceeds the 1MB limit");
@@ -387,6 +398,8 @@ export async function mountChatTools(integrations, signal, computerUse = false)
387
398
  const tool = registered.get(name);
388
399
  if (!tool)
389
400
  throw new Error("The requested tool was not advertised for this turn");
401
+ if (scope !== undefined && !allowsTool(scope, { kind: "mcp", server: tool.server, name: tool.name }))
402
+ throw new Error("Tool selection excludes this tool");
390
403
  omitBlankDefaults(tool.builtInBrowser, tool.name, args);
391
404
  if (!object(args) || !tool.schema(args))
392
405
  throw new Error("Tool arguments do not match the advertised input schema; use its required fields and types");
@@ -20,6 +20,8 @@ import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli
20
20
  import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
21
21
  import { ClaudeLoginController } from "./claude-login-auth.js";
22
22
  import { gateServer, resultBudget } from "../mcp-gate-config.js";
23
+ import { canUseMcpServer } from "../../shared/tool-scope.js";
24
+ import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
23
25
  import { newEventId, newId } from "../contracts.js";
24
26
  import { askInputSummary, commandSummary, toolDetailPreview } from "../tool-summary.js";
25
27
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
@@ -1127,6 +1129,7 @@ export const ClaudeDriver = {
1127
1129
  // is a fresh sendTurn, and the attempt cap must survive across launches
1128
1130
  const retryState = new Map();
1129
1131
  const sendTurn = async (turn, logicalTurnId) => {
1132
+ turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
1130
1133
  if (config.managedModels && (!turn.model || !config.managedModels.includes(turn.model)))
1131
1134
  throw new Error("This model is not assigned to this workspace.");
1132
1135
  if (config.requireApiKey && !input.environment.ANTHROPIC_API_KEY)
@@ -1200,7 +1203,7 @@ export const ClaudeDriver = {
1200
1203
  args.push("--disallowedTools", config.disallowedTools.join(","));
1201
1204
  }
1202
1205
  const turnEnvironment = environment();
1203
- if ((turn.refreshSystemPrompt || turn.guestConfined) && !cliVersionChecked) {
1206
+ if ((turn.refreshSystemPrompt || turn.guestConfined || turn.toolScope !== undefined) && !cliVersionChecked) {
1204
1207
  const version = await readCliVersion(turnEnvironment);
1205
1208
  if (version) {
1206
1209
  cliVersion = parseClaudeCliVersion(version);
@@ -1216,6 +1219,9 @@ export const ClaudeDriver = {
1216
1219
  if (replaysUserMessages)
1217
1220
  args.push("--replay-user-messages");
1218
1221
  const isolated = !inheritsUserConfig(turnEnvironment);
1222
+ if (turn.toolScope !== undefined && (!isolated || turn.mcpFromUserConfig || cliVersion === null || !claudeCliSupports(cliVersion, "--strict-mcp-config"))) {
1223
+ throw new Error("Claude cannot confirm a restricted MCP configuration for this account. Disable inherited Claude MCP servers and use a current, identifiable Claude CLI before using tool selection.");
1224
+ }
1219
1225
  if (isolated) {
1220
1226
  // A bot gets the tools and instructions its owner gave it, not
1221
1227
  // whatever this machine's Claude Code happens to be set up with.
@@ -1350,11 +1356,16 @@ export const ClaudeDriver = {
1350
1356
  // permission broker, computer, browser, agents, dweb) are already
1351
1357
  // bounded and are deliberately left alone.
1352
1358
  const budget = resultBudget(turnEnvironment);
1353
- for (const name of botOwned) {
1354
- const gated = gateServer({ name, server: mcpServers[name], threadId, budget, nodeEnv: NODE_ENV_FLAG });
1359
+ for (const name of turn.toolScope === undefined ? botOwned : Object.keys(mcpServers)) {
1360
+ if (!canUseMcpServer(turn.toolScope, name)) {
1361
+ delete mcpServers[name];
1362
+ continue;
1363
+ }
1364
+ const gated = gateServer({ name, server: mcpServers[name], threadId, budget: botOwned.has(name) ? budget : 0, nodeEnv: NODE_ENV_FLAG, toolScope: turn.toolScope });
1355
1365
  if (gated)
1356
- mcpServers[name] = gated;
1366
+ mcpServers[name] = { ...gated, ...(mcpServers[name]?.alwaysLoad === true ? { alwaysLoad: true } : {}) };
1357
1367
  }
1368
+ allowed.splice(0, allowed.length, ...allowed.filter((name) => Object.hasOwn(mcpServers, name.slice("mcp__".length))));
1358
1369
  // Keep ask_user available even in Full access. Native bypass skips
1359
1370
  // permission prompts, not questions requiring a person's answer.
1360
1371
  let broker;
@@ -1362,7 +1373,8 @@ export const ClaudeDriver = {
1362
1373
  if (permissionMode !== "bypassPermissions") {
1363
1374
  args.push("--permission-prompt-tool", "mcp__ogb__approve");
1364
1375
  }
1365
- mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
1376
+ const permissionEnv = { ...NODE_ENV_FLAG, ...(turn.toolScope !== undefined ? { OMB_PERMISSION_TOOL_SCOPE: JSON.stringify(turn.toolScope) } : {}) };
1377
+ mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: permissionEnv, alwaysLoad: true };
1366
1378
  allowed.push("mcp__ogb");
1367
1379
  // A guest's turn pre-allows only the harness's own tools: anything
1368
1380
  // else (the browser can open a file: address) asks the owner first.
@@ -1575,7 +1587,7 @@ export const ClaudeDriver = {
1575
1587
  // the base path: the nonce is not part of the spawn contract, and a
1576
1588
  // retained session keeps its own broker object anyway.
1577
1589
  if (broker.socketPath !== socketPath && mcpConfigPath) {
1578
- mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, broker.socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
1590
+ mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, broker.socketPath], env: permissionEnv, alwaysLoad: true };
1579
1591
  }
1580
1592
  }
1581
1593
  // Write once, only after the broker has selected its real endpoint.