evOo-local/packages/adapters/src/linux-desktop.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

225 lines
7.9 KiB
TypeScript

import { randomUUID } from "node:crypto";
import type {
AdapterContext,
ComputerAction,
ComputerActionRequest,
ComputerInput,
ComputerRef,
ScreenRequest,
ScreenSession,
} from "@rakazo/adapter-kit";
import {
BROWSER_APPLICATIONS,
browserLauncherPath,
browserProfilePathForScreen,
type DesktopEnvironment,
desktopControlCommand,
desktopUrl,
managedDesktopCommand,
releaseDesktopCommand,
screenPorts,
shellQuote,
stopAllDesktopBrowsersCommand,
} from "@rakazo/core/node/desktop-runtime";
import { ComputerScreenUnavailableError, screenSessionKey } from "./computer-screens.js";
import {
boundedComputerActions,
clampRounded,
computerObservation,
workspacePath,
} from "./computer-support.js";
import {
extraDisplayActionCommand,
extraDisplayInputCommand,
observeExtraDisplayCommand,
parseExtraDisplayObservation,
} from "./extra-displays.js";
export interface LinuxDesktopHost {
environment(computer: ComputerRef): Promise<DesktopEnvironment>;
run(
computer: ComputerRef,
command: string,
context: AdapterContext,
): Promise<{ code: number; stdout: string; stderr?: string }>;
screenUrl(computer: ComputerRef, port: number, context: AdapterContext): Promise<string>;
}
/** Provider SDKs supply only command execution, workspace paths, and protected port URLs. */
export class LinuxDesktop {
constructor(private readonly host: LinuxDesktopHost) {}
private async run(computer: ComputerRef, command: string, context: AdapterContext) {
context.signal.throwIfAborted();
const result = await this.host.run(computer, command, context);
if (result.code === 75) throw new ComputerScreenUnavailableError();
if (result.code !== 0) throw new Error(result.stderr || "computer desktop operation failed");
return result.stdout;
}
private async ensure(computer: ComputerRef, context: AdapterContext) {
const env = await this.host.environment(computer);
const key = screenSessionKey(context);
const output = await this.run(
computer,
managedDesktopCommand(key, context.screenLeaseId, env, randomUUID()),
context,
);
const match = output.match(/RAKAZO_DESKTOP=(\d+):([a-zA-Z0-9_-]+)/);
if (!match) throw new ComputerScreenUnavailableError();
const index = Number(match[1]);
const ports = screenPorts(index, env);
const layout = {
...ports,
viewPort: Number(ports.viewPort),
controlPort: Number(ports.controlPort),
};
return { env, key, layout, token: match[2]! };
}
async connectScreen(
computer: ComputerRef,
request: ScreenRequest,
context: AdapterContext,
): Promise<ScreenSession> {
const screen = await this.ensure(computer, context);
let token = screen.token;
if (request.interactive) {
if (!request.controlToken) throw new Error("interactive screen requires a control token");
token = request.controlToken;
await this.run(
computer,
desktopControlCommand(screen.key, context.screenLeaseId, screen.env, true, token),
context,
);
}
const url = await this.host.screenUrl(
computer,
request.interactive ? screen.layout.controlPort : screen.layout.viewPort,
context,
);
return { url: desktopUrl(url, token), mimeType: "text/html", close: async () => undefined };
}
async setScreenControl(
computer: ComputerRef,
interactive: boolean,
context: AdapterContext,
token?: string,
) {
if (!token) {
if (interactive) throw new Error("interactive screen requires a control token");
return;
}
const env = interactive
? (await this.ensure(computer, context)).env
: await this.host.environment(computer);
await this.run(
computer,
desktopControlCommand(
screenSessionKey(context),
context.screenLeaseId,
env,
interactive,
token,
),
context,
);
}
async observe(computer: ComputerRef, context: AdapterContext) {
const { layout } = await this.ensure(computer, context);
return this.observeLayout(computer, layout, context);
}
private async observeLayout(
computer: ComputerRef,
layout: { display: string; displayNumber: number },
context: AdapterContext,
) {
const output = await this.run(computer, observeExtraDisplayCommand(layout), context);
const observed = parseExtraDisplayObservation(output);
return computerObservation(observed.image, {
mimeType: "image/png",
width: 1280,
height: 800,
cursor: observed.cursor,
});
}
async sendInput(computer: ComputerRef, input: ComputerInput, context: AdapterContext) {
const { layout } = await this.ensure(computer, context);
await this.run(computer, extraDisplayInputCommand(layout, input), context);
}
async act(computer: ComputerRef, request: ComputerActionRequest, context: AdapterContext) {
const { layout, env, key } = await this.ensure(computer, context);
let completed = 0;
for (const action of boundedComputerActions(request.actions)) {
await this.run(
computer,
`export CHROME_USER_DATA_DIR=${shellQuote(browserProfilePathForScreen(key, env))} BROWSER=${browserLauncherPath(layout.displayNumber)}\n${browserActionCommand(action, layout, env)}`,
context,
);
completed += 1;
}
if (request.settleMs)
await this.run(computer, `sleep ${clampRounded(request.settleMs, 0, 5000) / 1000}`, context);
return {
completed,
...(request.observe === false
? {}
: { observation: await this.observeLayout(computer, layout, context) }),
};
}
async stopBrowsers(computer: ComputerRef, context: AdapterContext) {
const env = await this.host.environment(computer);
await this.run(computer, stopAllDesktopBrowsersCommand(env), context);
}
async releaseScreen(computer: ComputerRef, context: AdapterContext) {
const env = await this.host.environment(computer);
// A stale release is a successful no-op. All other errors must retain the slot for retry.
const result = await this.host.run(
computer,
releaseDesktopCommand(screenSessionKey(context), context.screenLeaseId, env),
context,
);
if (result.code !== 0 && result.code !== 75)
throw new Error(result.stderr || "computer desktop failed to stop");
}
}
function browserActionCommand(
action: ComputerAction,
layout: Parameters<typeof extraDisplayActionCommand>[0],
env: DesktopEnvironment,
) {
const browser =
action.kind === "open" && /^https?:\/\//i.test(action.path)
? action.path
: action.kind === "launch" && BROWSER_APPLICATIONS.has(action.application.toLowerCase())
? (action.uri ?? "about:blank")
: undefined;
if (browser !== undefined) {
return `nohup ${browserLauncherPath(layout.displayNumber)} ${shellQuote(browser)} </dev/null >/tmp/rakazo/browser-open-${layout.displayNumber}.log 2>&1 &`;
}
const workspace = env.workspaceDir;
return `cd ${shellQuote(workspace)}\n${extraDisplayActionCommand(layout, action.kind === "open" ? { ...action, path: workspacePath(workspace, action.path) } : action)}`;
}
/** Install the same X11 tools in minimal Ubuntu sandboxes, only if their image lacks them. */
export const PREPARE_LINUX_DESKTOP = [
"set -eu",
'missing=""',
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion
'for pair in python3:python3 flock:util-linux Xvfb:xvfb xdpyinfo:x11-utils x11vnc:x11vnc fluxbox:fluxbox xdotool:xdotool scrot:scrot; do command -v "${pair%%:*}" >/dev/null 2>&1 || missing="$missing ${pair#*:}"; done',
'if ! command -v websockify >/dev/null 2>&1 && [ ! -x /opt/noVNC/utils/websockify/run ]; then missing="$missing websockify"; fi',
'if [ ! -d /usr/share/novnc ] && [ ! -d /opt/noVNC ]; then missing="$missing novnc"; fi',
'if [ -n "$missing" ]; then',
' if [ "$(id -u)" -eq 0 ]; then root=""; else root="sudo -n"; fi',
" $root apt-get update -qq && $root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $missing",
"fi",
].join("\n");