Spaces:
Runtime error
Runtime error
File size: 5,317 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | /**
* 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 });
}
|