fix: expose blocked AgentRun cancel disposition (#2092)
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// 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";
|
||||
|
||||
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);
|
||||
if (url.protocol !== "http:" || (!isAgentRunManagerInternalServiceHost(host) && !allowedByTest)) {
|
||||
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, "");
|
||||
}
|
||||
|
||||
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***");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user