diff --git a/contracts/concept-authority/controlled-vocabularies-v1.json b/contracts/concept-authority/controlled-vocabularies-v1.json index 4007b617..58489ba7 100644 --- a/contracts/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/concept-authority/controlled-vocabularies-v1.json @@ -762,6 +762,48 @@ } } }, + "os-distributions": { + "title": "Operating System Distributions and Product Lines", + "description": "Portable operating-system distribution or product-line identifiers shared by SDL and backend compatibility declarations.", + "kind": "vocabulary", + "governed_scopes": [ + "nodes.os_distribution", + "capabilities.provisioner.operating_systems.distribution", + "runtime.snapshot.realization_observations.operating-system.distribution" + ], + "extension_policy": "governed-extension", + "extension_pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$", + "terms": { + "ubuntu": { + "title": "Ubuntu", + "description": "Ubuntu Linux distribution." + }, + "debian": { + "title": "Debian", + "description": "Debian GNU/Linux distribution." + }, + "rocky-linux": { + "title": "Rocky Linux", + "description": "Rocky Linux distribution." + }, + "red-hat-enterprise-linux": { + "title": "Red Hat Enterprise Linux", + "description": "Red Hat Enterprise Linux product line." + }, + "windows-server": { + "title": "Windows Server", + "description": "Microsoft Windows Server product line." + }, + "windows-client": { + "title": "Windows Client", + "description": "Microsoft Windows client product line." + }, + "solaris": { + "title": "Solaris", + "description": "Oracle Solaris operating-system product line." + } + } + }, "provisioner-node-architectures": { "title": "Provisioner Node CPU Architectures", "description": "Target-node CPU architecture vocabulary shared by SDL Node.architecture and provisioner capability declarations.", diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json index a5d4bd7c..489cfa6f 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -153,6 +153,7 @@ "other", "windows" ], + "operating_systems": [], "supported_node_architectures": [ "aarch64", "x86_64" diff --git a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json index 4007b617..58489ba7 100644 --- a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json @@ -762,6 +762,48 @@ } } }, + "os-distributions": { + "title": "Operating System Distributions and Product Lines", + "description": "Portable operating-system distribution or product-line identifiers shared by SDL and backend compatibility declarations.", + "kind": "vocabulary", + "governed_scopes": [ + "nodes.os_distribution", + "capabilities.provisioner.operating_systems.distribution", + "runtime.snapshot.realization_observations.operating-system.distribution" + ], + "extension_policy": "governed-extension", + "extension_pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$", + "terms": { + "ubuntu": { + "title": "Ubuntu", + "description": "Ubuntu Linux distribution." + }, + "debian": { + "title": "Debian", + "description": "Debian GNU/Linux distribution." + }, + "rocky-linux": { + "title": "Rocky Linux", + "description": "Rocky Linux distribution." + }, + "red-hat-enterprise-linux": { + "title": "Red Hat Enterprise Linux", + "description": "Red Hat Enterprise Linux product line." + }, + "windows-server": { + "title": "Windows Server", + "description": "Microsoft Windows Server product line." + }, + "windows-client": { + "title": "Windows Client", + "description": "Microsoft Windows client product line." + }, + "solaris": { + "title": "Solaris", + "description": "Oracle Solaris operating-system product line." + } + } + }, "provisioner-node-architectures": { "title": "Provisioner Node CPU Architectures", "description": "Target-node CPU architecture vocabulary shared by SDL Node.architecture and provisioner capability declarations.", diff --git a/contracts/fixtures/realization-envelope/realization-envelope-v1/valid/generic.json b/contracts/fixtures/realization-envelope/realization-envelope-v1/valid/generic.json index e273d511..f4a2824b 100644 --- a/contracts/fixtures/realization-envelope/realization-envelope-v1/valid/generic.json +++ b/contracts/fixtures/realization-envelope/realization-envelope-v1/valid/generic.json @@ -12,12 +12,13 @@ }, "configuration": { "mode": "generic", - "configuration_digest": "sha256:af14a9757a7b80bedab5cea712f60d2a05ee1d3406fbb7c71e196eaca4746c15", + "configuration_digest": "sha256:cf7e5b0d28c25293d254c463a23958f66f92776579872fce8abbeb1227347b4b", "architecture": "x86_64", "image_policy": "local-qcow2", "network_policy": "libvirt-managed", "supported_node_types": ["switch", "compute"], "supported_os_families": ["linux"], + "operating_systems": [], "supported_content_types": ["file"], "supported_account_features": ["auth_method", "disabled", "groups", "home", "shell"], "supported_domain_profiles": [], @@ -27,6 +28,7 @@ }, "concerns": [ {"concern": "compute-substrate", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "virtual-machine", "transformations": []}, + {"concern": "operating-system", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, {"concern": "topology", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "libvirt-domain-network", "transformations": []}, {"concern": "architecture", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "qemu-x86_64-domain", "transformations": []}, {"concern": "image", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "qcow2-disk-attachment", "transformations": []}, @@ -38,5 +40,5 @@ {"concern": "service", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, {"concern": "acl", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "libvirt-nwfilter", "transformations": []} ], - "digest": "sha256:0a4ff252cde4d85a1f0ddc896a82d10679b805723df55d0042b9932a61d037e7" + "digest": "sha256:1c768fa96dda900757ca48a66ddd741c62dbd5bd86e1356e75e3cc559ece92e0" } diff --git a/contracts/realization-envelopes/libvirt-qemu/generic-v1.json b/contracts/realization-envelopes/libvirt-qemu/generic-v1.json index e273d511..f4a2824b 100644 --- a/contracts/realization-envelopes/libvirt-qemu/generic-v1.json +++ b/contracts/realization-envelopes/libvirt-qemu/generic-v1.json @@ -12,12 +12,13 @@ }, "configuration": { "mode": "generic", - "configuration_digest": "sha256:af14a9757a7b80bedab5cea712f60d2a05ee1d3406fbb7c71e196eaca4746c15", + "configuration_digest": "sha256:cf7e5b0d28c25293d254c463a23958f66f92776579872fce8abbeb1227347b4b", "architecture": "x86_64", "image_policy": "local-qcow2", "network_policy": "libvirt-managed", "supported_node_types": ["switch", "compute"], "supported_os_families": ["linux"], + "operating_systems": [], "supported_content_types": ["file"], "supported_account_features": ["auth_method", "disabled", "groups", "home", "shell"], "supported_domain_profiles": [], @@ -27,6 +28,7 @@ }, "concerns": [ {"concern": "compute-substrate", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "virtual-machine", "transformations": []}, + {"concern": "operating-system", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, {"concern": "topology", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "libvirt-domain-network", "transformations": []}, {"concern": "architecture", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "qemu-x86_64-domain", "transformations": []}, {"concern": "image", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "qcow2-disk-attachment", "transformations": []}, @@ -38,5 +40,5 @@ {"concern": "service", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, {"concern": "acl", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "libvirt-nwfilter", "transformations": []} ], - "digest": "sha256:0a4ff252cde4d85a1f0ddc896a82d10679b805723df55d0042b9932a61d037e7" + "digest": "sha256:1c768fa96dda900757ca48a66ddd741c62dbd5bd86e1356e75e3cc559ece92e0" } diff --git a/contracts/realization-envelopes/libvirt-qemu/guest-certified-appliance-v1.json b/contracts/realization-envelopes/libvirt-qemu/guest-certified-appliance-v1.json index 9f8ba1fb..06b7d801 100644 --- a/contracts/realization-envelopes/libvirt-qemu/guest-certified-appliance-v1.json +++ b/contracts/realization-envelopes/libvirt-qemu/guest-certified-appliance-v1.json @@ -22,6 +22,7 @@ "supported_os_families": [ "linux" ], + "operating_systems": [], "supported_content_types": [ "file" ], @@ -41,7 +42,7 @@ "minimum": 1, "maximum": 2 }, - "configuration_digest": "sha256:bc4987adb1c0a8b179ff3953649bea10347d9662a756c7193f21081084f1ddcb" + "configuration_digest": "sha256:bc773acad54a27a26f63ddaae96d7c8b0f8d37226dc19815fc146fa092884dbf" }, "concerns": [ { @@ -51,6 +52,13 @@ "mechanism": "virtual-machine", "transformations": [] }, + { + "concern": "operating-system", + "disposition": "unsupported", + "observation_strength": "none", + "mechanism": null, + "transformations": [] + }, { "concern": "topology", "disposition": "realized", @@ -122,5 +130,5 @@ "transformations": [] } ], - "digest": "sha256:dbb1aff11bd4666561a2823f8d8d6da449af2332976fcc5d301da14fdb4c84d2" + "digest": "sha256:39becb940224f84350d4c16bfa0c2159e5aae3478004265b0cb675423c339aba" } diff --git a/contracts/realization-envelopes/libvirt-qemu/techvault-appliance-v1.json b/contracts/realization-envelopes/libvirt-qemu/techvault-appliance-v1.json index 93403e66..1c583a4b 100644 --- a/contracts/realization-envelopes/libvirt-qemu/techvault-appliance-v1.json +++ b/contracts/realization-envelopes/libvirt-qemu/techvault-appliance-v1.json @@ -12,12 +12,13 @@ }, "configuration": { "mode": "techvault-appliance", - "configuration_digest": "sha256:6a502f097764f96394a465833f421ccaad21bb5bb94860db8b831722d4febe74", + "configuration_digest": "sha256:c90445ed25f95a37dc28a2b9fc33053aa9febd532a350187cf9722408e14bbca", "architecture": "x86_64", "image_policy": "generated-initramfs-appliance", "network_policy": "generated-appliance-network", "supported_node_types": ["switch", "compute"], "supported_os_families": ["linux"], + "operating_systems": [], "supported_content_types": [], "supported_account_features": [], "supported_domain_profiles": [], @@ -27,6 +28,7 @@ }, "concerns": [ {"concern": "compute-substrate", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "virtual-machine", "transformations": []}, + {"concern": "operating-system", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, {"concern": "topology", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "libvirt-domain-network-readback", "transformations": []}, {"concern": "architecture", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "libvirt-domain-xml-readback", "transformations": []}, {"concern": "image", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "generated-initramfs-attachment-readback", "transformations": []}, @@ -38,5 +40,5 @@ {"concern": "service", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, {"concern": "acl", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []} ], - "digest": "sha256:8876bb4288509288260aa39bc47a5f0aa8c10087a709fcf5f2d5565450b4d548" + "digest": "sha256:a0b7164802d10fe929c1cf1115c969eaaf69041ce497ccce6d289f44ac36fe05" } diff --git a/contracts/realization-envelopes/reference-emulation/in-process-v1.json b/contracts/realization-envelopes/reference-emulation/in-process-v1.json index 5b6a5c2b..b656a2e2 100644 --- a/contracts/realization-envelopes/reference-emulation/in-process-v1.json +++ b/contracts/realization-envelopes/reference-emulation/in-process-v1.json @@ -12,12 +12,13 @@ }, "configuration": { "mode": "in-process-emulation", - "configuration_digest": "sha256:aaea24e0dbc889cb03080115b2bd68c5c33a005d882400adfd4cd935cd710b17", + "configuration_digest": "sha256:6ebdcb05bec9d9691110df0ce23c1b660936b4565415ab254026dffc8040a57a", "architecture": "x86_64", "image_policy": "descriptor-only", "network_policy": "in-process-emulation", "supported_node_types": ["switch", "compute"], "supported_os_families": ["linux", "windows", "macos", "freebsd", "other"], + "operating_systems": [], "supported_content_types": ["file", "dataset", "directory"], "supported_account_features": ["auth_method", "disabled", "groups", "home", "mail", "shell"], "supported_domain_profiles": [], @@ -27,6 +28,7 @@ }, "concerns": [ {"concern": "compute-substrate", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "x-openrae:in-process-emulation", "transformations": []}, + {"concern": "operating-system", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, {"concern": "topology", "disposition": "realized", "observation_strength": "driver-reported", "mechanism": "in-process-ledger", "transformations": []}, {"concern": "architecture", "disposition": "descriptor-only", "observation_strength": "driver-reported", "mechanism": "portable-descriptor", "transformations": []}, {"concern": "image", "disposition": "descriptor-only", "observation_strength": "driver-reported", "mechanism": "portable-descriptor", "transformations": []}, @@ -38,5 +40,5 @@ {"concern": "service", "disposition": "descriptor-only", "observation_strength": "driver-reported", "mechanism": "portable-descriptor", "transformations": []}, {"concern": "acl", "disposition": "descriptor-only", "observation_strength": "driver-reported", "mechanism": "portable-descriptor", "transformations": []} ], - "digest": "sha256:7e7b4e907437562cb5fbee20527a41a69da90d84da72d52db616fe707410b5ea" + "digest": "sha256:c1b809119bdce795c368dd4e04a67a9d3e7e5a1e52e49d860d2f8eb07b0869a5" } diff --git a/contracts/realization-envelopes/reference-emulation/oci-container-v1.json b/contracts/realization-envelopes/reference-emulation/oci-container-v1.json index 0c67ced9..5840beb0 100644 --- a/contracts/realization-envelopes/reference-emulation/oci-container-v1.json +++ b/contracts/realization-envelopes/reference-emulation/oci-container-v1.json @@ -12,12 +12,13 @@ }, "configuration": { "mode": "oci-container", - "configuration_digest": "sha256:bedcd981f7c28030e5ae6b845ead667727a75c995d45f81a84ff0d1a0922b851", + "configuration_digest": "sha256:5af65c08e8530288ba7d471d69a3e6856dc4bac783aa0beb80f1c1b3eff09d23", "architecture": "x86_64", "image_policy": "operator-trusted-oci-image", "network_policy": "oci-runtime-managed", "supported_node_types": ["switch", "compute"], "supported_os_families": ["linux"], + "operating_systems": [], "supported_content_types": ["file", "dataset", "directory"], "supported_account_features": ["auth_method", "disabled", "groups", "home", "mail", "shell"], "supported_domain_profiles": [], @@ -27,6 +28,7 @@ }, "concerns": [ {"concern": "compute-substrate", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "operating-system-container", "transformations": []}, + {"concern": "operating-system", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []}, {"concern": "topology", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "oci-daemon-inspect", "transformations": []}, {"concern": "architecture", "disposition": "descriptor-only", "observation_strength": "driver-reported", "mechanism": "portable-descriptor", "transformations": []}, {"concern": "image", "disposition": "realized", "observation_strength": "daemon-observed", "mechanism": "oci-daemon-inspect", "transformations": []}, @@ -38,5 +40,5 @@ {"concern": "service", "disposition": "descriptor-only", "observation_strength": "driver-reported", "mechanism": "portable-descriptor", "transformations": []}, {"concern": "acl", "disposition": "unsupported", "observation_strength": "none", "mechanism": null, "transformations": []} ], - "digest": "sha256:62f5cf073d4fa35f323be024c9e16e195a0bf8aad2f1067e4f2863fa200029a0" + "digest": "sha256:e62aa8c27ec68ce7384c559d27e75b5cc6253335e552b139e6a37c9c91321b76" } diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index e5ee7722..1e021834 100644 --- a/contracts/schema-publication/entries/backend-manifest-v2.json +++ b/contracts/schema-publication/entries/backend-manifest-v2.json @@ -2,9 +2,9 @@ "contract_id": "backend-manifest-v2", "schema_path": "contracts/schemas/backend-manifest/backend-manifest-v2.json", "stability": "draft", - "content_hash": "7a0b8682ca0dee26b1df767a1c0e86bd2b0dc6d8125dc6ef67aeab3367a7d2ef", + "content_hash": "45a269d3dd15d4c864ae210d1bfa258412b0e33a8a542eedfc16fa9fc26913c6", "last_change": { - "summary": "Added typed process-resource-limit apparatus domains for issue #1066, including portable resource terms, process/subtree scope, finite bounds, and unlimited-value support.", - "content_hash": "7a0b8682ca0dee26b1df767a1c0e86bd2b0dc6d8125dc6ef67aeab3367a7d2ef" + "summary": "Added coupled operating-system family, distribution, and bounded release capability rows for issue #1077.", + "content_hash": "45a269d3dd15d4c864ae210d1bfa258412b0e33a8a542eedfc16fa9fc26913c6" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json b/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json index e7f22744..640236ac 100644 --- a/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json +++ b/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json @@ -2,9 +2,9 @@ "contract_id": "instantiated-scenario-snapshot-v1", "schema_path": "contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json", "stability": "draft", - "content_hash": "0b4f45eca295ce8a3b3113b4dc208267d1d32fa4bf644f8ee912ef8f307c6c84", + "content_hash": "3e60ed2a2878f49d0aa3db325c82f2e61b5b2caa2be204298b31099d950dcc93", "last_change": { - "summary": "Separated compute resource kind from substrate constraints and preserved exact legacy virtual-machine intent during historical v1 snapshot migration for issue #1076.", - "content_hash": "0b4f45eca295ce8a3b3113b4dc208267d1d32fa4bf644f8ee912ef8f307c6c84" + "summary": "Separated governed operating-system distribution from release-only version values for issue #1077.", + "content_hash": "3e60ed2a2878f49d0aa3db325c82f2e61b5b2caa2be204298b31099d950dcc93" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-v1.json b/contracts/schema-publication/entries/instantiated-scenario-v1.json index c1522b49..0c2b6c89 100644 --- a/contracts/schema-publication/entries/instantiated-scenario-v1.json +++ b/contracts/schema-publication/entries/instantiated-scenario-v1.json @@ -2,9 +2,9 @@ "contract_id": "instantiated-scenario-v1", "schema_path": "contracts/schemas/sdl/instantiated-scenario-v1.json", "stability": "draft", - "content_hash": "e0b0ff3c896785ff8932cef60beb48e47e193a4856bb0f6eb5fac50c1bf51f92", + "content_hash": "d3f83594bc9f7dfbe05e6c2ad522e6e0d3c099b563a07a3cf955a8a62e7eef62", "last_change": { - "summary": "Added canonical compute node kind and portable compute-substrate constraint provenance for issue #1076.", - "content_hash": "e0b0ff3c896785ff8932cef60beb48e47e193a4856bb0f6eb5fac50c1bf51f92" + "summary": "Added governed operating-system distribution identity and release-only version constraints for issue #1077.", + "content_hash": "d3f83594bc9f7dfbe05e6c2ad522e6e0d3c099b563a07a3cf955a8a62e7eef62" } } diff --git a/contracts/schema-publication/entries/realization-envelope-v1.json b/contracts/schema-publication/entries/realization-envelope-v1.json index 8284a55b..c7b9ad06 100644 --- a/contracts/schema-publication/entries/realization-envelope-v1.json +++ b/contracts/schema-publication/entries/realization-envelope-v1.json @@ -2,9 +2,9 @@ "contract_id": "realization-envelope-v1", "schema_path": "contracts/schemas/realization-envelope/realization-envelope-v1.json", "stability": "draft", - "content_hash": "ee120d70afef4ebbd3e63a3a9b39e966986f4cf7a42cf30339848c78de003396", + "content_hash": "d8c49ab8d6c0db23a53e4d6808470e85c0839d5092386d5978e419af708dc464", "last_change": { - "summary": "Added governed compute-substrate claims and observation capability binding to selected material configurations for issue #1076.", - "content_hash": "ee120d70afef4ebbd3e63a3a9b39e966986f4cf7a42cf30339848c78de003396" + "summary": "Added coupled operating-system compatibility rows and an explicit operating-system realization concern for issue #1077.", + "content_hash": "d8c49ab8d6c0db23a53e4d6808470e85c0839d5092386d5978e419af708dc464" } } diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index 2d39bbbd..9eceff0b 100644 --- a/contracts/schema-publication/entries/runtime-snapshot-v1.json +++ b/contracts/schema-publication/entries/runtime-snapshot-v1.json @@ -2,9 +2,9 @@ "contract_id": "runtime-snapshot-v1", "schema_path": "contracts/schemas/snapshots/runtime-snapshot-v1.json", "stability": "draft", - "content_hash": "cdb549ce727995c8e14775096bfc927e34fb22a639b83a85be5f115069d11073", + "content_hash": "4388410c8e7536fef3f1251b7649b7d2cefef3764bf9da31d221430c92935879", "last_change": { - "summary": "Added value-bearing compute-substrate observations bound to operation, envelope, configuration, observer, and sequence for issue #1076.", - "content_hash": "cdb549ce727995c8e14775096bfc927e34fb22a639b83a85be5f115069d11073" + "summary": "Added typed guest-observed operating-system identity bound to operation, envelope, configuration, observer, and sequence for issue #1077.", + "content_hash": "4388410c8e7536fef3f1251b7649b7d2cefef3764bf9da31d221430c92935879" } } diff --git a/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json b/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json index 5b10e606..5194d07c 100644 --- a/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json +++ b/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json @@ -2,9 +2,9 @@ "contract_id": "scenario-satisfiability-evidence-v1", "schema_path": "contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json", "stability": "draft", - "content_hash": "56a173d2383a9f277f5aa7351bf207ab96c2728d63077f7e5fbce6b5dc727267", + "content_hash": "617becb282057789766c4c77967e0241ae64adab788ba491b7fa910678f3b953", "last_change": { - "summary": "Propagated canonical compute kind and compute-substrate provenance through instantiated satisfiability witnesses for issue #1076.", - "content_hash": "56a173d2383a9f277f5aa7351bf207ab96c2728d63077f7e5fbce6b5dc727267" + "summary": "Propagated governed operating-system distribution and release identity through satisfiability witnesses for issue #1077.", + "content_hash": "617becb282057789766c4c77967e0241ae64adab788ba491b7fa910678f3b953" } } diff --git a/contracts/schema-publication/entries/sdl-authoring-input-v1.json b/contracts/schema-publication/entries/sdl-authoring-input-v1.json index 2c93e4cc..870b8fbc 100644 --- a/contracts/schema-publication/entries/sdl-authoring-input-v1.json +++ b/contracts/schema-publication/entries/sdl-authoring-input-v1.json @@ -2,9 +2,9 @@ "contract_id": "sdl-authoring-input-v1", "schema_path": "contracts/schemas/sdl/sdl-authoring-input-v1.json", "stability": "draft", - "content_hash": "69ba9eca79c570c38f98777640054fbde37a704d8caa9896cee053fcda5ad1c6", + "content_hash": "81714a436a72b511cdc61b2599af1327ad07d96520569718cc3d53b121d81074", "last_change": { - "summary": "Replaced the mechanism-specific vm node kind with portable compute and added addressed compute-substrate constraints for issue #1076.", - "content_hash": "69ba9eca79c570c38f98777640054fbde37a704d8caa9896cee053fcda5ad1c6" + "summary": "Added governed operating-system distribution authoring and release-only version semantics for issue #1077.", + "content_hash": "81714a436a72b511cdc61b2599af1327ad07d96520569718cc3d53b121d81074" } } diff --git a/contracts/schemas/backend-manifest/backend-manifest-v2.json b/contracts/schemas/backend-manifest/backend-manifest-v2.json index 2f2989ca..35f163a2 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -729,6 +729,42 @@ "title": "ObservationStrength", "type": "string" }, + "OperatingSystemCompatibilityModel": { + "additionalProperties": false, + "description": "One portable, coupled OS family/distribution/release capability row.", + "properties": { + "distribution": { + "minLength": 1, + "title": "Distribution", + "type": "string" + }, + "family": { + "minLength": 1, + "title": "Family", + "type": "string" + }, + "versions": { + "items": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[!-~](?:[ -~]{0,126}[!-~])?$", + "type": "string" + }, + "maxItems": 128, + "minItems": 1, + "title": "Versions", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "family", + "distribution", + "versions" + ], + "title": "OperatingSystemCompatibilityModel", + "type": "object" + }, "OrchestratorCapabilitiesModel": { "additionalProperties": false, "allOf": [ @@ -1925,6 +1961,14 @@ "title": "Name", "type": "string" }, + "operating_systems": { + "items": { + "$ref": "#/$defs/OperatingSystemCompatibilityModel" + }, + "title": "Operating Systems", + "type": "array", + "uniqueItems": true + }, "supported_account_features": { "items": { "minLength": 1, diff --git a/contracts/schemas/realization-envelope/realization-envelope-v1.json b/contracts/schemas/realization-envelope/realization-envelope-v1.json index 37ae5952..5898479a 100644 --- a/contracts/schemas/realization-envelope/realization-envelope-v1.json +++ b/contracts/schemas/realization-envelope/realization-envelope-v1.json @@ -315,6 +315,42 @@ "title": "ObservationStrength", "type": "string" }, + "OperatingSystemCompatibilityModel": { + "additionalProperties": false, + "description": "One portable, coupled OS family/distribution/release capability row.", + "properties": { + "distribution": { + "minLength": 1, + "title": "Distribution", + "type": "string" + }, + "family": { + "minLength": 1, + "title": "Family", + "type": "string" + }, + "versions": { + "items": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[!-~](?:[ -~]{0,126}[!-~])?$", + "type": "string" + }, + "maxItems": 128, + "minItems": 1, + "title": "Versions", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "family", + "distribution", + "versions" + ], + "title": "OperatingSystemCompatibilityModel", + "type": "object" + }, "Posture": { "description": "Author/backend intent for a bound value or child scope.", "enum": [ @@ -404,7 +440,8 @@ "feature-binding", "service", "acl", - "compute-substrate" + "compute-substrate", + "operating-system" ], "title": "RealizationConcern", "type": "string" @@ -668,6 +705,14 @@ "title": "Network Policy", "type": "string" }, + "operating_systems": { + "items": { + "$ref": "#/$defs/OperatingSystemCompatibilityModel" + }, + "title": "Operating Systems", + "type": "array", + "uniqueItems": true + }, "process_resource_limits": { "items": { "$ref": "#/$defs/ProcessResourceLimitCapabilityModel" @@ -1007,13 +1052,28 @@ }, "maxContains": 1, "minContains": 1 + }, + { + "contains": { + "properties": { + "concern": { + "const": "operating-system" + } + }, + "required": [ + "concern" + ], + "type": "object" + }, + "maxContains": 1, + "minContains": 1 } ], "items": { "$ref": "#/$defs/RealizationConcernDisclosureModel" }, - "maxItems": 11, - "minItems": 11, + "maxItems": 12, + "minItems": 12, "title": "Concerns", "type": "array" }, diff --git a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json index 235a5743..77ac1652 100644 --- a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json +++ b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json @@ -6579,6 +6579,59 @@ }, "Node": { "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "os_version": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "os_version" + ] + }, + "then": { + "properties": { + "os_distribution": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os_distribution" + ] + } + }, + { + "if": { + "properties": { + "os_distribution": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os_distribution" + ] + }, + "then": { + "properties": { + "os": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os" + ] + } + } + ], "description": "A scenario node \u2014 either a compute endpoint or a strict switch.\n\nThe ``type`` field determines which structural variant is active. Compute\nfields are only valid for compute nodes; switches carry no extra data.", "properties": { "architecture": { @@ -6685,11 +6738,31 @@ "default": null, "title": "Os" }, + "os_distribution": { + "anyOf": [ + { + "$ref": "#/$defs/OSDistribution" + }, + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*|\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Os Distribution" + }, "os_version": { "default": "", "not": { "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" }, + "pattern": "^(?:[!-~](?:[ -~]{0,126}[!-~])?|\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\})?$", "title": "Os Version", "type": "string" }, @@ -6981,6 +7054,20 @@ "title": "NumericType", "type": "string" }, + "OSDistribution": { + "description": "Canonical portable distribution or product-line identifiers.", + "enum": [ + "ubuntu", + "debian", + "rocky-linux", + "red-hat-enterprise-linux", + "windows-server", + "windows-client", + "solaris" + ], + "title": "OSDistribution", + "type": "string" + }, "OSFamily": { "description": "Operating system family. Vocabulary from OCSF Device.os.", "enum": [ diff --git a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json index c3e0880e..b03135d9 100644 --- a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json @@ -6729,6 +6729,59 @@ }, "Node": { "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "os_version": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "os_version" + ] + }, + "then": { + "properties": { + "os_distribution": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os_distribution" + ] + } + }, + { + "if": { + "properties": { + "os_distribution": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os_distribution" + ] + }, + "then": { + "properties": { + "os": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os" + ] + } + } + ], "description": "A scenario node \u2014 either a compute endpoint or a strict switch.\n\nThe ``type`` field determines which structural variant is active. Compute\nfields are only valid for compute nodes; switches carry no extra data.", "properties": { "architecture": { @@ -6835,11 +6888,31 @@ "default": null, "title": "Os" }, + "os_distribution": { + "anyOf": [ + { + "$ref": "#/$defs/OSDistribution" + }, + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*|\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Os Distribution" + }, "os_version": { "default": "", "not": { "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" }, + "pattern": "^(?:[!-~](?:[ -~]{0,126}[!-~])?|\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\})?$", "title": "Os Version", "type": "string" }, @@ -7071,6 +7144,20 @@ "title": "NumericType", "type": "string" }, + "OSDistribution": { + "description": "Canonical portable distribution or product-line identifiers.", + "enum": [ + "ubuntu", + "debian", + "rocky-linux", + "red-hat-enterprise-linux", + "windows-server", + "windows-client", + "solaris" + ], + "title": "OSDistribution", + "type": "string" + }, "OSFamily": { "description": "Operating system family. Vocabulary from OCSF Device.os.", "enum": [ diff --git a/contracts/schemas/sdl/instantiated-scenario-v1.json b/contracts/schemas/sdl/instantiated-scenario-v1.json index 52ee7225..8226bd5b 100644 --- a/contracts/schemas/sdl/instantiated-scenario-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-v1.json @@ -6086,6 +6086,59 @@ }, "Node": { "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "os_version": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "os_version" + ] + }, + "then": { + "properties": { + "os_distribution": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os_distribution" + ] + } + }, + { + "if": { + "properties": { + "os_distribution": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os_distribution" + ] + }, + "then": { + "properties": { + "os": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os" + ] + } + } + ], "description": "A scenario node \u2014 either a compute endpoint or a strict switch.\n\nThe ``type`` field determines which structural variant is active. Compute\nfields are only valid for compute nodes; switches carry no extra data.", "properties": { "architecture": { @@ -6192,11 +6245,31 @@ "default": null, "title": "Os" }, + "os_distribution": { + "anyOf": [ + { + "$ref": "#/$defs/OSDistribution" + }, + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "pattern": "^(?:x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*|\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Os Distribution" + }, "os_version": { "default": "", "not": { "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" }, + "pattern": "^(?:[!-~](?:[ -~]{0,126}[!-~])?|\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\})?$", "title": "Os Version", "type": "string" }, @@ -6428,6 +6501,20 @@ "title": "NumericType", "type": "string" }, + "OSDistribution": { + "description": "Canonical portable distribution or product-line identifiers.", + "enum": [ + "ubuntu", + "debian", + "rocky-linux", + "red-hat-enterprise-linux", + "windows-server", + "windows-client", + "solaris" + ], + "title": "OSDistribution", + "type": "string" + }, "OSFamily": { "description": "Operating system family. Vocabulary from OCSF Device.os.", "enum": [ diff --git a/contracts/schemas/sdl/sdl-authoring-input-v1.json b/contracts/schemas/sdl/sdl-authoring-input-v1.json index b2724dfc..bc91e802 100644 --- a/contracts/schemas/sdl/sdl-authoring-input-v1.json +++ b/contracts/schemas/sdl/sdl-authoring-input-v1.json @@ -5121,6 +5121,59 @@ }, "Node": { "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "os_version": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "os_version" + ] + }, + "then": { + "properties": { + "os_distribution": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os_distribution" + ] + } + }, + { + "if": { + "properties": { + "os_distribution": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os_distribution" + ] + }, + "then": { + "properties": { + "os": { + "not": { + "type": "null" + } + } + }, + "required": [ + "os" + ] + } + } + ], "description": "A scenario node \u2014 either a compute endpoint or a strict switch.\n\nThe ``type`` field determines which structural variant is active. Compute\nfields are only valid for compute nodes; switches carry no extra data.", "properties": { "architecture": { @@ -5206,8 +5259,25 @@ "default": null, "title": "Os" }, + "os_distribution": { + "anyOf": [ + { + "$ref": "#/$defs/OSDistribution" + }, + { + "pattern": "^(?:x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*|\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Os Distribution" + }, "os_version": { "default": "", + "pattern": "^(?:[!-~](?:[ -~]{0,126}[!-~])?|\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\})?$", "title": "Os Version", "type": "string" }, @@ -5424,6 +5494,20 @@ "title": "NumericType", "type": "string" }, + "OSDistribution": { + "description": "Canonical portable distribution or product-line identifiers.", + "enum": [ + "ubuntu", + "debian", + "rocky-linux", + "red-hat-enterprise-linux", + "windows-server", + "windows-client", + "solaris" + ], + "title": "OSDistribution", + "type": "string" + }, "OSFamily": { "description": "Operating system family. Vocabulary from OCSF Device.os.", "enum": [ diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index 878b672e..60b12ed3 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -811,6 +811,36 @@ "title": "ObservationStrength", "type": "string" }, + "ObservedOperatingSystemIdentityModel": { + "additionalProperties": false, + "description": "Typed guest-observed OS identity carried by a bound disclosure.", + "properties": { + "distribution": { + "minLength": 1, + "title": "Distribution", + "type": "string" + }, + "family": { + "minLength": 1, + "title": "Family", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[!-~](?:[ -~]{0,126}[!-~])?$", + "title": "Version", + "type": "string" + } + }, + "required": [ + "family", + "distribution", + "version" + ], + "title": "ObservedOperatingSystemIdentityModel", + "type": "object" + }, "OutcomeInterpretationSourceLayer": { "description": "Semantic layers that may feed a SEM-215 interpretation rule.", "enum": [ @@ -10526,6 +10556,17 @@ "default": null, "title": "Observer Version" }, + "operating_system": { + "anyOf": [ + { + "$ref": "#/$defs/ObservedOperatingSystemIdentityModel" + }, + { + "type": "null" + } + ], + "default": null + }, "operation_id": { "anyOf": [ { diff --git a/docs/decisions/issue-1077-os-distribution-version-realization-preflight.md b/docs/decisions/issue-1077-os-distribution-version-realization-preflight.md new file mode 100644 index 00000000..829c359f --- /dev/null +++ b/docs/decisions/issue-1077-os-distribution-version-realization-preflight.md @@ -0,0 +1,266 @@ +# Issue 1077 OS distribution and version realization preflight + +- **Status:** implementation guidance +- **Date:** 2026-08-12 +- **Issue:** #1077 +- **Requirement:** none; the issue is the authoritative contract + +This note records the repository-wide semantic boundary for carrying authored OS +distribution and version through admission and realization evidence. It does not +choose a backend image-selection algorithm and is not an implementation plan. +Existing ADRs already own the relevant boundaries, so this note does not create +a new architectural decision. + +## Current-state inventory + +| Fact | Current authority | Current behavior and ambiguity | +| --- | --- | --- | +| OS family | `Node.os` and the `OSFamily` controlled vocabulary | Portable, broad family (`linux`, `windows`, and similar). It is captured as a capability constraint, registered as the SEM-218 `os-family` concern, and admitted against `ProvisionerCapabilities.supported_os_families`. | +| OS distribution or product line | No authored field | It is sometimes embedded in `Node.os_version` (`Ubuntu 22.04`, `Server 2022`) and is otherwise selected by a backend. No portable admission contract exists. | +| OS version or release | `Node.os_version: str` | The unconstrained string is parsed and serialized but is not captured by the processor, registered as a realization concern, admitted by the planner, or checked by runtime non-approximation. | +| Architecture | `Node.architecture` | An independent compute constraint with its own controlled vocabulary, capability domain, SEM-218 concern, and admission checks. It must remain independent of OS identity. | +| Source or artifact | `Node.source`, `Source`, and `ArtifactRequirement` | A provider-neutral artifact selector and, when authored, an independent realization requirement. `Source.version` is an artifact selector; it is not an OS release. Source identity must not imply OS intent. | +| Backend selection | Reference and libvirt realization interpreters plus configuration-selected capability envelopes | The reference backend uses `Source.name` or an OS-family fallback. Libvirt interprets `Source.name` as a local image path. Both ignore `Source.version` and `Node.os_version`; neither independently observes guest OS identity. | +| Realized OS evidence | No typed OS observation | Planned node payloads can echo authored fields, but an echo is not observation. `RealizationObservationDisclosure` is the canonical operation-bound evidence carrier and currently has no value-bearing OS identity payload. | + +`Node.os` and `Node.os_version` entered the extracted SDL model together in +commit `2e73ee6c` without a validator or a documented relationship between them. +The only normative origin recorded for `os` is the OCSF-style family vocabulary. +No later history assigns `os_version` release-only semantics. The current model, +documentation, and examples therefore do not justify treating the existing +string as an already-normalized release. + +The external APTL behavior described by issue #1077 is an ecosystem boundary, +not code in this repository: its generic selector accepts `os_version` but may +choose Debian 12 or Rocky Linux 9 from package-manager hints. This repository +must make that substitution impossible to claim as successful, but cannot +prescribe or certify APTL's selection implementation here. + +## Existing scenario audit + +The authored examples with populated `os_version` currently mean: + +| Location | Current value | Intended interpretation to make explicit | +| --- | --- | --- | +| `hospital-ransomware-surgery-day.sdl.yaml` | `Server 2022`, `Server 2019` | Windows Server product line plus release | +| `hospital-ransomware-surgery-day.sdl.yaml` | `11` | Windows client product line plus release | +| `port-authority-surge-response.sdl.yaml` | `10`, `11` | Windows client product line plus release | +| SDL section documentation | `Ubuntu 22.04` | Ubuntu distribution plus release | + +The test and stress corpus additionally contains Ubuntu releases, Windows Server +releases, `Solaris 10`, and `SIEMENS S7-300`. The first three groups have the +same distribution/product-line-plus-release ambiguity. `SIEMENS S7-300` is a +device/platform model and is not OS identity; it must move to the appropriate +platform, device, or source semantic instead of being carried forward as an OS +release. + +Migration must audit these values explicitly. It must not split arbitrary legacy +strings by whitespace, infer distribution from capitalization, or preserve a +dual meaning in `os_version`. + +## Semantic boundary + +Portable authored OS identity has three separate dimensions: + +1. **Family** is the existing broad `Node.os` constraint. +2. **Distribution/product line** is a governed portable token such as `ubuntu`, + `windows-server`, `windows-client`, or `solaris`. +3. **Version/release** is a bounded, non-empty opaque token within that + distribution/product line. + +The authoring surface should add one distribution/product-line field and narrow +`os_version` to release-only meaning. A version requires a distribution, and a +distribution requires a family. Family alone remains valid. Architecture, +artifact/source identity, package manager, kernel, edition, image reference, +and device model are not aliases for any of these dimensions. + +Distribution tokens need one controlled-vocabulary authority and the repository's +existing governed-extension mechanism. Version comparison is exact token +comparison unless an authored finite domain is present. Do not introduce SemVer +interpretation, numeric ordering, wildcards, `latest`, implicit case folding, or +an `other`/`unknown` distribution sentinel. Absence and SEM-218 designation +already express openness without sentinel values. + +The existing SEM-218 classifier and realization-designation resolver remain +separate authorities: + +- an authored concrete literal leaf is **exact**, except for the classifier's + existing `other`/`unknown` enum sentinels, which are **open**; +- a whole-field variable is **constrained**; OS planning admission requires a + finite `allowed_values` domain rather than treating an unbounded variable as + portable backend permission; and +- an absent leaf has no classifier record and becomes an **open** realization + requirement only when explicitly designated through `realization.scopes`. + +This permits an author to constrain only `os: linux` while deliberately leaving +distribution and version open. The open designation belongs on the missing OS +leaf or leaves; it must not be fabricated through `Source`, an artifact +requirement, an image mechanism, `*`, or a magic version value. An omitted leaf +without an open designation remains absent rather than becoming an implicit +backend promise. + +When OS identity and source/artifact requirements are both authored, they are +conjunctive. A selected artifact must satisfy the artifact contract and the +realized guest must satisfy the OS contract. Image names, tags, package-manager +hints, and artifact metadata may inform backend selection but do not rewrite +authored OS intent or independently prove the running OS. + +## Planning and capability admission + +The canonical SEM-218 registry in +`raes_processor.semantics.realization_concerns` must own separate +`os-distribution` and `os-version` leaf concerns, alongside `os-family` and +`node-architecture`. The processor's typed `NodeRuntime` projection and +top-level plan payload must carry the corresponding values; an opaque copy under +`spec.node` is not canonical planning authority. + +`capture_capability_constraints`, `CapabilityConstraint`, instantiated +explicitness, realization requirements, and `CompiledRealizationAuthority` must +carry the same finite value domains. The planner and runtime must reuse the +bounded-domain algebra in `raes_contracts.bounded_domains` and +`scalar_in_domain`. Adding OS-only constraint syntax or allowing ordinary +constrained scalar concerns to accept an arbitrary backend value would duplicate +and weaken the existing domain authority. + +Backend support must be represented as coupled OS compatibility entries, not +three independent allowlists. A canonical entry binds: + +- family; +- distribution/product line; and +- a bounded version domain. + +An equivalent shape should be shared by `ProvisionerCapabilities`, its Pydantic +model, `RealizerConfigurationModel`, `BackendManifestV2Model` serialization, +and schema output. +The existing coarse family set should be derived from or mechanically +cross-validated against these entries so there is one value-level authority. +Independent `supported_os_distributions` and `supported_os_versions` sets are +forbidden because they falsely advertise the Cartesian product. The existing +`ArtifactMechanismCapability` pairing is the precedent for avoiding that +overclaim. + +Admission intersects authored exact/constrained/open posture with a compatible +entry before backend side effects. Exact values must match one entry; +constrained values must have a non-empty intersection and retain their plan +authority bound; an open leaf permits a backend choice only within a compatible +entry. A family-only backend claim cannot satisfy a distribution or version +requirement. + +## Backend selection and realization evidence + +Reference, libvirt, and downstream backends may choose their own selection +algorithm, but the following contract is invariant: + +- capability/configuration admission occurs before driver I/O; +- source/artifact and OS requirements are checked independently and jointly; +- a backend cannot advertise exact or constrained OS support until its selected + artifact is compatible with the admitted OS entry; +- selected family, distribution, and version remain separate from the authored + desired payload; and +- successful boot, plan echo, image name, or configuration assertion alone is + not proof of the running OS. + +Extend the existing operation-bound `RealizationObservationDisclosure` path with +one typed, bounded, non-secret observed OS identity. One observation may +corroborate the family, distribution, and version leaf requirements; do not +perform three guest probes or create three competing evidence DTOs. Each leaf is +still evaluated against its own requirement and authority bound. The disclosure +must retain operation, envelope, configuration digest, observer, and sequence +binding and pass the existing observation-consistency validator. + +For a runnable guest, distribution and release claims require guest-observed +evidence (for example, the platform's canonical OS identity interface), not an +unverified image label. Raw probe output is not persisted. Backends that can +only describe or model an OS must disclose that weaker posture and cannot claim +exact runtime satisfaction. The selected identity belongs in realization +observations; it must not be placed in value-free provenance, metadata/details, +diagnostic messages, or by mutating the authored node payload. + +Runtime non-approximation must reject and return the baseline snapshot for: + +- an exact family, distribution, or version mismatch; +- a constrained selected value outside the authored finite domain; +- an admitted compatibility-row violation; +- missing, malformed, weak, stale, or execution-mismatched OS observation; and +- a backend claiming support without the corresponding manifest/configuration + envelope and observation capability. + +Use the existing stable diagnostic and backend-contract-invalid paths. Do not +add an OS-specific exception hierarchy or leak selected values, image paths, +probe output, or `str(exc)` through API error envelopes. + +## Cross-cutting incumbents and gates + +The implementation must build on these existing authorities rather than create +parallel ones: + +| Layer | Canonical incumbent and required treatment | +| --- | --- | +| Input safety and shape | `raes.parser` (`SDLParserLimits`, bounded UTF-8/YAML parsing, tag/alias/directive/depth/node/scalar and duplicate-key checks) followed by closed Pydantic `SDLModel` shapes. New fields and vocabulary tokens pass these same bounds and `extra="forbid"`; no secondary YAML or free-form mapping parser. | +| Semantic validation | `SemanticValidator`, variable-reference validation, compute-node field validation, and designation pointer resolution. Enforce family/distribution/version dependency once here and in generated JSON Schema where structural; do not repeat divergent rules in each backend. | +| Instantiation | Existing substitution, finite `allowed_values`, explicitness/provenance derivation, namespace rewriting, and concrete model revalidation. Preserve exact/constrained/open records for the new leaves. | +| Controlled vocabularies | Existing vocabulary loader, concept authority registry, governed extensions, catalog parity, and generated-schema checks. Distribution/product-line spelling has one authority. | +| Processor and planning | `NodeRuntime`, `capture_capability_constraints`, `CapabilityConstraint`, the realization concern registry, `CompiledRealizationAuthority`, capability-domain checks, manifest validation, and plan authority completeness. No opaque `spec.node` lookup or backend-local reinterpretation. | +| Manifest and configuration | `ProvisionerCapabilities` plus its model, `RealizerConfigurationModel`, `RealizerManifestModel`, configuration digest binding, and the configuration-selected support envelope. The coupled OS entry is the value-level authority; coarse support declarations remain posture/kind declarations. | +| Backend boundary | Pure reference/libvirt realization interpreters and capability-envelope validation before the driver boundary. Fixed argv/no-shell and bounded subprocess conventions remain unchanged; OS tokens never become shell fragments, credentials, URIs, native IDs, or host paths. | +| Runtime contract | `_call_backend_apply`, snapshot contract checks, `realization_authority_disclosure`, `realization_disclosure`, observation consistency, and baseline rollback. Reuse `runtime.backend-contract-invalid` and exception-class-only backend failure reduction. | +| Evidence and persistence | `RealizationObservationDisclosure`, `RuntimeSnapshotEnvelopeModel`, `ControlPlaneStore`, and the existing atomic snapshot write/round-trip path. Do not add a sidecar, cache, repository, database column, or metadata/details escape hatch. | +| API/auth/audit | Existing strict control-plane security defaults, `_ControlPlaneApiAuth`, role/target authorization, request-size limits, planner-authorized plan checks, idempotency, and `AuditEvent`. No new route is required; authorized snapshot reads expose the typed evidence, while diagnostics stay value-free. | +| Conformance | `run_target_conformance`, positive and single-fault negative envelope probes, no-mutation evidence, dishonest-backend fixtures, and `BackendConformanceReport`. Exact, constrained, and open cases must cover processor output, manifest/configuration admission, runtime mismatch rejection, evidence binding, schema round-trip, and no side effect on preflight failure. | + +OS identity is non-secret bounded data, but it still travels only through typed +models and authorized snapshot APIs. It must not be sourced from ambient +environment variables, passed in process arguments to ad hoc probes, written to +unstructured logs, or used to expose host paths. Artifact acquisition +credentials remain outside SDL, plans, snapshots, and process argv. + +The repository workflow authorities in `.ground-control.yaml`, +`.gc/plan-rules.md`, `noxfile.py`, and `tools/` remain applicable, including +repository policy, authority-boundary, concept-authority, generated-schema, +schema-publication, SDL catalog/lineage, and JSON-artifact checks. The four +draft Node-bearing contracts under ADR-061 (`sdl-authoring-input-v1`, +`instantiated-scenario-v1`, `instantiated-scenario-snapshot-v1`, and +`scenario-satisfiability-evidence-v1`) must change together. The affected +generated contract surface also includes `backend-manifest-v2`, +`realization-envelope-v1`, `provisioning-plan-v1`, `runtime-snapshot-v1`, and +the controlled-vocabulary/catalog artifacts. Regenerate and publish these +through the canonical bundle and publication tooling with an explicit migration +record; do not hand-edit schema copies. + +## Extensibility seam + +The coupled OS compatibility entry and the typed observed identity are the seam +for the next legitimate dimension, such as edition, build, or kernel. Such a +dimension should be added as another separately authored, separately designated +leaf and another bounded component of the same compatibility/observation +contract. It must not require replacing the canonical OS entry, and it must not +be smuggled into distribution or version strings. + +The bounded-domain and concern-registry machinery must remain generic enough for +the next constrained scalar concern. Backend selection stays behind the existing +manifest/configuration boundary, so a backend can add a selector without +changing portable authoring semantics. + +## Non-goals and anti-patterns + +This issue does not: + +- standardize a reference, libvirt, or APTL image-selection algorithm; +- turn OS distribution/version into artifact identity, package-manager policy, + architecture, runtime-package inventory, kernel, edition, or device model; +- expand the legacy whole-scenario satisfiability profile without a separately + governed profile revision; +- introduce implicit distribution inference, best-effort substitution, fuzzy or + ordered version comparison, magic sentinel values, or compatibility aliases + outside the controlled-vocabulary authority; +- accept plan echo, image tags, boot success, logs, metadata/details, or + value-free provenance as realization evidence; +- create duplicate schemas, validators, exception types, logging pipelines, + stores, conformance runners, or backend-specific OS contracts; or +- preserve ambiguous legacy `os_version` strings through an automatic heuristic + migration. + +Reference and libvirt must advertise only what their selectors and observers can +actually satisfy. Until they can admit a coupled OS requirement and produce the +required independent observation, unsupported exact/constrained cases must fail +before mutation rather than being approximated by family or image choice. diff --git a/docs/explain/sdl/sections.md b/docs/explain/sdl/sections.md index f7bc194b..07ace3e9 100644 --- a/docs/explain/sdl/sections.md +++ b/docs/explain/sdl/sections.md @@ -66,6 +66,15 @@ plane (ADR-055/064/069). Declarative `conditions` remain. Nodes are the compute and network elements of the scenario. +Operating-system identity is split into three independent fields: `os` is the +family, `os_distribution` is the governed distribution or product line, and +`os_version` is only the release token. A version requires a distribution, and +a distribution requires a family. Backends admit distribution/version pairs as +coupled compatibility rows; they must not infer support by cross-pairing values +from different rows. Runtime proof for these fields is guest-observed evidence +bound to the executing operation and selected realization envelope—not an image +tag, boot success, or a copied plan/snapshot value. + ```yaml nodes: corp-switch: @@ -76,7 +85,8 @@ nodes: type: compute os: linux # windows, linux, macos, freebsd, other architecture: x86_64 # target-node CPU architecture: x86_64, aarch64 - os_version: "Ubuntu 22.04" + os_distribution: ubuntu + os_version: "22.04" source: ubuntu-22.04 # provider-neutral image reference resources: ram: 4 GiB # human-readable: GiB, MiB, GB, MB @@ -684,7 +694,7 @@ nodes: domain_name: TECHVAULT realm: TECHVAULT.LOCAL base_dn: DC=techvault,DC=local - services: + services: - service_id: ldap-endpoint service: ldap # owning same-node Node.services[].name protocol: ldap diff --git a/docs/research/formal-semantic-validation/bundles/base-v1.2.json b/docs/research/formal-semantic-validation/bundles/base-v1.2.json index 68b84863..f8ff9baf 100644 --- a/docs/research/formal-semantic-validation/bundles/base-v1.2.json +++ b/docs/research/formal-semantic-validation/bundles/base-v1.2.json @@ -9,5 +9,5 @@ "protocol_sha256": "cda5d300dfb9ec553cbc86538eaaf4a8ef43dfa59a14c1dcc113a56cfac8d826", "revision": "1.2.0", "snapshot_path": "docs/research/formal-semantic-validation/execution-snapshot-v1.2.json", - "snapshot_sha256": "e6af364cce3e689de2d7af10ea8e783fcc86d4c13333f09302807d0391ef3199" + "snapshot_sha256": "6071efbcb9ff2c713a5f4d5a61f421b8adbcf217591ade1e146e4d222fa0b077" } diff --git a/docs/research/formal-semantic-validation/bundles/retest-v2.json b/docs/research/formal-semantic-validation/bundles/retest-v2.json index 3b94dad3..57c4f316 100644 --- a/docs/research/formal-semantic-validation/bundles/retest-v2.json +++ b/docs/research/formal-semantic-validation/bundles/retest-v2.json @@ -58,5 +58,5 @@ "protocol_sha256": "abf94093e344bf495dfb04e8b0c5985c0beaab8ebb17a75e15c8674fa81b1a7c", "revision": "3.0.0", "snapshot_path": "docs/research/formal-semantic-validation/execution-snapshot-v2.json", - "snapshot_sha256": "9906db9a340b56ecef4389f98b0c5d18bb00efe58fd8da2c8739b3bac039cb5b" + "snapshot_sha256": "a1db1b37cb383fc54d750d973c761ca09564053d6d57a96341f42bafabce9c81" } diff --git a/docs/research/formal-semantic-validation/bundles/satisfiability-v1.json b/docs/research/formal-semantic-validation/bundles/satisfiability-v1.json index e08b06ed..8dbb09a8 100644 --- a/docs/research/formal-semantic-validation/bundles/satisfiability-v1.json +++ b/docs/research/formal-semantic-validation/bundles/satisfiability-v1.json @@ -22,5 +22,5 @@ "protocol_sha256": "cda5d300dfb9ec553cbc86538eaaf4a8ef43dfa59a14c1dcc113a56cfac8d826", "revision": "2.0.0", "snapshot_path": "docs/research/formal-semantic-validation/execution-snapshot-v1.2.json", - "snapshot_sha256": "e6af364cce3e689de2d7af10ea8e783fcc86d4c13333f09302807d0391ef3199" + "snapshot_sha256": "6071efbcb9ff2c713a5f4d5a61f421b8adbcf217591ade1e146e4d222fa0b077" } diff --git a/docs/research/formal-semantic-validation/execution-snapshot-v1.2.json b/docs/research/formal-semantic-validation/execution-snapshot-v1.2.json index fb542d93..80b30d01 100644 --- a/docs/research/formal-semantic-validation/execution-snapshot-v1.2.json +++ b/docs/research/formal-semantic-validation/execution-snapshot-v1.2.json @@ -76,7 +76,7 @@ "replayable": true, "actual_outcome": "accepted", "diagnostic_kind": null, - "result_digest": "ba0ecbfcb3090ffd6b660cb51324fafcd47ca8dedbbb985e98b6e7f64f8cc25b", + "result_digest": "8291a196859dbb09e9b2ef586ce179f1138139f469b92f03f16afb35877461a1", "evidence_refs": [ "docs/research/formal-semantic-validation/corpus/semantic-valid.sdl.yaml" ], @@ -230,7 +230,7 @@ "replayable": true, "actual_outcome": "stable", "diagnostic_kind": null, - "result_digest": "23b9d84fa757bd80436357ed52569b5445b0e4161641598e4b15c3b18cf6e668", + "result_digest": "4bb77034a8f2b1a577700ad03772a80acc0f4515a6831c8a35ac1bf50482d760", "evidence_refs": [ "docs/research/formal-semantic-validation/corpus/determinism-a.sdl.yaml", "implementations/python/tests/test_pipeline_determinism.py" @@ -246,7 +246,7 @@ "replayable": true, "actual_outcome": "distinguishable", "diagnostic_kind": null, - "result_digest": "2e92bdb90a218c29201312052b64b7fb88e8a65e887f05168e2273d9710a5080", + "result_digest": "6cdc44529a87fb9addaf4040795c7f9ae702c5f6ae30e29a5086ee60072ded73", "evidence_refs": [ "docs/research/formal-semantic-validation/corpus/determinism-a.sdl.yaml", "docs/research/formal-semantic-validation/corpus/determinism-b.sdl.yaml" diff --git a/docs/research/formal-semantic-validation/execution-snapshot-v2.json b/docs/research/formal-semantic-validation/execution-snapshot-v2.json index b65bc5a6..7ec48714 100644 --- a/docs/research/formal-semantic-validation/execution-snapshot-v2.json +++ b/docs/research/formal-semantic-validation/execution-snapshot-v2.json @@ -120,11 +120,11 @@ "result_digest" ], "disposition": "accepted", - "rationale": "Canonical result bytes and their digest changed across the RAES identity cutover, the issue #674 target-node CPU architecture field addition, and issue #1066 registration of the portable process-resource-limit realization concern; the governed outcome and diagnostic remain unchanged.", + "rationale": "Canonical result bytes and their digest changed across the RAES identity cutover, the issue #674 target-node CPU architecture field addition, issue #1066 registration of the portable process-resource-limit realization concern, and issue #1077 separation of OS distribution and release identity; the governed outcome and diagnostic remain unchanged.", "retest": { "actual_outcome": "accepted", "diagnostic_kind": null, - "result_digest": "ba0ecbfcb3090ffd6b660cb51324fafcd47ca8dedbbb985e98b6e7f64f8cc25b" + "result_digest": "8291a196859dbb09e9b2ef586ce179f1138139f469b92f03f16afb35877461a1" } }, { @@ -158,11 +158,11 @@ "result_digest" ], "disposition": "accepted", - "rationale": "Canonical result bytes and their digest changed across the RAES identity cutover, the issue #674 target-node CPU architecture field addition, issue #1066 registration of the portable process-resource-limit realization concern, and issue #1067 publication of resolved realization authority; the governed outcome and diagnostic remain unchanged.", + "rationale": "Canonical result bytes and their digest changed across the RAES identity cutover, the issue #674 target-node CPU architecture field addition, issue #1066 registration of the portable process-resource-limit realization concern, issue #1067 publication of resolved realization authority, and issue #1077 separation of OS distribution and release identity; the governed outcome and diagnostic remain unchanged.", "retest": { "actual_outcome": "stable", "diagnostic_kind": null, - "result_digest": "23b9d84fa757bd80436357ed52569b5445b0e4161641598e4b15c3b18cf6e668" + "result_digest": "4bb77034a8f2b1a577700ad03772a80acc0f4515a6831c8a35ac1bf50482d760" } }, { @@ -177,11 +177,11 @@ "result_digest" ], "disposition": "accepted", - "rationale": "Canonical result bytes and their digest changed across the RAES identity cutover, the issue #674 target-node CPU architecture field addition, issue #1066 registration of the portable process-resource-limit realization concern, and issue #1067 publication of resolved realization authority; the governed outcome and diagnostic remain unchanged.", + "rationale": "Canonical result bytes and their digest changed across the RAES identity cutover, the issue #674 target-node CPU architecture field addition, issue #1066 registration of the portable process-resource-limit realization concern, issue #1067 publication of resolved realization authority, and issue #1077 separation of OS distribution and release identity; the governed outcome and diagnostic remain unchanged.", "retest": { "actual_outcome": "distinguishable", "diagnostic_kind": null, - "result_digest": "2e92bdb90a218c29201312052b64b7fb88e8a65e887f05168e2273d9710a5080" + "result_digest": "6cdc44529a87fb9addaf4040795c7f9ae702c5f6ae30e29a5086ee60072ded73" } } ], @@ -251,7 +251,7 @@ "This is a positive control for one objective-reference slice." ], "replayable": true, - "result_digest": "ba0ecbfcb3090ffd6b660cb51324fafcd47ca8dedbbb985e98b6e7f64f8cc25b", + "result_digest": "8291a196859dbb09e9b2ef586ce179f1138139f469b92f03f16afb35877461a1", "source_digest": null }, { @@ -476,7 +476,7 @@ "The witness ends at compiled output." ], "replayable": true, - "result_digest": "23b9d84fa757bd80436357ed52569b5445b0e4161641598e4b15c3b18cf6e668", + "result_digest": "4bb77034a8f2b1a577700ad03772a80acc0f4515a6831c8a35ac1bf50482d760", "source_digest": null }, { @@ -499,7 +499,7 @@ "Distinct digests are a non-vacuity control, not semantic non-equivalence proof." ], "replayable": true, - "result_digest": "2e92bdb90a218c29201312052b64b7fb88e8a65e887f05168e2273d9710a5080", + "result_digest": "6cdc44529a87fb9addaf4040795c7f9ae702c5f6ae30e29a5086ee60072ded73", "source_digest": null }, { diff --git a/docs/research/specification-coverage/analysis-v1.1.json b/docs/research/specification-coverage/analysis-v1.1.json index 44f26148..ec0bd1e0 100644 --- a/docs/research/specification-coverage/analysis-v1.1.json +++ b/docs/research/specification-coverage/analysis-v1.1.json @@ -2,7 +2,7 @@ "analysis_id": "aces-standardized-specification-coverage-analysis-v1.1", "protocol_revision": "1.0.0", "snapshot_id": "aces-standardized-specification-coverage-9347f64-v1", - "snapshot_sha256": "cb01647925b65fb17d88177b9eeb8b553be1b8e0183827cfff501ced2018464c", + "snapshot_sha256": "6a832fb1132f2e374e5f6aa0020c0deaf3baf5d3f74fda786183c3ade7ac9cc4", "generated_at": "2026-07-19", "execution_status": "complete", "classification_counts": { diff --git a/docs/research/specification-coverage/analysis-v1.json b/docs/research/specification-coverage/analysis-v1.json index c86b552d..b3ade983 100644 --- a/docs/research/specification-coverage/analysis-v1.json +++ b/docs/research/specification-coverage/analysis-v1.json @@ -2,7 +2,7 @@ "analysis_id": "aces-standardized-specification-coverage-analysis-v1", "protocol_revision": "1.0.0", "snapshot_id": "aces-standardized-specification-coverage-8bf12ee-v1", - "snapshot_sha256": "1186ec10ef380e3b0e638d215594c1f218139e6235ca1df0601e0d22524592c0", + "snapshot_sha256": "32ade62b2471771019758c99691e13f2b6c3e8a2614aefda86bf9162cc31e1ee", "generated_at": "2026-07-17", "execution_status": "complete", "classification_counts": { diff --git a/docs/research/specification-coverage/bundles/aces-standardized-specification-coverage-8bf12ee-v1.json b/docs/research/specification-coverage/bundles/aces-standardized-specification-coverage-8bf12ee-v1.json index dda2ae35..8a506f26 100644 --- a/docs/research/specification-coverage/bundles/aces-standardized-specification-coverage-8bf12ee-v1.json +++ b/docs/research/specification-coverage/bundles/aces-standardized-specification-coverage-8bf12ee-v1.json @@ -4,7 +4,7 @@ "protocol_path": "docs/research/specification-coverage/protocol-v1.json", "protocol_sha256": "e97a19e643e94c9e589dca823a63c6ce49d3329fe2a3cb888ab630838ed93125", "snapshot_path": "docs/research/specification-coverage/execution-snapshot-v1.json", - "snapshot_sha256": "1cca169b829d841d36f71c49c0dbb8e59a0ea70f83eb809325087aeb06f7e387", + "snapshot_sha256": "da1e41eed08512909328a267607577526787f1956d800ba9fcfff707f2bec62b", "analysis_path": "docs/research/specification-coverage/analysis-v1.json", - "analysis_sha256": "4960c27b6b9bddda77c0c4d830eddb8c181544400d83eeb2293dbea4764bf38a" + "analysis_sha256": "2726197378910018f62a64e63aae09d4799161acf999ddff971c47dd19f61e0b" } diff --git a/docs/research/specification-coverage/bundles/aces-standardized-specification-coverage-9347f64-v1.json b/docs/research/specification-coverage/bundles/aces-standardized-specification-coverage-9347f64-v1.json index 6461dd02..b8286777 100644 --- a/docs/research/specification-coverage/bundles/aces-standardized-specification-coverage-9347f64-v1.json +++ b/docs/research/specification-coverage/bundles/aces-standardized-specification-coverage-9347f64-v1.json @@ -4,7 +4,7 @@ "protocol_path": "docs/research/specification-coverage/protocol-v1.json", "protocol_sha256": "e97a19e643e94c9e589dca823a63c6ce49d3329fe2a3cb888ab630838ed93125", "snapshot_path": "docs/research/specification-coverage/execution-snapshot-v1.1.json", - "snapshot_sha256": "281e69e3b7dee7ad1fffa4dd1b20e7ad9d6a5a222a802d5c327af03ceac3a1bc", + "snapshot_sha256": "a05cc0d9a0ee221769d2dee1bc480819ab109476e67289d24254c2483cbad659", "analysis_path": "docs/research/specification-coverage/analysis-v1.1.json", - "analysis_sha256": "e3c106de9c2f7fb70113a8400b48b13a4be9c6073bfdae585525ae3610962075" + "analysis_sha256": "4d2f8580b5be899430190d9acedb0ac1e74d4eb0a7c306aba8f00f2162914744" } diff --git a/docs/research/specification-coverage/execution-snapshot-v1.1.json b/docs/research/specification-coverage/execution-snapshot-v1.1.json index ff9a9956..bef9c37f 100644 --- a/docs/research/specification-coverage/execution-snapshot-v1.1.json +++ b/docs/research/specification-coverage/execution-snapshot-v1.1.json @@ -35,7 +35,7 @@ "artifact_id": "port-range-sdl", "kind": "sdl", "path": "examples/scenarios/port-authority-surge-response.sdl.yaml", - "sha256": "c7f9374d87490145425e9ee3916d799ffac1b6a30fb97f50f7241f7ff9b6f21a", + "sha256": "a27c7a64e0c5c618fadaccafdf1a4e71600170a8b77b983190822b5141f00dec", "validator": "aces_sdl parse, semantic, instantiation/admission, and compiler pipeline" }, { diff --git a/docs/research/specification-coverage/execution-snapshot-v1.json b/docs/research/specification-coverage/execution-snapshot-v1.json index a6c55c9a..34bf6bc1 100644 --- a/docs/research/specification-coverage/execution-snapshot-v1.json +++ b/docs/research/specification-coverage/execution-snapshot-v1.json @@ -35,7 +35,7 @@ "artifact_id": "port-range-sdl", "kind": "sdl", "path": "examples/scenarios/port-authority-surge-response.sdl.yaml", - "sha256": "c7f9374d87490145425e9ee3916d799ffac1b6a30fb97f50f7241f7ff9b6f21a", + "sha256": "a27c7a64e0c5c618fadaccafdf1a4e71600170a8b77b983190822b5141f00dec", "validator": "aces_sdl parse, semantic, instantiation/admission, and compiler pipeline" }, { diff --git a/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml b/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml index 717bc844..5b686364 100644 --- a/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml +++ b/examples/scenarios/hospital-ransomware-surgery-day.sdl.yaml @@ -81,7 +81,8 @@ nodes: ram: 4294967296 cpu: 2 os: windows - os_version: Server 2022 + os_distribution: windows-server + os_version: '2022' features: hospital-ad: domain-admin hospital-adfs: federation-admin @@ -211,7 +212,8 @@ nodes: ram: 4294967296 cpu: 2 os: windows - os_version: Server 2019 + os_distribution: windows-server + os_version: '2019' features: exchange-mailbox: exchange-admin conditions: @@ -387,6 +389,7 @@ nodes: ram: 2147483648 cpu: 2 os: windows + os_distribution: windows-client os_version: '11' conditions: vendor-jump-online: support diff --git a/examples/scenarios/port-authority-surge-response.sdl.yaml b/examples/scenarios/port-authority-surge-response.sdl.yaml index 98e6f6f3..39d67a30 100644 --- a/examples/scenarios/port-authority-surge-response.sdl.yaml +++ b/examples/scenarios/port-authority-surge-response.sdl.yaml @@ -147,6 +147,7 @@ nodes: ram: 4294967296 cpu: 2 os: windows + os_distribution: windows-client os_version: '10' features: yard-hmi-app: hmi-admin @@ -194,6 +195,7 @@ nodes: ram: 2147483648 cpu: 1 os: windows + os_distribution: windows-client os_version: '11' conditions: gate-processing-healthy: kiosk-admin diff --git a/implementations/python/packages/raes/_capability_constraints.py b/implementations/python/packages/raes/_capability_constraints.py index 1351915f..434ecf34 100644 --- a/implementations/python/packages/raes/_capability_constraints.py +++ b/implementations/python/packages/raes/_capability_constraints.py @@ -84,6 +84,16 @@ def _node_capability_constraints( value=node.os, variables=variables, ), + _finite_domain_constraint( + field_pointer=f"{node_pointer}/os_distribution", + value=node.os_distribution, + variables=variables, + ), + _finite_domain_constraint( + field_pointer=f"{node_pointer}/os_version", + value=node.os_version, + variables=variables, + ), _finite_domain_constraint( field_pointer=f"{node_pointer}/architecture", value=node.architecture, diff --git a/implementations/python/packages/raes/nodes.py b/implementations/python/packages/raes/nodes.py index 513afff6..0e7f8389 100644 --- a/implementations/python/packages/raes/nodes.py +++ b/implementations/python/packages/raes/nodes.py @@ -7,6 +7,8 @@ from enum import Enum from pydantic import Field, field_validator, model_validator +from pydantic.json_schema import GetJsonSchemaHandler, JsonSchemaValue +from pydantic_core import CoreSchema from ._base import ( SDLModel, @@ -39,6 +41,13 @@ ImageSourceInput, ImageVerificationStatus, ) +from .operating_systems import ( + AuthoredOSDistributionString, + AuthoredOSVersionString, + OSDistribution, + normalize_os_distribution, + normalize_os_version, +) from .runtime_configuration import ( RuntimeCapabilityOverrideScope, RuntimeCapabilityPolicy, @@ -291,7 +300,8 @@ def normalize_type(cls, v: str) -> str: resources: Resources | None = None os: OSFamily | str | None = None - os_version: str = "" + os_distribution: OSDistribution | AuthoredOSDistributionString | None = None + os_version: AuthoredOSVersionString = "" architecture: NodeArchitecture | AuthoredNodeArchitectureString | None = None features: dict[str, str] = Field(default_factory=dict) conditions: dict[str, str] = Field(default_factory=dict) @@ -303,11 +313,56 @@ def normalize_type(cls, v: str) -> str: endpoint_persona: EndpointPersona | str | None = None runtime: RuntimeConfiguration | None = None + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + """Publish the same OS dependency chain enforced by semantic validation.""" + + json_schema = handler.resolve_ref_schema(handler(core_schema)) + json_schema.setdefault("allOf", []).extend( + [ + { + "if": { + "required": ["os_version"], + "properties": {"os_version": {"type": "string", "minLength": 1}}, + }, + "then": { + "required": ["os_distribution"], + "properties": {"os_distribution": {"not": {"type": "null"}}}, + }, + }, + { + "if": { + "required": ["os_distribution"], + "properties": {"os_distribution": {"not": {"type": "null"}}}, + }, + "then": { + "required": ["os"], + "properties": {"os": {"not": {"type": "null"}}}, + }, + }, + ] + ) + return json_schema + @field_validator("os", mode="before") @classmethod def normalize_os(cls, v): return parse_enum_or_var(v, OSFamily, field_name="os") if v is not None else v + @field_validator("os_distribution", mode="before") + @classmethod + def normalize_os_distribution_value(cls, v: object) -> object: + return normalize_os_distribution(v) if v is not None else v + + @field_validator("os_version", mode="before") + @classmethod + def normalize_os_version_value(cls, v: object) -> object: + return normalize_os_version(v) + @field_validator("architecture", mode="before") @classmethod def normalize_architecture_value(cls, v: object) -> object: @@ -334,6 +389,7 @@ def _populated_compute_only_fields(self) -> list[str]: "source": self.source is not None, "resources": self.resources is not None, "os": self.os is not None, + "os_distribution": self.os_distribution is not None, "os_version": bool(self.os_version), "architecture": self.architecture is not None, "features": bool(self.features), diff --git a/implementations/python/packages/raes/operating_systems.py b/implementations/python/packages/raes/operating_systems.py new file mode 100644 index 00000000..866e1e88 --- /dev/null +++ b/implementations/python/packages/raes/operating_systems.py @@ -0,0 +1,90 @@ +"""Governed portable operating-system identity vocabulary (issue #1077).""" + +from __future__ import annotations + +import re +from enum import Enum +from typing import Annotated + +from pydantic import WithJsonSchema +from raes_contracts.operating_systems import OS_VERSION_PATTERN, OS_VERSION_RE + +from ._base import VARIABLE_TOKEN_PATTERN, is_variable_ref + + +class OSDistribution(str, Enum): + """Canonical portable distribution or product-line identifiers.""" + + UBUNTU = "ubuntu" + DEBIAN = "debian" + ROCKY_LINUX = "rocky-linux" + RED_HAT_ENTERPRISE_LINUX = "red-hat-enterprise-linux" + WINDOWS_SERVER = "windows-server" + WINDOWS_CLIENT = "windows-client" + SOLARIS = "solaris" + + +OS_DISTRIBUTION_EXTENSION_PATTERN = r"^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$" +_OS_DISTRIBUTION_EXTENSION_RE = re.compile(OS_DISTRIBUTION_EXTENSION_PATTERN) +_OS_DISTRIBUTION_EXTENSION_BODY = r"x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*" + +AuthoredOSDistributionString = Annotated[ + str, + WithJsonSchema( + { + "type": "string", + "pattern": f"^(?:{_OS_DISTRIBUTION_EXTENSION_BODY}|{VARIABLE_TOKEN_PATTERN})$", + } + ), +] + +AuthoredOSVersionString = Annotated[ + str, + WithJsonSchema( + { + "type": "string", + "pattern": f"^(?:{OS_VERSION_PATTERN[1:-1]}|{VARIABLE_TOKEN_PATTERN})?$", + } + ), +] + + +def normalize_os_distribution(value: object) -> object: + """Normalize one authored distribution token and reject ungoverned values.""" + + if isinstance(value, OSDistribution) or is_variable_ref(value): + return value + if not isinstance(value, str): + raise ValueError("OS distribution must be a string") + lowered = value.lower() + if _OS_DISTRIBUTION_EXTENSION_RE.fullmatch(lowered): + return lowered + try: + return OSDistribution(lowered) + except ValueError as exc: + raise ValueError( + "OS distribution must be a governed portable term, a governed " + "x-: extension, or a ${var} placeholder" + ) from exc + + +def normalize_os_version(value: object) -> object: + """Validate a bounded release-only opaque token; empty means not authored.""" + + if is_variable_ref(value): + return value + if value == "": + return "" + if not isinstance(value, str) or OS_VERSION_RE.fullmatch(value) is None: + raise ValueError("OS version must be a bounded printable release token") + return value + + +__all__ = [ + "AuthoredOSDistributionString", + "AuthoredOSVersionString", + "OSDistribution", + "OS_DISTRIBUTION_EXTENSION_PATTERN", + "normalize_os_distribution", + "normalize_os_version", +] diff --git a/implementations/python/packages/raes/phase_contracts.py b/implementations/python/packages/raes/phase_contracts.py index d2570dae..2f6cb7a3 100644 --- a/implementations/python/packages/raes/phase_contracts.py +++ b/implementations/python/packages/raes/phase_contracts.py @@ -171,6 +171,8 @@ def _validate_constraint(self) -> CapabilityConstraint: parts = self.field_pointer.split("/") ordinary_pointer = len(parts) == 4 and (parts[1], parts[3]) in { ("nodes", "os"), + ("nodes", "os_distribution"), + ("nodes", "os_version"), ("nodes", "architecture"), ("infrastructure", "count"), } @@ -183,7 +185,7 @@ def _validate_constraint(self) -> CapabilityConstraint: ) if not ordinary_pointer and not process_limit_pointer: raise ValueError( - "field_pointer must address /nodes//os, /nodes//architecture, " + "field_pointer must address /nodes//(os|os_distribution|os_version|architecture), " "/infrastructure//count, or " "/nodes//runtime/operational_policy/resource_limits/process_limits//(soft|hard)" ) diff --git a/implementations/python/packages/raes/validator/_nodes_infra_network.py b/implementations/python/packages/raes/validator/_nodes_infra_network.py index cc1dbb18..6735fce0 100644 --- a/implementations/python/packages/raes/validator/_nodes_infra_network.py +++ b/implementations/python/packages/raes/validator/_nodes_infra_network.py @@ -27,8 +27,19 @@ def _verify_nodes(self) -> None: continue if vuln_name not in self._s.vulnerabilities: self._err(f"Node '{name}' references undefined vulnerability '{vuln_name}'") + self._verify_node_operating_system(name, node) self._verify_node_architecture(name, node) + def _verify_node_operating_system(self, name: str, node: object) -> None: + """Enforce the family -> distribution -> release dependency chain.""" + + distribution = node.os_distribution + version = node.os_version + if distribution is not None and node.os is None: + self._err(f"Node '{name}' OS distribution requires an OS family") + if version and distribution is None: + self._err(f"Node '{name}' OS version requires an OS distribution") + def _verify_node_architecture(self, name: str, node: object) -> None: """Enforce target-node/runtime-package CPU architecture compatibility. diff --git a/implementations/python/packages/raes_backend_libvirt/_payload.py b/implementations/python/packages/raes_backend_libvirt/_payload.py index 6ce26129..95e34fbc 100644 --- a/implementations/python/packages/raes_backend_libvirt/_payload.py +++ b/implementations/python/packages/raes_backend_libvirt/_payload.py @@ -45,6 +45,24 @@ def _os_family(payload: Mapping[str, object]) -> str: return node_os if isinstance(node_os, str) else "" +def _os_distribution(payload: Mapping[str, object]) -> str: + distribution = payload.get("os_distribution") + if isinstance(distribution, str) and distribution: + return distribution + node = _spec(payload).get("node") + nested = node.get("os_distribution") if isinstance(node, Mapping) else None + return nested if isinstance(nested, str) else "" + + +def _os_version(payload: Mapping[str, object]) -> str: + version = payload.get("os_version") + if isinstance(version, str) and version: + return version + node = _spec(payload).get("node") + nested = node.get("os_version") if isinstance(node, Mapping) else None + return nested if isinstance(nested, str) else "" + + def _architecture(payload: Mapping[str, object]) -> str: architecture = payload.get("architecture") if isinstance(architecture, str) and architecture: diff --git a/implementations/python/packages/raes_backend_libvirt/capability_envelope.py b/implementations/python/packages/raes_backend_libvirt/capability_envelope.py index 23c43ad3..89c81229 100644 --- a/implementations/python/packages/raes_backend_libvirt/capability_envelope.py +++ b/implementations/python/packages/raes_backend_libvirt/capability_envelope.py @@ -28,7 +28,9 @@ NODE_RESOURCE_TYPE, _architecture, _node_kind, + _os_distribution, _os_family, + _os_version, _spec, _str, ) @@ -41,6 +43,7 @@ _CODE_UNSUPPORTED_NODE_TYPE = "libvirt-backend.realization.unsupported-node-type" _CODE_UNSUPPORTED_OS_FAMILY = "libvirt-backend.realization.unsupported-os-family" +_CODE_UNSUPPORTED_OPERATING_SYSTEM = "libvirt-backend.realization.unsupported-operating-system" _CODE_UNSUPPORTED_NODE_ARCHITECTURE = "libvirt-backend.realization.unsupported-node-architecture" _CODE_UNSUPPORTED_CONTENT_TYPE = "libvirt-backend.realization.unsupported-content-type" _CODE_UNSUPPORTED_SERVICE_MATERIALIZATION_PROFILE = ( @@ -182,6 +185,26 @@ def _out_of_envelope_terms( for term in dimension.extract(payload): if term and term not in supported: yield (dimension.code, address, term), _envelope_diagnostic(dimension, address, term) + if resource_type == NODE_RESOURCE_TYPE: + family = _os_family(payload) + distribution = _os_distribution(payload) + version = _os_version(payload) or None + if distribution and not capabilities.supports_operating_system( + family=family, + distribution=distribution, + version=version, + ): + identity = "/".join((family, distribution, version or "")) + yield ( + (_CODE_UNSUPPORTED_OPERATING_SYSTEM, address, identity), + Diagnostic( + code=_CODE_UNSUPPORTED_OPERATING_SYSTEM, + domain=_DOMAIN, + address=address, + message=f"Libvirt backend does not realize operating-system identity '{identity}'.", + severity=Severity.ERROR, + ), + ) def _materialized_payloads(plan: ProvisioningPlan) -> Iterator[tuple[str, str, Mapping[str, object]]]: diff --git a/implementations/python/packages/raes_backend_libvirt/manifest.py b/implementations/python/packages/raes_backend_libvirt/manifest.py index b82474c0..87d275ba 100644 --- a/implementations/python/packages/raes_backend_libvirt/manifest.py +++ b/implementations/python/packages/raes_backend_libvirt/manifest.py @@ -9,6 +9,7 @@ PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, BackendCapabilitySet, BackendManifest, + OperatingSystemCompatibility, ParticipantFeatureSupport, ParticipantRuntimeCapabilities, ProvisionerCapabilities, @@ -57,6 +58,14 @@ def _provisioner_capabilities(mode: LibvirtDriverMode) -> ProvisionerCapabilitie ), supported_node_types=frozenset(configuration.supported_node_types), supported_os_families=frozenset(configuration.supported_os_families), + operating_systems=tuple( + OperatingSystemCompatibility( + family=entry.family, + distribution=entry.distribution, + versions=frozenset(entry.versions), + ) + for entry in configuration.operating_systems + ), supported_node_architectures=frozenset({realized_architecture}), supported_content_types=frozenset(configuration.supported_content_types), supported_account_features=account_features, diff --git a/implementations/python/packages/raes_backend_libvirt/target.py b/implementations/python/packages/raes_backend_libvirt/target.py index 8ea766fb..43f6a9d8 100644 --- a/implementations/python/packages/raes_backend_libvirt/target.py +++ b/implementations/python/packages/raes_backend_libvirt/target.py @@ -4,7 +4,7 @@ from typing import Any -from raes_backend_protocols.capabilities import BackendManifest +from raes_backend_protocols.capabilities import BackendManifest, OperatingSystemCompatibility from raes_runtime.registry import BackendRegistry, RuntimeTarget, RuntimeTargetComponents from .driver import LibvirtDriver @@ -117,6 +117,14 @@ def _validate_manifest_mode(manifest: BackendManifest, mode: LibvirtDriverMode) expected = { "supported_node_types": frozenset(configuration.supported_node_types), "supported_os_families": frozenset(configuration.supported_os_families), + "operating_systems": tuple( + OperatingSystemCompatibility( + family=entry.family, + distribution=entry.distribution, + versions=frozenset(entry.versions), + ) + for entry in configuration.operating_systems + ), "supported_node_architectures": frozenset({realized_target_architecture(configuration)}), "supported_content_types": frozenset(configuration.supported_content_types), "supported_account_features": frozenset(configuration.supported_account_features), diff --git a/implementations/python/packages/raes_backend_protocols/capabilities.py b/implementations/python/packages/raes_backend_protocols/capabilities.py index 2fec5d49..d61008ca 100644 --- a/implementations/python/packages/raes_backend_protocols/capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/capabilities.py @@ -24,6 +24,7 @@ ParticipantExecutionBinding = _participant_capabilities.ParticipantExecutionBinding ParticipantRuntimeCapabilities = _participant_capabilities.ParticipantRuntimeCapabilities PROVISIONER_DOMAIN_PROFILE_SCOPE = _provisioner_capabilities.PROVISIONER_DOMAIN_PROFILE_SCOPE +OperatingSystemCompatibility = _provisioner_capabilities.OperatingSystemCompatibility ProvisionerCapabilities = _provisioner_capabilities.ProvisionerCapabilities TIME_CAPABILITY_REQUIRED_CONTRACTS = _time_capabilities.TIME_CAPABILITY_REQUIRED_CONTRACTS TimeCapabilities = _time_capabilities.TimeCapabilities diff --git a/implementations/python/packages/raes_backend_protocols/provisioner_capabilities.py b/implementations/python/packages/raes_backend_protocols/provisioner_capabilities.py index 9fe111e4..c6e7e180 100644 --- a/implementations/python/packages/raes_backend_protocols/provisioner_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/provisioner_capabilities.py @@ -2,7 +2,11 @@ from dataclasses import dataclass, field -from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_scope_values +from raes_contracts.controlled_vocabularies import ( + validate_controlled_vocabulary_scope_values, + validate_controlled_vocabulary_value, +) +from raes_contracts.operating_systems import OS_VERSION_RE, validate_operating_system_pair from raes_contracts.vocabulary import GeneratedArtifactKind PROVISIONER_DOMAIN_PROFILE_SCOPE = "capabilities.provisioner.supported_domain_profiles" @@ -11,6 +15,29 @@ ) +@dataclass(frozen=True) +class OperatingSystemCompatibility: + """One inseparable OS family, distribution, and bounded release domain.""" + + family: str + distribution: str + versions: frozenset[str] + + def __post_init__(self) -> None: + validate_controlled_vocabulary_scope_values( + "capabilities.provisioner.supported_os_families", + (self.family,), + ) + validate_controlled_vocabulary_value("os-distributions", self.distribution) + validate_operating_system_pair(self.family, self.distribution) + if not self.versions: + raise ValueError("OperatingSystemCompatibility.versions must not be empty") + if any(OS_VERSION_RE.fullmatch(version) is None for version in self.versions): + raise ValueError( + "OperatingSystemCompatibility.versions must contain bounded non-empty printable release tokens" + ) + + def _require_string_values(name: str, values: frozenset[str], *, required: bool = False) -> None: if required and not values: raise ValueError(f"ProvisionerCapabilities.{name} must not be empty") @@ -30,6 +57,7 @@ class ProvisionerCapabilities: name: str supported_node_types: frozenset[str] = frozenset() supported_os_families: frozenset[str] = frozenset() + operating_systems: tuple[OperatingSystemCompatibility, ...] = () supported_node_architectures: frozenset[str] = frozenset() supported_content_types: frozenset[str] = frozenset() supported_account_features: frozenset[str] = frozenset() @@ -64,6 +92,19 @@ def __post_init__(self) -> None: "capabilities.provisioner.supported_os_families", self.supported_os_families, ) + os_keys = [(entry.family, entry.distribution) for entry in self.operating_systems] + if len(os_keys) != len(set(os_keys)): + raise ValueError( + "ProvisionerCapabilities.operating_systems must not contain duplicate family/distribution rows" + ) + undeclared_families = { + entry.family for entry in self.operating_systems if entry.family not in self.supported_os_families + } + if undeclared_families: + raise ValueError( + "ProvisionerCapabilities.operating_systems families must be present in supported_os_families: " + + ", ".join(sorted(undeclared_families)) + ) validate_controlled_vocabulary_scope_values( "capabilities.provisioner.supported_node_architectures", self.supported_node_architectures, @@ -104,9 +145,30 @@ def __post_init__(self) -> None: "ProvisionerCapabilities supported_generated_artifact_kinds require supports_generated_artifacts=True" ) + def supports_operating_system( + self, + *, + family: str, + distribution: str | None = None, + version: str | None = None, + ) -> bool: + """Return whether one coupled capability row admits the requested identity.""" + + if family not in self.supported_os_families: + return False + if distribution is None: + return version is None + return any( + entry.family == family + and entry.distribution == distribution + and (version is None or version in entry.versions) + for entry in self.operating_systems + ) + __all__ = [ "PROVISIONER_DOMAIN_PROFILE_SCOPE", "PROVISIONER_SERVICE_MATERIALIZATION_PROFILE_SCOPE", + "OperatingSystemCompatibility", "ProvisionerCapabilities", ] diff --git a/implementations/python/packages/raes_backend_protocols/provisioner_manifest.py b/implementations/python/packages/raes_backend_protocols/provisioner_manifest.py index 4c8fec61..aef232ac 100644 --- a/implementations/python/packages/raes_backend_protocols/provisioner_manifest.py +++ b/implementations/python/packages/raes_backend_protocols/provisioner_manifest.py @@ -6,7 +6,7 @@ from raes_contracts.contracts import ProvisionerCapabilitiesModel -from .provisioner_capabilities import ProvisionerCapabilities +from .provisioner_capabilities import OperatingSystemCompatibility, ProvisionerCapabilities def provisioner_capability_payload(provisioner: ProvisionerCapabilities) -> dict[str, Any]: @@ -16,6 +16,17 @@ def provisioner_capability_payload(provisioner: ProvisionerCapabilities) -> dict "name": provisioner.name, "supported_node_types": sorted(provisioner.supported_node_types), "supported_os_families": sorted(provisioner.supported_os_families), + "operating_systems": [ + { + "family": entry.family, + "distribution": entry.distribution, + "versions": sorted(entry.versions), + } + for entry in sorted( + provisioner.operating_systems, + key=lambda item: (item.family, item.distribution), + ) + ], "supported_node_architectures": sorted(provisioner.supported_node_architectures), "supported_content_types": sorted(provisioner.supported_content_types), "supported_account_features": sorted(provisioner.supported_account_features), @@ -40,6 +51,14 @@ def provisioner_from_model(model: ProvisionerCapabilitiesModel) -> ProvisionerCa name=model.name, supported_node_types=frozenset(model.supported_node_types), supported_os_families=frozenset(model.supported_os_families), + operating_systems=tuple( + OperatingSystemCompatibility( + family=entry.family, + distribution=entry.distribution, + versions=frozenset(entry.versions), + ) + for entry in model.operating_systems + ), supported_node_architectures=frozenset(model.supported_node_architectures), supported_content_types=frozenset(model.supported_content_types), supported_account_features=frozenset(model.supported_account_features), diff --git a/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py b/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py index d7ef8d74..5afb7c8a 100644 --- a/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py +++ b/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py @@ -21,6 +21,7 @@ ) from raes_contracts.participant_shared_state import iter_participant_shared_state_snapshot_violations from raes_contracts.planning import RuntimeDomain +from raes_contracts.realization_observation import ObservedOperatingSystemIdentity from raes_contracts.runtime_state import RealizationObservationDisclosure, RuntimeSnapshot, SnapshotEntry from raes_processor.models import ( ParticipantActionContractRuntime, @@ -127,6 +128,15 @@ def _snapshot_from_envelope(payload: dict[str, Any]) -> RuntimeSnapshot: verification_scope=entry.verification_scope, observation_strength=entry.observation_strength, observed_value=entry.observed_value, + operating_system=( + ObservedOperatingSystemIdentity( + family=entry.operating_system.family, + distribution=entry.operating_system.distribution, + version=entry.operating_system.version, + ) + if entry.operating_system is not None + else None + ), operation_id=entry.operation_id, envelope_digest=entry.envelope_digest, configuration_digest=entry.configuration_digest, diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index 22791ffc..99218237 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -93,6 +93,7 @@ ApparatusIdentityModel, BackendCompatibilityModel, EvaluatorCapabilitiesModel, + OperatingSystemCompatibilityModel, OrchestratorCapabilitiesModel, ProcessorCompatibilityModel, ProcessResourceLimitCapabilityModel, @@ -383,6 +384,7 @@ ) from .realization_plans import ( EvaluationPlanModel, + ObservedOperatingSystemIdentityModel, OperationReceiptModel, OperationStatusModel, OrchestrationPlanModel, diff --git a/implementations/python/packages/raes_contracts/contracts/_candidate_synthesis_exports.py b/implementations/python/packages/raes_contracts/contracts/_candidate_synthesis_exports.py new file mode 100644 index 00000000..218e61a7 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/_candidate_synthesis_exports.py @@ -0,0 +1,28 @@ +"""Public contract exports for source-neutral candidate synthesis.""" + +CANDIDATE_SYNTHESIS_EXPORTS = [ + "SDL_CANDIDATE_SYNTHESIS_INPUT_SCHEMA_VERSION", + "SDL_CANDIDATE_SYNTHESIS_PROFILE_SCHEMA_VERSION", + "SDL_CANDIDATE_SYNTHESIS_RECORD_SCHEMA_VERSION", + "CandidateSynthesisAssumptionModel", + "CandidateSynthesisChoiceModel", + "CandidateSynthesisConstructTraceModel", + "CandidateSynthesisContributionModel", + "CandidateSynthesisDecisionModel", + "CandidateSynthesisDisposition", + "CandidateSynthesisInputModel", + "CandidateSynthesisProfileCoordinateModel", + "CandidateSynthesisProfileDefinitionModel", + "CandidateSynthesisReason", + "CandidateSynthesisRecordModel", + "CandidateSynthesisSourceModel", + "CandidateSynthesisTargetModel", + "ConceptSourceAssertionModel", + "ExampleSourceAssertionModel", + "OrderingSourceAssertionModel", + "ParameterizationSourceAssertionModel", + "PreconditionSourceAssertionModel", + "RelationshipSourceAssertionModel", + "SourceAssertion", + "SynthesisContributionKind", +] diff --git a/implementations/python/packages/raes_contracts/contracts/_exports.py b/implementations/python/packages/raes_contracts/contracts/_exports.py index 4883ba39..b2909800 100644 --- a/implementations/python/packages/raes_contracts/contracts/_exports.py +++ b/implementations/python/packages/raes_contracts/contracts/_exports.py @@ -1,5 +1,7 @@ """Canonical public export manifest for the contracts facade.""" +from ._candidate_synthesis_exports import CANDIDATE_SYNTHESIS_EXPORTS + PUBLIC_EXPORTS = [ "ACTIVITYSTREAMS_ACTIVITY_TYPES_SOURCE_SCHEMA_VERSION", "ActivityStreamsActivityTypeSourceTermModel", @@ -27,30 +29,7 @@ "ArtifactTransformationStatus", "PreservationOutcome", "TransformationCheckOutcome", - "SDL_CANDIDATE_SYNTHESIS_INPUT_SCHEMA_VERSION", - "SDL_CANDIDATE_SYNTHESIS_PROFILE_SCHEMA_VERSION", - "SDL_CANDIDATE_SYNTHESIS_RECORD_SCHEMA_VERSION", - "CandidateSynthesisAssumptionModel", - "CandidateSynthesisChoiceModel", - "CandidateSynthesisConstructTraceModel", - "CandidateSynthesisContributionModel", - "CandidateSynthesisDecisionModel", - "CandidateSynthesisDisposition", - "CandidateSynthesisInputModel", - "CandidateSynthesisProfileCoordinateModel", - "CandidateSynthesisProfileDefinitionModel", - "CandidateSynthesisReason", - "CandidateSynthesisRecordModel", - "CandidateSynthesisSourceModel", - "CandidateSynthesisTargetModel", - "ConceptSourceAssertionModel", - "ExampleSourceAssertionModel", - "OrderingSourceAssertionModel", - "ParameterizationSourceAssertionModel", - "PreconditionSourceAssertionModel", - "RelationshipSourceAssertionModel", - "SourceAssertion", - "SynthesisContributionKind", + *CANDIDATE_SYNTHESIS_EXPORTS, "ArtifactAcquisitionTimingModel", "ArtifactAvailabilityContext", "ArtifactMechanismCapability", @@ -245,6 +224,7 @@ "OPERATION_SCHEMA_VERSION", "OperationReceiptModel", "OperationStatusModel", + "ObservedOperatingSystemIdentityModel", "ObservationCapabilitiesModel", "OrchestrationPlanModel", "OrchestratorCapabilitiesModel", @@ -371,6 +351,7 @@ "ProcessorManifestV2Model", "ProcessorCompatibilityModel", "ProcessorCapabilitiesV2Model", + "OperatingSystemCompatibilityModel", "ProvisionerCapabilitiesModel", "ProvisioningPlanModel", "PlannedRealizationConstraintModel", diff --git a/implementations/python/packages/raes_contracts/contracts/base.py b/implementations/python/packages/raes_contracts/contracts/base.py index c637da7d..ea16d085 100644 --- a/implementations/python/packages/raes_contracts/contracts/base.py +++ b/implementations/python/packages/raes_contracts/contracts/base.py @@ -214,6 +214,7 @@ def _validate_claim_strength(self) -> BehavioralClaimBindingModel: { "capabilities.provisioner.supported_node_types", "capabilities.provisioner.supported_os_families", + "capabilities.provisioner.operating_systems.distribution", "capabilities.provisioner.supported_node_architectures", "capabilities.provisioner.supported_content_types", "capabilities.provisioner.supported_account_features", @@ -299,6 +300,8 @@ def _validate_claim_strength(self) -> BehavioralClaimBindingModel: "external_concept_bindings.bindings.*.review.status", "external_concept_bindings.bindings.*.perspective.participant_availability.kind", "scenario.realization.constraints.compute-substrate", + "nodes.os_distribution", + "runtime.snapshot.realization_observations.operating-system.distribution", "runtime.snapshot.realization_observations.compute-substrate", *_BACKEND_CONCEPT_BINDING_SCOPES, *_PARTICIPANT_IMPLEMENTATION_CONCEPT_BINDING_SCOPES, diff --git a/implementations/python/packages/raes_contracts/contracts/capabilities.py b/implementations/python/packages/raes_contracts/contracts/capabilities.py index 6ae2bf96..0c4c7be3 100644 --- a/implementations/python/packages/raes_contracts/contracts/capabilities.py +++ b/implementations/python/packages/raes_contracts/contracts/capabilities.py @@ -2,11 +2,14 @@ from __future__ import annotations +from typing import Annotated + from pydantic import Field, GetJsonSchemaHandler, model_validator from pydantic.json_schema import JsonSchemaValue from pydantic_core import CoreSchema from ..artifact_requirements import ArtifactMechanismCapability +from ..operating_systems import OS_VERSION_PATTERN, validate_operating_system_pair from ..vocabulary import ( GeneratedArtifactKind, ObservationStrength, @@ -20,11 +23,47 @@ from .base import ContractModel, NonEmptyString from .validators import _validate_controlled_vocabulary_terms +OSReleaseString = Annotated[str, Field(min_length=1, max_length=128, pattern=OS_VERSION_PATTERN)] + + +class OperatingSystemCompatibilityModel(ContractModel): + """One portable, coupled OS family/distribution/release capability row.""" + + family: NonEmptyString + distribution: NonEmptyString + versions: list[OSReleaseString] = Field(min_length=1, max_length=128) + + @model_validator(mode="after") + def _validate_terms(self) -> OperatingSystemCompatibilityModel: + _validate_controlled_vocabulary_terms( + "capabilities.provisioner.supported_os_families", + [self.family], + ) + _validate_controlled_vocabulary_terms( + "capabilities.provisioner.operating_systems.distribution", + [self.distribution], + ) + validate_operating_system_pair(self.family, self.distribution) + if len(self.versions) != len(set(self.versions)): + raise ValueError("versions must not contain duplicates") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler.resolve_ref_schema(handler(core_schema)) + json_schema["properties"]["versions"]["uniqueItems"] = True + return json_schema + class ProvisionerCapabilitiesModel(ContractModel): name: NonEmptyString supported_node_types: list[NonEmptyString] = Field(min_length=1) supported_os_families: list[NonEmptyString] = Field(min_length=1) + operating_systems: list[OperatingSystemCompatibilityModel] = Field(default_factory=list) supported_node_architectures: list[NonEmptyString] = Field(default_factory=list) supported_content_types: list[NonEmptyString] = Field(default_factory=list) supported_account_features: list[NonEmptyString] = Field(default_factory=list) @@ -51,6 +90,17 @@ def _validate_account_support(self) -> ProvisionerCapabilitiesModel: "capabilities.provisioner.supported_os_families", self.supported_os_families, ) + os_keys = [(entry.family, entry.distribution) for entry in self.operating_systems] + if len(os_keys) != len(set(os_keys)): + raise ValueError("operating_systems must not contain duplicate family/distribution rows") + undeclared_families = { + entry.family for entry in self.operating_systems if entry.family not in self.supported_os_families + } + if undeclared_families: + raise ValueError( + "operating_systems families must be present in supported_os_families: " + + ", ".join(sorted(undeclared_families)) + ) _validate_controlled_vocabulary_terms( "capabilities.provisioner.supported_node_architectures", self.supported_node_architectures, @@ -93,6 +143,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) + json_schema["properties"]["operating_systems"]["uniqueItems"] = True json_schema.setdefault("allOf", []).extend( [ { diff --git a/implementations/python/packages/raes_contracts/contracts/operating_systems.py b/implementations/python/packages/raes_contracts/contracts/operating_systems.py new file mode 100644 index 00000000..96566e58 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/operating_systems.py @@ -0,0 +1,25 @@ +"""Public operating-system identity contract models.""" + +from __future__ import annotations + +from pydantic import Field, model_validator + +from ..operating_systems import validate_operating_system_pair +from .base import ContractModel, NonEmptyString + + +class ObservedOperatingSystemIdentityModel(ContractModel): + """Typed guest-observed OS identity carried by a bound disclosure.""" + + family: NonEmptyString + distribution: NonEmptyString + version: NonEmptyString = Field(max_length=128, pattern=r"^[!-~](?:[ -~]{0,126}[!-~])?$") + + @model_validator(mode="after") + def _validate_vocabulary(self) -> ObservedOperatingSystemIdentityModel: + from ..controlled_vocabularies import validate_controlled_vocabulary_value + + validate_controlled_vocabulary_value("provisioner-os-families", self.family) + validate_controlled_vocabulary_value("os-distributions", self.distribution) + validate_operating_system_pair(self.family, self.distribution) + return self diff --git a/implementations/python/packages/raes_contracts/contracts/realization_observation_validation.py b/implementations/python/packages/raes_contracts/contracts/realization_observation_validation.py new file mode 100644 index 00000000..f01e58bd --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/realization_observation_validation.py @@ -0,0 +1,64 @@ +"""Semantic validation for published realization-observation disclosures.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from raes_contracts.vocabulary import ObservationStrength + +if TYPE_CHECKING: + from raes_contracts.realization_observation import RealizationObservationDisclosure + + +def validate_realization_observation_disclosure(disclosure: RealizationObservationDisclosure) -> None: + if disclosure.observation_strength is ObservationStrength.NONE: + raise ValueError("realization observation disclosure must provide non-none evidence") + if disclosure.requirement_kind == "compute-substrate": + _require_compute_substrate_evidence(disclosure) + elif disclosure.requirement_kind == "operating-system": + _require_operating_system_evidence(disclosure) + elif _has_value_bearing_evidence(disclosure): + raise ValueError( + "value-bearing execution bindings are reserved for compute-substrate and operating-system disclosures" + ) + + +def _binding_fields(disclosure: RealizationObservationDisclosure) -> tuple[object, ...]: + return ( + disclosure.operation_id, + disclosure.envelope_digest, + disclosure.configuration_digest, + disclosure.observer_version, + disclosure.sequence, + ) + + +def _require_compute_substrate_evidence(disclosure: RealizationObservationDisclosure) -> None: + from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_value + + if ( + disclosure.observed_value is None + or any(value is None for value in _binding_fields(disclosure)) + or disclosure.operating_system is not None + or not disclosure.binding_verified + ): + raise ValueError("compute-substrate disclosure requires governed value and verified execution binding") + validate_controlled_vocabulary_value("compute-substrates", disclosure.observed_value) + + +def _require_operating_system_evidence(disclosure: RealizationObservationDisclosure) -> None: + if disclosure.observed_value is not None or disclosure.operating_system is None: + raise ValueError("operating-system disclosure requires one typed observed identity") + if any(value is None for value in _binding_fields(disclosure)) or not disclosure.binding_verified: + raise ValueError("operating-system disclosure requires a verified execution binding") + if disclosure.observation_strength is not ObservationStrength.GUEST_OBSERVED: + raise ValueError("operating-system disclosure requires guest-observed evidence") + + +def _has_value_bearing_evidence(disclosure: RealizationObservationDisclosure) -> bool: + return ( + disclosure.observed_value is not None + or disclosure.operating_system is not None + or any(value is not None for value in _binding_fields(disclosure)) + or disclosure.binding_verified + ) diff --git a/implementations/python/packages/raes_contracts/contracts/realization_plans.py b/implementations/python/packages/raes_contracts/contracts/realization_plans.py index 7fe7865a..5ba40cf6 100644 --- a/implementations/python/packages/raes_contracts/contracts/realization_plans.py +++ b/implementations/python/packages/raes_contracts/contracts/realization_plans.py @@ -28,6 +28,7 @@ WorkflowExecutionStateModel, WorkflowHistoryEventModel, ) +from .operating_systems import ObservedOperatingSystemIdentityModel from .participant_control import ParticipantControlOccurrenceModel from .participant_crossing import ParticipantCrossingOccurrenceModel from .participant_envelopes import ( @@ -48,6 +49,7 @@ ParticipantEpisodeHistoryEventModel, ParticipantEpisodeStateModel, ) +from .realization_observation_validation import validate_realization_observation_disclosure from .time_model import TimeRuntimeStateModel @@ -287,6 +289,7 @@ class RealizationObservationDisclosureModel(ContractModel): verification_scope: RealizationVerificationScope observation_strength: ObservationStrength = Field(json_schema_extra={"not": {"const": "none"}}) observed_value: NonEmptyString | None = None + operating_system: ObservedOperatingSystemIdentityModel | None = None operation_id: NonEmptyString | None = None envelope_digest: str | None = Field(default=None, pattern=r"^sha256:[a-f0-9]{64}$") configuration_digest: str | None = Field(default=None, pattern=r"^sha256:[a-f0-9]{64}$") @@ -296,24 +299,7 @@ class RealizationObservationDisclosureModel(ContractModel): @model_validator(mode="after") def _require_evidence(self) -> RealizationObservationDisclosureModel: - if self.observation_strength is ObservationStrength.NONE: - raise ValueError("realization observation disclosure must provide non-none evidence") - substrate_fields = ( - self.observed_value, - self.operation_id, - self.envelope_digest, - self.configuration_digest, - self.observer_version, - self.sequence, - ) - if self.requirement_kind == "compute-substrate": - if any(value is None for value in substrate_fields) or not self.binding_verified: - raise ValueError("compute-substrate disclosure requires governed value and verified execution binding") - from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_value - - validate_controlled_vocabulary_value("compute-substrates", self.observed_value) - elif any(value is not None for value in substrate_fields) or self.binding_verified: - raise ValueError("value-bearing execution bindings are reserved for compute-substrate disclosures") + validate_realization_observation_disclosure(self) return self diff --git a/implementations/python/packages/raes_contracts/operating_systems.py b/implementations/python/packages/raes_contracts/operating_systems.py new file mode 100644 index 00000000..cd8ff19b --- /dev/null +++ b/implementations/python/packages/raes_contracts/operating_systems.py @@ -0,0 +1,34 @@ +"""Shared portable operating-system identity invariants.""" + +from __future__ import annotations + +import re + +OS_VERSION_PATTERN = r"^[!-~](?:[ -~]{0,126}[!-~])?$" +OS_VERSION_RE = re.compile(OS_VERSION_PATTERN) + +CORE_DISTRIBUTION_FAMILIES = { + "ubuntu": "linux", + "debian": "linux", + "rocky-linux": "linux", + "red-hat-enterprise-linux": "linux", + "windows-server": "windows", + "windows-client": "windows", + "solaris": "other", +} + + +def validate_operating_system_pair(family: str, distribution: str) -> None: + """Reject a core distribution paired with the wrong portable family.""" + + expected = CORE_DISTRIBUTION_FAMILIES.get(distribution) + if expected is not None and family != expected: + raise ValueError(f"operating-system distribution '{distribution}' requires family '{expected}', not '{family}'") + + +__all__ = [ + "CORE_DISTRIBUTION_FAMILIES", + "OS_VERSION_PATTERN", + "OS_VERSION_RE", + "validate_operating_system_pair", +] diff --git a/implementations/python/packages/raes_contracts/realization_envelope_carrier.py b/implementations/python/packages/raes_contracts/realization_envelope_carrier.py index d64130b1..fd7d1bec 100644 --- a/implementations/python/packages/raes_contracts/realization_envelope_carrier.py +++ b/implementations/python/packages/raes_contracts/realization_envelope_carrier.py @@ -13,7 +13,10 @@ from pydantic_core import CoreSchema from raes_contracts.contracts import ContractModel, NonEmptyString, RealizationEnvelopeIdentityModel -from raes_contracts.contracts.capabilities import ProcessResourceLimitCapabilityModel +from raes_contracts.contracts.capabilities import ( + OperatingSystemCompatibilityModel, + ProcessResourceLimitCapabilityModel, +) from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_value from raes_contracts.realization_envelope import RealizationEnvelopeModel from raes_contracts.vocabulary import ObservationStrength @@ -47,6 +50,7 @@ class RealizationConcern(str, Enum): SERVICE = "service" ACL = "acl" COMPUTE_SUBSTRATE = "compute-substrate" + OPERATING_SYSTEM = "operating-system" class TransformationKind(str, Enum): @@ -82,6 +86,7 @@ class RealizerConfigurationModel(ContractModel): network_policy: NonEmptyString supported_node_types: list[NonEmptyString] = Field(min_length=1) supported_os_families: list[NonEmptyString] = Field(min_length=1) + operating_systems: list[OperatingSystemCompatibilityModel] = Field(default_factory=list) supported_content_types: list[NonEmptyString] = Field(default_factory=list) supported_account_features: list[NonEmptyString] = Field(default_factory=list) supported_domain_profiles: list[NonEmptyString] = Field(default_factory=list) @@ -105,6 +110,17 @@ def _validate_unique_terms(self) -> RealizerConfigurationModel: resources = [capability.resource for capability in self.process_resource_limits] if len(resources) != len(set(resources)): raise ValueError("process_resource_limits must not contain duplicate resource terms") + os_keys = [(entry.family, entry.distribution) for entry in self.operating_systems] + if len(os_keys) != len(set(os_keys)): + raise ValueError("operating_systems must not contain duplicate family/distribution rows") + undeclared_families = { + entry.family for entry in self.operating_systems if entry.family not in self.supported_os_families + } + if undeclared_families: + raise ValueError( + "operating_systems families must be present in supported_os_families: " + + ", ".join(sorted(undeclared_families)) + ) # The configuration-bound realized target architecture is a governed # canonical CPU-architecture term (issue #674), so a backend's declared # realization architecture stays in the same portable vocabulary as the @@ -132,6 +148,7 @@ def __get_pydantic_json_schema__( ): properties[field_name]["uniqueItems"] = True properties["process_resource_limits"]["uniqueItems"] = True + properties["operating_systems"]["uniqueItems"] = True return json_schema @@ -267,6 +284,19 @@ def _validate_carrier(self) -> BackendRealizationEnvelopeModel: if missing_concerns: missing = ", ".join(sorted(concern.value for concern in missing_concerns)) raise ValueError(f"concerns must disclose every governed concern; missing: {missing}") + operating_system_claim = next( + claim for claim in self.concerns if claim.concern is RealizationConcern.OPERATING_SYSTEM + ) + if self.configuration.operating_systems: + if ( + operating_system_claim.disposition is not ConcernDisposition.REALIZED + or operating_system_claim.observation_strength is not ObservationStrength.GUEST_OBSERVED + ): + raise ValueError( + "operating_systems capability rows require a realized guest-observed operating-system concern" + ) + elif operating_system_claim.disposition is not ConcernDisposition.UNSUPPORTED: + raise ValueError("operating-system concern support requires coupled operating_systems capability rows") if self.configuration.configuration_digest != realizer_configuration_digest(self.configuration): raise ValueError("realizer configuration digest does not match canonical content") expected = realization_envelope_digest(self) diff --git a/implementations/python/packages/raes_contracts/realization_observation.py b/implementations/python/packages/raes_contracts/realization_observation.py index edd1f33e..e8285e87 100644 --- a/implementations/python/packages/raes_contracts/realization_observation.py +++ b/implementations/python/packages/raes_contracts/realization_observation.py @@ -5,13 +5,36 @@ import re from collections.abc import Sequence from dataclasses import dataclass +from typing import TYPE_CHECKING from raes_contracts.addressing import require_compiled_address from raes_contracts.bounded_domains import scalar_in_domain from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_value -from raes_contracts.realization_envelope import ObservationStrength, RealizationConcern +from raes_contracts.operating_systems import OS_VERSION_RE, validate_operating_system_pair +from raes_contracts.realization_envelope import ConcernDisposition, ObservationStrength, RealizationConcern +from raes_contracts.realization_observation_binding import operating_system_observation_binding_valid from raes_contracts.vocabulary import RealizationVerificationScope +if TYPE_CHECKING: + from raes_contracts.planning import PlanOperation, ProvisioningPlan, ResolvedRealizationAuthority + from raes_contracts.realization_envelope import BackendRealizationEnvelopeModel + + +@dataclass(frozen=True) +class ObservedOperatingSystemIdentity: + """Guest-observed OS family, distribution/product line, and release.""" + + family: str + distribution: str + version: str + + def __post_init__(self) -> None: + validate_controlled_vocabulary_value("provisioner-os-families", self.family) + validate_controlled_vocabulary_value("os-distributions", self.distribution) + validate_operating_system_pair(self.family, self.distribution) + if OS_VERSION_RE.fullmatch(self.version) is None: + raise ValueError("observed operating-system version must be a bounded printable release token") + @dataclass(frozen=True) class RealizationObservation: @@ -53,6 +76,7 @@ class RealizationObservationDisclosure: verification_scope: RealizationVerificationScope observation_strength: ObservationStrength observed_value: str | None = None + operating_system: ObservedOperatingSystemIdentity | None = None operation_id: str | None = None envelope_digest: str | None = None configuration_digest: str | None = None @@ -73,8 +97,12 @@ def __post_init__(self) -> None: raise ValueError("realization observation disclosure must provide non-none evidence") if self.requirement_kind == "compute-substrate": self._validate_compute_substrate_binding() + elif self.requirement_kind == "operating-system": + self._validate_operating_system_binding() elif self._has_execution_binding(): - raise ValueError("value-bearing execution bindings are reserved for compute-substrate disclosures") + raise ValueError( + "value-bearing execution bindings are reserved for compute-substrate and operating-system disclosures" + ) def _validate_compute_substrate_binding(self) -> None: if self.observed_value is None: @@ -87,22 +115,35 @@ def _validate_compute_substrate_binding(self) -> None: if not self.binding_verified: raise ValueError("compute-substrate disclosure must carry a verified execution binding") + def _validate_operating_system_binding(self) -> None: + if self.observed_value is not None or not isinstance(self.operating_system, ObservedOperatingSystemIdentity): + raise ValueError("operating-system disclosure must carry one typed observed identity") + if self.observation_strength is not ObservationStrength.GUEST_OBSERVED: + raise ValueError("operating-system disclosure requires guest-observed evidence") + self._validate_binding_text_fields() + self._validate_binding_digests() + if self.sequence is None or self.sequence < 0: + raise ValueError("operating-system disclosure sequence must be non-negative") + if not self.binding_verified: + raise ValueError("operating-system disclosure must carry a verified execution binding") + def _validate_binding_text_fields(self) -> None: for field_name in ("operation_id", "envelope_digest", "configuration_digest", "observer_version"): value = getattr(self, field_name) if value is None or not value.strip(): - raise ValueError(f"compute-substrate disclosure must carry {field_name}") + raise ValueError(f"bound realization disclosure must carry {field_name}") def _validate_binding_digests(self) -> None: for field_name in ("envelope_digest", "configuration_digest"): if re.fullmatch(r"sha256:[a-f0-9]{64}", getattr(self, field_name)) is None: - raise ValueError(f"compute-substrate disclosure {field_name} must be a sha256 digest") + raise ValueError(f"bound realization disclosure {field_name} must be a sha256 digest") def _has_execution_binding(self) -> bool: return self.binding_verified or any( value is not None for value in ( self.observed_value, + self.operating_system, self.operation_id, self.envelope_digest, self.configuration_digest, @@ -153,6 +194,129 @@ def bind_compute_substrate_observations( return (*non_substrate, *disclosures) +def bind_operating_system_observations( + *, + plan: object, + observations: Sequence[RealizationObservation], + envelope: object, + previous: Sequence[RealizationObservationDisclosure] = (), +) -> tuple[RealizationObservationDisclosure, ...]: + """Bind one guest OS identity to the plan operation and selected apparatus.""" + + from raes_contracts.planning import ProvisioningPlan + from raes_contracts.realization_envelope import BackendRealizationEnvelopeModel + + _require_binding_inputs(plan, envelope, "operating-system observation binding") + if not isinstance(plan, ProvisioningPlan) or not isinstance(envelope, BackendRealizationEnvelopeModel): + raise TypeError("operating-system observation binding requires typed plan and envelope") + if plan.operation_id is None or plan.realization_envelope != envelope.identity: + raise ValueError("operating-system observation binding requires matching operation and envelope identity") + authorities_by_address = _operating_system_authorities_by_address(plan.realization_authority) + operations = {operation.address: operation for operation in plan.operations} + native_by_address = _operating_system_observations_by_address(observations) + retained = tuple(item for item in previous if item.requirement_kind != "operating-system") + bound: list[RealizationObservationDisclosure] = [] + for address, authorities in authorities_by_address.items(): + disclosure = _bound_operating_system_disclosure( + address=address, + authorities=authorities, + operation=operations.get(address), + native=native_by_address.get(address), + plan=plan, + envelope=envelope, + ) + if disclosure is not None: + bound.append(disclosure) + return (*retained, *bound) + + +def _operating_system_authorities_by_address(authorities: Sequence[object]) -> dict[str, list[object]]: + os_kinds = {"os-family", "os-distribution", "os-version"} + by_address: dict[str, list[object]] = {} + for authority in authorities: + if authority.requirement_kind in os_kinds: + by_address.setdefault(authority.address, []).append(authority) + return by_address + + +def _operating_system_observations_by_address( + observations: Sequence[RealizationObservation], +) -> dict[str, RealizationObservation]: + native = tuple(item for item in observations if item.concern is RealizationConcern.OPERATING_SYSTEM) + by_address = {item.address: item for item in native} + if len(by_address) != len(native): + raise ValueError("operating-system observations must identify unique addresses") + return by_address + + +def _bound_operating_system_disclosure( + *, + address: str, + authorities: Sequence[ResolvedRealizationAuthority], + operation: PlanOperation | None, + native: RealizationObservation | None, + plan: ProvisioningPlan, + envelope: BackendRealizationEnvelopeModel, +) -> RealizationObservationDisclosure | None: + from raes_contracts.planning import ChangeAction, PlanOperation + + eligible = ( + isinstance(operation, PlanOperation) + and operation.action is not ChangeAction.DELETE + and native is not None + and _native_operating_system_observation_valid(native, plan, envelope) + ) + if not eligible: + return None + prefix = authorities[0].field_path.rsplit(".", 1)[0] + return RealizationObservationDisclosure( + address=address, + field_path=f"{prefix}.operating-system", + domain=authorities[0].domain, + requirement_kind="operating-system", + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=native.source, + operating_system=native.value, + operation_id=plan.operation_id, + envelope_digest=envelope.digest, + configuration_digest=envelope.configuration.configuration_digest, + observer_version=native.observer_version, + sequence=native.sequence, + binding_verified=True, + ) + + +def _native_operating_system_observation_valid( + observation: RealizationObservation, + plan: object, + envelope: object, +) -> bool: + concern = next( + (claim for claim in envelope.concerns if claim.concern is RealizationConcern.OPERATING_SYSTEM), + None, + ) + return ( + _operating_system_identity_supported(observation.value, envelope.configuration.operating_systems) + and _operating_system_concern_is_guest_observed(concern) + and operating_system_observation_binding_valid(observation, plan, envelope) + ) + + +def _operating_system_identity_supported(identity: object, rows: Sequence[object]) -> bool: + return isinstance(identity, ObservedOperatingSystemIdentity) and any( + row.family == identity.family and row.distribution == identity.distribution and identity.version in row.versions + for row in rows + ) + + +def _operating_system_concern_is_guest_observed(concern: object) -> bool: + return bool( + concern is not None + and concern.disposition is ConcernDisposition.REALIZED + and concern.observation_strength is ObservationStrength.GUEST_OBSERVED + ) + + def _native_observations_by_address( observations: Sequence[RealizationObservation], ) -> dict[str, RealizationObservation]: @@ -320,9 +484,11 @@ def _native_compute_substrate_observation_valid( __all__ = [ + "ObservedOperatingSystemIdentity", "RealizationObservation", "RealizationObservationDisclosure", "bind_compute_substrate_observations", + "bind_operating_system_observations", "compute_substrate_readback_addresses", "missing_compute_substrate_readbacks", ] diff --git a/implementations/python/packages/raes_contracts/realization_observation_binding.py b/implementations/python/packages/raes_contracts/realization_observation_binding.py new file mode 100644 index 00000000..33707aa5 --- /dev/null +++ b/implementations/python/packages/raes_contracts/realization_observation_binding.py @@ -0,0 +1,31 @@ +"""Execution-binding predicates for independently observed realization facts.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from raes_contracts.realization_envelope import ObservationStrength + +if TYPE_CHECKING: + from raes_contracts.planning import ProvisioningPlan + from raes_contracts.realization_envelope import BackendRealizationEnvelopeModel + from raes_contracts.realization_observation import RealizationObservation + + +def operating_system_observation_binding_valid( + observation: RealizationObservation, + plan: ProvisioningPlan, + envelope: BackendRealizationEnvelopeModel, +) -> bool: + """Return whether a guest OS observation is bound to this operation.""" + + return bool( + observation.source is ObservationStrength.GUEST_OBSERVED + and observation.operation_id == plan.operation_id + and observation.envelope_digest == envelope.digest + and observation.configuration_digest == envelope.configuration.configuration_digest + and observation.observer_version + and observation.sequence is not None + and observation.sequence >= 0 + and observation.binding_verified + ) diff --git a/implementations/python/packages/raes_processor/compiler/provisioning.py b/implementations/python/packages/raes_processor/compiler/provisioning.py index c7aca700..7bfe9dc9 100644 --- a/implementations/python/packages/raes_processor/compiler/provisioning.py +++ b/implementations/python/packages/raes_processor/compiler/provisioning.py @@ -185,6 +185,8 @@ def _record_node_runtime( node_name=node_name, node_kind=node_spec.get("type", ""), os_family=node_spec.get("os", "") or "", + os_distribution=node_spec.get("os_distribution", "") or "", + os_version=node_spec.get("os_version", "") or "", architecture=node_spec.get("architecture", "") or "", count=infra_spec.get("count"), network_namespace_target=network_namespace_target, diff --git a/implementations/python/packages/raes_processor/compiler/realization_requirements.py b/implementations/python/packages/raes_processor/compiler/realization_requirements.py index 9ddf9edd..770a21ce 100644 --- a/implementations/python/packages/raes_processor/compiler/realization_requirements.py +++ b/implementations/python/packages/raes_processor/compiler/realization_requirements.py @@ -39,6 +39,7 @@ _persistent_volume_address, ) from .realization_authority_posture import designated_registered_posture, explicit_registered_posture +from .realization_value_domains import compiled_os_value_domain, nested_authored_value def _append_source_artifact_requirement( @@ -70,8 +71,6 @@ def _append_source_artifact_requirements( requirements: list[CompiledRealizationRequirement], scenario: InstantiatedScenario, ) -> None: - """Lower every realized ``Source`` carrier into the existing demand graph.""" - _append_resource_source_artifact_requirements(requirements, scenario) _append_bound_source_artifact_requirements(requirements, scenario) _append_action_source_artifact_requirements(requirements, scenario) @@ -181,15 +180,6 @@ def _realization_requirement_address( raise ValueError("realization concern must resolve to one compiled resource address") -def _nested_authored_value(source: object, path: tuple[str, ...]) -> object: - current = source - for token in path: - if current is None: - return None - current = getattr(current, token, None) - return current - - def _append_domain_topology_requirements( requirements: list[CompiledRealizationRequirement], domain_analysis: DomainTopologyAnalysis, @@ -296,18 +286,25 @@ def _compiled_registered_realization( field_pointer = f"/{section_name}/{encoded_name}/{'/'.join(descriptor.authored_path)}" record = explicitness.get(registered.field_path) declarations = getattr(scenario, section_name) - authored_value = _nested_authored_value( + authored_value = nested_authored_value( declarations[declaration_name], descriptor.authored_path, ) value_constraints: tuple[RealizationValueConstraint, ...] = () process_resource_limits: tuple[ProcessResourceLimitDemand, ...] = () + value_domain = None + constraint_provenance = None if descriptor.concern_kind == "process-resource-limits": value_constraints, process_resource_limits = _compiled_process_resource_limits( scenario, field_pointer=field_pointer, authored_value=authored_value, ) + elif descriptor.concern_kind in {"os-family", "os-distribution", "os-version"}: + value_domain, constraint_provenance = compiled_os_value_domain( + scenario, + field_pointer=field_pointer, + ) if record is not None and not descriptor.includes_authored_value(authored_value): return None, None posture = ( @@ -351,6 +348,8 @@ def _compiled_registered_realization( delegated=posture.delegated, verification_scope=descriptor.required_verification_scope(authored_value), required_observation_strength=descriptor.required_observation_strength(), + value_domain=value_domain, + constraint_provenance=constraint_provenance, value_constraints=value_constraints, process_resource_limits=process_resource_limits, ) diff --git a/implementations/python/packages/raes_processor/compiler/realization_value_domains.py b/implementations/python/packages/raes_processor/compiler/realization_value_domains.py new file mode 100644 index 00000000..e19e293e --- /dev/null +++ b/implementations/python/packages/raes_processor/compiler/realization_value_domains.py @@ -0,0 +1,37 @@ +"""Value extraction and finite-domain helpers for realization compilation.""" + +from __future__ import annotations + +from raes.scenario import InstantiatedScenario +from raes_contracts.bounded_domains import EnumDomain + + +def nested_authored_value(source: object, path: tuple[str, ...]) -> object: + """Resolve one registered concern value from an authored declaration.""" + + current = source + for token in path: + if current is None: + return None + current = getattr(current, token, None) + return current + + +def compiled_os_value_domain( + scenario: InstantiatedScenario, + *, + field_pointer: str, +) -> tuple[EnumDomain | None, str | None]: + """Recover the finite authoring-variable domain for an OS identity leaf.""" + + constraint = next( + ( + item + for item in scenario.instantiation_provenance.capability_constraints + if item.field_pointer == field_pointer + ), + None, + ) + if constraint is None: + return None, None + return EnumDomain(values=list(constraint.allowed_values)), "variable-allowed-values" diff --git a/implementations/python/packages/raes_processor/models/resources.py b/implementations/python/packages/raes_processor/models/resources.py index b99d5155..5a63b724 100644 --- a/implementations/python/packages/raes_processor/models/resources.py +++ b/implementations/python/packages/raes_processor/models/resources.py @@ -59,6 +59,8 @@ class NodeRuntime(ResolvedResource): node_name: str = "" node_kind: str = "" os_family: str = "" + os_distribution: str = "" + os_version: str = "" architecture: str = "" count: int | str | None = None network_namespace_target: str = "" diff --git a/implementations/python/packages/raes_processor/models/runtime_model.py b/implementations/python/packages/raes_processor/models/runtime_model.py index 3a7db406..0fb3be13 100644 --- a/implementations/python/packages/raes_processor/models/runtime_model.py +++ b/implementations/python/packages/raes_processor/models/runtime_model.py @@ -82,7 +82,13 @@ class CompiledCapabilityConstraint: def __post_init__(self) -> None: require_compiled_address(self.address, field_name="capability constraint address") - if self.concern not in {"nodes.os", "nodes.architecture", "infrastructure.count"}: + if self.concern not in { + "nodes.os", + "nodes.os_distribution", + "nodes.os_version", + "nodes.architecture", + "infrastructure.count", + }: raise ValueError("compiled capability constraint has an unsupported concern") if not self.parameter or any(not segment for segment in self.parameter): raise ValueError("compiled capability constraint requires a parameter identity") diff --git a/implementations/python/packages/raes_processor/planner/capability_domains.py b/implementations/python/packages/raes_processor/planner/capability_domains.py index 712f2c7f..261d5c2f 100644 --- a/implementations/python/packages/raes_processor/planner/capability_domains.py +++ b/implementations/python/packages/raes_processor/planner/capability_domains.py @@ -120,18 +120,14 @@ def _node_os_with_constraint( diagnostics: list[Diagnostic] = [] if finite_domain is not None: - unsupported_values = sorted({value for value in finite_domain if value not in supported_os_families}) - if unsupported_values: - rendered = ", ".join(repr(value) for value in unsupported_values) + supported_values = sorted({value for value in finite_domain if value in supported_os_families}) + if not supported_values: diagnostics.append( Diagnostic( code="provisioner.unsupported-os-family", domain="provisioning", address=node.address, - message=( - "Provisioner does not support all OS families allowed by " - f"variable '{variable_name}': {rendered}." - ), + message=(f"Provisioner supports no OS family allowed by variable '{variable_name}'."), ) ) return diagnostics @@ -151,6 +147,28 @@ def _validate_node_os_family( return _node_os_with_constraint(constraint, node, supported_os_families) +def _node_os_domain( + model: RuntimeModel, + node: NodeRuntime, +) -> tuple[tuple[str, ...] | None, Diagnostic | None]: + constraint = _capability_constraint(model, address=node.address, concern="nodes.os") + if constraint is None: + unresolved = extract_variable_name(node.os_family) + if unresolved is not None: + return None, _error_diagnostic( + "provisioner.os-family-variable-ref-unbound", + node.address, + f"Provisioner capability validation cannot resolve undeclared variable '{unresolved}' " + "referenced by nodes.os.", + ) + return (node.os_family,), None + return _validate_os_allowed_values( + ".".join(constraint.parameter), + constraint.allowed_values, + address=node.address, + ) + + def _architecture_allowed_value( raw_value: str | int | float | bool, variable_name: str, diff --git a/implementations/python/packages/raes_processor/planner/manifest_validation.py b/implementations/python/packages/raes_processor/planner/manifest_validation.py index 24e6367e..620b3cfd 100644 --- a/implementations/python/packages/raes_processor/planner/manifest_validation.py +++ b/implementations/python/packages/raes_processor/planner/manifest_validation.py @@ -14,6 +14,7 @@ _validate_node_architecture, _validate_node_os_family, ) +from .operating_system_capability_domains import validate_node_operating_system from .stateful_admission import generated_artifact_payload_diagnostic _ORCHESTRATION_WORKFLOWS_ADDRESS = "orchestration.workflows" @@ -68,6 +69,7 @@ def _validate_node_kind_support(model: RuntimeModel, provisioner: ProvisionerCap provisioner.supported_os_families, ) ) + diagnostics.extend(validate_node_operating_system(model, node, provisioner)) diagnostics.extend( _validate_node_architecture( model, diff --git a/implementations/python/packages/raes_processor/planner/operating_system_capability_domains.py b/implementations/python/packages/raes_processor/planner/operating_system_capability_domains.py new file mode 100644 index 00000000..7fd5d158 --- /dev/null +++ b/implementations/python/packages/raes_processor/planner/operating_system_capability_domains.py @@ -0,0 +1,296 @@ +"""Coupled operating-system capability-domain validation.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from raes.nodes import OSFamily +from raes.operating_systems import normalize_os_distribution, normalize_os_version +from raes.value_parsing import extract_variable_name, parse_enum_or_var +from raes_backend_protocols.capabilities import ProvisionerCapabilities + +from ..models import CompiledCapabilityConstraint, Diagnostic, NodeRuntime, RuntimeModel + +_OS_DISTRIBUTION_DOMAIN_INVALID = "provisioner.os-distribution-variable-domain-invalid" +_OS_VERSION_DOMAIN_INVALID = "provisioner.os-version-variable-domain-invalid" + +_ScalarDomain = tuple[str, ...] | None +_OperatingSystemDomains = tuple[_ScalarDomain, _ScalarDomain, _ScalarDomain] +_OperatingSystemChoice = tuple[str, str, str] + + +@dataclass(frozen=True) +class FeasibleOperatingSystemDomains: + """Coupled apparatus choices remaining after authored-domain intersection.""" + + families: tuple[str, ...] + distributions: tuple[str, ...] + versions: tuple[str, ...] + + def values_for(self, requirement_kind: str) -> tuple[str, ...]: + return { + "os-family": self.families, + "os-distribution": self.distributions, + "os-version": self.versions, + }[requirement_kind] + + +def _capability_constraint( + model: RuntimeModel, + *, + address: str, + concern: str, +) -> CompiledCapabilityConstraint | None: + return next( + ( + constraint + for constraint in model.capability_constraints + if constraint.address == address and constraint.concern == concern + ), + None, + ) + + +def _error_diagnostic(code: str, address: str, message: str) -> Diagnostic: + return Diagnostic(code=code, domain="provisioning", address=address, message=message) + + +def _family_domain( + model: RuntimeModel, + node: NodeRuntime, +) -> tuple[tuple[str, ...] | None, Diagnostic | None]: + constraint = _capability_constraint(model, address=node.address, concern="nodes.os") + diagnostic = None + if constraint is None: + unresolved = extract_variable_name(node.os_family) + if unresolved is not None: + diagnostic = _error_diagnostic( + "provisioner.os-family-variable-ref-unbound", + node.address, + f"Provisioner capability validation cannot resolve undeclared variable '{unresolved}' " + "referenced by nodes.os.", + ) + domain = (node.os_family,) if node.os_family else None + else: + domain, diagnostic = _validated_family_constraint(constraint, node.address) + return (None, diagnostic) if diagnostic is not None else (domain, None) + + +def _validated_family_constraint( + constraint: CompiledCapabilityConstraint, + address: str, +) -> tuple[tuple[str, ...], Diagnostic | None]: + variable_name = ".".join(constraint.parameter) + validated: list[str] = [] + diagnostic = None + for raw_value in constraint.allowed_values: + token, message = _parse_family_domain_value(raw_value, variable_name) + if message is not None: + diagnostic = _error_diagnostic("provisioner.os-family-variable-domain-invalid", address, message) + break + if token is not None: + validated.append(token) + return tuple(validated), diagnostic + + +def _parse_family_domain_value(raw_value: object, variable_name: str) -> tuple[str | None, str | None]: + parsed = None + message = None + try: + parsed = parse_enum_or_var(raw_value, OSFamily, field_name="os") + except ValueError as exc: + message = f"Variable '{variable_name}' allowed_values contain value {raw_value!r} invalid for nodes.os: {exc}." + if message is None and extract_variable_name(parsed) is not None: + message = f"Variable '{variable_name}' has a non-concrete nodes.os domain." + if message is None and not isinstance(parsed, OSFamily): + message = f"Variable '{variable_name}' contains an invalid nodes.os value." + token = parsed.value if isinstance(parsed, OSFamily) else None + return token, message + + +def _scalar_domain_or_open( + model: RuntimeModel, + node: NodeRuntime, + *, + concern: str, + value: str, + field_name: str, + invalid_code: str, + normalizer: Callable[[object], object], +) -> tuple[tuple[str, ...] | None, Diagnostic | None]: + if not value and _capability_constraint(model, address=node.address, concern=concern) is None: + return None, None + return _validated_scalar_domain( + model, + node, + concern=concern, + value=value, + field_name=field_name, + invalid_code=invalid_code, + normalizer=normalizer, + ) + + +def _validated_scalar_domain( + model: RuntimeModel, + node: NodeRuntime, + *, + concern: str, + value: str, + field_name: str, + invalid_code: str, + normalizer: Callable[[object], object], +) -> tuple[tuple[str, ...] | None, Diagnostic | None]: + constraint = _capability_constraint(model, address=node.address, concern=concern) + variable_name = extract_variable_name(value) + if constraint is None: + if variable_name is not None: + return None, _error_diagnostic( + f"provisioner.{field_name}-variable-ref-unbound", + node.address, + f"Provisioner capability validation cannot resolve undeclared variable '{variable_name}' " + f"referenced by nodes.{field_name}.", + ) + raw_values: tuple[object, ...] = (value,) + else: + variable_name = ".".join(constraint.parameter) + raw_values = constraint.allowed_values + + validated: list[str] = [] + diagnostic = None + label = variable_name or field_name + for raw_value in raw_values: + token, message = _normalize_scalar_domain_value(raw_value, label, field_name, normalizer) + if message is not None: + diagnostic = _error_diagnostic(invalid_code, node.address, message) + break + if token is not None: + validated.append(token) + return (None, diagnostic) if diagnostic is not None else (tuple(validated), None) + + +def _normalize_scalar_domain_value( + raw_value: object, + label: str, + field_name: str, + normalizer: Callable[[object], object], +) -> tuple[str | None, str | None]: + parsed = None + message = None + try: + parsed = normalizer(raw_value) + except (TypeError, ValueError) as exc: + message = ( + f"Variable '{label}' allowed_values contain value {raw_value!r} invalid for nodes.{field_name}: {exc}." + ) + if message is None and extract_variable_name(parsed) is not None: + message = f"Variable '{label}' has a non-concrete nodes.{field_name} domain." + token = getattr(parsed, "value", parsed) + if message is None and (not isinstance(token, str) or not token): + message = f"Variable '{label}' contains an invalid nodes.{field_name} value." + return token if isinstance(token, str) else None, message + + +def feasible_operating_system_domains( + model: RuntimeModel, + node: NodeRuntime, + provisioner: ProvisionerCapabilities, +) -> tuple[FeasibleOperatingSystemDomains | None, Diagnostic | None]: + """Intersect exact, constrained, and open OS intent with coupled rows.""" + + os_requirements = { + requirement.requirement_kind + for requirement in model.realization_requirements + if requirement.address == node.address + and requirement.requirement_kind in {"os-family", "os-distribution", "os-version"} + } + feasible_domains = None + diagnostic = None + if os_requirements: + domains, diagnostic = _authored_operating_system_domains(model, node) + if diagnostic is None: + family_domain, distribution_domain, version_domain = domains + feasible = _intersect_operating_system_domains( + provisioner, + family_domain=family_domain, + distribution_domain=distribution_domain, + version_domain=version_domain, + ) + if feasible: + feasible_domains = _feasible_domains(feasible) + else: + diagnostic = _error_diagnostic( + "provisioner.unsupported-operating-system", + node.address, + "Provisioner has no coupled operating-system compatibility row intersecting the authored " + "exact, constrained, and open OS domains.", + ) + return feasible_domains, diagnostic + + +def _authored_operating_system_domains( + model: RuntimeModel, + node: NodeRuntime, +) -> tuple[_OperatingSystemDomains | None, Diagnostic | None]: + family_domain, diagnostic = _family_domain(model, node) + if diagnostic is not None: + return None, diagnostic + distribution_domain, diagnostic = _scalar_domain_or_open( + model, + node, + concern="nodes.os_distribution", + value=node.os_distribution, + field_name="os_distribution", + invalid_code=_OS_DISTRIBUTION_DOMAIN_INVALID, + normalizer=normalize_os_distribution, + ) + if diagnostic is not None: + return None, diagnostic + version_domain, diagnostic = _scalar_domain_or_open( + model, + node, + concern="nodes.os_version", + value=node.os_version, + field_name="os_version", + invalid_code=_OS_VERSION_DOMAIN_INVALID, + normalizer=normalize_os_version, + ) + domains = (family_domain, distribution_domain, version_domain) + return (None, diagnostic) if diagnostic is not None else (domains, None) + + +def _intersect_operating_system_domains( + provisioner: ProvisionerCapabilities, + *, + family_domain: _ScalarDomain, + distribution_domain: _ScalarDomain, + version_domain: _ScalarDomain, +) -> set[_OperatingSystemChoice]: + return { + (row.family, row.distribution, version) + for row in provisioner.operating_systems + if family_domain is None or row.family in family_domain + if distribution_domain is None or row.distribution in distribution_domain + for version in row.versions + if version_domain is None or version in version_domain + } + + +def _feasible_domains(feasible: set[_OperatingSystemChoice]) -> FeasibleOperatingSystemDomains: + return FeasibleOperatingSystemDomains( + families=tuple(sorted({family for family, _, _ in feasible})), + distributions=tuple(sorted({distribution for _, distribution, _ in feasible})), + versions=tuple(sorted({version for _, _, version in feasible})), + ) + + +def validate_node_operating_system( + model: RuntimeModel, + node: NodeRuntime, + provisioner: ProvisionerCapabilities, +) -> list[Diagnostic]: + """Reject only an empty intersection with the coupled capability rows.""" + + _, diagnostic = feasible_operating_system_domains(model, node, provisioner) + return [diagnostic] if diagnostic is not None else [] diff --git a/implementations/python/packages/raes_processor/planner/realization_authority_materialization.py b/implementations/python/packages/raes_processor/planner/realization_authority_materialization.py index 07dfb3aa..0bbf9086 100644 --- a/implementations/python/packages/raes_processor/planner/realization_authority_materialization.py +++ b/implementations/python/packages/raes_processor/planner/realization_authority_materialization.py @@ -19,12 +19,15 @@ CompiledRealizationAuthority, CompiledRealizationRequirement, ) +from .operating_system_capability_domains import feasible_operating_system_domains # These concerns are validated against closed semantic vocabularies before # capability constraints are compiled, so their finite domains are safe to # publish. Generic variable/value constraints are deliberately excluded. _PUBLICATION_SAFE_CAPABILITY_CONCERN_BY_KIND = { "os-family": "nodes.os", + "os-distribution": "nodes.os_distribution", + "os-version": "nodes.os_version", "node-architecture": "nodes.architecture", } @@ -80,25 +83,60 @@ def _authority_bounds( model: RuntimeModel, authority: CompiledRealizationAuthority, requirement: CompiledRealizationRequirement | None, + manifest: BackendManifest, ) -> tuple[RealizationAuthorityBound, ...]: - bounds: list[RealizationAuthorityBound] = [] if ( requirement is not None and requirement.requirement_kind == "process-resource-limits" and requirement.value_constraints ): - for constraint in requirement.value_constraints: - domain = _publication_safe_enum_domain(constraint.allowed_values) - if domain is None: - return () - bounds.append( - RealizationAuthorityBound( - identity_digest=constraint.identity_digest, - value_pointer=f"/{constraint.leaf}", - domain=domain, - ) + return _process_limit_authority_bounds(requirement) + if authority.requirement_kind in {"os-family", "os-distribution", "os-version"}: + return _operating_system_authority_bounds(model, authority, manifest) + return _capability_authority_bounds(model, authority) + + +def _process_limit_authority_bounds( + requirement: CompiledRealizationRequirement, +) -> tuple[RealizationAuthorityBound, ...]: + bounds: list[RealizationAuthorityBound] = [] + for constraint in requirement.value_constraints: + domain = _publication_safe_enum_domain(constraint.allowed_values) + if domain is None: + return () + bounds.append( + RealizationAuthorityBound( + identity_digest=constraint.identity_digest, + value_pointer=f"/{constraint.leaf}", + domain=domain, ) - return tuple(bounds) + ) + return tuple(bounds) + + +def _operating_system_authority_bounds( + model: RuntimeModel, + authority: CompiledRealizationAuthority, + manifest: BackendManifest, +) -> tuple[RealizationAuthorityBound, ...]: + node = model.node_deployments.get(authority.address) + feasible, diagnostic = ( + feasible_operating_system_domains(model, node, manifest.provisioner) + if node is not None and manifest.provisioner is not None + else (None, None) + ) + domain = ( + _publication_safe_enum_domain(feasible.values_for(authority.requirement_kind)) + if diagnostic is None and feasible is not None + else None + ) + return (RealizationAuthorityBound(value_pointer="", domain=domain),) if domain is not None else () + + +def _capability_authority_bounds( + model: RuntimeModel, + authority: CompiledRealizationAuthority, +) -> tuple[RealizationAuthorityBound, ...]: concern = _PUBLICATION_SAFE_CAPABILITY_CONCERN_BY_KIND.get(authority.requirement_kind) constraint = next( ( @@ -137,7 +175,9 @@ def materialize_realization_authority( diagnostics.append(_unresolved_authority_diagnostic(authority)) continue bounds = ( - _authority_bounds(model, authority, requirement) if mode is RealizationAuthorityMode.CONSTRAINED else () + _authority_bounds(model, authority, requirement, manifest) + if mode is RealizationAuthorityMode.CONSTRAINED + else () ) if mode is RealizationAuthorityMode.CONSTRAINED and not bounds: diagnostics.append(_unsafe_bound_diagnostic(authority)) diff --git a/implementations/python/packages/raes_processor/semantics/realization.py b/implementations/python/packages/raes_processor/semantics/realization.py index 32a76ab7..3b6df22a 100644 --- a/implementations/python/packages/raes_processor/semantics/realization.py +++ b/implementations/python/packages/raes_processor/semantics/realization.py @@ -32,7 +32,6 @@ Closure, RealizationSupportMode, observation_strength_satisfies, - verification_scope_satisfies, ) from .artifact_realization import ( @@ -54,6 +53,7 @@ registered_realization_concerns, resolve_realization_concern, ) +from .realization_observation_admission import has_required_observation_support from .realization_process_limits import ( ProcessResourceLimitDemand, RealizationValueConstraint, @@ -208,10 +208,15 @@ def _exact_support_diagnostic( ), severity=Severity.ERROR, ) - if requirement.verification_scope is None or any( - (capability := declaration.observation_capabilities.get(requirement.requirement_kind)) is not None - and verification_scope_satisfies(capability.verification_scope, requirement.verification_scope) - for declaration in exact_declarations + observation_kind = ( + "operating-system" + if requirement.requirement_kind in {"os-family", "os-distribution", "os-version"} + else requirement.requirement_kind + ) + if requirement.verification_scope is None or has_required_observation_support( + requirement, + exact_declarations, + observation_kind=observation_kind, ): return None return Diagnostic( @@ -231,18 +236,47 @@ def _constraint_support_diagnostic( requirement: CompiledRealizationRequirement, declarations: list[RealizationSupportDeclaration], ) -> Diagnostic | None: - if any(requirement.requirement_kind in declaration.supported_constraint_kinds for declaration in declarations): - return None - return Diagnostic( - code="realization.unsupported-constraint-requirement", - domain=requirement.domain, - address=requirement.address, - message=( - "Backend declares no constraint realization support " - f"for constraint kind '{requirement.requirement_kind}' at " - f"'{requirement.field_path}' in domain '{requirement.domain}'." - ), - severity=Severity.ERROR, + supporting = [ + declaration + for declaration in declarations + if requirement.requirement_kind in declaration.supported_constraint_kinds + ] + diagnostic = None + if not supporting: + diagnostic = Diagnostic( + code="realization.unsupported-constraint-requirement", + domain=requirement.domain, + address=requirement.address, + message=( + "Backend declares no constraint realization support " + f"for constraint kind '{requirement.requirement_kind}' at " + f"'{requirement.field_path}' in domain '{requirement.domain}'." + ), + severity=Severity.ERROR, + ) + elif _constrained_os_observation_missing(requirement, supporting): + diagnostic = Diagnostic( + code="realization.under-observed-constraint-requirement", + domain=requirement.domain, + address=requirement.address, + message=( + "Backend declares no guest-observed operating-system corroboration " + f"for constrained '{requirement.requirement_kind}' at '{requirement.field_path}' " + f"in domain '{requirement.domain}'." + ), + severity=Severity.ERROR, + ) + return diagnostic + + +def _constrained_os_observation_missing( + requirement: CompiledRealizationRequirement, + supporting: list[RealizationSupportDeclaration], +) -> bool: + return requirement.requirement_kind in {"os-distribution", "os-version"} and not has_required_observation_support( + requirement, + supporting, + observation_kind="operating-system", ) diff --git a/implementations/python/packages/raes_processor/semantics/realization_concerns.py b/implementations/python/packages/raes_processor/semantics/realization_concerns.py index 2583773f..2bd2023e 100644 --- a/implementations/python/packages/raes_processor/semantics/realization_concerns.py +++ b/implementations/python/packages/raes_processor/semantics/realization_concerns.py @@ -39,7 +39,7 @@ class RealizationConcernDescriptor: projector: Callable[[object, bool], object] | None = None sanitizer: Callable[[object, bool], object] | None = None observed_validator: Callable[[object], None] | None = None - verification_scope: Callable[[object], RealizationVerificationScope] | None = None + verification_scope: Callable[[object], RealizationVerificationScope | None] | None = None observation_strength: ObservationStrength | None = None non_stateful_mounts_only: bool = False @@ -125,6 +125,24 @@ def _forwarding_agent_verification_scope(value: object) -> RealizationVerificati authored_path=("os",), concern_kind="os-family", payload_path=("os_family",), + verification_scope=lambda value: RealizationVerificationScope.PRESENCE if value else None, + observation_strength=ObservationStrength.GUEST_OBSERVED, + ), + RealizationConcernDescriptor( + section="nodes", + authored_path=("os_distribution",), + concern_kind="os-distribution", + payload_path=("os_distribution",), + verification_scope=lambda value: RealizationVerificationScope.PRESENCE if value else None, + observation_strength=ObservationStrength.GUEST_OBSERVED, + ), + RealizationConcernDescriptor( + section="nodes", + authored_path=("os_version",), + concern_kind="os-version", + payload_path=("os_version",), + verification_scope=lambda value: RealizationVerificationScope.PRESENCE if value else None, + observation_strength=ObservationStrength.GUEST_OBSERVED, ), RealizationConcernDescriptor( section="nodes", diff --git a/implementations/python/packages/raes_processor/semantics/realization_observation_admission.py b/implementations/python/packages/raes_processor/semantics/realization_observation_admission.py new file mode 100644 index 00000000..2c868047 --- /dev/null +++ b/implementations/python/packages/raes_processor/semantics/realization_observation_admission.py @@ -0,0 +1,31 @@ +"""Observation-capability admission for authored realization demands.""" + +from raes_contracts.apparatus import RealizationSupportDeclaration +from raes_contracts.vocabulary import observation_strength_satisfies, verification_scope_satisfies + +from .realization_requirement import CompiledRealizationRequirement + + +def has_required_observation_support( + requirement: CompiledRealizationRequirement, + declarations: list[RealizationSupportDeclaration], + *, + observation_kind: str, +) -> bool: + """Return whether one declaration meets the requirement's evidence floor.""" + + return any( + (capability := declaration.observation_capabilities.get(observation_kind)) is not None + and ( + requirement.verification_scope is None + or verification_scope_satisfies(capability.verification_scope, requirement.verification_scope) + ) + and ( + requirement.required_observation_strength is None + or observation_strength_satisfies( + capability.observation_strength, + requirement.required_observation_strength, + ) + ) + for declaration in declarations + ) diff --git a/implementations/python/packages/raes_processor/semantics/realization_requirement.py b/implementations/python/packages/raes_processor/semantics/realization_requirement.py index e801b3ab..5fbcd5ab 100644 --- a/implementations/python/packages/raes_processor/semantics/realization_requirement.py +++ b/implementations/python/packages/raes_processor/semantics/realization_requirement.py @@ -7,6 +7,7 @@ from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance from raes_contracts.addressing import require_compiled_address +from raes_contracts.bounded_domains import EnumDomain from raes_contracts.compute_substrate import validate_compute_substrate_constraint from raes_contracts.vocabulary import ObservationStrength, RealizationVerificationScope @@ -70,6 +71,13 @@ def _validate_process_limit_metadata(self) -> None: def _validate_constraint_metadata(self) -> None: if self.requirement_kind == "compute-substrate": validate_compute_substrate_constraint(self.explicitness, self.value_domain) + elif self.requirement_kind in {"os-family", "os-distribution", "os-version"}: + if self.value_domain is not None and not isinstance(self.value_domain, EnumDomain): + raise ValueError("operating-system constraint domain must be a finite enum") + if self.value_domain is not None and self.explicitness is not ExplicitnessClass.CONSTRAINED: + raise ValueError("operating-system constraint domain requires constrained explicitness") + if self.constraint_provenance is not None and self.value_domain is None: + raise ValueError("operating-system constraint provenance requires a finite domain") elif self.value_domain is not None or self.constraint_provenance is not None: raise ValueError("constraint domain metadata requires compute-substrate") diff --git a/implementations/python/packages/raes_processor/semantics/realization_runtime_common.py b/implementations/python/packages/raes_processor/semantics/realization_runtime_common.py index 21b2e9da..7eace22c 100644 --- a/implementations/python/packages/raes_processor/semantics/realization_runtime_common.py +++ b/implementations/python/packages/raes_processor/semantics/realization_runtime_common.py @@ -30,12 +30,24 @@ BACKEND_CONTRACT_INVALID = "runtime.backend-contract-invalid" MISSING_CONCERN_VALUE = object() +OPERATING_SYSTEM_REQUIREMENT_KINDS = frozenset({"os-family", "os-distribution", "os-version"}) def matching_observation( requirement: CompiledRealizationRequirement, returned_snapshot: RuntimeSnapshot, ) -> RealizationObservationDisclosure | None: + if requirement.requirement_kind in OPERATING_SYSTEM_REQUIREMENT_KINDS: + return next( + ( + entry + for entry in returned_snapshot.realization_observations + if entry.address == requirement.address + and entry.domain == requirement.domain + and entry.requirement_kind == "operating-system" + ), + None, + ) return next( ( entry @@ -64,8 +76,13 @@ def manifest_corroborates( ) -> bool: if manifest is None: return False + capability_kind = ( + "operating-system" + if requirement.requirement_kind in OPERATING_SYSTEM_REQUIREMENT_KINDS + else requirement.requirement_kind + ) return any( - (capability := declaration.observation_capabilities.get(requirement.requirement_kind)) is not None + (capability := declaration.observation_capabilities.get(capability_kind)) is not None and observation_posture_supported(requirement, declaration) and verification_scope_satisfies(capability.verification_scope, observation.verification_scope) and observation_strength_satisfies(capability.observation_strength, observation.observation_strength) @@ -78,7 +95,9 @@ def observation_posture_supported( requirement: CompiledRealizationRequirement, declaration: RealizationSupportDeclaration, ) -> bool: - if requirement.requirement_kind == "compute-substrate" and requirement.explicitness in { + if requirement.requirement_kind in ( + {"compute-substrate"} | OPERATING_SYSTEM_REQUIREMENT_KINDS + ) and requirement.explicitness in { ExplicitnessClass.OPEN, ExplicitnessClass.CONSTRAINED, }: @@ -155,6 +174,7 @@ def concern_value(payload: dict[str, object], path: tuple[str, ...]) -> object: __all__ = [ "BACKEND_CONTRACT_INVALID", "MISSING_CONCERN_VALUE", + "OPERATING_SYSTEM_REQUIREMENT_KINDS", "concern_value", "manifest_corroborates", "matching_observation", diff --git a/implementations/python/packages/raes_processor/semantics/realization_runtime_evaluation.py b/implementations/python/packages/raes_processor/semantics/realization_runtime_evaluation.py index 3d998556..caf65a09 100644 --- a/implementations/python/packages/raes_processor/semantics/realization_runtime_evaluation.py +++ b/implementations/python/packages/raes_processor/semantics/realization_runtime_evaluation.py @@ -14,6 +14,7 @@ ProcessResourceLimitCapability, RealizationSupportDeclaration, ) +from raes_contracts.bounded_domains import scalar_in_domain from raes_contracts.diagnostics import Diagnostic, Severity from raes_contracts.planning import ChangeAction, ProvisioningPlan from raes_contracts.runtime_state import ( @@ -31,6 +32,7 @@ from .realization_runtime_common import ( BACKEND_CONTRACT_INVALID, MISSING_CONCERN_VALUE, + OPERATING_SYSTEM_REQUIREMENT_KINDS, concern_value, manifest_corroborates, matching_observation, @@ -77,10 +79,13 @@ def _evaluate_non_compute_registered_realization( op = declared_ops.get(requirement.address) if requirement.explicitness is None or path is None or op is None or op.action is ChangeAction.DELETE: return None, None - snapshot_entry = returned_snapshot.entries.get(requirement.address) - realized_value = ( - concern_value(snapshot_entry.payload, path) if snapshot_entry is not None else MISSING_CONCERN_VALUE - ) + if requirement.requirement_kind in OPERATING_SYSTEM_REQUIREMENT_KINDS: + realized_value = _observed_operating_system_value(requirement, returned_snapshot) + else: + snapshot_entry = returned_snapshot.entries.get(requirement.address) + realized_value = ( + concern_value(snapshot_entry.payload, path) if snapshot_entry is not None else MISSING_CONCERN_VALUE + ) if requirement.explicitness is ExplicitnessClass.OPEN: return _evaluate_open_realization(requirement, realized_value, returned_snapshot, manifest) return _evaluate_declared_realization( @@ -170,7 +175,13 @@ def _projected_declared_realization_result( else: if honoured is None: honoured = realized_projection == declared_projection - if requirement.explicitness is ExplicitnessClass.EXACT and not honoured: + constrained_os_rejected = ( + requirement.requirement_kind in OPERATING_SYSTEM_REQUIREMENT_KINDS + and requirement.explicitness is ExplicitnessClass.CONSTRAINED + and requirement.value_domain is not None + and not scalar_in_domain(realized_projection, requirement.value_domain) + ) + if constrained_os_rejected or (requirement.explicitness is ExplicitnessClass.EXACT and not honoured): result = (silent_approximation_diagnostic(requirement), None) elif realized_value is not MISSING_CONCERN_VALUE: result = (None, realization_provenance_entry(requirement, honoured)) @@ -187,8 +198,10 @@ def _corroboration_diagnostic( """Reject exact inventory equality that lacks its declared observation basis.""" required_scope = requirement.verification_scope - requires_process_limit_evidence = requirement.requirement_kind == "process-resource-limits" - if (requirement.explicitness is not ExplicitnessClass.EXACT and not requires_process_limit_evidence) or ( + requires_bound_evidence = requirement.requirement_kind in ( + {"process-resource-limits"} | OPERATING_SYSTEM_REQUIREMENT_KINDS + ) + if (requirement.explicitness is not ExplicitnessClass.EXACT and not requires_bound_evidence) or ( required_scope is None and requirement.required_observation_strength is None ): return None @@ -219,6 +232,22 @@ def _corroboration_diagnostic( ) +def _observed_operating_system_value( + requirement: CompiledRealizationRequirement, + returned_snapshot: RuntimeSnapshot, +) -> object: + observation = matching_observation(requirement, returned_snapshot) + identity = observation.operating_system if observation is not None else None + if identity is None: + return MISSING_CONCERN_VALUE + attribute = { + "os-family": "family", + "os-distribution": "distribution", + "os-version": "version", + }[requirement.requirement_kind] + return getattr(identity, attribute) + + def _process_limit_declaration_supported( requirement: CompiledRealizationRequirement, declaration: RealizationSupportDeclaration, diff --git a/implementations/python/packages/raes_runtime/control_plane_api_models.py b/implementations/python/packages/raes_runtime/control_plane_api_models.py index 5183b575..b96253e9 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api_models.py +++ b/implementations/python/packages/raes_runtime/control_plane_api_models.py @@ -250,6 +250,15 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop **( { "observed_value": entry.observed_value, + "operating_system": ( + { + "family": entry.operating_system.family, + "distribution": entry.operating_system.distribution, + "version": entry.operating_system.version, + } + if entry.operating_system is not None + else None + ), "operation_id": entry.operation_id, "envelope_digest": entry.envelope_digest, "configuration_digest": entry.configuration_digest, @@ -257,7 +266,7 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "sequence": entry.sequence, "binding_verified": entry.binding_verified, } - if entry.requirement_kind == "compute-substrate" + if entry.requirement_kind in {"compute-substrate", "operating-system"} else {} ), } diff --git a/implementations/python/packages/raes_runtime/control_plane_store.py b/implementations/python/packages/raes_runtime/control_plane_store.py index 7b86d07a..f5e7a1e5 100644 --- a/implementations/python/packages/raes_runtime/control_plane_store.py +++ b/implementations/python/packages/raes_runtime/control_plane_store.py @@ -14,7 +14,7 @@ value_free_account_placement_payload, ) from raes_contracts.artifact_requirements import ArtifactSatisfactionDisclosureModel -from raes_contracts.contracts import RealizationEnvelopeIdentityModel, RealizationObservationDisclosureModel +from raes_contracts.contracts import RealizationEnvelopeIdentityModel from raes_contracts.contracts.time_model import TimeRuntimeStateModel from raes_contracts.participant_autonomous_state import require_participant_autonomous_runtime_snapshot from raes_contracts.planning import RuntimeDomain @@ -23,13 +23,13 @@ ExplicitnessProvenance, OperationReceipt, OperationStatus, - RealizationObservationDisclosure, RealizationProvenanceEntry, RuntimeSnapshot, RuntimeSnapshotEnvelope, SnapshotEntry, ) -from raes_contracts.vocabulary import ObservationStrength, RealizationVerificationScope + +from .control_plane_store_observations import realization_observation_from_payload if TYPE_CHECKING: from .control_plane_store_local import LocalControlPlaneStore @@ -257,6 +257,15 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: **( { "observed_value": entry.observed_value, + "operating_system": ( + { + "family": entry.operating_system.family, + "distribution": entry.operating_system.distribution, + "version": entry.operating_system.version, + } + if entry.operating_system is not None + else None + ), "operation_id": entry.operation_id, "envelope_digest": entry.envelope_digest, "configuration_digest": entry.configuration_digest, @@ -264,7 +273,7 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: "sequence": entry.sequence, "binding_verified": entry.binding_verified, } - if entry.requirement_kind == "compute-substrate" + if entry.requirement_kind in {"compute-substrate", "operating-system"} else {} ), } @@ -366,7 +375,7 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: if isinstance(item, dict) ), realization_observations=tuple( - _realization_observation_from_payload(item) + realization_observation_from_payload(item) for item in payload.get("realization_observations", []) if isinstance(item, dict) ), @@ -381,25 +390,6 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: return snapshot -def _realization_observation_from_payload(payload: dict[str, Any]) -> RealizationObservationDisclosure: - model = RealizationObservationDisclosureModel.model_validate(payload) - return RealizationObservationDisclosure( - address=model.address, - field_path=model.field_path, - domain=model.domain, - requirement_kind=model.requirement_kind, - verification_scope=RealizationVerificationScope(model.verification_scope), - observation_strength=ObservationStrength(model.observation_strength), - observed_value=model.observed_value, - operation_id=model.operation_id, - envelope_digest=model.envelope_digest, - configuration_digest=model.configuration_digest, - observer_version=model.observer_version, - sequence=model.sequence, - binding_verified=model.binding_verified, - ) - - class InMemoryControlPlaneStore: """Simple in-memory store.""" diff --git a/implementations/python/packages/raes_runtime/control_plane_store_observations.py b/implementations/python/packages/raes_runtime/control_plane_store_observations.py new file mode 100644 index 00000000..4f6d9540 --- /dev/null +++ b/implementations/python/packages/raes_runtime/control_plane_store_observations.py @@ -0,0 +1,39 @@ +"""Runtime observation restoration for control-plane persistence.""" + +from typing import Any + +from raes_contracts.contracts import RealizationObservationDisclosureModel +from raes_contracts.realization_observation import ObservedOperatingSystemIdentity +from raes_contracts.runtime_state import RealizationObservationDisclosure +from raes_contracts.vocabulary import ObservationStrength, RealizationVerificationScope + + +def realization_observation_from_payload(payload: dict[str, Any]) -> RealizationObservationDisclosure: + """Restore one validated observation disclosure from persisted JSON.""" + + model = RealizationObservationDisclosureModel.model_validate(payload) + operating_system = model.operating_system + return RealizationObservationDisclosure( + address=model.address, + field_path=model.field_path, + domain=model.domain, + requirement_kind=model.requirement_kind, + verification_scope=RealizationVerificationScope(model.verification_scope), + observation_strength=ObservationStrength(model.observation_strength), + observed_value=model.observed_value, + operating_system=( + ObservedOperatingSystemIdentity( + family=operating_system.family, + distribution=operating_system.distribution, + version=operating_system.version, + ) + if operating_system is not None + else None + ), + operation_id=model.operation_id, + envelope_digest=model.envelope_digest, + configuration_digest=model.configuration_digest, + observer_version=model.observer_version, + sequence=model.sequence, + binding_verified=model.binding_verified, + ) diff --git a/implementations/python/tests/test_authored_domain_topology.py b/implementations/python/tests/test_authored_domain_topology.py index fe8121ba..432c4c15 100644 --- a/implementations/python/tests/test_authored_domain_topology.py +++ b/implementations/python/tests/test_authored_domain_topology.py @@ -686,7 +686,10 @@ def test_shared_plan_analysis_rejects_account_binding_that_disagrees_with_node() def test_control_plane_rejects_incoherent_domain_topology_before_backend_validation() -> None: - model = compile_runtime_model(_parse_payload(_valid_payload())) + payload = _valid_payload() + for node in payload["nodes"].values(): + node.pop("os", None) + model = compile_runtime_model(_parse_payload(payload)) provisioning = plan(model, _manifest_with_domain_profiles("active_directory")).provisioning operations = [ replace(operation, ordering_dependencies=(), refresh_dependencies=()) diff --git a/implementations/python/tests/test_dsl_437_benign_participant_execution.py b/implementations/python/tests/test_dsl_437_benign_participant_execution.py index e2202eaf..a51c8cf7 100644 --- a/implementations/python/tests/test_dsl_437_benign_participant_execution.py +++ b/implementations/python/tests/test_dsl_437_benign_participant_execution.py @@ -87,6 +87,10 @@ def _advance_stepped_clock_to_tick(manager: RuntimeManager, target_tick: int) -> def _scenario_yaml(*, role: str = "green") -> str: payload = yaml.safe_load(EXAMPLE.read_text(encoding="utf-8")) + for node in payload["nodes"].values(): + node.pop("os", None) + node.pop("os_distribution", None) + node.pop("os_version", None) payload["entities"]["enterprise-participant"]["role"] = role payload["objectives"] = {} payload["workflows"] = {} diff --git a/implementations/python/tests/test_issue_1066_runtime_resource_limits.py b/implementations/python/tests/test_issue_1066_runtime_resource_limits.py index 407e6342..b89fcff0 100644 --- a/implementations/python/tests/test_issue_1066_runtime_resource_limits.py +++ b/implementations/python/tests/test_issue_1066_runtime_resource_limits.py @@ -72,7 +72,6 @@ def _scenario(process_limits: str, *, variables: str = "", realization: str = "" nodes: worker: type: compute - os: linux resources: {{ram: 1 gib, cpu: 1}} runtime: processes: @@ -323,7 +322,6 @@ def test_compiler_preserves_exact_empty_and_constrained_process_limit_posture() nodes: worker: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} runtime: processes: diff --git a/implementations/python/tests/test_issue_1067_resolved_realization_authority.py b/implementations/python/tests/test_issue_1067_resolved_realization_authority.py index 4b27b46e..2e12c5c1 100644 --- a/implementations/python/tests/test_issue_1067_resolved_realization_authority.py +++ b/implementations/python/tests/test_issue_1067_resolved_realization_authority.py @@ -15,9 +15,13 @@ from raes_backend_libvirt.driver import DriverResult as LibvirtDriverResult from raes_backend_libvirt.manifest import create_libvirt_manifest from raes_backend_libvirt.provisioner import LibvirtProvisioner -from raes_backend_protocols.capabilities import BackendManifest, ProvisionerCapabilities +from raes_backend_protocols.capabilities import ( + BackendManifest, + OperatingSystemCompatibility, + ProvisionerCapabilities, +) from raes_backend_stubs.stubs import create_stub_target -from raes_contracts.apparatus import ConceptBinding +from raes_contracts.apparatus import ConceptBinding, RealizationObservationCapability from raes_contracts.bounded_domains import EnumDomain from raes_contracts.contracts import ProvisioningPlanModel, ResolvedRealizationAuthorityModel, schema_bundle from raes_contracts.plan_projection import provisioning_plan_model @@ -33,7 +37,12 @@ ) from raes_contracts.realization_authority import planned_realization_selection_diagnostics from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot, SnapshotEntry -from raes_contracts.vocabulary import Closure, RealizationSupportMode +from raes_contracts.vocabulary import ( + Closure, + ObservationStrength, + RealizationSupportMode, + RealizationVerificationScope, +) from raes_processor.compiler import compile_runtime_model from raes_processor.planner import ( plan, @@ -73,6 +82,13 @@ def _manifest(mode: RealizationSupportMode = RealizationSupportMode.OPEN_REALIZA if mode is RealizationSupportMode.EXACT_ONLY else frozenset({*base.realization_support[0].supported_constraint_kinds, "os-family", "node-architecture"}) ), + observation_capabilities={ + **base.realization_support[0].observation_capabilities, + "operating-system": RealizationObservationCapability( + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + ), + }, ) return BackendManifest( name="authority-handoff", @@ -85,6 +101,10 @@ def _manifest(mode: RealizationSupportMode = RealizationSupportMode.OPEN_REALIZA name="authority-handoff", supported_node_types=frozenset({"compute"}), supported_os_families=frozenset({"linux", "windows"}), + operating_systems=( + OperatingSystemCompatibility("linux", "ubuntu", frozenset({"22.04"})), + OperatingSystemCompatibility("windows", "windows-server", frozenset({"2022"})), + ), ), realization_envelope=base.realization_envelope, ) @@ -137,14 +157,22 @@ def test_planner_resolves_delegation_and_carries_complete_authority() -> None: apparatus_realization_default=lambda _requirement, _manifest: Closure.OPEN_WORLD, ) os_authority = _planned_authority(execution, "nodes.web.os") + distribution_authority = _planned_authority(execution, "nodes.web.os_distribution") + version_authority = _planned_authority(execution, "nodes.web.os_version") assert os_authority.mode is RealizationAuthorityMode.OPEN assert os_authority.source is RealizationResolutionSource.APPARATUS_DEFAULT + assert distribution_authority.mode is RealizationAuthorityMode.OPEN + assert distribution_authority.source is RealizationResolutionSource.APPARATUS_DEFAULT + assert version_authority.mode is RealizationAuthorityMode.OPEN + assert version_authority.source is RealizationResolutionSource.APPARATUS_DEFAULT assert not hasattr(os_authority, "delegated") - assert len(execution.provisioning.realization_authority) == 10 + assert len(execution.provisioning.realization_authority) == 12 assert {entry.requirement_kind for entry in execution.provisioning.realization_authority} >= { "node-type", "os-family", + "os-distribution", + "os-version", "node-architecture", "runtime-environment", } @@ -506,6 +534,11 @@ def test_closed_omission_rejects_backend_materialization_and_emits_no_provenance def test_open_selection_uses_plan_authority_and_discloses_backend_origin() -> None: + from raes_contracts.realization_observation import ( + ObservedOperatingSystemIdentity, + RealizationObservationDisclosure, + ) + execution = plan( compile_runtime_model( _scenario("realization:\n default: closed\n scopes:\n - {field_pointer: /nodes/web/os, posture: open}") @@ -517,6 +550,30 @@ def test_open_selection_uses_plan_authority_and_discloses_backend_origin() -> No payload = deepcopy(entry.payload) payload["os_family"] = "linux" snapshot = snapshot.with_entries({**snapshot.entries, entry.address: replace(entry, payload=payload)}) + snapshot = replace( + snapshot, + realization_observations=( + RealizationObservationDisclosure( + address=entry.address, + field_path="nodes.web.operating-system", + domain="runtime-realization", + requirement_kind="operating-system", + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + operating_system=ObservedOperatingSystemIdentity( + family="linux", + distribution="ubuntu", + version="22.04", + ), + operation_id="op-open-authority-1", + envelope_digest="sha256:" + "a" * 64, + configuration_digest="sha256:" + "b" * 64, + observer_version="guest-os-release/v1", + sequence=1, + binding_verified=True, + ), + ), + ) diagnostics, provenance = realization_authority_disclosure( execution.provisioning, @@ -534,6 +591,11 @@ def test_open_selection_uses_plan_authority_and_discloses_backend_origin() -> No def test_in_bound_constrained_selection_passes_pre_mutation_and_runtime_gates() -> None: + from raes_contracts.realization_observation import ( + ObservedOperatingSystemIdentity, + RealizationObservationDisclosure, + ) + manifest = _manifest() execution = plan(compile_runtime_model(_scenario(web_os="linux")), manifest) plan_value = _constrained_os_selection( @@ -548,6 +610,30 @@ def test_in_bound_constrained_selection_passes_pre_mutation_and_runtime_gates() payload = deepcopy(entry.payload) payload["os_family"] = "windows" snapshot = snapshot.with_entries({**snapshot.entries, entry.address: replace(entry, payload=payload)}) + snapshot = replace( + snapshot, + realization_observations=( + RealizationObservationDisclosure( + address=entry.address, + field_path="nodes.web.operating-system", + domain="runtime-realization", + requirement_kind="operating-system", + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + operating_system=ObservedOperatingSystemIdentity( + family="windows", + distribution="windows-server", + version="2022", + ), + operation_id="op-constrained-authority-1", + envelope_digest="sha256:" + "a" * 64, + configuration_digest="sha256:" + "b" * 64, + observer_version="guest-os-release/v1", + sequence=1, + binding_verified=True, + ), + ), + ) diagnostics, provenance = realization_authority_disclosure( plan_value, snapshot, diff --git a/implementations/python/tests/test_issue_1077_os_identity_realization.py b/implementations/python/tests/test_issue_1077_os_identity_realization.py new file mode 100644 index 00000000..26f16205 --- /dev/null +++ b/implementations/python/tests/test_issue_1077_os_identity_realization.py @@ -0,0 +1,745 @@ +"""Issue #1077: authored OS identity admission and realization evidence.""" + +from __future__ import annotations + +import pytest +from jsonschema import Draft202012Validator +from pydantic import ValidationError + + +class TestAuthoredOperatingSystemIdentity: + def test_distribution_vocabulary_and_release_normalization(self) -> None: + from raes.nodes import Node + from raes.operating_systems import OSDistribution + + node = Node( + type="compute", + os="linux", + os_distribution="Ubuntu", + os_version="22.04", + ) + + assert node.os_distribution is OSDistribution.UBUNTU + assert node.os_version == "22.04" + + def test_governed_distribution_extension_is_canonicalized(self) -> None: + from raes.nodes import Node + + node = Node( + type="compute", + os="other", + os_distribution="X-Siemens:Simatic", + ) + + assert node.os_distribution == "x-siemens:simatic" + + @pytest.mark.parametrize("distribution", ["ubuntu latest", "unknown", "other", "kali"]) + def test_ungoverned_distribution_fails_closed(self, distribution: str) -> None: + from raes.nodes import Node + + with pytest.raises(ValidationError, match="distribution"): + Node(type="compute", os="linux", os_distribution=distribution) + + @pytest.mark.parametrize("version", [" latest", "latest ", "line\nbreak"]) + def test_invalid_explicit_release_fails_closed(self, version: str) -> None: + from raes.nodes import Node + + with pytest.raises(ValidationError, match="version"): + Node( + type="compute", + os="linux", + os_distribution="ubuntu", + os_version=version, + ) + + def test_distribution_requires_family(self) -> None: + from raes import SDLValidationError, parse_sdl + + with pytest.raises(SDLValidationError, match="distribution.*family"): + parse_sdl("name: invalid-os\nnodes:\n web: {type: compute, os_distribution: ubuntu}\n") + + def test_version_requires_distribution(self) -> None: + from raes import SDLValidationError, parse_sdl + + with pytest.raises(SDLValidationError, match="version.*distribution"): + parse_sdl("name: invalid-os\nnodes:\n web: {type: compute, os: linux, os_version: '22.04'}\n") + + def test_family_only_remains_valid(self) -> None: + from raes import parse_sdl + + scenario = parse_sdl("name: family-only\nnodes:\n web: {type: compute, os: linux}\n") + + assert scenario.nodes["web"].os_distribution is None + assert scenario.nodes["web"].os_version == "" + + def test_public_node_schema_encodes_distribution_and_version_dependencies(self) -> None: + from raes.nodes import Node + + validator = Draft202012Validator(Node.model_json_schema()) + + assert list(validator.iter_errors({"type": "compute", "os_version": "22.04"})) + assert list(validator.iter_errors({"type": "compute", "os_distribution": "ubuntu"})) + assert not list( + validator.iter_errors( + { + "type": "compute", + "os": "linux", + "os_distribution": "ubuntu", + "os_version": "22.04", + } + ) + ) + + +class TestCoupledOperatingSystemCapabilities: + def test_capability_preserves_family_distribution_release_coupling(self) -> None: + from raes_backend_protocols.capabilities import ( + OperatingSystemCompatibility, + ProvisionerCapabilities, + ) + + capabilities = ProvisionerCapabilities( + name="paired-os", + supported_node_types=frozenset({"compute"}), + supported_os_families=frozenset({"linux"}), + operating_systems=( + OperatingSystemCompatibility( + family="linux", + distribution="ubuntu", + versions=frozenset({"22.04", "24.04"}), + ), + ), + ) + + assert capabilities.supports_operating_system( + family="linux", + distribution="ubuntu", + version="22.04", + ) + assert not capabilities.supports_operating_system( + family="linux", + distribution="ubuntu", + version="9", + ) + + def test_cross_pairing_between_rows_is_rejected(self) -> None: + from raes_backend_protocols.capabilities import ( + OperatingSystemCompatibility, + ProvisionerCapabilities, + ) + + capabilities = ProvisionerCapabilities( + name="paired-os", + supported_node_types=frozenset({"compute"}), + supported_os_families=frozenset({"linux"}), + operating_systems=( + OperatingSystemCompatibility("linux", "ubuntu", frozenset({"22.04"})), + OperatingSystemCompatibility("linux", "rocky-linux", frozenset({"9"})), + ), + ) + + assert not capabilities.supports_operating_system( + family="linux", + distribution="ubuntu", + version="9", + ) + + def test_row_family_must_match_coarse_family_capability(self) -> None: + from raes_backend_protocols.capabilities import ( + OperatingSystemCompatibility, + ProvisionerCapabilities, + ) + + compatibility = OperatingSystemCompatibility("windows", "windows-server", frozenset({"2022"})) + kwargs = { + "name": "mismatched-os", + "supported_node_types": frozenset({"compute"}), + "supported_os_families": frozenset({"linux"}), + "operating_systems": (compatibility,), + } + with pytest.raises(ValueError, match="supported_os_families"): + ProvisionerCapabilities(**kwargs) + + def test_core_distribution_cannot_be_paired_with_wrong_family(self) -> None: + from raes_backend_protocols.capabilities import OperatingSystemCompatibility + + versions = frozenset({"22.04"}) + with pytest.raises(ValueError, match="requires family 'linux'"): + OperatingSystemCompatibility("windows", "ubuntu", versions) + + def test_manifest_contract_roundtrip_preserves_coupled_rows(self) -> None: + from raes_backend_protocols.capabilities import ( + OperatingSystemCompatibility, + ProvisionerCapabilities, + ) + from raes_backend_protocols.provisioner_manifest import ( + provisioner_capability_payload, + provisioner_from_model, + ) + from raes_contracts.contracts import ProvisionerCapabilitiesModel + + capabilities = ProvisionerCapabilities( + name="roundtrip-os", + supported_node_types=frozenset({"compute"}), + supported_os_families=frozenset({"linux"}), + operating_systems=(OperatingSystemCompatibility("linux", "ubuntu", frozenset({"22.04", "24.04"})),), + ) + + model = ProvisionerCapabilitiesModel.model_validate(provisioner_capability_payload(capabilities)) + + assert provisioner_from_model(model).operating_systems == capabilities.operating_systems + assert model.operating_systems[0].model_dump(mode="json") == { + "family": "linux", + "distribution": "ubuntu", + "versions": ["22.04", "24.04"], + } + + def test_realizer_configuration_validates_coupled_rows(self) -> None: + from raes_contracts.realization_envelope import RealizerConfigurationModel + + payload = { + "mode": "test", + "configuration_digest": "sha256:" + "1" * 64, + "architecture": "x86_64", + "image_policy": "exact", + "network_policy": "isolated", + "supported_node_types": ["compute"], + "supported_os_families": ["linux"], + "operating_systems": [ + { + "family": "linux", + "distribution": "ubuntu", + "versions": ["22.04"], + } + ], + "memory_mib": {"minimum": 128}, + "vcpus": {"minimum": 1}, + } + configuration = RealizerConfigurationModel.model_validate(payload) + + assert configuration.operating_systems[0].distribution == "ubuntu" + payload["operating_systems"] = [ + { + "family": "windows", + "distribution": "windows-server", + "versions": ["2022"], + } + ] + with pytest.raises(ValidationError, match="supported_os_families"): + RealizerConfigurationModel.model_validate(payload) + + +_OS_NODE_SCENARIO = ( + "name: os-compile\n" + "nodes:\n" + " web:\n" + " type: compute\n" + " os: linux\n" + " os_distribution: ubuntu\n" + " os_version: '22.04'\n" + " resources: {ram: 1 gib, cpu: 1}\n" +) + + +def _os_manifest(*versions: str): + from raes_backend_protocols.capabilities import ( + BackendManifest, + OperatingSystemCompatibility, + ProvisionerCapabilities, + ) + from raes_contracts.apparatus import ( + ConceptBinding, + RealizationObservationCapability, + RealizationSupportDeclaration, + ) + from raes_contracts.realization_envelope import ObservationStrength + from raes_contracts.vocabulary import RealizationSupportMode, RealizationVerificationScope + + return BackendManifest( + name="os-limited", + version="0.0.1", + supported_contract_versions=frozenset({"backend-manifest-v2"}), + compatible_processors=frozenset({"raes-reference-processor"}), + realization_support=( + RealizationSupportDeclaration( + domain="runtime-realization", + support_mode=RealizationSupportMode.CONSTRAINED, + supported_constraint_kinds=frozenset({"node-type", "os-family", "os-distribution", "os-version"}), + supported_exact_requirement_kinds=frozenset({"declared-capability-match"}), + disclosure_kinds=frozenset({"runtime-snapshot-v1"}), + observation_capabilities={ + "operating-system": RealizationObservationCapability( + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + ) + }, + ), + ), + concept_bindings=(ConceptBinding(scope="capabilities.provisioner.supported_node_types", family="assets"),), + provisioner=ProvisionerCapabilities( + name="os-limited-provisioner", + supported_node_types=frozenset({"compute"}), + supported_os_families=frozenset({"linux"}), + operating_systems=(OperatingSystemCompatibility("linux", "ubuntu", frozenset(versions)),), + ), + ) + + +def _bound_os_observation_context(): + from dataclasses import replace + + from raes import parse_sdl + from raes_backend_libvirt.envelopes import LibvirtDriverMode, load_libvirt_realization_envelope + from raes_contracts.realization_envelope import ( + BackendRealizationEnvelopeModel, + ObservationStrength, + RealizationConcern, + realization_envelope_digest, + realizer_configuration_digest, + ) + from raes_contracts.realization_observation import ( + ObservedOperatingSystemIdentity, + RealizationObservation, + ) + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan + + execution_plan = plan(compile_runtime_model(parse_sdl(_OS_NODE_SCENARIO)), _os_manifest("22.04")) + envelope_payload = load_libvirt_realization_envelope(LibvirtDriverMode.GENERIC).model_dump(mode="json") + envelope_payload["configuration"]["operating_systems"] = [ + {"family": "linux", "distribution": "ubuntu", "versions": ["22.04"]} + ] + envelope_payload["configuration"]["configuration_digest"] = realizer_configuration_digest( + envelope_payload["configuration"] + ) + os_concern = next(claim for claim in envelope_payload["concerns"] if claim["concern"] == "operating-system") + os_concern.update( + disposition="realized", + observation_strength="guest-observed", + mechanism="guest-os-release", + ) + envelope_payload["digest"] = realization_envelope_digest(envelope_payload) + envelope = BackendRealizationEnvelopeModel.model_validate(envelope_payload) + provisioning = replace( + execution_plan.provisioning, + operation_id="op-os-bound", + realization_envelope=envelope.identity, + ) + native = RealizationObservation( + address="provision.node.web", + field_path="guest.os-release", + concern=RealizationConcern.OPERATING_SYSTEM, + source=ObservationStrength.GUEST_OBSERVED, + value=ObservedOperatingSystemIdentity("linux", "ubuntu", "22.04"), + operation_id="op-os-bound", + envelope_digest=envelope.digest, + configuration_digest=envelope.configuration.configuration_digest, + observer_version="guest-os-release/v1", + sequence=7, + binding_verified=True, + ) + return provisioning, envelope, native + + +class TestOperatingSystemCompilationAndAdmission: + def test_compiled_runtime_and_plan_payload_carry_authored_identity(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan + + model = compile_runtime_model(parse_sdl(_OS_NODE_SCENARIO)) + node = next(iter(model.node_deployments.values())) + + assert (node.os_family, node.os_distribution, node.os_version) == ("linux", "ubuntu", "22.04") + execution_plan = plan(model, _os_manifest("22.04")) + operation = next( + operation for operation in execution_plan.provisioning.operations if operation.resource_type == "node" + ) + assert operation.payload["os_distribution"] == "ubuntu" + assert operation.payload["os_version"] == "22.04" + + def test_distribution_and_release_are_separate_realization_concerns(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + + model = compile_runtime_model(parse_sdl(_OS_NODE_SCENARIO)) + by_field = {requirement.field_path: requirement for requirement in model.realization_requirements} + + assert by_field["nodes.web.os_distribution"].requirement_kind == "os-distribution" + assert by_field["nodes.web.os_version"].requirement_kind == "os-version" + assert by_field["nodes.web.os_version"].required_observation_strength.value == "guest-observed" + + def test_exact_unsupported_release_fails_before_backend_execution(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan + + execution_plan = plan(compile_runtime_model(parse_sdl(_OS_NODE_SCENARIO)), _os_manifest("24.04")) + + assert not execution_plan.is_valid + assert any( + diagnostic.code == "provisioner.unsupported-operating-system" for diagnostic in execution_plan.diagnostics + ) + + @pytest.mark.parametrize("backend", ["reference", "libvirt"]) + def test_existing_backends_reject_specific_os_without_selector_and_observer(self, backend: str) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan + + if backend == "reference": + from raes_reference_backend.manifest import create_reference_backend_manifest + + manifest = create_reference_backend_manifest() + else: + from raes_backend_libvirt.manifest import create_libvirt_manifest + + manifest = create_libvirt_manifest() + + execution_plan = plan(compile_runtime_model(parse_sdl(_OS_NODE_SCENARIO)), manifest) + + assert not execution_plan.is_valid + assert any(d.code == "provisioner.unsupported-operating-system" for d in execution_plan.diagnostics) + + def test_finite_release_domain_retains_the_nonempty_supported_intersection(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan + + scenario = parse_sdl( + "name: os-version-domain\n" + "variables:\n" + " release: {type: string, default: '22.04', allowed_values: ['22.04', '24.04']}\n" + "nodes:\n" + " web:\n" + " type: compute\n" + " os: linux\n" + " os_distribution: ubuntu\n" + " os_version: '${release}'\n" + " resources: {ram: 1 gib, cpu: 1}\n" + ) + + partially_supported = plan(compile_runtime_model(scenario), _os_manifest("22.04")) + fully_supported = plan(compile_runtime_model(scenario), _os_manifest("22.04", "24.04")) + + assert not any(d.code == "provisioner.unsupported-operating-system" for d in partially_supported.diagnostics) + version_authority = next( + entry + for entry in partially_supported.provisioning.realization_authority + if entry.requirement_kind == "os-version" + ) + assert version_authority.bounds[0].domain.values == ["22.04"] + assert not any(d.code == "provisioner.unsupported-operating-system" for d in fully_supported.diagnostics) + + def test_open_distribution_and_version_require_a_compatible_apparatus_row(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan + from raes_reference_backend.manifest import create_reference_backend_manifest + + scenario = parse_sdl( + "name: open-os-details\n" + "realization:\n" + " scopes:\n" + " - {field_pointer: /nodes/web/os_distribution, posture: open}\n" + " - {field_pointer: /nodes/web/os_version, posture: open}\n" + "nodes:\n" + " web:\n" + " type: compute\n" + " os: linux\n" + " resources: {ram: 1 gib, cpu: 1}\n" + ) + + admitted = plan(compile_runtime_model(scenario), _os_manifest("22.04")) + rejected = plan(compile_runtime_model(scenario), create_reference_backend_manifest()) + + assert not any(d.code == "provisioner.unsupported-operating-system" for d in admitted.diagnostics) + assert any(d.code == "provisioner.unsupported-operating-system" for d in rejected.diagnostics) + + +class TestBoundOperatingSystemObservation: + @staticmethod + def _snapshot(execution_plan, *, version: str, include_observation: bool = True): + import copy + + from raes_contracts.planning import RuntimeDomain + from raes_contracts.realization_envelope import ObservationStrength + from raes_contracts.realization_observation import ( + ObservedOperatingSystemIdentity, + RealizationObservationDisclosure, + ) + from raes_contracts.runtime_state import RuntimeSnapshot, SnapshotEntry + from raes_contracts.vocabulary import RealizationVerificationScope + + operation = next(op for op in execution_plan.provisioning.operations if op.resource_type == "node") + payload = copy.deepcopy(operation.payload) + payload["os_version"] = "snapshot-echo-is-not-evidence" + observations = ( + ( + RealizationObservationDisclosure( + address=operation.address, + field_path="nodes.web.operating-system", + domain="runtime-realization", + requirement_kind="operating-system", + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + operating_system=ObservedOperatingSystemIdentity( + family="linux", + distribution="ubuntu", + version=version, + ), + operation_id="op-os-1", + envelope_digest="sha256:" + "a" * 64, + configuration_digest="sha256:" + "b" * 64, + observer_version="guest-os-release/v1", + sequence=1, + binding_verified=True, + ), + ) + if include_observation + else () + ) + return RuntimeSnapshot( + entries={ + operation.address: SnapshotEntry( + address=operation.address, + domain=RuntimeDomain.PROVISIONING, + resource_type=operation.resource_type, + payload=payload, + ordering_dependencies=operation.ordering_dependencies, + refresh_dependencies=operation.refresh_dependencies, + ) + }, + realization_observations=observations, + ) + + def test_one_typed_guest_observation_corroborates_all_os_leaves(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan, realization_disclosure + + model = compile_runtime_model(parse_sdl(_OS_NODE_SCENARIO)) + manifest = _os_manifest("22.04") + execution_plan = plan(model, manifest) + requirements = tuple( + requirement + for requirement in model.realization_requirements + if requirement.requirement_kind in {"os-family", "os-distribution", "os-version"} + ) + + diagnostics, provenance = realization_disclosure( + requirements, + execution_plan.provisioning, + self._snapshot(execution_plan, version="22.04"), + manifest=manifest, + ) + + assert diagnostics == [] + assert {entry.requirement_kind for entry in provenance} == { + "os-family", + "os-distribution", + "os-version", + } + + def test_family_only_requirement_cannot_use_snapshot_echo(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan, realization_disclosure + + scenario = parse_sdl( + "name: family-only\nnodes:\n web: {type: compute, os: linux, resources: {ram: 1 gib, cpu: 1}}\n" + ) + model = compile_runtime_model(scenario) + manifest = _os_manifest("22.04") + execution_plan = plan(model, manifest) + family = next( + requirement for requirement in model.realization_requirements if requirement.requirement_kind == "os-family" + ) + + missing, _ = realization_disclosure( + (family,), + execution_plan.provisioning, + self._snapshot(execution_plan, version="22.04", include_observation=False), + manifest=manifest, + ) + observed, provenance = realization_disclosure( + (family,), + execution_plan.provisioning, + self._snapshot(execution_plan, version="22.04"), + manifest=manifest, + ) + + assert any(d.code == "runtime.backend-contract-invalid" for d in missing) + assert observed == [] + assert [entry.requirement_kind for entry in provenance] == ["os-family"] + + def test_snapshot_echo_cannot_replace_or_override_guest_observation(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan, realization_disclosure + + model = compile_runtime_model(parse_sdl(_OS_NODE_SCENARIO)) + manifest = _os_manifest("22.04", "24.04") + execution_plan = plan(model, manifest) + requirements = tuple( + requirement + for requirement in model.realization_requirements + if requirement.requirement_kind in {"os-family", "os-distribution", "os-version"} + ) + + missing, _ = realization_disclosure( + requirements, + execution_plan.provisioning, + self._snapshot(execution_plan, version="22.04", include_observation=False), + manifest=manifest, + ) + mismatched, _ = realization_disclosure( + requirements, + execution_plan.provisioning, + self._snapshot(execution_plan, version="24.04"), + manifest=manifest, + ) + + assert any(d.code == "runtime.backend-contract-invalid" for d in missing) + assert any(d.code == "runtime.backend-contract-invalid" for d in mismatched) + + def test_constrained_release_rejects_observation_outside_authored_domain(self) -> None: + from raes import parse_sdl + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan, realization_disclosure + + scenario = parse_sdl( + "name: constrained-os-version\n" + "variables:\n" + " release: {type: string, default: '22.04', allowed_values: ['22.04', '23.10']}\n" + "nodes:\n" + " web:\n" + " type: compute\n" + " os: linux\n" + " os_distribution: ubuntu\n" + " os_version: '${release}'\n" + " resources: {ram: 1 gib, cpu: 1}\n" + ) + model = compile_runtime_model(scenario) + manifest = _os_manifest("22.04", "24.04") + execution_plan = plan(model, manifest) + version_requirement = next( + requirement + for requirement in model.realization_requirements + if requirement.requirement_kind == "os-version" + ) + + diagnostics, provenance = realization_disclosure( + (version_requirement,), + execution_plan.provisioning, + self._snapshot(execution_plan, version="24.04"), + manifest=manifest, + ) + + assert version_requirement.explicitness.value == "constrained" + assert [diagnostic.code for diagnostic in diagnostics] == ["runtime.backend-contract-invalid"] + assert provenance == () + + def test_typed_observation_roundtrips_through_public_snapshot_contract(self) -> None: + from raes import parse_sdl + from raes_contracts.contracts import RuntimeSnapshotEnvelopeModel + from raes_processor.compiler import compile_runtime_model + from raes_processor.planner import plan + from raes_runtime.control_plane_store import _snapshot_from_payload, _snapshot_payload + + execution_plan = plan(compile_runtime_model(parse_sdl(_OS_NODE_SCENARIO)), _os_manifest("22.04")) + snapshot = self._snapshot(execution_plan, version="22.04") + payload = _snapshot_payload(snapshot) + + assert _snapshot_from_payload(payload).realization_observations == snapshot.realization_observations + public = RuntimeSnapshotEnvelopeModel.model_validate(payload) + assert public.realization_observations[0].operating_system.version == "22.04" + + def test_binding_derives_verified_disclosure_from_operation_and_envelope(self) -> None: + from raes_contracts.realization_observation import bind_operating_system_observations + + provisioning, envelope, native = _bound_os_observation_context() + + disclosures = bind_operating_system_observations( + plan=provisioning, + observations=(native,), + envelope=envelope, + ) + + assert len(disclosures) == 1 + assert disclosures[0].operating_system == native.value + assert disclosures[0].operation_id == "op-os-bound" + assert disclosures[0].envelope_digest == envelope.digest + + @pytest.mark.parametrize( + "invalid_binding", + [ + "unsupported-version", + "concern-not-realized", + "concern-not-guest-observed", + "observation-not-guest-observed", + "operation-mismatch", + "envelope-mismatch", + "configuration-mismatch", + "missing-observer-version", + "missing-sequence", + "negative-sequence", + "unverified-binding", + ], + ) + def test_binding_rejects_each_invalid_native_os_observation_invariant(self, invalid_binding: str) -> None: + from dataclasses import replace + + from raes_contracts.realization_envelope import ( + ConcernDisposition, + ObservationStrength, + RealizationConcern, + ) + from raes_contracts.realization_observation import ( + ObservedOperatingSystemIdentity, + bind_operating_system_observations, + ) + + provisioning, envelope, native = _bound_os_observation_context() + if invalid_binding == "unsupported-version": + native = replace(native, value=ObservedOperatingSystemIdentity("linux", "ubuntu", "24.04")) + elif invalid_binding == "concern-not-realized": + claims = [ + claim.model_copy(update={"disposition": ConcernDisposition.TRANSFORMED}) + if claim.concern is RealizationConcern.OPERATING_SYSTEM + else claim + for claim in envelope.concerns + ] + envelope = envelope.model_copy(update={"concerns": claims}) + elif invalid_binding == "concern-not-guest-observed": + claims = [ + claim.model_copy(update={"observation_strength": ObservationStrength.DRIVER_REPORTED}) + if claim.concern is RealizationConcern.OPERATING_SYSTEM + else claim + for claim in envelope.concerns + ] + envelope = envelope.model_copy(update={"concerns": claims}) + elif invalid_binding == "observation-not-guest-observed": + native = replace(native, source=ObservationStrength.DRIVER_REPORTED) + elif invalid_binding == "operation-mismatch": + native = replace(native, operation_id="op-other") + elif invalid_binding == "envelope-mismatch": + native = replace(native, envelope_digest="sha256:" + "c" * 64) + elif invalid_binding == "configuration-mismatch": + native = replace(native, configuration_digest="sha256:" + "c" * 64) + elif invalid_binding == "missing-observer-version": + native = replace(native, observer_version=None) + elif invalid_binding == "missing-sequence": + native = replace(native, sequence=None) + elif invalid_binding == "negative-sequence": + native = replace(native, sequence=-1) + else: + native = replace(native, binding_verified=False) + + disclosures = bind_operating_system_observations( + plan=provisioning, + observations=(native,), + envelope=envelope, + ) + + assert disclosures == () diff --git a/implementations/python/tests/test_issue_985_runtime_observation_contract.py b/implementations/python/tests/test_issue_985_runtime_observation_contract.py index c0fe4fe1..57b02246 100644 --- a/implementations/python/tests/test_issue_985_runtime_observation_contract.py +++ b/implementations/python/tests/test_issue_985_runtime_observation_contract.py @@ -63,7 +63,6 @@ def _authoritative_environment_plan() -> tuple[ProvisioningPlan, BackendManifest nodes: worker: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} runtime: environment: diff --git a/implementations/python/tests/test_libvirt_backend_guest_certified.py b/implementations/python/tests/test_libvirt_backend_guest_certified.py index 3d179513..176c018d 100644 --- a/implementations/python/tests/test_libvirt_backend_guest_certified.py +++ b/implementations/python/tests/test_libvirt_backend_guest_certified.py @@ -374,7 +374,6 @@ def _bounded_guest_scenario(tmp_path: Path) -> Path: type: switch demo: type: compute - os: linux resources: {ram: 128 MiB, cpu: 1} services: [] infrastructure: diff --git a/implementations/python/tests/test_libvirt_backend_techvault_integration.py b/implementations/python/tests/test_libvirt_backend_techvault_integration.py index 80e163d9..bf89ea1e 100644 --- a/implementations/python/tests/test_libvirt_backend_techvault_integration.py +++ b/implementations/python/tests/test_libvirt_backend_techvault_integration.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from collections import Counter from libvirt_conformance_fixtures import daemon_compute_substrate_observations @@ -21,6 +22,10 @@ } +def _without_os_identity(source: str): + return parse_sdl(re.sub(r"(?m)^\s+os(?:_distribution|_version)?:.*\n", "", source)) + + class _RecordingLibvirtDriver: driver_mode = "generic" @@ -56,7 +61,7 @@ def test_techvault_scenario_plans_and_applies_through_libvirt_provisioning(): driver = _RecordingLibvirtDriver() target = create_libvirt_target(driver=driver, name_prefix="techvault-test") manager = RuntimeManager(target) - scenario = parse_sdl((EXAMPLES_DIR / "techvault.sdl.yaml").read_text(encoding="utf-8")) + scenario = _without_os_identity((EXAMPLES_DIR / "techvault.sdl.yaml").read_text(encoding="utf-8")) execution_plan = manager.plan(scenario, parameters=_TECHVAULT_PARAMETERS) @@ -108,7 +113,7 @@ def test_techvault_operational_scenario_drives_full_libvirt_surface(): driver = _RecordingLibvirtDriver() target = create_libvirt_target(driver=driver, name_prefix="techvault-operational") manager = RuntimeManager(target) - scenario = parse_sdl((EXAMPLES_DIR / "techvault-operational.sdl.yaml").read_text(encoding="utf-8")) + scenario = _without_os_identity((EXAMPLES_DIR / "techvault-operational.sdl.yaml").read_text(encoding="utf-8")) admission_plan = manager.plan(scenario) @@ -215,7 +220,7 @@ def _techvault_manager() -> tuple[RuntimeManager, _RecordingLibvirtDriver, objec driver = _RecordingLibvirtDriver() target = create_libvirt_target(driver=driver, name_prefix="techvault-recon") manager = RuntimeManager(target) - scenario = parse_sdl((EXAMPLES_DIR / "techvault.sdl.yaml").read_text(encoding="utf-8")) + scenario = _without_os_identity((EXAMPLES_DIR / "techvault.sdl.yaml").read_text(encoding="utf-8")) return manager, driver, scenario diff --git a/implementations/python/tests/test_libvirt_backend_techvault_native.py b/implementations/python/tests/test_libvirt_backend_techvault_native.py index ed7ded4a..e193a342 100644 --- a/implementations/python/tests/test_libvirt_backend_techvault_native.py +++ b/implementations/python/tests/test_libvirt_backend_techvault_native.py @@ -4,6 +4,7 @@ import gzip import json +import re import xml.etree.ElementTree as ET from dataclasses import replace from pathlib import Path @@ -206,7 +207,6 @@ def _bounded_scenario(tmp_path: Path) -> Path: lab: {type: switch} demo: type: compute - os: linux resources: {ram: 128 MiB, cpu: 1} services: [] infrastructure: @@ -252,7 +252,8 @@ def _submit_native_scenario(path: Path, tmp_path: Path): ) target = create_libvirt_target(driver=driver, name_prefix="native-test") manager = RuntimeManager(target) - scenario = parse_sdl(path.read_text(encoding="utf-8")) + content = re.sub(r"(?m)^\s+os(?:_distribution|_version)?:.*\n", "", path.read_text(encoding="utf-8")) + scenario = parse_sdl(content) execution_plan = manager.plan(scenario) control_plane = RuntimeControlPlane(target) receipt = control_plane.submit_provisioning(execution_plan.provisioning) diff --git a/implementations/python/tests/test_libvirt_conformance.py b/implementations/python/tests/test_libvirt_conformance.py index 805ecd2d..0359f4fd 100644 --- a/implementations/python/tests/test_libvirt_conformance.py +++ b/implementations/python/tests/test_libvirt_conformance.py @@ -46,7 +46,6 @@ nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} """ ) @@ -87,7 +86,10 @@ def _bounded_report_payload(report) -> dict: def _libvirt_conformance_report(): - return run_target_conformance(create_libvirt_target(driver=RecordingLibvirtDriver())) + return run_target_conformance( + create_libvirt_target(driver=RecordingLibvirtDriver()), + reference_scenario=_PROVISIONING_SCENARIO, + ) # --------------------------------------------------------------------------- diff --git a/implementations/python/tests/test_libvirt_evidence_run.py b/implementations/python/tests/test_libvirt_evidence_run.py index 52d5e8a8..0cf46a23 100644 --- a/implementations/python/tests/test_libvirt_evidence_run.py +++ b/implementations/python/tests/test_libvirt_evidence_run.py @@ -154,7 +154,6 @@ def _bounded_scenario(tmp_path: Path) -> Path: type: switch demo: type: compute - os: linux resources: {ram: 128 MiB, cpu: 1} services: [] infrastructure: diff --git a/implementations/python/tests/test_libvirt_participant_runtime.py b/implementations/python/tests/test_libvirt_participant_runtime.py index 79541de0..ca320993 100644 --- a/implementations/python/tests/test_libvirt_participant_runtime.py +++ b/implementations/python/tests/test_libvirt_participant_runtime.py @@ -96,7 +96,10 @@ def test_ac2_conformance_requires_constructive_envelope_with_participant_runtime # The participant and hermetic target surfaces remain valid, but the # published libvirt envelope cannot produce native ASR-519 probes. target = _libvirt_target_with_participant_runtime(driver=RecordingLibvirtDriver()) - report = run_target_conformance(target) + report = run_target_conformance( + target, + reference_scenario="name: conformance\nnodes:\n vm: {type: compute}\n", + ) assert report.passed is False assert report.unsupported_contract_gaps == () diff --git a/implementations/python/tests/test_mcp_server.py b/implementations/python/tests/test_mcp_server.py index 6a879efa..725a443e 100644 --- a/implementations/python/tests/test_mcp_server.py +++ b/implementations/python/tests/test_mcp_server.py @@ -52,7 +52,7 @@ def _json_call(server, tool: str, args: dict | None = None) -> dict: name: test-scenario nodes: net: {type: Switch} - web: {type: compute, os: linux, resources: {ram: 2 GiB, cpu: 1}} + web: {type: compute, resources: {ram: 2 GiB, cpu: 1}} infrastructure: net: {count: 1, properties: {cidr: 10.0.0.0/24, gateway: 10.0.0.1}} web: {count: 1, links: [net]} @@ -69,8 +69,8 @@ def _json_call(server, tool: str, args: dict | None = None) -> dict: nodes: corp-net: {type: Switch} - web: {type: compute, os: linux, resources: {ram: 2 GiB, cpu: 1}, features: {app: admin}, roles: {admin: www}, conditions: {alive: admin}} - db: {type: compute, os: linux, resources: {ram: 1 GiB, cpu: 1}, features: {pg: dba}, roles: {dba: postgres}, services: [{port: 5432, name: pg-port}]} + web: {type: compute, resources: {ram: 2 GiB, cpu: 1}, features: {app: admin}, roles: {admin: www}, conditions: {alive: admin}} + db: {type: compute, resources: {ram: 1 GiB, cpu: 1}, features: {pg: dba}, roles: {dba: postgres}, services: [{port: 5432, name: pg-port}]} infrastructure: corp-net: {count: 1, properties: {cidr: 10.0.0.0/24, gateway: 10.0.0.1}} diff --git a/implementations/python/tests/test_plan_inspection_cli.py b/implementations/python/tests/test_plan_inspection_cli.py index 5960b2b0..2ce67494 100644 --- a/implementations/python/tests/test_plan_inspection_cli.py +++ b/implementations/python/tests/test_plan_inspection_cli.py @@ -5,6 +5,7 @@ import json from pathlib import Path +import yaml from paths import EXAMPLES_DIR from raes._errors import ( SDLInstantiationError, @@ -39,8 +40,19 @@ def _invoke(*args: str): return CliRunner().invoke(app, ["processor", "plan", *args]) -def test_plan_default_manifest_emits_contract_json() -> None: - result = _invoke(str(_SCENARIO), "--format", "json") +def _scenario_without_os_identity(tmp_path: Path) -> Path: + payload = yaml.safe_load(_SCENARIO.read_text(encoding="utf-8")) + for node in payload["nodes"].values(): + node.pop("os", None) + node.pop("os_distribution", None) + node.pop("os_version", None) + path = tmp_path / _SCENARIO.name + path.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") + return path + + +def test_plan_default_manifest_emits_contract_json(tmp_path: Path) -> None: + result = _invoke(str(_scenario_without_os_identity(tmp_path)), "--format", "json") assert result.exit_code == 0, result.output payload = json.loads(result.stdout) @@ -59,9 +71,10 @@ def test_plan_default_manifest_emits_contract_json() -> None: assert any(op["payload"] for op in provisioning_ops) -def test_plan_output_is_deterministic() -> None: - first = _invoke(str(_SCENARIO), "--format", "json") - second = _invoke(str(_SCENARIO), "--format", "json") +def test_plan_output_is_deterministic(tmp_path: Path) -> None: + scenario = _scenario_without_os_identity(tmp_path) + first = _invoke(str(scenario), "--format", "json") + second = _invoke(str(scenario), "--format", "json") assert first.exit_code == 0 assert first.stdout == second.stdout @@ -71,7 +84,7 @@ def test_plan_accepts_supplied_backend_manifest(tmp_path: Path) -> None: manifest_file = tmp_path / "stub-manifest.json" manifest_file.write_text(json.dumps(backend_manifest_payload(create_stub_manifest())), encoding="utf-8") - result = _invoke(str(_SCENARIO), "--manifest", str(manifest_file), "--format", "json") + result = _invoke(str(_scenario_without_os_identity(tmp_path)), "--manifest", str(manifest_file), "--format", "json") assert result.exit_code == 0, result.output payload = json.loads(result.stdout) @@ -86,7 +99,7 @@ def test_plan_emits_full_json_and_nonzero_exit_on_error_diagnostics(tmp_path: Pa manifest_file = tmp_path / "no-vm-manifest.json" manifest_file.write_text(json.dumps(payload), encoding="utf-8") - result = _invoke(str(_SCENARIO), "--manifest", str(manifest_file), "--format", "json") + result = _invoke(str(_scenario_without_os_identity(tmp_path)), "--manifest", str(manifest_file), "--format", "json") assert result.exit_code == 1 emitted = json.loads(result.stdout) # the full plan is still emitted @@ -143,7 +156,7 @@ def test_plan_invalid_manifest_does_not_leak_rejected_value(tmp_path: Path) -> N manifest_file = tmp_path / "leaky-manifest.json" manifest_file.write_text(json.dumps(payload), encoding="utf-8") - result = _invoke(str(_SCENARIO), "--manifest", str(manifest_file), "--format", "json") + result = _invoke(str(_scenario_without_os_identity(tmp_path)), "--manifest", str(manifest_file), "--format", "json") assert result.exit_code != 0 assert result.stdout.strip() == "" @@ -158,7 +171,7 @@ def test_plan_manifest_with_realization_envelope_fails_closed(tmp_path: Path) -> manifest_file = tmp_path / "envelope-manifest.json" manifest_file.write_text(json.dumps(payload), encoding="utf-8") - result = _invoke(str(_SCENARIO), "--manifest", str(manifest_file), "--format", "json") + result = _invoke(str(_scenario_without_os_identity(tmp_path)), "--manifest", str(manifest_file), "--format", "json") assert result.exit_code != 0 assert result.stdout.strip() == "" @@ -169,13 +182,13 @@ def test_plan_rejects_non_json_manifest(tmp_path: Path) -> None: manifest_file = tmp_path / "not-json.json" manifest_file.write_text("this is not json", encoding="utf-8") - result = _invoke(str(_SCENARIO), "--manifest", str(manifest_file), "--format", "json") + result = _invoke(str(_scenario_without_os_identity(tmp_path)), "--manifest", str(manifest_file), "--format", "json") assert result.exit_code != 0 assert result.stdout.strip() == "" -def test_plan_requires_explicit_format() -> None: - result = _invoke(str(_SCENARIO)) +def test_plan_requires_explicit_format(tmp_path: Path) -> None: + result = _invoke(str(_scenario_without_os_identity(tmp_path))) assert result.exit_code != 0 diff --git a/implementations/python/tests/test_realization_envelope_contract.py b/implementations/python/tests/test_realization_envelope_contract.py index 118c3a46..635241d1 100644 --- a/implementations/python/tests/test_realization_envelope_contract.py +++ b/implementations/python/tests/test_realization_envelope_contract.py @@ -85,6 +85,7 @@ def _payload() -> dict[str, object]: "transformations": [], } for concern in ( + "operating-system", "architecture", "image", "network", @@ -167,6 +168,34 @@ def test_backend_realization_envelope_rejects_missing_concern_disclosure(): BackendRealizationEnvelopeModel.model_validate(payload) +def test_operating_system_capability_rows_require_realized_guest_observed_claim(): + payload = _payload() + payload["configuration"]["operating_systems"] = [ # type: ignore[index] + {"family": "linux", "distribution": "ubuntu", "versions": ["22.04"]} + ] + payload["configuration"]["configuration_digest"] = realizer_configuration_digest( # type: ignore[index] + payload["configuration"] + ) + payload["digest"] = realization_envelope_digest(payload) + + with pytest.raises(ValidationError, match="capability rows require a realized guest-observed"): + BackendRealizationEnvelopeModel.model_validate(payload) + + +def test_operating_system_claim_requires_coupled_capability_rows(): + payload = _payload() + operating_system = next(claim for claim in payload["concerns"] if claim["concern"] == "operating-system") # type: ignore[index] + operating_system.update( + disposition="realized", + observation_strength="guest-observed", + mechanism="guest-os-release", + ) + payload["digest"] = realization_envelope_digest(payload) + + with pytest.raises(ValidationError, match="support requires coupled operating_systems capability rows"): + BackendRealizationEnvelopeModel.model_validate(payload) + + def test_published_schema_enforces_expressible_realization_invariants(): schema = BackendRealizationEnvelopeModel.model_json_schema() validator = Draft202012Validator(schema) diff --git a/implementations/python/tests/test_realization_envelope_relation.py b/implementations/python/tests/test_realization_envelope_relation.py index 6465c0de..b89eed2a 100644 --- a/implementations/python/tests/test_realization_envelope_relation.py +++ b/implementations/python/tests/test_realization_envelope_relation.py @@ -543,7 +543,9 @@ def test_record_domain_membership_and_witness() -> None: assert result.scenario is not None assert member(result.scenario, env).holds # A closed record rejects an undeclared field on the node. - extra = _instantiate('name: rec\nnodes:\n web:\n type: compute\n os: linux\n os_version: "9"\n') + extra = _instantiate( + 'name: rec\nnodes:\n web:\n type: compute\n os: linux\n os_distribution: rocky-linux\n os_version: "9"\n' + ) rejected = member(extra, env) assert not rejected.holds assert any(d.code == "realization-envelope.membership.closed-world-extra" for d in rejected.diagnostics) @@ -668,7 +670,9 @@ def test_open_world_overlay_removes_inherited_closed_state() -> None: ) ], ) - instance = _instantiate('name: open-overlay\nnodes:\n web: {type: compute, os: linux, os_version: "9"}\n') + instance = _instantiate( + 'name: open-overlay\nnodes:\n web: {type: compute, os: linux, os_distribution: rocky-linux, os_version: "9"}\n' + ) assert member(instance, env).holds diff --git a/implementations/python/tests/test_realization_honesty_conformance.py b/implementations/python/tests/test_realization_honesty_conformance.py index 9e5bb05e..99d709bb 100644 --- a/implementations/python/tests/test_realization_honesty_conformance.py +++ b/implementations/python/tests/test_realization_honesty_conformance.py @@ -26,7 +26,6 @@ from raes_contracts.realization_envelope import ( BackendRealizationEnvelopeModel, ConcernDisposition, - EnumDomain, EnvelopeBinding, EnvelopeScope, ExactDomain, @@ -45,7 +44,6 @@ nodes: vm: type: compute - os: linux """ @@ -56,12 +54,10 @@ def _constructive_expression() -> RealizationEnvelopeModel: domains={ "name": ExactDomain(value="honesty"), "type": ExactDomain(value="compute"), - "os": EnumDomain(values=["linux"]), }, bindings=[ EnvelopeBinding(path="name", scope=EnvelopeScope.SCENARIO, posture=Posture.EXACT, domain="name"), EnvelopeBinding(path="nodes.vm.type", scope=EnvelopeScope.NODE, posture=Posture.EXACT, domain="type"), - EnvelopeBinding(path="nodes.vm.os", scope=EnvelopeScope.FIELD, posture=Posture.CONSTRAINED, domain="os"), ], ) diff --git a/implementations/python/tests/test_reference_backend_components.py b/implementations/python/tests/test_reference_backend_components.py index 0104a3a4..c46d75e4 100644 --- a/implementations/python/tests/test_reference_backend_components.py +++ b/implementations/python/tests/test_reference_backend_components.py @@ -18,7 +18,6 @@ nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} diff --git a/implementations/python/tests/test_reference_backend_docker_integration.py b/implementations/python/tests/test_reference_backend_docker_integration.py index 2f2ebf07..71f6e372 100644 --- a/implementations/python/tests/test_reference_backend_docker_integration.py +++ b/implementations/python/tests/test_reference_backend_docker_integration.py @@ -33,7 +33,6 @@ nodes: web: type: compute - os: linux source: {_IMAGE} resources: {{ram: 1 gib, cpu: 1}} """ @@ -121,6 +120,7 @@ def test_real_driver_conformance_executes_native_cases_and_fails_closed_for_nonc report = run_target_conformance( target, participant_opacity_harness=ReferenceParticipantOpacityHarness(), + reference_scenario=_SCENARIO, ) assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE diff --git a/implementations/python/tests/test_reference_backend_provenance.py b/implementations/python/tests/test_reference_backend_provenance.py index 58bc4a49..bbb001d6 100644 --- a/implementations/python/tests/test_reference_backend_provenance.py +++ b/implementations/python/tests/test_reference_backend_provenance.py @@ -5,7 +5,7 @@ import textwrap from raes import parse_sdl -from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance +from raes.explicitness import ExplicitnessClass from raes_reference_backend import create_reference_backend_target from raes_runtime.manager import RuntimeManager @@ -14,7 +14,6 @@ nodes: web: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} """ @@ -27,9 +26,6 @@ def test_apply_records_realization_provenance(): assert result.success, [diag.message for diag in result.diagnostics] by_field = {entry.field_path: entry for entry in result.snapshot.realization_provenance} - assert by_field["nodes.web.os"].provenance is ExplicitnessProvenance.AUTHOR_DECLARED - assert by_field["nodes.web.os"].explicitness is ExplicitnessClass.EXACT - assert by_field["nodes.web.os"].requirement_kind == "os-family" assert by_field["nodes.web.type"].explicitness is ExplicitnessClass.EXACT @@ -40,7 +36,7 @@ def test_apply_snapshot_preserves_planned_payload_no_emulator_state(): result = manager.apply(plan) entry = result.snapshot.entries["provision.node.web"] - assert entry.payload.get("os_family") == "linux" + assert entry.payload.get("node_kind") == "compute" rendered = repr(result.snapshot.entries) for forbidden in ("container_id", "docker", "podman", "/var/run", "InProcessDriver"): assert forbidden not in rendered diff --git a/implementations/python/tests/test_reference_backend_provisioner.py b/implementations/python/tests/test_reference_backend_provisioner.py index c3a3ef5a..f3c303a5 100644 --- a/implementations/python/tests/test_reference_backend_provisioner.py +++ b/implementations/python/tests/test_reference_backend_provisioner.py @@ -25,7 +25,6 @@ nodes: web: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} """ diff --git a/implementations/python/tests/test_reference_processor.py b/implementations/python/tests/test_reference_processor.py index 403521f2..71d66afe 100644 --- a/implementations/python/tests/test_reference_processor.py +++ b/implementations/python/tests/test_reference_processor.py @@ -44,7 +44,6 @@ nodes: vm1: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -81,7 +80,6 @@ nodes: vm1: type: compute - os: linux resources: ram: 1 gib cpu: ${cpu_count} diff --git a/implementations/python/tests/test_run_300_lifecycle.py b/implementations/python/tests/test_run_300_lifecycle.py index d747deea..3385ab1b 100644 --- a/implementations/python/tests/test_run_300_lifecycle.py +++ b/implementations/python/tests/test_run_300_lifecycle.py @@ -41,7 +41,7 @@ EXPECTED_NODE_ADDRESS = f"provision.node.{NODE_NAME}" EXPECTED_WORKFLOW_ADDRESS = "orchestration.workflow.response" DRIFT_DIAGNOSTIC_CODE = "runtime.plan-snapshot-mismatch" -PARAM_OS_KIND = "linux" +PARAM_NODE_DESCRIPTION = "primary-node" PARAM_CPU_COUNT = 2 @@ -56,7 +56,7 @@ def _raw_scenario(): the parser explicitly rejects ``${var}`` in user-defined mapping keys (see ``_reject_variable_mapping_keys`` in the SDL parser). The meaning-preservation probe therefore lives in value positions: - the ``os_kind`` and ``cpu_count`` variables must flow through + the ``node_description`` and ``cpu_count`` variables must flow through instantiation and land, fully substituted, inside the compiled model, the planner's operation payload, and the applied snapshot entry for ``provision.node.vm1``. @@ -71,17 +71,17 @@ def _raw_scenario(): f""" name: run-300-lifecycle variables: - os_kind: + node_description: type: string - default: linux - allowed_values: [linux] + default: primary-node + allowed_values: [primary-node] cpu_count: type: integer default: 1 nodes: {NODE_NAME}: type: compute - os: ${{os_kind}} + description: ${{node_description}} resources: ram: 1 gib cpu: ${{cpu_count}} @@ -130,7 +130,7 @@ class TestRun300Lifecycle: def test_valid_scenario_flows_through_all_five_stages_preserving_identity(self): """One parameterized scenario → instantiate → compile → plan → apply → observe. - Asserts that the values substituted for ``${os_kind}`` and + Asserts that the values substituted for ``${node_description}`` and ``${cpu_count}`` at instantiation time, and the canonical node and workflow addresses, survive unchanged into the downstream typed contracts that publish them. Substituted values must remain @@ -144,7 +144,7 @@ def test_valid_scenario_flows_through_all_five_stages_preserving_identity(self): raw = _raw_scenario() instantiated = instantiate_scenario( raw, - parameters={"os_kind": PARAM_OS_KIND, "cpu_count": PARAM_CPU_COUNT}, + parameters={"node_description": PARAM_NODE_DESCRIPTION, "cpu_count": PARAM_CPU_COUNT}, ) assert isinstance(instantiated, InstantiatedScenario), ( @@ -153,7 +153,7 @@ def test_valid_scenario_flows_through_all_five_stages_preserving_identity(self): "meaning across stages." ) assert instantiated.instantiation_provenance.root_binding_values == { - "os_kind": PARAM_OS_KIND, + "node_description": PARAM_NODE_DESCRIPTION, "cpu_count": PARAM_CPU_COUNT, }, ( "Instantiation parameters must be captured in portable provenance " @@ -169,8 +169,8 @@ def test_valid_scenario_flows_through_all_five_stages_preserving_identity(self): "The canonical node identity must appear under its literal key after instantiation." ) instantiated_node = instantiated_payload["nodes"][NODE_NAME] - assert instantiated_node["os"] == PARAM_OS_KIND, ( - "Instantiation must substitute os_kind before compilation begins." + assert instantiated_node["description"] == PARAM_NODE_DESCRIPTION, ( + "Instantiation must substitute node_description before compilation begins." ) assert instantiated_node["resources"]["cpu"] == PARAM_CPU_COUNT, ( "Instantiation must substitute cpu_count before compilation begins." @@ -190,10 +190,9 @@ def test_valid_scenario_flows_through_all_five_stages_preserving_identity(self): ) compiled_node = model.node_deployments[EXPECTED_NODE_ADDRESS] assert compiled_node.node_name == NODE_NAME - assert compiled_node.os_family == PARAM_OS_KIND, ( - f"os_kind parameter ({PARAM_OS_KIND!r}) must survive " - f"substitution through into the compiled node's os_family; " - f"got {compiled_node.os_family!r}." + assert compiled_node.spec["node"]["description"] == PARAM_NODE_DESCRIPTION, ( + f"node_description parameter ({PARAM_NODE_DESCRIPTION!r}) must survive " + "substitution through into the compiled node specification." ) assert compiled_node.spec["node"]["resources"]["cpu"] == PARAM_CPU_COUNT, ( f"cpu_count parameter ({PARAM_CPU_COUNT!r}) must survive " @@ -239,8 +238,8 @@ def test_valid_scenario_flows_through_all_five_stages_preserving_identity(self): "the compiled model." ) create_op = create_ops[0] - assert create_op.payload["os_family"] == PARAM_OS_KIND, ( - "Planner payload must preserve the compiled os_family without rewriting or reinterpreting it." + assert create_op.payload["spec"]["node"]["description"] == PARAM_NODE_DESCRIPTION, ( + "Planner payload must preserve the compiled node description without rewriting or reinterpreting it." ) assert create_op.payload["spec"]["node"]["resources"]["cpu"] == PARAM_CPU_COUNT, ( "Planner payload must preserve the compiled cpu_count without rewriting or reinterpreting it." @@ -280,8 +279,8 @@ def test_valid_scenario_flows_through_all_five_stages_preserving_identity(self): "ApplyResult must report the canonical address as changed so " "live-observation consumers can subscribe by canonical identity." ) - assert applied_entry.payload["os_family"] == PARAM_OS_KIND, ( - "Apply must preserve the planned os_family in the stored snapshot entry." + assert applied_entry.payload["spec"]["node"]["description"] == PARAM_NODE_DESCRIPTION, ( + "Apply must preserve the planned node description in the stored snapshot entry." ) assert applied_entry.payload["spec"]["node"]["resources"]["cpu"] == PARAM_CPU_COUNT, ( "Apply must preserve the planned cpu_count in the stored snapshot entry." @@ -352,7 +351,7 @@ def test_reapplying_stale_plan_is_rejected_by_provenance_drift_check(self): raw = _raw_scenario() instantiated = instantiate_scenario( raw, - parameters={"os_kind": PARAM_OS_KIND, "cpu_count": PARAM_CPU_COUNT}, + parameters={"node_description": PARAM_NODE_DESCRIPTION, "cpu_count": PARAM_CPU_COUNT}, ) model = compile_runtime_model(instantiated) target = _enveloped_stub_target() @@ -408,12 +407,12 @@ def test_unresolved_instantiation_parameter_is_rejected_before_compilation(self) """ name: run-300-unresolved variables: - os_kind: + node_description: type: string nodes: vm1: type: compute - os: ${os_kind} + description: ${node_description} resources: {ram: 1 gib, cpu: 1} """ ) diff --git a/implementations/python/tests/test_runtime_conformance.py b/implementations/python/tests/test_runtime_conformance.py index 25d33f01..912486e5 100644 --- a/implementations/python/tests/test_runtime_conformance.py +++ b/implementations/python/tests/test_runtime_conformance.py @@ -1286,13 +1286,12 @@ def apply(self, plan, snapshot: RuntimeSnapshot) -> ApplyResult: return ApplyResult(success=True, snapshot=snapshot, changed_addresses=[]) -def _reference_scenario(node_name: str, *, os_family: str = "linux") -> str: +def _reference_scenario(node_name: str) -> str: return f""" name: conformance nodes: {node_name}: type: compute - os: {os_family} resources: {{ram: 1 gib, cpu: 1}} conditions: {{health: ops}} roles: {{ops: operator}} diff --git a/implementations/python/tests/test_runtime_control_plane.py b/implementations/python/tests/test_runtime_control_plane.py index 48a2a7bc..25a699c0 100644 --- a/implementations/python/tests/test_runtime_control_plane.py +++ b/implementations/python/tests/test_runtime_control_plane.py @@ -272,7 +272,6 @@ def test_control_plane_submits_provisioning_and_updates_snapshot(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} """) execution_plan = plan(compile_runtime_model(scenario), create_stub_target().manifest) @@ -576,7 +575,6 @@ def test_control_plane_submits_orchestration_with_portable_workflow_state(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} diff --git a/implementations/python/tests/test_runtime_control_plane_api.py b/implementations/python/tests/test_runtime_control_plane_api.py index a22a41e0..6b70ce11 100644 --- a/implementations/python/tests/test_runtime_control_plane_api.py +++ b/implementations/python/tests/test_runtime_control_plane_api.py @@ -164,7 +164,6 @@ def test_control_plane_api_accepts_orchestration_plan_and_exposes_snapshot(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -247,7 +246,6 @@ def test_control_plane_api_exposes_operational_apparatus_summary_to_auditors(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -402,7 +400,6 @@ def test_control_plane_api_persists_operations_and_snapshot(tmp_path: Path): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} """) target = create_stub_target() @@ -621,7 +618,6 @@ def test_control_plane_api_cancels_workflow_runs(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -703,7 +699,6 @@ def test_control_plane_api_reconciles_workflow_timeouts(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -796,7 +791,6 @@ def test_control_plane_api_cancellation_triggers_compensation_history(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -922,7 +916,6 @@ def test_control_plane_api_timeout_triggers_compensation_history(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} diff --git a/implementations/python/tests/test_runtime_manager.py b/implementations/python/tests/test_runtime_manager.py index b66d322e..a45aba59 100644 --- a/implementations/python/tests/test_runtime_manager.py +++ b/implementations/python/tests/test_runtime_manager.py @@ -71,7 +71,6 @@ def _full_scenario(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -100,7 +99,6 @@ def _provisioning_only_scenario(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} """) @@ -111,7 +109,6 @@ def _workflow_scenario(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -149,7 +146,6 @@ def _workflow_call_scenario(): nodes: vm: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -1464,8 +1460,7 @@ def test_apply_uses_matching_initial_snapshot(self): nodes: vm: type: compute - os: windows - resources: {ram: 1 gib, cpu: 1} + resources: {ram: 2 gib, cpu: 1} """) manager = RuntimeManager(target, initial_snapshot=initial_snapshot) @@ -1473,7 +1468,7 @@ def test_apply_uses_matching_initial_snapshot(self): assert result.success assert calls == ["provision-apply"] - assert manager.snapshot.entries["provision.node.vm"].payload["os_family"] == "windows" + assert manager.snapshot.entries["provision.node.vm"].payload["spec"]["node"]["resources"]["ram"] == 2**31 def test_identical_second_apply_skips_runtime_service_restarts(self): calls: list[str] = [] diff --git a/implementations/python/tests/test_runtime_planner.py b/implementations/python/tests/test_runtime_planner.py index 373e0db1..8c19b0ab 100644 --- a/implementations/python/tests/test_runtime_planner.py +++ b/implementations/python/tests/test_runtime_planner.py @@ -3,6 +3,7 @@ from __future__ import annotations import textwrap +from dataclasses import replace import pytest from paths import EXAMPLES_DIR @@ -10,14 +11,23 @@ from raes_backend_protocols.capabilities import ( BackendManifest, EvaluatorCapabilities, + OperatingSystemCompatibility, OrchestratorCapabilities, ProvisionerCapabilities, WorkflowFeature, WorkflowStatePredicateFeature, ) from raes_backend_stubs.stubs import create_stub_manifest -from raes_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration -from raes_contracts.vocabulary import RealizationSupportMode +from raes_contracts.apparatus import ( + ConceptBinding, + RealizationObservationCapability, + RealizationSupportDeclaration, +) +from raes_contracts.vocabulary import ( + ObservationStrength, + RealizationSupportMode, + RealizationVerificationScope, +) from raes_processor.compiler import compile_runtime_model from raes_processor.models import RuntimeDomain, RuntimeSnapshot, SnapshotEntry from raes_processor.planner import plan @@ -85,15 +95,42 @@ def _limited_backend_manifest( ), supported_exact_requirement_kinds=frozenset({"declared-capability-match"}), disclosure_kinds=frozenset({"runtime-snapshot-v1"}), + observation_capabilities={ + "operating-system": RealizationObservationCapability( + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + ) + }, ), ), concept_bindings=(ConceptBinding(scope="capabilities.provisioner.supported_node_types", family="assets"),), - provisioner=provisioner, + provisioner=replace( + provisioner, + operating_systems=tuple( + OperatingSystemCompatibility(family, distribution, frozenset({version})) + for family, distribution, version in ( + ("linux", "ubuntu", "22.04"), + ("windows", "windows-server", "2022"), + ("other", "solaris", "11.4"), + ) + if family in provisioner.supported_os_families + ), + ), orchestrator=orchestrator, evaluator=evaluator, ) +def _os_capable_stub_manifest() -> BackendManifest: + base = create_stub_manifest() + return _limited_backend_manifest( + name="os-capable-stub", + provisioner=base.provisioner, + orchestrator=base.orchestrator, + evaluator=base.evaluator, + ) + + class TestRuntimePlanner: def test_plan_records_provenance(self): snapshot = RuntimeSnapshot(metadata={"seed": "planner"}) @@ -318,13 +355,11 @@ def test_assertion_refs_remain_unambiguous_with_multiple_condition_bindings(self nodes: a: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} b: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} conditions: {health: ops} roles: {ops: operator} @@ -358,7 +393,6 @@ def test_top_level_inject_refs_resolve_directly(self): nodes: web: type: compute - os: linux resources: {ram: 1 gib, cpu: 1} injects: mail: {source: inbox} @@ -843,7 +877,7 @@ def test_objective_window_refs_are_refresh_only(self): ) objective = model.objectives["evaluation.objective.initial"] - execution_plan = plan(model, create_stub_manifest()) + execution_plan = plan(model, _os_capable_stub_manifest()) assert "orchestration.workflow.flow" not in objective.ordering_dependencies assert "orchestration.workflow.flow" in objective.refresh_dependencies @@ -1299,7 +1333,7 @@ def test_variable_backed_os_allowed_values_pass_when_all_supported(self): assert "provisioner.unsupported-os-family" not in codes assert execution_plan.is_valid - def test_variable_backed_os_allowed_values_fail_when_any_are_unsupported(self): + def test_variable_backed_os_allowed_values_pass_with_nonempty_supported_intersection(self): manifest = _limited_backend_manifest( name="limited", provisioner=ProvisionerCapabilities( @@ -1327,8 +1361,8 @@ def test_variable_backed_os_allowed_values_fail_when_any_are_unsupported(self): codes = {diag.code for diag in execution_plan.diagnostics} - assert "provisioner.unsupported-os-family" in codes - assert not execution_plan.is_valid + assert "provisioner.unsupported-os-family" not in codes + assert execution_plan.is_valid def test_variable_backed_os_defaults_must_be_valid_for_nodes_os(self): with pytest.raises(SDLInstantiationError) as exc: @@ -1346,7 +1380,7 @@ def test_variable_backed_os_defaults_must_be_valid_for_nodes_os(self): ) assert "/nodes/vm/os" in str(exc.value) - def test_variable_backed_os_without_allowed_values_fails_closed_at_portable_authority(self): + def test_variable_backed_os_without_allowed_values_is_narrowed_to_feasible_authority(self): manifest = _limited_backend_manifest( name="limited", provisioner=ProvisionerCapabilities( @@ -1375,8 +1409,14 @@ def test_variable_backed_os_without_allowed_values_fails_closed_at_portable_auth assert "provisioner.os-family-validation-deferred" not in codes assert "provisioner.unsupported-os-family" not in codes - assert "realization.authority-bound-unavailable" in codes - assert not execution_plan.is_valid + assert "realization.authority-bound-unavailable" not in codes + assert execution_plan.is_valid + authority = next( + entry + for entry in execution_plan.provisioning.realization_authority + if entry.requirement_kind == "os-family" + ) + assert authority.bounds[0].domain.values == ["linux"] def test_variable_backed_os_with_undeclared_variable_fails_instantiation(self): manifest = _limited_backend_manifest( @@ -1434,7 +1474,7 @@ def test_variable_backed_counts_with_allowed_values_enforce_max_nodes(self): assert "provisioner.max-total-nodes-exceeded" in codes assert not execution_plan.is_valid - def test_imported_module_allowed_values_enforce_against_backend(self, tmp_path): + def test_imported_module_allowed_values_retain_supported_intersection(self, tmp_path): # SDL module-import composition strips imported variables from the # merged payload, so the side-channel provenance must carry both the # imported variable spec AND the imported nodes' captured refs onto @@ -1495,8 +1535,8 @@ def test_imported_module_allowed_values_enforce_against_backend(self, tmp_path): codes = {diag.code for diag in execution_plan.diagnostics} - assert "provisioner.unsupported-os-family" in codes - assert not execution_plan.is_valid + assert "provisioner.unsupported-os-family" not in codes + assert execution_plan.is_valid def test_variable_backed_counts_defaults_must_be_valid_for_infrastructure_count(self): manifest = _limited_backend_manifest( @@ -1526,11 +1566,11 @@ def test_variable_backed_counts_defaults_must_be_valid_for_infrastructure_count( ) assert "/infrastructure/vm/count" in str(exc.value) - def test_variable_backed_os_allowed_values_fail_when_pre_instantiated(self): + def test_variable_backed_os_allowed_values_retain_intersection_when_pre_instantiated(self): # Manager-path coverage: when the caller instantiates upstream and # passes an InstantiatedScenario to compile_runtime_model, the # captured-ref snapshot must still flow through so the - # allowed_values-vs-supported_os_families check fires. + # allowed_values-vs-supported_os_families intersection remains available. manifest = _limited_backend_manifest( name="limited", provisioner=ProvisionerCapabilities( @@ -1558,8 +1598,8 @@ def test_variable_backed_os_allowed_values_fail_when_pre_instantiated(self): codes = {diag.code for diag in execution_plan.diagnostics} - assert "provisioner.unsupported-os-family" in codes - assert not execution_plan.is_valid + assert "provisioner.unsupported-os-family" not in codes + assert execution_plan.is_valid def test_variable_backed_switch_count_enforces_max_nodes(self): # Switch (network) resources go through the same max_total_nodes @@ -1748,7 +1788,7 @@ def test_satcom_release_poisoning_compiles_to_valid_execution_plan(self): scenario_path = EXAMPLES_DIR / "satcom-release-poisoning.sdl.yaml" content = scenario_path.read_text(encoding="utf-8") model = compile_runtime_model(parse_sdl(content)) - execution_plan = plan(model, create_stub_manifest()) + execution_plan = plan(model, _os_capable_stub_manifest()) # Pinned counts from the satcom example. A partial regression # (e.g. half the nodes failing to compile) keeps `> 5` green; exact diff --git a/implementations/python/tests/test_sce_002_trial_compiler.py b/implementations/python/tests/test_sce_002_trial_compiler.py index 3faee89a..aa50784d 100644 --- a/implementations/python/tests/test_sce_002_trial_compiler.py +++ b/implementations/python/tests/test_sce_002_trial_compiler.py @@ -341,6 +341,13 @@ def _request(*, run_count: int = 2, sample: bool = False) -> TrialCompilationReq manifest_payload["identity"] = {"name": "backend-a", "version": "1"} manifest_payload["supported_contract_versions"].append("realization-envelope-v1") manifest_payload["realization_envelope"] = envelope.identity.model_dump(mode="json") + manifest_payload["capabilities"]["provisioner"]["operating_systems"] = [ + {"family": "linux", "distribution": "ubuntu", "versions": ["22.04"]} + ] + manifest_payload["realization_support"][0]["observation_capabilities"]["operating-system"] = { + "verification_scope": "presence", + "observation_strength": "guest-observed", + } manifest = BackendManifestV2Model.model_validate(manifest_payload) manifest_ref = ExperimentManifestReferenceModel( ref_kind="manifest", diff --git a/implementations/python/tests/test_sdl_realworld.py b/implementations/python/tests/test_sdl_realworld.py index ff50f9d5..a98e7a71 100644 --- a/implementations/python/tests/test_sdl_realworld.py +++ b/implementations/python/tests/test_sdl_realworld.py @@ -241,6 +241,7 @@ def _parse(yaml_str: str, label: str): workstation: type: compute os: windows + os_distribution: windows-client os_version: "10" resources: {ram: 4 gib, cpu: 2} services: [{port: 3389, name: rdp}] @@ -248,7 +249,8 @@ def _parse(yaml_str: str, label: str): ad-server: type: compute os: windows - os_version: "Server 2019" + os_distribution: windows-server + os_version: "2019" resources: {ram: 4 gib, cpu: 2} features: {ad-ds: admin, dns-server: admin} services: @@ -261,7 +263,8 @@ def _parse(yaml_str: str, label: str): fileshare: type: compute os: windows - os_version: "Server 2019" + os_distribution: windows-server + os_version: "2019" resources: {ram: 2 gib, cpu: 1} features: {smb-shares: admin} services: [{port: 445, name: smb}] @@ -335,7 +338,8 @@ def _parse(yaml_str: str, label: str): win2003-ad: type: compute os: windows - os_version: "Server 2003" + os_distribution: windows-server + os_version: "2003" resources: {ram: 1 gib, cpu: 1} features: {ad-ds: admin, iis-web: admin} services: @@ -347,6 +351,7 @@ def _parse(yaml_str: str, label: str): win2000-dns: type: compute os: windows + os_distribution: windows-server os_version: "2000" resources: {ram: 512 mib, cpu: 1} features: {dns-server: admin, iis-web-2: admin} @@ -358,7 +363,8 @@ def _parse(yaml_str: str, label: str): solaris-ecom: type: compute os: other - os_version: "Solaris 10" + os_distribution: solaris + os_version: "10" resources: {ram: 1 gib, cpu: 1} features: {ecom-frontend: app} services: @@ -369,7 +375,8 @@ def _parse(yaml_str: str, label: str): ubuntu-backend: type: compute os: linux - os_version: "Ubuntu 6.06" + os_distribution: ubuntu + os_version: "6.06" resources: {ram: 1 gib, cpu: 1} features: {ecom-backend: app, mysql-db: dba} services: @@ -451,7 +458,8 @@ def _parse(yaml_str: str, label: str): mssql-server: type: compute os: windows - os_version: "Server 2019" + os_distribution: windows-server + os_version: "2019" resources: {ram: 4 gib, cpu: 2} features: {mssql: dba} services: [{port: 1433, name: mssql}] @@ -461,7 +469,8 @@ def _parse(yaml_str: str, label: str): dc-corp: type: compute os: windows - os_version: "Server 2022" + os_distribution: windows-server + os_version: "2022" resources: {ram: 4 gib, cpu: 2} features: {ad-corp: admin} services: [{port: 389, name: ldap}, {port: 88, name: kerberos}, {port: 445, name: smb}] @@ -471,7 +480,8 @@ def _parse(yaml_str: str, label: str): dc-dev: type: compute os: windows - os_version: "Server 2019" + os_distribution: windows-server + os_version: "2019" resources: {ram: 4 gib, cpu: 2} features: {ad-dev: admin} services: [{port: 389, name: ldap}, {port: 88, name: kerberos}] @@ -590,7 +600,8 @@ def _parse(yaml_str: str, label: str): metasploitable: type: compute os: linux - os_version: "Ubuntu 8.04" + os_distribution: ubuntu + os_version: "8.04" resources: {ram: 512 mib, cpu: 1} features: [vsftpd, openssh, apache-web, samba-smb, mysql-db, postgres-db, unrealirc, distccd, java-rmi, tomcat-mgr, vnc-server] services: @@ -698,7 +709,8 @@ def _parse(yaml_str: str, label: str): ad-dc: type: compute os: windows - os_version: "Server 2022" + os_distribution: windows-server + os_version: "2022" resources: {ram: 4 gib, cpu: 2} features: {ad-berylia: admin} services: [{port: 389, name: ldap}, {port: 88, name: kerberos}, {port: 445, name: smb}] @@ -732,7 +744,8 @@ def _parse(yaml_str: str, label: str): hmi-server: type: compute os: windows - os_version: "Server 2008 R2" + os_distribution: windows-server + os_version: "2008 R2" resources: {ram: 2 gib, cpu: 1} description: "SCADA Human-Machine Interface" features: {scada-hmi: operator} @@ -743,7 +756,6 @@ def _parse(yaml_str: str, label: str): plc-power: type: compute os: other - os_version: "SIEMENS S7-300" resources: {ram: 256 mib, cpu: 1} description: "Power grid PLC controller" services: [{port: 102, name: s7comm}] @@ -752,7 +764,6 @@ def _parse(yaml_str: str, label: str): plc-water: type: compute os: other - os_version: "SIEMENS S7-300" resources: {ram: 256 mib, cpu: 1} description: "Water treatment PLC controller" services: [{port: 102, name: s7comm}] diff --git a/implementations/python/tests/test_sdl_stress.py b/implementations/python/tests/test_sdl_stress.py index 4a1231db..a7677fc1 100644 --- a/implementations/python/tests/test_sdl_stress.py +++ b/implementations/python/tests/test_sdl_stress.py @@ -468,6 +468,7 @@ def _parse(yaml_str: str, label: str): target: type: compute os: windows + os_distribution: windows-client os_version: "10" resources: {ram: 4 gib, cpu: 2} vulnerabilities: [lsass-access] @@ -1015,7 +1016,8 @@ def _parse(yaml_str: str, label: str): exchange: type: compute os: windows - os_version: "Server 2019" + os_distribution: windows-server + os_version: "2019" source: exchange-2019 resources: {ram: 8 gib, cpu: 4} features: [exchange-server, outlook-web] @@ -1037,7 +1039,8 @@ def _parse(yaml_str: str, label: str): dc: type: compute os: windows - os_version: "Server 2022" + os_distribution: windows-server + os_version: "2022" source: windows-server-2022 resources: {ram: 4 gib, cpu: 2} features: [ad-ds] @@ -1399,14 +1402,16 @@ def _parse(yaml_str: str, label: str): dc01: type: compute os: windows - os_version: "Server 2022" + os_distribution: windows-server + os_version: "2022" resources: {ram: 4 gib, cpu: 2} features: {ad-forest-root: admin} roles: {admin: Administrator} dc02: type: compute os: windows - os_version: "Server 2022" + os_distribution: windows-server + os_version: "2022" resources: {ram: 4 gib, cpu: 2} features: {ad-child-domain: admin} roles: {admin: Administrator} diff --git a/implementations/python/tests/test_sem_218_realization.py b/implementations/python/tests/test_sem_218_realization.py index 382462af..44f415d5 100644 --- a/implementations/python/tests/test_sem_218_realization.py +++ b/implementations/python/tests/test_sem_218_realization.py @@ -15,10 +15,21 @@ from raes import instantiate_scenario, parse_sdl from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance -from raes_backend_protocols.capabilities import BackendManifest, ProvisionerCapabilities -from raes_backend_stubs.stubs import create_stub_manifest -from raes_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration -from raes_contracts.vocabulary import RealizationSupportMode +from raes_backend_protocols.capabilities import ( + BackendManifest, + OperatingSystemCompatibility, + ProvisionerCapabilities, +) +from raes_contracts.apparatus import ( + ConceptBinding, + RealizationObservationCapability, + RealizationSupportDeclaration, +) +from raes_contracts.vocabulary import ( + ObservationStrength, + RealizationSupportMode, + RealizationVerificationScope, +) from raes_processor.compiler import compile_runtime_model from raes_processor.planner import plan @@ -71,6 +82,35 @@ def _manifest( name="realization-gate-provisioner", supported_node_types=node_types, supported_os_families=os_families, + operating_systems=tuple( + OperatingSystemCompatibility( + family=family, + distribution=("ubuntu" if family == "linux" else "windows-server"), + versions=frozenset({"22.04" if family == "linux" else "2022"}), + ) + for family in sorted(os_families & {"linux", "windows"}) + ), + ), + ) + + +def _supporting_manifest(*, constrained: bool = False) -> BackendManifest: + return _manifest( + os_families=frozenset({"linux", "windows"}) if constrained else frozenset({"linux"}), + realization_support=( + RealizationSupportDeclaration( + domain="runtime-realization", + support_mode=RealizationSupportMode.CONSTRAINED, + supported_constraint_kinds=frozenset({"node-type", "os-family"}), + supported_exact_requirement_kinds=frozenset({"declared-capability-match"}), + disclosure_kinds=frozenset({"runtime-snapshot-v1"}), + observation_capabilities={ + "operating-system": RealizationObservationCapability( + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + ) + }, + ), ), ) @@ -150,7 +190,7 @@ def test_planner_accepts_exact_declaration_when_backend_supports_it(): """The stub manifest declares declared-capability-match, so an exact declaration plans without a realization diagnostic.""" - execution_plan = plan(compile_runtime_model(_scenario(_EXACT_SCENARIO)), create_stub_manifest()) + execution_plan = plan(compile_runtime_model(_scenario(_EXACT_SCENARIO)), _supporting_manifest()) codes = {diag.code for diag in execution_plan.diagnostics} assert not any(code.startswith("realization.") for code in codes) @@ -192,7 +232,10 @@ def test_planner_rejects_unsupported_constraint_kind(): def test_planner_accepts_constrained_declaration_when_backend_supports_it(): """Constrained (and open) declarations plan as before against a supporting backend.""" - execution_plan = plan(compile_runtime_model(_scenario(_CONSTRAINED_SCENARIO)), create_stub_manifest()) + execution_plan = plan( + compile_runtime_model(_scenario(_CONSTRAINED_SCENARIO)), + _supporting_manifest(constrained=True), + ) codes = {diag.code for diag in execution_plan.diagnostics} assert not any(code.startswith("realization.") for code in codes) diff --git a/implementations/python/tests/test_sem_218_realization_designation.py b/implementations/python/tests/test_sem_218_realization_designation.py index 69559abf..1a8ce31f 100644 --- a/implementations/python/tests/test_sem_218_realization_designation.py +++ b/implementations/python/tests/test_sem_218_realization_designation.py @@ -21,7 +21,11 @@ resolve_json_pointer_surface, ) from raes_backend_libvirt.manifest import create_libvirt_manifest -from raes_backend_protocols.capabilities import BackendManifest, ProvisionerCapabilities +from raes_backend_protocols.capabilities import ( + BackendManifest, + OperatingSystemCompatibility, + ProvisionerCapabilities, +) from raes_contracts.apparatus import ( ConceptBinding, ProcessResourceLimitCapability, @@ -54,6 +58,8 @@ _OPEN_NODE_REALIZATION_SUFFIXES = { "os", + "os_distribution", + "os_version", "architecture", "runtime.environment", "runtime.mounts", @@ -111,6 +117,10 @@ def _manifest(mode: RealizationSupportMode) -> BackendManifest: supported_exact_requirement_kinds=frozenset({"declared-capability-match"}), disclosure_kinds=frozenset({"runtime-snapshot-v1"}), observation_capabilities={ + "operating-system": RealizationObservationCapability( + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + ), "process-resource-limits": RealizationObservationCapability( verification_scope=RealizationVerificationScope.CONFIGURATION, observation_strength=ObservationStrength.GUEST_OBSERVED, @@ -124,6 +134,7 @@ def _manifest(mode: RealizationSupportMode) -> BackendManifest: name="designation-test", supported_node_types=frozenset({"compute"}), supported_os_families=frozenset({"linux"}), + operating_systems=(OperatingSystemCompatibility("linux", "ubuntu", frozenset({"22.04"})),), ), realization_envelope=_envelope_with_process_limit_capability(envelope, capability), ) @@ -178,7 +189,16 @@ def _open_manifest_with_envelope(*, path: str, value: str) -> BackendManifest: }, process_resource_limits=(capability,), ) - return replace(manifest, realization_support=(support,), realization_envelope=envelope) + provisioner = replace( + manifest.provisioner, + operating_systems=(OperatingSystemCompatibility("linux", "ubuntu", frozenset({"22.04"})),), + ) + return replace( + manifest, + realization_support=(support,), + capabilities=replace(manifest.capabilities, provisioner=provisioner), + realization_envelope=envelope, + ) def test_root_open_is_typed_and_carried_to_compilation(): @@ -429,10 +449,16 @@ def test_root_delegation_uses_injected_selected_apparatus_default(): def test_delegated_open_posture_is_materialized_through_runtime_disclosure(): + from raes_contracts.realization_observation import ( + ObservedOperatingSystemIdentity, + RealizationObservationDisclosure, + ) + + manifest = _manifest(RealizationSupportMode.OPEN_REALIZATION) model = compile_runtime_model(_scenario("realization:\n default: unspecified")) execution = plan( model, - _manifest(RealizationSupportMode.OPEN_REALIZATION), + manifest, apparatus_realization_default=lambda _requirement, _manifest: Closure.OPEN_WORLD, ) requirement = _requirement(execution.model, "nodes.web.os") @@ -447,10 +473,30 @@ def test_delegated_open_posture_is_materialized_through_runtime_disclosure(): if operation.action is not ChangeAction.DELETE } + os_observation = RealizationObservationDisclosure( + address=requirement.address, + field_path="nodes.web.operating-system", + domain=requirement.domain, + requirement_kind="operating-system", + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + operating_system=ObservedOperatingSystemIdentity( + family="linux", + distribution="ubuntu", + version="22.04", + ), + operation_id="op-open-os-1", + envelope_digest="sha256:" + "a" * 64, + configuration_digest="sha256:" + "b" * 64, + observer_version="guest-os-release/v1", + sequence=1, + binding_verified=True, + ) diagnostics, provenance = realization_disclosure( (requirement,), execution.provisioning, - RuntimeSnapshot(entries=entries), + RuntimeSnapshot(entries=entries, realization_observations=(os_observation,)), + manifest=manifest, ) assert execution.is_valid @@ -486,6 +532,11 @@ def test_open_request_uses_envelope_subsumption_only_for_open_concern_paths(): def test_backend_realized_open_slot_discloses_governing_scope_through_api(): + from raes_contracts.realization_observation import ( + ObservedOperatingSystemIdentity, + RealizationObservationDisclosure, + ) + model = compile_runtime_model(_scenario("realization:\n default: open")) requirement = _requirement(model, "nodes.web.os") plan_payload = ProvisioningPlan( @@ -506,10 +557,36 @@ def test_backend_realized_open_slot_discloses_governing_scope_through_api(): resource_type="node", payload={"node_kind": "compute", "os_family": "linux"}, ) - } + }, + realization_observations=( + RealizationObservationDisclosure( + address=requirement.address, + field_path="nodes.web.operating-system", + domain=requirement.domain, + requirement_kind="operating-system", + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + operating_system=ObservedOperatingSystemIdentity( + family="linux", + distribution="ubuntu", + version="22.04", + ), + operation_id="op-open-api-1", + envelope_digest="sha256:" + "a" * 64, + configuration_digest="sha256:" + "b" * 64, + observer_version="guest-os-release/v1", + sequence=1, + binding_verified=True, + ), + ), ) - diagnostics, provenance = realization_disclosure((requirement,), plan_payload, snapshot) + diagnostics, provenance = realization_disclosure( + (requirement,), + plan_payload, + snapshot, + manifest=_manifest(RealizationSupportMode.OPEN_REALIZATION), + ) delivered = _snapshot_model(RuntimeSnapshotEnvelope(snapshot=RuntimeSnapshot(realization_provenance=provenance))) assert diagnostics == [] diff --git a/implementations/python/tests/test_sem_218_runtime_realization.py b/implementations/python/tests/test_sem_218_runtime_realization.py index aaff745a..edc79131 100644 --- a/implementations/python/tests/test_sem_218_runtime_realization.py +++ b/implementations/python/tests/test_sem_218_runtime_realization.py @@ -23,14 +23,21 @@ from raes import parse_sdl from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance +from raes_backend_protocols.capabilities import OperatingSystemCompatibility from raes_backend_stubs.manifest import load_stub_realization_envelope from raes_backend_stubs.stubs import StubProvisioner, create_stub_target +from raes_contracts.apparatus import RealizationObservationCapability +from raes_contracts.realization_observation import ( + ObservedOperatingSystemIdentity, + RealizationObservationDisclosure, +) from raes_contracts.runtime_state import ( ApplyResult, RealizationProvenanceEntry, RuntimeSnapshot, ) from raes_contracts.versions import RUNTIME_SNAPSHOT_SCHEMA_VERSION +from raes_contracts.vocabulary import ObservationStrength, RealizationVerificationScope from raes_runtime.control_plane_store import _snapshot_from_payload, _snapshot_payload from raes_runtime.manager import RuntimeManager from raes_runtime.registry import RuntimeTarget @@ -62,9 +69,34 @@ def _plan_exact(manager: RuntimeManager): def _target_with_provisioner(provisioner) -> RuntimeTarget: base = create_stub_target() + manifest = replace( + base.manifest, + realization_support=( + replace( + base.manifest.realization_support[0], + observation_capabilities={ + **base.manifest.realization_support[0].observation_capabilities, + "operating-system": RealizationObservationCapability( + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + ), + }, + ), + ), + capabilities=replace( + base.manifest.capabilities, + provisioner=replace( + base.manifest.provisioner, + operating_systems=( + OperatingSystemCompatibility("linux", "ubuntu", frozenset({"22.04"})), + OperatingSystemCompatibility("windows", "windows-server", frozenset({"2022"})), + ), + ), + ), + ) return RuntimeTarget( name=base.name, - manifest=base.manifest, + manifest=manifest, provisioner=provisioner, orchestrator=base.orchestrator, evaluator=base.evaluator, @@ -73,33 +105,63 @@ def _target_with_provisioner(provisioner) -> RuntimeTarget: ) -class _WeakeningProvisioner: - """A provisioner that silently downgrades an exact ``os`` declaration. +class _ObservedProvisioner: + """In-memory provisioner that supplies one typed guest OS identity.""" - It honours the plan structurally (delegating to the reference stub) but - realizes the exact ``linux`` os family as the weaker ``other`` sentinel — - the silent-approximation failure mode the runtime gate must reject. - """ + def __init__(self, *, family: str = "linux", include_observation: bool = True) -> None: + self._family = family + self._include_observation = include_observation def validate(self, plan) -> list: return [] def apply(self, plan, snapshot: RuntimeSnapshot) -> ApplyResult: - honest = StubProvisioner(load_stub_realization_envelope()).apply(plan, snapshot) - entries = dict(honest.snapshot.entries) - for address, entry in entries.items(): - if entry.payload.get("os_family") == "linux": - weakened = dict(entry.payload) - weakened["os_family"] = "other" - entries[address] = replace(entry, payload=weakened) - return ApplyResult( - success=True, - snapshot=honest.snapshot.with_entries(entries), - changed_addresses=honest.changed_addresses, + result = StubProvisioner(load_stub_realization_envelope()).apply(plan, snapshot) + if not self._include_observation: + return result + operation = next(operation for operation in plan.operations if operation.resource_type == "node") + distribution, version = ("ubuntu", "22.04") if self._family == "linux" else ("windows-server", "2022") + observation = RealizationObservationDisclosure( + address=operation.address, + field_path="nodes.web.operating-system", + domain="runtime-realization", + requirement_kind="operating-system", + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.GUEST_OBSERVED, + operating_system=ObservedOperatingSystemIdentity( + family=self._family, + distribution=distribution, + version=version, + ), + operation_id=plan.operation_id, + envelope_digest="sha256:" + "a" * 64, + configuration_digest="sha256:" + "b" * 64, + observer_version="test-guest-os-release/v1", + sequence=1, + binding_verified=True, + ) + return replace( + result, + snapshot=result.snapshot.with_entries( + result.snapshot.entries, + realization_observations=(*result.snapshot.realization_observations, observation), + ), ) -class _OmittingProvisioner: +class _WeakeningProvisioner(_ObservedProvisioner): + """A provisioner that silently downgrades an exact ``os`` declaration. + + It honours the plan structurally (delegating to the reference stub) but + realizes the exact ``linux`` os family as the weaker ``other`` sentinel — + the silent-approximation failure mode the runtime gate must reject. + """ + + def __init__(self) -> None: + super().__init__(family="windows") + + +class _OmittingProvisioner(_ObservedProvisioner): """A provisioner that realizes the node but silently omits its exact ``os``. It drops the ``os_family`` key from the realized entry payload — the @@ -108,21 +170,8 @@ class _OmittingProvisioner: an unrealized exact declaration as an I2 violation, not a non-event. """ - def validate(self, plan) -> list: - return [] - - def apply(self, plan, snapshot: RuntimeSnapshot) -> ApplyResult: - honest = StubProvisioner(load_stub_realization_envelope()).apply(plan, snapshot) - entries = dict(honest.snapshot.entries) - for address, entry in entries.items(): - if "os_family" in entry.payload: - stripped = {key: value for key, value in entry.payload.items() if key != "os_family"} - entries[address] = replace(entry, payload=stripped) - return ApplyResult( - success=True, - snapshot=honest.snapshot.with_entries(entries), - changed_addresses=honest.changed_addresses, - ) + def __init__(self) -> None: + super().__init__(include_observation=False) def test_runtime_gate_rejects_silently_weakened_exact_value(): @@ -172,7 +221,7 @@ def test_runtime_gate_rejects_silently_omitted_exact_value(): def test_honest_apply_records_realization_provenance(): """I5: an honoured exact realization is recorded as author-declared/exact.""" - manager = RuntimeManager(create_stub_target()) + manager = RuntimeManager(_target_with_provisioner(_ObservedProvisioner())) result = manager.apply(_plan_exact(manager)) assert result.success @@ -187,7 +236,7 @@ def test_honest_apply_records_realization_provenance(): def test_honoured_parameter_substitution_records_processor_derived_provenance(): """I5: an honoured substituted value retains its processor-derived origin.""" - manager = RuntimeManager(_target_with_provisioner(create_stub_target().provisioner)) + manager = RuntimeManager(_target_with_provisioner(_ObservedProvisioner())) plan = manager.plan(parse_sdl(textwrap.dedent(_PARAMETERIZED_SCENARIO))) result = manager.apply(plan) diff --git a/tools/check_formal_semantic_validation.py b/tools/check_formal_semantic_validation.py index 8f693837..e61e9980 100644 --- a/tools/check_formal_semantic_validation.py +++ b/tools/check_formal_semantic_validation.py @@ -166,11 +166,11 @@ def __call__(self, repo_root: Path, test_refs: list[str]) -> tuple[bool, str]: . ), "compile-repeatability-control": ( "23b9d84fa757bd80436357ed52569b5445b0e4161641598e4b15c3b18cf6e668", - "0c301c3abc83d7c510b7f2a822521efcc3930c0b442cf6c36d345342306e7b55", + "4bb77034a8f2b1a577700ad03772a80acc0f4515a6831c8a35ac1bf50482d760", ), "compile-non-vacuity-control": ( "2e92bdb90a218c29201312052b64b7fb88e8a65e887f05168e2273d9710a5080", - "c5a892924200a197dccaffefbfd8c90186f419c8f258ef9706e1651276226706", + "6cdc44529a87fb9addaf4040795c7f9ae702c5f6ae30e29a5086ee60072ded73", ), } _HISTORICAL_VM_REPLAY_INPUTS = { @@ -212,7 +212,7 @@ def __call__(self, repo_root: Path, test_refs: list[str]) -> tuple[bool, str]: . _MIGRATED_PRODUCTION_EVIDENCE_DIGESTS = { "finite-domain-satisfiable-v2": ( "sha256:03925bfe0b209c3c77069c97061aa63e8795389be7ed7b78376020b7dc87853c", - "sha256:fc049a7288a9ad3b6fcc1f37f181cb3625db7ad79e57b04fa09dec12a01bf839", + "sha256:60495371aecdd9dff463726e54af424359e09429f8283cd31c1de847bbc38cba", ), "finite-domain-unsatisfiable-v2": ( "sha256:c2dc067c406ee9c26837e9565b6b52f8a6268e06e95dbc5937a455700b0c8109", @@ -220,11 +220,11 @@ def __call__(self, repo_root: Path, test_refs: list[str]) -> tuple[bool, str]: . ), "typed-exploit-path-valid-v2": ( "sha256:0683b55cd2a52ba626bb5cfbf10de109798d8d31ba467aabd13d4930df204798", - "sha256:e621c00851c9065288f34dd316ea039d6600340cc1f8ff993091813451770096", + "sha256:00a7d75ddaf8e21fb82de2ecbff3dfafc29660d0e60829610fcb607d3da5ef0f", ), "typed-exploit-path-invalid-v2": ( "sha256:1ec2ff4423088ad2ac6328aba7fbced5cd89b1569cff057e44ad30ef5c5befc0", - "sha256:e8f0619f48501384e48e59b66e08a8478c80e0341ad9e9d2015e89aab7e6896a", + "sha256:74db3e5df9c19fe7a9a203ad3440656229af9df56164d63ec90f0c55e0aab8f2", ), } _RENAMED_SOLVER_CONFIGURATION_DIGEST = ( diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index ba49d53d..4b2d48b1 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -1524,7 +1524,7 @@ "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 19, - "content_sha256": "e6af364cce3e689de2d7af10ea8e783fcc86d4c13333f09302807d0391ef3199" + "content_sha256": "6071efbcb9ff2c713a5f4d5a61f421b8adbcf217591ade1e146e4d222fa0b077" }, { "path": "docs/research/formal-semantic-validation/execution-snapshot-v1.json", @@ -1720,42 +1720,42 @@ "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 5, - "content_sha256": "e3c106de9c2f7fb70113a8400b48b13a4be9c6073bfdae585525ae3610962075" + "content_sha256": "4d2f8580b5be899430190d9acedb0ac1e74d4eb0a7c306aba8f00f2162914744" }, { "path": "docs/research/specification-coverage/analysis-v1.json", "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 5, - "content_sha256": "4960c27b6b9bddda77c0c4d830eddb8c181544400d83eeb2293dbea4764bf38a" + "content_sha256": "2726197378910018f62a64e63aae09d4799161acf999ddff971c47dd19f61e0b" }, { "path": "docs/research/specification-coverage/bundles/\u0061ces-standardized-specification-coverage-8bf12ee-v1.json", "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 1, - "content_sha256": "51513e923425510585f0507ce84e979435318b5832b5a0531c9d2e76cc2504dd" + "content_sha256": "975a9daaf1a4673b5ddfbf92c0c498562c1f3e129a0811babf39660ebbdd9a14" }, { "path": "docs/research/specification-coverage/bundles/\u0061ces-standardized-specification-coverage-9347f64-v1.json", "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 1, - "content_sha256": "dbdec80e4f1f9e47dbd6b0a46e94513639647c1054791de387293d8e300fa713" + "content_sha256": "4020a1d56c7fe2831cec59ea64a12bbda9d38ccd94f93b916dd90f1a28f17fcb" }, { "path": "docs/research/specification-coverage/execution-snapshot-v1.1.json", "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 13, - "content_sha256": "281e69e3b7dee7ad1fffa4dd1b20e7ad9d6a5a222a802d5c327af03ceac3a1bc" + "content_sha256": "a05cc0d9a0ee221769d2dee1bc480819ab109476e67289d24254c2483cbad659" }, { "path": "docs/research/specification-coverage/execution-snapshot-v1.json", "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", "occurrences": 13, - "content_sha256": "1cca169b829d841d36f71c49c0dbb8e59a0ea70f83eb809325087aeb06f7e387" + "content_sha256": "da1e41eed08512909328a267607577526787f1956d800ba9fcfff707f2bec62b" }, { "path": "docs/research/specification-coverage/index.md",