// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0. // Responsibility: AgentRun manager HTTP client, internal service URL authority, and upstream timing metrics. import { backendPerformanceRouteTemplate, currentBackendPerformanceContext } from "./backend-performance.ts"; import { truthyFlag } from "./server-http-utils.ts"; import { isIP } from "node:net"; export const DEFAULT_AGENTRUN_MGR_URL = "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080"; const AGENTRUN_MANAGER_HOST_PATTERN = /^agentrun-mgr\.[a-z0-9]([-a-z0-9]*[a-z0-9])?\.svc\.cluster\.local$/u; export async function agentRunJson(fetchImpl, managerUrl, path, { method = "GET", body, timeoutMs = 20_000, env = process.env } = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); const performanceContext = currentBackendPerformanceContext(); const startedAt = nowMs(); let statusCode = 0; let outcome = "ok"; try { const headers: Record = {}; const apiKey = resolveAgentRunApiKey(env); if (body !== undefined) headers["content-type"] = "application/json"; if (apiKey) headers.authorization = `Bearer ${apiKey}`; const response = await fetchImpl(`${managerUrl}${path}`, { method, headers: Object.keys(headers).length === 0 ? undefined : headers, body: body === undefined ? undefined : JSON.stringify(body), signal: controller.signal }); statusCode = Number(response.status ?? 0); const text = await response.text(); const parsed = text ? JSON.parse(text) : {}; if (!response.ok || parsed?.ok === false) { outcome = "error"; throw Object.assign(new Error(parsed?.message ?? `AgentRun ${method} ${path} failed with HTTP ${response.status}`), { code: parsed?.failureKind ?? "agentrun_request_failed", statusCode: response.status, agentRunError: parsed }); } return parsed?.ok === true && Object.hasOwn(parsed, "data") ? parsed.data : parsed; } catch (error) { if (error?.name === "AbortError") { outcome = "timeout"; statusCode = 504; throw Object.assign(new Error(`AgentRun ${method} ${path} timed out after ${timeoutMs}ms`), agentRunHttpErrorContext({ managerUrl, path, method, timeoutMs, code: "agentrun_timeout", statusCode: 504, category: "upstream-timeout" })); } if (outcome === "ok") outcome = "error"; throw error; } finally { clearTimeout(timeout); performanceContext?.recordUpstream?.({ component: "agentrun", operation: agentRunOperationName(method, path), route: backendPerformanceRouteTemplate(path), method, statusClass: statusClass(statusCode), outcome, durationMs: nowMs() - startedAt }); } } export function resolveAgentRunManagerUrl(env = process.env, override = null) { const raw = firstNonEmpty(override, env.AGENTRUN_MGR_URL, env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL, DEFAULT_AGENTRUN_MGR_URL); const url = new URL(raw); const host = url.hostname.toLowerCase(); const allowedByTest = truthyFlag(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL) && ["127.0.0.1", "localhost"].includes(host); const allowedByNative = env.WORKBENCH_MODE === "agentrun-native" && truthyFlag(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_PRIVATE_URL) && isPrivateIp(host); if (url.protocol !== "http:" || (!isAgentRunManagerInternalServiceHost(host) && !allowedByTest && !allowedByNative)) { throw Object.assign(new Error(`AGENTRUN_MGR_URL must use internal k3s Service DNS agentrun-mgr..svc.cluster.local; got ${redactUrl(raw)}`), { code: "agentrun_internal_url_required", statusCode: 500 }); } url.pathname = url.pathname.replace(/\/+$/u, ""); url.search = ""; url.hash = ""; return url.toString().replace(/\/+$/u, ""); } function isPrivateIp(host: string) { if (isIP(host) !== 4) return false; const [a, b] = host.split(".").map(Number); return a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168); } export function isAgentRunManagerInternalServiceHost(host) { return AGENTRUN_MANAGER_HOST_PATTERN.test(String(host ?? "").trim().toLowerCase()); } function resolveAgentRunApiKey(env = process.env) { return firstNonEmpty(env.AGENTRUN_API_KEY, env.HWLAB_CODE_AGENT_AGENTRUN_API_KEY, env.HWLAB_API_KEY); } function agentRunOperationName(method, path) { const route = backendPerformanceRouteTemplate(path).replace(/^\/api\/v1\//u, "").replace(/\/:id/gu, ""); return `${String(method ?? "GET").toLowerCase()}_${route || "root"}`.replace(/[^a-z0-9]+/giu, "_").replace(/^_+|_+$/gu, "").toLowerCase(); } function statusClass(value) { const status = Number(value); if (!Number.isFinite(status) || status <= 0) return "unknown"; return `${Math.floor(status / 100)}xx`; } function nowMs() { if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now(); return Date.now(); } function agentRunHttpErrorContext({ managerUrl, path, method, timeoutMs, code, statusCode, category }) { return { code, statusCode, layer: "agentrun", category, retryable: true, method, route: path, path, timeoutMs, timeoutStage: "agentrun-http", managerHost: safeUrlHost(managerUrl), valuesPrinted: false }; } function safeUrlHost(value) { try { return new URL(value).host; } catch { return null; } } function firstNonEmpty(...values) { for (const value of values) { const text = String(value ?? "").trim(); if (text) return text; } return null; } function redactUrl(value) { try { const url = new URL(value); if (url.username) url.username = "***"; if (url.password) url.password = "***"; return url.toString(); } catch { return String(value ?? "").replace(/([?&](?:token|key|secret|password)=)[^&]+/giu, "$1***"); } }