// SPEC: PJ2026-010205 HWLAB接入 draft-2026-06-17-r0; PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor. // Responsibility: AgentRun backend profile, internal repo source authority, provider identity, and trace metadata. import { truthyFlag } from "./server-http-utils.ts"; export const DEFAULT_AGENTRUN_REPO_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"; export const GITEA_AGENTRUN_REPO_URL = "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git"; const ADAPTER_ID = "agentrun-v01"; const AGENTRUN_BACKEND_PREFIX = ADAPTER_ID; const AGENTRUN_RUNNER_KIND = "agentrun-v01-shared-runner"; const AGENTRUN_SESSION_MODE = "agentrun-v01-durable-session"; const AGENTRUN_IMPLEMENTATION_TYPE = "agentrun-v01-shared-execution-infra"; const AGENTRUN_PROVIDER_TRACE_PROTOCOL = "agentrun-v01-jsonrpc"; const AGENTRUN_BACKEND_ALIASES = Object.freeze({ "codex-api": "codex", codex: "codex", "gpt.pika": "gpt-pika" }); const AGENTRUN_BACKEND_PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u; export function resolveAgentRunBackendProfile(env = process.env, params = {}) { const requested = String(params.providerProfile ?? params.codeAgentProviderProfile ?? env.HWLAB_CODE_AGENT_AGENTRUN_BACKEND_PROFILE ?? env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE ?? "deepseek").trim().toLowerCase(); const fallback = String(env.HWLAB_CODE_AGENT_AGENTRUN_DEFAULT_BACKEND_PROFILE ?? "deepseek").trim().toLowerCase() || "deepseek"; const resolved = requested === "runtime-default" ? fallback : requested; if (!resolved || resolved === "runtime-default") return "deepseek"; const mapped = AGENTRUN_BACKEND_ALIASES[resolved] ?? resolved; return AGENTRUN_BACKEND_PROFILE_ID_PATTERN.test(mapped) ? mapped : "deepseek"; } export function agentRunTraceMeta(env = process.env) { return { runnerKind: AGENTRUN_RUNNER_KIND, workspace: resolveAgentRunRepoUrl(env), sandbox: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_SANDBOX, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, "danger-full-access"), protocol: AGENTRUN_PROVIDER_TRACE_PROTOCOL, sessionMode: AGENTRUN_SESSION_MODE, implementationType: AGENTRUN_IMPLEMENTATION_TYPE }; } export function backendForBackendProfile(profile) { return `${AGENTRUN_BACKEND_PREFIX}/${resolveAgentRunBackendProfile({}, { providerProfile: profile })}`; } export function resolveAgentRunRepoUrl(env = process.env) { const raw = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_REPO_URL, env.HWLAB_BOOT_READ_URL, DEFAULT_AGENTRUN_REPO_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); url.pathname = url.pathname.replace(/\/+$/u, ""); url.search = ""; url.hash = ""; const normalized = url.toString().replace(/\/+$/u, ""); if (url.protocol !== "http:" || (!isAllowedAgentRunRepoUrl(normalized) && !allowedByTest)) { throw Object.assign(new Error(`HWLAB_CODE_AGENT_AGENTRUN_REPO_URL must use an approved internal k3s git mirror (${DEFAULT_AGENTRUN_REPO_URL} or ${GITEA_AGENTRUN_REPO_URL}); got ${redactUrl(raw)}`), { code: "agentrun_internal_repo_url_required", statusCode: 500 }); } return normalized; } function isAllowedAgentRunRepoUrl(repoUrl) { return repoUrl === DEFAULT_AGENTRUN_REPO_URL || repoUrl === GITEA_AGENTRUN_REPO_URL; } export function requireAgentRunSourceCommit(env) { const text = String(env.HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT ?? "").trim().toLowerCase(); if (/^[0-9a-f]{40}$/u.test(text)) return text; throw Object.assign(new Error("HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT must be a full 40-character source commit before AgentRun dispatch"), { code: "agentrun_bundle_source_commit_invalid", statusCode: 500 }); } export function resolveAgentRunProviderId(env = process.env) { const providerId = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID, env.AGENTRUN_PROVIDER_ID); if (!providerId) { throw Object.assign(new Error("HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID must be configured from the runtime lane node id before AgentRun dispatch"), { code: "agentrun_provider_id_required", statusCode: 500 }); } if (!/^[A-Za-z0-9._-]+$/u.test(providerId)) { throw Object.assign(new Error("HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID must be a simple node id"), { code: "agentrun_provider_id_invalid", statusCode: 500 }); } return providerId; } export function agentRunProviderIdOrNull(env = process.env) { try { return resolveAgentRunProviderId(env); } catch { return null; } } export function modelForBackendProfile(profile, env = process.env) { const resolved = resolveAgentRunBackendProfile({}, { providerProfile: profile }); if (resolved === "codex") return firstNonEmpty(env.HWLAB_CODE_AGENT_CODEX_API_MODEL, env.HWLAB_CODE_AGENT_MODEL, "gpt-5.5"); if (resolved === "minimax-m3") return firstNonEmpty(env.HWLAB_CODE_AGENT_MINIMAX_M3_MODEL, "MiniMax-M3"); if (resolved === "deepseek") return firstNonEmpty(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, "deepseek-chat"); if (resolved === "dsflash-go") return firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_DSFLASH_GO_MODEL, env.HWLAB_CODE_AGENT_DSFLASH_GO_MODEL, "deepseek-v4-flash"); const profileEnvKey = dynamicBackendProfileEnvKey(resolved); return firstNonEmpty(env[`HWLAB_CODE_AGENT_AGENTRUN_${profileEnvKey}_MODEL`], env[`HWLAB_CODE_AGENT_${profileEnvKey}_MODEL`], resolved); } export function providerForBackendProfile(profile) { const resolved = resolveAgentRunBackendProfile({}, { providerProfile: profile }); if (resolved === "codex") return "codex-api"; if (resolved === "gpt-pika") return "gpt.pika"; return resolved; } export function agentRunAvailabilityBlocker(error, scope) { return { code: error?.code ?? `agentrun_${String(scope).replace(/[^a-z0-9]+/giu, "_")}_blocked`, type: "agent_blocker", scope: `agentrun-${scope}`, status: "open", sourceIssue: "pikasTech/HWLAB#879", summary: error?.message ?? `AgentRun ${scope} is not configured correctly.` , secretMaterialRead: false, valuesRedacted: true }; } function dynamicBackendProfileEnvKey(profile) { return String(profile ?? "") .trim() .toUpperCase() .replace(/[^A-Z0-9]+/gu, "_") .replace(/^_+|_+$/gu, "") || "PROFILE"; } 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.password) { url.username = "***"; url.password = ""; } return url.toString(); } catch { return ""; } }