diff --git a/src/mitm/dns/provision.ts b/src/mitm/dns/provision.ts new file mode 100644 index 0000000000..544207dbb8 --- /dev/null +++ b/src/mitm/dns/provision.ts @@ -0,0 +1,92 @@ +/** + * AgentBridge DNS provisioning — best-effort, extracted from manager.ts so each step + * is guarded and unit-testable without spawning the MITM server (#6127 / #6198). + */ + +import { addDNSEntry, addDNSEntries } from "./dnsConfig.ts"; +import { ALL_TARGETS } from "../targets/index.ts"; +import { getAllAgentBridgeStates } from "@/lib/db/agentBridgeState.ts"; +import { listCustomHosts } from "@/lib/db/inspectorCustomHosts.ts"; +import { createLogger } from "@/shared/utils/logger.ts"; + +const defaultLog = createLogger("mitm-dns-provision"); + +/** Minimal logger shape used by {@link provisionDnsEntries} (injectable for tests). */ +interface DnsProvisionLogger { + error: (payload: unknown, msg: string) => void; + info: (payload: unknown, msg?: string) => void; +} + +/** Injectable dependencies for {@link provisionDnsEntries} (all default to the real ones). */ +export interface DnsProvisionDeps { + addDefaultDns?: (sudoPassword: string) => Promise; + addHostsDns?: (hosts: string[], sudoPassword: string) => Promise; + getAgentStates?: () => ReturnType; + listEnabledCustomHosts?: () => ReturnType; + logger?: DnsProvisionLogger; +} + +/** + * Provision every AgentBridge DNS entry (Antigravity defaults + agents with + * `dns_enabled=true` + enabled custom hosts). **Every step is best-effort**: a failure + * is logged with the full `err` — which carries the privileged command's stderr + * (`systemCommands.ts` folds stderr into the Error message) — and never aborts the + * bridge start. + * + * Previously the default step (`addDNSEntry`) was called unguarded while the two + * sibling steps and cert install were wrapped, so in containers/headless (Docker + * `USER node`, no `sudo`, read-only /etc/hosts) it threw out of `startMitmInternal` + * and killed the whole start (#6127); its stderr also never reached app.log — only a + * bare exit code hit the toast (#6198). Extracting + guarding all three steps here + * restores the symmetry and makes the behavior unit-testable without spawning the + * MITM server. + */ +export async function provisionDnsEntries( + sudoPassword: string, + deps: DnsProvisionDeps = {} +): Promise { + const addDefaultDns = deps.addDefaultDns ?? addDNSEntry; + const addHostsDns = deps.addHostsDns ?? addDNSEntries; + const getAgentStates = deps.getAgentStates ?? getAllAgentBridgeStates; + const listEnabledCustomHosts = + deps.listEnabledCustomHosts ?? (() => listCustomHosts({ enabledOnly: true })); + const logger = deps.logger ?? defaultLog; + + // Antigravity default hosts. + try { + await addDefaultDns(sudoPassword); + } catch (err) { + logger.error({ err }, "Failed to add default DNS entries (continuing)"); + } + + // Collect hosts from agents that have dns_enabled=true in the DB. + try { + const agentStates = getAgentStates(); + const agentHostsToAdd: string[] = []; + for (const state of agentStates) { + if (!state.dns_enabled) continue; + const target = ALL_TARGETS.find((t) => t.id === state.agent_id); + if (target) { + agentHostsToAdd.push(...target.hosts); + } + } + if (agentHostsToAdd.length > 0) { + logger.info({ count: agentHostsToAdd.length }, "Adding DNS for agent host(s)..."); + await addHostsDns(agentHostsToAdd, sudoPassword); + } + } catch (err) { + logger.error({ err }, "Failed to add agent DNS entries (continuing)"); + } + + // Collect enabled custom hosts. + try { + const customHosts = listEnabledCustomHosts(); + const customHostNames = customHosts.map((h) => h.host); + if (customHostNames.length > 0) { + logger.info({ count: customHostNames.length }, "Adding DNS for custom host(s)..."); + await addHostsDns(customHostNames, sudoPassword); + } + } catch (err) { + logger.error({ err }, "Failed to add custom host DNS entries (continuing)"); + } +} diff --git a/src/mitm/manager.ts b/src/mitm/manager.ts index 1d16941b13..856c3c84fd 100644 --- a/src/mitm/manager.ts +++ b/src/mitm/manager.ts @@ -2,7 +2,8 @@ import { spawn, type ChildProcess } from "child_process"; import path from "path"; import fs from "fs"; import { resolveMitmDataDir } from "./dataDir.ts"; -import { addDNSEntry, addDNSEntries, removeDNSEntry, removeDNSEntries } from "./dns/dnsConfig.ts"; +import { removeDNSEntry, removeDNSEntries } from "./dns/dnsConfig.ts"; +import { provisionDnsEntries } from "./dns/provision.ts"; import { generateCert } from "./cert/generate.ts"; import { installCertResult, uninstallCert } from "./cert/install.ts"; import { ALL_TARGETS } from "./targets/index.ts"; @@ -501,86 +502,6 @@ export async function startMitm( } } -/** Minimal logger shape used by {@link provisionDnsEntries} (injectable for tests). */ -interface DnsProvisionLogger { - error: (payload: unknown, msg: string) => void; - info: (payload: unknown, msg?: string) => void; -} - -/** Injectable dependencies for {@link provisionDnsEntries} (all default to the real ones). */ -export interface DnsProvisionDeps { - addDefaultDns?: (sudoPassword: string) => Promise; - addHostsDns?: (hosts: string[], sudoPassword: string) => Promise; - getAgentStates?: () => ReturnType; - listEnabledCustomHosts?: () => ReturnType; - logger?: DnsProvisionLogger; -} - -/** - * Provision every AgentBridge DNS entry (Antigravity defaults + agents with - * `dns_enabled=true` + enabled custom hosts). **Every step is best-effort**: a failure - * is logged with the full `err` — which carries the privileged command's stderr - * (`systemCommands.ts` folds stderr into the Error message) — and never aborts the - * bridge start. - * - * Previously the default step (`addDNSEntry`) was called unguarded while the two - * sibling steps and cert install were wrapped, so in containers/headless (Docker - * `USER node`, no `sudo`, read-only /etc/hosts) it threw out of `startMitmInternal` - * and killed the whole start (#6127); its stderr also never reached app.log — only a - * bare exit code hit the toast (#6198). Extracting + guarding all three steps here - * restores the symmetry and makes the behavior unit-testable without spawning the - * MITM server. - */ -export async function provisionDnsEntries( - sudoPassword: string, - deps: DnsProvisionDeps = {} -): Promise { - const addDefaultDns = deps.addDefaultDns ?? addDNSEntry; - const addHostsDns = deps.addHostsDns ?? addDNSEntries; - const getAgentStates = deps.getAgentStates ?? getAllAgentBridgeStates; - const listEnabledCustomHosts = - deps.listEnabledCustomHosts ?? (() => listCustomHosts({ enabledOnly: true })); - const logger = deps.logger ?? log; - - // Antigravity default hosts. - try { - await addDefaultDns(sudoPassword); - } catch (err) { - logger.error({ err }, "Failed to add default DNS entries (continuing)"); - } - - // Collect hosts from agents that have dns_enabled=true in the DB. - try { - const agentStates = getAgentStates(); - const agentHostsToAdd: string[] = []; - for (const state of agentStates) { - if (!state.dns_enabled) continue; - const target = ALL_TARGETS.find((t) => t.id === state.agent_id); - if (target) { - agentHostsToAdd.push(...target.hosts); - } - } - if (agentHostsToAdd.length > 0) { - logger.info({ count: agentHostsToAdd.length }, "Adding DNS for agent host(s)..."); - await addHostsDns(agentHostsToAdd, sudoPassword); - } - } catch (err) { - logger.error({ err }, "Failed to add agent DNS entries (continuing)"); - } - - // Collect enabled custom hosts. - try { - const customHosts = listEnabledCustomHosts(); - const customHostNames = customHosts.map((h) => h.host); - if (customHostNames.length > 0) { - logger.info({ count: customHostNames.length }, "Adding DNS for custom host(s)..."); - await addHostsDns(customHostNames, sudoPassword); - } - } catch (err) { - logger.error({ err }, "Failed to add custom host DNS entries (continuing)"); - } -} - /** * Internal body of startMitm(), extracted so the single-flight lock in * startMitm() cleanly wraps it in try/finally without re-indenting the diff --git a/tests/unit/mitm-dns-graceful-degrade-6127.test.ts b/tests/unit/mitm-dns-graceful-degrade-6127.test.ts index 71c3ba8c43..b82f6ab740 100644 --- a/tests/unit/mitm-dns-graceful-degrade-6127.test.ts +++ b/tests/unit/mitm-dns-graceful-degrade-6127.test.ts @@ -19,7 +19,7 @@ */ import { test } from "node:test"; import assert from "node:assert/strict"; -import { provisionDnsEntries } from "../../src/mitm/manager.ts"; +import { provisionDnsEntries } from "../../src/mitm/dns/provision.ts"; function makeSpyLogger() { const errorCalls: Array<{ payload: unknown; msg: string }> = [];