Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/local-audit-event-schema.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Local Audit Event Schema

`aethermesh_core.local_audit_event` defines the small, stable v1 event format for a local JSONL audit log. It makes prototype actions traceable on one machine. It is not a production security log, peer protocol, blockchain, consensus record, reward ledger, or claim of decentralized finality.
`aethermesh_core.local_audit_event` defines the small, stable v2 event format for a local JSONL audit log. It makes prototype actions traceable on one machine. It is not a production security log, peer protocol, blockchain, consensus record, reward ledger, or claim of decentralized finality.

## Storage and append-only rule

Expand All @@ -10,13 +10,16 @@ Store one event per UTF-8 newline-delimited JSON (JSONL) line, normally under th

| Field | Type | Meaning |
| --- | --- | --- |
| `schema_version` | integer `1` | Event-format version. |
| `schema_version` | integer `2` | Event-format version. |
| `event_id` | non-empty string | Locally unique event identifier chosen by the caller. |
| `timestamp` | UTC timestamp (`YYYY-MM-DDTHH:MM:SSZ`) | Human-readable timestamp recorded by the caller. |
| `event_type` | enum | Prototype action listed below. |
| `actor_node_id` | non-empty string | Node performing or recording the action. |
| `creator_node_id` | non-empty string or `null` | Original creator node when known; `null` explicitly records that it is not known. |
| `local_run_id` | non-empty string | Local invocation/run that produced the event. |
| `event_sequence` | positive integer | Stable ordering position of the event within its local run. |

Version 2 adds `event_sequence` so events from separate JSONL files can be reconstructed in flow order without relying on equal-second timestamps. Existing version 1 lines remain immutable historical evidence; the current validator accepts newly produced version 2 events.

Supported event types are `node_initialized`, `manifest_created`, `work_submitted`, `job_submitted`, `validation_attempted`, `validation_result`, `validation_receipt_created`, `lineage_linked`, `contribution_record_updated`, `capability_advertised`, and `node.shutdown`.

Expand Down Expand Up @@ -62,13 +65,13 @@ A `validation_receipt_created` event is appended to `data/audit/validation-recei
A minimal initialization event intentionally has no optional references:

```json
{"actor_node_id":"local-node-a","creator_node_id":"local-node-a","event_id":"audit-init-001","event_type":"node_initialized","local_run_id":"run-001","schema_version":1,"timestamp":"2026-07-15T06:00:00Z"}
{"actor_node_id":"local-node-a","creator_node_id":"local-node-a","event_id":"audit-init-001","event_sequence":1,"event_type":"node_initialized","local_run_id":"run-001","schema_version":2,"timestamp":"2026-07-15T06:00:00Z"}
```

This validation-result event links local manifest, work, receipt, lineage, contribution attribution, and related artifacts without any network access:

```json
{"actor_node_id":"validator-local-a","contribution_attribution_ids":["local-contribution-work-001"],"creator_node_id":"creator-local-a","event_id":"audit-validation-001","event_type":"validation_result","hashes":{"result_hash":"sha256:example"},"lineage_parent_ids":["work-parent-001"],"local_run_id":"run-001","manifest_id":"manifest-work-001","related_file_paths":["data/manifests/work-001.json","data/receipts/work-001.json"],"schema_version":1,"timestamp":"2026-07-15T06:01:00Z","validation_receipt_id":"receipt-work-001","work_id":"work-001"}
{"actor_node_id":"validator-local-a","contribution_attribution_ids":["local-contribution-work-001"],"creator_node_id":"creator-local-a","event_id":"audit-validation-001","event_sequence":2,"event_type":"validation_result","hashes":{"result_hash":"sha256:example"},"lineage_parent_ids":["work-parent-001"],"local_run_id":"run-001","manifest_id":"manifest-work-001","related_file_paths":["data/manifests/work-001.json","data/receipts/work-001.json"],"schema_version":2,"timestamp":"2026-07-15T06:01:00Z","validation_receipt_id":"receipt-work-001","work_id":"work-001"}
```

The examples are individual JSONL lines. They remain readable with ordinary JSON tooling, and a local script can parse each non-empty line with `json.loads` before calling `validate_local_audit_event`.
3 changes: 2 additions & 1 deletion src/aethermesh_core/contribution_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,14 @@ def _append_ledger_update_audit(
"sha256:"
)
event: dict[str, Any] = {
"schema_version": 1,
"schema_version": 2,
"event_id": update_id,
"timestamp": _utc_timestamp(clock()),
"event_type": "contribution_record_updated",
"actor_node_id": contributor,
"creator_node_id": creator,
"local_run_id": update_id,
"event_sequence": 1,
"validation_status": outcome,
}
if contribution:
Expand Down
24 changes: 20 additions & 4 deletions src/aethermesh_core/local_audit_event.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
"""Canonical local JSONL audit events for prototype actions."""
"""Canonical local JSONL audit events for prototype actions.

Every event records UTC ``timestamp``, a stable per-flow ``local_run_id``, and
the positive ``event_sequence`` offset within that run. Together with actor
and creator IDs plus event-specific manifest, receipt, lineage, and
contribution references, these are the minimum local-only reconstruction
fields. References are relative paths to avoid disclosing host locations.
"""

from __future__ import annotations

Expand All @@ -9,7 +16,7 @@
from pathlib import Path, PureWindowsPath
from typing import Any

AUDIT_EVENT_SCHEMA_VERSION = 1
AUDIT_EVENT_SCHEMA_VERSION = 2
AUDIT_EVENT_TYPES = frozenset(
{
"node_initialized",
Expand All @@ -36,6 +43,7 @@
"actor_node_id",
"creator_node_id",
"local_run_id",
"event_sequence",
"schema_version",
}
)
Expand Down Expand Up @@ -87,7 +95,7 @@


class LocalAuditEventError(ValueError):
"""Raised when a local audit event does not match the stable v1 format."""
"""Raised when a local audit event does not match the canonical format."""


def validate_local_audit_event(event: Mapping[str, Any]) -> dict[str, Any]:
Expand All @@ -111,9 +119,17 @@ def validate_local_audit_event(event: Mapping[str, Any]) -> dict[str, Any]:
or isinstance(document["schema_version"], bool)
or document["schema_version"] != AUDIT_EVENT_SCHEMA_VERSION
):
raise LocalAuditEventError("local audit event.schema_version must be integer 1")
raise LocalAuditEventError("local audit event.schema_version must be integer 2")
for field in ("event_id", "actor_node_id", "local_run_id"):
_require_text(document[field], f"local audit event.{field}")
if (
not isinstance(document["event_sequence"], int)
or isinstance(document["event_sequence"], bool)
or document["event_sequence"] < 1
):
raise LocalAuditEventError(
"local audit event.event_sequence must be a positive integer"
)
_require_timestamp(document["timestamp"])
creator_node_id = document["creator_node_id"]
if creator_node_id is not None:
Expand Down
3 changes: 2 additions & 1 deletion src/aethermesh_core/local_shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,14 @@ def _append_shutdown_audit_event(
append_local_audit_event(
path,
{
"schema_version": 1,
"schema_version": 2,
"event_id": event_id,
"timestamp": timestamp,
"event_type": "node.shutdown",
"actor_node_id": node_id,
"creator_node_id": creator_node_id,
"local_run_id": node_instance_id,
"event_sequence": 1,
"node_instance_id": node_instance_id,
"shutdown_reason": "normal local shutdown",
"exit_mode": exit_mode,
Expand Down
7 changes: 5 additions & 2 deletions src/aethermesh_core/local_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,9 @@ def _append_capability_advertisement_audit_events(
) -> None:
"""Append one digest-only audit event for each validated local claim."""

for advertisement in cast(list[dict[str, object]], advertisements):
for event_sequence, advertisement in enumerate(
cast(list[dict[str, object]], advertisements), start=1
):
validation = cast(dict[str, object], advertisement["validation"])
attribution = cast(dict[str, object], advertisement["contribution_attribution"])
required_receipt_ref = cast(str, validation["required_receipt_ref"])
Expand All @@ -739,7 +741,7 @@ def _append_capability_advertisement_audit_events(
append_local_audit_event(
root / "logs" / "local-audit-events.jsonl",
{
"schema_version": 1,
"schema_version": 2,
"event_id": (
f"capability-advertisement:{action}:{validation_receipt_ref}:"
f"{capability_id}"
Expand All @@ -749,6 +751,7 @@ def _append_capability_advertisement_audit_events(
"actor_node_id": node_id,
"creator_node_id": creator_node_id,
"local_run_id": f"local-startup:{validation_receipt_ref}",
"event_sequence": event_sequence,
"capability_advertisement_action": action,
"node_id": node_id,
"capability_id": capability_id,
Expand Down
15 changes: 10 additions & 5 deletions src/aethermesh_core/runtime_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1011,14 +1011,15 @@ def submit_local_job(self, request: dict[str, Any]) -> dict[str, Any]:
append_local_audit_event(
self.paths.data_dir / "audit" / "job-submissions.jsonl",
{
"schema_version": 1,
"schema_version": 2,
"event_id": f"local-audit-{job_id}-submitted",
"timestamp": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
"event_type": "job_submitted",
"actor_node_id": local_node_id,
"local_node_id": local_node_id,
"creator_node_id": creator_node_id,
"local_run_id": job_id,
"event_sequence": 1,
"job_id": job_id,
"work_id": job_id,
"manifest_id": job_id,
Expand Down Expand Up @@ -2611,13 +2612,14 @@ def _append_validation_receipt_created_audit_event(
append_local_audit_event(
self.paths.data_dir / "audit" / "validation-receipt-creations.jsonl",
{
"schema_version": 1,
"schema_version": 2,
"event_id": f"local-audit-{job_id}-validation-receipt-created",
"timestamp": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
"event_type": "validation_receipt_created",
"actor_node_id": worker_node_id,
"creator_node_id": creator_node_id,
"local_run_id": job_id,
"event_sequence": 3,
"work_id": job_id,
"manifest_id": manifest_id,
"manifest_ref": manifest_ref,
Expand Down Expand Up @@ -2667,13 +2669,14 @@ def _append_job_execution_started_audit_event(
append_local_audit_event(
self.paths.data_dir / "audit" / "job-executions.jsonl",
{
"schema_version": 1,
"schema_version": 2,
"event_id": f"local-audit-{job_id}-execution-started",
"timestamp": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
"event_type": "job.execution.started",
"actor_node_id": worker_node_id,
"creator_node_id": creator_node_id,
"local_run_id": job_id,
"event_sequence": 2,
"job_id": job_id,
"work_id": job_id,
"executing_node_id": worker_node_id,
Expand Down Expand Up @@ -2736,13 +2739,14 @@ def _append_result_reported_audit_event(
append_local_audit_event(
audit_path,
{
"schema_version": 1,
"schema_version": 2,
"event_id": event_id,
"timestamp": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
"event_type": "result.reported",
"actor_node_id": worker_node_id,
"creator_node_id": creator_node_id,
"local_run_id": job_id,
"event_sequence": 5,
"reporting_node_id": worker_node_id,
"work_id": job_id,
"manifest_id": manifest_id,
Expand Down Expand Up @@ -2819,13 +2823,14 @@ def _append_job_execution_finished_audit_event(
append_local_audit_event(
audit_path,
{
"schema_version": 1,
"schema_version": 2,
"event_id": f"local-audit-{job_id}-execution-finished",
"timestamp": audit_finished_at,
"event_type": "job.execution.finished",
"actor_node_id": actor_node_id,
"creator_node_id": creator_node_id,
"local_run_id": job_id,
"event_sequence": 4,
"job_id": job_id,
"work_id": job_id,
"execution_id": f"local-execution-{job_id}",
Expand Down
7 changes: 5 additions & 2 deletions tests/test_local_audit_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ def test_rejects_invalid_required_fields_and_unknown_event_type(self) -> None:
},
"missing required",
),
({**_minimal_event(), "schema_version": 2}, "schema_version"),
({**_minimal_event(), "schema_version": 1}, "schema_version"),
({**_minimal_event(), "schema_version": True}, "schema_version"),
({**_minimal_event(), "timestamp": "not-utc"}, "timestamp"),
({**_minimal_event(), "event_sequence": 0}, "event_sequence"),
({**_minimal_event(), "event_sequence": True}, "event_sequence"),
({**_minimal_event(), "event_type": "unknown"}, "event_type"),
({**_minimal_event(), "event_type": []}, "event_type"),
({**_minimal_event(), "creator_node_id": ""}, "creator_node_id"),
Expand Down Expand Up @@ -161,13 +163,14 @@ def test_execution_started_requires_compact_execution_provenance(self) -> None:

def _minimal_event() -> dict[str, object]:
return {
"schema_version": 1,
"schema_version": 2,
"event_id": "audit-init-001",
"timestamp": "2026-07-15T06:00:00Z",
"event_type": "node_initialized",
"actor_node_id": "local-node-a",
"creator_node_id": None,
"local_run_id": "run-001",
"event_sequence": 1,
}


Expand Down
39 changes: 39 additions & 0 deletions tests/test_runtime_service_api_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2608,6 +2608,45 @@ def run_after_asserting_report_is_absent(*args: Any, **kwargs: Any) -> Any:
)
self.assertNotIn("output_payload", audit_event)

audit_paths = (
root / "data" / "audit" / "job-submissions.jsonl",
root / "data" / "audit" / "job-executions.jsonl",
root / "data" / "audit" / "validation-receipt-creations.jsonl",
root / "data" / "audit" / "result-reports.jsonl",
)
run_events = [
json.loads(line)
for path in audit_paths
for line in path.read_text(encoding="utf-8").splitlines()
if json.loads(line)["local_run_id"] == job_id
]
self.assertEqual(
[
event["event_sequence"]
for event in sorted(
run_events, key=lambda event: event["event_sequence"]
)
],
[1, 2, 3, 4, 5],
)
for event in run_events:
self.assertEqual(event["local_run_id"], job_id)
self.assertEqual(event["creator_node_id"], request["creator_node_id"])
self.assertRegex(event["timestamp"], r"^\d{4}-\d{2}-\d{2}T.*Z$")
self.assertTrue(event["manifest_id"])
receipt_event = next(
event
for event in run_events
if event["event_type"] == "validation_receipt_created"
)
self.assertEqual(
receipt_event["validation_receipt_id"], report["validation_receipt_id"]
)
self.assertEqual(
receipt_event["lineage_refs"],
[submission["manifest_ref"], *request["lineage_parent_refs"]],
)

service._append_result_reported_audit_event(
job_id=job_id,
worker_node_id="worker-local-fixture",
Expand Down
Loading