openmausbot 0.1.85 → 0.1.86
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/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/abnfDiagram-O67JEVCF-DpUZNnFD.js +1 -0
- package/dist/assets/arc-DW7mrI4p.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
- package/dist/assets/channel-BXFkgus9.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
- package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
- package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
- package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
- package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BGgqlWPJ.css +1 -0
- package/dist/assets/index-BtHo8ZKl.js +577 -0
- package/dist/assets/{index-CTNNoqSJ.js → index-h9FFNwTu.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
- package/dist/assets/linear-3su2TG93.js +1 -0
- package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +19 -10
- package/dist-server/drivers/agents-proxy.js +16159 -1596
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +5 -5
- package/dist-server/index.js +4779 -1667
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +4679 -4266
- package/dist-server/pair-cli.js +4679 -4266
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box.js +2 -1
- package/dist-server/server/browser-runtime.js +1 -1
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/cli-prompts.js +24 -2
- package/dist-server/server/cli.js +79 -10
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/config.js +79 -13
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +20 -3
- package/dist-server/server/delegations.js +11 -9
- package/dist-server/server/drivers/acp/core.js +110 -22
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1123 -0
- package/dist-server/server/drivers/agents-catalog.js +659 -0
- package/dist-server/server/drivers/agents-client.js +28 -0
- package/dist-server/server/drivers/agents-proxy.js +18 -1722
- package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
- package/dist-server/server/drivers/claude.js +21 -6
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex.js +26 -4
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/openai-compat.js +15 -8
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -0
- package/dist-server/server/hosted-models.js +151 -0
- package/dist-server/server/hosted-slack.js +11 -0
- package/dist-server/server/index.js +1718 -348
- package/dist-server/server/managed-desktop.js +79 -14
- package/dist-server/server/managed-policy.js +153 -0
- package/dist-server/server/message-db.js +28 -0
- package/dist-server/server/model-context-window.js +7 -0
- package/dist-server/server/model-prices.js +121 -0
- package/dist-server/server/notify.js +10 -1
- package/dist-server/server/peer-roster.js +13 -2
- package/dist-server/server/recent-work.js +15 -0
- package/dist-server/server/request-auth.js +96 -0
- package/dist-server/server/room-handoffs.js +25 -11
- package/dist-server/server/routes/hosted-slack.js +20 -0
- package/dist-server/server/routes/table.js +14 -0
- package/dist-server/server/routines.js +22 -0
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +6 -1
- package/dist-server/server/store.js +292 -21
- package/dist-server/server/team-backup.js +10 -1
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/usage-ledger.js +31 -8
- package/dist-server/server/workspace-backup-policy.js +9 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +19 -10
- package/enterprise/README.md +33 -12
- package/enterprise/server/index.js +5 -5
- package/package.json +1 -1
- package/dist/assets/index-Ba9G44HI.js +0 -310
- package/dist/assets/index-CkRNp7wX.css +0 -1
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { Ajv } from "ajv";
|
|
4
4
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
5
5
|
import formats from "ajv-formats";
|
|
6
|
+
import { stripControlPlaneEnv } from "../config.js";
|
|
7
|
+
import { augmentedPath } from "../env-path.js";
|
|
6
8
|
import { killCliTree, spawnCli } from "../procs.js";
|
|
7
9
|
/** The transport cannot safely continue this turn. A dispatched operation may
|
|
8
10
|
* already have taken effect, so callers must not retry it through a new round. */
|
|
@@ -21,6 +23,14 @@ function object(value) {
|
|
|
21
23
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
24
|
}
|
|
23
25
|
function aborted() { return new Error("MCP operation cancelled"); }
|
|
26
|
+
/** These servers are a chat-runtime bot's tools, the counterpart of an engine
|
|
27
|
+
* CLI's children: the operator's control-plane secrets never ride along. What
|
|
28
|
+
* the server entry itself names is a deliberate grant and is applied last. */
|
|
29
|
+
export function chatMcpEnvironment(serverEnv, source = process.env) {
|
|
30
|
+
const env = { ...source, PATH: augmentedPath() };
|
|
31
|
+
stripControlPlaneEnv(env);
|
|
32
|
+
return { ...env, ...serverEnv };
|
|
33
|
+
}
|
|
24
34
|
class ChatMcpClient {
|
|
25
35
|
child;
|
|
26
36
|
buffer = "";
|
|
@@ -31,8 +41,11 @@ class ChatMcpClient {
|
|
|
31
41
|
pending = new Map();
|
|
32
42
|
constructor(server) {
|
|
33
43
|
try {
|
|
44
|
+
// The desktop shell inherits Finder's bare PATH, where `npx`-style
|
|
45
|
+
// servers cannot find `node` and exit at once. Widen it the way the
|
|
46
|
+
// Claude and Codex drivers do; a PATH the user set on the server wins.
|
|
34
47
|
this.child = spawnCli(server.command, server.args, {
|
|
35
|
-
stdio: ["pipe", "pipe", "pipe"], env:
|
|
48
|
+
stdio: ["pipe", "pipe", "pipe"], env: chatMcpEnvironment(server.env),
|
|
36
49
|
});
|
|
37
50
|
}
|
|
38
51
|
catch {
|
|
@@ -64,7 +77,9 @@ class ChatMcpClient {
|
|
|
64
77
|
entry.reject(new Error("MCP session closed"));
|
|
65
78
|
this.pending.clear();
|
|
66
79
|
this.buffer = "";
|
|
67
|
-
|
|
80
|
+
// Confirm within the codebase-default grace: Windows reaps the tree via
|
|
81
|
+
// taskkill /T, which can exceed shorter budgets on a loaded machine.
|
|
82
|
+
this.closing = killCliTree(this.child, 5_000).then((stopped) => {
|
|
68
83
|
if (!stopped)
|
|
69
84
|
throw new Error("MCP server shutdown could not be confirmed; execution outcome may be uncertain");
|
|
70
85
|
});
|
|
@@ -322,6 +322,7 @@ export const STATIC_CLAUDE_MODELS = {
|
|
|
322
322
|
options: [
|
|
323
323
|
{ id: "claude-fable-5-1", label: "Claude Fable 5.1" },
|
|
324
324
|
{ id: "claude-fable-5", label: "Claude Fable 5" },
|
|
325
|
+
{ id: "claude-opus-5-5", label: "Claude Opus 5.5", contextWindow: 1_000_000 },
|
|
325
326
|
{ id: "claude-opus-5", label: "Claude Opus 5" },
|
|
326
327
|
{ id: "claude-sonnet-5", label: "Claude Sonnet 5" },
|
|
327
328
|
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
@@ -724,10 +725,13 @@ function decodeConfig(raw) {
|
|
|
724
725
|
const configDir = typeof o.configDir === "string" ? o.configDir.trim() : undefined;
|
|
725
726
|
if (configDir)
|
|
726
727
|
resolveClaudeConfigDir(configDir);
|
|
728
|
+
if (o.managedModels !== undefined && (o.managed !== true || !Array.isArray(o.managedModels) || !o.managedModels.length || o.managedModels.some(model => typeof model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/.test(model))))
|
|
729
|
+
throw new Error("Invalid hosted Claude models.");
|
|
727
730
|
return {
|
|
728
731
|
cli: typeof o.cli === "string" ? o.cli : "claude",
|
|
729
732
|
...(configDir ? { configDir } : {}),
|
|
730
733
|
...(o.managed === true ? { managed: true } : {}),
|
|
734
|
+
...(o.managedModels ? { managedModels: o.managedModels } : {}),
|
|
731
735
|
permissionMode: mode ?? "acceptEdits",
|
|
732
736
|
...(tools !== undefined ? { tools } : {}),
|
|
733
737
|
...(disallowedTools !== undefined ? { disallowedTools } : {}),
|
|
@@ -821,7 +825,7 @@ export const ClaudeDriver = {
|
|
|
821
825
|
if (inheritsUserConfig(catalogEnv)) {
|
|
822
826
|
console.error(`claude (${instanceId}): OMB_CLAUDE_INHERIT_USER_CONFIG=1 — bots inherit this machine's Claude Code MCP servers, skills, hooks and CLAUDE.md on every turn; remove it unless a bot needs a user-scope server`);
|
|
823
827
|
}
|
|
824
|
-
let models = STATIC_CLAUDE_MODELS;
|
|
828
|
+
let models = config.managedModels ? { default: config.managedModels[0], options: config.managedModels.map(id => ({ id, label: id })) } : STATIC_CLAUDE_MODELS;
|
|
825
829
|
const refreshModels = async () => {
|
|
826
830
|
if (config.managed)
|
|
827
831
|
return;
|
|
@@ -932,6 +936,8 @@ export const ClaudeDriver = {
|
|
|
932
936
|
// is a fresh sendTurn, and the attempt cap must survive across launches
|
|
933
937
|
const retryState = new Map();
|
|
934
938
|
const sendTurn = async (turn, logicalTurnId) => {
|
|
939
|
+
if (config.managedModels && (!turn.model || !config.managedModels.includes(turn.model)))
|
|
940
|
+
throw new Error("This model is not assigned to this workspace.");
|
|
935
941
|
if (config.managed && (!turn.model || turn.model.includes("::") || !config.configDir ||
|
|
936
942
|
!input.environment.ANTHROPIC_API_KEY || !input.environment.ANTHROPIC_BASE_URL)) {
|
|
937
943
|
throw new Error("Company model access is unavailable. Reconnect your organization; personal billing will not be used.");
|
|
@@ -1224,6 +1230,8 @@ export const ClaudeDriver = {
|
|
|
1224
1230
|
clearTimeout(live.idleTimer);
|
|
1225
1231
|
live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false };
|
|
1226
1232
|
active.set(threadId, { stop: () => {
|
|
1233
|
+
if (live.turn)
|
|
1234
|
+
live.turn.stopRequested = true;
|
|
1227
1235
|
closeSession(threadId, "interrupted");
|
|
1228
1236
|
retry.cancelled = true;
|
|
1229
1237
|
retryAbort.abort();
|
|
@@ -1235,8 +1243,9 @@ export const ClaudeDriver = {
|
|
|
1235
1243
|
? promptMsg
|
|
1236
1244
|
: claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
|
|
1237
1245
|
live.volatile = volatile;
|
|
1246
|
+
const running = live.turn;
|
|
1238
1247
|
const written = await writeUser(live, threadId, message);
|
|
1239
|
-
if (!written) {
|
|
1248
|
+
if (!written && !running?.stopRequested) {
|
|
1240
1249
|
active.delete(threadId);
|
|
1241
1250
|
live.turn = null;
|
|
1242
1251
|
closeSession(threadId, "stdin write failed");
|
|
@@ -1610,7 +1619,10 @@ export const ClaudeDriver = {
|
|
|
1610
1619
|
// a turn still running when the process died is a failed turn; a
|
|
1611
1620
|
// process that exited between turns (idle close, contract change)
|
|
1612
1621
|
// is just a session ending
|
|
1613
|
-
if (session.turn && !session.turn.settled) {
|
|
1622
|
+
if (session.turn?.stopRequested && !session.turn.settled) {
|
|
1623
|
+
settle(false, "interrupted");
|
|
1624
|
+
}
|
|
1625
|
+
else if (session.turn && !session.turn.settled) {
|
|
1614
1626
|
// A retained process may be running a later user turn. Its close
|
|
1615
1627
|
// handler must retry that request, not the process's first prompt.
|
|
1616
1628
|
const { turnId, input: turn, retryAbort } = session.turn;
|
|
@@ -1794,6 +1806,8 @@ export const ClaudeDriver = {
|
|
|
1794
1806
|
void session.finishClose();
|
|
1795
1807
|
});
|
|
1796
1808
|
const stop = () => {
|
|
1809
|
+
if (session.turn)
|
|
1810
|
+
session.turn.stopRequested = true;
|
|
1797
1811
|
// taskkill is asynchronous on Windows. Retire steering and approvals
|
|
1798
1812
|
// now, before a still-connected child can submit more work.
|
|
1799
1813
|
closeSession(threadId, "interrupted");
|
|
@@ -1807,7 +1821,8 @@ export const ClaudeDriver = {
|
|
|
1807
1821
|
// stdin stays OPEN: that is what keeps the session alive for a
|
|
1808
1822
|
// mid-turn steer or the next turn; closeSession() ends it.
|
|
1809
1823
|
if (!(await writeUser(session, threadId, promptMsg))) {
|
|
1810
|
-
|
|
1824
|
+
if (!session.turn?.stopRequested)
|
|
1825
|
+
settle(false, "stdin_write_failed");
|
|
1811
1826
|
closeSession(threadId, "stdin write failed");
|
|
1812
1827
|
}
|
|
1813
1828
|
return { turnId };
|
|
@@ -1844,9 +1859,9 @@ export const ClaudeDriver = {
|
|
|
1844
1859
|
* `claude -p "prompt"` shape is not safe for review. No tools or MCP
|
|
1845
1860
|
* servers are mounted in this isolated process. */
|
|
1846
1861
|
const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
|
|
1847
|
-
const child = spawnCli(config.cli, ["-p", "--model", "claude-haiku-4-5", "--output-format", "text"], {
|
|
1862
|
+
const child = spawnCli(config.cli, ["-p", "--model", config.managedModels?.[0] ?? "claude-haiku-4-5", "--output-format", "text"], {
|
|
1848
1863
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1849
|
-
env: environment("claude-haiku-4-5"),
|
|
1864
|
+
env: environment(config.managedModels?.[0] ?? "claude-haiku-4-5"),
|
|
1850
1865
|
});
|
|
1851
1866
|
let stdout = "";
|
|
1852
1867
|
let stderr = "";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// MCP server names the user already declared in their own Codex config.
|
|
2
|
+
//
|
|
3
|
+
// Custom MCP servers reach codex as `-c mcp_servers.<name>.…` overrides, and
|
|
4
|
+
// codex merges an override into any same-named table in config.toml. A stdio
|
|
5
|
+
// command laid over a remote `url` entry is "invalid configuration": codex
|
|
6
|
+
// falls back to defaults, config/read fails, and the whole turn dies before
|
|
7
|
+
// the model is even asked. Mounting a colliding server under a name of its
|
|
8
|
+
// own keeps both definitions usable.
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
const HEADER = /^[ \t]*\[[ \t]*mcp_servers[ \t]*\.[ \t]*(?:"((?:[^"\\]|\\.)*)"|'([^']*)'|([A-Za-z0-9_-]+))[ \t]*[.\]]/gm;
|
|
13
|
+
/** Names declared as `[mcp_servers.<name>]` (or a sub-table of one). Inline
|
|
14
|
+
* tables are not scanned; a collision there still surfaces as codex's own
|
|
15
|
+
* configuration error. */
|
|
16
|
+
export function mcpServerNamesInToml(toml) {
|
|
17
|
+
const names = new Set();
|
|
18
|
+
for (const match of toml.matchAll(HEADER)) {
|
|
19
|
+
const name = match[1] !== undefined ? match[1].replace(/\\(.)/g, "$1") : match[2] ?? match[3];
|
|
20
|
+
if (name)
|
|
21
|
+
names.add(name);
|
|
22
|
+
}
|
|
23
|
+
return names;
|
|
24
|
+
}
|
|
25
|
+
/** Server names in the config.toml of the Codex home this child will use. */
|
|
26
|
+
export function codexConfigMcpServerNames(env) {
|
|
27
|
+
const codexHome = env.CODEX_HOME || join(env.HOME || env.USERPROFILE || homedir(), ".codex");
|
|
28
|
+
try {
|
|
29
|
+
return mcpServerNamesInToml(readFileSync(join(codexHome, "config.toml"), "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return new Set();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** The name to mount a custom server under: its own, unless the user's Codex
|
|
36
|
+
* config already has a server by that name. */
|
|
37
|
+
export function mountedMcpServerName(name, taken) {
|
|
38
|
+
if (!taken.has(name))
|
|
39
|
+
return name;
|
|
40
|
+
let candidate = `${name}_openmausbot`;
|
|
41
|
+
for (let i = 2; taken.has(candidate); i++)
|
|
42
|
+
candidate = `${name}_openmausbot${i}`;
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// and preserves that history or reports a failed resume.
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { codexConfigMcpServerNames, mountedMcpServerName } from "./codex-mcp-names.js";
|
|
14
15
|
import { stripWorkspaceCredentialEnv } from "../config.js";
|
|
15
16
|
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
16
17
|
import { isHarnessOwnedMcpEnvName } from "../mcp-registry.js";
|
|
@@ -128,6 +129,14 @@ export function managedCodexArgs(config) {
|
|
|
128
129
|
const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
|
|
129
130
|
const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
130
131
|
const skippedSseServers = new Set();
|
|
132
|
+
const renamedMcpServers = new Set();
|
|
133
|
+
/** Logged once per name: the rename is deliberate, not a lost server. */
|
|
134
|
+
function noteRenamedMcpServer(name, mountName) {
|
|
135
|
+
if (renamedMcpServers.has(name))
|
|
136
|
+
return;
|
|
137
|
+
renamedMcpServers.add(name);
|
|
138
|
+
console.error(`codex: MCP server ${JSON.stringify(name)} is also declared in Codex's own config.toml — mounted as ${JSON.stringify(mountName)} for this bot so the two do not merge`);
|
|
139
|
+
}
|
|
131
140
|
function noteSkippedSseServer(name) {
|
|
132
141
|
if (skippedSseServers.has(name))
|
|
133
142
|
return;
|
|
@@ -593,6 +602,11 @@ export const CodexDriver = {
|
|
|
593
602
|
if (turn.integrations?.browser) {
|
|
594
603
|
mountMcpServer(appServerArgs, env, "browser", turn.integrations.browser);
|
|
595
604
|
}
|
|
605
|
+
// A custom server named like one in the user's own config.toml would
|
|
606
|
+
// be merged with it by the `-c` override — a stdio command over a
|
|
607
|
+
// remote url is "invalid configuration" and kills the turn before the
|
|
608
|
+
// model is asked. Such a server mounts under a name of its own.
|
|
609
|
+
const declaredInCodexConfig = codexConfigMcpServerNames(env);
|
|
596
610
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
597
611
|
// codex speaks streamable HTTP to a remote server, not the older
|
|
598
612
|
// SSE transport: such an entry still reaches Claude bots, and is
|
|
@@ -601,7 +615,10 @@ export const CodexDriver = {
|
|
|
601
615
|
noteSkippedSseServer(name);
|
|
602
616
|
continue;
|
|
603
617
|
}
|
|
604
|
-
|
|
618
|
+
const mountName = mountedMcpServerName(name, declaredInCodexConfig);
|
|
619
|
+
if (mountName !== name)
|
|
620
|
+
noteRenamedMcpServer(name, mountName);
|
|
621
|
+
mountMcpServer(appServerArgs, env, mountName, server, false);
|
|
605
622
|
}
|
|
606
623
|
if (turn.integrations?.phone) {
|
|
607
624
|
const bridge = turn.integrations.phone;
|
|
@@ -1288,10 +1305,15 @@ export const CodexDriver = {
|
|
|
1288
1305
|
});
|
|
1289
1306
|
effectiveConfig = configured?.config;
|
|
1290
1307
|
}
|
|
1291
|
-
catch {
|
|
1308
|
+
catch (error) {
|
|
1292
1309
|
// Do not expose a possibly secret-bearing native config error or
|
|
1293
|
-
// overwrite unknown instructions with an empty fallback.
|
|
1294
|
-
|
|
1310
|
+
// overwrite unknown instructions with an empty fallback. A config
|
|
1311
|
+
// codex itself rejected is the one case worth naming: the person
|
|
1312
|
+
// can act on it, and the message carries no credential.
|
|
1313
|
+
const invalid = error instanceof CodexRpcError && /^invalid configuration\b/i.test(error.message);
|
|
1314
|
+
throw new Error(invalid
|
|
1315
|
+
? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
|
|
1316
|
+
: "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
|
|
1295
1317
|
}
|
|
1296
1318
|
const developerInstructions = codexDeveloperInstructions(effectiveConfig, turn.system ?? "");
|
|
1297
1319
|
let approvalParams;
|
|
@@ -2,8 +2,9 @@ import { createOpenAIChatRuntime } from "./openai-chat.js";
|
|
|
2
2
|
const DRIVER_KIND = "grok";
|
|
3
3
|
const DEFAULT_URL = "https://api.x.ai/v1";
|
|
4
4
|
const MODELS = {
|
|
5
|
-
default: "grok-4",
|
|
5
|
+
default: "grok-4.7",
|
|
6
6
|
options: [
|
|
7
|
+
{ id: "grok-4.7", label: "Grok 4.7", contextWindow: 500_000 },
|
|
7
8
|
{ id: "grok-4", label: "Grok 4" },
|
|
8
9
|
{ id: "grok-4-fast", label: "Grok 4 Fast" },
|
|
9
10
|
{ id: "grok-3-mini", label: "Grok 3 Mini" },
|
|
@@ -28,9 +28,12 @@ function decodeConfig(raw) {
|
|
|
28
28
|
const config = (raw ?? {});
|
|
29
29
|
if (config.tools !== undefined && typeof config.tools !== "boolean")
|
|
30
30
|
throw new Error("tools must be a boolean");
|
|
31
|
+
if (config.managedModels !== undefined && (!Array.isArray(config.managedModels) || !config.managedModels.length || config.managedModels.some(model => typeof model !== "string" || !model.trim())))
|
|
32
|
+
throw new Error("Invalid managed models.");
|
|
31
33
|
const envUrl = process.env.OPENAI_COMPAT_URL;
|
|
32
34
|
return {
|
|
33
35
|
...(config.tools !== undefined ? { tools: config.tools } : {}),
|
|
36
|
+
...(config.managedModels ? { managedModels: config.managedModels } : {}),
|
|
34
37
|
url: (typeof config.url === "string" && config.url ? config.url : envUrl || "https://openrouter.ai/api/v1")
|
|
35
38
|
.replace(/\/+$/, ""),
|
|
36
39
|
apiKeyEnv: typeof config.apiKeyEnv === "string" && config.apiKeyEnv
|
|
@@ -74,15 +77,19 @@ export const OpenAICompatDriver = {
|
|
|
74
77
|
process.env[config.apiKeyEnv] ??
|
|
75
78
|
process.env.OPENAI_COMPAT_API_KEY ??
|
|
76
79
|
"";
|
|
77
|
-
let catalog = config.
|
|
78
|
-
? {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
let catalog = config.managedModels
|
|
81
|
+
? { default: config.managedModels[0], options: config.managedModels.map(id => ({ id, label: id })) }
|
|
82
|
+
: config.model
|
|
83
|
+
? {
|
|
84
|
+
default: config.model,
|
|
85
|
+
options: DEFAULT_MODELS.options.some((model) => model.id === config.model)
|
|
86
|
+
? DEFAULT_MODELS.options
|
|
87
|
+
: [{ id: config.model, label: config.model, custom: true }, ...DEFAULT_MODELS.options],
|
|
88
|
+
}
|
|
89
|
+
: DEFAULT_MODELS;
|
|
85
90
|
const fetchModels = async () => {
|
|
91
|
+
if (config.managedModels)
|
|
92
|
+
return;
|
|
86
93
|
if (!apiKey)
|
|
87
94
|
return;
|
|
88
95
|
try {
|
|
@@ -8,10 +8,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
// Neutral hook point for the source-available enterprise layer.
|
|
10
10
|
//
|
|
11
|
-
// The layer lives in
|
|
11
|
+
// The layer lives in enterprise/ (its own LICENSE) and is loaded only if that
|
|
12
12
|
// folder is present: delete it and this file still compiles, the server still
|
|
13
13
|
// starts, and /api/edition reports the open-source edition. Core never imports
|
|
14
|
-
// the layer statically
|
|
14
|
+
// the layer statically; a build ships it as its own bundled file beside the
|
|
15
|
+
// server (see enterpriseLayerDirs for where it is looked up).
|
|
15
16
|
// The layer's only obligation is `register()`, which turns OMB_LICENSE_KEY into
|
|
16
17
|
// entitlements; core keeps the resulting status and answers `entitled()`.
|
|
17
18
|
import { existsSync } from "node:fs";
|
|
@@ -25,7 +26,28 @@ const layerSchema = z.object({
|
|
|
25
26
|
features: z.array(z.string().min(1)),
|
|
26
27
|
expiresAt: z.string().nullable(),
|
|
27
28
|
});
|
|
28
|
-
|
|
29
|
+
/** Days before expiry that the startup log, /api/edition and Settings start
|
|
30
|
+
* saying so, and days after it that the features keep working while the
|
|
31
|
+
* operator renews. The key and its verification are unchanged: the layer
|
|
32
|
+
* accepts a key that expired less than the grace period ago. */
|
|
33
|
+
export const LICENSE_WARN_DAYS = 30;
|
|
34
|
+
export const LICENSE_GRACE_DAYS = 7;
|
|
35
|
+
const DAY_MS = 24 * 60 * 60_000;
|
|
36
|
+
/** Where the layer may be, in order. An explicit OMB_ENTERPRISE_DIR is the
|
|
37
|
+
* only place looked at when set. Otherwise: beside the server root (a
|
|
38
|
+
* checkout, where server/ and enterprise/ are siblings, and the npm package,
|
|
39
|
+
* which copies the bundled layer to <package>/enterprise), then inside it
|
|
40
|
+
* (the Docker image and the packaged desktop ship dist-server/ alone, and
|
|
41
|
+
* scripts/bundle-server.mjs writes the layer to dist-server/enterprise). */
|
|
42
|
+
export function enterpriseLayerDirs(env = process.env, serverRoot = SERVER_ROOT) {
|
|
43
|
+
if (env.OMB_ENTERPRISE_DIR)
|
|
44
|
+
return [env.OMB_ENTERPRISE_DIR];
|
|
45
|
+
return [join(serverRoot, "..", "enterprise"), join(serverRoot, "enterprise")];
|
|
46
|
+
}
|
|
47
|
+
/** Source in a checkout, a compiled bundle in an image: same convention as proxy-paths.ts. */
|
|
48
|
+
function layerEntry(dir) {
|
|
49
|
+
return [join(dir, "server", "index.ts"), join(dir, "server", "index.js")].find((candidate) => existsSync(candidate));
|
|
50
|
+
}
|
|
29
51
|
let current = { edition: "oss", features: [] };
|
|
30
52
|
let workspaceAccessFactory;
|
|
31
53
|
export function hostedWorkspaceConfigured(env = process.env) {
|
|
@@ -53,6 +75,26 @@ export function hostedWorkspaceConfiguration(env = process.env) {
|
|
|
53
75
|
return null;
|
|
54
76
|
}
|
|
55
77
|
}
|
|
78
|
+
/** Who decides who may use this workspace, for Settings → People and
|
|
79
|
+
* Remote access. On a portal-membership workspace the organisation's Admin
|
|
80
|
+
* does, and `peopleUrl` opens its People page for this workspace (identifiers
|
|
81
|
+
* only; Admin authorizes its own visitor). A hosted workspace never issues
|
|
82
|
+
* pairing codes or email sign-in: people come in through the portal. */
|
|
83
|
+
export function workspaceMembership(env = process.env) {
|
|
84
|
+
const hosted = hostedWorkspaceConfiguration(env);
|
|
85
|
+
const pairingCodes = !hostedWorkspaceConfigured(env);
|
|
86
|
+
if (!hosted?.portalMembership)
|
|
87
|
+
return { authority: "local", pairingCodes };
|
|
88
|
+
const people = new URL("/people", hosted.admin);
|
|
89
|
+
people.search = new URLSearchParams({ workspace: hosted.workspace }).toString();
|
|
90
|
+
return { authority: "portal", pairingCodes, peopleUrl: people.href };
|
|
91
|
+
}
|
|
92
|
+
/** An operator may authorize new Full tasks only on a dedicated,
|
|
93
|
+
* portal-managed server. This is not an HTTP setting or a desktop grant. */
|
|
94
|
+
export function sharedWorkspaceFullAccessConfigured(env = process.env) {
|
|
95
|
+
return env.OMB_SHARED_WORKSPACE_FULL_ACCESS === "1" && env.OMB_DESKTOP_PARENT !== "1" &&
|
|
96
|
+
hostedWorkspaceConfiguration(env)?.portalMembership === true;
|
|
97
|
+
}
|
|
56
98
|
/** No enterprise import or portal dependency enters the core bundle. A
|
|
57
99
|
* configured server without this hook must refuse hosted access, not fall
|
|
58
100
|
* back to legacy email or QR credentials. */
|
|
@@ -69,12 +111,11 @@ function oss(notice) {
|
|
|
69
111
|
* degrades to the open-source edition with a notice that says what to fix. */
|
|
70
112
|
export async function loadEnterpriseLayer(options = {}) {
|
|
71
113
|
workspaceAccessFactory = undefined;
|
|
72
|
-
const
|
|
114
|
+
const dirs = options.dir ? [options.dir] : enterpriseLayerDirs(options.env, options.serverRoot);
|
|
73
115
|
const licenseKey = options.licenseKey ?? process.env.OMB_LICENSE_KEY;
|
|
74
|
-
|
|
75
|
-
const entry = [join(dir, "server", "index.ts"), join(dir, "server", "index.js")].find((candidate) => existsSync(candidate));
|
|
116
|
+
const entry = dirs.map(layerEntry).find((candidate) => candidate !== undefined);
|
|
76
117
|
if (!entry) {
|
|
77
|
-
return oss(licenseKey ? `OMB_LICENSE_KEY is set but no enterprise layer exists at ${
|
|
118
|
+
return oss(licenseKey ? `OMB_LICENSE_KEY is set but no enterprise layer exists at ${dirs.join(" or ")}` : undefined);
|
|
78
119
|
}
|
|
79
120
|
try {
|
|
80
121
|
const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(entry).href));
|
|
@@ -86,7 +127,9 @@ export async function loadEnterpriseLayer(options = {}) {
|
|
|
86
127
|
throw new Error(`${entry} does not export register()`);
|
|
87
128
|
if (!licenseKey)
|
|
88
129
|
return oss("enterprise layer present but OMB_LICENSE_KEY is not set");
|
|
89
|
-
|
|
130
|
+
// graceDays is additive: a layer that ignores it refuses an expired key
|
|
131
|
+
// at startup exactly as before.
|
|
132
|
+
const registered = await register({ licenseKey, graceDays: LICENSE_GRACE_DAYS });
|
|
90
133
|
const layer = layerSchema.parse(registered);
|
|
91
134
|
current = {
|
|
92
135
|
edition: "enterprise",
|
|
@@ -100,27 +143,65 @@ export async function loadEnterpriseLayer(options = {}) {
|
|
|
100
143
|
return oss(`enterprise layer disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
144
|
}
|
|
102
145
|
}
|
|
146
|
+
/** When the key stops being valid, or null for perpetual / open-source. */
|
|
147
|
+
function expiryAt() {
|
|
148
|
+
if (current.edition !== "enterprise" || !current.expiresAt)
|
|
149
|
+
return null;
|
|
150
|
+
const at = new Date(current.expiresAt).getTime();
|
|
151
|
+
return Number.isFinite(at) ? at : null;
|
|
152
|
+
}
|
|
103
153
|
/** A license that expires while the server keeps running stops granting
|
|
104
|
-
* features at that moment, not at the next restart. */
|
|
154
|
+
* features once its grace period ends, at that moment, not at the next restart. */
|
|
105
155
|
function expired(now) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return now >= new Date(current.expiresAt).getTime();
|
|
156
|
+
const at = expiryAt();
|
|
157
|
+
return at !== null && now >= at + LICENSE_GRACE_DAYS * DAY_MS;
|
|
109
158
|
}
|
|
110
159
|
export function editionStatus(now = Date.now()) {
|
|
111
|
-
|
|
160
|
+
const at = expiryAt();
|
|
161
|
+
if (at === null)
|
|
112
162
|
return current;
|
|
163
|
+
if (expired(now)) {
|
|
164
|
+
return {
|
|
165
|
+
edition: "oss",
|
|
166
|
+
features: [],
|
|
167
|
+
notice: `enterprise layer disabled: OMB_LICENSE_KEY expired on ${current.expiresAt}; renew it to keep enterprise features`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const expiresInDays = Math.ceil((at - now) / DAY_MS);
|
|
171
|
+
if (now < at)
|
|
172
|
+
return { ...current, expiresInDays };
|
|
173
|
+
const graceEndsAt = new Date(at + LICENSE_GRACE_DAYS * DAY_MS).toISOString().slice(0, 10);
|
|
113
174
|
return {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
175
|
+
...current,
|
|
176
|
+
expiresInDays,
|
|
177
|
+
graceEndsAt,
|
|
178
|
+
notice: `OMB_LICENSE_KEY expired on ${current.expiresAt}; enterprise features keep working until ${graceEndsAt} while it is renewed`,
|
|
117
179
|
};
|
|
118
180
|
}
|
|
181
|
+
/** The edition as a non-admin session sees it: what is entitled, without
|
|
182
|
+
* the license's countdown, grace date or operator notice. */
|
|
183
|
+
export function editionForMembers(status) {
|
|
184
|
+
const { expiresInDays: _days, graceEndsAt: _grace, notice: _notice, ...shared } = status;
|
|
185
|
+
return shared;
|
|
186
|
+
}
|
|
119
187
|
/** Feature gates in core ask this and nothing else. Unknown ids are simply not
|
|
120
188
|
* granted, and the open-source edition always carries an empty feature list. */
|
|
121
189
|
export function entitled(feature, now = Date.now()) {
|
|
122
190
|
return !expired(now) && current.features.includes(feature);
|
|
123
191
|
}
|
|
192
|
+
/** The startup warning for a key that is about to lapse or already in its
|
|
193
|
+
* grace period; null when there is nothing to say. */
|
|
194
|
+
export function licenseWarning(status) {
|
|
195
|
+
if (status.edition !== "enterprise" || !status.expiresAt || status.expiresInDays === undefined)
|
|
196
|
+
return null;
|
|
197
|
+
if (status.graceEndsAt) {
|
|
198
|
+
return `OMB_LICENSE_KEY expired on ${status.expiresAt}; enterprise features keep working until ${status.graceEndsAt} — renew the key before then`;
|
|
199
|
+
}
|
|
200
|
+
if (status.expiresInDays > LICENSE_WARN_DAYS)
|
|
201
|
+
return null;
|
|
202
|
+
const days = status.expiresInDays === 1 ? "1 day" : `${status.expiresInDays} days`;
|
|
203
|
+
return `OMB_LICENSE_KEY expires on ${status.expiresAt} (in ${days}); renew it before then to keep enterprise features`;
|
|
204
|
+
}
|
|
124
205
|
/** One line for the startup log. */
|
|
125
206
|
export function describeEdition(status) {
|
|
126
207
|
if (status.edition === "enterprise") {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
const MAX_FILE_BYTES = 1_048_576;
|
|
4
|
+
const ID = /^[\w-]{1,128}$/;
|
|
5
|
+
const fingerprint = (token) => createHash("sha256").update(token).digest("hex");
|
|
6
|
+
const sameFingerprint = (left, right) => {
|
|
7
|
+
const a = Buffer.from(left);
|
|
8
|
+
const b = Buffer.from(right);
|
|
9
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
10
|
+
};
|
|
11
|
+
/** Read a bounded regular file through the descriptor whose permissions we
|
|
12
|
+
* checked. A pipe must not block the request loop, nor a pathname swap replace
|
|
13
|
+
* the checked file before the read. */
|
|
14
|
+
function registrations(file) {
|
|
15
|
+
let fd;
|
|
16
|
+
try {
|
|
17
|
+
fd = openSync(file, constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK));
|
|
18
|
+
const stat = fstatSync(fd);
|
|
19
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES)
|
|
20
|
+
return [];
|
|
21
|
+
if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) {
|
|
22
|
+
console.warn("[comms] ignoring external runtime credentials: use a private file (chmod 600)");
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const bytes = Buffer.alloc(stat.size + 1);
|
|
26
|
+
let count = 0;
|
|
27
|
+
for (;;) {
|
|
28
|
+
const read = readSync(fd, bytes, count, bytes.length - count, null);
|
|
29
|
+
if (!read)
|
|
30
|
+
break;
|
|
31
|
+
count += read;
|
|
32
|
+
if (count > stat.size)
|
|
33
|
+
return []; // changed while being read
|
|
34
|
+
}
|
|
35
|
+
const parsed = JSON.parse(bytes.subarray(0, count).toString("utf8"));
|
|
36
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
37
|
+
return [];
|
|
38
|
+
return Object.entries(parsed).flatMap(([botId, value]) => {
|
|
39
|
+
if (!ID.test(botId) || !value || typeof value !== "object" || Array.isArray(value))
|
|
40
|
+
return [];
|
|
41
|
+
const { token, threadId } = value;
|
|
42
|
+
if (typeof token !== "string" || token.length < 32 || token.length > 4096 || /\s/.test(token))
|
|
43
|
+
return [];
|
|
44
|
+
if (typeof threadId !== "string" || !ID.test(threadId))
|
|
45
|
+
return [];
|
|
46
|
+
return [{ botId, tokenHash: fingerprint(token), threadId }];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (fd !== undefined)
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function resolveRegistration(file, tokenHash, botForId) {
|
|
58
|
+
const matches = registrations(file).filter(entry => sameFingerprint(entry.tokenHash, tokenHash));
|
|
59
|
+
// A copied secret must not select a different bot based on JSON key order.
|
|
60
|
+
if (matches.length !== 1)
|
|
61
|
+
return null;
|
|
62
|
+
const registration = matches[0];
|
|
63
|
+
const bot = botForId(registration.botId);
|
|
64
|
+
if (!bot || bot.hidden)
|
|
65
|
+
return null;
|
|
66
|
+
// There is no durable "main thread": bot.threadId is the UI selection and
|
|
67
|
+
// new tasks are prepended. Every grant needs an explicit binding.
|
|
68
|
+
const task = bot.tasks?.find(candidate => candidate.threadId === registration.threadId);
|
|
69
|
+
if (!task || task.archivedAt !== undefined)
|
|
70
|
+
return null;
|
|
71
|
+
return { botId: bot.id, threadId: task.threadId, tokenHash };
|
|
72
|
+
}
|
|
73
|
+
export function authorizeExternalRuntime(file, header, botForId) {
|
|
74
|
+
if (typeof header !== "string" || !/^Bearer \S{32,4096}$/.test(header))
|
|
75
|
+
return null;
|
|
76
|
+
return resolveRegistration(file, fingerprint(header.slice(7)), botForId);
|
|
77
|
+
}
|
|
78
|
+
/** Re-check after every body/approval/poll wait. The captured identity is not
|
|
79
|
+
* a lease: token rotation, rebinding, deletion and archive take effect now. */
|
|
80
|
+
export function externalRuntimeIsActive(file, grant, botForId) {
|
|
81
|
+
const current = resolveRegistration(file, grant.tokenHash, botForId);
|
|
82
|
+
return current?.botId === grant.botId && current.threadId === grant.threadId;
|
|
83
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function requestConflict(message = "The request no longer owns this conversation") {
|
|
2
|
+
return Object.assign(new Error(message), { status: 409, code: "guarded_request_changed" });
|
|
3
|
+
}
|
|
4
|
+
export function assertRequestTarget(current, expected) {
|
|
5
|
+
if (current.messageId !== expected.messageId || current.activeLeafId !== expected.expectedActiveLeafId ||
|
|
6
|
+
current.activeTurnId !== expected.expectedTurnId || current.executionId !== expected.expectedExecutionId)
|
|
7
|
+
throw requestConflict();
|
|
8
|
+
}
|
|
9
|
+
/** Never infer ownership from timestamps or the latest assistant text. */
|
|
10
|
+
export function guardedRequestPath(messages, activeLeafId, sendId) {
|
|
11
|
+
const sources = messages.filter(message => message.sendId === sendId);
|
|
12
|
+
const source = sources[0];
|
|
13
|
+
if (sources.length !== 1 || !source || source.role !== "user" || source.kind !== "text" || source.steered || source.peerAsk) {
|
|
14
|
+
throw requestConflict("No unique acknowledged user request matches this sendId");
|
|
15
|
+
}
|
|
16
|
+
const byId = new Map(messages.map(message => [message.id, message]));
|
|
17
|
+
const path = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
let id = activeLeafId;
|
|
20
|
+
while (id) {
|
|
21
|
+
if (seen.has(id))
|
|
22
|
+
throw requestConflict();
|
|
23
|
+
seen.add(id);
|
|
24
|
+
const message = byId.get(id);
|
|
25
|
+
if (!message)
|
|
26
|
+
throw requestConflict();
|
|
27
|
+
path.unshift(message);
|
|
28
|
+
if (message.id === source.id)
|
|
29
|
+
break;
|
|
30
|
+
if (path.length >= 500)
|
|
31
|
+
throw Object.assign(new Error("Open this request in the workspace; its transcript exceeds the response limit"), { status: 413 });
|
|
32
|
+
id = message.parentId ?? null;
|
|
33
|
+
}
|
|
34
|
+
if (path[0]?.id !== source.id || path.slice(1).some(message => message.role === "user"))
|
|
35
|
+
throw requestConflict();
|
|
36
|
+
// An abandoned sibling can hold a detached result or approval. A caller
|
|
37
|
+
// must never receive it as the answer to the current branch.
|
|
38
|
+
const ancestors = new Set(path.map(message => message.id));
|
|
39
|
+
if (messages.some(message => message.parentId && ancestors.has(message.parentId) && !seen.has(message.id)))
|
|
40
|
+
throw requestConflict();
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
/** Used only for an explicit control-plane card, never the latest user. */
|
|
44
|
+
export function requestSourceForCard(messages, messageId) {
|
|
45
|
+
const byId = new Map(messages.map(message => [message.id, message]));
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
let message = byId.get(messageId);
|
|
48
|
+
while (message && !seen.has(message.id)) {
|
|
49
|
+
seen.add(message.id);
|
|
50
|
+
if (message.role === "user")
|
|
51
|
+
return message.peerAsk || message.steered || message.requestCancelled ? undefined : message.id;
|
|
52
|
+
message = message.parentId ? byId.get(message.parentId) : undefined;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
export function requestNeedsInput(message) {
|
|
57
|
+
return Boolean((message.card?.requestId && !message.card.answered && !message.card.dismissed) ||
|
|
58
|
+
(message.connector && !message.connector.dismissed && !message.connector.resumed && message.connector.status !== "connected") ||
|
|
59
|
+
(message.secret && !message.secret.provided && !message.secret.dismissed));
|
|
60
|
+
}
|