Files
pikasTech-HWLAB/internal/cloud/m3-io-control-runtime.ts
T

772 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 未 greenstatus=${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";
}
}