docs: add evidence chain fixtures
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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`);
|
||||
Reference in New Issue
Block a user