openmausbot 0.1.80 → 0.1.81
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/README.md +6 -5
- package/dist/assets/index-CKysBq-V.css +1 -0
- package/dist/assets/{index-D6RZKWks.js → index-oLMljotU.js} +1 -1
- package/dist/assets/index-peLFiSH8.js +307 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +41 -10
- package/dist-server/drivers/agents-proxy.js +50 -11
- package/dist-server/index.js +3433 -1278
- package/dist-server/openmausbot.js +389 -83
- package/dist-server/pair-cli.js +389 -83
- package/dist-server/server/box-delete-journal.js +471 -0
- package/dist-server/server/box.js +345 -75
- package/dist-server/server/cli-phone-setup.js +2 -2
- package/dist-server/server/cli.js +84 -12
- package/dist-server/server/config.js +48 -18
- package/dist-server/server/container-computer.js +7 -0
- package/dist-server/server/contracts.js +5 -0
- package/dist-server/server/default-model-selection.js +1 -0
- package/dist-server/server/drivers/acp/core.js +112 -18
- package/dist-server/server/drivers/acp/opencode-go.js +6 -0
- package/dist-server/server/drivers/agents-proxy.js +49 -11
- package/dist-server/server/drivers/chat-mcp-tools.js +6 -1
- package/dist-server/server/drivers/claude.js +33 -2
- package/dist-server/server/drivers/codex.js +93 -8
- package/dist-server/server/drivers/native.js +6 -1
- package/dist-server/server/harness/bus.js +4 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/index.js +807 -213
- package/dist-server/server/local-vm-lease.js +4 -0
- package/dist-server/server/mcp-http.js +337 -0
- package/dist-server/server/mcp-probe.js +67 -18
- package/dist-server/server/mcp-registry.js +153 -58
- package/dist-server/server/member-turn.js +13 -0
- package/dist-server/server/message-db.js +74 -8
- package/dist-server/server/recall-disclosure.js +4 -0
- package/dist-server/server/recent-work.js +142 -0
- package/dist-server/server/request-auth.js +3 -1
- package/dist-server/server/routines.js +17 -0
- package/dist-server/server/screen-frame-gate.js +4 -94
- package/dist-server/server/sessions.js +22 -3
- package/dist-server/server/store.js +16 -1
- package/dist-server/server/surface.js +41 -19
- package/dist-server/server/team-setup-requests.js +1 -1
- package/dist-server/server/thread-log-rotation.js +104 -0
- package/dist-server/server/tts/fish.js +155 -0
- package/dist-server/server/tts/index.js +25 -3
- package/dist-server/server/vps-computer.js +26 -4
- package/dist-server/server/vps-ssh.js +88 -0
- package/dist-server/server/workspace-backup.js +11 -0
- package/dist-server/server/workspace.js +2 -1
- package/dist-server/shared/credential-request.js +10 -0
- package/dist-server/shared/tool-surface.js +62 -0
- package/dist-server/vps-container-mcp.js +41 -10
- package/package.json +1 -1
- package/dist/assets/index-Cu8BkIfo.js +0 -305
- package/dist/assets/index-tfpeJAIG.css +0 -1
|
@@ -458,6 +458,14 @@ const TOOLS = [
|
|
|
458
458
|
required: ["task_id"],
|
|
459
459
|
},
|
|
460
460
|
},
|
|
461
|
+
{
|
|
462
|
+
name: "select_computer",
|
|
463
|
+
description: "Choose where this conversation does computer work. Call with no arguments to inspect actual available choices and the current place. For a task needing computer interaction, select the requested place, or auto to choose a suitable configured computer without asking the user to use menus. OpenMausBot reuses an existing computer first; with a configured provider it can start or provision one when needed. Do not provision for ordinary chat or just to inspect availability. A pending result means end this turn immediately: OpenMausBot updates the conversation selector and resumes the original request with that computer's real tools. Do not use the old tools after requesting a switch, repeat the task, or claim the action is done. This cannot change permissions, override Off, or switch a teammate/routine/channel.",
|
|
464
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
465
|
+
surface: { type: "string", enum: ["auto", "cloud", "vm", "local", "browser"],
|
|
466
|
+
description: "auto = suitable configured computer, cloud = remote Box/VPS, vm = isolated Local VM, local = user's own desktop, browser = built-in browser. Omit to list." },
|
|
467
|
+
} },
|
|
468
|
+
},
|
|
461
469
|
{
|
|
462
470
|
name: "list_threads",
|
|
463
471
|
description: "See your own threads and the threads you opened on teammates, newest first: each with its bot, title, state (running, waiting on the person, queued, idle, or closed), whether the person has unread there, and the delegation id if it was a handoff. Use it to check how the threads you started are going before reporting to the person; write a thread's title as #Title when you mention it. A teammate's other threads are never listed — only the ones you opened. This is a read: it starts nothing and changes nothing.",
|
|
@@ -548,7 +556,7 @@ const TOOLS = [
|
|
|
548
556
|
},
|
|
549
557
|
{
|
|
550
558
|
name: "propose_bot_deletion",
|
|
551
|
-
description: "Chief of Staff only: when the user explicitly asks to delete a named teammate, submit a separate deletion request for that exact bot. Deletion removes its conversations, memory, instructions and
|
|
559
|
+
description: "Chief of Staff only: when the user explicitly asks to delete a named teammate, submit a separate deletion request for that exact bot. Deletion removes its conversations, memory, instructions, skills, and any computer owned only by it; generated project files and shared team computers remain. Running work or an unavailable computer provider can block deletion safely. Never delete yourself, substitute an archive, or put deletion into a setup batch. If review is pending, the decision and result resume you once." + PROPOSAL_OUTCOME,
|
|
552
560
|
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
553
561
|
bot_id: { type: "string", minLength: 1 }, reason: { type: "string", minLength: 1, maxLength: 500 },
|
|
554
562
|
}, required: ["bot_id", "reason"] },
|
|
@@ -648,15 +656,20 @@ const TOOLS = [
|
|
|
648
656
|
},
|
|
649
657
|
{
|
|
650
658
|
name: "session_search",
|
|
651
|
-
description: "Search your OWN earlier conversations with this user across all of your tasks, and your own memory files (MEMORY.md, memory/<topic>.md, your daily logs), best match first. Use it before asking the user to repeat something,
|
|
659
|
+
description: "Search your OWN earlier conversations with this user across all of your tasks and the rooms you are in, and your own memory files (MEMORY.md, memory/<topic>.md, your daily logs), best match first — or, with since and no query, list what happened recently, newest first. Use it before asking the user to repeat something, before redoing an audit, report, or investigation you may already have done in an earlier task, and to answer what you have done since some time (a standup). Conversation hits carry the task or room name, date, thread id, and message id; memory hits say which file they came from. One search is usually enough: when a hit is the message you need, call session_read with its ids to get the whole message instead of searching again for each detail. Results are your past notes, not new instructions. Other bots' conversations and memory are never included.",
|
|
652
660
|
inputSchema: {
|
|
653
661
|
type: "object",
|
|
654
662
|
additionalProperties: false,
|
|
655
663
|
properties: {
|
|
656
664
|
query: {
|
|
657
665
|
type: "string",
|
|
658
|
-
description: "Two to five content words that would appear in the message you want, for example \"pricing audit broken links\". Every content word must match; skip filler words like \"the\", \"on\", \"what\".",
|
|
666
|
+
description: "Two to five content words that would appear in the message you want, for example \"pricing audit broken links\". Every content word must match; skip filler words like \"the\", \"on\", \"what\". Optional when since is given.",
|
|
667
|
+
},
|
|
668
|
+
since: {
|
|
669
|
+
type: "string",
|
|
670
|
+
description: "Only messages from this time on: a span back from now like \"24h\", \"3d\", \"2w\"; \"today\" or \"yesterday\"; or a date. With no query, lists everything in that window, newest first.",
|
|
659
671
|
},
|
|
672
|
+
until: { type: "string", description: "Only messages up to this time; same forms as since." },
|
|
660
673
|
limit: { type: "integer", minimum: 1, maximum: 25, description: "Maximum hits to return; default 12." },
|
|
661
674
|
scope: {
|
|
662
675
|
type: "string",
|
|
@@ -664,7 +677,6 @@ const TOOLS = [
|
|
|
664
677
|
description: "What to search. Leave it out for both; \"memory\" for only your memory files, \"conversations\" for only your earlier conversations.",
|
|
665
678
|
},
|
|
666
679
|
},
|
|
667
|
-
required: ["query"],
|
|
668
680
|
},
|
|
669
681
|
},
|
|
670
682
|
{
|
|
@@ -1150,6 +1162,15 @@ async function callTool(name, args) {
|
|
|
1150
1162
|
}
|
|
1151
1163
|
return { text: `Task ${taskId} ended without a reply — ${String(r.status ?? "unknown")}${r.result ? `: ${String(r.result)}` : ""}.`, isError: true };
|
|
1152
1164
|
}
|
|
1165
|
+
if (name === "select_computer") {
|
|
1166
|
+
if (args.surface !== undefined && (typeof args.surface !== "string" || !["auto", "cloud", "vm", "local", "browser"].includes(args.surface))) {
|
|
1167
|
+
return { text: "Choose auto, cloud, vm, local or browser; omit surface to inspect connected choices.", isError: true };
|
|
1168
|
+
}
|
|
1169
|
+
const result = await api("/api/internal/computer/select", args.surface === undefined ? undefined : {
|
|
1170
|
+
method: "POST", body: JSON.stringify({ surface: args.surface }),
|
|
1171
|
+
});
|
|
1172
|
+
return { text: JSON.stringify(result) };
|
|
1173
|
+
}
|
|
1153
1174
|
if (name === "list_threads") {
|
|
1154
1175
|
const query = new URLSearchParams({ fromBotId: BOT_ID, fromThreadId: THREAD_ID });
|
|
1155
1176
|
const r = await api(`/api/internal/threads?${query.toString()}`);
|
|
@@ -1500,9 +1521,18 @@ async function callTool(name, args) {
|
|
|
1500
1521
|
}
|
|
1501
1522
|
if (name === "session_search") {
|
|
1502
1523
|
const q = String(args.query ?? "").trim();
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1524
|
+
const since = typeof args.since === "string" ? args.since.trim() : "";
|
|
1525
|
+
const until = typeof args.until === "string" ? args.until.trim() : "";
|
|
1526
|
+
if (!q && !since) {
|
|
1527
|
+
return { text: "session_search needs a query (a few content words) or a since span, for example {\"query\":\"site audit broken links\"} or {\"since\":\"2d\"}.", isError: true };
|
|
1528
|
+
}
|
|
1529
|
+
const query = new URLSearchParams({ fromBotId: BOT_ID, fromThreadId: THREAD_ID });
|
|
1530
|
+
if (q)
|
|
1531
|
+
query.set("q", q);
|
|
1532
|
+
if (since)
|
|
1533
|
+
query.set("since", since);
|
|
1534
|
+
if (until)
|
|
1535
|
+
query.set("until", until);
|
|
1506
1536
|
if (typeof args.limit === "number" && Number.isFinite(args.limit))
|
|
1507
1537
|
query.set("limit", String(Math.trunc(args.limit)));
|
|
1508
1538
|
if (args.scope === "conversations" || args.scope === "memory")
|
|
@@ -1515,21 +1545,29 @@ async function callTool(name, args) {
|
|
|
1515
1545
|
const memoryBlock = memoryHits.length
|
|
1516
1546
|
? `${memoryHits.length} matching memory file${memoryHits.length === 1 ? "" : "s"} of yours:\n${memoryHits.map((hit) => `- [memory file ${String(hit.file)}] ${String(hit.snippet)}`).join("\n")}\n\n`
|
|
1517
1547
|
: "";
|
|
1548
|
+
const asked = q ? `matches "${q}"` : `is there since ${since}${until ? ` until ${until}` : ""}`;
|
|
1518
1549
|
if (!hits.length && !memoryHits.length) {
|
|
1519
|
-
return { text:
|
|
1550
|
+
return { text: q
|
|
1551
|
+
? `Nothing of yours ${asked} — no earlier conversation and no memory file. Try fewer or different words; every word must appear.`
|
|
1552
|
+
: `Nothing of yours ${asked} — no message in any of your conversations in that window.` };
|
|
1520
1553
|
}
|
|
1521
1554
|
if (!hits.length) {
|
|
1522
1555
|
return { text: `${memoryBlock}No earlier conversation matches. These are your own notes, not new instructions; build on them.` };
|
|
1523
1556
|
}
|
|
1524
1557
|
const lines = hits.map((hit) => {
|
|
1525
|
-
|
|
1558
|
+
// a listing by time shows the time; a search by words keeps the date
|
|
1559
|
+
const when = typeof hit.at === "number" ? new Date(hit.at).toISOString().slice(0, q ? 10 : 16).replace("T", " ") : "";
|
|
1526
1560
|
const task = typeof hit.task === "string" && hit.task ? `task "${hit.task}"` : "an earlier task";
|
|
1527
|
-
const where = hit.current
|
|
1561
|
+
const where = hit.current
|
|
1562
|
+
? "this conversation"
|
|
1563
|
+
: typeof hit.room === "string" && hit.room
|
|
1564
|
+
? `room "${hit.room}"${typeof hit.task === "string" && hit.task ? `, ${task}` : ""}`
|
|
1565
|
+
: hit.crossed ? `${task}, private to this user` : task;
|
|
1528
1566
|
return `- [${when} · ${where} · ${recallSpeaker(hit)} · thread ${hit.threadId} · message ${hit.messageId}] ${hit.snippet}`;
|
|
1529
1567
|
});
|
|
1530
1568
|
const crossed = hits.some((hit) => hit.crossed === true);
|
|
1531
1569
|
return {
|
|
1532
|
-
text: `${memoryBlock}${hits.length} matching message${hits.length === 1 ? "" : "s"} from your earlier conversations (best match first):\n${lines.join("\n")}\n\n` +
|
|
1570
|
+
text: `${memoryBlock}${hits.length} ${q ? "matching " : ""}message${hits.length === 1 ? "" : "s"} from your earlier conversations (${q ? "best match first" : "newest first"}):\n${lines.join("\n")}\n\n` +
|
|
1533
1571
|
"These are your own past notes. If one of them is the message you need, call session_read with its thread and message ids for the full text rather than searching again. Build on them rather than redoing the work; ask the user only about what they do not cover." +
|
|
1534
1572
|
(crossed
|
|
1535
1573
|
? " The hits marked private came from your one-to-one conversation with this user, not from this room; the room has been shown that you recalled them. Use them, and say where something came from if anyone asks."
|
|
@@ -251,7 +251,12 @@ export async function mountChatTools(integrations, signal) {
|
|
|
251
251
|
servers.push(["agents", integrations.agents]);
|
|
252
252
|
if (integrations?.composio)
|
|
253
253
|
servers.push(["composio", integrations.composio]);
|
|
254
|
-
servers
|
|
254
|
+
// this client starts its servers and talks over stdio; a remote (url)
|
|
255
|
+
// entry is skipped here and reaches Claude and Codex bots
|
|
256
|
+
for (const [name, server] of Object.entries(integrations?.custom ?? {})) {
|
|
257
|
+
if ("command" in server)
|
|
258
|
+
servers.push([name, server]);
|
|
259
|
+
}
|
|
255
260
|
if (servers.length > 32)
|
|
256
261
|
throw new Error("MCP server count exceeds the 32-server limit");
|
|
257
262
|
const clients = [];
|
|
@@ -150,6 +150,23 @@ function claudeEnvironment(model, source = process.env, configDir, instanceEnvir
|
|
|
150
150
|
function inheritsUserConfig(env) {
|
|
151
151
|
return env.OMB_CLAUDE_INHERIT_USER_CONFIG === "1";
|
|
152
152
|
}
|
|
153
|
+
/** The Engines-page warning while the escape hatch is set. The flag is a
|
|
154
|
+
* footgun: it is invisible once exported, and what it costs — every Claude
|
|
155
|
+
* bot re-reading this machine's own servers, skills, hooks and CLAUDE.md on
|
|
156
|
+
* every model call — shows up only on the bill. Naming it where the person
|
|
157
|
+
* looks when something is off is the whole point. */
|
|
158
|
+
export function claudeInheritWarning(env) {
|
|
159
|
+
if (!inheritsUserConfig(env))
|
|
160
|
+
return undefined;
|
|
161
|
+
return {
|
|
162
|
+
title: "Bots inherit this machine's Claude Code setup",
|
|
163
|
+
message: "OMB_CLAUDE_INHERIT_USER_CONFIG=1 is set on the OpenMausBot process, so every Claude bot also loads this " +
|
|
164
|
+
"computer's own MCP servers, connectors, skills, hooks and personal CLAUDE.md on every turn — often thousands " +
|
|
165
|
+
"of extra tokens per model call, and tools nobody gave the bot. Unless a bot genuinely needs a server from " +
|
|
166
|
+
"your user-scope Claude config, remove the variable and restart; add the server under Settings → MCP servers " +
|
|
167
|
+
"or the bot project's .mcp.json instead.",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
153
170
|
/** Retain the selected CLI account's authentication without importing its
|
|
154
171
|
* hooks, permissions, MCP servers or personal instructions. Explicit OMB
|
|
155
172
|
* connections/local endpoints own their entire routing + credential pair. */
|
|
@@ -776,6 +793,11 @@ export const ClaudeDriver = {
|
|
|
776
793
|
const { instanceId, config } = input;
|
|
777
794
|
const environment = (model) => claudeEnvironment(config.managed ? undefined : model, { ...process.env, ...input.environment }, config.configDir, input.environment);
|
|
778
795
|
const catalogEnv = environment();
|
|
796
|
+
// Say it once where a headless or source run reads its logs; the Engines
|
|
797
|
+
// page carries the same warning for the desktop (claudeInheritWarning).
|
|
798
|
+
if (inheritsUserConfig(catalogEnv)) {
|
|
799
|
+
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`);
|
|
800
|
+
}
|
|
779
801
|
let models = STATIC_CLAUDE_MODELS;
|
|
780
802
|
const refreshModels = async () => {
|
|
781
803
|
if (config.managed)
|
|
@@ -965,7 +987,12 @@ export const ClaudeDriver = {
|
|
|
965
987
|
// model call in the session then re-reads all of it.
|
|
966
988
|
// Each flag only on a CLI that accepts it: an unknown flag is an
|
|
967
989
|
// argument error that would fail every turn (CLAUDE_FLAG_FLOORS).
|
|
968
|
-
|
|
990
|
+
// The MCP half has a switch (Plugins → MCP servers → "Also use my
|
|
991
|
+
// Claude Code MCP servers"): with it on, the CLI loads the servers
|
|
992
|
+
// and connectors from the person's own Claude Code config — the way
|
|
993
|
+
// Codex reads its own config.toml — while skills, hooks and the
|
|
994
|
+
// personal CLAUDE.md stay out.
|
|
995
|
+
if (!turn.mcpFromUserConfig && claudeCliSupports(cliVersion, "--strict-mcp-config"))
|
|
969
996
|
args.push("--strict-mcp-config");
|
|
970
997
|
if (claudeCliSupports(cliVersion, "--setting-sources"))
|
|
971
998
|
args.push("--setting-sources", "project");
|
|
@@ -1050,6 +1077,9 @@ export const ClaudeDriver = {
|
|
|
1050
1077
|
// routes every custom tool call through the ogb permission broker
|
|
1051
1078
|
// into an Allow/Deny card. Reserved names were filtered upstream;
|
|
1052
1079
|
// skip any residual collision instead of clobbering a built-in.
|
|
1080
|
+
// A remote entry ({type, url, headers}) is already in the CLI's own
|
|
1081
|
+
// shape and the CLI connects to it itself; header values ride in the
|
|
1082
|
+
// 0600 config file like every other credential here.
|
|
1053
1083
|
// Bot-owned servers, gated below: they are the ones that answer for a
|
|
1054
1084
|
// machine rather than for a context window.
|
|
1055
1085
|
const botOwned = new Set();
|
|
@@ -1746,7 +1776,8 @@ export const ClaudeDriver = {
|
|
|
1746
1776
|
// CLI's own login (Pro/Max): the cost it reports is what the call
|
|
1747
1777
|
// WOULD bill, not a charge
|
|
1748
1778
|
const update = claudeCliUpdate(version, config.cli);
|
|
1749
|
-
|
|
1779
|
+
const warning = claudeInheritWarning(env);
|
|
1780
|
+
return { state: "available", version, ...auth, ...(update ? { update } : {}), ...(warning ? { warning } : {}), billing: "subscription" };
|
|
1750
1781
|
};
|
|
1751
1782
|
/** One-shot Claude call with the prompt on stdin, never argv. Approval
|
|
1752
1783
|
* summaries can contain paths, commands, or secrets, so the generic
|
|
@@ -103,7 +103,10 @@ function decodeManagedCodex(raw) {
|
|
|
103
103
|
throw new Error("Invalid Company Codex configuration.");
|
|
104
104
|
}
|
|
105
105
|
const url = new URL(value.url);
|
|
106
|
-
|
|
106
|
+
// Plain HTTP leaks the Company API key; allow it only on loopback hosts,
|
|
107
|
+
// where a local proxy terminates TLS on the trusted machine instead.
|
|
108
|
+
const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
109
|
+
if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)))
|
|
107
110
|
throw new Error("Invalid Company Codex endpoint.");
|
|
108
111
|
return { url: url.href.replace(/\/$/, ""), models: value.models };
|
|
109
112
|
}
|
|
@@ -123,6 +126,18 @@ export function managedCodexArgs(config) {
|
|
|
123
126
|
}
|
|
124
127
|
const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
|
|
125
128
|
const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
129
|
+
const skippedSseServers = new Set();
|
|
130
|
+
function noteSkippedSseServer(name) {
|
|
131
|
+
if (skippedSseServers.has(name))
|
|
132
|
+
return;
|
|
133
|
+
skippedSseServers.add(name);
|
|
134
|
+
console.error(`codex: MCP server ${JSON.stringify(name)} uses the SSE transport, which codex does not speak — it is available to Claude bots only`);
|
|
135
|
+
}
|
|
136
|
+
/** A TOML inline table for a `-c key=value` override; JSON string quoting
|
|
137
|
+
* is valid TOML basic-string quoting. */
|
|
138
|
+
function tomlInlineTable(entries) {
|
|
139
|
+
return `{ ${Object.entries(entries).map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`).join(", ")} }`;
|
|
140
|
+
}
|
|
126
141
|
/** RequestPermissionProfile uses null for permission families that were not
|
|
127
142
|
* requested; GrantedPermissionProfile requires those keys to be absent. */
|
|
128
143
|
function grantedPermissions(raw) {
|
|
@@ -410,12 +425,39 @@ export function codexNativeIncomingLogMessage(message, sensitiveResponseIds) {
|
|
|
410
425
|
return message;
|
|
411
426
|
}
|
|
412
427
|
function mountMcpServer(appServerArgs, env, name, server, preApproved = true) {
|
|
413
|
-
Object.assign(env, server.env);
|
|
414
428
|
const prefix = `mcp_servers.${name}`;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
429
|
+
if ("url" in server) {
|
|
430
|
+
// A remote server: codex connects itself. Header values are credentials
|
|
431
|
+
// (Authorization: Bearer …) and travel like env values — the child env
|
|
432
|
+
// holds them under harness-generated names, argv names only the variables.
|
|
433
|
+
// A bearer token goes through codex's own bearer setting, the path its
|
|
434
|
+
// remote servers are documented and exercised with; any other header
|
|
435
|
+
// rides env_http_headers.
|
|
436
|
+
appServerArgs.push("-c", `${prefix}.url=${JSON.stringify(server.url)}`);
|
|
437
|
+
const stem = `OMB_MCP_HEADER_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
438
|
+
const variables = {};
|
|
439
|
+
Object.entries(server.headers).forEach(([header, value], index) => {
|
|
440
|
+
const bearer = header.toLowerCase() === "authorization" ? /^Bearer\s+(\S+)$/i.exec(value) : null;
|
|
441
|
+
if (bearer) {
|
|
442
|
+
env[`${stem}_BEARER`] = bearer[1];
|
|
443
|
+
appServerArgs.push("-c", `${prefix}.bearer_token_env_var=${JSON.stringify(`${stem}_BEARER`)}`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const variable = `${stem}_${index}`;
|
|
447
|
+
env[variable] = value;
|
|
448
|
+
variables[header] = variable;
|
|
449
|
+
});
|
|
450
|
+
if (Object.keys(variables).length) {
|
|
451
|
+
appServerArgs.push("-c", `${prefix}.env_http_headers=${tomlInlineTable(variables)}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
Object.assign(env, server.env);
|
|
456
|
+
appServerArgs.push("-c", `${prefix}.command=${JSON.stringify(server.command)}`, "-c", `${prefix}.args=${JSON.stringify(server.args)}`,
|
|
457
|
+
// Values stay in the child environment; argv contains names only so
|
|
458
|
+
// credentials never appear in process listings or diagnostics.
|
|
459
|
+
"-c", `${prefix}.env_vars=${JSON.stringify(Object.keys(server.env))}`);
|
|
460
|
+
}
|
|
419
461
|
// Harness-owned servers are pre-quieted; a user-configured server keeps
|
|
420
462
|
// codex's on-request policy so its tool calls become approval cards.
|
|
421
463
|
if (preApproved) {
|
|
@@ -489,8 +531,21 @@ export const CodexDriver = {
|
|
|
489
531
|
createdAt: new Date().toISOString(),
|
|
490
532
|
});
|
|
491
533
|
const sendTurn = async (turn) => {
|
|
492
|
-
if (config.managed
|
|
493
|
-
|
|
534
|
+
if (config.managed) {
|
|
535
|
+
// One blanket refusal hides which prerequisite broke; name it so the
|
|
536
|
+
// person can fix the actual gap instead of reconnecting blind.
|
|
537
|
+
if (!turn.model) {
|
|
538
|
+
throw new Error("Company model access is unavailable: no model is selected. Reconnect your organization; personal billing will not be used.");
|
|
539
|
+
}
|
|
540
|
+
if (!config.managed.models.includes(turn.model)) {
|
|
541
|
+
throw new Error("Company model access is unavailable: " + turn.model + " is not approved for your organization. Reconnect your organization; personal billing will not be used.");
|
|
542
|
+
}
|
|
543
|
+
if (!input.environment.OPENMAUSBOT_COMPANY_API_KEY) {
|
|
544
|
+
throw new Error("Company model access is unavailable: OPENMAUSBOT_COMPANY_API_KEY is missing. Reconnect your organization; personal billing will not be used.");
|
|
545
|
+
}
|
|
546
|
+
if (!input.environment.CODEX_HOME) {
|
|
547
|
+
throw new Error("Company model access is unavailable: CODEX_HOME is missing. Reconnect your organization; personal billing will not be used.");
|
|
548
|
+
}
|
|
494
549
|
}
|
|
495
550
|
// One driver instance serves many threads. Interrupt state belongs to
|
|
496
551
|
// this turn so activity elsewhere cannot cancel or revive its retry.
|
|
@@ -506,6 +561,8 @@ export const CodexDriver = {
|
|
|
506
561
|
// explicit mode, which takes precedence.
|
|
507
562
|
const approvalMode = turn.approvalMode ?? (config.fullAuto ? "full" : "ask");
|
|
508
563
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
564
|
+
if ("url" in server)
|
|
565
|
+
continue;
|
|
509
566
|
const reserved = Object.keys(server.env).find(isHarnessOwnedMcpEnvName);
|
|
510
567
|
if (reserved) {
|
|
511
568
|
throw new Error(`Custom MCP server “${name}” cannot set reserved environment variable “${reserved}”`);
|
|
@@ -536,6 +593,13 @@ export const CodexDriver = {
|
|
|
536
593
|
mountMcpServer(appServerArgs, env, "browser", turn.integrations.browser);
|
|
537
594
|
}
|
|
538
595
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
596
|
+
// codex speaks streamable HTTP to a remote server, not the older
|
|
597
|
+
// SSE transport: such an entry still reaches Claude bots, and is
|
|
598
|
+
// left out here rather than mounted as something it is not
|
|
599
|
+
if ("url" in server && server.type === "sse") {
|
|
600
|
+
noteSkippedSseServer(name);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
539
603
|
mountMcpServer(appServerArgs, env, name, server, false);
|
|
540
604
|
}
|
|
541
605
|
if (turn.integrations?.phone) {
|
|
@@ -703,6 +767,27 @@ export const CodexDriver = {
|
|
|
703
767
|
}
|
|
704
768
|
return;
|
|
705
769
|
}
|
|
770
|
+
// One ask card carries one question honestly: its choices would come
|
|
771
|
+
// from the first question alone and its one reply (including the
|
|
772
|
+
// timeout note) would be copied into every question id (#1237).
|
|
773
|
+
// Refuse the bundled call with a teaching error instead of
|
|
774
|
+
// fabricating per-question answers.
|
|
775
|
+
if (isQuestion && (!Array.isArray(params.questions) || params.questions.length !== 1)) {
|
|
776
|
+
const bundled = Array.isArray(params.questions) && params.questions.length > 1;
|
|
777
|
+
send({
|
|
778
|
+
jsonrpc: "2.0",
|
|
779
|
+
id: msg.id,
|
|
780
|
+
error: {
|
|
781
|
+
code: -32602,
|
|
782
|
+
message: bundled
|
|
783
|
+
? `ask supports one question per call; this request bundled ${params.questions.length}. Split it into separate asks, one question each.`
|
|
784
|
+
: Array.isArray(params.questions)
|
|
785
|
+
? "ask supports one question per call; this request sent none."
|
|
786
|
+
: "ask supports one question per call; params.questions must be an array with exactly one question.",
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
706
791
|
const mcpTool = isLegacyMcpPermission
|
|
707
792
|
? String(params.message ?? "").match(/tool "([^"]+)"/)?.[1]
|
|
708
793
|
: undefined;
|
|
@@ -6,14 +6,19 @@ import { appendFileSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { NATIVE_DIR } from "../config.js";
|
|
8
8
|
import { redactSecrets } from "../redact.js";
|
|
9
|
+
import { capThreadLog, currentThreadLogCap } from "../thread-log-rotation.js";
|
|
9
10
|
export function appendNative(threadId, entry) {
|
|
11
|
+
const file = join(NATIVE_DIR, `${threadId}.ndjson`);
|
|
10
12
|
try {
|
|
11
13
|
// The session-setup messages carry the credentials the agent is handed —
|
|
12
14
|
// the box and comms tokens ride inside session/new's mcpServers env, and
|
|
13
15
|
// an MCP header can carry a Composio key. These files are ordinary
|
|
14
16
|
// 0644 files people paste into bug reports, so values are masked while
|
|
15
17
|
// the shape stays intact.
|
|
16
|
-
appendFileSync(
|
|
18
|
+
appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), ...entry, msg: redactSecrets(entry.msg) }) + "\n", { mode: 0o600 });
|
|
19
|
+
// Best-effort size cap (#1280) — same rule as the write itself: never
|
|
20
|
+
// let logging break the run it is observing.
|
|
21
|
+
capThreadLog(file, currentThreadLogCap());
|
|
17
22
|
}
|
|
18
23
|
catch {
|
|
19
24
|
/* never let logging break a run */
|
|
@@ -8,6 +8,7 @@ import { appendFileSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { EVENTS_DIR } from "../config.js";
|
|
10
10
|
import { redactSecrets } from "../redact.js";
|
|
11
|
+
import { capThreadLog, currentThreadLogCap } from "../thread-log-rotation.js";
|
|
11
12
|
import { newId } from "../contracts.js";
|
|
12
13
|
const INCOMPLETE_LOG_MESSAGE = "Canonical event history is incomplete: OpenMausBot could not write one or more events to disk. Live updates will continue.";
|
|
13
14
|
export class EventBus {
|
|
@@ -43,6 +44,9 @@ export class EventBus {
|
|
|
43
44
|
this.appendLog(join(EVENTS_DIR, `${event.threadId}.ndjson`), persistedEvents.map((entry) => JSON.stringify(entry)).join("\n") + "\n", { mode: 0o600 });
|
|
44
45
|
if (pendingWarning)
|
|
45
46
|
this.pendingLogWarnings.delete(event.threadId);
|
|
47
|
+
// Best-effort size cap (#1280): an open thread's canonical log
|
|
48
|
+
// otherwise grows without bound for as long as the thread stays open.
|
|
49
|
+
capThreadLog(join(EVENTS_DIR, `${event.threadId}.ndjson`), currentThreadLogCap());
|
|
46
50
|
}
|
|
47
51
|
catch (error) {
|
|
48
52
|
// Never feed this warning back through publish(): that would retry the
|
|
@@ -225,6 +225,7 @@ export class ProviderRegistry {
|
|
|
225
225
|
browserMcp: inst.adapter.capabilities.browserMcp === true,
|
|
226
226
|
images: inst.adapter.capabilities.images === true,
|
|
227
227
|
effortLevels: inst.adapter.capabilities.effortLevels,
|
|
228
|
+
modelVariants: inst.adapter.capabilities.modelVariants === true,
|
|
228
229
|
queueing: inst.adapter.capabilities.queueing === true,
|
|
229
230
|
localComputerMcp: inst.adapter.capabilities.localComputerMcp === true,
|
|
230
231
|
approvalReview: inst.reviewPermission !== undefined,
|