Files
2026-07-13 13:39:12 +08:00

113 lines
3.8 KiB
TypeScript

type EnvSource = Record<string, string | undefined>;
type HeaderLike = Headers | Record<string, string | string[] | undefined> | null | undefined;
export type AgentGoalPolicy = {
detected: boolean;
readinessMaxTimeoutMs: number;
streamRecoveryEnabled: boolean;
};
export const DEFAULT_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS = 600_000;
const GOAL_COMMAND_RE = /(^|[\s"'`])\/goal(?=$|[\s"'`:;,.!?])/i;
const MAX_VISITED_NODES = 5_000;
const MAX_STRING_CHARS = 256_000;
function readHeader(headers: HeaderLike, name: string): string | null {
if (!headers) return null;
if (typeof (headers as Headers).get === "function") {
return (headers as Headers).get(name);
}
const lower = name.toLowerCase();
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() !== lower) continue;
if (Array.isArray(value)) return value.join(",");
return typeof value === "string" ? value : null;
}
return null;
}
function parseBoolean(value: string | undefined | null, fallback: boolean): boolean {
if (value == null || value.trim() === "") return fallback;
const normalized = value.trim().toLowerCase();
if (["true", "1", "yes", "on"].includes(normalized)) return true;
if (["false", "0", "no", "off"].includes(normalized)) return false;
return fallback;
}
function readPositiveMs(env: EnvSource, name: string, fallback: number): number {
const raw = env[name];
if (raw == null || raw.trim() === "") return fallback;
const parsed = Number(raw);
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
}
function hasGoalCommandText(value: string): boolean {
const sample = value.length > MAX_STRING_CHARS ? value.slice(0, MAX_STRING_CHARS) : value;
return GOAL_COMMAND_RE.test(sample);
}
export function isAgentGoalRequestBody(body: unknown): boolean {
const seen = new Set<object>();
let visited = 0;
const visit = (value: unknown, depth: number): boolean => {
if (visited++ > MAX_VISITED_NODES || depth > 10) return false;
if (typeof value === "string") return hasGoalCommandText(value);
if (!value || typeof value !== "object") return false;
if (seen.has(value)) return false;
seen.add(value);
if (Array.isArray(value)) {
for (const item of value) {
if (visit(item, depth + 1)) return true;
}
return false;
}
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
if (key === "metadata" || key === "usage") continue;
if (visit(child, depth + 1)) return true;
}
return false;
};
return visit(body, 0);
}
export function resolveAgentGoalPolicy(
body: unknown,
headers: HeaderLike = null,
env: EnvSource = process.env
): AgentGoalPolicy {
// Kill-switch (default ON — preserves existing behavior). When explicitly
// disabled, the whole heuristic is a no-op: it never elevates readiness
// timeouts or stream recovery, regardless of request body/headers. This
// mitigates client-controlled timeout amplification when an operator does
// not want request bodies/headers to influence upstream timeout budgets.
const policyEnabled = parseBoolean(env.OMNIROUTE_AGENT_GOAL_POLICY_ENABLED, true);
if (!policyEnabled) {
return {
detected: false,
readinessMaxTimeoutMs: DEFAULT_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS,
streamRecoveryEnabled: false,
};
}
const forcedByHeader = parseBoolean(readHeader(headers, "x-omniroute-agent-goal"), false);
const detected = forcedByHeader || isAgentGoalRequestBody(body);
const readinessMaxTimeoutMs = readPositiveMs(
env,
"OMNIROUTE_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS",
DEFAULT_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS
);
const streamRecoveryEnabled =
detected && parseBoolean(env.OMNIROUTE_AGENT_GOAL_STREAM_RECOVERY, true);
return {
detected,
readinessMaxTimeoutMs,
streamRecoveryEnabled,
};
}