Merge pull request #283 from pikasTech/feat/m3-cloud-api-rpc-227

feat: add M3 cloud-api IO RPC contract
This commit is contained in:
Lyon
2026-05-23 15:20:06 +08:00
committed by GitHub
6 changed files with 419 additions and 3 deletions
+5
View File
@@ -18,10 +18,13 @@ import { buildDbRuntimeReadiness } from "./db-contract.mjs";
import { describeCodeAgentAvailability } from "./code-agent-chat.mjs";
import { buildCloudApiReadiness } from "./health-contract.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
import { M3_IO_RPC_METHODS, handleM3IoRpc } from "./m3-io-control.mjs";
export const SUPPORTED_RPC_METHODS = Object.freeze([
"system.health",
"cloud.adapter.describe",
M3_IO_RPC_METHODS.doWrite,
M3_IO_RPC_METHODS.diRead,
"gateway.session.register",
"box.resource.register",
"box.capability.report",
@@ -36,6 +39,8 @@ export const SUPPORTED_RPC_METHODS = Object.freeze([
const rpcHandlers = new Map([
["system.health", handleSystemHealth],
["cloud.adapter.describe", handleAdapterDescribe],
[M3_IO_RPC_METHODS.doWrite, async (params, envelope, context) => handleM3IoRpc("do.write", params, envelope, context)],
[M3_IO_RPC_METHODS.diRead, async (params, envelope, context) => handleM3IoRpc("di.read", params, envelope, context)],
["gateway.session.register", async (params, envelope, context) => getRuntimeStore(context).registerGatewaySession(params, envelope.meta)],
["box.resource.register", async (params, envelope, context) => getRuntimeStore(context).registerBoxResource(params, envelope.meta)],
["box.capability.report", async (params, envelope, context) => getRuntimeStore(context).reportBoxCapabilities(params, envelope.meta)],
+174
View File
@@ -13,6 +13,10 @@ import {
export const M3_IO_CONTROL_CONTRACT_VERSION = "m3-io-control-v1";
export const M3_IO_CONTROL_ROUTE = "/v1/m3/io";
export const M3_IO_RPC_METHODS = Object.freeze({
doWrite: "m3.io.do.write",
diRead: "m3.io.di.read"
});
export const M3_IO_CHAIN = Object.freeze({
projectId: "prj_m3_hardware_loop",
@@ -235,6 +239,25 @@ export async function handleM3IoControl(params = {}, context = {}) {
}));
}
export async function handleM3IoRpc(action, params = {}, envelope = {}, context = {}) {
return handleM3IoControl(
{
...params,
action,
traceId: envelope.meta?.traceId ?? params.traceId,
requestId: envelope.id ?? params.requestId,
actorId: envelope.meta?.actorId ?? params.actorId
},
{
...context,
requestJson: context.requestJson ?? context.m3IoRequestJson,
traceId: envelope.meta?.traceId ?? context.traceId,
requestId: envelope.id ?? context.requestId,
actorId: envelope.meta?.actorId ?? context.actorId
}
);
}
export function buildM3IoConfig(env = process.env) {
const enabledValue = String(env.HWLAB_M3_IO_CONTROL_ENABLED ?? env.HWLAB_M3_CONTROL_ENABLED ?? "true").toLowerCase();
return {
@@ -927,6 +950,7 @@ async function successControlResult(input) {
outcome: "succeeded",
reason: null
});
const target = controlTarget(input.command, input.operation.gatewaySessionId);
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
@@ -935,10 +959,22 @@ async function successControlResult(input) {
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,
operationState: {
status: "succeeded",
reachable: true
},
auditState: auditState(records.auditWrite, records.runtimeAfter),
operation: {
...input.operation,
status: "succeeded",
@@ -964,6 +1000,12 @@ async function successControlResult(input) {
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidenceState(records.runtimeAfter, records),
durableStatus: durableStatus(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
};
@@ -975,6 +1017,7 @@ async function blockedControlResult(input) {
outcome: "failed",
reason: input.reason
});
const target = controlTarget(input.command, input.operation.gatewaySessionId);
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
@@ -983,6 +1026,13 @@ async function blockedControlResult(input) {
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,
@@ -992,6 +1042,17 @@ async function blockedControlResult(input) {
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: auditState(records.auditWrite, records.runtimeAfter),
operation: {
...input.operation,
status: "failed",
@@ -1018,12 +1079,14 @@ async function blockedControlResult(input) {
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidenceState(records.runtimeAfter, records),
durableStatus: durableStatus(records.runtimeAfter),
persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records),
observedAt: input.now
};
}
function blockedResult({ action, command = null, meta, runtime, code, reason, httpStatus, now, details = {} }) {
const target = controlTarget(command, command?.gatewaySessionId);
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
@@ -1032,6 +1095,13 @@ function blockedResult({ action, command = null, meta, runtime, code, reason, ht
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,
@@ -1041,6 +1111,21 @@ function blockedResult({ action, command = null, meta, runtime, code, reason, ht
message: reason,
zh: reason
},
blockerClassification: redactedBlockerClassification({
code,
reason,
runtime
}),
operationState: {
status: "blocked",
reachable: false,
blocker: code
},
auditState: {
status: "not_written",
durableStatus: durableStatus(runtime),
reason: "operation was blocked before audit write"
},
controlPath: {
status: "blocked",
cloudApi: true,
@@ -1053,6 +1138,7 @@ function blockedResult({ action, command = null, meta, runtime, code, reason, ht
auditWrite: { written: false, reason: "operation was blocked before audit write" },
evidenceWrite: { written: false, reason: "operation was blocked before evidence write" }
}),
durableStatus: durableStatus(runtime),
persistence: {
runtime,
writes: []
@@ -1166,11 +1252,14 @@ function createEvidenceRecord({ evidenceId, projectId, operationId, traceId, pay
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 才能宣称验收通过。"
};
}
@@ -1180,10 +1269,95 @@ function evidenceState(runtime, records) {
sourceKind: "BLOCKED",
durable: runtime?.durable === true,
blocker,
writeStatus: evidenceWriteState.status,
durableStatus: durableStatus(runtime),
reason: `操作链路可达性与可信持久化分离:runtime durable 未 greenstatus=${runtime?.status ?? "unknown"}blocker=${blocker}),不能宣称可信闭环完全 green。`
};
}
function auditState(auditWrite, runtime) {
return {
...recordWriteState(auditWrite, runtime),
durableStatus: durableStatus(runtime)
};
}
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"
};
}
function durableStatus(runtime) {
return {
status: runtime?.status ?? "unknown",
durable: runtime?.durable === true,
ready: runtime?.ready === true,
liveRuntimeEvidence: runtime?.liveRuntimeEvidence === true,
blocker: runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked,
redacted: true
};
}
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
};
}
function blockerCategory(code) {
if (String(code).startsWith("runtime_durable")) return "runtime_durable";
if (String(code).includes("gateway")) return "gateway_session";
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";
}
function redactBlockerMessage(message) {
return String(message)
.replace(/https?:\/\/[^\s,;]+/giu, "[redacted-url]")
.replace(/postgres(?:ql)?:\/\/[^\s,;]+/giu, "[redacted-db-url]");
}
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
};
}
function persistenceSummary(runtime, registration, operationWrite, records) {
return {
runtime,
+213
View File
@@ -4,9 +4,12 @@ import test from "node:test";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
import { createBoxState, createGatewayState } from "../sim/model.mjs";
import { writeBoxPort } from "../sim/l2-runtime.mjs";
import { validateResponse } from "../protocol/index.mjs";
import { handleJsonRpcRequest, SUPPORTED_RPC_METHODS } from "./json-rpc.mjs";
import {
M3_IO_BLOCKER_CODES,
M3_IO_CHAIN,
M3_IO_RPC_METHODS,
describeM3IoControl,
handleM3IoControl
} from "./m3-io-control.mjs";
@@ -65,10 +68,31 @@ test("M3 DO write dispatches through gateway-simu, ticks patch-panel, reads DI,
assert.equal(result.action, "do.write");
assert.equal(result.command.port, "DO1");
assert.equal(result.command.value, true);
assert.equal(result.gatewayId, "gwsimu_1");
assert.equal(result.gatewaySessionId, "gws_gwsimu_1");
assert.equal(result.resourceId, "res_boxsimu_1");
assert.equal(result.boxId, "boxsimu_1");
assert.equal(result.port, "DO1");
assert.equal(result.value, true);
assert.deepEqual(result.target, {
gatewayId: "gwsimu_1",
gatewaySessionId: "gws_gwsimu_1",
resourceId: "res_boxsimu_1",
boxId: "boxsimu_1",
port: "DO1",
value: true
});
assert.match(result.operationId, /^op_m3_do_write_/u);
assert.equal(result.traceId, "trc_m3_control_write_true");
assert.match(result.auditId, /^aud_m3_do_write_/u);
assert.match(result.evidenceId, /^evd_m3_do_write_/u);
assert.equal(result.operationState.status, "succeeded");
assert.equal(result.auditState.status, "written_non_durable");
assert.equal(result.auditState.durableStatus.durable, false);
assert.equal(result.durableStatus.status, "degraded");
assert.equal(result.durableStatus.redacted, true);
assert.equal(result.blockerClassification.category, "runtime_durable");
assert.equal(result.blockerClassification.redacted, true);
assert.equal(result.controlPath.cloudApi, true);
assert.equal(result.controlPath.gatewaySimu, true);
assert.equal(result.controlPath.boxSimu, true);
@@ -78,6 +102,7 @@ test("M3 DO write dispatches through gateway-simu, ticks patch-panel, reads DI,
assert.equal(result.result.targetReadback.value, true);
assert.equal(result.evidenceState.status, "blocked");
assert.equal(result.evidenceState.sourceKind, "BLOCKED");
assert.equal(result.evidenceState.writeStatus, "written_non_durable");
assert.match(result.evidenceState.reason, /runtime durable 未 green/u);
assert.equal(fixture.box1.ports.DO1.value, true);
assert.equal(fixture.box2.ports.DI1.value, true);
@@ -89,6 +114,15 @@ test("M3 DO write dispatches through gateway-simu, ticks patch-panel, reads DI,
"/invoke"
]);
assert.equal(fixture.calls.every((call) => call.url.startsWith("http://gateway-") || call.url.startsWith("http://patch-panel")), true);
assert.equal(fixture.calls[1].body.operation, "hardware.port.write");
assert.equal(fixture.calls[1].body.gatewaySessionId, "gws_gwsimu_1");
assert.equal(fixture.calls[1].body.resourceId, "res_boxsimu_1");
assert.equal(fixture.calls[1].body.port, "DO1");
assert.equal(fixture.calls[1].body.value, true);
assert.equal(fixture.calls[2].body.signals[0].fromResourceId, "res_boxsimu_1");
assert.equal(fixture.calls[4].body.operation, "hardware.port.read");
assert.equal(fixture.calls[4].body.resourceId, "res_boxsimu_2");
assert.equal(fixture.calls[4].body.port, "DI1");
const audit = runtimeStore.queryAuditEvents({ projectId: M3_IO_CHAIN.projectId });
assert.ok(audit.events.some((event) => event.action === "m3.io.do.write"));
@@ -133,6 +167,9 @@ test("M3 DO write can prove control reachability while durable trusted persisten
assert.equal(result.evidenceState.sourceKind, "BLOCKED");
assert.equal(result.evidenceState.durable, false);
assert.equal(result.evidenceState.blocker, "runtime_durable_adapter_query_blocked");
assert.equal(result.durableStatus.blocker, "runtime_durable_adapter_query_blocked");
assert.equal(result.blockerClassification.code, "runtime_durable_adapter_query_blocked");
assert.equal(result.blockerClassification.category, "runtime_durable");
assert.equal(result.persistence.runtime.status, "blocked");
assert.equal(result.persistence.runtime.liveRuntimeEvidence, false);
assert.equal(result.persistence.writes.every((write) => write.written !== true), true);
@@ -175,6 +212,14 @@ test("M3 DI read returns structured value through gateway-simu without patch-pan
assert.equal(result.accepted, true);
assert.equal(result.action, "di.read");
assert.equal(result.command.port, "DI1");
assert.equal(result.gatewayId, "gwsimu_2");
assert.equal(result.gatewaySessionId, "gws_gwsimu_2");
assert.equal(result.resourceId, "res_boxsimu_2");
assert.equal(result.boxId, "boxsimu_2");
assert.equal(result.port, "DI1");
assert.equal(result.value, null);
assert.equal(result.operationState.status, "succeeded");
assert.equal(result.auditState.status, "written_non_durable");
assert.equal(result.result.value, true);
assert.equal(result.controlPath.patchPanel, false);
assert.deepEqual(fixture.calls.map((call) => call.path), ["/status", "/invoke"]);
@@ -212,6 +257,9 @@ test("M3 IO control blocks with Chinese reason when gateway session is unavailab
assert.equal(result.status, "blocked");
assert.equal(result.accepted, false);
assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.gatewayUnavailable);
assert.equal(result.blockerClassification.category, "gateway_session");
assert.equal(result.auditState.status, "not_written");
assert.equal(result.durableStatus.redacted, true);
assert.match(result.blocker.zh, /gateway 未注册\/不可用/u);
assert.equal(result.controlPath.cloudApi, true);
assert.equal(result.controlPath.frontendBypass, false);
@@ -245,6 +293,7 @@ test("M3 IO control blocks invalid port direction instead of allowing generic wr
assert.equal(result.status, "blocked");
assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.portDirectionInvalid);
assert.equal(result.blockerClassification.category, "port_direction");
assert.match(result.blocker.zh, /端口方向|资源越界/u);
});
@@ -354,9 +403,173 @@ test("M3 IO control blocks when patch-panel wiring does not deliver DO1 to DI1",
assert.equal(result.status, "blocked");
assert.equal(result.accepted, false);
assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.wiringMissing);
assert.equal(result.blockerClassification.category, "patch_panel_wiring");
assert.match(result.blocker.zh, /wiring 不存在|未路由/u);
});
test("M3 IO control blocks when patch-panel reports missing target box", async () => {
const fixture = createM3ControlFixture({
patchPanelBody: {
accepted: false,
diagnostics: [
{
code: "target_endpoint_missing",
message: "target endpoint missing for res_boxsimu_2"
}
],
routes: []
}
});
const result = await handleM3IoControl(
{
action: "do.write",
value: true,
traceId: "trc_m3_target_box_missing",
requestId: "req_m3_target_box_missing",
actorId: "usr_m3_operator"
},
{
runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }),
now: () => fixedNow,
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
requestJson: fixture.requestJson
}
);
assert.equal(result.status, "blocked");
assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.boxUnavailable);
assert.equal(result.blockerClassification.category, "box_resource");
assert.match(result.blocker.zh, /target endpoint missing/u);
assert.equal(result.controlPath.cloudApi, true);
assert.equal(result.controlPath.frontendBypass, false);
});
test("M3 IO JSON-RPC methods route do.write and di.read through the cloud-api handler", async () => {
const fixture = createM3ControlFixture();
const runtimeStore = createCloudRuntimeStore({
now: () => fixedNow
});
const context = {
runtimeStore,
now: () => fixedNow,
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
m3IoRequestJson: fixture.requestJson
};
assert.ok(SUPPORTED_RPC_METHODS.includes(M3_IO_RPC_METHODS.doWrite));
assert.ok(SUPPORTED_RPC_METHODS.includes(M3_IO_RPC_METHODS.diRead));
const write = await handleJsonRpcRequest(
{
jsonrpc: "2.0",
id: "req_m3_rpc_write_true",
method: M3_IO_RPC_METHODS.doWrite,
params: {
resourceId: "res_boxsimu_1",
boxId: "boxsimu_1",
port: "DO1",
value: true
},
meta: {
traceId: "trc_m3_rpc_write_true",
actorId: "usr_m3_rpc",
serviceId: "hwlab-agent-worker",
environment: "dev"
}
},
context
);
validateResponse(write);
assert.equal(Object.hasOwn(write, "error"), false, write.error?.message);
assert.equal(write.result.status, "succeeded");
assert.equal(write.result.action, "do.write");
assert.equal(write.result.traceId, "trc_m3_rpc_write_true");
assert.equal(write.result.resourceId, "res_boxsimu_1");
assert.equal(write.result.gatewaySessionId, "gws_gwsimu_1");
assert.equal(write.result.port, "DO1");
assert.equal(write.result.value, true);
assert.match(write.result.operationId, /^op_m3_do_write_/u);
assert.match(write.result.auditId, /^aud_m3_do_write_/u);
assert.match(write.result.evidenceId, /^evd_m3_do_write_/u);
assert.equal(write.result.controlPath.frontendBypass, false);
assert.equal(write.result.evidenceState.sourceKind, "BLOCKED");
const read = await handleJsonRpcRequest(
{
jsonrpc: "2.0",
id: "req_m3_rpc_read_di",
method: M3_IO_RPC_METHODS.diRead,
params: {
resourceId: "res_boxsimu_2",
boxId: "boxsimu_2",
port: "DI1"
},
meta: {
traceId: "trc_m3_rpc_read_di",
actorId: "usr_m3_rpc",
serviceId: "hwlab-agent-worker",
environment: "dev"
}
},
context
);
validateResponse(read);
assert.equal(Object.hasOwn(read, "error"), false, read.error?.message);
assert.equal(read.result.status, "succeeded");
assert.equal(read.result.action, "di.read");
assert.equal(read.result.traceId, "trc_m3_rpc_read_di");
assert.equal(read.result.gatewayId, "gwsimu_2");
assert.equal(read.result.gatewaySessionId, "gws_gwsimu_2");
assert.equal(read.result.resourceId, "res_boxsimu_2");
assert.equal(read.result.port, "DI1");
assert.equal(read.result.result.value, true);
assert.equal(read.result.controlPath.patchPanel, false);
assert.equal(read.result.evidenceState.sourceKind, "BLOCKED");
assert.equal(read.result.durableStatus.durable, false);
});
test("M3 IO fixture success cannot be promoted to DEV-LIVE without durable evidence", async () => {
const fixture = createM3ControlFixture();
const result = await handleM3IoControl(
{
action: "do.write",
value: true,
traceId: "trc_m3_fixture_not_dev_live",
requestId: "req_m3_fixture_not_dev_live",
actorId: "usr_m3_operator"
},
{
runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }),
now: () => fixedNow,
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
requestJson: fixture.requestJson
}
);
assert.equal(result.status, "succeeded");
assert.equal(result.result.targetReadback.value, true);
assert.equal(result.evidenceState.status, "blocked");
assert.equal(result.evidenceState.sourceKind, "BLOCKED");
assert.equal(result.durableStatus.durable, false);
assert.notEqual(result.evidenceState.sourceKind, "DEV-LIVE");
assert.notEqual(result.auditState.sourceKind, "DEV-LIVE");
});
function createBlockedDurableRuntimeStore() {
return {
async readiness() {
+4 -2
View File
@@ -157,7 +157,8 @@ async function handleRpcHttpRequest(request, response, options) {
adapter: "json-rpc",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson
});
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
@@ -262,7 +263,8 @@ async function handleRestAdapter(request, response, url, options) {
adapter: "rest",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson
}
);
+2
View File
@@ -1165,6 +1165,8 @@ test("cloud api /v1 exposes M3 IO control contract without generic frontend hard
const indexPayload = await index.json();
assert.equal(indexPayload.m3IoControl.route, "/v1/m3/io");
assert.equal(indexPayload.m3IoControl.boundaries.directFrontendGatewayOrBoxAccess, false);
assert.ok(indexPayload.methods.includes("m3.io.do.write"));
assert.ok(indexPayload.methods.includes("m3.io.di.read"));
const contract = await fetch(`http://127.0.0.1:${port}/v1/m3/io`);
assert.equal(contract.status, 200);
+21 -1
View File
@@ -224,8 +224,9 @@ export async function buildM3IoControlSourceReport(options = {}) {
}
export async function runM3IoControlSourceChecks({ repoRoot: root = repoRoot } = {}) {
const [serverSource, m3Source, appSource, htmlSource, artifactPublisherSource] = await Promise.all([
const [serverSource, jsonRpcSource, m3Source, appSource, htmlSource, artifactPublisherSource] = await Promise.all([
readRepoText(root, "internal/cloud/server.mjs"),
readRepoText(root, "internal/cloud/json-rpc.mjs"),
readRepoText(root, "internal/cloud/m3-io-control.mjs"),
readRepoText(root, "web/hwlab-cloud-web/app.mjs"),
readRepoText(root, "web/hwlab-cloud-web/index.html"),
@@ -278,6 +279,18 @@ export async function runM3IoControlSourceChecks({ repoRoot: root = repoRoot } =
evidenceLevel: labels.source
}
),
checkResult(
"cloud-api-json-rpc-method-contract",
jsonRpcSource.includes("M3_IO_RPC_METHODS.doWrite") &&
jsonRpcSource.includes("M3_IO_RPC_METHODS.diRead") &&
jsonRpcSource.includes('handleM3IoRpc("do.write"') &&
jsonRpcSource.includes('handleM3IoRpc("di.read"'),
"cloud-api JSON-RPC exposes M3 do.write and di.read through the same controlled handler.",
{
methods: ["m3.io.do.write", "m3.io.di.read"],
evidenceLevel: labels.source
}
),
checkResult(
"request-schema-contract",
requestSchema.status === "pass",
@@ -416,7 +429,14 @@ export function validateM3IoSourceResponseContract(m3Source) {
/traceId:\s*input\.meta\.traceId/u,
/auditId:\s*records\.auditEvent\.auditId/u,
/evidenceId:\s*records\.evidenceRecord\.evidenceId/u,
/gatewaySessionId:\s*target\.gatewaySessionId/u,
/resourceId:\s*target\.resourceId/u,
/port:\s*target\.port/u,
/value:\s*target\.value/u,
/auditState:\s*auditState\(/u,
/evidenceState:\s*evidenceState\(/u,
/durableStatus:\s*durableStatus\(/u,
/blockerClassification:\s*redactedBlockerClassification\(/u,
/persistence:\s*persistenceSummary\(/u,
/frontendBypass:\s*false/u,
/patchPanelOnlyPropagation:\s*true/u,