772 lines
23 KiB
TypeScript
772 lines
23 KiB
TypeScript
|
||
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";
|
||
}
|
||
}
|