Files
pikasTech-HWLAB/internal/cloud/code-agent-agentrun-runtime.ts
2026-07-18 04:41:49 +02:00

151 lines
5.9 KiB
TypeScript

// 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<string, string> = {};
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.<namespace>.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***");
}
}