Files
pikasTech-HWLAB/internal/cloud/code-agent-agentrun-profile.ts
T
2026-07-09 09:50:02 +02:00

156 lines
6.7 KiB
TypeScript

// 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 "<invalid-url>";
}
}