fix(antigravity): wrap Pro fallback chain in try/catch for timeout resilience (#7290)

* fix(antigravity): wrap executeOnce in try/catch for Pro fallback chain

When a Pro-tier candidate times out or throws a network error, the

exception now continues to the next candidate instead of aborting

the entire chain. Includes diagnostic logging and unit tests.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(antigravity): propagate abort signal in Pro fallback catch block

Re-throw AbortError and signal.aborted immediately instead of
retrying the next candidate. Prevents wasted upstream requests
after client disconnect.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(mitm): skip DNS modification when sudo unavailable (container)

In containers (USER node, no sudo, not root) provisionDnsEntries() now detects the condition up-front and logs a clear message instead of attempting sudo and silently swallowing the error. Adds canElevate() to the injectable deps interface for testability, and supports SKIP_ANTIGRAVITY_DNS=true for explicit opt-out.

* fix(antigravity): improve abort detection and fallback error handling

Check Error.name === 'AbortError' for non-DOMException environments

(polyfills, test harnesses). Capture first 400 from any candidate

(not just i===0) so mixed paths surface the 400 instead of a

generic error. Return firstResult when last candidate throws,

consistent with the all-400 case.

* test(mitm): add coverage for container-skip DNS provisioning

* test(mitm): harden container-skip DNS test assertions

The SKIP_ANTIGRAVITY_DNS=true and canElevate()=false tests used empty
agentStates/customHosts, so they could not distinguish 'all steps
skipped' from 'only the default step skipped'.  Provide non-empty mocks
and assert addHostsDns was NOT called.

Also add a SKIP_ANTIGRAVITY_DNS=false boundary test confirming the
strict === "true" comparison does not block normal provisioning, and
verify sudoPassword passthrough in the canElevate=true happy-path test.

* refactor(mitm): split provisionDnsEntries below complexity gate

provisionDnsEntries() (complexity ~18, this PR's try/catch/log
additions pushed it over check-complexity.mjs's threshold of 15)
and execute()'s Pro-fallback loop (complexity 27, from wrapping
executeOnce() in try/catch for timeout resilience) were both over
the gate. Decomposed each into small named helpers, no behavior
change:

- provision.ts: split into provisionDefaultDns/provisionAgentDns/
  provisionCustomHostsDns, each wrapping one best-effort DNS step.
- antigravity.ts: extracted the fallback-chain catch/400-handling
  decisions (handleAntigravityFallbackChainError,
  isAntigravityAbortError, handleAntigravityFallback400) into a new
  antigravity/proFallbackChain.ts submodule (pure, no executor
  instance state), mirroring the existing antigravity/sseCollect.ts
  submodule pattern. Also fixes the antigravity.ts file-size cap
  (was pushed to 1854 lines > 1813 frozen ceiling by this PR's own
  try/catch addition; now 1771).

execute/provisionDnsEntries no longer appear with ruleId complexity
or max-lines-per-function in the check-complexity.mjs report.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(antigravity): drop redundant loop continue (cognitive-complexity gate)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(antigravity): fold fallback outcome dispatch into switch (cognitive gate)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: HouMinXi <19586012+HouMinXi@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Bob.Hou
2026-07-18 14:14:14 -04:00
committed by GitHub
parent 43eb470790
commit 65fbba4893
5 changed files with 557 additions and 59 deletions

View File

@@ -3,7 +3,8 @@
* is guarded and unit-testable without spawning the MITM server (#6127 / #6198).
*/
import { addDNSEntry, addDNSEntries } from "./dnsConfig.ts";
import { addDNSEntry, addDNSEntries, isSudoAvailable } from "./dnsConfig.ts";
import { isRoot } from "../systemCommands.ts";
import { ALL_TARGETS } from "../targets/index.ts";
import { getAllAgentBridgeStates } from "@/lib/db/agentBridgeState.ts";
import { listCustomHosts } from "@/lib/db/inspectorCustomHosts.ts";
@@ -23,9 +24,73 @@ export interface DnsProvisionDeps {
addHostsDns?: (hosts: string[], sudoPassword: string) => Promise<void>;
getAgentStates?: () => ReturnType<typeof getAllAgentBridgeStates>;
listEnabledCustomHosts?: () => ReturnType<typeof listCustomHosts>;
/** Return true if privileged host-file writes are possible (sudo or root). */
canElevate?: () => boolean;
logger?: DnsProvisionLogger;
}
/** Fully-resolved dependency set used by the per-step provisioning helpers below. */
type ResolvedDnsProvisionDeps = {
addDefaultDns: (sudoPassword: string) => Promise<void>;
addHostsDns: (hosts: string[], sudoPassword: string) => Promise<void>;
getAgentStates: () => ReturnType<typeof getAllAgentBridgeStates>;
listEnabledCustomHosts: () => ReturnType<typeof listCustomHosts>;
logger: DnsProvisionLogger;
};
/** Antigravity default hosts — best-effort, never throws. */
async function provisionDefaultDns(
sudoPassword: string,
deps: ResolvedDnsProvisionDeps
): Promise<void> {
try {
await deps.addDefaultDns(sudoPassword);
} catch (err) {
deps.logger.error({ err }, "Failed to add default DNS entries (continuing)");
}
}
/** Hosts for agents with `dns_enabled=true` in the DB — best-effort, never throws. */
async function provisionAgentDns(
sudoPassword: string,
deps: ResolvedDnsProvisionDeps
): Promise<void> {
try {
const agentStates = deps.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) {
deps.logger.info({ count: agentHostsToAdd.length }, "Adding DNS for agent host(s)...");
await deps.addHostsDns(agentHostsToAdd, sudoPassword);
}
} catch (err) {
deps.logger.error({ err }, "Failed to add agent DNS entries (continuing)");
}
}
/** Enabled custom hosts — best-effort, never throws. */
async function provisionCustomHostsDns(
sudoPassword: string,
deps: ResolvedDnsProvisionDeps
): Promise<void> {
try {
const customHosts = deps.listEnabledCustomHosts();
const customHostNames = customHosts.map((h) => h.host);
if (customHostNames.length > 0) {
deps.logger.info({ count: customHostNames.length }, "Adding DNS for custom host(s)...");
await deps.addHostsDns(customHostNames, sudoPassword);
}
} catch (err) {
deps.logger.error({ err }, "Failed to add custom host DNS entries (continuing)");
}
}
/**
* Provision every AgentBridge DNS entry (Antigravity defaults + agents with
* `dns_enabled=true` + enabled custom hosts). **Every step is best-effort**: a failure
@@ -45,48 +110,35 @@ export async function provisionDnsEntries(
sudoPassword: string,
deps: DnsProvisionDeps = {}
): Promise<void> {
const addDefaultDns = deps.addDefaultDns ?? addDNSEntry;
const addHostsDns = deps.addHostsDns ?? addDNSEntries;
const getAgentStates = deps.getAgentStates ?? getAllAgentBridgeStates;
const listEnabledCustomHosts =
deps.listEnabledCustomHosts ?? (() => listCustomHosts({ enabledOnly: true }));
const canElevate = deps.canElevate ?? (() => isSudoAvailable() || isRoot());
const logger = deps.logger ?? defaultLog;
// Antigravity default hosts.
try {
await addDefaultDns(sudoPassword);
} catch (err) {
logger.error({ err }, "Failed to add default DNS entries (continuing)");
// Explicit opt-out: skip all DNS modification when the env var is set.
if (process.env.SKIP_ANTIGRAVITY_DNS === "true") {
logger.info("Skipping DNS entries - SKIP_ANTIGRAVITY_DNS=true");
return;
}
// 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)");
// In containers (USER node, no sudo installed, not root) we cannot write
// to /etc/hosts. Rather than attempting sudo and swallowing the error,
// detect the condition up-front and bail out with a clear message.
if (!canElevate()) {
logger.info(
"Skipping DNS entries - sudo not available and not running as root (likely a container)"
);
return;
}
// 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)");
}
const resolvedDeps: ResolvedDnsProvisionDeps = {
addDefaultDns: deps.addDefaultDns ?? addDNSEntry,
addHostsDns: deps.addHostsDns ?? addDNSEntries,
getAgentStates: deps.getAgentStates ?? getAllAgentBridgeStates,
listEnabledCustomHosts:
deps.listEnabledCustomHosts ?? (() => listCustomHosts({ enabledOnly: true })),
logger,
};
await provisionDefaultDns(sudoPassword, resolvedDeps);
await provisionAgentDns(sudoPassword, resolvedDeps);
await provisionCustomHostsDns(sudoPassword, resolvedDeps);
}