ai_api / src /lib /headroom /detect.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
5.32 kB
/**
* Headroom proxy detection helpers.
*
* Ported from upstream 9router (decolua/9router @ b55cf36d + 50ed79fe).
* Original authors: decolua, Carmelo Campos (@carmelogunsroses), Cursor.
*
* Headroom is the optional third-party token-saver proxy (headroom-ai
* Python CLI). OmniRoute can either:
* 1. Manage a local proxy lifecycle (loopback URL β†’ start/stop from the
* dashboard via `process.ts`).
* 2. Use an external Docker sidecar proxy (non-loopback HEADROOM_URL).
* In this case we only probe /health; start/stop are NOT exposed.
*
* All functions here are pure / side-effect-free where possible so they can
* be unit-tested without spawning processes.
*/
import { execFileSync } from "node:child_process";
const EXTRA_BINS = ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin"];
const EXTENDED_PATH = [...EXTRA_BINS, process.env.PATH || ""].filter(Boolean).join(":");
const PYTHON_CANDIDATES = [
"python3.13",
"python3.12",
"python3.11",
"python3.10",
"python3",
"python",
];
const MIN_VERSION: readonly [number, number] = [3, 10];
const HEADROOM_HEALTH_TIMEOUT_MS = 1500;
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
export const DEFAULT_HEADROOM_URL = process.env.HEADROOM_URL || "http://localhost:8787";
export interface HeadroomStatus {
installed: boolean;
path: string | null;
running: boolean;
python: string | null;
localUrl: boolean;
canStart: boolean;
}
export interface BuildHeadroomStatusInput {
url: string;
binaryPath: string | null;
python: string | null;
proxyReachable: boolean;
}
// ──────────────── Pure helpers (unit-testable) ────────────────
export function isLoopbackHeadroomUrl(url: string): boolean {
try {
const parsed = new URL(url);
return LOOPBACK_HOSTS.has(parsed.hostname);
} catch {
return false;
}
}
export function parsePortFromHeadroomUrl(url: string): number | null {
try {
const u = new URL(url);
if (!u.port) return null;
const p = parseInt(u.port, 10);
if (Number.isFinite(p) && p > 0 && p < 65536) return p;
} catch {
// fall through
}
return null;
}
/**
* Assemble the headroom status payload from already-resolved inputs.
* Kept pure so unit tests can exercise every branch without spawning
* processes or hitting the network.
*
* - `running` reflects /health reachability regardless of local CLI presence
* (pair commit 50ed79fe β€” Docker sidecar support).
* - `canStart` is only true for a loopback URL with the CLI installed; we
* never spawn against a non-loopback URL.
*/
export function buildHeadroomStatus(input: BuildHeadroomStatusInput): HeadroomStatus {
const installed = Boolean(input.binaryPath);
const localUrl = isLoopbackHeadroomUrl(input.url);
return {
installed,
path: input.binaryPath,
running: input.proxyReachable,
python: input.python,
localUrl,
canStart: installed && localUrl,
};
}
// ──────────────── Side-effecting probes ────────────────
export function findHeadroomBinary(): string | null {
try {
// execFileSync (no shell): "which" is invoked directly with "headroom" as an
// arg, so even if some upstream caller passes attacker-controlled input the
// shell metacharacters cannot reach a shell parser.
const out = execFileSync("which", ["headroom"], {
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true,
env: { ...process.env, PATH: EXTENDED_PATH },
})
.toString()
.trim();
return out || null;
} catch {
return null;
}
}
export function findPython310(): string | null {
for (const candidate of PYTHON_CANDIDATES) {
try {
// candidate is from a fixed allowlist (PYTHON_CANDIDATES) above β€” no
// user input β€” but use execFileSync anyway to remove the shell entirely.
const ver = execFileSync(candidate, ["--version"], {
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true,
env: { ...process.env, PATH: EXTENDED_PATH },
})
.toString()
.trim();
const match = ver.match(/(\d+)\.(\d+)/);
if (!match) continue;
const major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10);
if (major > MIN_VERSION[0] || (major === MIN_VERSION[0] && minor >= MIN_VERSION[1])) {
return candidate;
}
} catch {
// try next candidate
}
}
return null;
}
export async function probeProxyRunning(url: string): Promise<boolean> {
if (!url) return false;
const base = String(url).replace(/\/$/, "");
try {
const res = await fetch(`${base}/health`, {
signal: AbortSignal.timeout(HEADROOM_HEALTH_TIMEOUT_MS),
});
return res.ok;
} catch {
return false;
}
}
/**
* Aggregated dashboard status. Composes the three probes above with the
* pure builder so the I/O happens in one place.
*/
export async function getHeadroomStatus(url: string): Promise<HeadroomStatus> {
const binaryPath = findHeadroomBinary();
const python = findPython310();
const proxyReachable = await probeProxyRunning(url);
return buildHeadroomStatus({ url, binaryPath, python, proxyReachable });
}