openmausbot 0.1.94 → 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-bxM7NV3N.js → DesktopViewer-BwWpc4RK.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-jsQkIqTn.js} +1 -1
- package/dist/assets/{arc-B5itKDcx.js → arc-7kei4Hsu.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-CRWDFDMq.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CTKW_IKs.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-BBTW7-6M.js} +1 -1
- package/dist/assets/channel-DS2NbT5x.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-M_Ox-MkD.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D2YiN-Ud.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-BmVYJ0f8.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-BtiLVVnZ.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-DFYSb6J6.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-mknKBzgV.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-DgSYGiOu.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-C7ZvuguH.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-CPqxINpA.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-Cd6xfJaC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-BSXY5qRh.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-D_lhEMj1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-Da9FrFwk.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-T77jMOXe.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-Dkj96X5E.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-CMi9zlD_.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-CXK6YUfS.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-BE7B1u9Q.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-CCMQphKr.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-iHJ_-tGH.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DONsCWQ7.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-CU_WOaXk.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-BeYwM04G.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-Bdw9DqEl.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-C9iwq3qm.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-Dx-VWzUg.js} +1 -1
- package/dist/assets/{index-CPiqc_DN.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-o64ma4CI.js → infoDiagram-VRGFBTTK-BG7uaEd4.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-D72WAXnM.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-DKH1gjYT.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-3upm9JUT.js} +1 -1
- package/dist/assets/{linear-BpKhuyLm.js → linear-CcR4pc0-.js} +1 -1
- package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-C6yU5FSS.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-DOI2xEuv.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-BVWJSgLd.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-jtbYezhq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-g0u8mtPX.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-k8689Wny.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-DsJ22PDt.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-CneWBRgG.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-DWsFCWap.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-Bvf0YE3S.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-Bxb3Iei8.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-DLkk7W4A.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-KR9bIHYC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-DfsOrj8t.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-DRSTzG3h.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-DtEdhPgT.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-B-XhTxjG.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +2 -1
- package/dist-server/companion/src/routes.js +39 -0
- package/dist-server/container-mcp.js +2 -2
- package/dist-server/drivers/agents-proxy.js +58 -38
- package/dist-server/enterprise/server/index.js +2 -1
- package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +35 -19
- package/dist-server/index.js +3159 -2313
- package/dist-server/local-computer.js +2 -1
- package/dist-server/openmausbot.js +314 -1336
- package/dist-server/pair-cli.js +314 -1336
- package/dist-server/proxy-paths.js +2 -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 +6 -0
- 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-runtime.js +1 -2
- package/dist-server/server/chief-of-staff.js +2 -3
- 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/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/config.js +29 -1
- 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/drivers/acp/core.js +7 -5
- package/dist-server/server/drivers/agents-call.js +42 -29
- package/dist-server/server/drivers/agents-catalog.js +24 -11
- 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 +23 -4
- package/dist-server/server/drivers/cerebras.js +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
- package/dist-server/server/drivers/claude.js +16 -14
- package/dist-server/server/drivers/openai-chat.js +3 -6
- package/dist-server/server/drivers/retry.js +8 -4
- package/dist-server/server/engine-install.js +1 -5
- 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 +789 -256
- package/dist-server/server/lending-memory.js +12 -8
- 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/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/proxy-paths.js +2 -1
- package/dist-server/server/room-handoffs.js +5 -13
- 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 +0 -15
- package/dist-server/server/store.js +9 -1
- package/dist-server/server/surface.js +65 -20
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-memory.js +209 -0
- package/dist-server/server/thread-events.js +2 -2
- 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/bot-avatar.js +8 -0
- package/dist-server/shared/connector-scopes.js +78 -0
- package/dist-server/shared/outbound.js +128 -0
- package/dist-server/shared/tool-surface.js +3 -2
- package/dist-server/vps-container-mcp.js +2 -2
- package/enterprise/server/index.js +2 -1
- package/package.json +1 -1
- package/dist/assets/channel-CdJX6i8k.js +0 -1
- package/dist/assets/index-CPYrGsv1.js +0 -610
- package/dist/assets/index-CzT42GR8.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.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
|
@@ -8,7 +8,8 @@ function resolveProxy(relative) {
|
|
|
8
8
|
return existsSync(source) ? source : join(SERVER_ROOT, `${relative}.js`);
|
|
9
9
|
}
|
|
10
10
|
var SPAWNED_PROXIES = {
|
|
11
|
-
browser:
|
|
11
|
+
// The built-in browser and the cloud computer: argv[2] picks which.
|
|
12
|
+
harnessMcp: resolveProxy("harness-mcp-proxy"),
|
|
12
13
|
localComputer: resolveProxy("local-computer-proxy"),
|
|
13
14
|
permission: resolveProxy("permission-proxy"),
|
|
14
15
|
containerMcp: resolveProxy("container-mcp"),
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
// not expose the routes.
|
|
12
12
|
import { resolveCompanionControlPlaneURL } from "../electron/companion-account-service.mjs";
|
|
13
13
|
import { ControlPlaneError, createControlPlaneClient, normalizeAccountEmail } from "../electron/control-plane-client.mjs";
|
|
14
|
-
export const ADMIN_EMAILS_ENV = "OMB_SIGNIN_EMAILS";
|
|
15
|
-
export const MEMBER_EMAILS_ENV = "OMB_SIGNIN_MEMBER_EMAILS";
|
|
16
14
|
/** Commas, spaces or newlines between entries; case does not matter. */
|
|
17
15
|
export function parseAllowList(value) {
|
|
18
16
|
return (value ?? "")
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// A bot's activity log: what it did, in plain words, with the outcome.
|
|
2
|
+
//
|
|
3
|
+
// Nothing here is captured for the log's sake. Two records already on disk
|
|
4
|
+
// between them know everything a receipt needs: the per-thread runtime
|
|
5
|
+
// events (harness/bus.ts) say what ran and whether it finished, and the
|
|
6
|
+
// fleet-wide decision log (decision-log.ts) says what was asked, with which
|
|
7
|
+
// arguments, and who allowed it. This module reads both back and folds
|
|
8
|
+
// them into one row per thing that happened.
|
|
9
|
+
//
|
|
10
|
+
// Connector calls pass through the harness relay, whose approval and scope
|
|
11
|
+
// decisions share the decision log. Read those alongside native tool runs.
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { readDecisions } from "./decision-log.js";
|
|
14
|
+
import { isRuntimeEvent, readRecentLines } from "./thread-events.js";
|
|
15
|
+
/** Composio toolkit slugs whose display name is not just a capitalised word. */
|
|
16
|
+
const TOOLKIT_NAMES = {
|
|
17
|
+
GMAIL: "Gmail",
|
|
18
|
+
GITHUB: "GitHub",
|
|
19
|
+
GOOGLESHEETS: "Google Sheets",
|
|
20
|
+
GOOGLECALENDAR: "Google Calendar",
|
|
21
|
+
GOOGLEDRIVE: "Google Drive",
|
|
22
|
+
GOOGLEDOCS: "Google Docs",
|
|
23
|
+
HUBSPOT: "HubSpot",
|
|
24
|
+
LINKEDIN: "LinkedIn",
|
|
25
|
+
YOUTUBE: "YouTube",
|
|
26
|
+
TWITTER: "X",
|
|
27
|
+
WHATSAPP: "WhatsApp",
|
|
28
|
+
ONEDRIVE: "OneDrive",
|
|
29
|
+
};
|
|
30
|
+
/** The agent's own built-in tools, phrased as what they did. */
|
|
31
|
+
const BUILTIN_LABELS = {
|
|
32
|
+
Bash: "Ran a command",
|
|
33
|
+
Read: "Read a file",
|
|
34
|
+
Edit: "Edited a file",
|
|
35
|
+
MultiEdit: "Edited files",
|
|
36
|
+
Write: "Wrote a file",
|
|
37
|
+
Glob: "Searched files",
|
|
38
|
+
Grep: "Searched files",
|
|
39
|
+
LS: "Listed a folder",
|
|
40
|
+
WebSearch: "Searched the web",
|
|
41
|
+
WebFetch: "Fetched a page",
|
|
42
|
+
Skill: "Used a skill",
|
|
43
|
+
Task: "Ran a subagent",
|
|
44
|
+
Agent: "Ran a subagent",
|
|
45
|
+
AskUserQuestion: "Asked you a question",
|
|
46
|
+
TodoWrite: "Updated its task list",
|
|
47
|
+
};
|
|
48
|
+
/** "save_issue" → "Save issue", "TaskUpdate" → "Task update",
|
|
49
|
+
* "CREATE_AN_ISSUE" → "Create an issue". */
|
|
50
|
+
function humanize(raw) {
|
|
51
|
+
const spaced = raw
|
|
52
|
+
.replace(/_+/g, " ")
|
|
53
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
54
|
+
.replace(/\s+/g, " ")
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase();
|
|
57
|
+
return spaced ? spaced[0].toUpperCase() + spaced.slice(1) : raw;
|
|
58
|
+
}
|
|
59
|
+
function toolkitName(slug) {
|
|
60
|
+
return TOOLKIT_NAMES[slug] ?? slug[0] + slug.slice(1).toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
/** Composio names its tools TOOLKIT_ACTION_WORDS, all upper case. */
|
|
63
|
+
const COMPOSIO_TOOL = /^([A-Z][A-Z0-9]+)_([A-Z0-9_]+)$/;
|
|
64
|
+
/** Turn a raw tool name into the app it touched and the action, in words. */
|
|
65
|
+
export function describeTool(name) {
|
|
66
|
+
const builtin = BUILTIN_LABELS[name];
|
|
67
|
+
if (builtin)
|
|
68
|
+
return { app: null, label: builtin };
|
|
69
|
+
if (name.startsWith("mcp__")) {
|
|
70
|
+
const [, server = "", ...rest] = name.split("__");
|
|
71
|
+
const action = rest.join("__");
|
|
72
|
+
if (server === "composio")
|
|
73
|
+
return describeTool(action);
|
|
74
|
+
if (server === "computer")
|
|
75
|
+
return { app: "Computer", label: humanize(action) };
|
|
76
|
+
if (server === "agents")
|
|
77
|
+
return { app: "Team", label: humanize(action) };
|
|
78
|
+
if (server.startsWith("claude_ai_")) {
|
|
79
|
+
return { app: server.slice("claude_ai_".length).replace(/_/g, " "), label: humanize(action) };
|
|
80
|
+
}
|
|
81
|
+
return { app: humanize(server), label: humanize(action) };
|
|
82
|
+
}
|
|
83
|
+
const composio = COMPOSIO_TOOL.exec(name);
|
|
84
|
+
if (composio)
|
|
85
|
+
return { app: toolkitName(composio[1]), label: humanize(composio[2]) };
|
|
86
|
+
return { app: null, label: humanize(name) };
|
|
87
|
+
}
|
|
88
|
+
/** How many decision rows to read back before filtering to one bot. The
|
|
89
|
+
* file is rotation-capped, so this is a ceiling on parse work, not a
|
|
90
|
+
* window that quietly drops history. */
|
|
91
|
+
const DECISION_SCAN = 20_000;
|
|
92
|
+
/** Runtime events read per thread, newest first; the reader tail-walks. */
|
|
93
|
+
const EVENTS_PER_THREAD = 2_000;
|
|
94
|
+
/** An approval is folded into the tool run it unblocked when the run
|
|
95
|
+
* started inside this window around the decision. A verdict is written
|
|
96
|
+
* as the request opens; the run follows it, occasionally by a clock skew's
|
|
97
|
+
* worth in the other direction. */
|
|
98
|
+
const MATCH_BEFORE_MS = 5_000;
|
|
99
|
+
const MATCH_AFTER_MS = 30_000;
|
|
100
|
+
const OUTCOME_OF_DECISION = {
|
|
101
|
+
"auto-approved": "allowed",
|
|
102
|
+
"auto-denied": "denied",
|
|
103
|
+
"user-approved": "allowed",
|
|
104
|
+
"review-would-approve": "allowed",
|
|
105
|
+
"user-denied": "denied",
|
|
106
|
+
"review-would-deny": "denied",
|
|
107
|
+
"card-shown": "waiting",
|
|
108
|
+
};
|
|
109
|
+
function describeRow(base) {
|
|
110
|
+
const { app, label } = describeTool(base.tool);
|
|
111
|
+
return { ...base, app, label };
|
|
112
|
+
}
|
|
113
|
+
/** One row per request: the decision log writes a row when a card is shown
|
|
114
|
+
* and another when it is answered, and the receipt wants the answer. */
|
|
115
|
+
function foldDecisions(rows) {
|
|
116
|
+
const byRequest = new Map();
|
|
117
|
+
rows.forEach((row, index) => {
|
|
118
|
+
const key = row.requestId ?? `row:${index}`;
|
|
119
|
+
const group = byRequest.get(key);
|
|
120
|
+
if (group)
|
|
121
|
+
group.push(row);
|
|
122
|
+
else
|
|
123
|
+
byRequest.set(key, [row]);
|
|
124
|
+
});
|
|
125
|
+
const folded = [];
|
|
126
|
+
for (const group of byRequest.values()) {
|
|
127
|
+
const opened = group[0];
|
|
128
|
+
const latest = group[group.length - 1];
|
|
129
|
+
const summary = group.find((row) => row.summary)?.summary;
|
|
130
|
+
folded.push(describeRow({
|
|
131
|
+
at: opened.at,
|
|
132
|
+
threadId: opened.threadId,
|
|
133
|
+
requestId: opened.requestId,
|
|
134
|
+
tool: latest.tool ?? opened.tool ?? "unknown",
|
|
135
|
+
...(summary ? { summary } : {}),
|
|
136
|
+
outcome: OUTCOME_OF_DECISION[latest.decision],
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
return folded;
|
|
140
|
+
}
|
|
141
|
+
function readToolRuns(eventsDir, threadId) {
|
|
142
|
+
const { lines } = readRecentLines(join(eventsDir, `${threadId}.ndjson`), EVENTS_PER_THREAD, isRuntimeEvent);
|
|
143
|
+
const runs = new Map();
|
|
144
|
+
for (const event of lines) {
|
|
145
|
+
if (event.type === "item.started" && event.itemType === "tool") {
|
|
146
|
+
const key = JSON.stringify([event.turnId ?? "", event.itemId ?? event.eventId]);
|
|
147
|
+
runs.set(key, describeRow({
|
|
148
|
+
at: event.createdAt,
|
|
149
|
+
threadId,
|
|
150
|
+
...(event.turnId ? { turnId: event.turnId } : {}),
|
|
151
|
+
tool: event.title ?? "unknown",
|
|
152
|
+
outcome: "running",
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
else if (event.type === "item.completed" && event.itemType === "tool" && event.itemId) {
|
|
156
|
+
const run = runs.get(JSON.stringify([event.turnId ?? "", event.itemId]));
|
|
157
|
+
if (run)
|
|
158
|
+
run.outcome = event.ok ? "ran" : "failed";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return [...runs.values()];
|
|
162
|
+
}
|
|
163
|
+
/** The bot's activity, newest first: every tool run in the threads named,
|
|
164
|
+
* and every decision the broker logged for the bot, with an approval folded
|
|
165
|
+
* into the run it unblocked wherever the two can be matched. */
|
|
166
|
+
export function readBotActivity(input) {
|
|
167
|
+
const { dataDir, eventsDir, botId, threadIds, limit } = input;
|
|
168
|
+
const runs = threadIds.flatMap((threadId) => readToolRuns(eventsDir, threadId));
|
|
169
|
+
const decisions = foldDecisions(readDecisions(dataDir, DECISION_SCAN).filter((row) => row.botId === botId));
|
|
170
|
+
const matched = new Set();
|
|
171
|
+
const rows = [...runs];
|
|
172
|
+
for (const decision of decisions) {
|
|
173
|
+
if (decision.outcome !== "allowed") {
|
|
174
|
+
rows.push(decision);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const askedAt = Date.parse(decision.at);
|
|
178
|
+
let best = null;
|
|
179
|
+
for (const run of runs) {
|
|
180
|
+
if (matched.has(run) || run.threadId !== decision.threadId || run.tool !== decision.tool)
|
|
181
|
+
continue;
|
|
182
|
+
const delta = Date.parse(run.at) - askedAt;
|
|
183
|
+
if (delta < -MATCH_BEFORE_MS || delta > MATCH_AFTER_MS)
|
|
184
|
+
continue;
|
|
185
|
+
if (!best || Math.abs(delta) < Math.abs(Date.parse(best.at) - askedAt))
|
|
186
|
+
best = run;
|
|
187
|
+
}
|
|
188
|
+
if (best) {
|
|
189
|
+
matched.add(best);
|
|
190
|
+
if (decision.summary)
|
|
191
|
+
best.summary = decision.summary;
|
|
192
|
+
if (decision.requestId)
|
|
193
|
+
best.requestId = decision.requestId;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
rows.push(decision);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
rows.sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
|
|
200
|
+
return rows.slice(0, Math.max(1, limit));
|
|
201
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// grants the app applies are Full access and the person's exact saved commands.
|
|
8
8
|
// Questions never come through here: a bot's question always reaches a human.
|
|
9
9
|
import { supportsApprovalMode } from "../shared/approval-mode.js";
|
|
10
|
+
import { isOutboundTool } from "../shared/outbound.js";
|
|
10
11
|
/** A failed delivery is a runtime error, not another permission decision.
|
|
11
12
|
* An expired ask must never become a fresh Allow/Deny card. */
|
|
12
13
|
export async function deliverFullAccessApproval(adapter, threadId, requestId, turnId, isCurrent = () => false) {
|
|
@@ -91,6 +92,8 @@ export function autoVerdict(mode, tool, context) {
|
|
|
91
92
|
return { approve: `approved ${tool} (full access)`, source: "full-access" };
|
|
92
93
|
if (context?.requiresExplicitApproval)
|
|
93
94
|
return { approve: null, source: "explicit-approval-block" };
|
|
95
|
+
if (isOutboundTool(tool))
|
|
96
|
+
return { approve: null, source: "outbound-guard" };
|
|
94
97
|
if (context?.commandAllowed)
|
|
95
98
|
return { approve: `approved ${tool} (saved command)`, source: "command-allowlist" };
|
|
96
99
|
if (mode === "auto" || mode === "custom")
|
|
@@ -105,6 +108,7 @@ export function autoVerdict(mode, tool, context) {
|
|
|
105
108
|
* the server keeps sending: cards saved before the key existed still render,
|
|
106
109
|
* and so do the free-text apply errors that have no key at all. */
|
|
107
110
|
export const HELD_NOTE = {
|
|
111
|
+
"approval.held.outbound": "This sends something on your behalf, so it always asks first.",
|
|
108
112
|
"approval.held.native": "The provider requires your approval for this action.",
|
|
109
113
|
"approval.held.sandbox": "This changes the provider sandbox, so only Full access can approve it automatically.",
|
|
110
114
|
"approval.held.undeliveredFull": "Full access couldn't deliver this approval.",
|
|
@@ -115,6 +119,8 @@ export const HELD_NOTE = {
|
|
|
115
119
|
export function approvalHeldNote(context) {
|
|
116
120
|
if (!context.permission)
|
|
117
121
|
return undefined;
|
|
122
|
+
if (context.source === "outbound-guard")
|
|
123
|
+
return "approval.held.outbound";
|
|
118
124
|
if (context.source === "explicit-approval-block")
|
|
119
125
|
return "approval.held.sandbox";
|
|
120
126
|
if (context.source === "native-approval")
|
|
@@ -7,7 +7,7 @@ export function recoveryCapabilityError(from, to) {
|
|
|
7
7
|
supportsWorkspaceFiles(from.driverKind) !== supportsWorkspaceFiles(to.driverKind)) {
|
|
8
8
|
return "The backup cannot use the same workspace.";
|
|
9
9
|
}
|
|
10
|
-
const tools = ["agentsMcp", "computerMcp", "
|
|
10
|
+
const tools = ["agentsMcp", "computerMcp", "composioMcp",
|
|
11
11
|
"phoneMcp", "browserMcp", "localComputerMcp", "customMcp", "images"];
|
|
12
12
|
if (tools.some(key => from.capabilities[key] === true && to.capabilities[key] !== true)) {
|
|
13
13
|
return "The backup does not support all of this engine's tools and attachments.";
|
|
@@ -388,13 +388,6 @@ export function resolveBoatCreate(request) {
|
|
|
388
388
|
return { ...resolved };
|
|
389
389
|
});
|
|
390
390
|
}
|
|
391
|
-
/** Read-only deletion guard. Both a key-only request with an ambiguous
|
|
392
|
-
* provider outcome and a known-but-not-yet-named Boat must keep its bot owner. */
|
|
393
|
-
export function hasUnresolvedBoatCreate(botId) {
|
|
394
|
-
if (!BOT_ID.test(botId))
|
|
395
|
-
throw new Error("invalid bot id for cloud computer creation");
|
|
396
|
-
return withJournalLock((requests) => (requests.some((request) => request.botId === botId && request.resolved !== true)));
|
|
397
|
-
}
|
|
398
391
|
/** Sanitized local recovery authority for configuration guards. The fresh,
|
|
399
392
|
* lock-protected read can prove a remembered provider id even while the
|
|
400
393
|
* account-wide LIST endpoint is eventually consistent. Provider request
|
|
@@ -442,7 +442,6 @@ export function markBoatDeletionBlocked(boxId, value) {
|
|
|
442
442
|
export function boatDeletionSnapshot() {
|
|
443
443
|
return withJournalLock((records) => records.map(clone));
|
|
444
444
|
}
|
|
445
|
-
export const listBoatDeletions = boatDeletionSnapshot;
|
|
446
445
|
export function getBoatDeletion(boxId) {
|
|
447
446
|
if (!BOAT_ID.test(boxId))
|
|
448
447
|
throw new Error("invalid cloud computer id for deletion");
|
|
@@ -451,9 +450,6 @@ export function getBoatDeletion(boxId) {
|
|
|
451
450
|
return record ? clone(record) : null;
|
|
452
451
|
});
|
|
453
452
|
}
|
|
454
|
-
export function isBoatDeletionPending(boxId) {
|
|
455
|
-
return getBoatDeletion(boxId) !== null;
|
|
456
|
-
}
|
|
457
453
|
export function hasPendingBoatDeletionForBot(botId) {
|
|
458
454
|
if (!BOT_ID.test(botId))
|
|
459
455
|
throw new Error("invalid bot id for cloud computer deletion");
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { stringify as stringifyYaml } from "yaml";
|
|
2
2
|
import { cronScheduleLabel } from "../shared/cron-label.js";
|
|
3
|
-
import { BOTMRR_MARKDOWN_VERSION, isPackageDocument,
|
|
3
|
+
import { BOTMRR_MARKDOWN_VERSION, isPackageDocument, PACKAGE_V1_MAX_SKILLS, parsePackageV1, utf8Bytes, } from "../shared/package-format.js";
|
|
4
4
|
// The package file format (v1 and v2) lives in shared/package-format.ts: one
|
|
5
5
|
// validation gate for the server, the renderer and Admin. This module keeps
|
|
6
6
|
// the v1 names the v1 exporter and its Markdown playbook are written in, and
|
|
7
7
|
// re-exports the v2 reader so server code has one place to import from.
|
|
8
8
|
export { canonicalJson, downgradeToV1, PACKAGE_MAX_BYTES, PACKAGE_VERSION, PackageFormatError, packageKeys, packageScanFindings, packageSecretFindings, packageSummary, parsePackageDocument, redactPackageSecrets, upgradeV1, } from "../shared/package-format.js";
|
|
9
|
-
export const BOT_PACKAGE_FORMAT = PACKAGE_FORMAT;
|
|
10
|
-
export const BOT_PACKAGE_VERSION = PACKAGE_V1_VERSION;
|
|
11
|
-
export { BOTMRR_MARKDOWN_VERSION };
|
|
12
|
-
export const BOT_PACKAGE_SKILLS_VERSION = 1;
|
|
13
9
|
export const BOT_PACKAGE_MAX_SKILLS = PACKAGE_V1_MAX_SKILLS;
|
|
14
10
|
const BOT_PACKAGE_MARKDOWN_MAX_BYTES = 1_000_000;
|
|
15
11
|
export function isBotPackage(value) {
|
|
@@ -98,17 +94,3 @@ export function renderBotPackageMarkdown(document) {
|
|
|
98
94
|
throw new Error("The bot playbook is too large");
|
|
99
95
|
return markdown;
|
|
100
96
|
}
|
|
101
|
-
export function packageAgentAsMember(agent) {
|
|
102
|
-
return {
|
|
103
|
-
key: agent.key,
|
|
104
|
-
name: agent.name,
|
|
105
|
-
title: agent.title ?? "",
|
|
106
|
-
description: agent.description ?? "",
|
|
107
|
-
...(agent.soul !== undefined ? { soul: agent.soul } : {}),
|
|
108
|
-
appearance: {
|
|
109
|
-
color: agent.appearance.color,
|
|
110
|
-
...(agent.appearance.mascotExpression ? { mascotExpression: agent.appearance.mascotExpression } : {}),
|
|
111
|
-
...(agent.appearance.mascotBody ? { mascotBody: agent.appearance.mascotBody } : {}),
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
@@ -216,8 +216,7 @@ export class BrowserRuntime {
|
|
|
216
216
|
options;
|
|
217
217
|
closeBrowser;
|
|
218
218
|
constructor({ closeBrowser, ...options } = {}) {
|
|
219
|
-
|
|
220
|
-
this.options = { requestTimeoutMs: 120_000, takeoverTimeoutMs: 15_000, idleMs: 60_000, maxPending: 16, resultBudget: Number.isFinite(budget) && budget > 0 ? budget : DEFAULT_BROWSER_RESULT_BUDGET, ...options };
|
|
219
|
+
this.options = { requestTimeoutMs: 120_000, takeoverTimeoutMs: 15_000, idleMs: 60_000, maxPending: 16, resultBudget: DEFAULT_BROWSER_RESULT_BUDGET, ...options };
|
|
221
220
|
this.closeBrowser = closeBrowser ?? (async () => false);
|
|
222
221
|
}
|
|
223
222
|
gate(session) {
|
|
@@ -9,7 +9,7 @@ const sectionKey = (section) => section?.trim() || "";
|
|
|
9
9
|
/** Dynamic system context for a section's Chief of Staff.
|
|
10
10
|
* It names the current team on every turn, while list_bots remains the
|
|
11
11
|
* authoritative tool for IDs and live availability at delegation time. */
|
|
12
|
-
export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate,
|
|
12
|
+
export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, boundedCoordination = false) {
|
|
13
13
|
const chief = bots.find((bot) => bot.id === chiefId);
|
|
14
14
|
const chiefSection = sectionKey(chief?.section);
|
|
15
15
|
const sectionName = peerName(chiefSection) || "General";
|
|
@@ -29,7 +29,7 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
|
|
|
29
29
|
});
|
|
30
30
|
const delegation = canDelegate
|
|
31
31
|
? boundedCoordination
|
|
32
|
-
? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work.
|
|
32
|
+
? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work. Give clear briefs, then end your turn; you resume automatically after their results return. Leads can coordinate their own specialists. Do not poll, send acknowledgements as new work, or substitute native helpers for named bots. On return, verify the requested outcome, resolve decisions within the user's scope, request concrete corrections with rework=true when necessary, and return one consolidated answer. Consultations are advice, not proof that work or tests ran. A refusal from coordinate_bots means nothing was sent: fix what it names (usually the id — copy it from list_bots or your roster; a unique teammate name also works) and retry, and never describe a handoff the tool did not accept."
|
|
33
33
|
: [
|
|
34
34
|
"Use list_bots to confirm the live roster and IDs. When assigning work to a teammate, use delegate_bot: it returns immediately, keeps you available to the user, and delivers the teammate's outcome back into this conversation automatically — success or failure. When the result arrives you are woken with it: report it to the user and act. If the teammate fails or stalls, tell the user plainly and decide the next step yourself.",
|
|
35
35
|
"After delegate_bot accepts the task, acknowledge the handoff and continue with any independent work or end your turn. Do not call wait_delegation or repeatedly poll check_delegation in the same turn.",
|
|
@@ -52,6 +52,5 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
|
|
|
52
52
|
canDelegate ? "When the user asks you to assemble or configure a team, use list_team_setup for the exact authorized teams, bot IDs and model catalog, then propose_team_setup once with all named specialists and their profile/model changes. Include new teams explicitly; the plan covers their creation and your access. Existing thread models and other bots' execution permissions stay unchanged. Follow the tool result: granted Full Access may apply the plan immediately; after an applied result, continue already-requested work without another confirmation. Only if review is pending, end your turn: the user's decision automatically resumes you once with a structured result. Report failed or cancelled results honestly. Do not ask for another yes, poll, or repeat the proposal. After successful setup, use the available coordination tools for already requested work. Use create_bot only for a single specialist when no combined setup was requested. For explicitly requested bot deletion, use propose_bot_deletion separately and follow its applied or pending result too. Do not create duplicate or unnecessary bots." : "",
|
|
53
53
|
chief?.managedSections?.length ? "Reachable teammates in your allowed teams:" : `Current ${sectionName} section team:`,
|
|
54
54
|
roster,
|
|
55
|
-
trustedOpenMausStatus,
|
|
56
55
|
].filter(Boolean).join("\n");
|
|
57
56
|
}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
export const CLOUD_BACKEND_CHANGE_ERROR = "stop the active turn before changing the cloud backend";
|
|
2
|
-
export const VPS_ALIAS_CHANGE_ERROR = "stop the active VPS turn before changing the SSH config alias";
|
|
3
2
|
export const BOAT_ACCOUNT_RESOURCES_ERROR = "remove this installation's cloud computers before changing or clearing the Boat account";
|
|
4
3
|
export const VPS_ALIAS_RESOURCES_ERROR = "remove this installation's VPS computers before changing or clearing the SSH config alias";
|
|
5
4
|
export function cloudBackendChangeError(botBusy, activeVpsThread) {
|
|
6
5
|
return botBusy || activeVpsThread ? CLOUD_BACKEND_CHANGE_ERROR : null;
|
|
7
6
|
}
|
|
8
|
-
export function vpsAliasChangeError(currentAlias, nextAlias, activeVpsThread) {
|
|
9
|
-
return activeVpsThread && currentAlias !== nextAlias ? VPS_ALIAS_CHANGE_ERROR : null;
|
|
10
|
-
}
|
|
11
7
|
/** An old Boat account may be detached only when it owns no local resources.
|
|
12
8
|
* Token rotation is safe when the replacement credential proves access to
|
|
13
9
|
* the exact same provider identities. */
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isolatedRemoteCommand, runCommand, screenshotBoat } from "./boat.js";
|
|
2
|
+
import { CONTROL_REFUSAL_PLAIN } from "./control-client.js";
|
|
3
|
+
import { compileToolSchema } from "./drivers/chat-mcp-tools.js";
|
|
4
|
+
const coordinate = { type: "integer", minimum: 0, maximum: 32767 };
|
|
5
|
+
const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
|
|
6
|
+
export const CLOUD_COMPUTER_TOOLS = [
|
|
7
|
+
tool("screenshot", "See the assigned cloud desktop at native pixel resolution."),
|
|
8
|
+
tool("get_screen_size", "Get the cloud desktop width and height."),
|
|
9
|
+
tool("click", "Click a point on the cloud desktop.", { x: coordinate, y: coordinate, button: { type: "string", enum: ["left", "middle", "right"] }, count: { type: "integer", minimum: 1, maximum: 3 } }, ["x", "y"]),
|
|
10
|
+
tool("move", "Move the cloud mouse pointer.", { x: coordinate, y: coordinate }, ["x", "y"]),
|
|
11
|
+
tool("drag", "Drag on the cloud desktop.", { x: coordinate, y: coordinate, to_x: coordinate, to_y: coordinate }, ["x", "y", "to_x", "to_y"]),
|
|
12
|
+
tool("type_text", "Type Unicode text into the focused cloud application.", { text: { type: "string", maxLength: 4000 } }, ["text"]),
|
|
13
|
+
tool("key_press", "Press an X11 key or shortcut.", { key: { type: "string", pattern: "^[A-Za-z0-9_+]+$", minLength: 1, maxLength: 100 } }, ["key"]),
|
|
14
|
+
tool("scroll", "Scroll at a point on the cloud desktop.", { x: coordinate, y: coordinate, direction: { type: "string", enum: ["up", "down", "left", "right"] }, amount: { type: "integer", minimum: 1, maximum: 30 } }, ["x", "y", "direction"]),
|
|
15
|
+
tool("open_url", "Request opening an HTTP or HTTPS URL in the cloud desktop browser; inspect the screen to confirm it loaded.", { url: { type: "string", pattern: "^https?://", maxLength: 2000 } }, ["url"]),
|
|
16
|
+
tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
|
|
17
|
+
];
|
|
18
|
+
const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
19
|
+
const buttons = { left: 1, middle: 2, right: 3 };
|
|
20
|
+
const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
|
|
21
|
+
let validators;
|
|
22
|
+
/** Why a call cannot run, or null. Checked before anything else touches the
|
|
23
|
+
* call: x and y reach the remote shell unquoted, so only the advertised
|
|
24
|
+
* integer schema stands between a model's arguments and a command line. */
|
|
25
|
+
export function cloudComputerCallProblem(name, args) {
|
|
26
|
+
validators ??= new Map(CLOUD_COMPUTER_TOOLS.map(entry => [entry.name, compileToolSchema(entry.inputSchema)]));
|
|
27
|
+
const validate = typeof name === "string" ? validators.get(name) : undefined;
|
|
28
|
+
if (!validate)
|
|
29
|
+
return "Unknown cloud computer tool. Use one of the advertised computer tools.";
|
|
30
|
+
if (!args || typeof args !== "object" || Array.isArray(args) || !validate(args)) {
|
|
31
|
+
return "Tool arguments do not match the advertised input schema; use its required fields and types.";
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** One tool call on the given Boat with the server's own Boat account. The
|
|
36
|
+
* caller has already checked the arguments and the person's control. */
|
|
37
|
+
export async function runCloudComputerTool(cfg, boxId, name, args, signal) {
|
|
38
|
+
const problem = cloudComputerCallProblem(name, args);
|
|
39
|
+
if (problem)
|
|
40
|
+
return { isError: true, content: [{ type: "text", text: problem }] };
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
if (name === "screenshot") {
|
|
43
|
+
const shot = await screenshotBoat(cfg, "", boxId, { signal, nativeSize: true });
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
return { content: [{ type: "image", mimeType: "image/jpeg", data: shot.png }] };
|
|
46
|
+
}
|
|
47
|
+
const a = args;
|
|
48
|
+
const mouse = `xdotool mousemove --sync ${a.x} ${a.y}`;
|
|
49
|
+
let command;
|
|
50
|
+
switch (name) {
|
|
51
|
+
case "get_screen_size":
|
|
52
|
+
command = "xdotool getdisplaygeometry";
|
|
53
|
+
break;
|
|
54
|
+
case "click":
|
|
55
|
+
command = `${mouse} click --repeat ${a.count ?? 1} --delay 100 ${buttons[String(a.button ?? "left")]}`;
|
|
56
|
+
break;
|
|
57
|
+
case "move":
|
|
58
|
+
command = mouse;
|
|
59
|
+
break;
|
|
60
|
+
case "drag":
|
|
61
|
+
command = `${mouse} mousedown 1 mousemove --sync ${a.to_x} ${a.to_y} mouseup 1`;
|
|
62
|
+
break;
|
|
63
|
+
case "type_text":
|
|
64
|
+
command = `printf %s ${quote(Buffer.from(String(a.text)).toString("base64"))} | base64 -d | xclip -selection clipboard && xdotool key --clearmodifiers ctrl+v`;
|
|
65
|
+
break;
|
|
66
|
+
case "key_press":
|
|
67
|
+
command = `xdotool key --clearmodifiers ${quote(String(a.key))}`;
|
|
68
|
+
break;
|
|
69
|
+
case "scroll":
|
|
70
|
+
command = `${mouse} click --repeat ${a.amount ?? 3} --delay 80 ${scrollButtons[String(a.direction)]}`;
|
|
71
|
+
break;
|
|
72
|
+
case "open_url":
|
|
73
|
+
command = `nohup xdg-open ${quote(String(a.url))} >/dev/null 2>&1 </dev/null &`;
|
|
74
|
+
break;
|
|
75
|
+
case "exec":
|
|
76
|
+
command = String(a.command);
|
|
77
|
+
break;
|
|
78
|
+
default: return { isError: true, content: [{ type: "text", text: "Unknown cloud computer tool." }] };
|
|
79
|
+
}
|
|
80
|
+
const result = await runCommand(cfg, boxId, isolatedRemoteCommand(command), { signal });
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
if (name === "open_url" && result.ok)
|
|
83
|
+
return { content: [{ type: "text", text: "Browser launch requested. Page loading is not confirmed; inspect the screen before continuing." }] };
|
|
84
|
+
return { isError: !result.ok, content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
85
|
+
}
|
|
86
|
+
/** One JSON-RPC request from harness-mcp-proxy, answered with its MCP
|
|
87
|
+
* `result`. The order is the safety argument: arguments are checked before
|
|
88
|
+
* the control gate (which may claim the seat), a person's control refuses
|
|
89
|
+
* the call before anything reaches the Boat, and the turn's capability must
|
|
90
|
+
* still be live before and after the Boat acts. */
|
|
91
|
+
export async function cloudComputerRpc(request, deps) {
|
|
92
|
+
if (request?.method === "tools/list") {
|
|
93
|
+
deps.assertActive();
|
|
94
|
+
return { tools: CLOUD_COMPUTER_TOOLS };
|
|
95
|
+
}
|
|
96
|
+
if (request?.method !== "tools/call")
|
|
97
|
+
throw Object.assign(new Error("unsupported computer method"), { status: 400 });
|
|
98
|
+
const params = (request.params && typeof request.params === "object" ? request.params : {});
|
|
99
|
+
const args = params.arguments ?? {};
|
|
100
|
+
const problem = cloudComputerCallProblem(params.name, args);
|
|
101
|
+
if (problem)
|
|
102
|
+
return { isError: true, content: [{ type: "text", text: problem }] };
|
|
103
|
+
const control = await deps.gate();
|
|
104
|
+
deps.assertActive();
|
|
105
|
+
if (control.held)
|
|
106
|
+
return { isError: true, content: [{ type: "text", text: control.blockedReason || CONTROL_REFUSAL_PLAIN }] };
|
|
107
|
+
const result = await runCloudComputerTool(deps.cfg, deps.boxId, params.name, args, deps.signal);
|
|
108
|
+
deps.assertActive();
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
@@ -26,7 +26,6 @@ import { join } from "node:path";
|
|
|
26
26
|
import { writeFileAtomic } from "./atomic.js";
|
|
27
27
|
import { hostedWorkspaceConfigured } from "./enterprise.js";
|
|
28
28
|
import { formatPairingCode } from "./sessions.js";
|
|
29
|
-
export const CLOUD_HOME_CONTRACT_VERSION = 1;
|
|
30
29
|
/** Any of these switches the server into Cloud home mode; then all are required. */
|
|
31
30
|
export const CLOUD_HOME_KEYS = ["OMB_CLOUD_ROLE", "OMB_CLOUD_MACHINE_ID", "OMB_CLOUD_ADMIN_URL", "OMB_CLOUD_BOOTSTRAP_SECRET"];
|
|
32
31
|
/** A platform model gateway's settings. A Cloud home never uses them: given
|
|
@@ -21,9 +21,6 @@ import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { writeFileAtomic } from "./atomic.js";
|
|
23
23
|
const personOf = (line) => line?.role === "user" && !line.peerAsk ? line.sender?.id : undefined;
|
|
24
|
-
export function cloudHomeTurnMayLend(turn) {
|
|
25
|
-
return cloudHomeLendingRefusal(turn) === null;
|
|
26
|
-
}
|
|
27
24
|
export function cloudHomeLendingRefusal(turn) {
|
|
28
25
|
const { request } = turn;
|
|
29
26
|
if (!request?.messageId || request.stopped || !request.generations.has(turn.generation))
|
|
@@ -244,9 +244,35 @@ function trustedAuthUrl(value, slug) {
|
|
|
244
244
|
}
|
|
245
245
|
return url.toString();
|
|
246
246
|
}
|
|
247
|
+
/** Composio's own hosts are always trusted. A backend the operator pointed
|
|
248
|
+
* the app at explicitly (OMB_COMPOSIO_API — a dev or test stub) may hand back
|
|
249
|
+
* a Session on its own origin, since the API itself was already trusted that
|
|
250
|
+
* far; any other host is refused. */
|
|
251
|
+
export function trustedSessionMcpUrl(value) {
|
|
252
|
+
let mcp;
|
|
253
|
+
try {
|
|
254
|
+
mcp = new URL(value);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (mcp.protocol === "https:" && (mcp.hostname === "composio.dev" || mcp.hostname.endsWith(".composio.dev")))
|
|
260
|
+
return true;
|
|
261
|
+
if (mcp.protocol !== "https:" && !(mcp.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(mcp.hostname)))
|
|
262
|
+
return false;
|
|
263
|
+
const override = process.env.OMB_COMPOSIO_API;
|
|
264
|
+
if (!override)
|
|
265
|
+
return false;
|
|
266
|
+
try {
|
|
267
|
+
return new URL(override).origin === mcp.origin;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
247
273
|
function parseSessionResponse(session) {
|
|
248
274
|
const mcp = new URL(session.mcp.url);
|
|
249
|
-
if (
|
|
275
|
+
if (!trustedSessionMcpUrl(session.mcp.url)) {
|
|
250
276
|
throw new Error("Composio returned an untrusted Session MCP URL");
|
|
251
277
|
}
|
|
252
278
|
return { ...session, mcp: { ...session.mcp, url: mcp.toString() } };
|
|
@@ -497,7 +523,7 @@ export async function mcpIntegration(cfg, context) {
|
|
|
497
523
|
},
|
|
498
524
|
};
|
|
499
525
|
}
|
|
500
|
-
export async function relayMcp(cfg, payload, transportSessionId) {
|
|
526
|
+
export async function relayMcp(cfg, payload, transportSessionId, beforeSend) {
|
|
501
527
|
const apiKey = projectApiKey(cfg);
|
|
502
528
|
let url;
|
|
503
529
|
let identity;
|
|
@@ -529,6 +555,9 @@ export async function relayMcp(cfg, payload, transportSessionId) {
|
|
|
529
555
|
if (forwardedTransportSessionId) {
|
|
530
556
|
headers.set("mcp-session-id", forwardedTransportSessionId);
|
|
531
557
|
}
|
|
558
|
+
// Session discovery can await network I/O. Turn authority and mutable
|
|
559
|
+
// permissions must be checked after it, immediately before dispatch.
|
|
560
|
+
beforeSend?.();
|
|
532
561
|
const response = await fetch(url, {
|
|
533
562
|
method: "POST",
|
|
534
563
|
headers,
|
|
@@ -153,18 +153,3 @@ export class ComputerControl {
|
|
|
153
153
|
return snapshot;
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
/** Safely executes an async tool or desktop control call, catching runtime exceptions gracefully. */
|
|
157
|
-
export async function executeToolSafely(toolName, fn) {
|
|
158
|
-
try {
|
|
159
|
-
const data = await fn();
|
|
160
|
-
return { success: true, data };
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
const errorMessage = err?.message || String(err) || 'Unknown execution error';
|
|
164
|
-
console.error(`[Tool Execution Error] Name: ${toolName} | Details: ${errorMessage}`);
|
|
165
|
-
return {
|
|
166
|
-
success: false,
|
|
167
|
-
error: `Tool '${toolName}' failed to execute: ${errorMessage}`
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|