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 });
}