Files
pikasTech-HWLAB/internal/cloud/provider-profile-management.ts
T

545 lines
24 KiB
TypeScript

import { randomUUID } from "node:crypto";
const CONTRACT_VERSION = "provider-profile-management-v1";
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;
const DEFAULT_TIMEOUT_MS = 30000;
const PROVIDER_PROFILE_ALIASES = Object.freeze({ codex: "codex-api" });
const PROVIDER_PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
const RESERVED_PROVIDER_PROFILES = new Set(["runtime-default"]);
export async function handleProviderProfileCatalogHttp(request, response, options = {}) {
try {
const auth = await authenticateActor(request, options);
if (!auth.ok) return sendJson(response, numericStatus(auth.status) ?? numericStatus(auth.statusCode) ?? 401, auth);
const requestId = requestIdFor(request);
const env = options.env ?? process.env;
const managerUrl = resolveAgentRunManagerUrl(env);
const agentRunApiKey = resolveAgentRunApiKey(env);
const fetchImpl = options.fetchImpl ?? fetch;
const timeoutMs = positiveInteger(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_HTTP_TIMEOUT_MS, positiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, DEFAULT_TIMEOUT_MS));
const delegated = await agentRunJson(fetchImpl, managerUrl, "/api/v1/provider-profiles", { method: "GET", requestId, timeoutMs, apiKey: agentRunApiKey });
const data = normalizeCatalogData(delegated.data);
const status = delegated.ok ? delegated.httpStatus || 200 : delegated.httpStatus || 502;
const base = {
ok: delegated.ok,
status: delegated.ok ? "ok" : "failed",
contractVersion: CONTRACT_VERSION,
actor: actorSummary(auth.actor),
valuesPrinted: false
};
if (delegated.ok) {
return sendJson(response, status, { ...base, items: data.items, count: data.count, data });
}
return sendJson(response, status, {
...base,
error: delegatedError(delegated),
data: null
});
} catch (error) {
return sendError(response, error.httpStatus ?? 500, error.code ?? "provider_profile_catalog_failed", error.message ?? "Provider profile catalog failed", error.details);
}
}
export async function handleProviderProfilesHttp(request, response, url, options = {}) {
try {
const auth = await authenticateAdmin(request, options);
if (!auth.ok) return sendJson(response, numericStatus(auth.status) ?? numericStatus(auth.statusCode) ?? 401, auth);
const requestId = requestIdFor(request);
const route = parseProviderProfileRoute(url.pathname, request.method);
const env = options.env ?? process.env;
const managerUrl = resolveAgentRunManagerUrl(env);
const agentRunApiKey = resolveAgentRunApiKey(env);
const fetchImpl = options.fetchImpl ?? fetch;
const timeoutMs = positiveInteger(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_HTTP_TIMEOUT_MS, positiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, DEFAULT_TIMEOUT_MS));
if (route.kind === "list") {
const delegated = await agentRunJson(fetchImpl, managerUrl, "/api/v1/provider-profiles", { method: "GET", requestId, timeoutMs, apiKey: agentRunApiKey });
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeListData(delegated.data));
}
if (route.kind === "remove") {
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}`, { method: "DELETE", requestId, timeoutMs, apiKey: agentRunApiKey });
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeProfileData(delegated.data));
}
if (route.kind === "credential") {
const body = await readJsonObject(request, options.bodyLimitBytes);
const credentialApiKey = text(body.apiKey);
const authJson = typeof body.authJson === "string" ? body.authJson : "";
if (credentialApiKey.length < 8 && !authJson.trim()) return sendError(response, 400, "credential_required", "apiKey or authJson is required and must not be empty");
const delegatedBody = {
...(authJson.trim() ? { authJson } : { apiKey: credentialApiKey }),
...(body.config && typeof body.config === "object" ? { config: body.config } : {}),
delegatedBy: delegatedBy(auth, requestId),
reason: text(body.reason) || "hwlab-provider-management"
};
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/credential`, { method: "PUT", body: delegatedBody, requestId, timeoutMs, apiKey: agentRunApiKey });
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeProfileData(delegated.data));
}
if (route.kind === "config") {
if (route.method === "GET") {
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/config`, { method: "GET", requestId, timeoutMs, apiKey: agentRunApiKey });
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeConfigData(delegated.data), { allowConfigToml: true });
}
const body = await readJsonObject(request, options.bodyLimitBytes);
const configToml = typeof body.configToml === "string" ? body.configToml : "";
if (!configToml.trim()) return sendError(response, 400, "config_toml_required", "configToml is required and must not be empty");
const delegatedBody = {
configToml,
delegatedBy: delegatedBy(auth, requestId),
reason: text(body.reason) || "hwlab-provider-management"
};
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/config`, { method: "PUT", body: delegatedBody, requestId, timeoutMs, apiKey: agentRunApiKey });
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeConfigData(delegated.data));
}
if (route.kind === "validate") {
const body = await readOptionalJsonObject(request, options.bodyLimitBytes);
const delegatedBody = {
...(text(body.prompt) ? { prompt: text(body.prompt) } : {}),
delegatedBy: delegatedBy(auth, requestId),
reason: text(body.reason) || "hwlab-provider-management"
};
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/validate`, { method: "POST", body: delegatedBody, requestId, timeoutMs, apiKey: agentRunApiKey });
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeValidationData(delegated.data, route.publicProfile));
}
if (route.kind === "validation") {
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/validations/${encodeURIComponent(route.validationId)}`, { method: "GET", requestId, timeoutMs, apiKey: agentRunApiKey });
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeValidationData(delegated.data, route.publicProfile));
}
return sendError(response, 404, "not_found", "Provider profile route is not implemented");
} catch (error) {
return sendError(response, error.httpStatus ?? 500, error.code ?? "provider_profile_management_failed", error.message ?? "Provider profile management failed", error.details);
}
}
async function authenticateAdmin(request, options) {
const auth = await authenticateActor(request, options);
if (!auth.ok) return auth;
if (auth.actor?.role !== "admin") return errorPayload("admin_required", "Only admin users can manage provider profiles", 403);
return auth;
}
async function authenticateActor(request, options) {
const controller = options.accessController;
if (!controller || typeof controller.authenticate !== "function") {
return errorPayload("access_controller_unavailable", "HWLAB access controller is not available", 503);
}
const auth = await controller.authenticate(request, { required: true });
if (!auth.ok) return auth;
return auth;
}
function parseProviderProfileRoute(pathname, method) {
if (pathname === "/v1/admin/provider-profiles") {
if (method !== "GET") throw routeError(405, "method_not_allowed", "Provider profiles collection only supports GET");
return { kind: "list", publicProfile: null, agentRunProfile: null };
}
const match = pathname.match(/^\/v1\/admin\/provider-profiles\/([^/]+)(?:\/(credential|config|validate|validations\/([^/]+)))?$/u);
if (!match) throw routeError(404, "not_found", "Provider profile route is not implemented");
const profile = normalizeProfile(decodeURIComponent(match[1]));
const suffix = match[2] ?? "";
if (suffix === "credential") {
if (method !== "PUT") throw routeError(405, "method_not_allowed", "Provider profile credential only supports PUT");
return { kind: "credential", ...profile };
}
if (!suffix) {
if (method !== "DELETE") throw routeError(405, "method_not_allowed", "Provider profile root only supports DELETE");
return { kind: "remove", ...profile };
}
if (suffix === "config") {
if (method !== "GET" && method !== "PUT") throw routeError(405, "method_not_allowed", "Provider profile config only supports GET and PUT");
return { kind: "config", method, ...profile };
}
if (suffix === "validate") {
if (method !== "POST") throw routeError(405, "method_not_allowed", "Provider profile validation only supports POST");
return { kind: "validate", ...profile };
}
if (suffix.startsWith("validations/")) {
if (method !== "GET") throw routeError(405, "method_not_allowed", "Provider profile validation lookup only supports GET");
return { kind: "validation", ...profile, validationId: decodeURIComponent(match[3] ?? "") };
}
throw routeError(404, "not_found", "Provider profile route is not implemented");
}
function normalizeProfile(value) {
const input = text(value).toLowerCase();
const publicProfile = PROVIDER_PROFILE_ALIASES[input] ?? input;
if (!publicProfile || RESERVED_PROVIDER_PROFILES.has(publicProfile) || !PROVIDER_PROFILE_ID_PATTERN.test(publicProfile)) {
throw routeError(400, "invalid_provider_profile", "Provider profile must be a lowercase slug such as deepseek, minimax-m3, dsflash-go, or codex-api", {
profile: input,
alias: "codex -> codex-api",
pattern: String(PROVIDER_PROFILE_ID_PATTERN)
});
}
return { publicProfile, agentRunProfile: publicProfile === "codex-api" ? "codex" : publicProfile };
}
async function agentRunJson(fetchImpl, managerUrl, path, { method = "GET", body, requestId, timeoutMs, apiKey }) {
const startedAt = Date.now();
const targetUrl = `${managerUrl}${path}`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const headers: Record<string, string> = {
accept: "application/json",
"content-type": "application/json",
"x-request-id": requestId,
"x-source-service-id": "hwlab-cloud-api",
"x-hwlab-delegation": "provider-profile-management-v1"
};
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
const response = await fetchImpl(targetUrl, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal
});
const textBody = await response.text();
const payload = parseJson(textBody);
const ok = response.ok && payload?.ok !== false;
return {
ok,
httpStatus: response.status,
data: payload?.data ?? payload,
payload,
traceId: text(payload?.traceId),
elapsedMs: Date.now() - startedAt,
method,
path
};
} catch (error) {
const timedOut = controller.signal.aborted || error?.name === "AbortError";
return {
ok: false,
httpStatus: 0,
data: null,
payload: { ok: false, error: { code: timedOut ? "agentrun_timeout" : "agentrun_request_failed", message: error?.message ?? String(error) } },
traceId: "",
elapsedMs: Date.now() - startedAt,
method,
path,
transportError: { code: timedOut ? "agentrun_timeout" : "agentrun_request_failed", message: error?.message ?? String(error), valuesPrinted: false }
};
} finally {
clearTimeout(timer);
}
}
function sendDelegated(response, delegated, auth, route, managerUrl, requestId, data, options = {}) {
const mutation = route.kind === "remove" || route.kind === "credential" || (route.kind === "config" && route.method === "PUT");
const status = delegated.ok ? (mutation ? 200 : delegated.httpStatus || 200) : delegated.httpStatus || 502;
const base = {
ok: delegated.ok,
status: delegated.ok ? (route.kind === "remove" ? "removed" : mutation ? "updated" : "ok") : "failed",
contractVersion: CONTRACT_VERSION,
actor: actorSummary(auth.actor),
profile: route.publicProfile ?? undefined,
delegation: delegationSummary(managerUrl, delegated, requestId),
valuesPrinted: false
};
if (delegated.ok) return sendJson(response, status, { ...base, ...listAliases(data), data }, options);
return sendJson(response, status, {
...base,
error: delegatedError(delegated),
data: null
});
}
function normalizeListData(data) {
const sourceItems = Array.isArray(data?.items) ? data.items : [];
const items = sourceItems.map(normalizeProfileData);
return { ...redactObject(data), items, count: items.length, valuesPrinted: false };
}
function normalizeCatalogData(data) {
const sourceItems = Array.isArray(data?.items) ? data.items : [];
const items = sourceItems.map(normalizeCatalogProfileData);
return { items, count: items.length, valuesPrinted: false };
}
function normalizeProfileData(item) {
if (!item || typeof item !== "object") return item;
const copy = redactObject(item);
if (copy.profile === "codex") {
copy.backendProfile = "codex";
copy.profile = "codex-api";
}
if (copy.profile === "deepseek") copy.bridge = deepseekBridgeSummary();
if (text(copy.configSummary?.model ?? copy.model) === "deepseek-v4-flash") copy.bridge = opencodeZenGoBridgeSummary();
copy.valuesPrinted = false;
return copy;
}
function normalizeCatalogProfileData(item) {
const normalized = normalizeProfileData(item);
if (!normalized || typeof normalized !== "object") return normalized;
return {
profile: normalized.profile,
backendProfile: normalized.backendProfile,
backendKind: normalized.backendKind,
configured: normalized.configured,
failureKind: normalized.failureKind ?? null,
bridge: normalized.bridge,
valuesPrinted: false
};
}
function normalizeConfigData(data) {
if (!data || typeof data !== "object") return data;
const configToml = typeof data.configToml === "string" ? data.configToml : typeof data["config.toml"] === "string" ? data["config.toml"] : undefined;
const copy = normalizeProfileData(data);
if (typeof configToml === "string") copy.configToml = configToml;
copy.configTomlPrinted = typeof configToml === "string";
copy.credentialValuesPrinted = false;
copy.valuesPrinted = false;
return copy;
}
function normalizeValidationData(data, fallbackProfile) {
if (!data || typeof data !== "object") return data;
const copy = normalizeProfileData(data);
if (!copy.profile) copy.profile = fallbackProfile;
if (copy.result && typeof copy.result === "object") copy.result = redactObject(copy.result);
if (copy.runnerJobs && Array.isArray(copy.runnerJobs)) copy.runnerJobs = copy.runnerJobs.map(redactObject);
return copy;
}
function listAliases(data) {
return Array.isArray(data?.items) ? { items: data.items, count: data.count ?? data.items.length } : {};
}
function delegatedError(delegated) {
const body = delegated.payload && typeof delegated.payload === "object" ? delegated.payload : {};
const nested = body.error && typeof body.error === "object" ? body.error : {};
return {
code: text(nested.code ?? body.code) || "agentrun_delegation_failed",
message: text(nested.message ?? body.message) || `AgentRun provider profile delegation failed with HTTP ${delegated.httpStatus}`,
failureKind: text(nested.failureKind ?? body.failureKind) || undefined,
agentRun: {
httpStatus: delegated.httpStatus,
traceId: delegated.traceId || undefined,
route: { method: delegated.method, path: delegated.path },
elapsedMs: delegated.elapsedMs,
transportError: delegated.transportError ? redactObject(delegated.transportError) : undefined,
valuesPrinted: false
},
valuesPrinted: false
};
}
function delegatedBy(auth, requestId) {
return {
system: "hwlab-v02",
userId: text(auth.actor?.id),
username: text(auth.actor?.username),
authMethod: text(auth.authMethod) || (auth.apiKey ? "api-key" : "web-session"),
requestId,
valuesPrinted: false
};
}
function actorSummary(actor) {
return actor ? {
id: text(actor.id),
username: text(actor.username),
displayName: text(actor.displayName),
role: text(actor.role),
status: text(actor.status),
valuesPrinted: false
} : null;
}
function delegationSummary(managerUrl, delegated, requestId) {
const url = new URL(managerUrl);
return {
provider: "agentrun-v01",
requestId,
agentRunBaseUrl: redactUrl(managerUrl),
agentRunHost: url.hostname,
traceId: delegated.traceId || undefined,
httpStatus: delegated.httpStatus,
elapsedMs: delegated.elapsedMs,
valuesPrinted: false
};
}
function deepseekBridgeSummary() {
return {
kind: "moon-bridge",
route: "HWLAB Moon Bridge -> DeepSeek official upstream",
serviceUrl: "http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1",
responsesPath: "/v1/responses",
forbiddenHosts: ["hyueapi.com"],
valuesPrinted: false
};
}
function opencodeZenGoBridgeSummary() {
return {
kind: "moon-bridge",
route: "HWLAB Moon Bridge -> OpenCode Zen Go Chat Completions",
serviceUrl: "http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1",
responsesPath: "/v1/responses",
model: "deepseek-v4-flash",
upstreamProvider: "opencode",
upstreamProtocol: "openai-chat",
forbiddenHosts: ["hyueapi.com"],
valuesPrinted: false
};
}
function resolveAgentRunManagerUrl(env) {
const raw = text(env.AGENTRUN_MGR_URL) || text(env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL) || text(env.AGENTRUN_MANAGER_URL) || DEFAULT_AGENTRUN_MGR_URL;
let parsed;
try {
parsed = new URL(raw);
} catch {
throw routeError(503, "agentrun_manager_url_invalid", "AgentRun manager URL is invalid", { managerUrl: redactUrl(raw) });
}
const allowLocal = truthy(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_ALLOW_NON_K3S_URL) || truthy(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL);
const localAllowed = allowLocal && ["127.0.0.1", "localhost"].includes(parsed.hostname);
if (!isAgentRunManagerInternalServiceHost(parsed.hostname) && !localAllowed) {
throw routeError(503, "agentrun_manager_url_invalid", "AgentRun manager URL must use internal k3s Service DNS agentrun-mgr.<namespace>.svc.cluster.local", { managerUrl: redactUrl(raw) });
}
return raw.replace(/\/+$/u, "");
}
function resolveAgentRunApiKey(env) {
return text(env.AGENTRUN_API_KEY) || text(env.HWLAB_CODE_AGENT_AGENTRUN_API_KEY) || text(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_API_KEY) || text(env.HWLAB_API_KEY);
}
function isAgentRunManagerInternalServiceHost(host) {
return AGENTRUN_MANAGER_HOST_PATTERN.test(String(host ?? "").trim().toLowerCase());
}
async function readOptionalJsonObject(request, limitBytes) {
const body = await readBody(request, limitBytes);
if (!body.trim()) return {};
return parseJsonObject(body);
}
async function readJsonObject(request, limitBytes) {
return parseJsonObject(await readBody(request, limitBytes));
}
async function readBody(request, limitBytes = 1024 * 1024) {
const chunks = [];
let total = 0;
for await (const chunk of request) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
total += buffer.length;
if (total > limitBytes) throw routeError(413, "body_too_large", "Request body is too large");
chunks.push(buffer);
}
return Buffer.concat(chunks).toString("utf8");
}
function parseJsonObject(body) {
const parsed = parseJson(body);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw routeError(400, "invalid_json_body", "Request body must be a JSON object");
return parsed;
}
function parseJson(value) {
try {
return value ? JSON.parse(value) : null;
} catch (error) {
throw routeError(400, "parse_error", "Invalid JSON body", { reason: error?.message ?? String(error) });
}
}
function redactObject(value, key = "") {
if (Array.isArray(value)) return value.map((item) => redactObject(item, key));
if (value && typeof value === "object") {
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactObject(entryValue, entryKey)]));
}
if (typeof value === "string" && (secretField(key) || looksSecret(value))) return "[redacted]";
return value;
}
function secretField(key) {
const lower = key.toLowerCase();
if (["apikey", "api_key", "authorization", "authjson", "auth.json", "configtoml", "config.toml", "stringdata"].includes(lower)) return true;
if (lower === "secretref" || lower === "secretname" || lower === "secretkey" || lower === "keys") return false;
return lower.includes("password") || lower.endsWith("token");
}
function looksSecret(value) {
return /^sk-[A-Za-z0-9_-]{10,}/u.test(value) || /^Bearer\s+\S+/u.test(value);
}
function requestIdFor(request) {
return getHeader(request, "x-request-id") || `req_provider_${randomUUID().replace(/-/gu, "")}`;
}
function getHeader(request, name) {
const value = request.headers?.[name.toLowerCase()];
if (Array.isArray(value)) return text(value[0]);
return text(value);
}
function positiveInteger(value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
}
function numericStatus(value) {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 100 && parsed <= 599 ? parsed : null;
}
function truthy(value) {
return ["1", "true", "yes", "on"].includes(text(value).toLowerCase());
}
function redactUrl(value) {
try {
const url = new URL(value);
if (url.username) url.username = "redacted";
if (url.password) url.password = "redacted";
return url.toString().replace(/\/$/u, "");
} catch {
return "[invalid-url]";
}
}
function routeError(httpStatus, code, message, details = {}) {
return Object.assign(new Error(message), { httpStatus, code, details });
}
function errorPayload(code, message, status, details = {}) {
return { ok: false, status: "failed", contractVersion: CONTRACT_VERSION, error: { code, message, ...details, valuesPrinted: false }, valuesPrinted: false, statusCode: status };
}
function sendError(response, statusCode, code, message, details = {}) {
return sendJson(response, statusCode, { ok: false, status: "failed", contractVersion: CONTRACT_VERSION, error: { code, message, ...redactObject(details), valuesPrinted: false }, valuesPrinted: false });
}
function sendJson(response, statusCode, payload, options = {}) {
response.writeHead(statusCode, { "content-type": "application/json" });
const body = options.allowConfigToml ? redactObjectAllowingConfigToml(payload) : redactObject(payload);
response.end(`${JSON.stringify(body)}\n`);
}
function redactObjectAllowingConfigToml(value, key = "") {
if ((key === "configToml" || key === "config.toml") && typeof value === "string") return value;
if (Array.isArray(value)) return value.map((item) => redactObjectAllowingConfigToml(item, key));
if (value && typeof value === "object") {
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactObjectAllowingConfigToml(entryValue, entryKey)]));
}
if (typeof value === "string" && (secretField(key) || looksSecret(value))) return "[redacted]";
return value;
}
function text(value) {
return String(value ?? "").trim();
}