From 32849b17c50355811710c26b0560af9be86650ab Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:32:51 -0400 Subject: [PATCH 01/10] Add ReleaseSet schema --- schemas/release-set.schema.v0.1.json | 114 +++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 schemas/release-set.schema.v0.1.json diff --git a/schemas/release-set.schema.v0.1.json b/schemas/release-set.schema.v0.1.json new file mode 100644 index 0000000..136771d --- /dev/null +++ b/schemas/release-set.schema.v0.1.json @@ -0,0 +1,114 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/nlboot/release-set.schema.v0.1.json", + "title": "SourceOS ReleaseSet v0.1", + "description": "A signed SourceOS lifecycle release object binding immutable system target, user-space closures, agent-space closures, policy bundle, BOM, rollback lineage, and evidence requirements.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "kind", + "releaseSetId", + "channel", + "system", + "userSpace", + "agentSpace", + "policy", + "bom", + "signing", + "rollback", + "evidence" + ], + "properties": { + "schemaVersion": { "const": "v0.1" }, + "kind": { "const": "ReleaseSet" }, + "releaseSetId": { "type": "string", "pattern": "^urn:srcos:release-set:" }, + "channel": { "type": "string", "enum": ["dev", "staging", "stable", "rescue", "rollback", "demo"] }, + "description": { "type": "string" }, + "system": { + "type": "object", + "additionalProperties": false, + "required": ["systemPlane", "targetRef", "updateModel", "mutationPolicy"], + "properties": { + "systemPlane": { "type": "string", "enum": ["ostree-silverblue", "ostree-coreos", "nixos", "other"] }, + "targetRef": { "type": "string" }, + "updateModel": { "type": "string", "enum": ["ostree-rebase", "ostree-deploy", "nix-generation", "dry-run-only"] }, + "mutationPolicy": { "type": "string", "enum": ["non-mutating", "requires-policy", "requires-human-confirmation", "recovery-only"] } + } + }, + "userSpace": { + "type": "object", + "additionalProperties": false, + "required": ["profileRefs", "closureRefs"], + "properties": { + "profileRefs": { "type": "array", "items": { "type": "string" } }, + "closureRefs": { "type": "array", "items": { "type": "string" } }, + "experienceProfileRef": { "type": ["string", "null"] } + } + }, + "agentSpace": { + "type": "object", + "additionalProperties": false, + "required": ["profileRefs", "closureRefs", "defaultIsolation"], + "properties": { + "profileRefs": { "type": "array", "items": { "type": "string" } }, + "closureRefs": { "type": "array", "items": { "type": "string" } }, + "defaultIsolation": { "type": "string", "enum": ["container", "vm", "layered", "microvm-mesh", "dry-run-only"] }, + "policyCanUpgradeIsolation": { "type": "boolean", "default": true } + } + }, + "policy": { + "type": "object", + "additionalProperties": false, + "required": ["policyBundleRef", "policyHash", "approvalRequired"], + "properties": { + "policyBundleRef": { "type": "string" }, + "policyHash": { "type": "string" }, + "approvalRequired": { "type": "boolean" }, + "guardrailRefs": { "type": "array", "items": { "type": "string" } } + } + }, + "bom": { + "type": "object", + "additionalProperties": false, + "required": ["bomRef", "bomHash"], + "properties": { + "bomRef": { "type": "string" }, + "bomHash": { "type": "string" }, + "sbomRef": { "type": ["string", "null"] } + } + }, + "signing": { + "type": "object", + "additionalProperties": false, + "required": ["signerRef", "signatureRef", "signatureAlgorithm", "cryptoProfile"], + "properties": { + "signerRef": { "type": "string" }, + "signatureRef": { "type": "string", "pattern": "^urn:srcos:signature:" }, + "signatureAlgorithm": { "type": "string", "enum": ["rsa-pss-sha256", "ed25519", "other"] }, + "cryptoProfile": { "type": "string", "enum": ["fips-140-3-compatible", "standard", "development"] } + } + }, + "rollback": { + "type": "object", + "additionalProperties": false, + "required": ["previousReleaseSetRefs", "rollbackAllowed", "lastKnownGoodRequired"], + "properties": { + "previousReleaseSetRefs": { "type": "array", "items": { "type": "string" } }, + "rollbackAllowed": { "type": "boolean" }, + "lastKnownGoodRequired": { "type": "boolean" } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["emitFingerprint", "emitVerificationRecord", "emitRollbackRecord"], + "properties": { + "emitFingerprint": { "type": "boolean" }, + "emitVerificationRecord": { "type": "boolean" }, + "emitRollbackRecord": { "type": "boolean" }, + "agentPlaneEvidenceRef": { "type": ["string", "null"] } + } + } + } +} From e7793d90efd6671760a435845f4ba3330bc8ede3 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:33:46 -0400 Subject: [PATCH 02/10] Add BootReleaseSet schema --- schemas/boot-release-set.schema.v0.1.json | 109 ++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 schemas/boot-release-set.schema.v0.1.json diff --git a/schemas/boot-release-set.schema.v0.1.json b/schemas/boot-release-set.schema.v0.1.json new file mode 100644 index 0000000..82c7c84 --- /dev/null +++ b/schemas/boot-release-set.schema.v0.1.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/nlboot/boot-release-set.schema.v0.1.json", + "title": "SourceOS BootReleaseSet v0.1", + "description": "A bootable SourceOS lifecycle object binding a ReleaseSet to signed boot artifacts, NLBoot manifest refs, platform adapters, live/install/recovery channels, and proof requirements.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "kind", + "bootReleaseSetId", + "baseReleaseSetRef", + "bootMode", + "signedManifestRef", + "channels", + "artifacts", + "platformAdapters", + "authorization", + "offlineFallback", + "signing", + "proofs" + ], + "properties": { + "schemaVersion": { "const": "v0.1" }, + "kind": { "const": "BootReleaseSet" }, + "bootReleaseSetId": { "type": "string", "pattern": "^urn:srcos:boot-release-set:" }, + "baseReleaseSetRef": { "type": "string", "pattern": "^urn:srcos:release-set:" }, + "bootMode": { "type": "string", "enum": ["installer", "recovery", "ephemeral", "bootstrap"] }, + "signedManifestRef": { "type": "string", "pattern": "^urn:srcos:boot-manifest:" }, + "channels": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "enum": ["live", "installer", "recovery", "rollback", "rescue", "bootstrap"] } + }, + "artifacts": { + "type": "object", + "additionalProperties": false, + "required": ["kernelRef", "initrdRef", "rootfsRef"], + "properties": { + "kernelRef": { "type": "string" }, + "initrdRef": { "type": "string" }, + "rootfsRef": { "type": "string" }, + "artifactMapRef": { "type": ["string", "null"] }, + "cachePolicyRef": { "type": ["string", "null"] } + } + }, + "platformAdapters": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["adapter", "mode", "status"], + "properties": { + "adapter": { "type": "string", "enum": ["apple-silicon-m2", "linux-kexec", "uefi-ipxe", "purism-linux", "vm-bootstrap", "other"] }, + "mode": { "type": "string", "enum": ["dry-run", "load-only", "execute", "planned"] }, + "status": { "type": "string", "enum": ["implemented", "dry-run-only", "planned", "blocked"] }, + "entryRefs": { "type": "array", "items": { "type": "string" } }, + "requiresHostMutation": { "type": "boolean", "default": true }, + "requiresRebootAck": { "type": "boolean", "default": false } + } + } + }, + "authorization": { + "type": "object", + "additionalProperties": false, + "required": ["enrollmentTokenRequired", "oneTimeUseRequired", "deviceClaimRequired"], + "properties": { + "enrollmentTokenRequired": { "type": "boolean" }, + "oneTimeUseRequired": { "type": "boolean" }, + "deviceClaimRequired": { "type": "boolean" }, + "purpose": { "type": "string", "enum": ["install", "recover", "bootstrap", "ephemeral", "test"] } + } + }, + "offlineFallback": { + "type": "object", + "additionalProperties": false, + "required": ["lastKnownGoodRequired", "allowUnsignedFallback", "fallbackCacheRef"], + "properties": { + "lastKnownGoodRequired": { "type": "boolean" }, + "allowUnsignedFallback": { "const": false }, + "fallbackCacheRef": { "type": ["string", "null"] } + } + }, + "signing": { + "type": "object", + "additionalProperties": false, + "required": ["signerRef", "signatureRef", "signatureAlgorithm", "cryptoProfile"], + "properties": { + "signerRef": { "type": "string" }, + "signatureRef": { "type": "string", "pattern": "^urn:srcos:signature:" }, + "signatureAlgorithm": { "type": "string", "enum": ["rsa-pss-sha256", "ed25519", "other"] }, + "cryptoProfile": { "type": "string", "enum": ["fips-140-3-compatible", "standard", "development"] } + } + }, + "proofs": { + "type": "object", + "additionalProperties": false, + "required": ["emitBootPlan", "emitFetchRecord", "emitAdapterRecord", "emitFingerprint"], + "properties": { + "emitBootPlan": { "type": "boolean" }, + "emitFetchRecord": { "type": "boolean" }, + "emitAdapterRecord": { "type": "boolean" }, + "emitFingerprint": { "type": "boolean" }, + "requiredEvidenceKinds": { "type": "array", "items": { "type": "string" } } + } + } + } +} From 601cffd43c6940ee25438759b40aea6032a36ed9 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:34:53 -0400 Subject: [PATCH 03/10] Add lifecycle state record schema --- .../lifecycle-state-record.schema.v0.1.json | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 schemas/lifecycle-state-record.schema.v0.1.json diff --git a/schemas/lifecycle-state-record.schema.v0.1.json b/schemas/lifecycle-state-record.schema.v0.1.json new file mode 100644 index 0000000..186eaf5 --- /dev/null +++ b/schemas/lifecycle-state-record.schema.v0.1.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/nlboot/lifecycle-state-record.schema.v0.1.json", + "title": "SourceOS LifecycleStateRecord v0.1", + "description": "A control-plane lifecycle transition record for ReleaseSet and BootReleaseSet construction, assignment, fetch, attestation, compliance, and rollback.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "kind", + "recordId", + "capturedAt", + "subjectRef", + "objectRef", + "objectKind", + "fromState", + "toState", + "transition", + "proofs", + "policy" + ], + "properties": { + "schemaVersion": { "const": "v0.1" }, + "kind": { "const": "LifecycleStateRecord" }, + "recordId": { "type": "string", "pattern": "^urn:srcos:lifecycle-state-record:" }, + "capturedAt": { "type": "string", "format": "date-time" }, + "subjectRef": { "type": "string", "description": "Device, user, project, group, org, or control-plane actor ref." }, + "objectRef": { "type": "string" }, + "objectKind": { "type": "string", "enum": ["ReleaseSet", "BootReleaseSet", "SignedBootManifest", "EnrollmentToken", "BootPlan", "ArtifactCacheRecord", "AdapterRecord", "Fingerprint"] }, + "fromState": { "type": ["string", "null"], "enum": ["draft", "resolved", "built", "signed", "assigned", "planned", "fetched", "loaded", "executed", "attested", "compliant", "noncompliant", "rollback-available", "rolled-back", "blocked", null] }, + "toState": { "type": "string", "enum": ["draft", "resolved", "built", "signed", "assigned", "planned", "fetched", "loaded", "executed", "attested", "compliant", "noncompliant", "rollback-available", "rolled-back", "blocked"] }, + "transition": { + "type": "object", + "additionalProperties": false, + "required": ["name", "allowed", "reason"], + "properties": { + "name": { "type": "string", "enum": ["resolve-bom", "build", "sign", "assign", "validate-token", "plan", "fetch", "load-only", "execute", "attest", "evaluate-compliance", "mark-rollback-available", "rollback", "refuse"] }, + "allowed": { "type": "boolean" }, + "reason": { "type": "string" }, + "refusalRef": { "type": ["string", "null"] } + } + }, + "proofs": { + "type": "object", + "additionalProperties": false, + "required": ["required", "present"], + "properties": { + "required": { "type": "array", "items": { "type": "string" } }, + "present": { "type": "array", "items": { "type": "string" } }, + "missing": { "type": "array", "items": { "type": "string" } } + } + }, + "policy": { + "type": "object", + "additionalProperties": false, + "required": ["policyRef", "policyHash", "approvalRequired"], + "properties": { + "policyRef": { "type": "string" }, + "policyHash": { "type": "string" }, + "approvalRequired": { "type": "boolean" }, + "approvalRef": { "type": ["string", "null"] } + } + }, + "sideEffects": { + "type": "object", + "additionalProperties": false, + "properties": { + "hostMutation": { "type": "boolean", "default": false }, + "diskWrite": { "type": "boolean", "default": false }, + "reboot": { "type": "boolean", "default": false }, + "networkFetch": { "type": "boolean", "default": false } + } + } + } +} From 40a1ca8ef556c6a4825dbfddb82d691e9ed8dc0f Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:35:49 -0400 Subject: [PATCH 04/10] Add M2 demo ReleaseSet example --- examples/release_set.m2_demo.json | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 examples/release_set.m2_demo.json diff --git a/examples/release_set.m2_demo.json b/examples/release_set.m2_demo.json new file mode 100644 index 0000000..5fe8f65 --- /dev/null +++ b/examples/release_set.m2_demo.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": "v0.1", + "kind": "ReleaseSet", + "releaseSetId": "urn:srcos:release-set:m2-demo-2026-04-26", + "channel": "demo", + "description": "M2 demo ReleaseSet binding SourceOS immutable system target, user profile closures, agent profile closures, policy, BOM, signing, rollback, and evidence requirements.", + "system": { + "systemPlane": "ostree-silverblue", + "targetRef": "urn:srcos:ostree-commit:m2-demo-sourceos-silverblue-gnome-2026-04-26", + "updateModel": "ostree-rebase", + "mutationPolicy": "requires-policy" + }, + "userSpace": { + "profileRefs": [ + "urn:srcos:experience-profile:macos-like-gnome" + ], + "closureRefs": [ + "urn:srcos:nix-closure:user-macos-like-gnome-demo" + ], + "experienceProfileRef": "urn:srcos:experience-profile:macos-like-gnome" + }, + "agentSpace": { + "profileRefs": [ + "urn:srcos:agent-profile:default-devtools" + ], + "closureRefs": [ + "urn:srcos:nix-closure:agent-default-devtools-demo" + ], + "defaultIsolation": "container", + "policyCanUpgradeIsolation": true + }, + "policy": { + "policyBundleRef": "urn:srcos:policy-bundle:m2-demo-standard", + "policyHash": "sha256:release-set-policy-demo", + "approvalRequired": true, + "guardrailRefs": [ + "urn:socioprophet:guardrail-fabric:sourceos-m2-demo" + ] + }, + "bom": { + "bomRef": "urn:srcos:bom:m2-demo-2026-04-26", + "bomHash": "sha256:bom-demo", + "sbomRef": "urn:srcos:sbom:m2-demo-2026-04-26" + }, + "signing": { + "signerRef": "urn:srcos:key:sourceos-release-root", + "signatureRef": "urn:srcos:signature:m2-demo-release-set-2026-04-26", + "signatureAlgorithm": "rsa-pss-sha256", + "cryptoProfile": "fips-140-3-compatible" + }, + "rollback": { + "previousReleaseSetRefs": [ + "urn:srcos:release-set:m2-demo-previous-known-good" + ], + "rollbackAllowed": true, + "lastKnownGoodRequired": true + }, + "evidence": { + "emitFingerprint": true, + "emitVerificationRecord": true, + "emitRollbackRecord": true, + "agentPlaneEvidenceRef": "urn:socioprophet:agentplane:evidence:release-set-m2-demo" + } +} From da72b863363b116e57a4f64d66bcd34f3984e9c0 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:41:10 -0400 Subject: [PATCH 05/10] Add M2 demo BootReleaseSet example --- .../boot_release_set.m2_demo_recovery.json | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 examples/boot_release_set.m2_demo_recovery.json diff --git a/examples/boot_release_set.m2_demo_recovery.json b/examples/boot_release_set.m2_demo_recovery.json new file mode 100644 index 0000000..3e126a4 --- /dev/null +++ b/examples/boot_release_set.m2_demo_recovery.json @@ -0,0 +1,71 @@ +{ + "schemaVersion": "v0.1", + "kind": "BootReleaseSet", + "bootReleaseSetId": "urn:srcos:boot-release-set:m2-demo-recovery-2026-04-26", + "baseReleaseSetRef": "urn:srcos:release-set:m2-demo-2026-04-26", + "bootMode": "recovery", + "signedManifestRef": "urn:srcos:boot-manifest:m2-demo-recovery-2026-04-26", + "channels": [ + "recovery", + "rollback", + "rescue" + ], + "artifacts": { + "kernelRef": "urn:srcos:artifact:m2-demo-recovery-kernel-sha256-0f3b6d7f", + "initrdRef": "urn:srcos:artifact:m2-demo-recovery-initrd-sha256-08f4c82e", + "rootfsRef": "urn:srcos:artifact:m2-demo-recovery-rootfs-sha256-5f4dcc3b", + "artifactMapRef": "examples/artifact_map.recovery.json", + "cachePolicyRef": "urn:srcos:nlboot-cache-policy:m2-demo-last-known-good" + }, + "platformAdapters": [ + { + "adapter": "apple-silicon-m2", + "mode": "dry-run", + "status": "dry-run-only", + "entryRefs": [ + "urn:srcos:boot-entry:m2-demo-sourceos", + "urn:srcos:boot-entry:m2-demo-sourceos-recovery-installer" + ], + "requiresHostMutation": true, + "requiresRebootAck": false + }, + { + "adapter": "linux-kexec", + "mode": "load-only", + "status": "implemented", + "entryRefs": [], + "requiresHostMutation": true, + "requiresRebootAck": true + } + ], + "authorization": { + "enrollmentTokenRequired": true, + "oneTimeUseRequired": true, + "deviceClaimRequired": true, + "purpose": "recover" + }, + "offlineFallback": { + "lastKnownGoodRequired": true, + "allowUnsignedFallback": false, + "fallbackCacheRef": "urn:srcos:nlboot-cache:last-known-good:m2-demo" + }, + "signing": { + "signerRef": "urn:srcos:key:sourceos-release-root", + "signatureRef": "urn:srcos:signature:m2-demo-recovery-2026-04-26", + "signatureAlgorithm": "rsa-pss-sha256", + "cryptoProfile": "fips-140-3-compatible" + }, + "proofs": { + "emitBootPlan": true, + "emitFetchRecord": true, + "emitAdapterRecord": true, + "emitFingerprint": true, + "requiredEvidenceKinds": [ + "BootPlan", + "ArtifactCacheRecord", + "AdapterPlanRecord", + "BootEntryRecord", + "LifecycleStateRecord" + ] + } +} From 85e0b94b22c02ee415adfba2d6e69b055543195e Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:42:01 -0400 Subject: [PATCH 06/10] Add M2 demo signed lifecycle state record --- ...lifecycle_state_record.m2_demo_signed.json | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/lifecycle_state_record.m2_demo_signed.json diff --git a/examples/lifecycle_state_record.m2_demo_signed.json b/examples/lifecycle_state_record.m2_demo_signed.json new file mode 100644 index 0000000..7edf669 --- /dev/null +++ b/examples/lifecycle_state_record.m2_demo_signed.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": "v0.1", + "kind": "LifecycleStateRecord", + "recordId": "urn:srcos:lifecycle-state-record:m2-demo-release-set-signed", + "capturedAt": "2026-04-26T14:35:00Z", + "subjectRef": "urn:srcos:control-plane:local-m2-demo", + "objectRef": "urn:srcos:release-set:m2-demo-2026-04-26", + "objectKind": "ReleaseSet", + "fromState": "built", + "toState": "signed", + "transition": { + "name": "sign", + "allowed": true, + "reason": "ReleaseSet signed by SourceOS release root for M2 demo lifecycle proof.", + "refusalRef": null + }, + "proofs": { + "required": [ + "bomHash", + "policyHash", + "signatureRef" + ], + "present": [ + "urn:srcos:bom:m2-demo-2026-04-26", + "sha256:release-set-policy-demo", + "urn:srcos:signature:m2-demo-release-set-2026-04-26" + ], + "missing": [] + }, + "policy": { + "policyRef": "urn:srcos:policy-bundle:m2-demo-standard", + "policyHash": "sha256:release-set-policy-demo", + "approvalRequired": true, + "approvalRef": "urn:srcos:approval:m2-demo-release-sign" + }, + "sideEffects": { + "hostMutation": false, + "diskWrite": false, + "reboot": false, + "networkFetch": false + } +} From bfeba9e9f4a5ec511f11f13762a4b74d588049fc Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:43:02 -0400 Subject: [PATCH 07/10] Add lifecycle contract validator --- tools/validate_lifecycle_contracts.py | 105 ++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tools/validate_lifecycle_contracts.py diff --git a/tools/validate_lifecycle_contracts.py b/tools/validate_lifecycle_contracts.py new file mode 100644 index 0000000..2b9f9b1 --- /dev/null +++ b/tools/validate_lifecycle_contracts.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Validate SourceOS NLBoot lifecycle contract schemas and examples. + +This validator is intentionally stdlib-only. It proves that the lifecycle +schema/example files exist, parse as JSON, declare the expected kinds, and keep +critical safety invariants such as signed releases, unsigned-fallback denial, +and explicit evidence requirements. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] + +PAIRS = [ + ( + "ReleaseSet", + ROOT / "schemas" / "release-set.schema.v0.1.json", + ROOT / "examples" / "release_set.m2_demo.json", + ), + ( + "BootReleaseSet", + ROOT / "schemas" / "boot-release-set.schema.v0.1.json", + ROOT / "examples" / "boot_release_set.m2_demo_recovery.json", + ), + ( + "LifecycleStateRecord", + ROOT / "schemas" / "lifecycle-state-record.schema.v0.1.json", + ROOT / "examples" / "lifecycle_state_record.m2_demo_signed.json", + ), +] + + +class ValidationError(Exception): + pass + + +def load_json(path: Path) -> dict[str, Any]: + if not path.exists(): + raise ValidationError(f"missing file: {path.relative_to(ROOT)}") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValidationError(f"invalid JSON in {path.relative_to(ROOT)}: {exc}") from exc + if not isinstance(payload, dict): + raise ValidationError(f"expected JSON object in {path.relative_to(ROOT)}") + return payload + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def validate_pair(kind: str, schema_path: Path, example_path: Path) -> None: + schema = load_json(schema_path) + example = load_json(example_path) + + rel_schema = schema_path.relative_to(ROOT) + rel_example = example_path.relative_to(ROOT) + + require(schema.get("$schema") == "https://json-schema.org/draft/2020-12/schema", f"{rel_schema}: must use JSON Schema draft 2020-12") + require(schema.get("type") == "object", f"{rel_schema}: must describe an object") + require(schema.get("properties", {}).get("kind", {}).get("const") == kind, f"{rel_schema}: kind const must be {kind}") + require(example.get("schemaVersion") == "v0.1", f"{rel_example}: schemaVersion must be v0.1") + require(example.get("kind") == kind, f"{rel_example}: kind must be {kind}") + + if kind == "ReleaseSet": + require(str(example.get("releaseSetId", "")).startswith("urn:srcos:release-set:"), f"{rel_example}: invalid releaseSetId") + require(example.get("signing", {}).get("signatureRef", "").startswith("urn:srcos:signature:"), f"{rel_example}: signatureRef required") + require(example.get("rollback", {}).get("lastKnownGoodRequired") is True, f"{rel_example}: last-known-good rollback required") + require(example.get("evidence", {}).get("emitFingerprint") is True, f"{rel_example}: fingerprint evidence required") + elif kind == "BootReleaseSet": + require(str(example.get("bootReleaseSetId", "")).startswith("urn:srcos:boot-release-set:"), f"{rel_example}: invalid bootReleaseSetId") + require(example.get("authorization", {}).get("oneTimeUseRequired") is True, f"{rel_example}: one-time enrollment required") + require(example.get("authorization", {}).get("deviceClaimRequired") is True, f"{rel_example}: device claim required") + require(example.get("offlineFallback", {}).get("allowUnsignedFallback") is False, f"{rel_example}: unsigned fallback must be denied") + require(example.get("proofs", {}).get("emitAdapterRecord") is True, f"{rel_example}: adapter evidence required") + elif kind == "LifecycleStateRecord": + require(str(example.get("recordId", "")).startswith("urn:srcos:lifecycle-state-record:"), f"{rel_example}: invalid recordId") + require(example.get("transition", {}).get("allowed") is True, f"{rel_example}: example transition should be allowed") + require(example.get("policy", {}).get("approvalRequired") is True, f"{rel_example}: lifecycle approval required") + side_effects = example.get("sideEffects", {}) + require(side_effects.get("hostMutation") is False, f"{rel_example}: signing example must not mutate host") + + +def main() -> int: + try: + for kind, schema_path, example_path in PAIRS: + validate_pair(kind, schema_path, example_path) + print(f"ok: {kind}") + except ValidationError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + print("OK: NLBoot lifecycle contracts validated") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From d901f20ba0dd55e5cc9f7842306d33fd4e122cd0 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:44:38 -0400 Subject: [PATCH 08/10] Wire lifecycle contract validation into make validate --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bce362e..3927ef1 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,15 @@ -.PHONY: validate test rust-check rust-test rust-run-fixture rust-fetch-fixture rust-execute-dry-run-fixture rust-exec-dry-run-fixture rust-apple-m2-dry-run-fixture +.PHONY: validate test validate-lifecycle-contracts rust-check rust-test rust-run-fixture rust-fetch-fixture rust-execute-dry-run-fixture rust-exec-dry-run-fixture rust-apple-m2-dry-run-fixture -validate: test +validate: test validate-lifecycle-contracts @echo "OK: validate" test: python3 -m pip install --user pytest cryptography >/dev/null PYTHONPATH=src python3 -m pytest -q +validate-lifecycle-contracts: + python3 tools/validate_lifecycle_contracts.py + rust-check: cd rust/nlboot-client && cargo check From 2cc97eb07d658d4cdc51c9abb9969f05f8c79ba9 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 13:47:19 -0400 Subject: [PATCH 09/10] Document NLBoot lifecycle contracts --- docs/LIFECYCLE_CONTRACTS.md | 114 ++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/LIFECYCLE_CONTRACTS.md diff --git a/docs/LIFECYCLE_CONTRACTS.md b/docs/LIFECYCLE_CONTRACTS.md new file mode 100644 index 0000000..1b9a8b6 --- /dev/null +++ b/docs/LIFECYCLE_CONTRACTS.md @@ -0,0 +1,114 @@ +# NLBoot lifecycle contracts + +NLBoot now carries the contract layer that connects the executable boot/recovery client to the SourceOS control plane. + +The Rust client already proves signed manifest validation, token validation, artifact fetch/cache, evidence output, gated Linux kexec dry-run, and Apple Silicon M2 adapter dry-run. The lifecycle contracts define the higher-level objects that the website/control plane must build, sign, assign, and verify. + +## Contract objects + +| Object | Schema | Purpose | +|---|---|---| +| `ReleaseSet` | `schemas/release-set.schema.v0.1.json` | Signed SourceOS lifecycle release binding system target, user closures, agent closures, policy, BOM, rollback, and evidence. | +| `BootReleaseSet` | `schemas/boot-release-set.schema.v0.1.json` | Bootable/recovery release binding `ReleaseSet` to signed boot manifest, boot artifacts, platform adapters, authorization, offline fallback, signing, and proof requirements. | +| `LifecycleStateRecord` | `schemas/lifecycle-state-record.schema.v0.1.json` | Control-plane transition record for build/sign/assign/plan/fetch/load/execute/attest/compliance/rollback states. | + +Examples: + +```text +examples/release_set.m2_demo.json +examples/boot_release_set.m2_demo_recovery.json +examples/lifecycle_state_record.m2_demo_signed.json +``` + +Validation: + +```bash +make validate-lifecycle-contracts +make validate +``` + +## State design + +The lifecycle path is intentionally explicit: + +```text +DraftProfile + -> ResolvedBOM + -> Built + -> Signed + -> Assigned + -> Planned + -> Fetched + -> Loaded + -> Executed + -> Attested + -> Compliant / Noncompliant + -> RollbackAvailable + -> RolledBack +``` + +Each transition emits or references a `LifecycleStateRecord`. + +## BootReleaseSet and NLBoot + +`BootReleaseSet` does not replace `SignedBootManifest` or `BootPlan`. + +It binds them to the SourceOS control-plane lifecycle: + +```text +ReleaseSet + -> BootReleaseSet + -> SignedBootManifest + -> EnrollmentToken + -> BootPlan + -> fetch/cache evidence + -> adapter evidence + -> fingerprint/compliance/rollback evidence +``` + +## M2 proof target + +The Apple Silicon M2 path remains first-class but not exclusive. + +`BootReleaseSet` supports platform adapters including: + +- `apple-silicon-m2` +- `linux-kexec` +- `uefi-ipxe` +- `purism-linux` +- `vm-bootstrap` + +The M2 adapter remains dry-run only until reviewed platform-specific host mutation exists. The contract requires evidence for proposed visible entries: + +```text +SourceOS +SourceOS Recovery/Installer +``` + +## Safety invariants + +- Unsigned fallback is forbidden. +- One-time enrollment token is required for boot/recovery authorization. +- Device claim is required. +- Last-known-good fallback is required for recovery posture. +- Host mutation is explicit and evidence-backed. +- Reboot paths require explicit acknowledgement in executor paths. +- Signing records do not mutate host state. +- Boot/recovery action must emit plan, fetch, adapter, and fingerprint evidence. + +## Website/control-plane responsibility + +The website/control plane must eventually own: + +- profile selection; +- BOM resolution; +- ReleaseSet creation and signing; +- BootReleaseSet creation and signing; +- enrollment token issuance; +- device assignment; +- lifecycle transition records; +- artifact hosting; +- compliance dashboard; +- rollback assignment. + +NLBoot owns portable boot planning, artifact fetch/cache/evidence, and platform adapter execution boundaries. From a0e68450af310fcccc70a3401514f645ad3a4e73 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 17:44:30 -0400 Subject: [PATCH 10/10] Document ReleaseSet BootReleaseSet lifecycle contracts in README --- README.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57ff7f9..4fce1a7 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ `nlboot` is the SourceOS network/live/recovery boot protocol reference implementation and Rust production-client lane. -This repository now contains two layers: +This repository now contains three layers: - Python reference planner and conformance harness. - Rust `nlboot-client` usable-MVP lane for planning, artifact fetch/cache, evidence output, gated Linux handoff, and Apple Silicon M2 adapter dry-run proof. +- SourceOS lifecycle contracts for `ReleaseSet`, `BootReleaseSet`, and `LifecycleStateRecord` control-plane objects. NLBoot is not scoped to one machine. The M2 path is the first-class proof target because it is the first real machine we are proving on. The portable protocol must also support generic UEFI/iPXE, Purism/Linux-first hardware, and VM/bootstrap targets. @@ -23,6 +24,7 @@ See: - `docs/PLATFORM_ADAPTER_MATRIX.md` - `docs/APPLE_SILICON_M2_ADAPTER_PLAN.md` - `docs/APPLE_SILICON_M2_ADAPTER_CONTRACT.md` +- `docs/LIFECYCLE_CONTRACTS.md` ## What is implemented now @@ -42,6 +44,10 @@ See: - Apple Silicon M2 dry-run adapter evidence path. - Dry-run proofs for CI and local validation. - Refusal records for blocked paths. +- `ReleaseSet` lifecycle contract schema and M2 demo example. +- `BootReleaseSet` lifecycle contract schema and M2 recovery demo example. +- `LifecycleStateRecord` schema and signed-state transition demo example. +- Lifecycle contract validation wired into `make validate`. ## What is still intentionally gated @@ -51,7 +57,8 @@ The production client does not yet implement: - rollback execution; - real Apple Silicon boot-entry changes; - host repair actions; -- persistent enrollment-secret storage. +- persistent enrollment-secret storage; +- website/control-plane assignment flows. Those operations are host mutation and require explicit platform adapters, evidence emission, and review. @@ -93,6 +100,31 @@ Those operations are host mutation and require explicit platform adapters, evide - offline fallback posture - `execute=false` +## Lifecycle objects + +`ReleaseSet` binds the immutable SourceOS system target to user-space closures, agent-space closures, policy bundles, BOM/SBOM refs, signing refs, rollback lineage, and evidence requirements. + +`BootReleaseSet` binds a `ReleaseSet` to signed boot artifacts, the signed boot manifest, live/install/recovery channels, platform adapters, authorization requirements, offline fallback, signing refs, and proof requirements. + +`LifecycleStateRecord` records state transitions such as build, sign, assign, plan, fetch, load-only, execute, attest, evaluate compliance, and rollback. + +Lifecycle contracts and examples: + +```text +schemas/release-set.schema.v0.1.json +schemas/boot-release-set.schema.v0.1.json +schemas/lifecycle-state-record.schema.v0.1.json +examples/release_set.m2_demo.json +examples/boot_release_set.m2_demo_recovery.json +examples/lifecycle_state_record.m2_demo_signed.json +``` + +Lifecycle validation: + +```bash +make validate-lifecycle-contracts +``` + ## Usable MVP flow Run the generic Linux/Purism/VM local usable-MVP fixture path: @@ -147,6 +179,7 @@ A real `kexec --load` path removes `--dry-run` and must run with root or equival ```bash make validate +make validate-lifecycle-contracts make rust-check make rust-test make rust-run-fixture