electron/cua.mjs
Rama main · réplica de lectura del repositorio original.
// Packaged: an embedded cua-driver daemon, so macOS TCC grants attribute to OpenMausBot; dev attaches to CuaDriver.app.
// Agents use the stdio MCP proxy, which executes nothing; the host-owned daemon does.
import { app, ipcMain } from "electron";
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { promisify } from "node:util";
import { setTimeout as delay } from "node:timers/promises";
import fs from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import localOriginModule from "./local-origin.cjs";
import { createLocalComputerGrant } from "./local-computer-grant.mjs";
import { driverFailure } from "./cua-failure.mjs";
import { createHelperRestarts, followLinuxConnection } from "./cua-restart.mjs";
import { watchDriver } from "./cua-watch.mjs";
import { missingPermissions } from "./lent-screen-failures.mjs";
const { localOnly } = localOriginModule;
const require = createRequire(import.meta.url);
const { createCuaConnectionStore } = require("./cua-connection.cjs");
const {
createLinuxCuaPreferenceStore,
createLinuxCuaRuntime,
createUnavailableLinuxRuntime,
} = require("./cua-linux-runtime.cjs");
const {
cleanupAppImageCuaBundle,
reapStaleAppImageCuaBundles,
stageAppImageCuaBundle,
} = require("./cua-linux-bundle.cjs");
const { driverBlocked, linuxLocalControlSupport } = require("./capabilities.cjs");
const { probeCuaHyprlandPlugin } = require("./cua-hyprland.cjs");
const INSTALLED_DRIVER = "/Applications/CuaDriver.app/Contents/MacOS/cua-driver";
const STANDALONE_SOCKET = path.join(
app.getPath("home"),
"Library/Caches/cua-driver/cua-driver.sock",
);
const HOST_BUNDLE_ID = "com.openmausbot.app";
const CUA_ENV = { CUA_DRIVER_RS_TELEMETRY_ENABLED: "0" };
const execFileAsync = promisify(execFile);
process.env.CUA_DRIVER_RS_TELEMETRY_ENABLED ??= "0";
// Where cua-driver installs itself on Windows; the installer's own directory is the fallback.
const WIN_INSTALLED_DRIVERS = [
path.join(app.getPath("home"), ".cua-driver", "packages", "current", "cua-driver.exe"),
path.join(app.getPath("home"), "AppData", "Local", "Programs", "CuaDriver", "cua-driver.exe"),
];
let embeddedHost = null;
let embeddedGeneration = null;
let startupAbort = null;
/** On Windows a start that failed for a reason that passes (a slow first start while the system scans the new helper)
* is tried again by itself after these waits; a blocked or missing helper is not. */
const RETRY_AFTER_MS = [15_000, 60_000];
let retryTimer = null;
let driverWatch = null;
let lifecycleGeneration = 0;
let macRetry = null;
let linuxRuntime = null;
let linuxRuntimeLoading = null;
// Omarchy without Cua's Hyprland plugin: the person can turn the plugin on, so Enable and Try again look again.
let linuxRuntimeAwaitsPlugin = false;
let linuxBundleStage = null;
let stateListener = () => {};
const connectionStore = createCuaConnectionStore({
getUserData: () => app.getPath("userData"),
});
const localComputerGrant = createLocalComputerGrant({
connection: () => connectionStore.get(),
log: (line) => console.error(`[cua] ${line}`),
});
const helperRestarts = createHelperRestarts({
restart: async () => {
if (process.platform === "linux") return (await (await ensureLinuxRuntime()).retry())?.status === "ready";
const connection = await startCua({ attempt: RETRY_AFTER_MS.length });
if (connection?.mode === "embedded" || connection?.mode === "standalone") return true;
return driverBlocked(connection?.reason) || missingPermissions(connection?.reason).length > 0 ? "needs-person" : false;
},
// Linux already published its own unavailable record: a legacy-shaped one would not fit its schema.
report: (reason) => { if (process.platform !== "linux") persistAndNotify({ mode: "unavailable", reason }); },
log: (line) => console.error(`[cua] ${line}`),
});
function ensureLinuxRuntime({ recheck = false } = {}) {
if (linuxRuntime && !(recheck && linuxRuntimeAwaitsPlugin)) return Promise.resolve(linuxRuntime);
linuxRuntimeLoading ??= loadLinuxRuntime().finally(() => {
linuxRuntimeLoading = null;
});
return linuxRuntimeLoading;
}
async function loadLinuxRuntime() {
const hyprlandPlugin = await probeCuaHyprlandPlugin({ env: process.env });
const support = linuxLocalControlSupport(process.platform, process.env, { hyprlandPlugin });
linuxRuntimeAwaitsPlugin = support.reasonCode === "linux-hyprland-plugin-required";
if (!support.available) {
linuxRuntime = createUnavailableLinuxRuntime({
connectionStore,
preferenceStore: createLinuxCuaPreferenceStore({
getUserData: () => app.getPath("userData"),
}),
// A blocked Wayland desktop drops an old opt-in; a missing plugin keeps it for when the plugin is on.
clearPreference: !linuxRuntimeAwaitsPlugin,
reasonCode: support.reasonCode,
message: support.message,
compositor: support.compositor,
onChange: (connection) => notifyState(connection),
});
return linuxRuntime;
}
try {
let bundledDriverPath;
if (app.isPackaged && !process.env.CUA_DRIVER_PATH) {
bundledDriverPath = path.join(process.resourcesPath, "cua-linux-x64", "cua-driver");
// AppImage builders may normalize the resource tree to 0755/0775: copy the pinned binaries to a fresh 0700 dir and re-verify.
if (process.env.APPIMAGE) {
reapStaleAppImageCuaBundles();
linuxBundleStage ??= stageAppImageCuaBundle({ resourcesPath: process.resourcesPath });
bundledDriverPath = linuxBundleStage.driverPath;
}
}
linuxRuntime = createLinuxCuaRuntime({
getUserData: () => app.getPath("userData"),
connectionStore,
bundledDriverPath,
onChange: (connection) => {
notifyState(connection);
followLinuxConnection(helperRestarts, connection);
},
});
} catch (error) {
console.error("[cua] Bundled Linux driver failed integrity validation:", error);
linuxRuntime = createUnavailableLinuxRuntime({
connectionStore,
onChange: (connection) => notifyState(connection),
});
}
return linuxRuntime;
}
/** The app's own server child gets the driver connection, and every change to it, over its private port. */
export function grantLocalComputer(proc, isCurrent) {
localComputerGrant.attach(proc, isCurrent);
}
function notifyState(connection) {
localComputerGrant.changed();
stateListener(connection);
}
export function setCuaStateListener(listener) {
stateListener = typeof listener === "function" ? listener : () => {};
}
function persistAndNotify(next) {
const connection = connectionStore.persist(next);
notifyState(connection);
return connection;
}
export function resolveWindowsDriver() {
for (const candidate of WIN_INSTALLED_DRIVERS) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
export function resolveDriverBinary() {
if (process.env.CUA_DRIVER_PATH) return process.env.CUA_DRIVER_PATH;
if (app.isPackaged) {
const bundled = path.join(process.resourcesPath, "cua-driver" + (process.platform === "win32" ? ".exe" : ""));
if (fs.existsSync(bundled)) return bundled;
}
if (process.platform === "darwin" && fs.existsSync(INSTALLED_DRIVER)) return INSTALLED_DRIVER;
if (process.platform === "win32") {
const staged = path.join(app.getAppPath(), "dist-native", "cua-win32-x64", "cua-driver.exe");
return fs.existsSync(staged) ? staged : resolveWindowsDriver();
}
return null;
}
export function resolveEmbeddedDriverBinary(binary) {
if (process.platform !== "win32" || !app.isPackaged || process.env.CUA_DRIVER_PATH ||
binary !== path.join(process.resourcesPath, "cua-driver.exe")) return binary;
const background = path.join(process.resourcesPath, "cua-driver-background.exe");
if (!fs.existsSync(background)) throw new Error("Packaged background CUA driver is missing; reinstall OpenMausBot");
return background;
}
function socketAlive(sockPath) {
return new Promise((resolve) => {
if (!fs.existsSync(sockPath)) return resolve(false);
const s = net.createConnection(sockPath);
let timer;
const done = (ok) => {
clearTimeout(timer);
s.destroy();
resolve(ok);
};
s.once("connect", () => done(true));
s.once("error", () => done(false));
timer = setTimeout(() => done(false), 1500);
timer.unref();
});
}
async function loadEmbeddedSdk() {
if (!app.isPackaged) {
if (process.platform === "win32") return import("@trycua/cua-driver/embedded");
const [embedded, permissions] = await Promise.all([
import("@trycua/cua-driver/embedded"),
import("@trycua/cua-driver/electron"),
]);
return { ...embedded, ...permissions };
}
const isWindows = process.platform === "win32";
process.env.OPENMAUSBOT_CUA_SDK_LIBRARY = path.join(
process.resourcesPath,
"cua-sdk",
"native",
isWindows ? "cua_driver_sdk.dll" : "libcua_driver_sdk.dylib",
);
return import(pathToFileURL(path.join(process.resourcesPath, "cua-sdk", "cua-sdk.mjs")).href);
}
async function attachStandalone(signal) {
// Windows must stay on the owned embedded host, never an unrelated daemon.
if (process.platform !== "darwin") return null;
const driver = fs.existsSync(INSTALLED_DRIVER) ? INSTALLED_DRIVER : null;
if (!driver) return null;
if (!(await socketAlive(STANDALONE_SOCKET))) {
signal.throwIfAborted();
// Through LaunchServices so Accessibility/Screen Recording stay on com.trycua.driver, the identity already granted.
const launch = execFileAsync("/usr/bin/open", ["-a", "CuaDriver"], {
timeout: 8_000, killSignal: "SIGKILL", maxBuffer: 8_192,
});
// execFile's AbortSignal can send TERM regardless of killSignal; bound a launcher that ignores TERM.
const abort = () => launch.child.kill("SIGKILL");
signal.addEventListener("abort", abort, { once: true });
if (signal.aborted) abort();
try { await launch; }
finally { signal.removeEventListener("abort", abort); }
for (let i = 0; i < 25; i++) {
signal.throwIfAborted();
if (await socketAlive(STANDALONE_SOCKET)) break;
await delay(200, undefined, { signal });
}
}
if (!(await socketAlive(STANDALONE_SOCKET))) return null;
return {
mode: "standalone",
socketPath: STANDALONE_SOCKET,
mcpCommand: driver,
mcpArgs: ["mcp"],
mcpEnv: { ...CUA_ENV },
};
}
function embeddedSocketPath() {
if (process.platform === "win32") return `\\\\.\\pipe\\cua-omb-${process.pid}`;
const preferred = path.join(os.tmpdir(), `cua-omb-${process.pid}.sock`);
return preferred.length < 100 ? preferred : `/tmp/cua-omb-${process.pid}.sock`;
}
async function startEmbedded(binary, signal) {
// The packaged app ships no general node_modules, so a bare package import only works in dev.
const sdk = await loadEmbeddedSdk();
signal.throwIfAborted();
// CUA's embedding contract needs grants before the daemon starts, asked from Electron main so macOS attributes them here.
if (process.platform === "darwin") {
const permissionStatus = sdk.requestMacOSPermissions();
if (!sdk.hasRequiredMacOSPermissions(permissionStatus)) {
const missing = [
!permissionStatus.accessibility && "Accessibility",
!permissionStatus.screenRecording && "Screen Recording",
].filter(Boolean).join(" and ");
throw new Error(`${missing || "macOS permissions"} required; grant access in System Settings and restart OpenMausBot`);
}
}
// The native SDK owns the child lifecycle but exposes no windowsHide option.
const host = sdk.EmbeddedCuaDriverHost.withOptions({
binaryPath: resolveEmbeddedDriverBinary(binary),
hostBundleId: HOST_BUNDLE_ID,
// The SDK's default socket has the start's random id in it; a fixed one stays valid for a bridge that outlives a restart.
socketPath: embeddedSocketPath(),
approveCapabilityManifest: false,
approveSessionPolicy: false,
dangerouslyBypassApprovals: false,
environment: [],
inheritStderr: true,
noOverlay: false,
captureStderr: false,
});
try {
const conn = await host.start({ signal });
signal.throwIfAborted();
embeddedHost = host;
embeddedGeneration = conn.generation;
return {
mode: "embedded",
socketPath: conn.socketPath,
mcpCommand: binary,
mcpArgs: ["mcp", "--embedded", "--socket", conn.socketPath],
mcpEnv: { ...CUA_ENV, CUA_DRIVER_EMBEDDED: "1", CUA_DRIVER_HOST_BUNDLE_ID: HOST_BUNDLE_ID },
};
} catch (err) {
try {
await host.stop();
} catch {
}
host.uniffiDestroy?.();
throw err;
}
}
export async function startCua({ attempt = 0 } = {}) {
if (process.platform === "linux") return (await ensureLinuxRuntime()).initialize();
clearTimeout(retryTimer);
retryTimer = null;
lifecycleGeneration++;
startupAbort?.abort();
startupAbort = new AbortController();
const { signal } = startupAbort;
const binary = resolveDriverBinary();
if (!binary) {
return persistAndNotify({
mode: "unavailable",
reason: "cua-driver binary not found",
});
}
const wantEmbedded =
process.platform === "win32" || app.isPackaged || process.env.OPENMAUSBOT_CUA_EMBEDDED === "1";
let nextConnection;
if (wantEmbedded) {
try {
nextConnection = await startEmbedded(binary, signal);
} catch (err) {
signal.throwIfAborted();
try {
nextConnection = await attachStandalone(signal);
} catch (standaloneError) {
signal.throwIfAborted();
nextConnection = {
mode: "unavailable",
reason: `embedded host failed: ${driverFailure(err)}; standalone launch failed: ${standaloneError?.message ?? standaloneError}`,
};
}
if (!nextConnection) {
nextConnection = {
mode: "unavailable",
reason: `embedded host failed: ${driverFailure(err)}`,
};
}
}
} else if (process.platform === "darwin" && (await socketAlive(STANDALONE_SOCKET))) {
// Never on Windows: the shared pipe belongs to whatever driver the user has, so it is never adopted implicitly.
nextConnection = {
mode: "standalone",
socketPath: STANDALONE_SOCKET,
mcpCommand: binary,
mcpArgs: ["mcp"],
mcpEnv: { ...CUA_ENV },
};
} else {
nextConnection = {
mode: "unavailable",
reason:
"no running cua-driver daemon; run `cua-driver serve` or grant via `cua-driver permissions grant`",
};
}
signal.throwIfAborted();
if (nextConnection.mode === "embedded") watchEmbeddedHost(embeddedHost, embeddedGeneration);
if (nextConnection.mode === "embedded" || nextConnection.mode === "standalone") helperRestarts.started();
if (process.platform === "win32" && nextConnection.mode === "unavailable" && !driverBlocked(nextConnection.reason) && attempt < RETRY_AFTER_MS.length) {
retryTimer = setTimeout(() => { retryTimer = null; void startCua({ attempt: attempt + 1 }).catch(() => {}); }, RETRY_AFTER_MS[attempt]);
retryTimer.unref?.();
}
watchMacDriver(nextConnection);
return persistAndNotify(nextConnection);
}
function watchEmbeddedHost(host, generation) {
const gone = (detail) => {
// An exit we asked for is not a crash: stopCua forgets the host first.
if (host !== embeddedHost) return;
embeddedHost = null;
embeddedGeneration = null;
void host.stop().catch(() => {}).finally(() => host.uniffiDestroy?.());
helperRestarts.exited(detail);
};
void host.waitForExit(generation).then(
(exit) => gone(`code ${exit?.code ?? "none"}`),
(error) => gone(driverFailure(error)),
);
}
/** A Mac driver that stopped answering without exiting, or a standalone one whose exit nothing here sees, counts as one that
* died: the helper's bounded restarts take it from there (cua-restart.mjs), the same as an embedded host that exited. */
function watchMacDriver(connection) {
driverWatch?.stop();
driverWatch = null;
if (process.platform !== "darwin" || (connection.mode !== "embedded" && connection.mode !== "standalone")) return;
const host = connection.mode === "embedded" ? embeddedHost : null;
driverWatch = watchDriver({ alive: () => socketAlive(connection.socketPath), restart: () => {
if (host) {
// Already gone and handled by the exit watch, or replaced since.
if (host !== embeddedHost) return;
embeddedHost = null;
embeddedGeneration = null;
void host.stop().catch(() => {}).finally(() => host.uniffiDestroy?.());
}
helperRestarts.exited("its socket stopped answering");
} });
}
export async function cuaPermissionsStatus() {
const binary = resolveDriverBinary();
if (!binary) return { available: false };
const out = await execFileAsync(binary, ["permissions", "status", "--json"], {
encoding: "utf8",
timeout: 5000,
killSignal: "SIGKILL",
maxBuffer: 65_536,
env: { ...process.env, ...CUA_ENV },
}).catch((error) => ({ stdout: error.stdout }));
try {
return { available: true, ...JSON.parse(out.stdout) };
} catch {
return { available: true, raw: out.stdout?.trim() };
}
}
export async function stopCua() {
helperRestarts.reset();
clearTimeout(retryTimer);
retryTimer = null;
driverWatch?.stop();
driverWatch = null;
lifecycleGeneration++;
startupAbort?.abort();
startupAbort = null;
if (linuxRuntime) {
await linuxRuntime.shutdown();
if (linuxBundleStage) {
cleanupAppImageCuaBundle(linuxBundleStage);
linuxBundleStage = null;
}
return;
}
const host = embeddedHost;
embeddedHost = null;
embeddedGeneration = null;
if (host) {
try {
await host.stop();
host.uniffiDestroy?.();
} catch {
}
}
if (!startupAbort && connectionStore.get()) {
persistAndNotify({ mode: "unavailable", reason: "desktop-host-stopped" });
}
}
/** Starts computer control again on macOS and Windows; on macOS that asks for the permissions it lacks. One restart at a
* time; a later explicit Stop (or quit) cancels it. */
function restartCua() {
const platformLabel = process.platform === "darwin" ? "macOS" : "Windows";
macRetry ??= (async () => {
try {
const stopping = stopCua();
const generation = lifecycleGeneration;
await stopping;
if (generation !== lifecycleGeneration) throw new Error("Computer use restart cancelled");
const connection = await startCua();
const ready = connection?.mode === "embedded" || connection?.mode === "standalone";
return {
enabled: ready,
status: ready ? "ready" : "error",
reasonCode: ready ? undefined : "permissions-required",
message: connection?.reason,
};
} catch (error) {
console.error(`[cua] ${platformLabel} retry failed:`, error);
return {
enabled: false,
status: "error",
reasonCode: "permissions-required",
message: error instanceof Error ? error.message : String(error),
};
}
})().finally(() => { macRetry = null; });
return macRetry;
}
/** The person asked for computer control: started again on macOS and Windows (the Retry button's path), turned on on
* Linux (its Enable button's). */
export async function requestComputerUse() {
if (process.platform === "darwin" || process.platform === "win32") return restartCua();
if (process.platform !== "linux") return null;
helperRestarts.reset();
try {
await (await ensureLinuxRuntime({ recheck: true })).enable();
} catch (error) {
console.error("[cua] Linux enable failed:", error);
}
return (await ensureLinuxRuntime()).getStatus();
}
export function registerCuaIpc() {
ipcMain.handle("cua:connection", localOnly("cua:connection", () => connectionStore.get()));
ipcMain.handle("cua:permissions", localOnly("cua:permissions", () => cuaPermissionsStatus()));
ipcMain.handle("cua:linux-status", localOnly("cua:linux-status", async () =>
process.platform === "linux"
? (await ensureLinuxRuntime()).getStatus()
: { enabled: false, status: "unavailable", reasonCode: "unsupported-platform" },
));
ipcMain.handle("cua:linux-enable", localOnly("cua:linux-enable", async () => {
if (process.platform !== "linux") {
return { enabled: false, status: "unavailable", reasonCode: "unsupported-platform" };
}
helperRestarts.reset();
try {
await (await ensureLinuxRuntime({ recheck: true })).enable();
} catch (error) {
console.error("[cua] Linux enable failed:", error);
}
return (await ensureLinuxRuntime()).getStatus();
}));
ipcMain.handle("cua:linux-disable", localOnly("cua:linux-disable", async () => {
if (process.platform !== "linux") {
return { enabled: false, status: "unavailable", reasonCode: "unsupported-platform" };
}
helperRestarts.reset();
try {
await (await ensureLinuxRuntime()).disable();
} catch (error) {
console.error("[cua] Linux disable failed:", error);
}
return (await ensureLinuxRuntime()).getStatus();
}));
ipcMain.handle("cua:linux-retry", localOnly("cua:linux-retry", async () => {
if (process.platform === "darwin" || process.platform === "win32") return restartCua();
if (process.platform !== "linux") {
return { enabled: false, status: "unavailable", reasonCode: "unsupported-platform" };
}
helperRestarts.reset();
try {
await (await ensureLinuxRuntime({ recheck: true })).retry();
} catch (error) {
console.error("[cua] Linux retry failed:", error);
}
return (await ensureLinuxRuntime()).getStatus();
}));
}