Files
OmniRoute/src/lib/proxyHealth/decision.ts
Diego Rodrigues de Sa e Souza f680aacff0 fix(proxy): #6246 stop the v3.8.44 proxy IP-leak + over-deactivation regression (#6296)
Merged into release/v3.8.45. Reds pré-existentes classificados: dast-smoke (infra), check:file-size (drift de baseline de god-files congelados), e 3 testes de contagem agentSkills stale (43→44 já corrigidos no tip pelo #6186 — somem no squash sobre o tip). Núcleo do fix #6246 (proxy IP-leak + over-deactivation).
2026-07-05 17:26:38 -03:00

78 lines
3.2 KiB
TypeScript

/**
* Pure, network-free decision for the proxy health scheduler (#6246).
*
* Separated from the sweep so the status/removal policy can be unit-tested
* exhaustively without any I/O. The sweep classifies each probe into a tri-state
* {@link ProxyProbeOutcome} and applies the returned {@link ProxyHealthDecision}.
*
* Policy (agreed for #6246):
* A — downgrade only after `removeAfter` CONSECUTIVE conclusive failures.
* B — an `inconclusive` probe (our own timeout/abort, or the probe TARGET
* erroring) never penalizes: it neither counts nor changes status.
* C — by DEFAULT (auto-remove off) the health check NEVER mutates a proxy's
* status. It only counts failures for logging. A proxy is downgraded to
* `inactive` (and removed) only when the operator opts in via
* PROXY_AUTO_REMOVE=true. This mirrors how accounts are only auto-disabled
* when the operator allows it — the operator owns their (often paid) proxies.
*/
export type ProxyProbeOutcome = "ok" | "fail" | "inconclusive";
export interface ProxyHealthDecisionInput {
/** Tri-state result of the reachability probe for this proxy. */
outcome: ProxyProbeOutcome;
/** Consecutive failure count recorded BEFORE this probe. */
priorFailures: number;
/** PROXY_AUTO_REMOVE === "true" — operator opted into status management. */
autoRemove: boolean;
/** Consecutive conclusive failures required before a downgrade/removal. */
removeAfter: number;
}
export interface ProxyHealthDecision {
/** New consecutive-failure count to persist for this proxy. */
failures: number;
/** Whether to drop this proxy from the consecutive-failure map. */
clearFailures: boolean;
/** Status to write, or `null` to leave the operator-controlled status untouched. */
setStatus: "active" | "inactive" | null;
/** Whether to auto-remove the proxy (only ever true when autoRemove is on). */
remove: boolean;
}
export function decideProxyHealthAction(input: ProxyHealthDecisionInput): ProxyHealthDecision {
const { outcome, priorFailures, autoRemove, removeAfter } = input;
const threshold = Number.isFinite(removeAfter) && removeAfter > 0 ? removeAfter : 3;
// B: inconclusive probes are neutral — do not touch count or status.
if (outcome === "inconclusive") {
return { failures: priorFailures, clearFailures: false, setStatus: null, remove: false };
}
// Success: reset the streak. Only (re)assert "active" when the operator has
// opted into status management; otherwise never touch the user's status (C).
if (outcome === "ok") {
return {
failures: 0,
clearFailures: true,
setStatus: autoRemove ? "active" : null,
remove: false,
};
}
// Conclusive failure.
const failures = priorFailures + 1;
// C: default mode only counts/logs — never downgrades.
if (!autoRemove) {
return { failures, clearFailures: false, setStatus: null, remove: false };
}
// A: downgrade + remove only once the consecutive threshold is reached.
if (failures >= threshold) {
return { failures, clearFailures: false, setStatus: "inactive", remove: true };
}
return { failures, clearFailures: false, setStatus: null, remove: false };
}