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
|
@@ -41,6 +41,12 @@ import { classifyQuiet, describeQuiet, lastSeenPhrase, LogTail, quietKey, sample
|
|
|
41
41
|
export function skipSubscriptionAuthForLocalInject(model) {
|
|
42
42
|
return Boolean(decodeInjectId(model));
|
|
43
43
|
}
|
|
44
|
+
/** A catalog can contain credential-bearing native configuration, including late replies. */
|
|
45
|
+
export function acpNativeIncomingLogMessage(message, privateResponses) {
|
|
46
|
+
if (!privateResponses.has(message?.id))
|
|
47
|
+
return message;
|
|
48
|
+
return { jsonrpc: message.jsonrpc, id: message.id, ...(message.error !== undefined ? { error: "[MCP catalog error omitted]" } : { result: "[MCP catalog omitted]" }) };
|
|
49
|
+
}
|
|
44
50
|
import { newEventId, newId, TurnNotStartedError } from "../../contracts.js";
|
|
45
51
|
import { augmentedPath } from "../../env-path.js";
|
|
46
52
|
import { supportsApprovalMode } from "../../../shared/approval-mode.js";
|
|
@@ -53,6 +59,8 @@ import { redactSecretsInText } from "../../redact.js";
|
|
|
53
59
|
import { recoveryPromptFor } from "../../resume-recovery.js";
|
|
54
60
|
import { sessionIdlePolicy } from "../session-idle.js";
|
|
55
61
|
import { classifyError } from "../retry.js";
|
|
62
|
+
import { canUseMcpServer, narrowsNativeTools, parseToolScope } from "../../../shared/tool-scope.js";
|
|
63
|
+
import { gateServer } from "../../mcp-gate-config.js";
|
|
56
64
|
const ACCOUNT_ERROR_CODES = new Set([
|
|
57
65
|
"invalid_credentials", "inactive_subscription", "insufficient_funds", "quota_or_region_restriction",
|
|
58
66
|
]);
|
|
@@ -83,10 +91,12 @@ function acpRpcError(value, method) {
|
|
|
83
91
|
});
|
|
84
92
|
}
|
|
85
93
|
const envOr = (key, fallback) => Number(process.env[key] ?? fallback);
|
|
86
|
-
const INIT_TIMEOUT =
|
|
87
|
-
const SESSION_CONFIG_TIMEOUT =
|
|
88
|
-
const NEW_SESSION_TIMEOUT =
|
|
89
|
-
const LOAD_SESSION_TIMEOUT =
|
|
94
|
+
const INIT_TIMEOUT = 300_000;
|
|
95
|
+
const SESSION_CONFIG_TIMEOUT = 300_000; // configureSession's per-request default
|
|
96
|
+
const NEW_SESSION_TIMEOUT = 300_000;
|
|
97
|
+
const LOAD_SESSION_TIMEOUT = 120_000; // history replay on a long thread is slow
|
|
98
|
+
/** How long a quiet agent shows no sign of life before it is called stuck. */
|
|
99
|
+
const ACP_STUCK_AFTER_MS = 180_000;
|
|
90
100
|
/** ACP agents may compact their own history without telling the client;
|
|
91
101
|
* re-send the full prompt after this many bare turns as a backstop to
|
|
92
102
|
* compaction detection. */
|
|
@@ -133,6 +143,18 @@ function trackRunningTool(current, update, defaultStatus) {
|
|
|
133
143
|
else if (status === "pending" || status === "in_progress")
|
|
134
144
|
current.runningTools.add(update.toolCallId);
|
|
135
145
|
}
|
|
146
|
+
let unkeyedToolSeq = 0;
|
|
147
|
+
/** Stamp an unkeyed `tool_call` with a synthetic id and queue it for the
|
|
148
|
+
* call's first terminal update, which carries no id of its own to match.
|
|
149
|
+
* FIFO is the best available pairing when the agent omits `toolCallId`:
|
|
150
|
+
* the ids exist to pair start with completion, not to order concurrent
|
|
151
|
+
* unkeyed calls, and the synthetic id deliberately stays out of
|
|
152
|
+
* `runningTools` so turn-settle semantics are unchanged. */
|
|
153
|
+
function nextUnkeyedToolId(current) {
|
|
154
|
+
const id = `acp-tool-unkeyed-${++unkeyedToolSeq}`;
|
|
155
|
+
current.unkeyedToolIds.push(id);
|
|
156
|
+
return id;
|
|
157
|
+
}
|
|
136
158
|
const CLIENT_FILE_MAX_BYTES = 8 * 1024 * 1024;
|
|
137
159
|
function acpVariantOption(result) {
|
|
138
160
|
const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find((entry) => entry?.type === "select" && typeof entry.id === "string"
|
|
@@ -509,7 +531,18 @@ export function createAcpDriver(support) {
|
|
|
509
531
|
}
|
|
510
532
|
servers.push({ name, command: server.command, args: server.args, env: acpEnv(server.env) });
|
|
511
533
|
}
|
|
512
|
-
|
|
534
|
+
if (turn.toolScope === undefined)
|
|
535
|
+
return servers;
|
|
536
|
+
return servers.filter((server) => canUseMcpServer(turn.toolScope, server.name)).map((server) => {
|
|
537
|
+
const original = "url" in server
|
|
538
|
+
? { type: server.type, url: server.url, headers: Object.fromEntries(server.headers.map(({ name, value }) => [name, value])) }
|
|
539
|
+
: { command: server.command, args: server.args, env: Object.fromEntries(server.env.map(({ name, value }) => [name, value])) };
|
|
540
|
+
const gated = gateServer({ name: server.name, server: original, threadId: turn.threadId, budget: 0,
|
|
541
|
+
toolScope: turn.toolScope, nodeEnv: { ELECTRON_RUN_AS_NODE: "1" } });
|
|
542
|
+
if (!gated)
|
|
543
|
+
throw new Error("Tool selection requires an MCP gate.");
|
|
544
|
+
return { name: server.name, command: gated.command, args: gated.args, env: acpEnv(gated.env) };
|
|
545
|
+
});
|
|
513
546
|
};
|
|
514
547
|
/** While a prompt runs, notice when the agent goes quiet and tell the
|
|
515
548
|
* person what it is doing: retrying, compressing, waiting on its
|
|
@@ -583,7 +616,7 @@ export function createAcpDriver(support) {
|
|
|
583
616
|
// "no sign of life" is called only once that has lasted minutes.
|
|
584
617
|
if (next.kind === "no-signs") {
|
|
585
618
|
noSignsSince ??= now;
|
|
586
|
-
if (now - noSignsSince <
|
|
619
|
+
if (now - noSignsSince < ACP_STUCK_AFTER_MS)
|
|
587
620
|
next = { kind: "between-requests" };
|
|
588
621
|
}
|
|
589
622
|
else {
|
|
@@ -667,6 +700,7 @@ export function createAcpDriver(support) {
|
|
|
667
700
|
stdio: ["pipe", "pipe", "pipe"],
|
|
668
701
|
});
|
|
669
702
|
let nextId = 1;
|
|
703
|
+
const privateResponses = new Set();
|
|
670
704
|
const rpcPending = new Map();
|
|
671
705
|
const send = (obj) => {
|
|
672
706
|
// A permission/file response resumes an agent that was waiting on us.
|
|
@@ -683,6 +717,8 @@ export function createAcpDriver(support) {
|
|
|
683
717
|
};
|
|
684
718
|
const request = (method, params, timeoutMs, receive, idleMs, idleMessage) => new Promise((resolve, reject) => {
|
|
685
719
|
const id = nextId++;
|
|
720
|
+
if (method === "_x.ai/mcp/list")
|
|
721
|
+
privateResponses.add(id);
|
|
686
722
|
let timer = null;
|
|
687
723
|
if (timeoutMs) {
|
|
688
724
|
timer = setTimeout(() => {
|
|
@@ -1007,7 +1043,7 @@ export function createAcpDriver(support) {
|
|
|
1007
1043
|
...base(threadId, current.turnId),
|
|
1008
1044
|
type: "item.started",
|
|
1009
1045
|
itemType: "tool",
|
|
1010
|
-
itemId: u.toolCallId,
|
|
1046
|
+
itemId: (typeof u.toolCallId === "string" && u.toolCallId) ? u.toolCallId : nextUnkeyedToolId(current),
|
|
1011
1047
|
title: String(u.rawInput?.command ?? u.title ?? "tool").slice(0, 80),
|
|
1012
1048
|
summary: commandSummary(u.rawInput),
|
|
1013
1049
|
input: toolDetailPreview(u.rawInput),
|
|
@@ -1022,7 +1058,7 @@ export function createAcpDriver(support) {
|
|
|
1022
1058
|
...base(threadId, current.turnId),
|
|
1023
1059
|
type: "item.completed",
|
|
1024
1060
|
itemType: "tool",
|
|
1025
|
-
itemId: u.toolCallId,
|
|
1061
|
+
itemId: (typeof u.toolCallId === "string" && u.toolCallId) ? u.toolCallId : current.unkeyedToolIds.shift(),
|
|
1026
1062
|
ok: u.status !== "failed",
|
|
1027
1063
|
output: toolDetailPreview(u.rawOutput ?? u.content),
|
|
1028
1064
|
});
|
|
@@ -1084,7 +1120,7 @@ export function createAcpDriver(support) {
|
|
|
1084
1120
|
catch {
|
|
1085
1121
|
continue;
|
|
1086
1122
|
}
|
|
1087
|
-
appendNative(threadId, { dir: "in", source: SOURCE, msg: nativeLogMessage(msg) });
|
|
1123
|
+
appendNative(threadId, { dir: "in", source: SOURCE, msg: nativeLogMessage(acpNativeIncomingLogMessage(msg, privateResponses)) });
|
|
1088
1124
|
// Inbound traffic proves the child is alive and making progress,
|
|
1089
1125
|
// so every idle deadline restarts; only total silence trips it.
|
|
1090
1126
|
lastInboundAt = Date.now();
|
|
@@ -1160,6 +1196,13 @@ export function createAcpDriver(support) {
|
|
|
1160
1196
|
return session;
|
|
1161
1197
|
};
|
|
1162
1198
|
const sendTurn = async (turn) => {
|
|
1199
|
+
const parsedScope = parseToolScope(turn.toolScope);
|
|
1200
|
+
if (!parsedScope.ok)
|
|
1201
|
+
throw new Error(parsedScope.error);
|
|
1202
|
+
turn = { ...turn, toolScope: parsedScope.scope };
|
|
1203
|
+
if (narrowsNativeTools(turn.toolScope) && !support.toolScopeSessionParams) {
|
|
1204
|
+
throw new Error(`${support.displayName}: native tool selection is not supported by this engine. Keep native:* in the selection or choose a supported engine.`);
|
|
1205
|
+
}
|
|
1163
1206
|
const { threadId } = turn;
|
|
1164
1207
|
if (active.has(threadId))
|
|
1165
1208
|
throw new Error("a turn is already running on this thread");
|
|
@@ -1190,7 +1233,7 @@ export function createAcpDriver(support) {
|
|
|
1190
1233
|
}
|
|
1191
1234
|
const resolvedModel = support.resolveTurnModel?.(turn.model, env);
|
|
1192
1235
|
support.applyTurnEnv?.(env, {
|
|
1193
|
-
model: resolvedModel, requestedModel: turn.model, fullAuto: turnConfig.fullAuto === true, botId: turn.botId, cwd,
|
|
1236
|
+
model: resolvedModel, requestedModel: turn.model, fullAuto: turnConfig.fullAuto === true, botId: turn.botId, cwd, toolScope: turn.toolScope,
|
|
1194
1237
|
});
|
|
1195
1238
|
// Rebound once, before the prompt, when a fallbackModel support swaps
|
|
1196
1239
|
// a model this session does not offer for one it does.
|
|
@@ -1234,6 +1277,8 @@ export function createAcpDriver(support) {
|
|
|
1234
1277
|
// riding a child spawned under the old env. Hash it so secrets
|
|
1235
1278
|
// never sit in the key itself.
|
|
1236
1279
|
const envFingerprint = createHash("sha256").update(JSON.stringify(spawnEnv)).digest("hex").slice(0, 16);
|
|
1280
|
+
const inheritedScopeFingerprint = narrowsNativeTools(turn.toolScope) && support.toolScopeCacheKey
|
|
1281
|
+
? createHash("sha256").update(support.toolScopeCacheKey({ config: turnConfig, env, cwd })).digest("hex") : null;
|
|
1237
1282
|
// A support that applies the approval mode to the session on every
|
|
1238
1283
|
// turn gets the same process whatever the mode: an approval change
|
|
1239
1284
|
// is one RPC, not a cold start.
|
|
@@ -1241,6 +1286,8 @@ export function createAcpDriver(support) {
|
|
|
1241
1286
|
launch.command, launch.args ?? [], spawnArgs, cwd,
|
|
1242
1287
|
support.sessionScopedApproval ? null : turnConfig.fullAuto === true, envFingerprint,
|
|
1243
1288
|
support.spawnFingerprint?.(spawnEnv) ?? null,
|
|
1289
|
+
turn.toolScope ?? null,
|
|
1290
|
+
inheritedScopeFingerprint,
|
|
1244
1291
|
]);
|
|
1245
1292
|
// The new-session default model rides the spawn env but stays out of
|
|
1246
1293
|
// the fingerprint above: only session/new reads it, and a pooled
|
|
@@ -1259,7 +1306,7 @@ export function createAcpDriver(support) {
|
|
|
1259
1306
|
launchedThisTurn = true;
|
|
1260
1307
|
return opened;
|
|
1261
1308
|
};
|
|
1262
|
-
const sessionKey = JSON.stringify(mcpServers);
|
|
1309
|
+
const sessionKey = JSON.stringify([mcpServers, turn.toolScope ?? null, inheritedScopeFingerprint]);
|
|
1263
1310
|
if (turn.sessionReset) {
|
|
1264
1311
|
closeSession(threadId, "reset");
|
|
1265
1312
|
sessionAllows.delete(threadId);
|
|
@@ -1357,6 +1404,7 @@ export function createAcpDriver(support) {
|
|
|
1357
1404
|
acknowledge,
|
|
1358
1405
|
asks,
|
|
1359
1406
|
runningTools: new Set(),
|
|
1407
|
+
unkeyedToolIds: [],
|
|
1360
1408
|
interruptTimer: null,
|
|
1361
1409
|
stopQuietWatch: null,
|
|
1362
1410
|
flushAssistantText,
|
|
@@ -1467,10 +1515,12 @@ export function createAcpDriver(support) {
|
|
|
1467
1515
|
// an agent that advertised its transport, so an agent without
|
|
1468
1516
|
// http/sse never sees an entry it would refuse the session over
|
|
1469
1517
|
const sessionServers = mcpServers.filter((server) => !("type" in server) || init?.agentCapabilities?.mcpCapabilities?.[server.type] === true);
|
|
1518
|
+
const selectionParams = narrowsNativeTools(turn.toolScope)
|
|
1519
|
+
? support.toolScopeSessionParams(turn, init, sessionServers.length > 0, { config: turnConfig, env, cwd }) : {};
|
|
1470
1520
|
let loaded = false;
|
|
1471
1521
|
if (cursor) {
|
|
1472
1522
|
try {
|
|
1473
|
-
await request(support.resumeMethod === "resume" ? "session/resume" : "session/load", { sessionId: cursor, cwd, mcpServers: sessionServers }, LOAD_SESSION_TIMEOUT, (result) => {
|
|
1523
|
+
await request(support.resumeMethod === "resume" ? "session/resume" : "session/load", { sessionId: cursor, cwd, mcpServers: sessionServers, ...selectionParams }, LOAD_SESSION_TIMEOUT, (result) => {
|
|
1474
1524
|
if (result) {
|
|
1475
1525
|
loaded = true;
|
|
1476
1526
|
session.sessionId = cursor;
|
|
@@ -1526,7 +1576,7 @@ export function createAcpDriver(support) {
|
|
|
1526
1576
|
promptTurn = { ...turn, text: recovery.text };
|
|
1527
1577
|
rebuiltFromReplay = recovery.replayed;
|
|
1528
1578
|
}
|
|
1529
|
-
sessionResult = await request("session/new", { cwd, mcpServers: sessionServers }, NEW_SESSION_TIMEOUT, (result) => {
|
|
1579
|
+
sessionResult = await request("session/new", { cwd, mcpServers: sessionServers, ...selectionParams }, NEW_SESSION_TIMEOUT, (result) => {
|
|
1530
1580
|
session.sessionId = typeof result?.sessionId === "string" ? result.sessionId : null;
|
|
1531
1581
|
session.sessionKey = sessionKey;
|
|
1532
1582
|
receiveModelVariants(result);
|
|
@@ -1612,6 +1662,7 @@ export function createAcpDriver(support) {
|
|
|
1612
1662
|
sessionModels: Array.isArray(sessionResult?.models?.availableModels)
|
|
1613
1663
|
? sessionResult.models.availableModels
|
|
1614
1664
|
: [],
|
|
1665
|
+
currentModelId: session.sessionConfigResult?.models?.currentModelId,
|
|
1615
1666
|
});
|
|
1616
1667
|
approvalUnconfirmed = false;
|
|
1617
1668
|
// initialize's currentModelId is the CLI default,
|
|
@@ -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 {
|
|
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),
|
|
@@ -31,7 +31,6 @@ export function toolCallContextFromEnv(env) {
|
|
|
31
31
|
createdThisTurn: 0,
|
|
32
32
|
roomPostsThisTurn: 0,
|
|
33
33
|
threadsOpenedThisTurn: 0,
|
|
34
|
-
memoryRefusalsThisTurn: 0,
|
|
35
34
|
delegationTaskIdsThisTurn: new Set(),
|
|
36
35
|
},
|
|
37
36
|
};
|
|
@@ -49,11 +48,9 @@ const MAX_ROOM_POSTS_PER_TURN = 3;
|
|
|
49
48
|
// deciding. The harness holds the same ceiling; this copy exists so the
|
|
50
49
|
// refusal reaches the model without a round trip.
|
|
51
50
|
const MAX_THREADS_PER_TURN = 5;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// told what did not fit instead of a transcript of retries.
|
|
56
|
-
const MAX_MEMORY_REFUSALS_PER_TURN = 3;
|
|
51
|
+
/** How many entries a memory_update reply names when its write moved some
|
|
52
|
+
* to the archive; the rest are counted. */
|
|
53
|
+
const MOVED_ENTRIES_SHOWN = 5;
|
|
57
54
|
const SHORT_WEEKDAYS = {
|
|
58
55
|
mon: "monday",
|
|
59
56
|
tue: "tuesday",
|
|
@@ -278,7 +275,7 @@ function routineFields(args) {
|
|
|
278
275
|
const runOn = destination(args.run_on ?? args.runOn);
|
|
279
276
|
const timeoutMinutes = args.timeout_minutes ?? args.timeoutMinutes;
|
|
280
277
|
if (runOn != null && runOn !== "maus" && runOn !== "cloud") {
|
|
281
|
-
return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the Boat
|
|
278
|
+
return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the bot’s Boat cloud computer. Legacy "cloud" also means Boat.' };
|
|
282
279
|
}
|
|
283
280
|
if (timeoutMinutes != null && (typeof timeoutMinutes !== "number" || !Number.isInteger(timeoutMinutes) || timeoutMinutes < 5 || timeoutMinutes > 240)) {
|
|
284
281
|
return { fields, error: "timeout_minutes must be a whole number from 5 to 240. Use clear_timeout to remove a limit." };
|
|
@@ -483,30 +480,54 @@ export async function callTool(name, args, context) {
|
|
|
483
480
|
// retry needs instead of a generic validation error (#1239).
|
|
484
481
|
const canonical = { ...args };
|
|
485
482
|
delete canonical.botIds;
|
|
486
|
-
delete canonical.requestKey;
|
|
487
483
|
delete canonical.groupId;
|
|
488
484
|
if (canonical.bot_ids === undefined)
|
|
489
485
|
canonical.bot_ids = args.botIds;
|
|
490
|
-
if (canonical.request_key === undefined)
|
|
491
|
-
canonical.request_key = args.requestKey;
|
|
492
486
|
if (canonical.group_id === undefined)
|
|
493
487
|
canonical.group_id = args.groupId;
|
|
494
488
|
const ids = canonical.bot_ids;
|
|
495
489
|
const usable = Array.isArray(ids) && ids.length > 0 && ids.every((id) => typeof id === "string")
|
|
496
|
-
&& typeof canonical.message === "string" && canonical.message.trim().length > 0
|
|
497
|
-
&& typeof canonical.request_key === "string" && canonical.request_key.trim().length > 0;
|
|
490
|
+
&& typeof canonical.message === "string" && canonical.message.trim().length > 0;
|
|
498
491
|
if (!usable) {
|
|
499
492
|
return {
|
|
500
|
-
text: `coordinate_bots takes snake_case arguments: bot_ids (an array of 1-4 teammate ids)
|
|
493
|
+
text: `coordinate_bots takes snake_case arguments: bot_ids (an array of 1-4 teammate ids) and message are required; group_id and rework are optional. Received: ${Object.keys(args).join(", ") || "none"}.`,
|
|
501
494
|
isError: true,
|
|
502
495
|
};
|
|
503
496
|
}
|
|
504
497
|
const r = await api("/api/internal/coordinate-bots", { method: "POST", body: JSON.stringify({
|
|
505
498
|
groupId: canonical.group_id, botIds: ids, message: canonical.message,
|
|
506
|
-
|
|
499
|
+
rework: canonical.rework,
|
|
507
500
|
}) });
|
|
508
501
|
return { text: JSON.stringify(r), ...(r.error ? { isError: true } : {}) };
|
|
509
502
|
}
|
|
503
|
+
if (name === "send_to_bot") {
|
|
504
|
+
const botId = String(args.bot_id ?? "").trim();
|
|
505
|
+
const title = String(args.title ?? "").trim();
|
|
506
|
+
const message = String(args.message ?? "").trim();
|
|
507
|
+
if (!botId || !title || !message) {
|
|
508
|
+
return { text: "send_to_bot needs bot_id, title and message.", isError: true };
|
|
509
|
+
}
|
|
510
|
+
if (botId === context.botId) {
|
|
511
|
+
return { text: "send_to_bot is cross-bot only. Use start_thread to send independent work to yourself.", isError: true };
|
|
512
|
+
}
|
|
513
|
+
if (turn.threadsOpenedThisTurn >= MAX_THREADS_PER_TURN) {
|
|
514
|
+
return { text: `You have already opened ${MAX_THREADS_PER_TURN} threads this turn, which is the limit.`, isError: true };
|
|
515
|
+
}
|
|
516
|
+
const r = await api("/api/internal/threads", { method: "POST", body: JSON.stringify({
|
|
517
|
+
fromBotId: context.botId, fromThreadId: context.threadId, toBotId: botId,
|
|
518
|
+
title, message, depth: context.depth, oneWay: true,
|
|
519
|
+
}) });
|
|
520
|
+
if (r.error)
|
|
521
|
+
return { text: `Couldn't send to that bot: ${String(r.error)}`, isError: true };
|
|
522
|
+
turn.threadsOpenedThisTurn += 1;
|
|
523
|
+
const destination = `@${String(r.botName ?? "that bot")} in #${String(r.title ?? title)} [thread id: ${String(r.threadId ?? "")}]`;
|
|
524
|
+
const state = r.approvalRequired === true
|
|
525
|
+
? `Send to ${destination} is pending approval. The person's approval card appears after this turn ends; dispatch starts once approved.`
|
|
526
|
+
: r.state === "queued"
|
|
527
|
+
? `Send to ${destination} is queued ${ordinal(Number(r.position) || 1)} for a free slot and can dispatch after this turn ends.`
|
|
528
|
+
: `Send to ${destination} is pending until this turn ends, then dispatch starts.`;
|
|
529
|
+
return { text: `${state} Its result stays in that thread; nothing there will resume you.` };
|
|
530
|
+
}
|
|
510
531
|
if (name === "list_bots") {
|
|
511
532
|
const r = await api(`/api/internal/agents?self=${encodeURIComponent(BOT_ID)}`);
|
|
512
533
|
const bots = r.bots ?? [];
|
|
@@ -1087,12 +1108,6 @@ export async function callTool(name, args, context) {
|
|
|
1087
1108
|
|| (args.action !== "append" && (typeof args.old_text !== "string" || !args.old_text.trim()))) {
|
|
1088
1109
|
return { text: "Use memory_update action=append with text, replace or supersede with text and old_text, or remove with old_text.", isError: true };
|
|
1089
1110
|
}
|
|
1090
|
-
if (turn.memoryRefusalsThisTurn >= MAX_MEMORY_REFUSALS_PER_TURN) {
|
|
1091
|
-
return {
|
|
1092
|
-
text: `Memory updates are closed for the rest of this turn: ${MAX_MEMORY_REFUSALS_PER_TURN} were refused. Do not retry. Tell the person what you wanted to keep and why it did not fit; they can tidy MEMORY.md in Settings, and you can try again in your next turn.`,
|
|
1093
|
-
isError: true,
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
1096
1111
|
const { body: r } = await apiResponse("/api/internal/memory", {
|
|
1097
1112
|
method: "POST",
|
|
1098
1113
|
body: JSON.stringify({
|
|
@@ -1104,16 +1119,23 @@ export async function callTool(name, args, context) {
|
|
|
1104
1119
|
...(typeof args.until === "string" && args.until.trim() ? { until: args.until.trim() } : {}),
|
|
1105
1120
|
}),
|
|
1106
1121
|
});
|
|
1107
|
-
if (r.error || r.ok !== true)
|
|
1108
|
-
|
|
1109
|
-
const recent = Array.isArray(r.recent) ? r.recent.filter((line) => typeof line === "string") : [];
|
|
1110
|
-
// A full file: the refusal carries the newest entries so the model
|
|
1111
|
-
// can merge them in this same turn without a read round trip.
|
|
1112
|
-
const tail = r.code === "over-budget" && recent.length ? `\n\nMost recent entries, oldest first:\n${recent.join("\n")}` : "";
|
|
1113
|
-
return { text: `${String(r.error ?? "Memory update was not confirmed.")}${tail}`, isError: true };
|
|
1114
|
-
}
|
|
1122
|
+
if (r.error || r.ok !== true)
|
|
1123
|
+
return { text: String(r.error ?? "Memory update was not confirmed."), isError: true };
|
|
1115
1124
|
const entry = typeof r.entry === "string" && r.entry ? ` Entry: ${r.entry}` : "";
|
|
1116
|
-
|
|
1125
|
+
const moved = Array.isArray(r.moved) ? r.moved.filter((line) => typeof line === "string") : [];
|
|
1126
|
+
// A write never fails for size: the bot hears where older notes went,
|
|
1127
|
+
// and the one case it cannot fix itself, in one plain sentence.
|
|
1128
|
+
const full = r.truncated
|
|
1129
|
+
? "\nSaved, but the lines that never move out of MEMORY.md (hand-written ones, health and safety facts) fill what loads each session, so the newest entries do not load. Ask the person to trim MEMORY.md in Settings."
|
|
1130
|
+
: "";
|
|
1131
|
+
// Named by their first line, a few at most: a file grown far past the
|
|
1132
|
+
// budget by hand can move hundreds in one write.
|
|
1133
|
+
const shown = moved.slice(0, MOVED_ENTRIES_SHOWN).map((text) => text.split("\n")[0]);
|
|
1134
|
+
const more = moved.length > shown.length ? `\n…and ${moved.length - shown.length} more` : "";
|
|
1135
|
+
const movedNote = moved.length
|
|
1136
|
+
? `\nTo stay within what loads each session, moved ${moved.length === 1 ? "1 older entry" : `${moved.length} older entries`} to memory/archive.md (session_search finds them):\n${shown.join("\n")}${more}`
|
|
1137
|
+
: "";
|
|
1138
|
+
return { text: `Memory updated.${entry}${full}${movedNote}` };
|
|
1117
1139
|
}
|
|
1118
1140
|
if (name === "retry_thread") {
|
|
1119
1141
|
const botId = String(args.bot_id ?? "").trim();
|
|
@@ -1141,6 +1163,25 @@ export async function callTool(name, args, context) {
|
|
|
1141
1163
|
return { text: String(r.error ?? "The log line was not confirmed."), isError: true };
|
|
1142
1164
|
return { text: `Logged to ${String(r.file)}: ${String(r.line)}` };
|
|
1143
1165
|
}
|
|
1166
|
+
if (name === "propose_team_memory") {
|
|
1167
|
+
const kind = String(args.kind ?? "").trim();
|
|
1168
|
+
const entryName = String(args.name ?? "").trim();
|
|
1169
|
+
const detail = String(args.detail ?? "").trim();
|
|
1170
|
+
if (!kind || !entryName || !detail)
|
|
1171
|
+
return { text: "propose_team_memory needs kind, name, and detail.", isError: true };
|
|
1172
|
+
const r = await api("/api/internal/team-memory", {
|
|
1173
|
+
method: "POST",
|
|
1174
|
+
body: JSON.stringify({
|
|
1175
|
+
fromBotId: BOT_ID,
|
|
1176
|
+
fromThreadId: THREAD_ID,
|
|
1177
|
+
kind,
|
|
1178
|
+
name: entryName,
|
|
1179
|
+
detail,
|
|
1180
|
+
aliases: Array.isArray(args.aliases) ? args.aliases.filter((alias) => typeof alias === "string") : undefined,
|
|
1181
|
+
}),
|
|
1182
|
+
});
|
|
1183
|
+
return confirmationResult(r, `remembering ${entryName} for the team`, "entry");
|
|
1184
|
+
}
|
|
1144
1185
|
if (name === "session_search") {
|
|
1145
1186
|
const q = String(args.query ?? "").trim();
|
|
1146
1187
|
const since = typeof args.since === "string" ? args.since.trim() : "";
|