docs: add evidence chain fixtures

This commit is contained in:
HWLAB Code Queue
2026-05-21 15:04:27 +00:00
parent e039deea10
commit 440ef6adaf
7 changed files with 934 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
# Evidence Chain Contract
The evidence chain contract fixes the MVP relationship between L1 audit records
and L4 agent or worker evidence without changing either implementation surface.
It is a fixture-level contract for now: services may persist the records in their
existing tables, while the chain example binds the identities that must not
split later.
## Minimal ID Relationship
Every MVP evidence chain has one `operationId`, one `traceId`, one or more
`auditId` values, and one or more `evidenceId` values.
- `operationId` is the workflow anchor. It must match the
`hardwareOperation.operationId`, every chain `auditEvents[].operationId`,
every `traceEvents[].operationId`, and every
`evidenceRecords[].operationId`.
- `traceId` is the request and execution correlation ID. It must match every
`auditEvents[].traceId` and every `traceEvents[].traceId`. Because the L0
`evidence-record` schema does not carry `traceId` at the top level, MVP
evidence records must copy it to `evidenceRecords[].metadata.traceId`.
- `auditId` is unique per audit event in a chain. Each evidence record must set
`metadata.producedByAuditId` to an audit event in the same fixture. The audit
event that records evidence must target that evidence record with
`targetType: "evidence_record"` and `targetId: <evidenceId>`.
- `evidenceId` is the stable artifact record ID. It must be unique in a chain,
belong to the same `projectId`, `operationId`, and `environment`, and point to
a content-addressed artifact through `uri`, `sha256`, and optional
`sizeBytes`.
The same relationship applies whether the producer is a hardware trusted loop
or an agent automation loop. Agent and worker IDs are optional for hardware-only
chains, but when present they must be carried consistently by the operation,
trace events, and evidence records.
## MVP Required Fields
The fixture root is not a database table. It exists to verify cross-record
relationships and must include:
- `chainId`
- `schemaVersion`
- `scenario`
- `projectId`
- `operationId`
- `traceId`
- `environment`
- `hardwareOperation`
- `auditEvents`
- `traceEvents`
- `evidenceRecords`
`hardwareOperation` must satisfy the L0 hardware operation required fields:
`operationId`, `projectId`, `gatewaySessionId`, `resourceId`, `capabilityId`,
`requestedBy`, `status`, `environment`, `requestedAt`, and `updatedAt`.
Each MVP `auditEvents[]` entry must satisfy the L0 audit event required fields
and additionally include `projectId`, `operationId`, and `outcome` so the audit
stream can be joined back to the operation without service-specific inference.
Each MVP `traceEvents[]` entry must satisfy the L0 trace event required fields
and additionally include `operationId`.
Each MVP `evidenceRecords[]` entry must satisfy the L0 evidence record required
fields and additionally include:
- `metadata.traceId`
- `metadata.producedByAuditId`
Agent automation chains must also include `agentSessionId` and
`workerSessionId` at the fixture root, on the hardware operation, on trace
events, and on evidence records.
## Future Enhancements
The first version intentionally avoids heavier guarantees. Later contracts may
add:
- canonical chain hashing, previous-record links, and signatures
- attestation data from real hardware roots of trust
- retention policy, storage tier, and artifact redaction metadata
- richer OpenTelemetry or JSON-LD correlation fields
- multiple operations under one trace with explicit parent-child edges
- policy decision records for agent authorization and remediation
Those additions must preserve the MVP identity bindings above.
## Fixtures
The MVP fixture set lives under `protocol/examples/evidence-chain`:
- `fixtures/hardware-trusted-loop.json`
- `fixtures/agent-automation-loop.json`
- `artifacts/hardware-trusted-measurement.json`
- `artifacts/agent-automation-report.json`
Validate the examples with:
```sh
node scripts/validate-evidence-chain.mjs
```
@@ -0,0 +1,12 @@
# Evidence Chain Examples
These fixtures model the minimum MVP chain between hardware operations, audit
events, trace events, and evidence records.
- `fixtures/hardware-trusted-loop.json` covers the hardware trusted closed loop.
- `fixtures/agent-automation-loop.json` covers the agent automation closed loop.
- `artifacts/` contains small content-addressed files referenced by the
evidence records.
Run `node scripts/validate-evidence-chain.mjs` from the repository root to
verify field completeness, cross-record references, and artifact checksums.
@@ -0,0 +1,13 @@
{
"format": "hwlab.agent-report.v1",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"result": "succeeded",
"steps": [
"selected capability",
"ran firmware smoke command",
"persisted evidence"
],
"createdAt": "2026-05-21T00:20:06.000Z"
}
@@ -0,0 +1,15 @@
{
"format": "hwlab.measurement.v1",
"resourceId": "res_boxsim-alpha",
"capabilityId": "cap_gpio-measure",
"operationId": "op_hw-trusted-loop-001",
"samples": [
{
"name": "gpio0",
"unit": "logic",
"value": 1,
"trusted": true
}
],
"capturedAt": "2026-05-21T00:10:03.000Z"
}
@@ -0,0 +1,235 @@
{
"chainId": "echain_agent-auto-loop",
"schemaVersion": 1,
"scenario": "agent-automation-closed-loop",
"title": "Agent automation closed loop MVP",
"environment": "dev",
"projectId": "prj_evidence-chain",
"operationId": "op_agent-auto-loop-001",
"traceId": "trc_agent-auto-loop-001",
"gatewaySessionId": "gws_evidence-gateway-001",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"resourceId": "res_boxsim-beta",
"capabilityId": "cap_firmware-smoke",
"hardwareOperation": {
"operationId": "op_agent-auto-loop-001",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"resourceId": "res_boxsim-beta",
"capabilityId": "cap_firmware-smoke",
"requestedBy": "wkr_agent-auto-001",
"input": {
"command": "firmware-smoke",
"timeoutSeconds": 60
},
"output": {
"exitCode": 0,
"result": "passed"
},
"status": "succeeded",
"environment": "dev",
"requestedAt": "2026-05-21T00:20:02.000Z",
"startedAt": "2026-05-21T00:20:03.000Z",
"completedAt": "2026-05-21T00:20:05.000Z",
"updatedAt": "2026-05-21T00:20:05.000Z"
},
"auditEvents": [
{
"auditId": "aud_agent-auto-session-started",
"traceId": "trc_agent-auto-loop-001",
"actorType": "user",
"actorId": "usr_alice",
"action": "agent.session.started",
"targetType": "agent_session",
"targetId": "ags_agent-auto-001",
"projectId": "prj_evidence-chain",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-agent-mgr",
"environment": "dev",
"outcome": "accepted",
"metadata": {
"agentSessionId": "ags_agent-auto-001",
"goal": "run firmware smoke and persist evidence"
},
"occurredAt": "2026-05-21T00:20:00.000Z"
},
{
"auditId": "aud_agent-auto-worker-started",
"traceId": "trc_agent-auto-loop-001",
"actorType": "agent",
"actorId": "agt_agent-auto-001",
"action": "worker.session.started",
"targetType": "worker_session",
"targetId": "wks_agent-auto-001",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"outcome": "accepted",
"metadata": {
"agentSessionId": "ags_agent-auto-001"
},
"occurredAt": "2026-05-21T00:20:01.000Z"
},
{
"auditId": "aud_agent-auto-operation-requested",
"traceId": "trc_agent-auto-loop-001",
"actorType": "worker",
"actorId": "wkr_agent-auto-001",
"action": "hardware.operation.requested",
"targetType": "hardware_operation",
"targetId": "op_agent-auto-loop-001",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"outcome": "accepted",
"metadata": {
"agentSessionId": "ags_agent-auto-001",
"capabilityId": "cap_firmware-smoke"
},
"occurredAt": "2026-05-21T00:20:02.000Z"
},
{
"auditId": "aud_agent-auto-operation-succeeded",
"traceId": "trc_agent-auto-loop-001",
"actorType": "service",
"actorId": "svc_hwlab-box-simu",
"action": "hardware.operation.succeeded",
"targetType": "hardware_operation",
"targetId": "op_agent-auto-loop-001",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-box-simu",
"environment": "dev",
"outcome": "succeeded",
"metadata": {
"agentSessionId": "ags_agent-auto-001",
"exitCode": 0
},
"occurredAt": "2026-05-21T00:20:05.000Z"
},
{
"auditId": "aud_agent-auto-evidence-recorded",
"traceId": "trc_agent-auto-loop-001",
"actorType": "worker",
"actorId": "wkr_agent-auto-001",
"action": "evidence.recorded",
"targetType": "evidence_record",
"targetId": "evd_agent-auto-report",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"outcome": "succeeded",
"metadata": {
"agentSessionId": "ags_agent-auto-001",
"evidenceId": "evd_agent-auto-report",
"evidenceKind": "report"
},
"occurredAt": "2026-05-21T00:20:06.000Z"
}
],
"traceEvents": [
{
"traceEventId": "tev_agent-auto-goal",
"traceId": "trc_agent-auto-loop-001",
"projectId": "prj_evidence-chain",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-agent-mgr",
"level": "info",
"message": "Agent session accepted automation goal",
"environment": "dev",
"metadata": {
"auditId": "aud_agent-auto-session-started"
},
"occurredAt": "2026-05-21T00:20:00.000Z"
},
{
"traceEventId": "tev_agent-auto-worker",
"traceId": "trc_agent-auto-loop-001",
"projectId": "prj_evidence-chain",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-agent-worker",
"level": "info",
"message": "Worker requested firmware smoke operation",
"environment": "dev",
"metadata": {
"auditId": "aud_agent-auto-operation-requested"
},
"occurredAt": "2026-05-21T00:20:02.000Z"
},
{
"traceEventId": "tev_agent-auto-operation",
"traceId": "trc_agent-auto-loop-001",
"projectId": "prj_evidence-chain",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-box-simu",
"level": "info",
"message": "Firmware smoke operation completed",
"environment": "dev",
"metadata": {
"auditId": "aud_agent-auto-operation-succeeded"
},
"occurredAt": "2026-05-21T00:20:05.000Z"
},
{
"traceEventId": "tev_agent-auto-evidence",
"traceId": "trc_agent-auto-loop-001",
"projectId": "prj_evidence-chain",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"operationId": "op_agent-auto-loop-001",
"serviceId": "hwlab-agent-worker",
"level": "info",
"message": "Agent report evidence persisted",
"environment": "dev",
"metadata": {
"auditId": "aud_agent-auto-evidence-recorded",
"evidenceId": "evd_agent-auto-report"
},
"occurredAt": "2026-05-21T00:20:06.000Z"
}
],
"evidenceRecords": [
{
"evidenceId": "evd_agent-auto-report",
"projectId": "prj_evidence-chain",
"operationId": "op_agent-auto-loop-001",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"kind": "report",
"uri": "protocol/examples/evidence-chain/artifacts/agent-automation-report.json",
"mimeType": "application/json",
"sha256": "2fc63687cd3170c9cf34cd650aaedfb84d3fe75683ae969ae286230ce3b425a7",
"sizeBytes": 340,
"serviceId": "hwlab-agent-worker",
"environment": "dev",
"metadata": {
"traceId": "trc_agent-auto-loop-001",
"producedByAuditId": "aud_agent-auto-evidence-recorded",
"agentSessionId": "ags_agent-auto-001",
"workerSessionId": "wks_agent-auto-001",
"producerServiceId": "hwlab-agent-worker"
},
"createdAt": "2026-05-21T00:20:06.000Z"
}
]
}
@@ -0,0 +1,182 @@
{
"chainId": "echain_hw-trusted-loop",
"schemaVersion": 1,
"scenario": "hardware-trusted-closed-loop",
"title": "Hardware trusted closed loop MVP",
"environment": "dev",
"projectId": "prj_evidence-chain",
"operationId": "op_hw-trusted-loop-001",
"traceId": "trc_hw-trusted-loop-001",
"gatewaySessionId": "gws_evidence-gateway-001",
"resourceId": "res_boxsim-alpha",
"capabilityId": "cap_gpio-measure",
"hardwareOperation": {
"operationId": "op_hw-trusted-loop-001",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"resourceId": "res_boxsim-alpha",
"capabilityId": "cap_gpio-measure",
"requestedBy": "usr_alice",
"input": {
"command": "measure",
"port": "gpio0"
},
"output": {
"sampleCount": 1,
"result": "logic-high"
},
"status": "succeeded",
"environment": "dev",
"requestedAt": "2026-05-21T00:10:00.000Z",
"startedAt": "2026-05-21T00:10:01.000Z",
"completedAt": "2026-05-21T00:10:03.000Z",
"updatedAt": "2026-05-21T00:10:03.000Z"
},
"auditEvents": [
{
"auditId": "aud_hw-trusted-requested",
"traceId": "trc_hw-trusted-loop-001",
"actorType": "user",
"actorId": "usr_alice",
"action": "hardware.operation.requested",
"targetType": "hardware_operation",
"targetId": "op_hw-trusted-loop-001",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"operationId": "op_hw-trusted-loop-001",
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"outcome": "accepted",
"metadata": {
"requestId": "req_hw-trusted-001"
},
"occurredAt": "2026-05-21T00:10:00.000Z"
},
{
"auditId": "aud_hw-trusted-dispatched",
"traceId": "trc_hw-trusted-loop-001",
"actorType": "service",
"actorId": "svc_hwlab-gateway-simu",
"action": "hardware.operation.dispatched",
"targetType": "hardware_operation",
"targetId": "op_hw-trusted-loop-001",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"operationId": "op_hw-trusted-loop-001",
"serviceId": "hwlab-gateway-simu",
"environment": "dev",
"outcome": "accepted",
"metadata": {
"resourceId": "res_boxsim-alpha",
"capabilityId": "cap_gpio-measure"
},
"occurredAt": "2026-05-21T00:10:01.000Z"
},
{
"auditId": "aud_hw-trusted-succeeded",
"traceId": "trc_hw-trusted-loop-001",
"actorType": "service",
"actorId": "svc_hwlab-box-simu",
"action": "hardware.operation.succeeded",
"targetType": "hardware_operation",
"targetId": "op_hw-trusted-loop-001",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"operationId": "op_hw-trusted-loop-001",
"serviceId": "hwlab-box-simu",
"environment": "dev",
"outcome": "succeeded",
"metadata": {
"sampleCount": 1
},
"occurredAt": "2026-05-21T00:10:03.000Z"
},
{
"auditId": "aud_hw-trusted-evidence-recorded",
"traceId": "trc_hw-trusted-loop-001",
"actorType": "service",
"actorId": "svc_hwlab-cloud-api",
"action": "evidence.recorded",
"targetType": "evidence_record",
"targetId": "evd_hw-trusted-measurement",
"projectId": "prj_evidence-chain",
"gatewaySessionId": "gws_evidence-gateway-001",
"operationId": "op_hw-trusted-loop-001",
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"outcome": "succeeded",
"metadata": {
"evidenceId": "evd_hw-trusted-measurement",
"evidenceKind": "measurement"
},
"occurredAt": "2026-05-21T00:10:04.000Z"
}
],
"traceEvents": [
{
"traceEventId": "tev_hw-trusted-accepted",
"traceId": "trc_hw-trusted-loop-001",
"projectId": "prj_evidence-chain",
"operationId": "op_hw-trusted-loop-001",
"serviceId": "hwlab-cloud-api",
"level": "info",
"message": "Hardware operation accepted",
"environment": "dev",
"metadata": {
"auditId": "aud_hw-trusted-requested"
},
"occurredAt": "2026-05-21T00:10:00.000Z"
},
{
"traceEventId": "tev_hw-trusted-dispatched",
"traceId": "trc_hw-trusted-loop-001",
"projectId": "prj_evidence-chain",
"operationId": "op_hw-trusted-loop-001",
"serviceId": "hwlab-gateway-simu",
"level": "info",
"message": "Gateway dispatched trusted measurement",
"environment": "dev",
"metadata": {
"auditId": "aud_hw-trusted-dispatched"
},
"occurredAt": "2026-05-21T00:10:01.000Z"
},
{
"traceEventId": "tev_hw-trusted-evidence",
"traceId": "trc_hw-trusted-loop-001",
"projectId": "prj_evidence-chain",
"operationId": "op_hw-trusted-loop-001",
"serviceId": "hwlab-cloud-api",
"level": "info",
"message": "Measurement evidence persisted",
"environment": "dev",
"metadata": {
"auditId": "aud_hw-trusted-evidence-recorded",
"evidenceId": "evd_hw-trusted-measurement"
},
"occurredAt": "2026-05-21T00:10:04.000Z"
}
],
"evidenceRecords": [
{
"evidenceId": "evd_hw-trusted-measurement",
"projectId": "prj_evidence-chain",
"operationId": "op_hw-trusted-loop-001",
"kind": "measurement",
"uri": "protocol/examples/evidence-chain/artifacts/hardware-trusted-measurement.json",
"mimeType": "application/json",
"sha256": "da1bb981f0a966e6906bfdf6a0387180617c8f891c131d7bebf9e19b14989a99",
"sizeBytes": 318,
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"metadata": {
"traceId": "trc_hw-trusted-loop-001",
"producedByAuditId": "aud_hw-trusted-evidence-recorded",
"producerServiceId": "hwlab-box-simu",
"resourceId": "res_boxsim-alpha",
"capabilityId": "cap_gpio-measure"
},
"createdAt": "2026-05-21T00:10:04.000Z"
}
]
}
+376
View File
@@ -0,0 +1,376 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const fixturesDir = "protocol/examples/evidence-chain/fixtures";
const idPattern = /^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/;
const actionPattern = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
const sha256Pattern = /^[a-f0-9]{64}$/;
const rootRequired = [
"chainId",
"schemaVersion",
"scenario",
"projectId",
"operationId",
"traceId",
"environment",
"hardwareOperation",
"auditEvents",
"traceEvents",
"evidenceRecords"
];
const hardwareOperationRequired = [
"operationId",
"projectId",
"gatewaySessionId",
"resourceId",
"capabilityId",
"requestedBy",
"status",
"environment",
"requestedAt",
"updatedAt"
];
const auditEventRequired = [
"auditId",
"traceId",
"actorType",
"actorId",
"action",
"targetType",
"targetId",
"serviceId",
"environment",
"occurredAt",
"projectId",
"operationId",
"outcome"
];
const traceEventRequired = [
"traceEventId",
"traceId",
"projectId",
"serviceId",
"level",
"message",
"environment",
"occurredAt",
"operationId"
];
const evidenceRecordRequired = [
"evidenceId",
"projectId",
"operationId",
"kind",
"uri",
"sha256",
"serviceId",
"environment",
"createdAt"
];
const operationStatuses = new Set([
"requested",
"accepted",
"queued",
"running",
"succeeded",
"failed",
"rejected",
"canceled",
"timed_out"
]);
const actorTypes = new Set(["user", "service", "agent", "worker", "system"]);
const auditOutcomes = new Set(["accepted", "succeeded", "failed", "rejected", "canceled"]);
const traceLevels = new Set(["debug", "info", "warn", "error"]);
const evidenceKinds = new Set(["log", "screenshot", "measurement", "artifact", "report", "trace"]);
const scenarios = new Set(["hardware-trusted-closed-loop", "agent-automation-closed-loop"]);
function assertObject(value, label) {
assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
}
function assertRequired(value, fields, label) {
for (const field of fields) {
assert.ok(Object.hasOwn(value, field), `${label} missing ${field}`);
}
}
function assertId(value, label) {
assert.equal(typeof value, "string", `${label} must be a string`);
assert.match(value, idPattern, `${label} must match HWLAB id format`);
}
function assertTimestamp(value, label) {
assert.equal(typeof value, "string", `${label} must be a string`);
assert.ok(!Number.isNaN(Date.parse(value)), `${label} must be a parseable timestamp`);
}
function assertServiceId(value, label) {
assert.ok(SERVICE_IDS.includes(value), `${label} must be a frozen service id`);
}
function assertUnique(values, label) {
assert.equal(new Set(values).size, values.length, `${label} must be unique`);
}
function assertSame(actual, expected, label) {
assert.equal(actual, expected, `${label} must match ${expected}`);
}
async function parseFixtures() {
const entries = await readdir(path.join(repoRoot, fixturesDir), { withFileTypes: true });
const files = entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
.map((entry) => path.join(fixturesDir, entry.name))
.sort();
const fixtures = [];
for (const relativePath of files) {
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
fixtures.push({ relativePath, doc: JSON.parse(raw) });
}
return fixtures;
}
async function assertArtifact(record, label) {
assert.match(record.sha256, sha256Pattern, `${label}.sha256 must be lowercase sha256`);
assert.equal(typeof record.uri, "string", `${label}.uri must be a string`);
const artifactPath = path.resolve(repoRoot, record.uri);
assert.ok(
artifactPath.startsWith(`${repoRoot}${path.sep}`),
`${label}.uri must point inside the repository`
);
const content = await readFile(artifactPath);
const digest = createHash("sha256").update(content).digest("hex");
assert.equal(digest, record.sha256, `${label}.sha256 must match ${record.uri}`);
if (Object.hasOwn(record, "sizeBytes")) {
assert.equal(record.sizeBytes, content.byteLength, `${label}.sizeBytes must match ${record.uri}`);
}
}
function validateHardwareOperation(chain, operation) {
assertObject(operation, `${chain.chainId}.hardwareOperation`);
assertRequired(operation, hardwareOperationRequired, `${chain.chainId}.hardwareOperation`);
for (const field of [
"operationId",
"projectId",
"gatewaySessionId",
"resourceId",
"capabilityId",
"requestedBy"
]) {
assertId(operation[field], `${chain.chainId}.hardwareOperation.${field}`);
}
assertSame(operation.operationId, chain.operationId, `${chain.chainId}.hardwareOperation.operationId`);
assertSame(operation.projectId, chain.projectId, `${chain.chainId}.hardwareOperation.projectId`);
assertSame(operation.environment, chain.environment, `${chain.chainId}.hardwareOperation.environment`);
assert.ok(operationStatuses.has(operation.status), `${chain.chainId}.hardwareOperation.status is invalid`);
assertTimestamp(operation.requestedAt, `${chain.chainId}.hardwareOperation.requestedAt`);
assertTimestamp(operation.updatedAt, `${chain.chainId}.hardwareOperation.updatedAt`);
for (const field of ["startedAt", "completedAt"]) {
if (Object.hasOwn(operation, field)) {
assertTimestamp(operation[field], `${chain.chainId}.hardwareOperation.${field}`);
}
}
if (chain.agentSessionId) {
assertSame(operation.agentSessionId, chain.agentSessionId, `${chain.chainId}.hardwareOperation.agentSessionId`);
}
if (chain.workerSessionId) {
assertSame(operation.workerSessionId, chain.workerSessionId, `${chain.chainId}.hardwareOperation.workerSessionId`);
}
}
function validateAuditEvents(chain, auditEvents) {
assert.ok(Array.isArray(auditEvents), `${chain.chainId}.auditEvents must be an array`);
assert.ok(auditEvents.length >= 2, `${chain.chainId}.auditEvents must include operation and evidence audit events`);
assertUnique(
auditEvents.map((event) => event.auditId),
`${chain.chainId}.auditId values`
);
for (const [index, event] of auditEvents.entries()) {
const label = `${chain.chainId}.auditEvents[${index}]`;
assertObject(event, label);
assertRequired(event, auditEventRequired, label);
for (const field of ["auditId", "traceId", "actorId", "targetId", "projectId", "operationId"]) {
assertId(event[field], `${label}.${field}`);
}
assertSame(event.traceId, chain.traceId, `${label}.traceId`);
assertSame(event.projectId, chain.projectId, `${label}.projectId`);
assertSame(event.operationId, chain.operationId, `${label}.operationId`);
assertSame(event.environment, chain.environment, `${label}.environment`);
assert.ok(actorTypes.has(event.actorType), `${label}.actorType is invalid`);
assert.match(event.action, actionPattern, `${label}.action must be dotted`);
assert.equal(typeof event.targetType, "string", `${label}.targetType must be a string`);
assertServiceId(event.serviceId, `${label}.serviceId`);
assert.ok(auditOutcomes.has(event.outcome), `${label}.outcome is invalid`);
assertTimestamp(event.occurredAt, `${label}.occurredAt`);
if (Object.hasOwn(event, "gatewaySessionId")) {
assertSame(event.gatewaySessionId, chain.gatewaySessionId, `${label}.gatewaySessionId`);
}
if (chain.workerSessionId && Object.hasOwn(event, "workerSessionId")) {
assertSame(event.workerSessionId, chain.workerSessionId, `${label}.workerSessionId`);
}
}
assert.ok(
auditEvents.some((event) => event.targetType === "hardware_operation" && event.targetId === chain.operationId),
`${chain.chainId} must audit the hardware operation`
);
}
function validateTraceEvents(chain, traceEvents, auditIds) {
assert.ok(Array.isArray(traceEvents), `${chain.chainId}.traceEvents must be an array`);
assert.ok(traceEvents.length >= 1, `${chain.chainId}.traceEvents must not be empty`);
assertUnique(
traceEvents.map((event) => event.traceEventId),
`${chain.chainId}.traceEventId values`
);
for (const [index, event] of traceEvents.entries()) {
const label = `${chain.chainId}.traceEvents[${index}]`;
assertObject(event, label);
assertRequired(event, traceEventRequired, label);
for (const field of ["traceEventId", "traceId", "projectId", "operationId"]) {
assertId(event[field], `${label}.${field}`);
}
assertSame(event.traceId, chain.traceId, `${label}.traceId`);
assertSame(event.projectId, chain.projectId, `${label}.projectId`);
assertSame(event.operationId, chain.operationId, `${label}.operationId`);
assertSame(event.environment, chain.environment, `${label}.environment`);
assertServiceId(event.serviceId, `${label}.serviceId`);
assert.ok(traceLevels.has(event.level), `${label}.level is invalid`);
assert.equal(typeof event.message, "string", `${label}.message must be a string`);
assert.ok(event.message.length > 0, `${label}.message must not be empty`);
assertTimestamp(event.occurredAt, `${label}.occurredAt`);
if (chain.agentSessionId) {
assertSame(event.agentSessionId, chain.agentSessionId, `${label}.agentSessionId`);
}
if (chain.workerSessionId) {
assertSame(event.workerSessionId, chain.workerSessionId, `${label}.workerSessionId`);
}
if (event.metadata?.auditId) {
assert.ok(auditIds.has(event.metadata.auditId), `${label}.metadata.auditId must reference same-chain audit`);
}
}
}
async function validateEvidenceRecords(chain, evidenceRecords, auditEvents) {
assert.ok(Array.isArray(evidenceRecords), `${chain.chainId}.evidenceRecords must be an array`);
assert.ok(evidenceRecords.length >= 1, `${chain.chainId}.evidenceRecords must not be empty`);
assertUnique(
evidenceRecords.map((record) => record.evidenceId),
`${chain.chainId}.evidenceId values`
);
const auditsById = new Map(auditEvents.map((event) => [event.auditId, event]));
for (const [index, record] of evidenceRecords.entries()) {
const label = `${chain.chainId}.evidenceRecords[${index}]`;
assertObject(record, label);
assertRequired(record, evidenceRecordRequired, label);
for (const field of ["evidenceId", "projectId", "operationId"]) {
assertId(record[field], `${label}.${field}`);
}
assertSame(record.projectId, chain.projectId, `${label}.projectId`);
assertSame(record.operationId, chain.operationId, `${label}.operationId`);
assertSame(record.environment, chain.environment, `${label}.environment`);
assert.ok(evidenceKinds.has(record.kind), `${label}.kind is invalid`);
assertServiceId(record.serviceId, `${label}.serviceId`);
assertTimestamp(record.createdAt, `${label}.createdAt`);
assertObject(record.metadata, `${label}.metadata`);
assertSame(record.metadata.traceId, chain.traceId, `${label}.metadata.traceId`);
assertId(record.metadata.producedByAuditId, `${label}.metadata.producedByAuditId`);
const producerAudit = auditsById.get(record.metadata.producedByAuditId);
assert.ok(producerAudit, `${label}.metadata.producedByAuditId must reference same-chain audit`);
assert.equal(producerAudit.targetType, "evidence_record", `${label} producer audit must target evidence_record`);
assert.equal(producerAudit.targetId, record.evidenceId, `${label} producer audit must target this evidenceId`);
if (chain.agentSessionId) {
assertSame(record.agentSessionId, chain.agentSessionId, `${label}.agentSessionId`);
assertSame(record.metadata.agentSessionId, chain.agentSessionId, `${label}.metadata.agentSessionId`);
}
if (chain.workerSessionId) {
assertSame(record.workerSessionId, chain.workerSessionId, `${label}.workerSessionId`);
assertSame(record.metadata.workerSessionId, chain.workerSessionId, `${label}.metadata.workerSessionId`);
}
await assertArtifact(record, label);
}
}
async function validateFixture({ relativePath, doc }) {
assertObject(doc, relativePath);
assertRequired(doc, rootRequired, relativePath);
for (const field of ["chainId", "projectId", "operationId", "traceId"]) {
assertId(doc[field], `${relativePath}.${field}`);
}
assert.equal(doc.schemaVersion, 1, `${relativePath}.schemaVersion must be 1`);
assert.ok(scenarios.has(doc.scenario), `${relativePath}.scenario is not recognized`);
assert.equal(doc.environment, ENVIRONMENT_DEV, `${relativePath}.environment must be dev`);
for (const field of ["gatewaySessionId", "resourceId", "capabilityId", "agentSessionId", "workerSessionId"]) {
if (Object.hasOwn(doc, field)) {
assertId(doc[field], `${relativePath}.${field}`);
}
}
if (doc.scenario === "agent-automation-closed-loop") {
assertId(doc.agentSessionId, `${relativePath}.agentSessionId`);
assertId(doc.workerSessionId, `${relativePath}.workerSessionId`);
}
validateHardwareOperation(doc, doc.hardwareOperation);
validateAuditEvents(doc, doc.auditEvents);
const auditIds = new Set(doc.auditEvents.map((event) => event.auditId));
validateTraceEvents(doc, doc.traceEvents, auditIds);
await validateEvidenceRecords(doc, doc.evidenceRecords, doc.auditEvents);
}
const fixtures = await parseFixtures();
assert.ok(fixtures.length >= 2, "expected at least two MVP evidence chain fixtures");
const scenarioSet = new Set(fixtures.map(({ doc }) => doc.scenario));
for (const scenario of scenarios) {
assert.ok(scenarioSet.has(scenario), `missing ${scenario} fixture`);
}
for (const fixture of fixtures) {
await validateFixture(fixture);
}
const evidenceCount = fixtures.reduce((count, { doc }) => count + doc.evidenceRecords.length, 0);
console.log(`validated ${fixtures.length} evidence chain fixtures and ${evidenceCount} evidence records`);