import { createHash } from "node:crypto"; import { CLOUD_API_SERVICE_ID, createProtocolAuditEvent } from "../audit/index.mjs"; import { ENVIRONMENT_DEV, assertProtocolRecord } from "../protocol/index.mjs"; import { HWLAB_M3_IO_CAPABILITY_LEVELS } from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs"; const DEFAULT_TIMEOUT_MS = 2200; const M3_IO_CONTROL_CONTRACT_VERSION = "m3-io-control-v1"; const M3_IO_CONTROL_ROUTE = "/v1/m3/io"; const M3_IO_CHAIN = Object.freeze({ projectId: "prj_m3_hardware_loop", sourceGatewayId: "gwsimu_1", sourceGatewaySessionId: "gws_gwsimu_1", sourceResourceId: "res_boxsimu_1", sourceBoxId: "boxsimu_1", sourcePort: "DO1", targetGatewayId: "gwsimu_2", targetGatewaySessionId: "gws_gwsimu_2", targetResourceId: "res_boxsimu_2", targetBoxId: "boxsimu_2", targetPort: "DI1", patchPanelServiceId: "hwlab-patch-panel" }); const M3_IO_BLOCKER_CODES = Object.freeze({ gatewayUnavailable: "m3_gateway_session_unavailable", boxUnavailable: "m3_box_resource_unavailable", portDirectionInvalid: "m3_port_direction_invalid", wiringMissing: "m3_wiring_missing", patchPanelUnavailable: "m3_patch_panel_unavailable", dispatchFailed: "m3_gateway_dispatch_failed", runtimeDurableBlocked: "runtime_durable_not_green" }); export async function requestRuntimeJson(url, { method = "GET", body, requestJson, timeoutMs = DEFAULT_TIMEOUT_MS }) { if (requestJson) { try { const result = await requestJson(url, { method, body, timeoutMs }); return normalizeRuntimeJsonResponse(result); } catch (error) { return { ok: false, status: 0, error: error instanceof Error ? error.message : String(error) }; } } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { method, headers: body === undefined ? { accept: "application/json" } : { accept: "application/json", "content-type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), signal: controller.signal }); const text = await response.text(); let parsed = null; try { parsed = text ? JSON.parse(text) : null; } catch { parsed = null; } return { ok: response.ok, status: response.status, body: parsed, error: response.ok ? null : parsed?.error?.message ?? parsed?.message ?? response.statusText }; } catch (error) { return { ok: false, status: 0, error: error.name === "AbortError" ? `request timed out after ${timeoutMs}ms` : error.message }; } finally { clearTimeout(timer); } } export function normalizeRuntimeJsonResponse(result = {}) { if (Object.hasOwn(result, "ok") || Object.hasOwn(result, "body")) { return { ok: result.ok ?? (Number(result.status ?? 200) >= 200 && Number(result.status ?? 200) < 300), status: result.status ?? 200, body: result.body ?? result.json ?? null, error: result.error ?? result.body?.error?.message ?? null }; } if (Object.hasOwn(result, "status") || Object.hasOwn(result, "json")) { const status = result.status ?? 200; return { ok: status >= 200 && status < 300, status, body: result.json ?? null, error: result.error ?? null }; } return { ok: true, status: 200, body: result }; } export async function persistRuntimeRegistrations({ runtimeStore, gatewayStatus, box, capability, command, meta }) { const gatewaySessionId = gatewayStatus.session?.gatewaySessionId ?? gatewayStatus.gatewaySessionId ?? gatewayStatus.registry?.gatewaySessionId ?? command.gatewaySessionId; const gatewayId = gatewayStatus.gatewayId ?? gatewayStatus.session?.gatewayId ?? gatewayStatus.registry?.gatewayId ?? command.gatewayId; const gatewaySession = { projectId: M3_IO_CHAIN.projectId, gatewaySessionId, gatewayId, serviceId: "hwlab-gateway-simu", endpoint: gatewayStatus.registry?.endpoint ?? gatewayStatus.session?.endpoint, status: "connected", labels: { m3ControlSurface: "true" } }; const resource = { projectId: M3_IO_CHAIN.projectId, gatewaySessionId, resourceId: command.resourceId, boxId: box.boxId ?? command.boxId, resourceType: "simulator_endpoint", state: "available", metadata: { serviceId: "hwlab-box-simu", patchPanelOnlyPropagation: true, frontendBypassAllowed: false } }; const writes = []; writes.push(await tryRuntimeWrite("gateway.session.register", () => runtimeStore?.registerGatewaySession?.(gatewaySession, meta))); writes.push(await tryRuntimeWrite("box.resource.register", () => runtimeStore?.registerBoxResource?.(resource, meta))); writes.push(await tryRuntimeWrite("box.capability.report", () => runtimeStore?.reportBoxCapabilities?.({ capability }, meta))); return writes; } export async function tryRuntimeWrite(label, write) { if (typeof write !== "function") { return { label, written: false, skipped: true, reason: "runtimeStore method unavailable" }; } try { const result = await write(); if (result === undefined) { return { label, written: false, skipped: true, reason: "runtimeStore method unavailable" }; } return { label, written: true, result }; } catch (error) { return { label, written: false, reason: error instanceof Error ? error.message : String(error), code: error?.code ?? null, data: error?.data ?? null }; } } export async function successControlResult(input) { const records = await writeOutcomeRecords({ ...input, outcome: "succeeded", reason: null }); const target = controlTarget(input.command, input.operation.gatewaySessionId); const audit = auditState(records.auditWrite, records.runtimeAfter); const evidence = evidenceState(records.runtimeAfter, records); const readback = input.command.action === "di.read" ? { status: "succeeded", value: readValueFromGatewayResult(input.dispatch.body), resourceId: input.command.resourceId, port: input.command.port } : input.targetReadback ?? null; return { serviceId: CLOUD_API_SERVICE_ID, contractVersion: M3_IO_CONTROL_CONTRACT_VERSION, route: M3_IO_CONTROL_ROUTE, method: "POST", status: "succeeded", accepted: true, capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.ready, controlReady: true, action: input.action, chain: M3_IO_CHAIN, command: input.command, target, gatewayId: target.gatewayId, gatewaySessionId: target.gatewaySessionId, resourceId: target.resourceId, boxId: target.boxId, port: target.port, value: target.value, operationId: input.operation.operationId, traceId: input.meta.traceId, auditId: records.auditEvent.auditId, evidenceId: records.evidenceRecord.evidenceId, audit: { auditId: records.auditEvent.auditId, ...audit }, evidence: { evidenceId: records.evidenceRecord.evidenceId, ...evidence }, operationState: { status: "succeeded", reachable: true }, auditState: audit, operation: { ...input.operation, status: "succeeded", completedAt: input.now, updatedAt: input.now }, result: { value: input.command.action === "di.read" ? readValueFromGatewayResult(input.dispatch.body) : input.command.value, dispatch: gatewayResultSummary(input.gateway), patchPanel: input.patchPanel?.body ?? null, targetReadback: input.targetReadback ?? null }, readback, controlPath: { status: "succeeded", cloudApi: true, gatewaySimu: true, boxSimu: true, patchPanel: input.action === "do.write", frontendBypass: false }, auditEvent: records.auditEvent, evidenceRecord: records.evidenceRecord, evidenceState: evidence, durableStatus: durableStatus(records.runtimeAfter), trustBlocker: trustBlocker(records.runtimeAfter), blockerClassification: redactedBlockerClassification({ code: records.runtimeAfter?.blocker, reason: records.runtimeAfter?.reason, runtime: records.runtimeAfter }), persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records), observedAt: input.now }; } export async function blockedControlResult(input) { const records = await writeOutcomeRecords({ ...input, outcome: "failed", reason: input.reason }); const target = controlTarget(input.command, input.operation.gatewaySessionId); const audit = auditState(records.auditWrite, records.runtimeAfter); const evidence = evidenceState(records.runtimeAfter, records); const readback = input.command.action === "di.read" ? null : input.targetReadback ?? null; return { serviceId: CLOUD_API_SERVICE_ID, contractVersion: M3_IO_CONTROL_CONTRACT_VERSION, route: M3_IO_CONTROL_ROUTE, method: "POST", status: "blocked", accepted: false, capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked, controlReady: false, action: input.action, chain: M3_IO_CHAIN, command: input.command, target, gatewayId: target.gatewayId, gatewaySessionId: target.gatewaySessionId, resourceId: target.resourceId, boxId: target.boxId, port: target.port, value: target.value, operationId: input.operation.operationId, traceId: input.meta.traceId, auditId: records.auditEvent.auditId, evidenceId: records.evidenceRecord.evidenceId, audit: { auditId: records.auditEvent.auditId, ...audit }, evidence: { evidenceId: records.evidenceRecord.evidenceId, ...evidence }, blocker: { code: input.code, message: input.reason, zh: input.reason }, blockerClassification: redactedBlockerClassification({ code: input.code, reason: input.reason, runtime: records.runtimeAfter }), operationState: { status: "blocked", reachable: false, blocker: input.code }, auditState: audit, operation: { ...input.operation, status: "failed", completedAt: input.now, updatedAt: input.now, error: { code: input.code, message: input.reason } }, result: { dispatch: input.gateway ? gatewayResultSummary(input.gateway) : null, patchPanel: input.patchPanel?.body ?? null, targetReadback: input.targetReadback ?? null }, readback, controlPath: { status: "blocked", cloudApi: true, gatewaySimu: Boolean(input.gateway), boxSimu: input.code !== M3_IO_BLOCKER_CODES.gatewayUnavailable, patchPanel: Boolean(input.patchPanel?.ok), frontendBypass: false }, auditEvent: records.auditEvent, evidenceRecord: records.evidenceRecord, evidenceState: evidence, durableStatus: durableStatus(records.runtimeAfter), trustBlocker: trustBlocker(records.runtimeAfter), persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records), observedAt: input.now }; } export function blockedResult({ action, command = null, meta, runtime, code, reason, httpStatus, now, details = {} }) { const target = controlTarget(command, command?.gatewaySessionId); const audit = { auditId: null, status: "not_written", durableStatus: durableStatus(runtime), reason: "operation was blocked before audit write" }; const evidence = evidenceState(runtime, { auditWrite: { written: false, reason: "operation was blocked before audit write" }, evidenceWrite: { written: false, reason: "operation was blocked before evidence write" } }); return { serviceId: CLOUD_API_SERVICE_ID, contractVersion: M3_IO_CONTROL_CONTRACT_VERSION, route: M3_IO_CONTROL_ROUTE, method: "POST", status: "blocked", accepted: false, capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked, controlReady: false, action: action || "unknown", chain: M3_IO_CHAIN, command, target, gatewayId: target.gatewayId, gatewaySessionId: target.gatewaySessionId, resourceId: target.resourceId, boxId: target.boxId, port: target.port, value: target.value, operationId: null, traceId: meta.traceId, auditId: null, evidenceId: null, audit, evidence: { evidenceId: null, ...evidence }, blocker: { code, message: reason, zh: reason }, blockerClassification: redactedBlockerClassification({ code, reason, runtime }), operationState: { status: "blocked", reachable: false, blocker: code }, auditState: audit, result: { value: null, targetReadback: null }, readback: null, controlPath: { status: "blocked", cloudApi: true, gatewaySimu: false, boxSimu: false, patchPanel: false, frontendBypass: false }, evidenceState: evidence, durableStatus: durableStatus(runtime), trustBlocker: trustBlocker(runtime), persistence: { runtime, writes: [] }, observedAt: now, httpStatus, ...details }; } export async function writeOutcomeRecords({ action, command, operation, meta, runtimeStore, runtimeBefore, registration, operationWrite, tracePayload, outcome, reason, now }) { const auditEvent = createProtocolAuditEvent({ auditId: `aud_${operation.operationId.slice(3)}_${outcome}`, traceId: meta.traceId, actorType: "user", actorId: meta.actorId, action: action === "do.write" ? "m3.io.do.write" : "m3.io.di.read", targetType: "hardware_operation", targetId: operation.operationId, projectId: M3_IO_CHAIN.projectId, gatewaySessionId: operation.gatewaySessionId, operationId: operation.operationId, serviceId: CLOUD_API_SERVICE_ID, outcome, reason: reason ?? undefined, metadata: { route: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}->${M3_IO_CHAIN.patchPanelServiceId}->${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`, command, frontendBypass: false, runtimeDurableBefore: runtimeBefore?.status ?? "unknown" }, occurredAt: now }); const evidenceRecord = createEvidenceRecord({ evidenceId: `evd_${operation.operationId.slice(3)}_${outcome}`, projectId: M3_IO_CHAIN.projectId, operationId: operation.operationId, traceId: meta.traceId, createdAt: now, payload: { outcome, reason, tracePayload, command, frontendBypass: false, patchPanelOnlyPropagation: true } }); const auditWrite = await tryRuntimeWrite("audit.event.write", () => runtimeStore?.writeAuditEvent?.({ event: auditEvent }, meta)); const evidenceWrite = await tryRuntimeWrite("evidence.record.write", () => runtimeStore?.writeEvidenceRecord?.({ record: evidenceRecord }, meta)); const runtimeAfter = await safeRuntimeSummary(runtimeStore); return { auditEvent, evidenceRecord, auditWrite, evidenceWrite, runtimeAfter, registration, operationWrite }; } export function createEvidenceRecord({ evidenceId, projectId, operationId, traceId, payload, createdAt }) { const metadata = { traceId, serviceId: CLOUD_API_SERVICE_ID, route: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}->${M3_IO_CHAIN.patchPanelServiceId}->${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`, frontendBypass: false, patchPanelOnlyPropagation: true }; const serialized = JSON.stringify({ serviceId: CLOUD_API_SERVICE_ID, projectId, operationId, traceId, payload, metadata, createdAt }); const record = { evidenceId, projectId, operationId, kind: "trace", uri: `memory://hwlab-cloud-api/${operationId}/m3-io-control.json`, mimeType: "application/json", sha256: createHash("sha256").update(serialized).digest("hex"), sizeBytes: Buffer.byteLength(serialized), serviceId: CLOUD_API_SERVICE_ID, environment: ENVIRONMENT_DEV, metadata, createdAt }; assertProtocolRecord("evidenceRecord", record); return record; } export function evidenceState(runtime, records) { const durableReady = isDurableRuntimeReady(runtime); const recordsWritten = records.auditWrite?.written === true && records.evidenceWrite?.written === true; const evidenceWriteState = recordWriteState(records.evidenceWrite, runtime); if (durableReady && recordsWritten) { return { status: "green", sourceKind: "DEV-LIVE", durable: true, writeStatus: evidenceWriteState.status, durableStatus: durableStatus(runtime), reason: "operation/audit/evidence 已通过 durable runtime 写入;仍需 DEV 浏览器 E2E 才能宣称验收通过。" }; } const blocker = runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked; return { status: "blocked", sourceKind: "BLOCKED", durable: runtime?.durable === true, blocker, writeStatus: evidenceWriteState.status, durableStatus: durableStatus(runtime), reason: `操作链路可达性与可信持久化分离:runtime durable 未 green(status=${runtime?.status ?? "unknown"},blocker=${blocker}),不能宣称可信闭环完全 green。` }; } export function auditState(auditWrite, runtime) { return { ...recordWriteState(auditWrite, runtime), durableStatus: durableStatus(runtime) }; } export function recordWriteState(write, runtime) { if (write?.written === true && isDurableRuntimeReady(runtime)) { return { status: "persisted", durable: true, sourceKind: "DEV-LIVE" }; } if (write?.written === true) { return { status: "written_non_durable", durable: false, sourceKind: "BLOCKED", blocker: runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked }; } return { status: "not_written", durable: false, sourceKind: "BLOCKED", blocker: runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked, reason: write?.reason ?? "runtime write did not complete" }; } export function durableStatus(runtime) { const ready = isDurableRuntimeReady(runtime); return { status: runtime?.status ?? "unknown", durable: runtime?.durable === true, ready: runtime?.ready === true, liveRuntimeEvidence: runtime?.liveRuntimeEvidence === true, blocker: ready ? null : runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked, redacted: true }; } export function trustBlocker(runtime) { if (isDurableRuntimeReady(runtime)) return null; const durable = durableStatus(runtime); return { code: durable.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked, layer: "runtime-durable", zh: `runtime durable 未 green:${durable.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked}`, category: "runtime_durable", durableStatus: durable }; } export function redactedBlockerClassification({ code, reason, runtime } = {}) { const blockerCode = code ?? runtime?.blocker ?? null; if (!blockerCode) { return null; } return { code: blockerCode, category: blockerCategory(blockerCode), runtimeStatus: runtime?.status ?? "unknown", redacted: true, message: reason ? redactBlockerMessage(reason) : null }; } export function blockerCategory(code) { if (String(code).startsWith("runtime_durable")) return "runtime_durable"; if (String(code).includes("gateway")) return "gateway_session"; if (String(code).includes("patch_panel")) return "patch_panel"; if (String(code).includes("wiring")) return "patch_panel_wiring"; if (String(code).includes("box")) return "box_resource"; if (String(code).includes("port")) return "port_direction"; return "m3_io_control"; } export function redactBlockerMessage(message) { return String(message) .replace(/https?:\/\/[^\s,,;]+/giu, "[redacted-url]") .replace(/postgres(?:ql)?:\/\/[^\s,,;]+/giu, "[redacted-db-url]"); } export function controlTarget(command, gatewaySessionId) { return { gatewayId: command?.gatewayId ?? null, gatewaySessionId: gatewaySessionId ?? command?.gatewaySessionId ?? null, resourceId: command?.resourceId ?? null, boxId: command?.boxId ?? null, port: command?.port ?? null, value: Object.hasOwn(command ?? {}, "value") ? command.value : null }; } export function persistenceSummary(runtime, registration, operationWrite, records) { return { runtime, writes: [ ...(registration ?? []), operationWrite, records.auditWrite, records.evidenceWrite ].filter(Boolean) }; } export async function safeRuntimeSummary(runtimeStore) { try { if (typeof runtimeStore?.readiness === "function") { return await runtimeStore.readiness(); } if (typeof runtimeStore?.summary === "function") { return runtimeStore.summary(); } } catch (error) { return { adapter: "unknown", durable: false, ready: false, status: "blocked", blocker: "runtime_summary_unavailable", reason: error instanceof Error ? error.message : String(error), liveRuntimeEvidence: false }; } return { adapter: "none", durable: false, ready: false, status: "blocked", blocker: "runtime_store_missing", reason: "runtime store is not available", liveRuntimeEvidence: false }; } export function isDurableRuntimeReady(runtime) { if (!runtime || typeof runtime !== "object") return false; if (runtime.durable !== true) return false; if (runtime.ready === false) return false; if (runtime.liveRuntimeEvidence !== true) return false; return !["blocked", "degraded", "failed"].includes(String(runtime.status ?? "").toLowerCase()); } export function readValueFromGatewayResult(result) { return result?.result?.value ?? result?.value ?? null; } export function gatewayResultSummary(gateway) { return { gatewayId: gateway.gatewayId, gatewaySessionId: gateway.gatewaySessionId, url: redactUrl(gateway.url), role: gateway.role }; } export function publicGatewayStatus(status) { return { gatewayId: status?.gatewayId ?? status?.session?.gatewayId ?? null, gatewaySessionId: status?.session?.gatewaySessionId ?? status?.gatewaySessionId ?? null, boxCount: Array.isArray(status?.registry?.boxes) ? status.registry.boxes.length : null }; } export function finalizeControlResult(result) { return result; } export function ensureTrailingSlash(url) { return url.endsWith("/") ? url : `${url}/`; } export function trimUrl(value) { const text = String(value ?? "").trim(); return text ? text : null; } export function parseTimeout(value) { const parsed = Number.parseInt(value ?? "", 10); return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS; } export function redactUrl(url) { try { const parsed = new URL(url); parsed.username = parsed.username ? "***" : ""; parsed.password = parsed.password ? "***" : ""; return parsed.toString(); } catch { return "invalid-url"; } }