/* * SPEC: PJ2026-010405 云端控制台;PJ2026-010103 HWPOD 服务。 * Implementation reference: draft-2026-07-13-p0-cloud-console。 * Responsibility: 提供 HWLAB Node 更新、HWPOD typed topology、显式有界 readiness 探测与 node-ops 转发。 */ import { randomUUID } from "node:crypto"; import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs"; import { discoverHwpodSpecs, hwpodSpecDiscoveryPayload, hwpodSpecWorkspaceProbePlan } from "./hwpod-spec-discovery.ts"; import { buildHwpodTopologyReadModel } from "./hwpod-topology-read-model.ts"; import { buildHwlabNodeUpdatePayload, hwlabNodeBundledArtifact } from "../hwpod/node-artifact.ts"; import { getHeader, parsePositiveInteger, readBody, safeOpaqueId, sendJson, truthyFlag } from "./server-http-utils.ts"; import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts"; const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024; export async function handleHwpodNodeOpsHttp(request, response, options) { if (request.method === "GET") { const planId = new URL(request.url, "http://hwlab.local").searchParams.get("planId")?.trim() ?? ""; if (planId) { const operation = await options.hwpodNodeWsRegistry.lookup(planId); sendJson(response, operation ? 200 : 404, operation ? { ok: true, status: operation.result ? "completed" : "running", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, ...operation } : { ok: false, status: "not-found", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, planId, error: { code: "hwpod_operation_not_found", message: `hwpod operation ${planId} was not found` } }); return; } sendJson(response, 200, { ok: true, status: "ready", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, route: "/v1/hwpod-node-ops", specAuthority: "code-agent-workspace", compiler: "hwpod-compiler-cli", apiRole: "node-ops-forwarder", nodeRole: "thin-hwpod-node-executor", supportedOps: Array.from(HWPOD_NODE_OPS), websocket: options.hwpodNodeWsRegistry.describe() }); return; } if (request.method !== "POST") { sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "hwpod-node-ops only supports GET and POST" } }); return; } const body = await readJsonObject(request, options.bodyLimitBytes); if (!body.ok) { sendJson(response, 400, body.error); return; } const validation = validateHwpodNodeOpsPlan(body.value); if (!validation.ok) { sendJson(response, 400, { ok: false, status: "rejected", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, error: validation.error }); return; } const plan = validation.plan; const requestMeta = hwpodNodeOpsRequestMeta(request, options, "hwpod"); try { const handled = await dispatchHwpodNodeOpsPlan(plan, requestMeta, options, { request }); sendJson(response, handled.httpStatus, handled.payload); } catch (error) { const summary = error?.message ?? "hwpod-node-ops handler failed"; recordHwpodNodeOpsBlocked(request, options, plan, requestMeta, "hwpod_node_handler_failed", summary, { dispatchMode: "handler-exception" }); sendJson(response, 200, hwpodNodeOpsBlockedPayload(plan, requestMeta, summary, { dispatchMode: "handler-exception" })); } } export function handleHwlabNodeUpdateHttp(request, response, url, options) { if (request.method !== "GET") { sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "GET required." } }); return; } sendJson(response, 200, buildHwlabNodeUpdatePayload(url, options.env ?? process.env)); } export function handleHwlabNodeDownloadHttp(request, response, options) { if (request.method !== "GET") { sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "GET required." } }); return; } const bundled = hwlabNodeBundledArtifact(options.env ?? process.env); if (!bundled.content) { sendJson(response, 404, { ok: false, error: { code: "hwlab_node_bundle_missing", message: "bundled hwlab-node.py is not available" } }); return; } const bytes = Buffer.from(bundled.content, "utf8"); response.writeHead(200, { "content-type": "text/x-python; charset=utf-8", "cache-control": "no-store", "x-hwlab-node-version": bundled.version || "unknown", "x-hwlab-node-sha256": bundled.sha256 || "", "content-length": String(bytes.length) }); response.end(bytes); } export async function handleHwpodSpecDiscoveryHttp(request, response, url, options) { const probe = truthyFlag(url.searchParams.get("probe")); const observedAt = new Date().toISOString(); let specs = await discoverHwpodSpecs(options); if (probe) { specs = await probeDiscoveredHwpodSpecs(specs, { request, options, observedAt }); } sendJson(response, 200, hwpodSpecDiscoveryPayload(specs, { observedAt })); } export async function handleHwpodTopologyHttp(request, response, url, options) { if (request.method !== "GET") { sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "GET required." } }); return; } const observedAt = new Date().toISOString(); let specs = await discoverHwpodSpecs(options); if (truthyFlag(url.searchParams.get("probe"))) { specs = await probeDiscoveredHwpodSpecs(specs, { request, options, observedAt }); } const payload = buildHwpodTopologyReadModel(specs, options.hwpodNodeWsRegistry.describe(), { observedAt, resource: url.searchParams.get("resource"), q: url.searchParams.get("q"), status: url.searchParams.get("status"), sort: url.searchParams.get("sort"), direction: url.searchParams.get("direction"), cursor: url.searchParams.get("cursor"), limit: parsePositiveInteger(url.searchParams.get("limit"), 50) }); sendJson(response, 200, payload); } function runtimeEnvironment(env = process.env) { const value = env.HWLAB_GITOPS_PROFILE || env.HWLAB_ENVIRONMENT || ENVIRONMENT_DEV; return typeof value === "string" && value.trim() ? value.trim() : ENVIRONMENT_DEV; } function hwpodNodeOpsRequestMeta(request, options, label) { const httpContext = request?.hwlabHttpRequestContext ?? {}; return { requestId: getHeader(request, "x-request-id") || httpContext.requestId || `req_${label}_${randomUUID()}`, traceId: getHeader(request, "x-trace-id") || `trc_${label}_${randomUUID()}`, serviceId: getHeader(request, "x-source-service-id") || CLOUD_API_SERVICE_ID, environment: runtimeEnvironment(options.env ?? process.env), otelTraceId: httpContext.traceId ?? null, traceparent: httpContext.traceparent ?? null, valuesPrinted: false }; } function cleanText(value) { const text = String(value ?? "").trim(); return text || ""; } async function probeDiscoveredHwpodSpecs(specs, context) { const probed = []; for (const spec of specs) probed.push(await probeDiscoveredHwpodSpec(spec, context)); return probed; } async function probeDiscoveredHwpodSpec(spec, { request, options, observedAt }) { if (spec.ok === false) return spec; const plan = hwpodSpecWorkspaceProbePlan(spec); const validation = validateHwpodNodeOpsPlan(plan); if (!validation.ok) { return { ...spec, availability: { ok: false, status: "invalid_probe_plan", checkedAt: observedAt, blocker: validation.error } }; } const requestMeta = { ...hwpodNodeOpsRequestMeta(request, options, "hwpod_spec"), operationKind: "readiness-probe" }; const handled = await dispatchHwpodNodeOpsPlan(validation.plan, requestMeta, options, { request }); const payload = handled.payload; return { ...spec, availability: { ok: payload.ok === true, status: payload.ok === true ? "available" : "blocked", checkedAt: observedAt, probePlanId: payload.planId, nodeOpsRoute: "/v1/hwpod-node-ops", results: payload.results ?? [], blocker: payload.blocker ?? null, httpStatus: handled.httpStatus } }; } async function dispatchHwpodNodeOpsPlan(plan, requestMeta, options, context = {}) { const hwpodNodeOpsUrl = normalizedHwpodNodeOpsUrl(options.env ?? process.env); const hwpodNodeWsRegistry = options.hwpodNodeWsRegistry; const hasWsNode = hwpodNodeWsRegistry?.hasNode?.(plan.nodeId); if (typeof options.hwpodNodeOpsHandler !== "function" && !hasWsNode && !hwpodNodeOpsUrl) { const summary = "no outbound WebSocket hwpod-node is connected and HWLAB_HWPOD_NODE_OPS_URL is not configured; cloud-api is only validating the hwpod-node-ops contract"; const details = { dispatchMode: "none", websocketRegistryConfigured: Boolean(hwpodNodeWsRegistry), websocketConnected: false, directUrlConfigured: false }; recordHwpodNodeOpsBlocked(context.request, options, plan, requestMeta, "hwpod_node_unavailable", summary, details); return { httpStatus: 200, payload: hwpodNodeOpsBlockedPayload(plan, requestMeta, summary, details) }; } const handled = typeof options.hwpodNodeOpsHandler === "function" ? await options.hwpodNodeOpsHandler(plan, { request: context.request, requestMeta, env: options.env ?? process.env }) : hasWsNode ? await hwpodNodeWsRegistry.dispatch(plan, requestMeta, { timeoutMs: parsePositiveInteger(options.env?.HWLAB_HWPOD_NODE_OPS_TIMEOUT_MS, 30000) }) : await forwardHwpodNodeOpsPlan(hwpodNodeOpsUrl, plan, requestMeta, options); const results = Array.isArray(handled?.results) ? handled.results : []; const failed = results.some((item) => item?.ok === false); const payload = attachHwpodNodeOpsDiagnostics({ ok: handled?.ok ?? !failed, status: handled?.status ?? (failed ? "failed" : "completed"), contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, planId: plan.planId, hwpodId: plan.hwpodId, nodeId: plan.nodeId, acceptedOps: plan.ops.length, results, blocker: handled?.blocker ?? null, requestMeta }, requestMeta, { dispatchMode: hasWsNode ? "websocket" : hwpodNodeOpsUrl ? "direct-url" : "handler" }); if (payload.ok === false && payload.status === "blocked" && payload.blocker?.code) { recordHwpodNodeOpsBlocked(context.request, options, plan, requestMeta, payload.blocker.code, payload.blocker.summary ?? "hwpod-node-ops blocked", payload.blocker.details ?? {}); } return { httpStatus: handled?.httpStatus ?? (failed ? 409 : 200), payload }; } async function forwardHwpodNodeOpsPlan(targetUrl, plan, requestMeta, options) { const target = await describeDirectHwpodNode(targetUrl, options); if (!target.ok || !target.nodeId) { return directHwpodNodeBlocked(plan, "hwpod_node_identity_unverified", "configured hwpod-node URL did not expose a verifiable nodeId", { requestedNodeId: plan.nodeId, targetUrl: redactNodeOpsUrl(targetUrl), dispatchMode: "direct-url", targetStatus: target.status ?? null, error: target.error ?? null }, requestMeta); } if (target.ok && target.nodeId && target.nodeId !== plan.nodeId) { return directHwpodNodeBlocked(plan, "hwpod_node_id_mismatch", `HWLAB_HWPOD_NODE_OPS_URL points to ${target.nodeId}, not requested node ${plan.nodeId}`, { requestedNodeId: plan.nodeId, targetNodeId: target.nodeId, targetUrl: redactNodeOpsUrl(targetUrl), dispatchMode: "direct-url" }, requestMeta); } const response = await fetch(targetUrl, { method: "POST", headers: { "content-type": "application/json", "x-request-id": requestMeta.requestId, "x-trace-id": requestMeta.traceId, "x-source-service-id": CLOUD_API_SERVICE_ID, ...(requestMeta.otelTraceId ? { "x-hwlab-otel-trace-id": requestMeta.otelTraceId } : {}), ...(requestMeta.traceparent ? { traceparent: requestMeta.traceparent } : {}) }, body: JSON.stringify(plan), signal: AbortSignal.timeout(parsePositiveInteger(options.env?.HWLAB_HWPOD_NODE_OPS_TIMEOUT_MS, 30000)) }); const body = await response.json().catch(() => null); if (!body || typeof body !== "object") { return directHwpodNodeBlocked(plan, "hwpod_node_response_invalid", `hwpod-node returned HTTP ${response.status} without a JSON object payload`, { targetUrl: redactNodeOpsUrl(targetUrl), dispatchMode: "direct-url", targetStatus: response.status }, requestMeta); } return { ok: body.ok, status: body.status, httpStatus: response.status, results: body.results, blocker: body.blocker ?? null }; } function directHwpodNodeBlocked(plan, code, summary, details, requestMeta = {}) { const blocker = hwpodNodeOpsBlocker({ code, layer: "hwpod-node", retryable: true, summary, details }, requestMeta, details); return { ok: false, status: "blocked", httpStatus: 200, results: plan.ops.map((op) => ({ opId: op.opId, op: op.op, ok: false, status: "blocked", blocker })), blocker, otelTraceId: blocker.otelTraceId ?? null, diagnostic: blocker.diagnostic ?? null }; } async function describeDirectHwpodNode(targetUrl, options) { try { const response = await fetch(targetUrl, { method: "GET", signal: AbortSignal.timeout(parsePositiveInteger(options.env?.HWLAB_HWPOD_NODE_OPS_TIMEOUT_MS, 30000)) }); const body = await response.json().catch(() => null); return { ok: response.ok && body && typeof body === "object", status: response.status, nodeId: safeOpaqueId(body?.nodeId), body }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error), nodeId: "" }; } } function redactNodeOpsUrl(value) { try { const parsed = new URL(value); parsed.username = ""; parsed.password = ""; parsed.search = parsed.search ? "?redacted=1" : ""; return parsed.toString(); } catch { return ""; } } function normalizedHwpodNodeOpsUrl(env = process.env) { const direct = String(env.HWLAB_HWPOD_NODE_OPS_URL ?? "").trim(); if (direct) return direct; const base = String(env.HWLAB_HWPOD_NODE_URL ?? "").trim().replace(/\/+$/u, ""); return base ? `${base}/v1/hwpod-node-ops` : ""; } function validateHwpodNodeOpsPlan(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return { ok: false, error: { code: "invalid_hwpod_node_ops_plan", message: "hwpod-node-ops body must be a JSON object" } }; } if (value.contractVersion !== HWPOD_NODE_OPS_CONTRACT_VERSION) { return { ok: false, error: { code: "invalid_hwpod_node_ops_contract", message: `contractVersion must be ${HWPOD_NODE_OPS_CONTRACT_VERSION}`, actual: value.contractVersion ?? null } }; } const planId = safeOpaqueId(value.planId) || `hwpod_plan_${randomUUID()}`; const nodeId = safeOpaqueId(value.nodeId); const hwpodId = safeOpaqueId(value.hwpodId); if (!nodeId) return { ok: false, error: { code: "invalid_hwpod_node_id", message: "nodeId is required" } }; if (!hwpodId) return { ok: false, error: { code: "invalid_hwpod_id", message: "hwpodId is required" } }; if (!Array.isArray(value.ops) || value.ops.length === 0) { return { ok: false, error: { code: "invalid_hwpod_node_ops", message: "ops must be a non-empty array" } }; } const ops = []; for (const [index, item] of value.ops.entries()) { if (!item || typeof item !== "object" || Array.isArray(item)) { return { ok: false, error: { code: "invalid_hwpod_node_op", message: `ops[${index}] must be a JSON object` } }; } const op = typeof item.op === "string" ? item.op.trim() : ""; if (!HWPOD_NODE_OPS.has(op)) { return { ok: false, error: { code: "unsupported_hwpod_node_op", message: `unsupported hwpod-node op: ${op || ""}`, supportedOps: Array.from(HWPOD_NODE_OPS) } }; } ops.push({ opId: safeOpaqueId(item.opId) || `op_${index + 1}`, op, args: item.args && typeof item.args === "object" && !Array.isArray(item.args) ? item.args : {} }); } return { ok: true, plan: { ...value, planId, hwpodId, nodeId, ops } }; } function hwpodNodeOpsBlockedPayload(plan, requestMeta, summary, details = {}) { const blocker = hwpodNodeOpsBlocker({ code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary, details, userMessage: "hwpod-node 尚未接入执行面;cloud-api 已完成 hwpod-node-ops 合同校验。" }, requestMeta, details); return { ok: false, status: "blocked", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, planId: plan.planId, hwpodId: plan.hwpodId, nodeId: plan.nodeId, acceptedOps: plan.ops.length, results: plan.ops.map((op) => ({ opId: op.opId, op: op.op, ok: false, status: "blocked", blocker })), blocker, otelTraceId: blocker.otelTraceId ?? null, diagnostic: blocker.diagnostic ?? null, requestMeta }; } function hwpodNodeOpsBlocker(blocker, requestMeta = {}, details = {}) { const code = cleanText(blocker?.code) || "hwpod_node_ops_blocked"; const summary = cleanText(blocker?.summary ?? blocker?.message) || "hwpod-node-ops blocked"; const otelTraceId = cleanText(blocker?.otelTraceId ?? requestMeta?.otelTraceId) || null; const diagnostic = { code, rootCauseCode: code, summary, otelTraceId, traceLine: otelTraceId ? `OTel traceId: ${otelTraceId}` : null, details: details && typeof details === "object" ? details : {}, valuesPrinted: false }; const userMessage = cleanText(blocker?.userMessage); return { ...blocker, code, layer: blocker?.layer ?? "hwpod-node", retryable: blocker?.retryable ?? true, summary, ...(otelTraceId ? { otelTraceId } : {}), diagnostic, ...(userMessage ? { userMessage: `${userMessage}\n${diagnostic.traceLine ?? "OTel traceId: unavailable"}` } : {}) }; } function attachHwpodNodeOpsDiagnostics(payload, requestMeta, details = {}) { if (!payload || typeof payload !== "object" || payload.ok !== false) return payload; const topBlocker = payload.blocker ? hwpodNodeOpsBlocker(payload.blocker, requestMeta, payload.blocker.details ?? details) : null; const results = Array.isArray(payload.results) ? payload.results.map((result) => result?.blocker ? { ...result, blocker: hwpodNodeOpsBlocker(result.blocker, requestMeta, result.blocker.details ?? details) } : result) : payload.results; const diagnostic = topBlocker?.diagnostic ?? results?.find?.((result) => result?.blocker?.diagnostic)?.blocker?.diagnostic ?? null; return { ...payload, results, blocker: topBlocker, ...(diagnostic?.otelTraceId ? { otelTraceId: diagnostic.otelTraceId } : {}), ...(diagnostic ? { diagnostic } : {}) }; } function recordHwpodNodeOpsBlocked(request, options, plan, requestMeta, code, summary, details = {}) { const httpContext = request?.hwlabHttpRequestContext; const attributes = { "hwlab.hwpod.plan_id": cleanText(plan?.planId) || null, "hwlab.hwpod.hwpod_id": cleanText(plan?.hwpodId) || null, "hwlab.hwpod.node_id": cleanText(plan?.nodeId) || null, "hwlab.hwpod.accepted_ops": Array.isArray(plan?.ops) ? plan.ops.length : 0, "hwlab.hwpod.ops": Array.isArray(plan?.ops) ? plan.ops.map((op) => cleanText(op?.op)).filter(Boolean).join(",").slice(0, 240) : null, "hwlab.hwpod.blocker.code": code, "hwlab.hwpod.blocker.summary": String(summary ?? "").slice(0, 500), "hwlab.hwpod.dispatch_mode": cleanText(details?.dispatchMode) || "unknown", "hwlab.hwpod.websocket_connected": Boolean(details?.websocketConnected), "hwlab.hwpod.direct_url_configured": Boolean(details?.directUrlConfigured), "hwlab.hwpod.otel_trace_id": cleanText(requestMeta?.otelTraceId) || null, traceId: cleanText(requestMeta?.traceId) || null }; if (httpContext) httpContext.otelAttributes = { ...(httpContext.otelAttributes ?? {}), ...attributes }; const error = Object.assign(new Error(summary), { code }); options.emitHttpRoutePhaseSpan?.(request, options, "hwpod-node-ops.blocked", Date.now(), Date.now(), "error", error, attributes); } async function readJsonObject(request, limitBytes) { const body = await readBody(request, limitBytes ?? DEFAULT_BODY_LIMIT_BYTES); try { const value = body ? JSON.parse(body) : {}; if (!value || typeof value !== "object" || Array.isArray(value)) { return { ok: false, error: { accepted: false, error: { code: "invalid_params", message: "body must be a JSON object" } } }; } return { ok: true, value }; } catch (error) { return { ok: false, error: { accepted: false, error: { code: "parse_error", message: "Invalid JSON body", reason: error.message } } }; } }