#!/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`);