/** * Computer control hosted by the runner CLI: `npx @melaya/runner --computer`. * * On a headless Linux machine or VM (and on any computer without the Melaya * desktop app) the runner itself hosts the computer executor: it starts the * native helper for this platform (desktop/native/computer-linux on Linux; the * Windows / macOS helpers speak the same protocol) and drives it over stdio * exactly as the desktop app's helperProcess.ts does. The protocol is * desktop/native/PROTOCOL.md (v1): * * helper -> {"event":"ready",...} first line * host -> {"id","op":"init","denyPatterns","selfPids"} <- {"id","ok":true} * host -> {"id","op":"capabilities"} <- {"id","capabilities"} * host -> {"id","op":"ping"} <- {"id","pong":true,"busy","current"} * host -> {"id","op":"command","action","allowedAppIds","allowedApps","deadlineMs"} <- {"id","result"} * host -> {"op":"cancel","target?"} / {"op":"shutdown"} * helper -> events: target, user_input, log * * Like the desktop app, this host runs `batch` (step by step, per-step * `expect`), `keep_awake` (systemd-inhibit / caffeinate) and `handoff` (no one * is at a headless screen: unsupported) itself, and never sends them to the * helper. Real user input (a VNC / console user) PAUSES a run instead of * stopping it (2026-10-23, computerPause.ts, the same engine as the desktop * app): held input is let go, a pointer stream is cancelled, the run waits * until the user is idle, checks the window and continues where it was. * * computerRelay.ts (server socket <-> executor) loads this module lazily and * calls createCliComputerHost({ send, verbose }) when the runner is not inside * the desktop app. It returns null unless the CLI was started with --computer * (enableCliComputerHost) or MELAYA_COMPUTER=1. * * Before anything reaches the helper this file enforces what the desktop app's * bridge does (the runner cannot import desktop/): known kinds, batch limits, * the allowlist, per-app tiers (the server's requiredTierOf, * server/src/services/computer/computerEffects.ts) and a hard-deny list of apps * no agent may touch (Melaya itself, authentication prompts, lock screens). The * helper enforces the allowlist and tiers again per window. * * Helper binary: --computer-helper or MELAYA_COMPUTER_HELPER, else * /native/-/, else the per-user cache * ~/.melaya/computer//-/ where a pinned release (url + * sha256 in HELPER_MANIFEST) is downloaded and verified. Types MIRROR * packages/desktop-contract/src/computer.ts (this package cannot import it). */ import { type ChildProcess } from "child_process"; /** COMPUTER_POINTER_KINDS: in a batch, these steps skip their own after-step observation except the last one. */ export declare const QUIET_POINTER_KINDS: ReadonlySet; export type ComputerAppTier = "view" | "click" | "full"; export interface AllowedApp { appId: string; tier: ComputerAppTier; } export type ComputerRunnerToHost = { type: "computer.command"; id: string; action: unknown; deadlineMs: number; allowedAppIds: string[]; allowedApps?: AllowedApp[]; pauseBudgetMs?: number; } | { type: "computer.stop"; reason: string; } | { type: "computer.capabilities.request"; }; export type ComputerHostToRunner = { type: "computer.result"; id: string; result: ComputerResultLike; } | { type: "computer.capabilities"; capabilities: ComputerCapabilitiesLike | null; }; export type ComputerResultLike = Record & { ok: boolean; kind: string; elapsedMs: number; }; export type ComputerCapabilitiesLike = Record & { platform: string; backend: string; kinds: string[]; host?: "desktop" | "cli"; }; export declare const COMPUTER_ACTION_KINDS: readonly ["screen", "screenshot", "find", "get_text", "current_app", "list_windows", "list_apps", "list_displays", "clipboard_read", "ocr", "wait", "hover", "move", "scroll", "focus_window", "open_app", "open_url", "open_file", "click", "type", "press", "drag", "select", "set_value", "toggle", "window", "clipboard_write", "file_dialog", "batch", "zoom", "cursor_position", "read_table", "expect", "mouse", "hold_key", "select_text", "quit_app", "save_dialog", "handoff", "keep_awake", "path"]; /** Kinds that do not act on an app window: an empty allowlist does not block them. */ export declare const APP_INDEPENDENT_KINDS: Set; /** Steps per batch: a whole drawing in one call (raised from 100 on 2026-10-23). */ export declare const COMPUTER_MAX_BATCH_STEPS = 5000; /** The lowest app tier that allows this action (same rule as the server and the helpers). */ export declare function requiredTierOf(action: Record): ComputerAppTier; /** * Apps no agent may read or drive, whatever the allowlist says. Sent to the * helper at init (it adds them to its own built-in list) and checked here for * actions that name an app and for app / window lists coming back. Plain * regexes valid in JS, .NET and Rust. */ export declare const HARD_DENY_SOURCES: string[]; export declare function isHardDenied(appId: string): boolean; export type PolicyCheck = { ok: true; action: Record; allowedApps: AllowedApp[]; allowedAppIds: string[]; deadlineMs: number; } | { ok: false; result: ComputerResultLike; }; /** * Everything the CLI host checks before a command reaches the helper: shape, * kinds, batch limits, allowlist (minus hard-denied apps), the tier ceiling and * the app an action names. Returns the sanitized allowlist to forward. */ export declare function checkCommand(msg: { action?: unknown; allowedAppIds?: unknown; allowedApps?: unknown; deadlineMs?: unknown; }): PolicyCheck; /** * Pinned helper releases, one per platform-arch, from computerHelperManifest.ts: * generated by the release job (desktop-release.yml, * .github/scripts/write-computer-helper-manifest.mjs) with the published URL * and its sha256, and committed with the release. A download is only ever * accepted when its sha256 matches. Empty until the first release: use * MELAYA_COMPUTER_HELPER or --computer-helper meanwhile. */ export declare const HELPER_VERSION = "1.2.1"; export declare const HELPER_MANIFEST: Record; export declare function platformKey(platform?: string, arch?: string): string; export declare function helperCacheDir(env?: NodeJS.ProcessEnv): string; /** Finds the helper binary on disk (no download). */ export declare function resolveHelperPath(opts?: { helperPath?: string; env?: NodeJS.ProcessEnv; exists?: (p: string) => boolean; }): string | null; /** Downloads the pinned helper for this platform into the per-user cache and verifies its sha256. */ export declare function downloadHelper(opts?: { env?: NodeJS.ProcessEnv; fetchFn?: typeof fetch; log?: (l: string) => void; manifest?: typeof HELPER_MANIFEST; }): Promise; export declare const HELPER_MISSING_MESSAGE = "The computer helper is not installed on this machine. Build it (desktop/native/computer-linux: ./build.sh) and start the runner with MELAYA_COMPUTER_HELPER=/path/to/melaya-computer, or pass --computer-helper."; export type SpawnFn = (command: string, args: string[], options: { stdio: ["pipe", "pipe", "pipe"]; env?: NodeJS.ProcessEnv; }) => ChildProcess; /** Splits a byte stream into JSON objects, one per line; tolerant of partial chunks, CRLF and noise. */ export declare class LineDecoder { private buf; push(chunk: string, onObject: (o: Record) => void, onNoise?: (line: string) => void): void; } export interface HelperOptions { spawn?: SpawnFn; env?: NodeJS.ProcessEnv; log?: (l: string) => void; onEvent?: (e: Record) => void; /** How long a cancelled command may take to answer before the helper is killed (PROTOCOL.md ยง5). */ abortGraceMs?: number; } export declare class HelperProcess { private readonly path; private readonly opts; private child; private waiters; private aborted; private seq; private restarts; private disposed; private starting; private readyResolve; ready: Record | null; constructor(path: string, opts?: HelperOptions); private log; start(): Promise; private onMessage; private onExit; get crashLooping(): boolean; /** Writes one request and waits for the line with the same id. */ request(msg: Record, timeoutMs: number): Promise>; /** Fire-and-forget line. */ send(msg: Record): void; /** * Aborts one command (or every one). A command that has not answered `stopped` * within the grace period gets the helper killed; it restarts on the next command. */ /** Releases every key / button the helper holds (helper 1.2.0+; older helpers ignore the op). */ release(): void; /** * Cancels one command for a pause (pause and resume): unlike cancel(), its `stopped` reply (with the path's * `progress`) still answers the request, so the host knows how far it got and continues from there. */ pauseCancel(target: string): void; cancel(target?: string): void; get running(): boolean; dispose(): void; } /** Holds a sleep inhibitor child process: systemd-inhibit (Linux) or caffeinate (macOS). */ export declare class KeepAwake { private readonly opts; private child; constructor(opts?: { spawn?: SpawnFn; env?: NodeJS.ProcessEnv; platform?: string; }); available(): string | null; get active(): boolean; acquire(): string | null; release(): void; } export interface CliComputerHostOptions { /** Results / capabilities pushed outside a handle() call (the relay's executor callback). */ send?: (msg: ComputerHostToRunner) => void; verbose?: boolean; /** Path to the helper binary (else MELAYA_COMPUTER_HELPER, the package's native/ folder, the per-user cache). */ helperPath?: string; /** Force on / off (else enableCliComputerHost() or MELAYA_COMPUTER=1). */ enabled?: boolean; /** Download the pinned helper when none is installed (default: true). */ download?: boolean; env?: NodeJS.ProcessEnv; spawn?: SpawnFn; log?: (line: string) => void; /** Test seam for keep_awake. */ keepAwake?: KeepAwake; abortGraceMs?: number; } export interface CliComputerHostApi { handle(msg: ComputerRunnerToHost): Promise; capabilities(): Promise; stopAll(reason: string): void; dispose(): void; } /** Called by cli.ts for `--computer`: later createCliComputerHost() calls (from the relay) are enabled. */ export declare function enableCliComputerHost(opts?: Partial): void; export declare function isCliComputerEnabled(env?: NodeJS.ProcessEnv): boolean; /** Test seam: forget enableCliComputerHost(). */ export declare function _resetCliComputerHost(): void; export declare function createCliComputerHost(options?: CliComputerHostOptions): CliComputerHostApi | null;