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
8 changes: 7 additions & 1 deletion docs/local-audit-event-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Store one event per UTF-8 newline-delimited JSON (JSONL) line, normally under th
| `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. |

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

## Optional references

Expand All @@ -37,6 +37,12 @@ Omit optional fields when they do not apply. Their absence is valid and has no e

Hashes and signatures are optional. This format neither creates nor requires them unless existing local validation already produces them.

## Capability advertisement events

A `capability_advertised` event records one validated local startup-manifest claim after startup artifacts have been created. It requires `capability_advertisement_action` (`created`, `refreshed`, or `replaced`), `node_id`, `capability_id`, `manifest_ref`, `manifest_digest`, `advertisement_payload_digest`, `validation_status`, `validation_receipt_refs`, `lineage_refs`, and a string-only `contribution_attribution` map. The event stores digests and safe relative references rather than the full advertisement payload, so local runtime details are not copied into the audit log.

Startup appends one event per capability advertisement. Normal repeat startup is `refreshed`; explicit creator-identity reset is `replaced`; creating or upgrading a manifest advertisement is `created`. Rejected manifests fail before this event is appended.

## Examples

A minimal initialization event intentionally has no optional references:
Expand Down
58 changes: 58 additions & 0 deletions src/aethermesh_core/local_audit_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"validation_result",
"lineage_linked",
"contribution_record_updated",
"capability_advertised",
"node.shutdown",
}
)
Expand Down Expand Up @@ -51,6 +52,13 @@
"validation_receipt_ref",
"lineage_refs",
"contribution_attribution_refs",
"capability_advertisement_action",
"node_id",
"capability_id",
"manifest_digest",
"advertisement_payload_digest",
"validation_receipt_refs",
"contribution_attribution",
}
)

Expand Down Expand Up @@ -124,6 +132,8 @@ def validate_local_audit_event(event: Mapping[str, Any]) -> dict[str, Any]:
_require_text_mapping(document[field], f"local audit event.{field}")
if document["event_type"] == "node.shutdown":
_validate_shutdown_event(document)
if document["event_type"] == "capability_advertised":
_validate_capability_advertisement_event(document)
return document


Expand Down Expand Up @@ -214,3 +224,51 @@ def _validate_shutdown_event(document: Mapping[str, Any]) -> None:
f"node.shutdown event is missing required context: {', '.join(missing)}"
)
_require_text(document["creator_node_id"], "node.shutdown.creator_node_id")


def _validate_capability_advertisement_event(document: Mapping[str, Any]) -> None:
"""Require compact provenance for one local capability advertisement."""

required_fields = (
"capability_advertisement_action",
"node_id",
"capability_id",
"manifest_ref",
"manifest_digest",
"advertisement_payload_digest",
"validation_status",
"validation_receipt_refs",
"lineage_refs",
"contribution_attribution",
)
missing = [field for field in required_fields if field not in document]
if missing:
raise LocalAuditEventError(
"capability_advertised event is missing required context: "
f"{', '.join(missing)}"
)
if document["capability_advertisement_action"] not in {
"created",
"refreshed",
"replaced",
}:
raise LocalAuditEventError(
"capability_advertised action must be created, refreshed, or replaced"
)
for field in (
"node_id",
"capability_id",
"manifest_digest",
"advertisement_payload_digest",
"validation_status",
):
_require_text(document[field], f"capability_advertised.{field}")
_require_text(document["creator_node_id"], "capability_advertised.creator_node_id")
_require_local_paths(
document["validation_receipt_refs"],
"capability_advertised.validation_receipt_refs",
)
_require_text_mapping(
document["contribution_attribution"],
"capability_advertised.contribution_attribution",
)
76 changes: 76 additions & 0 deletions src/aethermesh_core/local_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
reset_identity,
)
from aethermesh_core.json_io import atomic_create_json, atomic_write_json
from aethermesh_core.local_audit_event import append_local_audit_event
from aethermesh_core.local_runtime_config import (
DEFAULT_RUNTIME_PATHS,
LOCAL_RUNTIME_CONFIG_PATH,
Expand Down Expand Up @@ -256,7 +257,9 @@ def start_local_node(
receipt_ref = _next_artifact_ref(
root, config, "validation_receipts", "startup-validation", timestamp
)
advertisement_action = "refreshed"
if reset_creator_identity and identity_existed:
advertisement_action = "replaced"
_write_default_manifest(
manifest_path,
node_id=identity.node_id,
Expand All @@ -268,6 +271,7 @@ def start_local_node(
replace_existing=True,
)
elif not manifest_path.exists():
advertisement_action = "created"
_write_default_manifest(
manifest_path,
node_id=identity.node_id,
Expand All @@ -280,6 +284,7 @@ def start_local_node(
)
manifest = _load_manifest(manifest_path)
if "capability_advertisements" not in manifest:
advertisement_action = "created"
manifest["capability_advertisements"] = _default_capability_advertisements(
creator_node_id=creator_node_id,
manifest_ref=_relative_ref(root, manifest_path),
Expand Down Expand Up @@ -360,6 +365,18 @@ def start_local_node(
)
)
handle.write("\n")
_append_capability_advertisement_audit_events(
root,
advertisements=manifest["capability_advertisements"],
action=advertisement_action,
timestamp=timestamp.replace("+00:00", "Z"),
node_id=identity.node_id,
creator_node_id=creator_node_id,
manifest_ref=_relative_ref(root, manifest_path),
manifest_hash=manifest_hash,
validation_receipt_ref=receipt_ref,
lineage_ref=lineage_ref,
)
except OSError as exc:
raise LocalStartupError(
f"could not record startup lineage or log: {exc}"
Expand Down Expand Up @@ -698,6 +715,65 @@ def _document_hash(document: dict[str, object]) -> str:
return "sha256:" + sha256(encoded).hexdigest()


def _append_capability_advertisement_audit_events(
root: Path,
*,
advertisements: object,
action: str,
timestamp: str,
node_id: str,
creator_node_id: str,
manifest_ref: str,
manifest_hash: str,
validation_receipt_ref: str,
lineage_ref: str,
) -> None:
"""Append one digest-only audit event for each validated local claim."""

for advertisement in cast(list[dict[str, object]], advertisements):
validation = cast(dict[str, object], advertisement["validation"])
attribution = cast(dict[str, object], advertisement["contribution_attribution"])
required_receipt_ref = cast(str, validation["required_receipt_ref"])
work_record_ref = cast(str, attribution["work_record_ref"])
capability_id = cast(str, advertisement["capability_id"])
append_local_audit_event(
root / "logs" / "local-audit-events.jsonl",
{
"schema_version": 1,
"event_id": (
f"capability-advertisement:{action}:{validation_receipt_ref}:"
f"{capability_id}"
),
"timestamp": timestamp,
"event_type": "capability_advertised",
"actor_node_id": node_id,
"creator_node_id": creator_node_id,
"local_run_id": f"local-startup:{validation_receipt_ref}",
"capability_advertisement_action": action,
"node_id": node_id,
"capability_id": capability_id,
"manifest_ref": manifest_ref,
"manifest_digest": manifest_hash,
"advertisement_payload_digest": _document_hash(advertisement),
"validation_status": "passed",
"validation_receipt_refs": [required_receipt_ref],
"lineage_refs": [lineage_ref],
"contribution_attribution": {
"creator_node_id": creator_node_id,
"attribution_node_id": node_id,
"work_record_ref": work_record_ref,
},
"related_file_paths": [
manifest_ref,
required_receipt_ref,
validation_receipt_ref,
lineage_ref,
work_record_ref,
],
},
)


def _next_artifact_ref(
root: Path,
config: LocalRuntimeConfig,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_local_audit_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_validates_required_fields_and_allows_missing_optional_fields(self) -> N
event = _minimal_event()

self.assertEqual(validate_local_audit_event(event), event)
self.assertEqual(len(AUDIT_EVENT_TYPES), 8)
self.assertEqual(len(AUDIT_EVENT_TYPES), 9)
self.assertIsNone(event["creator_node_id"])

def test_appends_parseable_jsonl_events_with_local_references(self) -> None:
Expand Down
25 changes: 21 additions & 4 deletions tests/test_local_shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from aethermesh_core.cli import main
from aethermesh_core.local_shutdown import (
LocalShutdownError,
_audit_event_exists,
_latest_startup_run_id,
_latest_validation_context,
shutdown_local_node,
Expand Down Expand Up @@ -135,6 +136,7 @@ def test_shutdown_appends_auditable_context_for_each_startup_instance(self) -> N
events = [
json.loads(line)
for line in audit_path.read_text(encoding="utf-8").splitlines()
if json.loads(line).get("event_type") == "node.shutdown"
]

self.assertEqual(len(events), 2)
Expand Down Expand Up @@ -165,8 +167,12 @@ def test_shutdown_records_unavailable_optional_validation_context_honestly(
start = start_local_node(root).to_dict()
(root / str(start["validation_receipt_path"])).unlink()
shutdown_local_node(root)
event = json.loads(
(root / "logs" / "local-audit-events.jsonl").read_text(encoding="utf-8")
event = next(
json.loads(line)
for line in (root / "logs" / "local-audit-events.jsonl")
.read_text(encoding="utf-8")
.splitlines()
if json.loads(line).get("event_type") == "node.shutdown"
)

self.assertIsNone(event["validation_receipt_ref"])
Expand All @@ -185,8 +191,12 @@ def test_shutdown_uses_latest_receipt_without_changing_startup_instance(
json.dumps({"validation_status": "pass"}), encoding="utf-8"
)
shutdown_local_node(root)
event = json.loads(
(root / "logs" / "local-audit-events.jsonl").read_text(encoding="utf-8")
event = next(
json.loads(line)
for line in (root / "logs" / "local-audit-events.jsonl")
.read_text(encoding="utf-8")
.splitlines()
if json.loads(line).get("event_type") == "node.shutdown"
)

self.assertEqual(
Expand Down Expand Up @@ -214,6 +224,7 @@ def test_missing_receipts_still_produce_distinct_shutdown_events(self) -> None:
for line in (root / "logs" / "local-audit-events.jsonl")
.read_text(encoding="utf-8")
.splitlines()
if json.loads(line).get("event_type") == "node.shutdown"
]

self.assertEqual(len(events), 2)
Expand Down Expand Up @@ -249,6 +260,12 @@ def test_latest_validation_context_handles_unreadable_or_malformed_receipts(
non_object_context, ("receipts/non-object.json", "unavailable")
)

def test_audit_event_lookup_returns_false_for_an_unreadable_log(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
self.assertFalse(
_audit_event_exists(Path(temp_dir) / "missing.jsonl", "event-001")
)

def test_latest_startup_run_id_handles_missing_or_malformed_logs(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "startup.log"
Expand Down
72 changes: 72 additions & 0 deletions tests/test_local_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,78 @@ def test_clean_startup_creates_auditable_artifacts_and_preserves_identity(
first["validation_receipt_path"],
)
self.assertEqual(advertisement["network_mode"], "local-only-no-p2p")
advertisement_audit_events = [
json.loads(line)
for line in (runtime / "logs" / "local-audit-events.jsonl")
.read_text(encoding="utf-8")
.splitlines()
]
self.assertEqual(len(advertisement_audit_events), 2)
for event, startup, action in zip(
advertisement_audit_events,
(first, second),
("created", "refreshed"),
strict=True,
):
self.assertEqual(event["event_type"], "capability_advertised")
self.assertEqual(event["capability_advertisement_action"], action)
self.assertEqual(event["creator_node_id"], startup["creator_node_id"])
self.assertEqual(event["node_id"], startup["node_id"])
self.assertEqual(event["capability_id"], advertisement["capability_id"])
self.assertEqual(event["manifest_digest"], startup["manifest_hash"])
self.assertEqual(
event["validation_receipt_refs"],
[advertisement["validation"]["required_receipt_ref"]],
)
self.assertEqual(event["lineage_refs"], [startup["lineage_path"]])
self.assertEqual(
event["contribution_attribution"]["creator_node_id"],
startup["creator_node_id"],
)
self.assertNotIn("description", event)
self.assertRegex(
event["advertisement_payload_digest"], r"^sha256:[0-9a-f]{64}$"
)

def test_reset_startup_appends_a_replaced_capability_advertisement_event(
self,
) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
runtime = Path(temp_dir)
start_local_node(runtime)
reset = start_local_node(runtime, reset_creator_identity=True)

events = [
json.loads(line)
for line in (runtime / "logs" / "local-audit-events.jsonl")
.read_text(encoding="utf-8")
.splitlines()
]

self.assertEqual(
[event["capability_advertisement_action"] for event in events],
["created", "replaced"],
)
self.assertEqual(events[-1]["creator_node_id"], reset.creator_node_id)

def test_rejected_manifest_does_not_append_capability_advertisement_event(
self,
) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
runtime = Path(temp_dir)
start_local_node(runtime)
audit_path = runtime / "logs" / "local-audit-events.jsonl"
before = audit_path.read_text(encoding="utf-8")
(runtime / "manifests" / "local-node-manifest.json").write_text(
"{not json", encoding="utf-8"
)

with self.assertRaisesRegex(
LocalStartupError, "startup manifest JSON is malformed"
):
start_local_node(runtime)

self.assertEqual(audit_path.read_text(encoding="utf-8"), before)

def test_existing_identity_missing_manifest_fails_closed(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
Expand Down
Loading