Spaces:
Runtime error
Runtime error
| /** | |
| * 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 }); | |
| } | |