diff --git a/Makefile b/Makefile index d714246..fb545fe 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: validate operations-decision-smoke operations-decision-api-smoke product-suite-policy-validate model-fabric-release-policy-validate +.PHONY: validate operations-decision-smoke operations-decision-api-smoke product-suite-policy-validate model-fabric-release-policy-validate lattice-data-governai-policy-subjects-validate -validate: operations-decision-smoke operations-decision-api-smoke product-suite-policy-validate model-fabric-release-policy-validate +validate: operations-decision-smoke operations-decision-api-smoke product-suite-policy-validate model-fabric-release-policy-validate lattice-data-governai-policy-subjects-validate @echo "OK: validate" operations-decision-smoke: @@ -16,3 +16,6 @@ product-suite-policy-validate: model-fabric-release-policy-validate: python3 tools/validate_model_fabric_release_policy.py + +lattice-data-governai-policy-subjects-validate: + python3 tools/validate_lattice_data_governai_policy_subjects.py diff --git a/examples/lattice-data-governai-policy-subjects.example.json b/examples/lattice-data-governai-policy-subjects.example.json new file mode 100644 index 0000000..8488b75 --- /dev/null +++ b/examples/lattice-data-governai-policy-subjects.example.json @@ -0,0 +1,137 @@ +{ + "apiVersion": "policy.socioprophet.dev/v1", + "kind": "LatticeDataGovernAIPolicySubjectPack", + "metadata": { + "name": "lattice-data-governai-policy-subjects", + "version": "0.1.0", + "description": "Policy subject pack for Lattice data, runtime, notebook, model, prompt, agent, evaluation, factsheet, and publication assets." + }, + "spec": { + "sourceOfTruth": "SocioProphet/policy-fabric", + "trackingRefs": [ + "SocioProphet/prophet-platform#291", + "SocioProphet/prophet-platform#292", + "SourceOS-Linux/sourceos-spec#75", + "SocioProphet/lattice-forge#10", + "SocioProphet/prophet-platform-fabric-mlops-ts-suite#33" + ], + "subjectKinds": [ + "DataProduct", + "DataContract", + "RuntimeAsset", + "NotebookSession", + "QueryRun", + "ModelAsset", + "ModelZooEntry", + "PromptAsset", + "RAGPipeline", + "AgentAsset", + "EvaluationBundle", + "Factsheet", + "PublicationArtifact", + "ResearchPackage" + ], + "actions": [ + "discover", + "request-access", + "query", + "launch-runtime", + "run-notebook", + "run-ray-job", + "run-beam-pipeline", + "promote-model", + "promote-prompt", + "publish-research-package", + "execute-agent", + "export-artifact" + ], + "promotionGates": { + "dataProductAccess": [ + "ownerRef-present", + "dataContract-present", + "qualityProfile-present", + "licensePolicy-present", + "policyRef-present", + "evidenceRef-present" + ], + "runtimeEligibility": [ + "runtimeAssetRef-present", + "lockfile-or-buildRef-present", + "sbomRef-present", + "signatureRef-present", + "scanRef-present" + ], + "modelPromotion": [ + "trainingData-lineage-present", + "evaluationData-lineage-present", + "EvaluationBundle-present", + "Factsheet-present", + "approvalWorkflow-present" + ], + "publicationReview": [ + "PublicationArtifact-present", + "reproductionRecipe-present", + "ReviewThread-present", + "evidenceRefs-present" + ], + "agentExecution": [ + "AgentAsset-present", + "tool-scope-present", + "runtime-isolation-present", + "policyRef-present", + "replay-evidence-required" + ] + }, + "decisions": [ + { + "decisionId": "decision:lattice:data-product-query:allow", + "subjectRef": "urn:srcos:data-product:community_truth_demo", + "subjectKind": "DataProduct", + "action": "query", + "result": "allow", + "because": [ + "dataContract-present", + "qualityProfile-present", + "queryAllowed=true", + "policyRef-present" + ], + "evidenceRefs": ["urn:srcos:evidence:community_truth_demo_data_product"] + }, + { + "decisionId": "decision:lattice:model-promotion:review", + "subjectRef": "urn:srcos:model:community_truth_demo_candidate", + "subjectKind": "ModelAsset", + "action": "promote-model", + "result": "review-required", + "because": [ + "EvaluationBundle.verdict=needs-review", + "Factsheet.approval.state=needs-review", + "riskTier=medium" + ], + "evidenceRefs": ["urn:srcos:evidence:community_truth_demo_model_eval"] + }, + { + "decisionId": "decision:lattice:publication-export:deny", + "subjectRef": "urn:srcos:publication-artifact:community_truth_demo_report", + "subjectKind": "PublicationArtifact", + "action": "export-artifact", + "result": "deny", + "because": [ + "review.state=under-review", + "export requires accepted publication state", + "policyRef missing for export grant" + ], + "evidenceRefs": ["urn:srcos:evidence:community_truth_demo_report"] + } + ], + "nonNegotiables": { + "noParallelMetadataSpines": true, + "canonicalSchemaOwner": "SourceOS-Linux/sourceos-spec", + "platformRecordProducer": "SocioProphet/prophet-platform", + "executionConsumer": "SocioProphet/agentplane", + "searchConsumer": "SocioProphet/sherlock-search", + "topicConsumer": "SocioProphet/slash-topics", + "semanticMembraneConsumer": "SocioProphet/new-hope" + } + } +} diff --git a/tools/validate_lattice_data_governai_policy_subjects.py b/tools/validate_lattice_data_governai_policy_subjects.py new file mode 100644 index 0000000..bc9e9d8 --- /dev/null +++ b/tools/validate_lattice_data_governai_policy_subjects.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +EXAMPLE = ROOT / "examples" / "lattice-data-governai-policy-subjects.example.json" + +REQUIRED_SUBJECTS = { + "DataProduct", + "DataContract", + "RuntimeAsset", + "NotebookSession", + "QueryRun", + "ModelAsset", + "ModelZooEntry", + "PromptAsset", + "RAGPipeline", + "AgentAsset", + "EvaluationBundle", + "Factsheet", + "PublicationArtifact", + "ResearchPackage", +} +REQUIRED_ACTIONS = { + "discover", + "request-access", + "query", + "launch-runtime", + "run-notebook", + "run-ray-job", + "run-beam-pipeline", + "promote-model", + "promote-prompt", + "publish-research-package", + "execute-agent", + "export-artifact", +} +REQUIRED_GATES = { + "dataProductAccess", + "runtimeEligibility", + "modelPromotion", + "publicationReview", + "agentExecution", +} +REQUIRED_RESULTS = {"allow", "deny", "review-required"} + + +def fail(message: str) -> int: + print(f"ERROR: {message}", file=sys.stderr) + return 1 + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValueError(message) + + +def main() -> int: + if not EXAMPLE.exists(): + return fail(f"missing {EXAMPLE}") + try: + data = json.loads(EXAMPLE.read_text(encoding="utf-8")) + require(data.get("apiVersion") == "policy.socioprophet.dev/v1", "apiVersion must be policy.socioprophet.dev/v1") + require(data.get("kind") == "LatticeDataGovernAIPolicySubjectPack", "kind must be LatticeDataGovernAIPolicySubjectPack") + metadata = data.get("metadata") + require(isinstance(metadata, dict), "metadata must be object") + require(metadata.get("name") == "lattice-data-governai-policy-subjects", "metadata.name mismatch") + spec = data.get("spec") + require(isinstance(spec, dict), "spec must be object") + + missing_subjects = sorted(REQUIRED_SUBJECTS - set(spec.get("subjectKinds", []))) + require(not missing_subjects, f"missing subject kinds: {missing_subjects}") + missing_actions = sorted(REQUIRED_ACTIONS - set(spec.get("actions", []))) + require(not missing_actions, f"missing actions: {missing_actions}") + gates = spec.get("promotionGates") + require(isinstance(gates, dict), "promotionGates must be object") + missing_gates = sorted(REQUIRED_GATES - set(gates)) + require(not missing_gates, f"missing promotion gates: {missing_gates}") + for gate_name, requirements in gates.items(): + require(isinstance(requirements, list) and requirements, f"promotionGates.{gate_name} must be non-empty list") + + decisions = spec.get("decisions") + require(isinstance(decisions, list) and len(decisions) >= 3, "decisions must include at least three examples") + result_set = set() + for decision in decisions: + require(isinstance(decision, dict), "decision entries must be objects") + for key in ["decisionId", "subjectRef", "subjectKind", "action", "result", "because", "evidenceRefs"]: + require(key in decision, f"decision missing {key}") + require(decision["subjectKind"] in REQUIRED_SUBJECTS, f"unknown decision subjectKind {decision['subjectKind']}") + require(decision["action"] in REQUIRED_ACTIONS, f"unknown decision action {decision['action']}") + require(decision["result"] in REQUIRED_RESULTS, f"unknown decision result {decision['result']}") + require(isinstance(decision["because"], list) and decision["because"], "decision.because must be non-empty") + require(isinstance(decision["evidenceRefs"], list) and decision["evidenceRefs"], "decision.evidenceRefs must be non-empty") + result_set.add(decision["result"]) + missing_results = sorted(REQUIRED_RESULTS - result_set) + require(not missing_results, f"missing decision result examples: {missing_results}") + + non_negotiables = spec.get("nonNegotiables") + require(isinstance(non_negotiables, dict), "nonNegotiables must be object") + require(non_negotiables.get("noParallelMetadataSpines") is True, "noParallelMetadataSpines must be true") + require(non_negotiables.get("canonicalSchemaOwner") == "SourceOS-Linux/sourceos-spec", "canonicalSchemaOwner mismatch") + except Exception as exc: # noqa: BLE001 + return fail(str(exc)) + + print("OK: validated Lattice Data/GovernAI policy-subject pack") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())