diff --git a/Makefile b/Makefile index 90f47df..ec4d070 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: validate validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-lifecycle-boundary-examples validate-svf-contracts +.PHONY: validate validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts -validate: validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-lifecycle-boundary-examples validate-svf-contracts +validate: validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts @echo "OK: validate" validate-control-plane-examples: @@ -29,3 +29,7 @@ validate-lifecycle-boundary-examples: validate-svf-contracts: python3 tools/validate_svf_contracts.py + +validate-sync-cycle-receipts: + python3 -m pip install --user jsonschema >/dev/null + python3 tools/validate_sync_cycle_receipts.py diff --git a/examples/sync-cycle-receipt.dry-run.json b/examples/sync-cycle-receipt.dry-run.json new file mode 100644 index 0000000..34a8c5f --- /dev/null +++ b/examples/sync-cycle-receipt.dry-run.json @@ -0,0 +1,33 @@ +{ + "id": "urn:srcos:sync-receipt:builder-aarch64-2026-06-16T00:00:00Z-dry", + "type": "SyncCycleReceipt", + "specVersion": "0.1.0", + "cycleId": "cycle-builder-aarch64-20260616-dry", + "engineId": "sourceos.sync.katello-content", + "org": "SocioProphet", + "contentView": "sourceos-builder-aarch64", + "fromVersion": "1.0", + "toVersion": "1.1", + "lifecycleEnv": "dev", + "locus": "local", + "outcome": "dry_run", + "policyGate": "allowed", + "policyReason": "locus 'local' is in ALLOWED_LOCI", + "steps": [ + { + "step": "nix copy --from http://127.0.0.1:8101 github:SociOS-Linux/source-os#builder-aarch64", + "status": "dry_run", + "reason": "dry_run=True; would execute nix copy" + }, + { + "step": "nixos-rebuild switch --flake github:SociOS-Linux/source-os#builder-aarch64", + "status": "dry_run", + "reason": "dry_run=True; would execute nixos-rebuild switch" + } + ], + "nixCacheUrl": "http://127.0.0.1:8101", + "flakeRef": "github:SociOS-Linux/source-os#builder-aarch64", + "durationMs": 0, + "issuedAt": "2026-06-16T00:00:00Z", + "auditId": "urn:srcos:audit:builder-aarch64-2026-06-16T00:00:00Z-dry" +} diff --git a/examples/sync-cycle-receipt.json b/examples/sync-cycle-receipt.json new file mode 100644 index 0000000..ac89552 --- /dev/null +++ b/examples/sync-cycle-receipt.json @@ -0,0 +1,37 @@ +{ + "id": "urn:srcos:sync-receipt:builder-aarch64-2026-06-16T00:00:00Z-001", + "type": "SyncCycleReceipt", + "specVersion": "0.1.0", + "cycleId": "cycle-builder-aarch64-20260616-001", + "engineId": "sourceos.sync.katello-content", + "org": "SocioProphet", + "contentView": "sourceos-builder-aarch64", + "fromVersion": null, + "toVersion": "1.0", + "lifecycleEnv": "dev", + "locus": "local", + "outcome": "applied", + "policyGate": "allowed", + "policyReason": "locus 'local' is in ALLOWED_LOCI", + "steps": [ + { + "step": "nix copy --from http://127.0.0.1:8101 github:SociOS-Linux/source-os#builder-aarch64", + "status": "ok", + "returncode": 0, + "stdout": "copying 12 paths...", + "stderr": "" + }, + { + "step": "nixos-rebuild switch --flake github:SociOS-Linux/source-os#builder-aarch64", + "status": "ok", + "returncode": 0, + "stdout": "activating configuration...", + "stderr": "" + } + ], + "nixCacheUrl": "http://127.0.0.1:8101", + "flakeRef": "github:SociOS-Linux/source-os#builder-aarch64", + "durationMs": 47200, + "issuedAt": "2026-06-16T00:00:00Z", + "auditId": "urn:srcos:audit:builder-aarch64-2026-06-16T00:00:00Z-001" +} diff --git a/schemas/SyncCycleReceipt.json b/schemas/SyncCycleReceipt.json new file mode 100644 index 0000000..81ba0fe --- /dev/null +++ b/schemas/SyncCycleReceipt.json @@ -0,0 +1,123 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/SyncCycleReceipt.json", + "title": "SyncCycleReceipt", + "description": "Immutable receipt emitted after a content sync cycle is planned or applied. Captures the before/after content view versions, steps executed, outcome, and audit reference. Owned by SourceOS-Linux/sourceos-syncd.", + "type": "object", + "required": [ + "id", + "type", + "specVersion", + "cycleId", + "engineId", + "org", + "contentView", + "toVersion", + "lifecycleEnv", + "locus", + "outcome", + "steps", + "issuedAt", + "auditId" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^urn:srcos:sync-receipt:", + "description": "Globally unique receipt identifier." + }, + "type": { + "const": "SyncCycleReceipt" + }, + "specVersion": { + "type": "string", + "description": "sourceos-spec version this receipt was emitted against." + }, + "cycleId": { + "type": "string", + "description": "Correlation ID for the sync cycle; may span plan + apply." + }, + "engineId": { + "type": "string", + "pattern": "^sourceos\\.sync\\.", + "description": "SyncEngineManifest engineId that produced this receipt." + }, + "org": { + "type": "string", + "description": "Katello organization name." + }, + "contentView": { + "type": "string", + "description": "Katello content view name (e.g. sourceos-builder-aarch64)." + }, + "fromVersion": { + "type": ["string", "null"], + "description": "Content view version before the sync; null on first sync." + }, + "toVersion": { + "type": "string", + "description": "Content view version targeted by this sync." + }, + "lifecycleEnv": { + "type": "string", + "description": "Katello lifecycle environment (dev, candidate, stable)." + }, + "locus": { + "enum": ["local", "trusted_private", "attested_fog", "burst_cloud"], + "description": "Execution locus at which the sync was authorized." + }, + "outcome": { + "enum": ["planned", "dry_run", "applied", "skipped", "denied", "failed"], + "description": "Result of the sync cycle." + }, + "policyGate": { + "type": "string", + "description": "Policy gate value from the ContentSyncPlan (allowed, denied, no-op)." + }, + "policyReason": { + "type": "string", + "description": "Human-readable reason for the policy gate decision." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": ["step", "status"], + "additionalProperties": false, + "properties": { + "step": { "type": "string" }, + "status": { + "enum": ["dry_run", "ok", "failed", "skipped", "timeout"] + }, + "returncode": { "type": "integer" }, + "stdout": { "type": "string" }, + "stderr": { "type": "string" }, + "reason": { "type": "string" } + } + } + }, + "nixCacheUrl": { + "type": "string", + "description": "Pulp content server URL used as the Nix binary cache." + }, + "flakeRef": { + "type": "string", + "description": "NixOS flake ref passed to nixos-rebuild." + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "description": "Wall-clock duration of the sync execution in milliseconds." + }, + "issuedAt": { + "type": "string", + "format": "date-time" + }, + "auditId": { + "type": "string", + "pattern": "^urn:srcos:audit:", + "description": "AuditEvent id that records this sync cycle in the append-only audit log." + } + } +} diff --git a/tools/validate_sync_cycle_receipts.py b/tools/validate_sync_cycle_receipts.py new file mode 100644 index 0000000..dc4b873 --- /dev/null +++ b/tools/validate_sync_cycle_receipts.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +from pathlib import Path + +import jsonschema + +ROOT = Path(__file__).resolve().parents[1] +PAIRS = [ + (ROOT / "schemas" / "SyncCycleReceipt.json", ROOT / "examples" / "sync-cycle-receipt.json"), + (ROOT / "schemas" / "SyncCycleReceipt.json", ROOT / "examples" / "sync-cycle-receipt.dry-run.json"), +] + + +def validate_pair(schema_path: Path, example_path: Path) -> None: + schema = json.loads(schema_path.read_text(encoding="utf-8")) + jsonschema.validators.validator_for(schema).check_schema(schema) + example = json.loads(example_path.read_text(encoding="utf-8")) + jsonschema.validate(example, schema) + + +def main() -> int: + checks: dict[str, bool] = {} + for schema_path, example_path in PAIRS: + validate_pair(schema_path, example_path) + checks[example_path.name] = True + print(json.dumps({"ok": all(checks.values()), "checks": checks}, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())