import { type ElevatedEndpoint } from "@aicommander/priv-helper"; import type { CommandHandlers, RunningCommand } from "./executor.js"; /** * Run ONE relay-signed elevated command via the privileged helper. This is a pure * local-IPC RELAY: it NEVER runs anything itself and NEVER falls back to * unprivileged exec on any error path. It opens the helper endpoint, hands over * the opaque signed capability, and streams the helper's result back through the * same CommandHandlers contract as executeCommand so connection.ts routes it * identically. * * `commandId` MUST equal the signed capability.requestId — the helper cross-checks * them and rejects a mismatch. `opts.endpoint` exists SOLELY so tests can point at * a temp socket; production callers omit it (default = elevatedEndpoint()). * * `opts.onBootId` (optional) fires once with the helper's live per-boot nonce as * soon as `hello-ok` arrives — BEFORE `exec` is sent. connection.ts uses it to * detect a helper restart (a bootId that differs from the one last advertised to * the relay) and re-register with the fresh nonce, so elevated exec self-heals * instead of staying dead until the next reconnect/reauth. * * Fail-closed everywhere: a null endpoint, a connection failure (no helper * listening), a version-skew handshake, a decode error, or a premature close all * surface as a terminal onError and never spawn a process. */ export declare function executeElevatedCommand(capability: string, commandId: string, handlers: CommandHandlers, opts?: { endpoint?: ElevatedEndpoint | null; onBootId?: (bootId: string) => void; }): RunningCommand; /** * What ONE handshake attempt saw. Everything except `ok` is still a refusal — * this type exists so the caller can EXPLAIN the refusal, never to soften it. * * - `ok` — `hello-ok` with a usable bootId. * - `protocol_mismatch` — it answered, speaking a version we will not speak. * Distinguished because the remedy is "finish the * half-applied upgrade and reboot", not "install". * - `refused` — the SAME SKEW SEEN FROM THE OTHER END: it answered our * `hello` with an `error` frame and hung up. A helper too * old to speak OUR version replies exactly this * (priv-helper/src/helper.ts; its other `error` cases * need a second frame or a completed handshake, neither * of which a bare `hello` reaches), so a half-applied * upgrade lands here rather than under `unreachable`. * Its own kind and not folded into `protocol_mismatch` * because the two shapes establish different facts: a * `hello-ok` names a version, an `error` frame names * nothing at all — it carries no bootId and nothing else * that says WHO sent it. Recorded as a SHAPE, never as * the peer's `message` text: that string comes from * whatever is on the endpoint, and this outcome is read * by things that end up in a report users forward. The * agent-side twin of `EndpointObservation.refused` * (priv-helper/src/doctor-endpoint.ts). * - `unreachable` — nothing answered: no endpoint, a connect error, a * timeout, a malformed or nonsensical handshake, a close. * Collapsed on purpose: they are all "not running here". */ export type HelperProbeOutcome = { kind: "ok"; bootId: string; } | { kind: "protocol_mismatch"; } | { kind: "refused"; } | { kind: "unreachable"; }; /** * Probe the privileged helper's current per-boot nonce (`bootId`) by performing * ONLY the handshake: connect → send `hello` → await `hello-ok` → resolve its * bootId. Used at register time so the agent can advertise the nonce the relay * must bind into every capability (machine + boot binding) AND so it can confirm * the helper is actually reachable before advertising elevatedExec. * * Fail-closed: every failure resolves a NON-`ok` outcome — no endpoint on this * platform, a connect error (ECONNREFUSED / ENOENT / EPERM when the helper isn't * listening or the pipe is unreachable), a ~2s timeout, a version-skew or * otherwise malformed handshake, a refusal, or a premature close. The socket is * ALWAYS destroyed before we resolve. Anything but `ok` means "don't advertise * elevatedExec". */ export declare function probeHelperHello(opts?: { endpoint?: ElevatedEndpoint | null; timeoutMs?: number; }): Promise; /** * Why discovery could not produce a helper. Every one of these is a REFUSAL that * behaves exactly as a bare `null` did — the value is only ever read to say * something true about the machine (see elevated-availability.ts). * * - `unreachable` — no candidate answered after every attempt. * - `protocol_mismatch` — the only answer(s) came back on a version we will not * speak, and nothing usable answered anywhere. BOTH * probe shapes of that skew land here — a `hello-ok` * announcing a version we do not speak * (`protocol_mismatch`) and an `error` frame refusing * ours (`refused`) — because they are one state of the * machine seen from whichever side is older, with one * remedy. The distinction the probe keeps is about what * each shape ESTABLISHES, not about what to do; this * word also goes on the wire as an * ELEVATED_UNAVAILABLE_REASON (@aicommander/protocol), * so it is a vocabulary, not a free-text field. * - `conflict` — endpoints answered with DIFFERENT bootIds, so at least * one answer is not the helper. */ export type HelperDiscoveryFailure = "unreachable" | "protocol_mismatch" | "conflict"; /** * Locate the reachable privileged helper among this platform's candidate endpoints * (Windows has a POOL of loopback ports; the helper binds EVERY free one). Probes * ALL candidates via probeHelperHello, then: * * - every answer carries the SAME bootId (the genuine helper on its bound ports) * → return that bootId + the first answering endpoint; the caller pins the * endpoint for subsequent execs so it doesn't re-scan the pool every command; * - answers DISAGREE on bootId → fail closed (`conflict`). Since a live helper * owns every pool port it could bind, a second distinct bootId means an * endpoint the helper does NOT own is answering the handshake — a local * squatter posing as the helper (it grabbed a port while the helper was down). * Refusing here downgrades that squat to a DoS (elevated unavailable) instead * of handing the squatter a signed capability. The 60s reconciler retries, so * a transient disagreement (helper restarting mid-scan) self-corrects. * * `attempts` retries the WHOLE list a few times with a short fixed delay: at * register time a single transient probe failure (I/O storm right after boot, * helper still finishing its bind) would otherwise wrongly advertise * elevatedExec:false and disable elevated exec until the next reconnect. * Fail-closed: refuses when no candidate answers after every attempt (or the * platform has no endpoints). * * THE ONLY DISCOVERY THERE IS. It used to have a `discoverHelper()` twin that * threw the cause away and returned `bootId | null`; nothing but its own test * called it once availability reasons existed, and a fail-closed rule with two * implementations is one that can start disagreeing with itself. The cause is * carried, and every caller that does not want it ignores it. */ export declare function discoverHelperDetailed(opts?: { endpoints?: ElevatedEndpoint[]; timeoutMs?: number; attempts?: number; retryDelayMs?: number; }): Promise<{ ok: true; bootId: string; endpoint: ElevatedEndpoint; } | { ok: false; cause: HelperDiscoveryFailure; }>;