From c7718a8e2c19bbf9fd564f63f875fd5a910bfa71 Mon Sep 17 00:00:00 2001 From: pai-scaffolde Date: Fri, 11 Sep 2026 17:09:16 -0700 Subject: [PATCH] fix(pulse): Arbol scanners are configured by their worker keys, not by config.yaml existing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bunker.ts arbolConfig() treated the presence of ~/.config/arbol/config.yaml (subdomain + auth_token — Arbol's general config) as proof that a security scanner and a site-health worker exist, defaulting their names when security_worker / site_health_worker were absent. fetchArbol() then hit https://infra-security..workers.dev/report, got a 404, and reported the scanner as `unreachable`: /api/bunker/arbol and /sitehealth answered 502, /critical said configured:true reachable:false, and the panel rendered a red "scanner unreachable" face on installs that simply have no scanner. The module already has the right state for that — `{ state: "not-configured" }` — it just could not reach it. A worker exists only when its key is set: drop the name defaults, make the two fields optional, and gate fetchArbol()/siteHealthReport() on the key (503 "not configured", the same shape arbolReport already used for a missing config). Installs with the keys set take exactly the old path; monitorWorkersRe() already filtered undefined names. Co-Authored-By: Claude Fable 5.1 --- LifeOS/install/LIFEOS/PULSE/modules/bunker.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/LifeOS/install/LIFEOS/PULSE/modules/bunker.ts b/LifeOS/install/LIFEOS/PULSE/modules/bunker.ts index 74654f28ad..9f4a5a7538 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/bunker.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/bunker.ts @@ -145,18 +145,19 @@ let pentestCache: { at: number; online: boolean } | null = null; // private-infrastructure identifiers (G18). `pentest_worker` was already // config-sourced; `security_worker` and `site_health_worker` joined it after // Max's 7.31.5 audit found their kebab-case names hardcoded two lines below a -// comment claiming exactly this property. Defaults are generic shapes, not -// this install's names: an install with different names sets the keys. -const DEFAULT_SECURITY_WORKER = "infra-security"; -const DEFAULT_SITE_HEALTH_WORKER = "site-health"; -function arbolConfig(): { subdomain: string; token: string; pentestWorker?: string; securityWorker: string; siteHealthWorker: string } | null { +// comment claiming exactly this property. A worker exists only when its key is +// set: ~/.config/arbol/config.yaml is Arbol's general config, so its presence +// says nothing about these two scanners, and defaulting their names turned +// every Arbol install without them into "scanner unreachable" (a 502) instead +// of the `not-configured` state the panel already knows how to render. +function arbolConfig(): { subdomain: string; token: string; pentestWorker?: string; securityWorker?: string; siteHealthWorker?: string } | null { try { const y = readFileSync(join(HOME, ".config", "arbol", "config.yaml"), "utf8"); const subdomain = y.match(/^subdomain:\s*"?([^"\n]+)"?/m)?.[1]?.trim(); const token = y.match(/^auth_token:\s*"?([^"\n]+)"?/m)?.[1]?.trim(); const pentestWorker = y.match(/^pentest_worker:\s*"?([^"\n]+)"?/m)?.[1]?.trim(); - const securityWorker = y.match(/^security_worker:\s*"?([^"\n]+)"?/m)?.[1]?.trim() || DEFAULT_SECURITY_WORKER; - const siteHealthWorker = y.match(/^site_health_worker:\s*"?([^"\n]+)"?/m)?.[1]?.trim() || DEFAULT_SITE_HEALTH_WORKER; + const securityWorker = y.match(/^security_worker:\s*"?([^"\n]+)"?/m)?.[1]?.trim(); + const siteHealthWorker = y.match(/^site_health_worker:\s*"?([^"\n]+)"?/m)?.[1]?.trim(); if (!subdomain || !token) return null; return { subdomain, token, pentestWorker, securityWorker, siteHealthWorker }; } catch { return null; } @@ -176,7 +177,7 @@ type ArbolFetch = async function fetchArbol(): Promise { if (arbolCache && Date.now() - arbolCache.at < 5 * 60_000) return { state: "ok", report: arbolCache.report }; const cfg = arbolConfig(); - if (!cfg) return { state: "not-configured" }; + if (!cfg?.securityWorker) return { state: "not-configured" }; try { const r = await fetch(`https://${cfg.securityWorker}.${cfg.subdomain}.workers.dev/report`, { headers: { Authorization: `Bearer ${cfg.token}` }, @@ -247,7 +248,7 @@ let siteHealthCache: { at: number; body: unknown } | null = null; async function siteHealthReport(): Promise { if (siteHealthCache && Date.now() - siteHealthCache.at < 60_000) return Response.json(siteHealthCache.body); const cfg = arbolConfig(); - if (!cfg) return Response.json({ error: "arbol not configured (~/.config/arbol/config.yaml)" }, { status: 503 }); + if (!cfg?.siteHealthWorker) return Response.json({ error: "site-health worker not configured (site_health_worker in ~/.config/arbol/config.yaml)" }, { status: 503 }); try { const r = await fetch(`https://${cfg.siteHealthWorker}.${cfg.subdomain}.workers.dev/status`, { headers: { Authorization: `Bearer ${cfg.token}` }, @@ -378,7 +379,7 @@ async function arbolReport(): Promise { const res = await fetchArbol(); if (res.state !== "ok") { return Response.json( - { error: res.state === "unreachable" ? `arbol worker unreachable: ${res.reason}` : "arbol not configured (~/.config/arbol/config.yaml)" }, + { error: res.state === "unreachable" ? `arbol worker unreachable: ${res.reason}` : "security scanner not configured (security_worker in ~/.config/arbol/config.yaml)" }, { status: res.state === "unreachable" ? 502 : 503 }, ); }