diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e59628873..f06b150bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v8 - name: Restore pinned Isabelle archive - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .cache/raes-sdl/tooling/archives/Isabelle2025-2_linux.tar.gz key: isabelle-linux-x86-64-2025-2-a20a507bc7c1270d diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 5a92da18e..e0a407fc6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -113,7 +113,7 @@ jobs: PY - name: Publish to PyPI (OIDC trusted publishing) - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 with: packages-dir: dist diff --git a/contracts/README.md b/contracts/README.md index d9a23ebf4..782661371 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -68,6 +68,18 @@ participant-local scope, audience scope, observation point, governed source layers, transformation rule, evidence/provenance basis, semantic limitations, and explicit comparability disclosure. +The participant-runtime `participant-information-state-record-v1` contract +closes ACT-604's portable information-state reference at one exact participant, +episode, state cut, projection, memory scope, and information guarantee. Strong +claims resolve through the immutable +`participant-information-reconstruction-profile-v1` corpus and preserve every +visible occurrence as a typed source relation. Conformance, snapshot reload, +and backend ingestion fail closed unless a trusted resolver supplies the +occurrence history, typed sources, proof digest, exact-cut membership, and +projection/policy coordinates needed by the contextual invariant. The record +is not a mutable fact map, hidden-world snapshot, or operational-holdings +taxonomy; ACT-615 remains the owner of concrete holdings kinds and lifecycle. + The participant-runtime `runtime-fact-binding-plane-v1` contract defines typed run-local fact declarations, immutable versions, compiled late-bound action sinks, value-free binding provenance, and redacted participant/workflow diff --git a/contracts/concept-authority/behavioral-relations-v1.json b/contracts/concept-authority/behavioral-relations-v1.json index eac6cc835..ce7beaeb4 100644 --- a/contracts/concept-authority/behavioral-relations-v1.json +++ b/contracts/concept-authority/behavioral-relations-v1.json @@ -1,7 +1,7 @@ { "schema_version": "behavioral-relations/v1", "taxonomy_id": "raes-behavioral-relations", - "taxonomy_revision": "rev9", + "taxonomy_revision": "rev12", "bibliography": [ { "source_id": "park-1981", @@ -153,6 +153,67 @@ "value": "10.3233/JCS-2009-0352" } }, + { + "source_id": "denning-1976", + "title": "A Lattice Model of Secure Information Flow", + "authors": [ + "Dorothy E. Denning" + ], + "publication_year": 1976, + "publication_venue": "Communications of the ACM 19(5), 236-243", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/360051.360056" + } + }, + { + "source_id": "myers-liskov-1998", + "title": "Complete, Safe Information Flow with Decentralized Labels", + "authors": [ + "Andrew C. Myers", + "Barbara Liskov" + ], + "publication_year": 1998, + "publication_venue": "1998 IEEE Symposium on Security and Privacy, 186-197", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/SECPRI.1998.674834" + } + }, + { + "source_id": "myers-sabelfeld-zdancewic-2006", + "title": "Enforcing Robust Declassification and Qualified Robustness", + "authors": [ + "Andrew C. Myers", + "Andrei Sabelfeld", + "Steve Zdancewic" + ], + "publication_year": 2006, + "publication_venue": "Journal of Computer Security 14(2), 157-196", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/JCS-2006-14203" + } + }, + { + "source_id": "cecchetti-myers-arden-2017", + "title": "Nonmalleable Information Flow Control", + "authors": [ + "Ethan Cecchetti", + "Andrew C. Myers", + "Owen Arden" + ], + "publication_year": 2017, + "publication_venue": "Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/3133956.3134054" + } + }, { "source_id": "lynch-tuttle-1989", "title": "An Introduction to Input/Output Automata", @@ -2001,7 +2062,9 @@ "The participant-opacity finite-state checker derives the complete reachable fixed point from an exact transition model, checks every reachable secret evaluation point, and binds catalog, profile, model, assumptions, explored coverage, tool version, result or safe counterexample, and replay evidence.", "The committed model-check input and evidence fixtures retain the exact positive baseline model, result, digests, complete coverage, tool identity, and explicit nonclaims; invalid fixtures exercise count and partial-result promotion failures.", "implementations/python/tests/test_issue_962_participant_opacity_model_check.py covers pair-probe incompleteness, supervisor behavior, active strategies, coalition fusion, retained memory, release changes, order and probability non-promotion, exact bounds, replay, and agreement with the bounded lane.", - "The Isabelle/HOL Participant_Opacity session kernel-checks the SEM-231 one-sided opacity definition, its information-cell knowledge characterization, and the conditional implication from a matching SEM-230 noninterference instance for an eligible predicate; checked countermodels preserve the invalid-promotion boundaries." + "The Isabelle/HOL Participant_Opacity session kernel-checks the SEM-231 one-sided opacity definition, its information-cell knowledge characterization, and the conditional implication from a matching SEM-230 noninterference instance for an eligible predicate; checked countermodels preserve the invalid-promotion boundaries.", + "The participant-opacity-runtime-reference-v1 profile and RUN-319 crossing boundary enforce one exact finite observation inventory with safe, atomic runtime-enforcement decision bindings." + ,"The reference backend declares bounded support for the exact runtime profile; generic target conformance separately observes backend-native realization across the protected and complement points and binds the manifest, profile, configuration, tool, environment, and probe-set digests." ], "explicit_non_claims": [ "Relation definition, catalog validation, claim-profile binding, and bounded finite analysis do not establish opacity of RAES, RUN-319, or any backend outside the exact admitted artifact.", @@ -2009,6 +2072,8 @@ "Taxonomy revision rev7 adds only an in-process bounded-test checker; it does not add a model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", "Taxonomy revision rev8 adds one exact finite-state model-check result; it does not add a mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", "Taxonomy revision rev9 adds only the abstract conditional mathematical theorem bound to participant-opacity-theorem-v1; it does not prove opacity of RAES, a runtime, a deployment, a backend, or the finite fixture profile.", + "Taxonomy revision rev10 adds partial runtime enforcement only for participant-opacity-runtime-reference-v1; it does not establish general opacity, live model checking or proof, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev11 adds one bounded reference-backend declaration, realization, and conformance lane; it does not establish universal opacity, proof of a live execution, native realization for other backends, or cross-backend equivalence.", "Bounded evidence authenticates only the normalized-input digest; it does not authenticate a claimed source artifact or materializer.", "Opacity of one predicate does not imply SEM-230 policy noninterference, projected-history equivalence, epistemic indistinguishability of two selected worlds, trace inclusion or equivalence, simulation, refinement, or strong or weak bisimulation.", "The possibilistic baseline makes no posterior-risk, entropy, probabilistic, differential-privacy, timed, progress-sensitive, or universal partial-order claim." @@ -2025,10 +2090,10 @@ "proof_status": "proved", "checker_status": "implemented", "model_check_status": "model-checked", - "runtime_enforcement_status": "not-enforced", - "backend_declaration_status": "not-declared", - "backend_realization_status": "not-realized", - "backend_conformance_status": "not-tested", + "runtime_enforcement_status": "partial", + "backend_declaration_status": "declared", + "backend_realization_status": "partial", + "backend_conformance_status": "bounded", "evidence_refs": [ "docs/decisions/adrs/adr-099-participant-relative-predicate-opacity.md", "specs/formal/participant-semantics/participant-predicate-opacity.md", @@ -2047,7 +2112,14 @@ "specs/formal/participant-semantics/isabelle/Participant_Opacity.thy", "specs/formal/participant-semantics/participant-opacity-proof-evidence.json", "tools/check_participant_opacity_proof.py", - "tools/isabelle_tool.py" + "tools/isabelle_tool.py", + "contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json", + "implementations/python/packages/raes_contracts/participant_opacity_runtime.py", + "implementations/python/packages/raes_runtime/participant_crossing_mediation.py", + "implementations/python/tests/test_issue_964_participant_opacity_runtime.py", + "docs/decisions/issue-965-participant-opacity-backend-realization-preflight.md", + "implementations/python/packages/raes_conformance/conformance/participant_opacity_probes.py", + "implementations/python/tests/test_issue_965_participant_opacity_backend.py" ] }, "source_refs": [ @@ -2155,14 +2227,14 @@ "relation_id": "policy-noninterference", "display_name": "Participant-policy noninterference", "relation_class": "behavioral", - "definition": "For a fixed participant, episode and memory scope, model, environment class, scheduler class, order model, exact-cut policy-decision sequence, permitted declassification schedule, and low-strategy class, every low participant strategy produces equal support sets of projected participant-visible histories from low-equivalent initial states despite unauthorized high variation.", + "definition": "For a fixed participant, episode and memory scope, model, environment class, scheduler class, order model, exact-cut policy-decision sequence, SEM-233 revisioned confidentiality/integrity flow-policy profile, permitted declassification and endorsement schedule, and low-strategy class, every low participant strategy produces equal support sets of projected participant-visible histories from low-equivalent initial states despite unauthorized high variation.", "left_carrier": "The support set of valid labelled participant-policy runs from one low-equivalent initial state under one adaptive low strategy.", "right_carrier": "The support set of valid labelled participant-policy runs from another low-equivalent initial state under the same adaptive low strategy.", - "initial_states": "Initial world, participant-view, delivered decision-surface history, participant memory, archival-evidence, controller, authority, marking, and policy states related by the SEM-230 low-equivalence relation at the declared initial state cut.", + "initial_states": "Initial world, participant-view, delivered decision-surface history, participant memory, archival-evidence, controller, authority, marking, policy, immutable provenance/influence, and revisioned two-coordinate flow-label states related by the SEM-230 low-equivalence relation at the declared initial state cut.", "transition_signature": { "applicability": "applicable", "labels": "The closed SEM-230 alphabet for proposal, approval or denial, direction, intervention, handoff, override or cancellation, admission or rejection, attempt or result, disclosure or withholding, concealment, revocation, transformation, delivery, observation, policy change, evidence, and audit actions.", - "transition_relation": "The SEM-230 participant-policy crossing relation over existing world, view, local-history, archival-evidence, action, lifecycle, ordering, marking, controller, authority, policy, and provenance state.", + "transition_relation": "The SEM-230 participant-policy crossing relation, parameterized by SEM-233 sem-233/rev1 confidentiality/integrity obligation joins and final-sink predicates, over existing world, view, local-history, archival-evidence, action, lifecycle, ordering, marking, controller, authority, policy, provenance, and influence state.", "observable_actions": "Labels retained for the named participant and audience by the exact-cut policy decision, marking/declassification intersection, and declared state-cut projection, including delivered decision surfaces.", "hidden_actions": "Only labels mapped to tau by the named participant-, audience-, policy-decision-, and state-cut-relative projection; backend-internal actions are not intrinsically hidden.", "stuttering_actions": "Finite hidden stuttering is removed by the declared tau closure; the baseline is termination- and progress-insensitive and does not claim divergence-sensitive preservation." @@ -2170,9 +2242,9 @@ "observation_projection": { "applicability": "required", "subject": "Named participant and audience within one episode scope", - "policy_ref": "SEM-230 participant-information-flow policy", - "policy_revision": "The complete declared policy-decision sequence and exact state-cut bindings", - "redaction_scope": "Projection, masking, redaction, declassification, transformation, marking, loss, and weakening remain distinct and are evaluated deny-first.", + "policy_ref": "SEM-230 participant-information-flow policy parameterized by participant-boundary-flow-policy-v1", + "policy_revision": "The complete declared policy-decision sequence and exact state-cut bindings plus SEM-233 sem-233/rev1 and participant-boundary-flow-policy-v1@rev1", + "redaction_scope": "Projection, masking, redaction, confidentiality declassification, integrity endorsement, transformation, marking, loss, and weakening remain distinct and are evaluated deny-first.", "order_treatment": "Compare occurrence-preserving visible histories under the same declared total, partial, causal, simultaneous, or backend-serialized order model; one convenient linearization is insufficient for a partial-order claim.", "simultaneity_treatment": "Preserve declared simultaneity groups and visible order relations; timestamp equality does not establish simultaneity." }, @@ -2214,10 +2286,12 @@ }, "bounded_evidence": [ "implementations/python/tests/test_sem_230_information_flow_control.py checks finite unauthorized-high, declassification-order, policy-revision, participant-relative hiding, deny-first, append-only-history, transformation-admission, and support-set counterexamples.", - "implementations/python/tests/test_asr_535_participant_flow_assurance.py exhausts a declared finite crossing domain for unauthorized-high purge and exact-cut declassification, and drives the shipped RUN-319 boundary for denial, withholding, redaction, governed declassification, transformation, stale or revoked policy, cross-participant leakage, participant-directed inject delivery, backend weakening, unsupported capability, and adversarial overclaim." + "implementations/python/tests/test_asr_535_participant_flow_assurance.py exhausts a declared finite crossing domain for unauthorized-high purge and exact-cut declassification, and drives the shipped RUN-319 boundary for denial, withholding, redaction, governed declassification, transformation, stale or revoked policy, cross-participant leakage, participant-directed inject delivery, backend weakening, unsupported capability, and adversarial overclaim.", + "implementations/python/tests/test_sem_233_adversarial_boundary_flow.py checks the finite SEM-233 sem-233/rev1 two-coordinate powerset algebra, conservative possible-influence joins, missing labels and provenance/influence refs, laundering, coordinate-specific release operations, handoff and cross-episode carriage, stale cuts, and deny-first sink predicates." ], "explicit_non_claims": [ "The finite SEM-230 executable cases do not establish universal noninterference.", + "The SEM-233 definition and test-local finite model do not publish a portable contract or establish runtime enforcement, backend realization, instrumentation completeness, intentional-subversion robustness, monitor honesty, model alignment, or covert-channel control.", "Projected-history equality does not establish policy noninterference without the stated low-equivalence, adaptive-strategy, memory, exact-cut policy, purge, declassification, scheduler, environment, and quantifier obligations.", "No trace equivalence, simulation, refinement, strong or weak bisimulation, epistemic indistinguishability, timing security, probabilistic security, or backend realization is claimed.", "The ASR-535 finite enumeration, runtime probes, and backend conformance cases are bounded falsification evidence and are not a model check or a proof; issues #810 to #813 own any stronger opacity, bisimulation, adversarial-control, or cross-backend status." @@ -2235,7 +2309,9 @@ "proof_status": "deliberately-unproved", "evidence_refs": [ "specs/formal/participant-semantics/information-flow-control.md", + "specs/formal/participant-semantics/adversarial-flow-control.md", "implementations/python/tests/test_sem_230_information_flow_control.py", + "implementations/python/tests/test_sem_233_adversarial_boundary_flow.py", "implementations/python/packages/raes_runtime/participant_crossing_policy.py", "implementations/python/packages/raes_conformance/conformance/participant_policy_probes.py", "implementations/python/tests/test_run_319_participant_flow_policy.py", @@ -2244,10 +2320,14 @@ }, "source_refs": [ "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "cecchetti-myers-arden-2017", "clarkson-schneider-2010", + "denning-1976", "fagin-halpern-moses-vardi-1995", "goguen-meseguer-1982", "milner-1980", + "myers-liskov-1998", + "myers-sabelfeld-zdancewic-2006", "sabelfeld-sands-2009", "van-glabbeek-1990" ] @@ -2909,7 +2989,7 @@ "intended_relation_ids": [ "policy-noninterference" ], - "evidence_boundary": "The SEM-230 relation is defined over named participant, audience, memory scope, exact-cut policy-decision sequence, low-equivalence, adaptive low-strategy class, dynamic purge, permitted declassification schedule, scheduler/environment classes, order model, and support-set semantics. Current executable evidence is limited to finite models, finite reference-runtime enforcement probes, and finite backend-conformance cases.", + "evidence_boundary": "The SEM-230 relation is defined over named participant, audience, memory scope, exact-cut policy-decision sequence, low-equivalence, adaptive low-strategy class, dynamic purge, permitted declassification schedule, scheduler/environment classes, order model, and support-set semantics. SEM-233 sem-233/rev1 parameterizes it with participant-boundary-flow-policy-v1@rev1, independent confidentiality/integrity obligation sets, conservative provenance/influence carriage, coordinate-specific release operations, and final-sink predicates. Current SEM-233 executable evidence is a test-local finite model; existing runtime and backend probes implement SEM-230 only.", "prohibited_relation_ids": [ "participant-projected-history-equivalence", "trace-equivalence", @@ -2923,6 +3003,7 @@ ], "explicit_non_claims": [ "Definition, catalog validation, claim-policy checks, and finite counterexamples do not prove universal noninterference or runtime/backend realization.", + "SEM-233 bounded algebra tests do not establish portable-contract support, runtime enforcement, backend realization, intentional-subversion robustness, model alignment, monitor honesty, instrumentation completeness, or covert-channel control.", "Reference-runtime enforcement and passing backend-conformance probes establish neither universal noninterference nor native-backend realization." ] }, diff --git a/contracts/concept-authority/controlled-vocabularies-v1.json b/contracts/concept-authority/controlled-vocabularies-v1.json index 63064684b..7ca1aa0a2 100644 --- a/contracts/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/concept-authority/controlled-vocabularies-v1.json @@ -987,6 +987,10 @@ "title": "Participant Intervention", "description": "Backend supports governed participant intervention and supervisory control occurrences." }, + "participant_predicate_opacity": { + "title": "Participant Predicate Opacity", + "description": "Backend declares evidence-bound realization of a governed participant-relative predicate-opacity profile." + }, "participant_transformation": { "title": "Participant Transformation", "description": "Backend supports governed non-mutating participant-boundary transformations with fresh result identity." diff --git a/contracts/concept-authority/history/behavioral-relations-v1-rev10.json b/contracts/concept-authority/history/behavioral-relations-v1-rev10.json new file mode 100644 index 000000000..127cebcf4 --- /dev/null +++ b/contracts/concept-authority/history/behavioral-relations-v1-rev10.json @@ -0,0 +1,3127 @@ +{ + "schema_version": "behavioral-relations/v1", + "taxonomy_id": "raes-behavioral-relations", + "taxonomy_revision": "rev10", + "bibliography": [ + { + "source_id": "park-1981", + "title": "Concurrency and Automata on Infinite Sequences", + "authors": [ + "David M. R. Park" + ], + "publication_year": 1981, + "publication_venue": "Theoretical Computer Science, LNCS 104", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0017309" + } + }, + { + "source_id": "milner-1980", + "title": "A Calculus of Communicating Systems", + "authors": [ + "Robin Milner" + ], + "publication_year": 1980, + "publication_venue": "Lecture Notes in Computer Science 92", + "edition_or_version": "first edition", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/3-540-10235-3" + } + }, + { + "source_id": "van-glabbeek-1990", + "title": "The Linear Time-Branching Time Spectrum", + "authors": [ + "Rob J. van Glabbeek" + ], + "publication_year": 1990, + "publication_venue": "CONCUR 1990, LNCS 458", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0039066" + } + }, + { + "source_id": "van-glabbeek-weijland-1996", + "title": "Branching Time and Abstraction in Bisimulation Semantics", + "authors": [ + "Rob J. van Glabbeek", + "W. Peter Weijland" + ], + "publication_year": 1996, + "publication_venue": "Journal of the ACM 43(3), 555-600", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/233551.233556" + } + }, + { + "source_id": "van-glabbeek-luttik-trcka-2009", + "title": "Branching Bisimilarity with Explicit Divergence", + "authors": [ + "Rob J. van Glabbeek", + "Bas Luttik", + "Nikola Trčka" + ], + "publication_year": 2009, + "publication_venue": "Fundamenta Informaticae 93(4), 371-392", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/FI-2009-109" + } + }, + { + "source_id": "abadi-lamport-1991", + "title": "The Existence of Refinement Mappings", + "authors": [ + "Martín Abadi", + "Leslie Lamport" + ], + "publication_year": 1991, + "publication_venue": "Theoretical Computer Science 82(2)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/0304-3975(91)90224-P" + } + }, + { + "source_id": "lynch-vaandrager-1995", + "title": "Forward and Backward Simulations, Part I: Untimed Systems", + "authors": [ + "Nancy A. Lynch", + "Frits W. Vaandrager" + ], + "publication_year": 1995, + "publication_venue": "Information and Computation 121(2)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1006/inco.1995.1134" + } + }, + { + "source_id": "fagin-halpern-moses-vardi-1995", + "title": "Reasoning About Knowledge", + "authors": [ + "Ronald Fagin", + "Joseph Y. Halpern", + "Yoram Moses", + "Moshe Y. Vardi" + ], + "publication_year": 1995, + "publication_venue": "MIT Press", + "edition_or_version": "hardcover first edition", + "immutable_locator": { + "kind": "isbn", + "value": "9780262061629" + } + }, + { + "source_id": "goguen-meseguer-1982", + "title": "Security Policies and Security Models", + "authors": [ + "Joseph A. Goguen", + "José Meseguer" + ], + "publication_year": 1982, + "publication_venue": "1982 IEEE Symposium on Security and Privacy", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/SP.1982.10014" + } + }, + { + "source_id": "sabelfeld-sands-2009", + "title": "Declassification: Dimensions and Principles", + "authors": [ + "Andrei Sabelfeld", + "David Sands" + ], + "publication_year": 2009, + "publication_venue": "Journal of Computer Security 17(5)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/JCS-2009-0352" + } + }, + { + "source_id": "lynch-tuttle-1989", + "title": "An Introduction to Input/Output Automata", + "authors": [ + "Nancy A. Lynch", + "Mark R. Tuttle" + ], + "publication_year": 1989, + "publication_venue": "CWI Quarterly 2(3), 219-246", + "edition_or_version": "published journal article", + "immutable_locator": { + "kind": "report", + "value": "MIT/LCS/TM-373" + } + }, + { + "source_id": "clarkson-schneider-2010", + "title": "Hyperproperties", + "authors": [ + "Michael R. Clarkson", + "Fred B. Schneider" + ], + "publication_year": 2010, + "publication_venue": "Journal of Computer Security 18(6), 1157-1210", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/JCS-2009-0393" + } + }, + { + "source_id": "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "title": "Reactive Noninterference", + "authors": [ + "Aaron Bohannon", + "Benjamin C. Pierce", + "Vilhelm Sjöberg", + "Stephanie Weirich", + "Steve Zdancewic" + ], + "publication_year": 2009, + "publication_venue": "Proceedings of the 16th ACM Conference on Computer and Communications Security, 79-90", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/1653662.1653673" + } + }, + { + "source_id": "alur-henzinger-kupferman-vardi-1998", + "title": "Alternating Refinement Relations", + "authors": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman", + "Moshe Y. Vardi" + ], + "publication_year": 1998, + "publication_venue": "CONCUR 1998, LNCS 1466", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0055622" + } + }, + { + "source_id": "alur-henzinger-kupferman-2002", + "title": "Alternating-Time Temporal Logic", + "authors": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman" + ], + "publication_year": 2002, + "publication_venue": "Journal of the ACM 49(5)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/585265.585270" + } + }, + { + "source_id": "larsen-skou-1991", + "title": "Bisimulation Through Probabilistic Testing", + "authors": [ + "Kim G. Larsen", + "Arne Skou" + ], + "publication_year": 1991, + "publication_venue": "Information and Computation 94(1)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/0890-5401(91)90030-6" + } + }, + { + "source_id": "wellek-2010", + "title": "Testing Statistical Hypotheses of Equivalence and Noninferiority", + "authors": [ + "Stefan Wellek" + ], + "publication_year": 2010, + "publication_venue": "Chapman and Hall/CRC", + "edition_or_version": "second edition", + "immutable_locator": { + "kind": "isbn", + "value": "9781439808184" + } + }, + { + "source_id": "bueno-1997", + "title": "Empirical Adequacy: A Partial Structures Approach", + "authors": [ + "Otávio Bueno" + ], + "publication_year": 1997, + "publication_venue": "Studies in History and Philosophy of Science Part A 28(4)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/S0039-3681(97)00012-5" + } + }, + { + "source_id": "halpern-pearl-2005", + "title": "Causes and Explanations: A Structural-Model Approach. Part I: Causes", + "authors": [ + "Joseph Y. Halpern", + "Judea Pearl" + ], + "publication_year": 2005, + "publication_venue": "The British Journal for the Philosophy of Science 56(4)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1093/bjps/axi147" + } + }, + { + "source_id": "bryans-koutny-mazare-ryan-2008", + "title": "Opacity Generalised to Transition Systems", + "authors": [ + "Jeremy W. Bryans", + "Maciej Koutny", + "Laurent Mazaré", + "Peter Y. A. Ryan" + ], + "publication_year": 2008, + "publication_venue": "International Journal of Information Security 7(6), 421-435", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/s10207-008-0058-x" + } + }, + { + "source_id": "schoepe-sabelfeld-2015", + "title": "Understanding and Enforcing Opacity", + "authors": [ + "Daniel Schoepe", + "Andrei Sabelfeld" + ], + "publication_year": 2015, + "publication_venue": "2015 IEEE Computer Security Foundations Symposium, 539-553", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/CSF.2015.41" + } + }, + { + "source_id": "lin-2011", + "title": "Opacity of Discrete Event Systems and its Applications", + "authors": [ + "Feng Lin" + ], + "publication_year": 2011, + "publication_venue": "Automatica 47(3), 496-503", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/j.automatica.2011.01.002" + } + }, + { + "source_id": "saboori-hadjicostis-2012", + "title": "Verification of Infinite-Step Opacity and Complexity Considerations", + "authors": [ + "Anooshiravan Saboori", + "Christoforos N. Hadjicostis" + ], + "publication_year": 2012, + "publication_venue": "IEEE Transactions on Automatic Control 57(5), 1265-1269", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2011.2173774" + } + }, + { + "source_id": "badouel-bednarczyk-borzyszkowski-caillaud-darondeau-2007", + "title": "Concurrent Secrets", + "authors": [ + "Éric Badouel", + "Marek A. Bednarczyk", + "Andrzej M. Borzyszkowski", + "Benoît Caillaud", + "Philippe Darondeau" + ], + "publication_year": 2007, + "publication_venue": "Discrete Event Dynamic Systems 17(4), 425-446", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/s10626-007-0020-5" + } + }, + { + "source_id": "yin-lafortune-2016", + "title": "A Uniform Approach for Synthesizing Property-Enforcing Supervisors for Partially-Observed Discrete-Event Systems", + "authors": [ + "Xiang Yin", + "Stéphane Lafortune" + ], + "publication_year": 2016, + "publication_venue": "IEEE Transactions on Automatic Control 61(8), 2140-2154", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2015.2484359" + } + }, + { + "source_id": "xie-yin-li-2022", + "title": "Opacity Enforcing Supervisory Control Using Nondeterministic Supervisors", + "authors": [ + "Yifan Xie", + "Xiang Yin", + "Shaoyuan Li" + ], + "publication_year": 2022, + "publication_venue": "IEEE Transactions on Automatic Control 67(12), 6567-6582", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2021.3131125" + } + }, + { + "source_id": "cui-ma-giua-yin-2026", + "title": "Opacity Enforcing Supervisory Control with a Priori Unknown Supervisors", + "authors": [ + "Juntang Cui", + "Ziyue Ma", + "Alessandro Giua", + "Xiang Yin" + ], + "publication_year": 2026, + "publication_venue": "arXiv preprint arXiv:2604.04070", + "edition_or_version": "preprint version 1", + "immutable_locator": { + "kind": "doi", + "value": "10.48550/arXiv.2604.04070" + } + }, + { + "source_id": "partovi-jung-hai-2020", + "title": "Opacity of Discrete Event Systems with Active Intruder", + "authors": [ + "Alireza Partovi", + "Taeho Jung", + "Lin Hai" + ], + "publication_year": 2020, + "publication_venue": "arXiv preprint arXiv:2007.14960", + "edition_or_version": "preprint version 1", + "immutable_locator": { + "kind": "doi", + "value": "10.48550/arXiv.2007.14960" + } + }, + { + "source_id": "berard-mullins-sassolas-2015", + "title": "Quantifying Opacity", + "authors": [ + "Béatrice Bérard", + "John Mullins", + "Mathieu Sassolas" + ], + "publication_year": 2015, + "publication_venue": "Mathematical Structures in Computer Science 25(2), 361-403", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1017/S0960129513000637" + } + }, + { + "source_id": "andre-lime-marinho-sun-2022", + "title": "Guaranteeing Timed Opacity using Parametric Timed Model Checking", + "authors": [ + "Étienne André", + "Didier Lime", + "Dylan Marinho", + "Jun Sun" + ], + "publication_year": 2022, + "publication_venue": "ACM Transactions on Software Engineering and Methodology 31(4), 64:1-64:36", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/3502851" + } + }, + { + "source_id": "broberg-van-delft-sands-2015", + "title": "The Anatomy and Facets of Dynamic Policies", + "authors": [ + "Niklas Broberg", + "Bart van Delft", + "David Sands" + ], + "publication_year": 2015, + "publication_venue": "2015 IEEE Computer Security Foundations Symposium, 122-137", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/CSF.2015.16" + } + } + ], + "relations": { + "structural-validity": { + "relation_id": "structural-validity", + "display_name": "Structural validity", + "relation_class": "predicate", + "definition": "A single artifact satisfies its published closed structural schema.", + "left_carrier": "An artifact payload.", + "right_carrier": "The published schema selected by the artifact discriminator.", + "initial_states": "Not applicable; this is a unary predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Schema validity is a predicate over an artifact and schema, not a transition-system relation." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Schema validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one artifact payload and one schema revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Conformance to the declared structural shape.", + "proof_obligation": "Validate the complete payload against the named published schema." + }, + "bounded_evidence": [ + "JSON Schema and closed-model validation of named artifacts." + ], + "explicit_non_claims": [ + "Does not establish semantic validity, executability, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Backend equivalence", + "Participant strategic behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "contracts/schemas/" + ] + }, + "source_refs": [ + "milner-1980" + ] + }, + "semantic-validity": { + "relation_id": "semantic-validity", + "display_name": "Semantic validity", + "relation_class": "predicate", + "definition": "A structurally admitted artifact satisfies the named cross-reference and domain invariants.", + "left_carrier": "A parsed RAES artifact.", + "right_carrier": "The named semantic invariant set.", + "initial_states": "Not applicable; this is a unary predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Semantic validity checks a static artifact model rather than matching transitions." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Semantic validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one admitted artifact and one invariant revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "The named static semantic invariants.", + "proof_obligation": "Run every invariant in the declared semantic profile without error." + }, + "bounded_evidence": [ + "SemanticValidator results and invariant mutation tests." + ], + "explicit_non_claims": [ + "Does not establish realization, execution success, trace inclusion, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Runtime conformance", + "Backend comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes/validator/" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "capability-declaration": { + "relation_id": "capability-declaration", + "display_name": "Capability declaration", + "relation_class": "predicate", + "definition": "An apparatus declares support for governed capability and contract identifiers.", + "left_carrier": "A processor, backend, or participant manifest.", + "right_carrier": "The governed capability and contract vocabulary.", + "initial_states": "Not applicable; this is a declaration predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "A declaration is not execution behavior." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Manifest consumer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one manifest revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Portable declared support metadata.", + "proof_obligation": "Validate the manifest and resolve every governed identifier." + }, + "bounded_evidence": [ + "Manifest schema validation and capability-gap conformance cases." + ], + "explicit_non_claims": [ + "Does not prove that a declared capability works for every input." + ], + "incompatible_claim_surfaces": [ + "Universal backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes_contracts/manifest_authority.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "profile-satisfaction": { + "relation_id": "profile-satisfaction", + "display_name": "Profile satisfaction", + "relation_class": "predicate", + "definition": "An artifact bundle satisfies every required concern in a named profile revision.", + "left_carrier": "An artifact or bundle.", + "right_carrier": "A governed profile with required concerns.", + "initial_states": "Not applicable; this is a profile predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Profile satisfaction aggregates named gates; it is not a behavioral matching relation." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Profile evaluator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one artifact bundle and one profile revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "The conjunction of the profile's required concerns.", + "proof_obligation": "Evaluate every required concern with the profile's named validator." + }, + "bounded_evidence": [ + "Scientific completeness and backend-profile case results." + ], + "explicit_non_claims": [ + "Does not promote profile satisfaction to behavioral equivalence or empirical adequacy." + ], + "incompatible_claim_surfaces": [ + "Behavioral equivalence", + "Scientific adequacy" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "contracts/profiles/" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "bounded-probe-success": { + "relation_id": "bounded-probe-success", + "display_name": "Bounded fixture or probe success", + "relation_class": "empirical", + "definition": "Every named finite fixture or probe in the disclosed run produced its expected result.", + "left_carrier": "A concrete implementation run.", + "right_carrier": "A finite, enumerated fixture or probe set.", + "initial_states": "The concrete initial state selected by each named case.", + "transition_signature": { + "applicability": "applicable", + "labels": "The actions exercised by the named cases.", + "transition_relation": "Only transitions actually exercised by the finite cases.", + "observable_actions": "Case outputs and sanitized diagnostics.", + "hidden_actions": "No hidden action unless a governed projection declares one.", + "stuttering_actions": "Stuttering is explicit and relation-specific." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Case reporter", + "policy_ref": "behavioral-relations/bounded-probe-projection", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Only states reached by named cases.", + "traces": "Only enumerated finite traces.", + "schedulers": "Only schedulers exercised by the harness.", + "strategies": "Only strategies exercised by the harness.", + "environments": "Only named environments.", + "observations": "Only observations emitted by named cases." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Success of the enumerated cases.", + "proof_obligation": "Execute every named case and compare its bounded expected result." + }, + "bounded_evidence": [ + "Fixture-suite and target-probe reports with exact case identifiers." + ], + "explicit_non_claims": [ + "Does not quantify over untested transitions, schedulers, strategies, or environments.", + "Does not establish trace equivalence, simulation, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Universal conformance", + "Backend equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes_conformance/conformance.py" + ] + }, + "source_refs": [ + "park-1981", + "van-glabbeek-1990" + ] + }, + "bounded-but-for-necessity": { + "relation_id": "bounded-but-for-necessity", + "display_name": "Bounded but-for necessity", + "relation_class": "empirical", + "definition": "Within one declared finite causal boundary, a named candidate is supported as necessary for a named outcome when the admitted baseline outcome is true, a verified intervention removes or disables only that candidate, the matched counterfactual outcome is false, and evidence, reset, and cleanup gates pass.", + "left_carrier": "A named condition, weakness, control, or behavior present in the admitted baseline world.", + "right_carrier": "A governed outcome proposition evaluated independently in the baseline and intervention worlds.", + "initial_states": "The exact admitted baseline lineage and matching policy for the finite world pair.", + "transition_signature": { + "applicability": "applicable", + "labels": "The admitted baseline execution, typed candidate intervention, counterfactual execution, observation, reset, and cleanup actions.", + "transition_relation": "Only the two immutable runs and the one declared intervention admitted by the comparison case.", + "observable_actions": "Governed outcome truth, intervention evidence, matching checks, and reset or cleanup evidence.", + "hidden_actions": "No hidden action may change a held-fixed dimension; any permitted nuisance variation must be declared by the matching policy.", + "stuttering_actions": "Stuttering is relevant only when the declared time and observation models admit it." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Bounded necessity comparison audience", + "policy_ref": "behavioral-relations/bounded-but-for-necessity-projection", + "policy_revision": "rev1", + "redaction_scope": "Only governed evidence references and stable redacted diagnostics cross the comparison boundary.", + "order_treatment": "World executions are distinct immutable runs; their declared matching policy, not wall-clock order, governs comparison.", + "simultaneity_treatment": "Simultaneity is outside the binary criterion unless the matching policy and time model explicitly preserve it." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Only the admitted baseline and intervention-world initial states and reached states.", + "traces": "Only the two named finite executions.", + "schedulers": "Only schedulers admitted and matched by the declared policy.", + "strategies": "Only participant strategies represented and matched in the two runs.", + "environments": "Only the named apparatus, backend, scenario family, and permitted nuisance variation.", + "observations": "Only governed outcome, intervention, comparability, reset, and cleanup evidence for the named case." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Random streams, seeds, draws, and uncertainty are governed by the matching policy; a shared seed alone is insufficient." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Scheduling and participant concurrency must be held fixed or declared as permitted nuisance variation." + }, + "probability": { + "status": "outside-scope", + "treatment": "The binary criterion does not establish probabilistic necessity; statistical criteria require a separate governed relation or adapter." + }, + "time": { + "status": "parameterized", + "treatment": "Both worlds bind an explicit time model and comparison boundary; wall-clock proximity is not comparability." + }, + "partial_order": { + "status": "parameterized", + "treatment": "Causal or partial-order differences must be held fixed or explicitly admitted by the matching policy." + } + }, + "preservation": { + "property": "A finite binary but-for result for the exact candidate, outcome, worlds, intervention, apparatus, and matching policy.", + "proof_obligation": "Admit a true baseline outcome, verify the candidate intervention, admit a false counterfactual outcome, prove the declared matching checks for every other semantic dimension, and independently verify reset and cleanup." + }, + "bounded_evidence": [ + "Immutable experiment-run identities, proposition-truth results, intervention evidence, matching-policy checks, and reset or cleanup evidence for one named comparison case." + ], + "explicit_non_claims": [ + "Does not establish universal, actual, sufficient, probabilistic, or model-identified causation.", + "Does not treat replay, temporal order, correlation, failed execution, a no-op intervention, or incomparable outcome differences as necessity evidence.", + "Does not promote a supported finite comparison to proof or falsification-backed validation strength." + ], + "incompatible_claim_surfaces": [ + "Universal causal proof", + "Unbounded actual-cause attribution", + "Statistical or probabilistic necessity without a separate criterion" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes_conformance/necessity_evidence.py", + "implementations/python/packages/raes_conformance/necessity_types.py", + "implementations/python/packages/raes_conformance/necessity_validation.py", + "implementations/python/tests/test_necessity_validation.py" + ] + }, + "source_refs": [ + "halpern-pearl-2005" + ] + }, + "canonical-artifact-identity": { + "relation_id": "canonical-artifact-identity", + "display_name": "Canonical artifact identity", + "relation_class": "predicate", + "definition": "Two artifacts have identical canonical bytes or digest under one named serialization profile.", + "left_carrier": "One canonical artifact.", + "right_carrier": "Another canonical artifact under the same profile.", + "initial_states": "Not applicable; this is artifact identity.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Digest identity compares canonical representations, not enabled behavior." + }, + "observation_projection": { + "applicability": "identity", + "subject": "Canonical serializer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "symmetric", + "quantification": { + "states": "For the two named canonical artifacts.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Canonical byte identity under the named profile.", + "proof_obligation": "Canonicalize both artifacts with the same revision and compare bytes or collision-resistant digests." + }, + "bounded_evidence": [ + "Canonicalization and digest equality tests." + ], + "explicit_non_claims": [ + "Does not establish common provenance, equal executions, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Trace comparison", + "Backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes/canonical.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "realization-envelope-membership": { + "relation_id": "realization-envelope-membership", + "display_name": "Realization-envelope membership", + "relation_class": "set-relation", + "definition": "A concrete or requested point belongs to a governed realization envelope.", + "left_carrier": "A realization point.", + "right_carrier": "A closed realization-envelope set.", + "initial_states": "Not applicable; this is set membership.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Envelope membership is set-theoretic support, not a transition match." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Realization-envelope validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "left-to-right", + "quantification": { + "states": "For one point and one envelope revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Membership in the declared support set.", + "proof_obligation": "Evaluate every envelope dimension and closure rule for the point." + }, + "bounded_evidence": [ + "Witness and negative-probe envelope tests." + ], + "explicit_non_claims": [ + "Does not establish that execution succeeds or that behavior refines an abstract runtime." + ], + "incompatible_claim_surfaces": [ + "Runtime trace inclusion", + "Backend equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/realization/envelope-semantics.md" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "realization-envelope-subsumption": { + "relation_id": "realization-envelope-subsumption", + "display_name": "Realization-envelope subsumption", + "relation_class": "set-relation", + "definition": "Every point admitted by one realization envelope is admitted by another under the named closure rules.", + "left_carrier": "One realization-envelope set.", + "right_carrier": "Another realization-envelope set.", + "initial_states": "Not applicable; this is set inclusion.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Envelope subsumption compares support sets rather than transition systems." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Realization-envelope validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "left-to-right", + "quantification": { + "states": "Universally over points in the left envelope.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Set inclusion of declared realization support.", + "proof_obligation": "Prove or decide inclusion for every governed envelope dimension." + }, + "bounded_evidence": [ + "Finite witness and mutation tests for current envelope operators." + ], + "explicit_non_claims": [ + "Does not establish behavioral refinement, trace inclusion, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Runtime behavior", + "Participant behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes_contracts/realization_envelope.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "trace-inclusion": { + "relation_id": "trace-inclusion", + "display_name": "Projected trace inclusion", + "relation_class": "behavioral", + "definition": "Every projected concrete trace belongs to the abstract trace set under a declared projection.", + "left_carrier": "Concrete implementation transition system.", + "right_carrier": "Abstract RAES transition system.", + "initial_states": "Related concrete and abstract initial states.", + "transition_signature": { + "applicability": "applicable", + "labels": "Labels in the declared concrete and abstract alphabets.", + "transition_relation": "Concrete and abstract labelled transition relations.", + "observable_actions": "Actions retained by the governed projection.", + "hidden_actions": "Only actions explicitly hidden by the projection.", + "stuttering_actions": "Concrete stuttering must be permitted by the abstract obligation." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named observer or abstraction", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "All reachable related states.", + "traces": "Universally over projected concrete traces.", + "schedulers": "All admitted schedulers unless narrowed.", + "strategies": "Outside scope unless the systems are strategic.", + "environments": "All admitted environments unless narrowed.", + "observations": "Through the named projection only." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Abstract trace safety for projected concrete executions.", + "proof_obligation": "Show Proj(Traces_concrete) is a subset of Traces_abstract under stated fairness and divergence assumptions." + }, + "bounded_evidence": [ + "Finite target probes can falsify but cannot prove universal inclusion." + ], + "explicit_non_claims": [ + "Does not establish completeness, reverse inclusion, trace equivalence, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Current backend conformance report" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-runtime/README.md" + ] + }, + "source_refs": [ + "van-glabbeek-1990", + "abadi-lamport-1991", + "lynch-vaandrager-1995" + ] + }, + "trace-equivalence": { + "relation_id": "trace-equivalence", + "display_name": "Trace equivalence", + "relation_class": "behavioral", + "definition": "Two systems have equal projected trace sets under the same declared alphabet and projection.", + "left_carrier": "One labelled transition system.", + "right_carrier": "Another labelled transition system.", + "initial_states": "Paired initial states.", + "transition_signature": { + "applicability": "applicable", + "labels": "A shared declared label alphabet.", + "transition_relation": "The two labelled transition relations.", + "observable_actions": "Labels retained by the common projection.", + "hidden_actions": "Labels hidden by the common projection.", + "stuttering_actions": "Stuttering treatment must be identical." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named comparison observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "All reachable states contributing traces.", + "traces": "Universally over both trace sets.", + "schedulers": "All admitted schedulers.", + "strategies": "Outside scope unless strategies are encoded.", + "environments": "All declared environments.", + "observations": "Through one common projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Equality of projected trace languages.", + "proof_obligation": "Prove both projected trace inclusions under identical assumptions." + }, + "bounded_evidence": [ + "Finite trace comparison may refute but cannot establish equality." + ], + "explicit_non_claims": [ + "Does not preserve branching structure and does not imply bisimulation." + ], + "incompatible_claim_surfaces": [ + "Finite backend comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "van-glabbeek-1990" + ] + }, + "forward-simulation": { + "relation_id": "forward-simulation", + "display_name": "Forward simulation", + "relation_class": "behavioral", + "definition": "A relation maps each concrete step to an abstract matching path while preserving related states.", + "left_carrier": "Concrete implementation states.", + "right_carrier": "Abstract specification states.", + "initial_states": "Every concrete initial state relates to an abstract initial state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Concrete and abstract labels under a declared matching function.", + "transition_relation": "Concrete and abstract step relations.", + "observable_actions": "Labels exposed by the abstraction.", + "hidden_actions": "Labels mapped to hidden or stuttering abstract behavior.", + "stuttering_actions": "Explicit abstract stuttering where allowed." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Abstraction observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over related reachable states.", + "traces": "All concrete traces induced by matched steps.", + "schedulers": "All admitted concrete choices.", + "strategies": "Outside scope unless extended strategically.", + "environments": "All admitted environments.", + "observations": "Under the named abstraction." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Usually projected trace inclusion and named safety properties.", + "proof_obligation": "Supply a simulation relation and discharge initiality plus step-correspondence obligations." + }, + "bounded_evidence": [ + "Tests may exercise candidate obligations on finite models only." + ], + "explicit_non_claims": [ + "Successful probes do not establish a simulation relation." + ], + "incompatible_claim_surfaces": [ + "Current conformance results" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "lynch-vaandrager-1995", + "abadi-lamport-1991" + ] + }, + "backward-simulation": { + "relation_id": "backward-simulation", + "display_name": "Backward simulation", + "relation_class": "behavioral", + "definition": "A relation reasons from abstract successor possibilities back to concrete predecessors to establish implementation inclusion where forward simulation is insufficient.", + "left_carrier": "Concrete implementation states.", + "right_carrier": "Abstract specification states.", + "initial_states": "Initial and reachable-state coverage follow the selected backward-simulation theorem.", + "transition_signature": { + "applicability": "applicable", + "labels": "Concrete and abstract labels under the theorem's matching rule.", + "transition_relation": "Concrete and abstract step relations.", + "observable_actions": "Declared external labels.", + "hidden_actions": "Declared internal labels.", + "stuttering_actions": "History, prophecy, and stuttering treatment must be explicit." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Abstraction observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over theorem-defined related states.", + "traces": "All represented implementation behaviors.", + "schedulers": "All admitted nondeterministic choices.", + "strategies": "Outside scope unless extended strategically.", + "environments": "All admitted environments.", + "observations": "Under the named abstraction." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Implementation behavior inclusion under the cited theorem's assumptions.", + "proof_obligation": "Supply a backward simulation relation and discharge its reachability, initiality, and step obligations." + }, + "bounded_evidence": [ + "Finite model tests can exercise examples but do not prove a backend relation." + ], + "explicit_non_claims": [ + "Does not follow from result equality or a forward-only sampled trace." + ], + "incompatible_claim_surfaces": [ + "Current backend conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "lynch-vaandrager-1995" + ] + }, + "data-refinement": { + "relation_id": "data-refinement", + "display_name": "Data refinement", + "relation_class": "behavioral", + "definition": "Concrete data states represent abstract states through a retrieve relation while operations preserve that relation.", + "left_carrier": "Concrete state and operation space.", + "right_carrier": "Abstract state and operation space.", + "initial_states": "Concrete and abstract initial states related by the retrieve relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Operation invocations and observations.", + "transition_relation": "Concrete and abstract operation relations.", + "observable_actions": "Client-visible operation effects.", + "hidden_actions": "Internal representation steps.", + "stuttering_actions": "Stuttering and enabledness obligations are method-specific." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Client observation", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "All states satisfying the retrieve relation.", + "traces": "All operation histories covered by the refinement method.", + "schedulers": "All admitted operation nondeterminism.", + "strategies": "Outside scope unless strategies are modeled.", + "environments": "All client environments under stated preconditions.", + "observations": "Client-visible results only." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "The abstract operation contract and selected client-observable properties.", + "proof_obligation": "Define the retrieve relation and discharge initialization, applicability/enabledness, and correctness obligations." + }, + "bounded_evidence": [ + "Contract and operation tests are bounded evidence only." + ], + "explicit_non_claims": [ + "An SDL transformation function is not data refinement without these obligations." + ], + "incompatible_claim_surfaces": [ + "SDL phase transformation" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "abadi-lamport-1991", + "lynch-vaandrager-1995" + ] + }, + "strong-bisimulation": { + "relation_id": "strong-bisimulation", + "display_name": "Strong Park-Milner bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric relation matches every labelled step immediately in both directions.", + "left_carrier": "One labelled transition system.", + "right_carrier": "Another labelled transition system.", + "initial_states": "The two initial states belong to the bisimulation relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common label alphabet including internal labels.", + "transition_relation": "Both labelled transition relations.", + "observable_actions": "Every label is matched exactly.", + "hidden_actions": "Hidden labels are still labels and must match immediately.", + "stuttering_actions": "Only explicitly labelled stuttering steps can match." + }, + "observation_projection": { + "applicability": "identity", + "subject": "External comparison observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "symmetric", + "quantification": { + "states": "Universally over every related state pair.", + "traces": "All branching continuations.", + "schedulers": "All nondeterministic branches.", + "strategies": "Outside scope unless lifted to games.", + "environments": "All transition-system environments encoded in state.", + "observations": "Identity observation of labels." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Branching structure and modal properties under the chosen semantics.", + "proof_obligation": "Exhibit a symmetric relation closed under immediate labelled steps in both directions." + }, + "bounded_evidence": [ + "Finite algorithms can decide the relation only for supplied finite models." + ], + "explicit_non_claims": [ + "One shared trace, result, digest, or terminal observation is insufficient." + ], + "incompatible_claim_surfaces": [ + "Finite probes", + "Digest comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "park-1981", + "milner-1980" + ] + }, + "weak-bisimulation": { + "relation_id": "weak-bisimulation", + "display_name": "Weak or observational bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric relation matches visible actions through closure over explicitly hidden tau steps.", + "left_carrier": "One labelled transition system with tau.", + "right_carrier": "Another labelled transition system with tau.", + "initial_states": "Initial states related after the selected tau closure.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common visible alphabet plus the declared tau label.", + "transition_relation": "Both labelled transition relations.", + "observable_actions": "Visible labels match through weak transitions.", + "hidden_actions": "Only the explicitly governed tau label is hidden.", + "stuttering_actions": "Tau closure and stuttering are explicit; divergence treatment is declared." + }, + "observation_projection": { + "applicability": "required", + "subject": "Observer that hides tau", + "policy_ref": "behavioral-relations/tau-projection", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related states under tau closure.", + "traces": "All weak traces and branching continuations.", + "schedulers": "All nondeterministic tau and visible branches.", + "strategies": "Outside scope unless lifted to games.", + "environments": "All encoded environments.", + "observations": "Through the named hiding projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Observation-preserving branching behavior under stated divergence and termination semantics.", + "proof_obligation": "Exhibit a weak bisimulation relation and discharge both directional weak-step obligations." + }, + "bounded_evidence": [ + "The catalog's hidden-action example demonstrates the definition on a finite toy model." + ], + "explicit_non_claims": [ + "Backend-internal work is not tau unless a governed projection declares it." + ], + "incompatible_claim_surfaces": [ + "Undeclared backend hiding" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "bounded", + "proof_status": "future", + "evidence_refs": [ + "contracts/concept-authority/behavioral-relations-v1.json#worked_examples" + ] + }, + "source_refs": [ + "milner-1980", + "van-glabbeek-1990" + ] + }, + "divergence-preserving-branching-bisimulation": { + "relation_id": "divergence-preserving-branching-bisimulation", + "display_name": "Divergence-preserving branching bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric branching bisimulation matches visible transitions through finite closure over an explicitly governed tau set while preserving each related branching point and explicit infinite tau behavior in both directions.", + "left_carrier": "One labelled transition system with a closed visible/tau partition and explicit deadlock, termination, and divergence semantics.", + "right_carrier": "Another labelled transition system over the same projected visible alphabet and governed tau treatment.", + "initial_states": "The revisioned relation-parameter profile names both initial states and requires them to belong to the greatest fixed-point relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common projected visible alphabet plus only the tau labels enumerated by the revisioned relation-parameter profile.", + "transition_relation": "Both complete labelled transition relations over the profile's quantified carriers.", + "observable_actions": "Every projected visible action is matched in both directions after finite tau closure while the pre-action branching state remains related.", + "hidden_actions": "Only profile-enumerated tau actions are hidden; redacted occurrences, refusals, unsupported outcomes, errors, deadlock, termination, and divergence are not hidden by default.", + "stuttering_actions": "Finite tau stuttering is admitted at a related branching point; explicit infinite tau paths must be preserved in both directions." + }, + "observation_projection": { + "applicability": "required", + "subject": "The participant, audience, auditor, or other observer named by the closed relation-parameter profile.", + "policy_ref": "Revisioned divergence-preserving branching-bisimulation projection from the claim profile.", + "policy_revision": "The exact projection revision bound by the claim.", + "redaction_scope": "The profile enumerates every visible, redacted-occurrence, and tau label; implementation-internal or content-redacted does not imply hidden.", + "order_treatment": "The profile fixes sequence, interleaving, step, causal, or other order semantics; one linearization cannot establish a partial-order claim.", + "simultaneity_treatment": "Only simultaneity represented in the selected LTS and visible projection is preserved." + }, + "projection_required": true, + "relation_parameter_profile_required": true, + "direction": "symmetric", + "quantification": { + "states": "greatest-fixed-point relation", + "traces": "All visible and tau continuations from every related state pair, including infinite tau continuations.", + "schedulers": "Every nondeterministic branch and scheduler admitted by the closed profile.", + "strategies": "Outside scope unless the carriers explicitly encode game or adaptive-strategy state.", + "environments": "Every environment state and input admitted by the closed profile.", + "observations": "Exactly the visible alphabet after the revisioned closed projection; the tau partition remains explicit." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "Every admitted branch is matched; finite samples or selected schedules are insufficient." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The profile declares interleaving, step, true-concurrent, or other semantics and the preserved visible order." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability measures are excluded; a probabilistic relation must be named separately." + }, + "time": { + "status": "parameterized", + "treatment": "Untimed profiles erase no visible time label; timed claims require a clock and timed relation." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim requires a carrier and relation that preserve the declared causal structure." + } + }, + "preservation": { + "property": "Visible branching structure, finite governed tau stuttering, explicit termination and structural deadlock, and explicit divergence under the named projection and model dimensions.", + "proof_obligation": "Exhibit or decide the greatest symmetric relation satisfying both branching transfer clauses and both explicit-divergence clauses for the complete quantified carriers and initial states." + }, + "bounded_evidence": [ + "Issue #811 supplies an exact complete-finite theorem profile, witness family, mutation design, and pinned checker contract; it does not run the equivalence decision.", + "A finite model-check result is final only when the supplied finite carrier is the complete quantified domain and the evidence binds exact inputs, counts, tool provenance, result, and independent reproduction." + ], + "explicit_non_claims": [ + "Taxonomy revision rev6 defines this relation and the participant-crossing claim surface but does not establish a model-check or proof result.", + "The participant-crossing design does not establish live-runtime realization, backend conformance, whole-runtime equivalence, policy noninterference, or predicate opacity.", + "Depth limits, sampled traces, probes, matching digests, schema equality, and ordinary weak bisimulation are not this relation." + ], + "incompatible_claim_surfaces": [ + "Undeclared tau hiding or divergence treatment", + "Incomplete, sampled, depth-limited, or timeout-truncated carriers promoted to a complete result", + "Formal equivalence promoted to live-runtime, backend, noninterference, opacity, timed, probabilistic, strategic, concurrent, or partial-order assurance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "deliberately-unproved", + "checker_status": "not-implemented", + "model_check_status": "not-model-checked", + "runtime_enforcement_status": "not-enforced", + "backend_declaration_status": "not-declared", + "backend_realization_status": "not-realized", + "backend_conformance_status": "not-tested", + "evidence_refs": [ + "docs/decisions/adrs/adr-100-participant-crossing-bisimulation.md", + "specs/formal/participant-semantics/participant-crossing-bisimulation.md", + "docs/research/participant-bisimulation/implementation-program.json" + ] + }, + "source_refs": [ + "van-glabbeek-weijland-1996", + "van-glabbeek-luttik-trcka-2009" + ] + }, + "participant-predicate-opacity": { + "relation_id": "participant-predicate-opacity", + "display_name": "Participant-relative predicate opacity", + "relation_class": "epistemic", + "definition": "For every actual possible point at which the selected secret predicate is true, the named participant, audience, or coalition information cell induced by equal declared initial information and accumulated observations contains at least one possible point at which the predicate is false. The baseline is one-sided and possibilistic.", + "left_carrier": "One declared possible-point system whose points compose model or supervisor realization, run, evaluation cut, observer-local state and retained memory, exact-cut policy realization, and scheduler, environment, and order context.", + "right_carrier": "Not applicable; opacity is a unary property of the declared possible-point system and relation-parameter profile. A nonsecret point is a witness inside the same carrier, not a second system.", + "initial_states": "The profile fixes observer or coalition initial information, participant and audience identity, retained memory, policy and supervisor visibility, environment and scheduler classes, and the initial cut or horizon.", + "transition_signature": { + "applicability": "applicable", + "labels": "SEM-230 and participant-runtime occurrences, including proposal and control decisions, admission, attempt and result, disclosure and withholding, transformation, delivery and observation, policy or supervisor change, evidence, and audit when retained by the selected observation profile.", + "transition_relation": "Valid runs over existing participant world, view, local-history, archival-evidence, controller, authority, marking, exact-cut policy, crossing, delivery, observation, scheduler, environment, and order carriers.", + "observable_actions": "Exactly the content, occurrence, omission, decision, failure, delivery, retry, order, timing, policy-change, retrieval, evidence, or audit coordinates retained for the named observer by the revisioned relation-parameter profile.", + "hidden_actions": "Only occurrences removed by that observer-, policy-, supervisor-visibility-, cut-, memory-, time-, and order-relative observation function; hidden implementation does not imply hidden behavior.", + "stuttering_actions": "The untimed baseline removes finite unobserved stuttering and is progress- and termination-insensitive. Omission is observable only when the profile supplies an opportunity, deadline, acknowledgement, progress, or clock model." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant, audience, or explicit coalition", + "policy_ref": "SEM-231 participant-predicate-opacity observation profile", + "policy_revision": "The revisioned relation-parameter profile bound by the claim", + "redaction_scope": "Projection, redaction, declassification, decision disclosure, concealment, revocation, loss, and prior knowledge remain distinct; the profile states which resulting facts are observations.", + "order_treatment": "The profile selects total, participant-local, causal partial, simultaneous, or backend-serialized order. One linearization cannot establish a partial-order claim.", + "simultaneity_treatment": "Only declared simultaneity and visible causal frontiers are retained; timestamp equality is not simultaneity." + }, + "projection_required": true, + "relation_parameter_profile_required": true, + "direction": "unary", + "quantification": { + "states": "For every actual point in the declared carrier at which the revisioned secret predicate is true, there exists a point in the same observer information cell at which it is false.", + "traces": "Over every run and cut admitted by the selected current-, initial-, K-step, infinite-step, language, or other declared horizon profile.", + "schedulers": "Over the fixed declared scheduler class and admitted order contexts; scheduler-sensitive variants state their quantifier order.", + "strategies": "Passive profiles quantify over observations only. Active profiles quantify universally over the declared allowed adaptive participant strategies, with actual and witness runs possible under the same strategy.", + "environments": "Over the fixed declared model and environment class, including only supervisor or policy realizations admitted by the selected visibility posture.", + "observations": "Equality of declared initial information and accumulated observer-local observations, including memory across retries, replay, episodes, policy revisions, controller handoffs, and coalition sharing when selected." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "The baseline is possibilistic over the declared support. Nondeterministic or randomized supervision changes possible points but is not itself opacity evidence." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The profile fixes interleaving, step, simultaneous, causal, or other declared concurrent semantics and observer-visible order." + }, + "probability": { + "status": "outside-scope", + "treatment": "The baseline compares possibility support, not probability mass, posterior belief, entropy, leakage probability, or differential privacy. Quantitative opacity requires a separately governed relation." + }, + "time": { + "status": "parameterized", + "treatment": "The baseline is untimed and progress-insensitive. A timed profile requires a governed clock, duration and progress observation model and separately scoped evidence." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim compares declared visible causal frontiers and admitted schedules; a witness from one convenient linearization is insufficient." + } + }, + "preservation": { + "property": "The named observer never knows that the selected one-sided secret predicate is true at a protected cut because every actual secret information cell retains a possible nonsecret point.", + "proof_obligation": "For every quantified actual secret point and, for active profiles, every allowed adaptive strategy, exhibit or prove the existence of a nonsecret point with equal declared initial information and accumulated observation under the same profile, strategy, release schedule, supervisor visibility, memory, scheduler, environment, time, and order assumptions." + }, + "bounded_evidence": [ + "The SEM-231 formal specification gives four finite counterexamples covering an incomplete equal-history witness, supervisor-decision leakage, opacity without noninterference, and declassification-induced knowledge change.", + "The participant-opacity-baseline-v1 profile closes every relation coordinate and the deterministic processor exhausts exact declared finite possible-point carriers with digest-bound bounded outcomes or sanitized counterexample references.", + "implementations/python/tests/test_issue_961_participant_opacity.py covers profile and claim resolution, finite bounds, active strategies, coalition fusion, decision and omission channels, retained release knowledge, vacuity, deterministic evidence, replay, and explicit nonclaims.", + "The participant-opacity finite-state checker derives the complete reachable fixed point from an exact transition model, checks every reachable secret evaluation point, and binds catalog, profile, model, assumptions, explored coverage, tool version, result or safe counterexample, and replay evidence.", + "The committed model-check input and evidence fixtures retain the exact positive baseline model, result, digests, complete coverage, tool identity, and explicit nonclaims; invalid fixtures exercise count and partial-result promotion failures.", + "implementations/python/tests/test_issue_962_participant_opacity_model_check.py covers pair-probe incompleteness, supervisor behavior, active strategies, coalition fusion, retained memory, release changes, order and probability non-promotion, exact bounds, replay, and agreement with the bounded lane.", + "The Isabelle/HOL Participant_Opacity session kernel-checks the SEM-231 one-sided opacity definition, its information-cell knowledge characterization, and the conditional implication from a matching SEM-230 noninterference instance for an eligible predicate; checked countermodels preserve the invalid-promotion boundaries.", + "The participant-opacity-runtime-reference-v1 profile and RUN-319 crossing boundary enforce one exact finite observation inventory with safe, atomic runtime-enforcement decision bindings." + ], + "explicit_non_claims": [ + "Relation definition, catalog validation, claim-profile binding, and bounded finite analysis do not establish opacity of RAES, RUN-319, or any backend outside the exact admitted artifact.", + "No checker, finite-state model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance is delivered by taxonomy revision rev5.", + "Taxonomy revision rev7 adds only an in-process bounded-test checker; it does not add a model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev8 adds one exact finite-state model-check result; it does not add a mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev9 adds only the abstract conditional mathematical theorem bound to participant-opacity-theorem-v1; it does not prove opacity of RAES, a runtime, a deployment, a backend, or the finite fixture profile.", + "Taxonomy revision rev10 adds partial runtime enforcement only for participant-opacity-runtime-reference-v1; it does not establish general opacity, live model checking or proof, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Bounded evidence authenticates only the normalized-input digest; it does not authenticate a claimed source artifact or materializer.", + "Opacity of one predicate does not imply SEM-230 policy noninterference, projected-history equivalence, epistemic indistinguishability of two selected worlds, trace inclusion or equivalence, simulation, refinement, or strong or weak bisimulation.", + "The possibilistic baseline makes no posterior-risk, entropy, probabilistic, differential-privacy, timed, progress-sensitive, or universal partial-order claim." + ], + "incompatible_claim_surfaces": [ + "Unrevisioned or untyped secret, observer, supervisor-visibility, memory, strategy, time, order, or release coordinates", + "Single equal-history pairs, finite probes, randomized behavior, runtime filters, or backend declarations promoted to universal opacity", + "Claims that erase prior knowledge through concealment, revocation, reset, rollback, or supersession" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "proved", + "checker_status": "implemented", + "model_check_status": "model-checked", + "runtime_enforcement_status": "partial", + "backend_declaration_status": "not-declared", + "backend_realization_status": "not-realized", + "backend_conformance_status": "not-tested", + "evidence_refs": [ + "docs/decisions/adrs/adr-099-participant-relative-predicate-opacity.md", + "specs/formal/participant-semantics/participant-predicate-opacity.md", + "contracts/profiles/behavioral-relation/history/participant-opacity-baseline-v1-sem-231-rev2.json", + "contracts/profiles/behavioral-relation/participant-opacity-theorem-v1.json", + "contracts/schemas/formal-analysis/participant-opacity-model-check-input-v1.json", + "contracts/schemas/formal-analysis/participant-opacity-model-check-evidence-v1.json", + "contracts/fixtures/formal-analysis/participant-opacity-model-check-input-v1/valid/opaque-transition-model.json", + "contracts/fixtures/formal-analysis/participant-opacity-model-check-evidence-v1/valid/opaque-transition-model.json", + "implementations/python/packages/raes_processor/participant_opacity/_service.py", + "implementations/python/packages/raes_processor/participant_opacity/_model_check.py", + "implementations/python/tests/test_sem_231_participant_predicate_opacity.py", + "implementations/python/tests/test_issue_961_participant_opacity.py", + "implementations/python/tests/test_issue_962_participant_opacity_model_check.py", + "implementations/python/tests/test_issue_963_participant_opacity_proof.py", + "specs/formal/participant-semantics/isabelle/Participant_Opacity.thy", + "specs/formal/participant-semantics/participant-opacity-proof-evidence.json", + "tools/check_participant_opacity_proof.py", + "tools/isabelle_tool.py", + "contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json", + "implementations/python/packages/raes_contracts/participant_opacity_runtime.py", + "implementations/python/packages/raes_runtime/participant_crossing_mediation.py", + "implementations/python/tests/test_issue_964_participant_opacity_runtime.py" + ] + }, + "source_refs": [ + "andre-lime-marinho-sun-2022", + "badouel-bednarczyk-borzyszkowski-caillaud-darondeau-2007", + "berard-mullins-sassolas-2015", + "broberg-van-delft-sands-2015", + "bryans-koutny-mazare-ryan-2008", + "cui-ma-giua-yin-2026", + "fagin-halpern-moses-vardi-1995", + "lin-2011", + "partovi-jung-hai-2020", + "saboori-hadjicostis-2012", + "schoepe-sabelfeld-2015", + "xie-yin-li-2022", + "yin-lafortune-2016" + ] + }, + "participant-projected-history-equivalence": { + "relation_id": "participant-projected-history-equivalence", + "display_name": "Participant-projected history equivalence", + "relation_class": "epistemic", + "definition": "Two finite or complete histories have equal projections for one participant under one observation-boundary revision.", + "left_carrier": "One global or backend history.", + "right_carrier": "Another global or backend history.", + "initial_states": "The compared histories share a declared participant and starting information state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Participant-visible events after projection.", + "transition_relation": "Underlying history extension relations.", + "observable_actions": "Events admitted by the participant observation boundary.", + "hidden_actions": "Events removed or redacted by the boundary.", + "stuttering_actions": "No additional stuttering assumption beyond projected history equality." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "States represented in the compared histories.", + "traces": "Finite histories unless a universal claim is separately proved.", + "schedulers": "Only schedulers represented in the histories.", + "strategies": "No strategic quantification.", + "environments": "Only the named environment/run context.", + "observations": "One participant and one policy revision." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Equality or indistinguishability of the named participant's projected histories.", + "proof_obligation": "Apply the existing participant observation boundary to both histories and compare the resulting ordered visible records." + }, + "bounded_evidence": [ + "Participant behavior-history and observation-envelope tests on named histories." + ], + "explicit_non_claims": [ + "Does not imply equality of global state, future behavior, knowledge, or strategy." + ], + "incompatible_claim_surfaces": [ + "Global-state comparison", + "Strategic equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-runtime/README.md" + ] + }, + "source_refs": [ + "fagin-halpern-moses-vardi-1995", + "milner-1980" + ] + }, + "policy-noninterference": { + "relation_id": "policy-noninterference", + "display_name": "Participant-policy noninterference", + "relation_class": "behavioral", + "definition": "For a fixed participant, episode and memory scope, model, environment class, scheduler class, order model, exact-cut policy-decision sequence, permitted declassification schedule, and low-strategy class, every low participant strategy produces equal support sets of projected participant-visible histories from low-equivalent initial states despite unauthorized high variation.", + "left_carrier": "The support set of valid labelled participant-policy runs from one low-equivalent initial state under one adaptive low strategy.", + "right_carrier": "The support set of valid labelled participant-policy runs from another low-equivalent initial state under the same adaptive low strategy.", + "initial_states": "Initial world, participant-view, delivered decision-surface history, participant memory, archival-evidence, controller, authority, marking, and policy states related by the SEM-230 low-equivalence relation at the declared initial state cut.", + "transition_signature": { + "applicability": "applicable", + "labels": "The closed SEM-230 alphabet for proposal, approval or denial, direction, intervention, handoff, override or cancellation, admission or rejection, attempt or result, disclosure or withholding, concealment, revocation, transformation, delivery, observation, policy change, evidence, and audit actions.", + "transition_relation": "The SEM-230 participant-policy crossing relation over existing world, view, local-history, archival-evidence, action, lifecycle, ordering, marking, controller, authority, policy, and provenance state.", + "observable_actions": "Labels retained for the named participant and audience by the exact-cut policy decision, marking/declassification intersection, and declared state-cut projection, including delivered decision surfaces.", + "hidden_actions": "Only labels mapped to tau by the named participant-, audience-, policy-decision-, and state-cut-relative projection; backend-internal actions are not intrinsically hidden.", + "stuttering_actions": "Finite hidden stuttering is removed by the declared tau closure; the baseline is termination- and progress-insensitive and does not claim divergence-sensitive preservation." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant and audience within one episode scope", + "policy_ref": "SEM-230 participant-information-flow policy", + "policy_revision": "The complete declared policy-decision sequence and exact state-cut bindings", + "redaction_scope": "Projection, masking, redaction, declassification, transformation, marking, loss, and weakening remain distinct and are evaluated deny-first.", + "order_treatment": "Compare occurrence-preserving visible histories under the same declared total, partial, causal, simultaneous, or backend-serialized order model; one convenient linearization is insufficient for a partial-order claim.", + "simultaneity_treatment": "Preserve declared simultaneity groups and visible order relations; timestamp equality does not establish simultaneity." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "For every pair of initial and reachable states related by low equivalence at the applicable participant-policy state cut and memory scope.", + "traces": "For all valid run support sets generated under the same low strategy, exact-cut policy decisions, and permitted declassification schedule.", + "schedulers": "For the fixed declared scheduler class; scheduler-sensitive variants must select and evidence a stronger relation.", + "strategies": "Universally over the declared class of adaptive low participant strategies mapping delivered local histories to choices or choice sets; no coalition-ability equivalence is implied.", + "environments": "For the fixed declared environment class and equal low environment inputs, allowing only the unauthorized high variation under examination.", + "observations": "Equality of support sets of occurrence-preserving histories projected for the named participant, audience, policy sequence, markings, declassification schedule, and order model." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "The baseline compares complete declared support sets of projected histories; equality of sampled or single histories is only bounded falsification evidence." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The selected sequential, total, partial, causal, simultaneous, or backend-serialized transition and visible-order model is fixed for the comparison." + }, + "probability": { + "status": "outside-scope", + "treatment": "The baseline compares support sets, not measures; probabilistic noninterference requires a separately governed probabilistic relation, kernel, bound, and evidence." + }, + "time": { + "status": "abstracted", + "treatment": "The baseline is termination- and progress-insensitive and excludes wall-clock timing; timed security requires a separately governed relation." + }, + "partial_order": { + "status": "parameterized", + "treatment": "When partial order is selected, the declared visible order relation and simultaneity groups are compared rather than one linear extension." + } + }, + "preservation": { + "property": "Unauthorized high variation does not change the support set of participant-visible histories observed by any declared adaptive low strategy, except at equal explicitly governed declassification events.", + "proof_obligation": "Prove support-set equality for every quantified low-equivalent pair and low strategy under the fixed participant, memory scope, exact-cut policy decisions, declassification schedule, model, scheduler, environment, and order assumptions, or report only the bounded counterexamples actually checked." + }, + "bounded_evidence": [ + "implementations/python/tests/test_sem_230_information_flow_control.py checks finite unauthorized-high, declassification-order, policy-revision, participant-relative hiding, deny-first, append-only-history, transformation-admission, and support-set counterexamples.", + "implementations/python/tests/test_asr_535_participant_flow_assurance.py exhausts a declared finite crossing domain for unauthorized-high purge and exact-cut declassification, and drives the shipped RUN-319 boundary for denial, withholding, redaction, governed declassification, transformation, stale or revoked policy, cross-participant leakage, participant-directed inject delivery, backend weakening, unsupported capability, and adversarial overclaim." + ], + "explicit_non_claims": [ + "The finite SEM-230 executable cases do not establish universal noninterference.", + "Projected-history equality does not establish policy noninterference without the stated low-equivalence, adaptive-strategy, memory, exact-cut policy, purge, declassification, scheduler, environment, and quantifier obligations.", + "No trace equivalence, simulation, refinement, strong or weak bisimulation, epistemic indistinguishability, timing security, probabilistic security, or backend realization is claimed.", + "The ASR-535 finite enumeration, runtime probes, and backend conformance cases are bounded falsification evidence and are not a model check or a proof; issues #810 to #813 own any stronger opacity, bisimulation, adversarial-control, or cross-backend status." + ], + "incompatible_claim_surfaces": [ + "Unrevisioned participant projection", + "Single-history or sampled-history equality", + "Undeclared scheduler, environment, timing, probability, or partial-order assumptions", + "Runtime or backend realization inferred from the definition" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-semantics/information-flow-control.md", + "implementations/python/tests/test_sem_230_information_flow_control.py", + "implementations/python/packages/raes_runtime/participant_crossing_policy.py", + "implementations/python/packages/raes_conformance/conformance/participant_policy_probes.py", + "implementations/python/tests/test_run_319_participant_flow_policy.py", + "implementations/python/tests/test_asr_535_participant_flow_assurance.py" + ] + }, + "source_refs": [ + "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "clarkson-schneider-2010", + "fagin-halpern-moses-vardi-1995", + "goguen-meseguer-1982", + "milner-1980", + "sabelfeld-sands-2009", + "van-glabbeek-1990" + ] + }, + "io-alternating-refinement": { + "relation_id": "io-alternating-refinement", + "display_name": "Input/output alternating refinement", + "relation_class": "behavioral", + "definition": "A directional concrete-to-abstract relation preserves abstract outputs and internal behavior while respecting input ownership and declared action-availability obligations against environment choices.", + "left_carrier": "A concrete backend participant I/O transition system.", + "right_carrier": "An abstract RAES participant I/O transition system.", + "initial_states": "Every concrete initial participant decision state, including decision epoch zero, relates to an abstract initial decision state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Participant proposals are inputs; participant views and observations are outputs; backend, scheduler, and environment labels retain their declared owners.", + "transition_relation": "Concrete and abstract I/O-labelled step relations under a declared refinement mapping.", + "observable_actions": "Participant-visible inputs and outputs under the named projection.", + "hidden_actions": "Only governed backend/internal labels mapped to tau by the named projection.", + "stuttering_actions": "Finite hidden concrete paths may match one abstract step only when the selected weak or branching treatment permits them." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant and audience", + "policy_ref": "participant-observation-boundary", + "policy_revision": "The exact-cut projection policy used by the claim", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Decision epochs are preserved under hidden stuttering; state cuts retain their declared order model.", + "simultaneity_treatment": "Simultaneity and partial-order frontiers are preserved only when declared by the claim." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over related reachable concrete and abstract states.", + "traces": "All concrete traces induced by quantified inputs, outputs, and environment choices.", + "schedulers": "All schedulers in the declared fairness class.", + "strategies": "All participant and environment strategies in the declared action-ownership classes.", + "environments": "All environment choices admitted by the declared alternating quantifiers.", + "observations": "Under the exact named participant projection and delivery semantics." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "Input, output, scheduler, backend, and environment choices are separately owned and quantified." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, simultaneous, step, or true-concurrency semantics must be declared." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability requires a separately governed probabilistic alternating relation." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require explicit clock, fairness, timeout, and progress semantics." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim relates declared causal frontiers rather than arbitrary linearizations." + } + }, + "preservation": { + "property": "Projected concrete traces remain abstractly admitted and declared participant inputs and outputs retain their availability and ownership obligations.", + "proof_obligation": "Supply the refinement relation, initial-state mapping, input/output ownership, availability and fairness obligations, and alternating step correspondence for every quantified choice." + }, + "bounded_evidence": [ + "Decision-surface lifecycle tests may falsify selected initiality, delivery, availability, freshness, and step-matching cases on finite models." + ], + "explicit_non_claims": [ + "Trace inclusion alone does not establish input availability or alternating refinement.", + "Successful participant loops do not establish the universal relation." + ], + "incompatible_claim_surfaces": [ + "Current bounded backend conformance reports" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md", + "implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py", + "implementations/python/tests/test_behavioral_relations.py" + ] + }, + "source_refs": [ + "alur-henzinger-kupferman-vardi-1998", + "lynch-tuttle-1989", + "lynch-vaandrager-1995" + ] + }, + "epistemic-indistinguishability": { + "relation_id": "epistemic-indistinguishability", + "display_name": "Epistemic indistinguishability", + "relation_class": "epistemic", + "definition": "Two worlds are indistinguishable to an agent when they occupy the same governed information set.", + "left_carrier": "One epistemic world/state.", + "right_carrier": "Another epistemic world/state.", + "initial_states": "Worlds in the same agent-indexed accessibility or information relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Optional temporal or action labels of the epistemic model.", + "transition_relation": "The declared interpreted-system or Kripke transition relation.", + "observable_actions": "Agent-observable propositions and events.", + "hidden_actions": "Facts excluded by the information projection.", + "stuttering_actions": "Stuttering is model-specific." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named agent", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "All worlds in the selected information relation.", + "traces": "Histories only when the interpreted-system model includes them.", + "schedulers": "All schedulers represented in the model.", + "strategies": "Strategies are outside the relation itself.", + "environments": "All environments represented by possible worlds.", + "observations": "One agent or explicitly named group." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Truth of formulas invariant over the selected information set, subject to the logic.", + "proof_obligation": "Define possible worlds, the agent-indexed indistinguishability relation, valuation, and any temporal interaction." + }, + "bounded_evidence": [ + "Equal projected finite histories may be evidence for a bounded information-state comparison." + ], + "explicit_non_claims": [ + "Does not follow from global-state equality and does not establish strategic equivalence." + ], + "incompatible_claim_surfaces": [ + "Current participant conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "fagin-halpern-moses-vardi-1995" + ] + }, + "alternating-strategic-equivalence": { + "relation_id": "alternating-strategic-equivalence", + "display_name": "Alternating or strategic equivalence", + "relation_class": "strategic", + "definition": "Two game structures preserve the abilities of named coalitions under explicit strategy and environment quantifiers.", + "left_carrier": "One concurrent or alternating game structure.", + "right_carrier": "Another concurrent or alternating game structure.", + "initial_states": "Related initial game states.", + "transition_signature": { + "applicability": "applicable", + "labels": "Joint actions, chance outcomes, and state transitions.", + "transition_relation": "Both game transition functions or relations.", + "observable_actions": "Player observations and public actions.", + "hidden_actions": "Hidden information under the named observation partitions.", + "stuttering_actions": "Stuttering, simultaneous moves, and scheduler steps are explicit." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named players or coalition", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related game states.", + "traces": "Outcome paths induced by quantified strategies.", + "schedulers": "Schedulers and chance kernels explicitly quantified.", + "strategies": "Coalitions and strategy classes universally/existentially quantified as declared.", + "environments": "Adversarial environment choices explicitly quantified.", + "observations": "Player-indexed observation partitions." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Environment and scheduler choices are explicit." + }, + "concurrency": { + "status": "supported", + "treatment": "Joint and simultaneous moves are part of the game structure." + }, + "probability": { + "status": "parameterized", + "treatment": "Chance kernels must be declared when present." + }, + "time": { + "status": "parameterized", + "treatment": "Timed strategies require an explicit clock model." + }, + "partial_order": { + "status": "parameterized", + "treatment": "Concurrent action order is part of the declared game semantics." + } + }, + "preservation": { + "property": "Coalition ability for the stated objective class.", + "proof_obligation": "Define players, legal joint actions, observations, strategy class, coalitions, chance, scheduler/fairness, objectives, and an alternating relation in both directions." + }, + "bounded_evidence": [ + "Finite recorded joint-action traces can only falsify selected cases." + ], + "explicit_non_claims": [ + "Capability declarations and shared probe outcomes do not establish strategic equivalence." + ], + "incompatible_claim_surfaces": [ + "Current multi-agent conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "alur-henzinger-kupferman-vardi-1998", + "alur-henzinger-kupferman-2002" + ] + }, + "probabilistic-bisimulation": { + "relation_id": "probabilistic-bisimulation", + "display_name": "Probabilistic bisimulation", + "relation_class": "behavioral", + "definition": "Related states match labelled probability distributions over equivalence classes under the selected probabilistic process model.", + "left_carrier": "One probabilistic labelled transition system.", + "right_carrier": "Another probabilistic labelled transition system.", + "initial_states": "Initial states belong to the probabilistic bisimulation relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Shared visible and hidden labels of the probabilistic model.", + "transition_relation": "Labelled transitions to probability distributions.", + "observable_actions": "Labels retained by the projection.", + "hidden_actions": "Declared hidden labels.", + "stuttering_actions": "Weak variants require an explicit probabilistic tau closure." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Probabilistic process observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related states.", + "traces": "All probabilistic traces or branching distributions required by the variant.", + "schedulers": "All nondeterministic schedulers explicitly quantified.", + "strategies": "Outside scope unless combined with games.", + "environments": "All admitted probabilistic environments.", + "observations": "Through the named projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Scheduler quantification is mandatory." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The process-composition semantics must be named." + }, + "probability": { + "status": "supported", + "treatment": "Probability distributions are matched over relation classes." + }, + "time": { + "status": "outside-scope", + "treatment": "Continuous or timed probability needs another variant." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The base relation uses labelled branching structure." + } + }, + "preservation": { + "property": "Probability mass over related behavior classes.", + "proof_obligation": "Exhibit a relation whose matched transitions assign equal probability to every relation-closed class under the chosen variant." + }, + "bounded_evidence": [ + "Statistical samples may refute parameters but do not prove distributional branching equivalence." + ], + "explicit_non_claims": [ + "Statistical similarity or equal sample means is not probabilistic bisimulation." + ], + "incompatible_claim_surfaces": [ + "Ordinary empirical study" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "larsen-skou-1991" + ] + }, + "statistical-similarity": { + "relation_id": "statistical-similarity", + "display_name": "Statistical similarity", + "relation_class": "empirical", + "definition": "A predeclared metric over sampled populations lies within a stated similarity criterion with uncertainty.", + "left_carrier": "One sampled population or system output distribution.", + "right_carrier": "Another sampled population, target distribution, or reference data.", + "initial_states": "The preregistered sampling frame and apparatus context.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Statistical similarity compares sampled measures, not transition systems unless a separate model binds them." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study analyst", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Sampled observations only.", + "traces": "Sampled run outcomes only.", + "schedulers": "Schedulers represented by the sampling design.", + "strategies": "Strategies represented by the sampling design.", + "environments": "The preregistered population and apparatus.", + "observations": "The named metric/estimand projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Variation is represented through the sampling model." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency matters only through measured outcomes." + }, + "probability": { + "status": "supported", + "treatment": "The sampling distribution and uncertainty method are explicit." + }, + "time": { + "status": "parameterized", + "treatment": "Sampling windows and time domains are declared." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Event partial order is not inferred from aggregate metrics." + } + }, + "preservation": { + "property": "The stated similarity criterion for the named estimand and population.", + "proof_obligation": "Predeclare population, sampling frame, metric, criterion, uncertainty method, and decision rule; then execute the study." + }, + "bounded_evidence": [ + "Experiment runs, derived measures, and uncertainty intervals." + ], + "explicit_non_claims": [ + "Does not establish behavioral, epistemic, strategic, or probabilistic bisimulation." + ], + "incompatible_claim_surfaces": [ + "Universal backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/experiment-core/README.md" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "statistical-equivalence": { + "relation_id": "statistical-equivalence", + "display_name": "Statistical equivalence", + "relation_class": "empirical", + "definition": "A preregistered equivalence test supports that a named estimand lies within a stated equivalence margin for the sampled population.", + "left_carrier": "One sampled population or treatment.", + "right_carrier": "Another sampled population, treatment, or reference.", + "initial_states": "The preregistered sampling frame, allocation, and apparatus context.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "A statistical equivalence test does not compare enabled transitions." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study analyst", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Sampled units only.", + "traces": "Sampled run outcomes only.", + "schedulers": "Schedulers represented by allocation/sampling.", + "strategies": "Strategies represented by sampled conditions.", + "environments": "The stated target population.", + "observations": "The named estimand and measurement projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Variation is handled by the statistical model." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency is only a measured covariate unless modeled." + }, + "probability": { + "status": "supported", + "treatment": "Equivalence margins, error rates, and uncertainty are explicit." + }, + "time": { + "status": "parameterized", + "treatment": "Study windows and time domains are declared." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Aggregate equivalence does not preserve event order." + } + }, + "preservation": { + "property": "Equivalence of the named estimand within the preregistered margin.", + "proof_obligation": "Specify the equivalence hypotheses, margin, error control, sampling design, and analysis before observing results." + }, + "bounded_evidence": [ + "Experiment-study analysis and derived measures." + ], + "explicit_non_claims": [ + "Statistical equivalence is not behavioral equivalence or proof of implementation conformance." + ], + "incompatible_claim_surfaces": [ + "Bisimulation claim" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/experiment-core/README.md" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "empirical-adequacy": { + "relation_id": "empirical-adequacy", + "display_name": "Empirical adequacy", + "relation_class": "empirical", + "definition": "Observed evidence supports a purpose-relative, bounded adequacy claim for a named phenomenon and intended use.", + "left_carrier": "A model, language, implementation, or method under study.", + "right_carrier": "A defined empirical target, task, or phenomenon.", + "initial_states": "The preregistered study population, tasks, and apparatus.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Empirical adequacy may study behavior but is not itself a transition-system equivalence." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study audience", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Observed study units.", + "traces": "Observed runs and tasks.", + "schedulers": "Schedulers represented by the protocol.", + "strategies": "Strategies represented by participant/task sampling.", + "environments": "The named target population and intended use.", + "observations": "The preregistered measures and coding projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Uncontrolled variation is handled as a validity limitation." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency is measured only when the protocol names it." + }, + "probability": { + "status": "parameterized", + "treatment": "Sampling and uncertainty must be reported." + }, + "time": { + "status": "parameterized", + "treatment": "Study period and temporal validity are explicit." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Adequacy does not imply partial-order preservation." + } + }, + "preservation": { + "property": "Fitness for the explicitly stated empirical purpose within the study boundary.", + "proof_obligation": "Predeclare tasks, population, measures, success/falsification criteria, analysis, limitations, and evidence lineage." + }, + "bounded_evidence": [ + "Independent parser, authoring, review, and diagnostic-recovery studies." + ], + "explicit_non_claims": [ + "Repeated bounded observations do not establish universal semantics, conformance, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Universal language equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "bueno-1997", + "wellek-2010" + ] + } + }, + "claim_surfaces": [ + { + "surface_id": "sdl-transformation", + "intended_relation_ids": [ + "structural-validity", + "semantic-validity", + "canonical-artifact-identity" + ], + "evidence_boundary": "Deterministic phase functions and finite invariant, round-trip, canonicalization, and property tests.", + "prohibited_relation_ids": [ + "data-refinement", + "forward-simulation", + "trace-equivalence", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "No universal behavioral refinement or equivalence is currently proved." + ] + }, + { + "surface_id": "backend-realization", + "intended_relation_ids": [ + "realization-envelope-membership", + "bounded-probe-success", + "trace-inclusion", + "io-alternating-refinement" + ], + "evidence_boundary": "Envelope checks and named target probes are bounded evidence; projected trace inclusion plus input/output ownership and availability under I/O alternating refinement are the intended universal actionable-participant obligations and remain deliberately unproved.", + "prohibited_relation_ids": [ + "trace-equivalence", + "backward-simulation", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "Provisioning, snapshots, witnesses, and negative probes do not prove behavioral equivalence." + ] + }, + { + "surface_id": "backend-comparison", + "intended_relation_ids": [ + "bounded-probe-success", + "statistical-similarity", + "canonical-artifact-identity" + ], + "evidence_boundary": "Only named invariants, probes, digests, populations, metrics, and uncertainty procedures are compared.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "A shared result, digest, or finite trace is not universal same behavior." + ] + }, + { + "surface_id": "participant-visible-behavior", + "intended_relation_ids": [ + "participant-projected-history-equivalence" + ], + "evidence_boundary": "Comparison is parameterized by participant, observation-boundary policy revision, redaction, order, simultaneity, and run context.", + "prohibited_relation_ids": [ + "epistemic-indistinguishability", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "Equal projected histories do not expose hidden global state or prove future knowledge or strategy equivalence." + ] + }, + { + "surface_id": "participant-information-flow-policy", + "intended_relation_ids": [ + "policy-noninterference" + ], + "evidence_boundary": "The SEM-230 relation is defined over named participant, audience, memory scope, exact-cut policy-decision sequence, low-equivalence, adaptive low-strategy class, dynamic purge, permitted declassification schedule, scheduler/environment classes, order model, and support-set semantics. Current executable evidence is limited to finite models, finite reference-runtime enforcement probes, and finite backend-conformance cases.", + "prohibited_relation_ids": [ + "participant-projected-history-equivalence", + "trace-equivalence", + "forward-simulation", + "backward-simulation", + "data-refinement", + "strong-bisimulation", + "weak-bisimulation", + "epistemic-indistinguishability", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "Definition, catalog validation, claim-policy checks, and finite counterexamples do not prove universal noninterference or runtime/backend realization.", + "Reference-runtime enforcement and passing backend-conformance probes establish neither universal noninterference nor native-backend realization." + ] + }, + { + "surface_id": "participant-opacity", + "intended_relation_ids": [ + "participant-predicate-opacity" + ], + "evidence_boundary": "Every claim binds a revisioned observer, secret predicate, possible-point carrier, initial-information and observation functions, memory and horizon, supervisor visibility, passive or active strategy domain, release schedule, scheduler and environment classes, time and order, nondeterminism and probability support, assurance axis, and evidence boundary.", + "prohibited_relation_ids": [ + "participant-projected-history-equivalence", + "policy-noninterference", + "trace-inclusion", + "trace-equivalence", + "forward-simulation", + "backward-simulation", + "data-refinement", + "strong-bisimulation", + "weak-bisimulation", + "epistemic-indistinguishability", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "A profile, finite witness, bounded probe, random choice, model check, runtime decision, or backend declaration establishes only its named assurance axis and evidence scope.", + "No current RAES runtime or backend is claimed opaque." + ] + }, + { + "surface_id": "participant-crossing-bisimulation", + "intended_relation_ids": [ + "divergence-preserving-branching-bisimulation" + ], + "evidence_boundary": "Claims bind the exact independently derived abstract and concrete model revisions and digests, initial states, complete quantified carrier and counts, closed participant/audience projection and tau partition, relation profile, source and mapping revisions, assurance axis, pinned tool provenance, result or safe counterexample, mutations, limitations, and independent reproduction.", + "prohibited_relation_ids": [ + "strong-bisimulation", + "weak-bisimulation", + "trace-equivalence", + "policy-noninterference", + "participant-predicate-opacity", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "Issue #811 defines the theorem and proof program but does not establish the formal equivalence result.", + "A formal model-check does not establish live-runtime realization, backend conformance, whole-runtime equivalence, noninterference, opacity, or a stronger timed, probabilistic, strategic, concurrent, or partial-order relation." + ] + }, + { + "surface_id": "multi-agent-interaction", + "intended_relation_ids": [ + "bounded-probe-success", + "alternating-strategic-equivalence", + "probabilistic-bisimulation" + ], + "evidence_boundary": "Current evidence is structural and finite; strategic and probabilistic relations are definitions for future governed models.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "Current joint-action, chance, simultaneous-move, and mean-field records do not prove strategic equivalence." + ] + }, + { + "surface_id": "counterfactual-necessity-validation", + "intended_relation_ids": [ + "bounded-but-for-necessity", + "bounded-probe-success" + ], + "evidence_boundary": "One revisioned claim, one immutable baseline/intervention-world pair, one typed and verified intervention, admitted proposition-truth evidence, a declared matching policy, and independently verified reset and cleanup.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "empirical-adequacy", + "statistical-equivalence" + ], + "explicit_non_claims": [ + "A supported finite but-for comparison is not universal causal proof, actual-cause attribution, sufficiency, determinism, or statistical necessity." + ] + }, + { + "surface_id": "independent-adequacy-study", + "intended_relation_ids": [ + "empirical-adequacy", + "statistical-similarity", + "statistical-equivalence" + ], + "evidence_boundary": "Claims bind to a preregistered population, task set, metric or coding scheme, uncertainty, falsification criteria, and limitations.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "Bounded observations and statistical findings cannot be promoted to universal behavioral proof." + ] + } + ], + "worked_examples": { + "finite-probe-counterexample": { + "example_id": "finite-probe-counterexample", + "purpose": "Two implementations pass the same finite visible probe a, but the left system has an additional enabled b transition that the right system cannot match.", + "left_system": { + "states": [ + "l0", + "l1", + "l2" + ], + "initial_state": "l0", + "transitions": [ + { + "source": "l0", + "action": "a", + "target": "l1" + }, + { + "source": "l0", + "action": "b", + "target": "l2" + } + ] + }, + "right_system": { + "states": [ + "r0", + "r1" + ], + "initial_state": "r0", + "transitions": [ + { + "source": "r0", + "action": "a", + "target": "r1" + } + ] + }, + "tested_visible_trace": [ + "a" + ], + "hidden_action": "tau", + "expected_strong_bisimulation": false, + "expected_weak_matching": false, + "evidence_boundary": "The shared a probe is evidence only for that finite trace; the unmatched b branch refutes strong bisimulation.", + "explicit_non_claims": [ + "This toy counterexample is not evidence about any RAES backend." + ] + }, + "hidden-action-counterexample": { + "example_id": "hidden-action-counterexample", + "purpose": "The abstract system performs visible send directly; the backend performs governed hidden tau and then send.", + "left_system": { + "states": [ + "a0", + "a1" + ], + "initial_state": "a0", + "transitions": [ + { + "source": "a0", + "action": "send", + "target": "a1" + } + ] + }, + "right_system": { + "states": [ + "b0", + "b1", + "b2" + ], + "initial_state": "b0", + "transitions": [ + { + "source": "b0", + "action": "tau", + "target": "b1" + }, + { + "source": "b1", + "action": "send", + "target": "b2" + } + ] + }, + "tested_visible_trace": [ + "send" + ], + "hidden_action": "tau", + "expected_strong_bisimulation": false, + "expected_weak_matching": true, + "evidence_boundary": "Strong matching fails on tau; weak visible-trace matching succeeds only under the declared tau-hiding projection and finite termination assumptions.", + "explicit_non_claims": [ + "The example does not declare arbitrary backend-internal work hidden and does not prove an RAES backend relation." + ] + } + } +} diff --git a/contracts/concept-authority/history/behavioral-relations-v1-rev11.json b/contracts/concept-authority/history/behavioral-relations-v1-rev11.json new file mode 100644 index 000000000..0493720ff --- /dev/null +++ b/contracts/concept-authority/history/behavioral-relations-v1-rev11.json @@ -0,0 +1,3132 @@ +{ + "schema_version": "behavioral-relations/v1", + "taxonomy_id": "raes-behavioral-relations", + "taxonomy_revision": "rev11", + "bibliography": [ + { + "source_id": "park-1981", + "title": "Concurrency and Automata on Infinite Sequences", + "authors": [ + "David M. R. Park" + ], + "publication_year": 1981, + "publication_venue": "Theoretical Computer Science, LNCS 104", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0017309" + } + }, + { + "source_id": "milner-1980", + "title": "A Calculus of Communicating Systems", + "authors": [ + "Robin Milner" + ], + "publication_year": 1980, + "publication_venue": "Lecture Notes in Computer Science 92", + "edition_or_version": "first edition", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/3-540-10235-3" + } + }, + { + "source_id": "van-glabbeek-1990", + "title": "The Linear Time-Branching Time Spectrum", + "authors": [ + "Rob J. van Glabbeek" + ], + "publication_year": 1990, + "publication_venue": "CONCUR 1990, LNCS 458", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0039066" + } + }, + { + "source_id": "van-glabbeek-weijland-1996", + "title": "Branching Time and Abstraction in Bisimulation Semantics", + "authors": [ + "Rob J. van Glabbeek", + "W. Peter Weijland" + ], + "publication_year": 1996, + "publication_venue": "Journal of the ACM 43(3), 555-600", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/233551.233556" + } + }, + { + "source_id": "van-glabbeek-luttik-trcka-2009", + "title": "Branching Bisimilarity with Explicit Divergence", + "authors": [ + "Rob J. van Glabbeek", + "Bas Luttik", + "Nikola Trčka" + ], + "publication_year": 2009, + "publication_venue": "Fundamenta Informaticae 93(4), 371-392", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/FI-2009-109" + } + }, + { + "source_id": "abadi-lamport-1991", + "title": "The Existence of Refinement Mappings", + "authors": [ + "Martín Abadi", + "Leslie Lamport" + ], + "publication_year": 1991, + "publication_venue": "Theoretical Computer Science 82(2)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/0304-3975(91)90224-P" + } + }, + { + "source_id": "lynch-vaandrager-1995", + "title": "Forward and Backward Simulations, Part I: Untimed Systems", + "authors": [ + "Nancy A. Lynch", + "Frits W. Vaandrager" + ], + "publication_year": 1995, + "publication_venue": "Information and Computation 121(2)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1006/inco.1995.1134" + } + }, + { + "source_id": "fagin-halpern-moses-vardi-1995", + "title": "Reasoning About Knowledge", + "authors": [ + "Ronald Fagin", + "Joseph Y. Halpern", + "Yoram Moses", + "Moshe Y. Vardi" + ], + "publication_year": 1995, + "publication_venue": "MIT Press", + "edition_or_version": "hardcover first edition", + "immutable_locator": { + "kind": "isbn", + "value": "9780262061629" + } + }, + { + "source_id": "goguen-meseguer-1982", + "title": "Security Policies and Security Models", + "authors": [ + "Joseph A. Goguen", + "José Meseguer" + ], + "publication_year": 1982, + "publication_venue": "1982 IEEE Symposium on Security and Privacy", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/SP.1982.10014" + } + }, + { + "source_id": "sabelfeld-sands-2009", + "title": "Declassification: Dimensions and Principles", + "authors": [ + "Andrei Sabelfeld", + "David Sands" + ], + "publication_year": 2009, + "publication_venue": "Journal of Computer Security 17(5)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/JCS-2009-0352" + } + }, + { + "source_id": "lynch-tuttle-1989", + "title": "An Introduction to Input/Output Automata", + "authors": [ + "Nancy A. Lynch", + "Mark R. Tuttle" + ], + "publication_year": 1989, + "publication_venue": "CWI Quarterly 2(3), 219-246", + "edition_or_version": "published journal article", + "immutable_locator": { + "kind": "report", + "value": "MIT/LCS/TM-373" + } + }, + { + "source_id": "clarkson-schneider-2010", + "title": "Hyperproperties", + "authors": [ + "Michael R. Clarkson", + "Fred B. Schneider" + ], + "publication_year": 2010, + "publication_venue": "Journal of Computer Security 18(6), 1157-1210", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/JCS-2009-0393" + } + }, + { + "source_id": "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "title": "Reactive Noninterference", + "authors": [ + "Aaron Bohannon", + "Benjamin C. Pierce", + "Vilhelm Sjöberg", + "Stephanie Weirich", + "Steve Zdancewic" + ], + "publication_year": 2009, + "publication_venue": "Proceedings of the 16th ACM Conference on Computer and Communications Security, 79-90", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/1653662.1653673" + } + }, + { + "source_id": "alur-henzinger-kupferman-vardi-1998", + "title": "Alternating Refinement Relations", + "authors": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman", + "Moshe Y. Vardi" + ], + "publication_year": 1998, + "publication_venue": "CONCUR 1998, LNCS 1466", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0055622" + } + }, + { + "source_id": "alur-henzinger-kupferman-2002", + "title": "Alternating-Time Temporal Logic", + "authors": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman" + ], + "publication_year": 2002, + "publication_venue": "Journal of the ACM 49(5)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/585265.585270" + } + }, + { + "source_id": "larsen-skou-1991", + "title": "Bisimulation Through Probabilistic Testing", + "authors": [ + "Kim G. Larsen", + "Arne Skou" + ], + "publication_year": 1991, + "publication_venue": "Information and Computation 94(1)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/0890-5401(91)90030-6" + } + }, + { + "source_id": "wellek-2010", + "title": "Testing Statistical Hypotheses of Equivalence and Noninferiority", + "authors": [ + "Stefan Wellek" + ], + "publication_year": 2010, + "publication_venue": "Chapman and Hall/CRC", + "edition_or_version": "second edition", + "immutable_locator": { + "kind": "isbn", + "value": "9781439808184" + } + }, + { + "source_id": "bueno-1997", + "title": "Empirical Adequacy: A Partial Structures Approach", + "authors": [ + "Otávio Bueno" + ], + "publication_year": 1997, + "publication_venue": "Studies in History and Philosophy of Science Part A 28(4)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/S0039-3681(97)00012-5" + } + }, + { + "source_id": "halpern-pearl-2005", + "title": "Causes and Explanations: A Structural-Model Approach. Part I: Causes", + "authors": [ + "Joseph Y. Halpern", + "Judea Pearl" + ], + "publication_year": 2005, + "publication_venue": "The British Journal for the Philosophy of Science 56(4)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1093/bjps/axi147" + } + }, + { + "source_id": "bryans-koutny-mazare-ryan-2008", + "title": "Opacity Generalised to Transition Systems", + "authors": [ + "Jeremy W. Bryans", + "Maciej Koutny", + "Laurent Mazaré", + "Peter Y. A. Ryan" + ], + "publication_year": 2008, + "publication_venue": "International Journal of Information Security 7(6), 421-435", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/s10207-008-0058-x" + } + }, + { + "source_id": "schoepe-sabelfeld-2015", + "title": "Understanding and Enforcing Opacity", + "authors": [ + "Daniel Schoepe", + "Andrei Sabelfeld" + ], + "publication_year": 2015, + "publication_venue": "2015 IEEE Computer Security Foundations Symposium, 539-553", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/CSF.2015.41" + } + }, + { + "source_id": "lin-2011", + "title": "Opacity of Discrete Event Systems and its Applications", + "authors": [ + "Feng Lin" + ], + "publication_year": 2011, + "publication_venue": "Automatica 47(3), 496-503", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/j.automatica.2011.01.002" + } + }, + { + "source_id": "saboori-hadjicostis-2012", + "title": "Verification of Infinite-Step Opacity and Complexity Considerations", + "authors": [ + "Anooshiravan Saboori", + "Christoforos N. Hadjicostis" + ], + "publication_year": 2012, + "publication_venue": "IEEE Transactions on Automatic Control 57(5), 1265-1269", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2011.2173774" + } + }, + { + "source_id": "badouel-bednarczyk-borzyszkowski-caillaud-darondeau-2007", + "title": "Concurrent Secrets", + "authors": [ + "Éric Badouel", + "Marek A. Bednarczyk", + "Andrzej M. Borzyszkowski", + "Benoît Caillaud", + "Philippe Darondeau" + ], + "publication_year": 2007, + "publication_venue": "Discrete Event Dynamic Systems 17(4), 425-446", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/s10626-007-0020-5" + } + }, + { + "source_id": "yin-lafortune-2016", + "title": "A Uniform Approach for Synthesizing Property-Enforcing Supervisors for Partially-Observed Discrete-Event Systems", + "authors": [ + "Xiang Yin", + "Stéphane Lafortune" + ], + "publication_year": 2016, + "publication_venue": "IEEE Transactions on Automatic Control 61(8), 2140-2154", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2015.2484359" + } + }, + { + "source_id": "xie-yin-li-2022", + "title": "Opacity Enforcing Supervisory Control Using Nondeterministic Supervisors", + "authors": [ + "Yifan Xie", + "Xiang Yin", + "Shaoyuan Li" + ], + "publication_year": 2022, + "publication_venue": "IEEE Transactions on Automatic Control 67(12), 6567-6582", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2021.3131125" + } + }, + { + "source_id": "cui-ma-giua-yin-2026", + "title": "Opacity Enforcing Supervisory Control with a Priori Unknown Supervisors", + "authors": [ + "Juntang Cui", + "Ziyue Ma", + "Alessandro Giua", + "Xiang Yin" + ], + "publication_year": 2026, + "publication_venue": "arXiv preprint arXiv:2604.04070", + "edition_or_version": "preprint version 1", + "immutable_locator": { + "kind": "doi", + "value": "10.48550/arXiv.2604.04070" + } + }, + { + "source_id": "partovi-jung-hai-2020", + "title": "Opacity of Discrete Event Systems with Active Intruder", + "authors": [ + "Alireza Partovi", + "Taeho Jung", + "Lin Hai" + ], + "publication_year": 2020, + "publication_venue": "arXiv preprint arXiv:2007.14960", + "edition_or_version": "preprint version 1", + "immutable_locator": { + "kind": "doi", + "value": "10.48550/arXiv.2007.14960" + } + }, + { + "source_id": "berard-mullins-sassolas-2015", + "title": "Quantifying Opacity", + "authors": [ + "Béatrice Bérard", + "John Mullins", + "Mathieu Sassolas" + ], + "publication_year": 2015, + "publication_venue": "Mathematical Structures in Computer Science 25(2), 361-403", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1017/S0960129513000637" + } + }, + { + "source_id": "andre-lime-marinho-sun-2022", + "title": "Guaranteeing Timed Opacity using Parametric Timed Model Checking", + "authors": [ + "Étienne André", + "Didier Lime", + "Dylan Marinho", + "Jun Sun" + ], + "publication_year": 2022, + "publication_venue": "ACM Transactions on Software Engineering and Methodology 31(4), 64:1-64:36", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/3502851" + } + }, + { + "source_id": "broberg-van-delft-sands-2015", + "title": "The Anatomy and Facets of Dynamic Policies", + "authors": [ + "Niklas Broberg", + "Bart van Delft", + "David Sands" + ], + "publication_year": 2015, + "publication_venue": "2015 IEEE Computer Security Foundations Symposium, 122-137", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/CSF.2015.16" + } + } + ], + "relations": { + "structural-validity": { + "relation_id": "structural-validity", + "display_name": "Structural validity", + "relation_class": "predicate", + "definition": "A single artifact satisfies its published closed structural schema.", + "left_carrier": "An artifact payload.", + "right_carrier": "The published schema selected by the artifact discriminator.", + "initial_states": "Not applicable; this is a unary predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Schema validity is a predicate over an artifact and schema, not a transition-system relation." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Schema validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one artifact payload and one schema revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Conformance to the declared structural shape.", + "proof_obligation": "Validate the complete payload against the named published schema." + }, + "bounded_evidence": [ + "JSON Schema and closed-model validation of named artifacts." + ], + "explicit_non_claims": [ + "Does not establish semantic validity, executability, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Backend equivalence", + "Participant strategic behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "contracts/schemas/" + ] + }, + "source_refs": [ + "milner-1980" + ] + }, + "semantic-validity": { + "relation_id": "semantic-validity", + "display_name": "Semantic validity", + "relation_class": "predicate", + "definition": "A structurally admitted artifact satisfies the named cross-reference and domain invariants.", + "left_carrier": "A parsed RAES artifact.", + "right_carrier": "The named semantic invariant set.", + "initial_states": "Not applicable; this is a unary predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Semantic validity checks a static artifact model rather than matching transitions." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Semantic validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one admitted artifact and one invariant revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "The named static semantic invariants.", + "proof_obligation": "Run every invariant in the declared semantic profile without error." + }, + "bounded_evidence": [ + "SemanticValidator results and invariant mutation tests." + ], + "explicit_non_claims": [ + "Does not establish realization, execution success, trace inclusion, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Runtime conformance", + "Backend comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes/validator/" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "capability-declaration": { + "relation_id": "capability-declaration", + "display_name": "Capability declaration", + "relation_class": "predicate", + "definition": "An apparatus declares support for governed capability and contract identifiers.", + "left_carrier": "A processor, backend, or participant manifest.", + "right_carrier": "The governed capability and contract vocabulary.", + "initial_states": "Not applicable; this is a declaration predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "A declaration is not execution behavior." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Manifest consumer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one manifest revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Portable declared support metadata.", + "proof_obligation": "Validate the manifest and resolve every governed identifier." + }, + "bounded_evidence": [ + "Manifest schema validation and capability-gap conformance cases." + ], + "explicit_non_claims": [ + "Does not prove that a declared capability works for every input." + ], + "incompatible_claim_surfaces": [ + "Universal backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes_contracts/manifest_authority.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "profile-satisfaction": { + "relation_id": "profile-satisfaction", + "display_name": "Profile satisfaction", + "relation_class": "predicate", + "definition": "An artifact bundle satisfies every required concern in a named profile revision.", + "left_carrier": "An artifact or bundle.", + "right_carrier": "A governed profile with required concerns.", + "initial_states": "Not applicable; this is a profile predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Profile satisfaction aggregates named gates; it is not a behavioral matching relation." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Profile evaluator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one artifact bundle and one profile revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "The conjunction of the profile's required concerns.", + "proof_obligation": "Evaluate every required concern with the profile's named validator." + }, + "bounded_evidence": [ + "Scientific completeness and backend-profile case results." + ], + "explicit_non_claims": [ + "Does not promote profile satisfaction to behavioral equivalence or empirical adequacy." + ], + "incompatible_claim_surfaces": [ + "Behavioral equivalence", + "Scientific adequacy" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "contracts/profiles/" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "bounded-probe-success": { + "relation_id": "bounded-probe-success", + "display_name": "Bounded fixture or probe success", + "relation_class": "empirical", + "definition": "Every named finite fixture or probe in the disclosed run produced its expected result.", + "left_carrier": "A concrete implementation run.", + "right_carrier": "A finite, enumerated fixture or probe set.", + "initial_states": "The concrete initial state selected by each named case.", + "transition_signature": { + "applicability": "applicable", + "labels": "The actions exercised by the named cases.", + "transition_relation": "Only transitions actually exercised by the finite cases.", + "observable_actions": "Case outputs and sanitized diagnostics.", + "hidden_actions": "No hidden action unless a governed projection declares one.", + "stuttering_actions": "Stuttering is explicit and relation-specific." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Case reporter", + "policy_ref": "behavioral-relations/bounded-probe-projection", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Only states reached by named cases.", + "traces": "Only enumerated finite traces.", + "schedulers": "Only schedulers exercised by the harness.", + "strategies": "Only strategies exercised by the harness.", + "environments": "Only named environments.", + "observations": "Only observations emitted by named cases." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Success of the enumerated cases.", + "proof_obligation": "Execute every named case and compare its bounded expected result." + }, + "bounded_evidence": [ + "Fixture-suite and target-probe reports with exact case identifiers." + ], + "explicit_non_claims": [ + "Does not quantify over untested transitions, schedulers, strategies, or environments.", + "Does not establish trace equivalence, simulation, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Universal conformance", + "Backend equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes_conformance/conformance.py" + ] + }, + "source_refs": [ + "park-1981", + "van-glabbeek-1990" + ] + }, + "bounded-but-for-necessity": { + "relation_id": "bounded-but-for-necessity", + "display_name": "Bounded but-for necessity", + "relation_class": "empirical", + "definition": "Within one declared finite causal boundary, a named candidate is supported as necessary for a named outcome when the admitted baseline outcome is true, a verified intervention removes or disables only that candidate, the matched counterfactual outcome is false, and evidence, reset, and cleanup gates pass.", + "left_carrier": "A named condition, weakness, control, or behavior present in the admitted baseline world.", + "right_carrier": "A governed outcome proposition evaluated independently in the baseline and intervention worlds.", + "initial_states": "The exact admitted baseline lineage and matching policy for the finite world pair.", + "transition_signature": { + "applicability": "applicable", + "labels": "The admitted baseline execution, typed candidate intervention, counterfactual execution, observation, reset, and cleanup actions.", + "transition_relation": "Only the two immutable runs and the one declared intervention admitted by the comparison case.", + "observable_actions": "Governed outcome truth, intervention evidence, matching checks, and reset or cleanup evidence.", + "hidden_actions": "No hidden action may change a held-fixed dimension; any permitted nuisance variation must be declared by the matching policy.", + "stuttering_actions": "Stuttering is relevant only when the declared time and observation models admit it." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Bounded necessity comparison audience", + "policy_ref": "behavioral-relations/bounded-but-for-necessity-projection", + "policy_revision": "rev1", + "redaction_scope": "Only governed evidence references and stable redacted diagnostics cross the comparison boundary.", + "order_treatment": "World executions are distinct immutable runs; their declared matching policy, not wall-clock order, governs comparison.", + "simultaneity_treatment": "Simultaneity is outside the binary criterion unless the matching policy and time model explicitly preserve it." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Only the admitted baseline and intervention-world initial states and reached states.", + "traces": "Only the two named finite executions.", + "schedulers": "Only schedulers admitted and matched by the declared policy.", + "strategies": "Only participant strategies represented and matched in the two runs.", + "environments": "Only the named apparatus, backend, scenario family, and permitted nuisance variation.", + "observations": "Only governed outcome, intervention, comparability, reset, and cleanup evidence for the named case." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Random streams, seeds, draws, and uncertainty are governed by the matching policy; a shared seed alone is insufficient." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Scheduling and participant concurrency must be held fixed or declared as permitted nuisance variation." + }, + "probability": { + "status": "outside-scope", + "treatment": "The binary criterion does not establish probabilistic necessity; statistical criteria require a separate governed relation or adapter." + }, + "time": { + "status": "parameterized", + "treatment": "Both worlds bind an explicit time model and comparison boundary; wall-clock proximity is not comparability." + }, + "partial_order": { + "status": "parameterized", + "treatment": "Causal or partial-order differences must be held fixed or explicitly admitted by the matching policy." + } + }, + "preservation": { + "property": "A finite binary but-for result for the exact candidate, outcome, worlds, intervention, apparatus, and matching policy.", + "proof_obligation": "Admit a true baseline outcome, verify the candidate intervention, admit a false counterfactual outcome, prove the declared matching checks for every other semantic dimension, and independently verify reset and cleanup." + }, + "bounded_evidence": [ + "Immutable experiment-run identities, proposition-truth results, intervention evidence, matching-policy checks, and reset or cleanup evidence for one named comparison case." + ], + "explicit_non_claims": [ + "Does not establish universal, actual, sufficient, probabilistic, or model-identified causation.", + "Does not treat replay, temporal order, correlation, failed execution, a no-op intervention, or incomparable outcome differences as necessity evidence.", + "Does not promote a supported finite comparison to proof or falsification-backed validation strength." + ], + "incompatible_claim_surfaces": [ + "Universal causal proof", + "Unbounded actual-cause attribution", + "Statistical or probabilistic necessity without a separate criterion" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes_conformance/necessity_evidence.py", + "implementations/python/packages/raes_conformance/necessity_types.py", + "implementations/python/packages/raes_conformance/necessity_validation.py", + "implementations/python/tests/test_necessity_validation.py" + ] + }, + "source_refs": [ + "halpern-pearl-2005" + ] + }, + "canonical-artifact-identity": { + "relation_id": "canonical-artifact-identity", + "display_name": "Canonical artifact identity", + "relation_class": "predicate", + "definition": "Two artifacts have identical canonical bytes or digest under one named serialization profile.", + "left_carrier": "One canonical artifact.", + "right_carrier": "Another canonical artifact under the same profile.", + "initial_states": "Not applicable; this is artifact identity.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Digest identity compares canonical representations, not enabled behavior." + }, + "observation_projection": { + "applicability": "identity", + "subject": "Canonical serializer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "symmetric", + "quantification": { + "states": "For the two named canonical artifacts.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Canonical byte identity under the named profile.", + "proof_obligation": "Canonicalize both artifacts with the same revision and compare bytes or collision-resistant digests." + }, + "bounded_evidence": [ + "Canonicalization and digest equality tests." + ], + "explicit_non_claims": [ + "Does not establish common provenance, equal executions, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Trace comparison", + "Backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes/canonical.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "realization-envelope-membership": { + "relation_id": "realization-envelope-membership", + "display_name": "Realization-envelope membership", + "relation_class": "set-relation", + "definition": "A concrete or requested point belongs to a governed realization envelope.", + "left_carrier": "A realization point.", + "right_carrier": "A closed realization-envelope set.", + "initial_states": "Not applicable; this is set membership.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Envelope membership is set-theoretic support, not a transition match." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Realization-envelope validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "left-to-right", + "quantification": { + "states": "For one point and one envelope revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Membership in the declared support set.", + "proof_obligation": "Evaluate every envelope dimension and closure rule for the point." + }, + "bounded_evidence": [ + "Witness and negative-probe envelope tests." + ], + "explicit_non_claims": [ + "Does not establish that execution succeeds or that behavior refines an abstract runtime." + ], + "incompatible_claim_surfaces": [ + "Runtime trace inclusion", + "Backend equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/realization/envelope-semantics.md" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "realization-envelope-subsumption": { + "relation_id": "realization-envelope-subsumption", + "display_name": "Realization-envelope subsumption", + "relation_class": "set-relation", + "definition": "Every point admitted by one realization envelope is admitted by another under the named closure rules.", + "left_carrier": "One realization-envelope set.", + "right_carrier": "Another realization-envelope set.", + "initial_states": "Not applicable; this is set inclusion.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Envelope subsumption compares support sets rather than transition systems." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Realization-envelope validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "left-to-right", + "quantification": { + "states": "Universally over points in the left envelope.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Set inclusion of declared realization support.", + "proof_obligation": "Prove or decide inclusion for every governed envelope dimension." + }, + "bounded_evidence": [ + "Finite witness and mutation tests for current envelope operators." + ], + "explicit_non_claims": [ + "Does not establish behavioral refinement, trace inclusion, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Runtime behavior", + "Participant behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes_contracts/realization_envelope.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "trace-inclusion": { + "relation_id": "trace-inclusion", + "display_name": "Projected trace inclusion", + "relation_class": "behavioral", + "definition": "Every projected concrete trace belongs to the abstract trace set under a declared projection.", + "left_carrier": "Concrete implementation transition system.", + "right_carrier": "Abstract RAES transition system.", + "initial_states": "Related concrete and abstract initial states.", + "transition_signature": { + "applicability": "applicable", + "labels": "Labels in the declared concrete and abstract alphabets.", + "transition_relation": "Concrete and abstract labelled transition relations.", + "observable_actions": "Actions retained by the governed projection.", + "hidden_actions": "Only actions explicitly hidden by the projection.", + "stuttering_actions": "Concrete stuttering must be permitted by the abstract obligation." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named observer or abstraction", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "All reachable related states.", + "traces": "Universally over projected concrete traces.", + "schedulers": "All admitted schedulers unless narrowed.", + "strategies": "Outside scope unless the systems are strategic.", + "environments": "All admitted environments unless narrowed.", + "observations": "Through the named projection only." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Abstract trace safety for projected concrete executions.", + "proof_obligation": "Show Proj(Traces_concrete) is a subset of Traces_abstract under stated fairness and divergence assumptions." + }, + "bounded_evidence": [ + "Finite target probes can falsify but cannot prove universal inclusion." + ], + "explicit_non_claims": [ + "Does not establish completeness, reverse inclusion, trace equivalence, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Current backend conformance report" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-runtime/README.md" + ] + }, + "source_refs": [ + "van-glabbeek-1990", + "abadi-lamport-1991", + "lynch-vaandrager-1995" + ] + }, + "trace-equivalence": { + "relation_id": "trace-equivalence", + "display_name": "Trace equivalence", + "relation_class": "behavioral", + "definition": "Two systems have equal projected trace sets under the same declared alphabet and projection.", + "left_carrier": "One labelled transition system.", + "right_carrier": "Another labelled transition system.", + "initial_states": "Paired initial states.", + "transition_signature": { + "applicability": "applicable", + "labels": "A shared declared label alphabet.", + "transition_relation": "The two labelled transition relations.", + "observable_actions": "Labels retained by the common projection.", + "hidden_actions": "Labels hidden by the common projection.", + "stuttering_actions": "Stuttering treatment must be identical." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named comparison observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "All reachable states contributing traces.", + "traces": "Universally over both trace sets.", + "schedulers": "All admitted schedulers.", + "strategies": "Outside scope unless strategies are encoded.", + "environments": "All declared environments.", + "observations": "Through one common projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Equality of projected trace languages.", + "proof_obligation": "Prove both projected trace inclusions under identical assumptions." + }, + "bounded_evidence": [ + "Finite trace comparison may refute but cannot establish equality." + ], + "explicit_non_claims": [ + "Does not preserve branching structure and does not imply bisimulation." + ], + "incompatible_claim_surfaces": [ + "Finite backend comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "van-glabbeek-1990" + ] + }, + "forward-simulation": { + "relation_id": "forward-simulation", + "display_name": "Forward simulation", + "relation_class": "behavioral", + "definition": "A relation maps each concrete step to an abstract matching path while preserving related states.", + "left_carrier": "Concrete implementation states.", + "right_carrier": "Abstract specification states.", + "initial_states": "Every concrete initial state relates to an abstract initial state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Concrete and abstract labels under a declared matching function.", + "transition_relation": "Concrete and abstract step relations.", + "observable_actions": "Labels exposed by the abstraction.", + "hidden_actions": "Labels mapped to hidden or stuttering abstract behavior.", + "stuttering_actions": "Explicit abstract stuttering where allowed." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Abstraction observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over related reachable states.", + "traces": "All concrete traces induced by matched steps.", + "schedulers": "All admitted concrete choices.", + "strategies": "Outside scope unless extended strategically.", + "environments": "All admitted environments.", + "observations": "Under the named abstraction." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Usually projected trace inclusion and named safety properties.", + "proof_obligation": "Supply a simulation relation and discharge initiality plus step-correspondence obligations." + }, + "bounded_evidence": [ + "Tests may exercise candidate obligations on finite models only." + ], + "explicit_non_claims": [ + "Successful probes do not establish a simulation relation." + ], + "incompatible_claim_surfaces": [ + "Current conformance results" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "lynch-vaandrager-1995", + "abadi-lamport-1991" + ] + }, + "backward-simulation": { + "relation_id": "backward-simulation", + "display_name": "Backward simulation", + "relation_class": "behavioral", + "definition": "A relation reasons from abstract successor possibilities back to concrete predecessors to establish implementation inclusion where forward simulation is insufficient.", + "left_carrier": "Concrete implementation states.", + "right_carrier": "Abstract specification states.", + "initial_states": "Initial and reachable-state coverage follow the selected backward-simulation theorem.", + "transition_signature": { + "applicability": "applicable", + "labels": "Concrete and abstract labels under the theorem's matching rule.", + "transition_relation": "Concrete and abstract step relations.", + "observable_actions": "Declared external labels.", + "hidden_actions": "Declared internal labels.", + "stuttering_actions": "History, prophecy, and stuttering treatment must be explicit." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Abstraction observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over theorem-defined related states.", + "traces": "All represented implementation behaviors.", + "schedulers": "All admitted nondeterministic choices.", + "strategies": "Outside scope unless extended strategically.", + "environments": "All admitted environments.", + "observations": "Under the named abstraction." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Implementation behavior inclusion under the cited theorem's assumptions.", + "proof_obligation": "Supply a backward simulation relation and discharge its reachability, initiality, and step obligations." + }, + "bounded_evidence": [ + "Finite model tests can exercise examples but do not prove a backend relation." + ], + "explicit_non_claims": [ + "Does not follow from result equality or a forward-only sampled trace." + ], + "incompatible_claim_surfaces": [ + "Current backend conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "lynch-vaandrager-1995" + ] + }, + "data-refinement": { + "relation_id": "data-refinement", + "display_name": "Data refinement", + "relation_class": "behavioral", + "definition": "Concrete data states represent abstract states through a retrieve relation while operations preserve that relation.", + "left_carrier": "Concrete state and operation space.", + "right_carrier": "Abstract state and operation space.", + "initial_states": "Concrete and abstract initial states related by the retrieve relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Operation invocations and observations.", + "transition_relation": "Concrete and abstract operation relations.", + "observable_actions": "Client-visible operation effects.", + "hidden_actions": "Internal representation steps.", + "stuttering_actions": "Stuttering and enabledness obligations are method-specific." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Client observation", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "All states satisfying the retrieve relation.", + "traces": "All operation histories covered by the refinement method.", + "schedulers": "All admitted operation nondeterminism.", + "strategies": "Outside scope unless strategies are modeled.", + "environments": "All client environments under stated preconditions.", + "observations": "Client-visible results only." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "The abstract operation contract and selected client-observable properties.", + "proof_obligation": "Define the retrieve relation and discharge initialization, applicability/enabledness, and correctness obligations." + }, + "bounded_evidence": [ + "Contract and operation tests are bounded evidence only." + ], + "explicit_non_claims": [ + "An SDL transformation function is not data refinement without these obligations." + ], + "incompatible_claim_surfaces": [ + "SDL phase transformation" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "abadi-lamport-1991", + "lynch-vaandrager-1995" + ] + }, + "strong-bisimulation": { + "relation_id": "strong-bisimulation", + "display_name": "Strong Park-Milner bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric relation matches every labelled step immediately in both directions.", + "left_carrier": "One labelled transition system.", + "right_carrier": "Another labelled transition system.", + "initial_states": "The two initial states belong to the bisimulation relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common label alphabet including internal labels.", + "transition_relation": "Both labelled transition relations.", + "observable_actions": "Every label is matched exactly.", + "hidden_actions": "Hidden labels are still labels and must match immediately.", + "stuttering_actions": "Only explicitly labelled stuttering steps can match." + }, + "observation_projection": { + "applicability": "identity", + "subject": "External comparison observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "symmetric", + "quantification": { + "states": "Universally over every related state pair.", + "traces": "All branching continuations.", + "schedulers": "All nondeterministic branches.", + "strategies": "Outside scope unless lifted to games.", + "environments": "All transition-system environments encoded in state.", + "observations": "Identity observation of labels." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Branching structure and modal properties under the chosen semantics.", + "proof_obligation": "Exhibit a symmetric relation closed under immediate labelled steps in both directions." + }, + "bounded_evidence": [ + "Finite algorithms can decide the relation only for supplied finite models." + ], + "explicit_non_claims": [ + "One shared trace, result, digest, or terminal observation is insufficient." + ], + "incompatible_claim_surfaces": [ + "Finite probes", + "Digest comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "park-1981", + "milner-1980" + ] + }, + "weak-bisimulation": { + "relation_id": "weak-bisimulation", + "display_name": "Weak or observational bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric relation matches visible actions through closure over explicitly hidden tau steps.", + "left_carrier": "One labelled transition system with tau.", + "right_carrier": "Another labelled transition system with tau.", + "initial_states": "Initial states related after the selected tau closure.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common visible alphabet plus the declared tau label.", + "transition_relation": "Both labelled transition relations.", + "observable_actions": "Visible labels match through weak transitions.", + "hidden_actions": "Only the explicitly governed tau label is hidden.", + "stuttering_actions": "Tau closure and stuttering are explicit; divergence treatment is declared." + }, + "observation_projection": { + "applicability": "required", + "subject": "Observer that hides tau", + "policy_ref": "behavioral-relations/tau-projection", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related states under tau closure.", + "traces": "All weak traces and branching continuations.", + "schedulers": "All nondeterministic tau and visible branches.", + "strategies": "Outside scope unless lifted to games.", + "environments": "All encoded environments.", + "observations": "Through the named hiding projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Observation-preserving branching behavior under stated divergence and termination semantics.", + "proof_obligation": "Exhibit a weak bisimulation relation and discharge both directional weak-step obligations." + }, + "bounded_evidence": [ + "The catalog's hidden-action example demonstrates the definition on a finite toy model." + ], + "explicit_non_claims": [ + "Backend-internal work is not tau unless a governed projection declares it." + ], + "incompatible_claim_surfaces": [ + "Undeclared backend hiding" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "bounded", + "proof_status": "future", + "evidence_refs": [ + "contracts/concept-authority/behavioral-relations-v1.json#worked_examples" + ] + }, + "source_refs": [ + "milner-1980", + "van-glabbeek-1990" + ] + }, + "divergence-preserving-branching-bisimulation": { + "relation_id": "divergence-preserving-branching-bisimulation", + "display_name": "Divergence-preserving branching bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric branching bisimulation matches visible transitions through finite closure over an explicitly governed tau set while preserving each related branching point and explicit infinite tau behavior in both directions.", + "left_carrier": "One labelled transition system with a closed visible/tau partition and explicit deadlock, termination, and divergence semantics.", + "right_carrier": "Another labelled transition system over the same projected visible alphabet and governed tau treatment.", + "initial_states": "The revisioned relation-parameter profile names both initial states and requires them to belong to the greatest fixed-point relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common projected visible alphabet plus only the tau labels enumerated by the revisioned relation-parameter profile.", + "transition_relation": "Both complete labelled transition relations over the profile's quantified carriers.", + "observable_actions": "Every projected visible action is matched in both directions after finite tau closure while the pre-action branching state remains related.", + "hidden_actions": "Only profile-enumerated tau actions are hidden; redacted occurrences, refusals, unsupported outcomes, errors, deadlock, termination, and divergence are not hidden by default.", + "stuttering_actions": "Finite tau stuttering is admitted at a related branching point; explicit infinite tau paths must be preserved in both directions." + }, + "observation_projection": { + "applicability": "required", + "subject": "The participant, audience, auditor, or other observer named by the closed relation-parameter profile.", + "policy_ref": "Revisioned divergence-preserving branching-bisimulation projection from the claim profile.", + "policy_revision": "The exact projection revision bound by the claim.", + "redaction_scope": "The profile enumerates every visible, redacted-occurrence, and tau label; implementation-internal or content-redacted does not imply hidden.", + "order_treatment": "The profile fixes sequence, interleaving, step, causal, or other order semantics; one linearization cannot establish a partial-order claim.", + "simultaneity_treatment": "Only simultaneity represented in the selected LTS and visible projection is preserved." + }, + "projection_required": true, + "relation_parameter_profile_required": true, + "direction": "symmetric", + "quantification": { + "states": "greatest-fixed-point relation", + "traces": "All visible and tau continuations from every related state pair, including infinite tau continuations.", + "schedulers": "Every nondeterministic branch and scheduler admitted by the closed profile.", + "strategies": "Outside scope unless the carriers explicitly encode game or adaptive-strategy state.", + "environments": "Every environment state and input admitted by the closed profile.", + "observations": "Exactly the visible alphabet after the revisioned closed projection; the tau partition remains explicit." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "Every admitted branch is matched; finite samples or selected schedules are insufficient." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The profile declares interleaving, step, true-concurrent, or other semantics and the preserved visible order." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability measures are excluded; a probabilistic relation must be named separately." + }, + "time": { + "status": "parameterized", + "treatment": "Untimed profiles erase no visible time label; timed claims require a clock and timed relation." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim requires a carrier and relation that preserve the declared causal structure." + } + }, + "preservation": { + "property": "Visible branching structure, finite governed tau stuttering, explicit termination and structural deadlock, and explicit divergence under the named projection and model dimensions.", + "proof_obligation": "Exhibit or decide the greatest symmetric relation satisfying both branching transfer clauses and both explicit-divergence clauses for the complete quantified carriers and initial states." + }, + "bounded_evidence": [ + "Issue #811 supplies an exact complete-finite theorem profile, witness family, mutation design, and pinned checker contract; it does not run the equivalence decision.", + "A finite model-check result is final only when the supplied finite carrier is the complete quantified domain and the evidence binds exact inputs, counts, tool provenance, result, and independent reproduction." + ], + "explicit_non_claims": [ + "Taxonomy revision rev6 defines this relation and the participant-crossing claim surface but does not establish a model-check or proof result.", + "The participant-crossing design does not establish live-runtime realization, backend conformance, whole-runtime equivalence, policy noninterference, or predicate opacity.", + "Depth limits, sampled traces, probes, matching digests, schema equality, and ordinary weak bisimulation are not this relation." + ], + "incompatible_claim_surfaces": [ + "Undeclared tau hiding or divergence treatment", + "Incomplete, sampled, depth-limited, or timeout-truncated carriers promoted to a complete result", + "Formal equivalence promoted to live-runtime, backend, noninterference, opacity, timed, probabilistic, strategic, concurrent, or partial-order assurance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "deliberately-unproved", + "checker_status": "not-implemented", + "model_check_status": "not-model-checked", + "runtime_enforcement_status": "not-enforced", + "backend_declaration_status": "not-declared", + "backend_realization_status": "not-realized", + "backend_conformance_status": "not-tested", + "evidence_refs": [ + "docs/decisions/adrs/adr-100-participant-crossing-bisimulation.md", + "specs/formal/participant-semantics/participant-crossing-bisimulation.md", + "docs/research/participant-bisimulation/implementation-program.json" + ] + }, + "source_refs": [ + "van-glabbeek-weijland-1996", + "van-glabbeek-luttik-trcka-2009" + ] + }, + "participant-predicate-opacity": { + "relation_id": "participant-predicate-opacity", + "display_name": "Participant-relative predicate opacity", + "relation_class": "epistemic", + "definition": "For every actual possible point at which the selected secret predicate is true, the named participant, audience, or coalition information cell induced by equal declared initial information and accumulated observations contains at least one possible point at which the predicate is false. The baseline is one-sided and possibilistic.", + "left_carrier": "One declared possible-point system whose points compose model or supervisor realization, run, evaluation cut, observer-local state and retained memory, exact-cut policy realization, and scheduler, environment, and order context.", + "right_carrier": "Not applicable; opacity is a unary property of the declared possible-point system and relation-parameter profile. A nonsecret point is a witness inside the same carrier, not a second system.", + "initial_states": "The profile fixes observer or coalition initial information, participant and audience identity, retained memory, policy and supervisor visibility, environment and scheduler classes, and the initial cut or horizon.", + "transition_signature": { + "applicability": "applicable", + "labels": "SEM-230 and participant-runtime occurrences, including proposal and control decisions, admission, attempt and result, disclosure and withholding, transformation, delivery and observation, policy or supervisor change, evidence, and audit when retained by the selected observation profile.", + "transition_relation": "Valid runs over existing participant world, view, local-history, archival-evidence, controller, authority, marking, exact-cut policy, crossing, delivery, observation, scheduler, environment, and order carriers.", + "observable_actions": "Exactly the content, occurrence, omission, decision, failure, delivery, retry, order, timing, policy-change, retrieval, evidence, or audit coordinates retained for the named observer by the revisioned relation-parameter profile.", + "hidden_actions": "Only occurrences removed by that observer-, policy-, supervisor-visibility-, cut-, memory-, time-, and order-relative observation function; hidden implementation does not imply hidden behavior.", + "stuttering_actions": "The untimed baseline removes finite unobserved stuttering and is progress- and termination-insensitive. Omission is observable only when the profile supplies an opportunity, deadline, acknowledgement, progress, or clock model." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant, audience, or explicit coalition", + "policy_ref": "SEM-231 participant-predicate-opacity observation profile", + "policy_revision": "The revisioned relation-parameter profile bound by the claim", + "redaction_scope": "Projection, redaction, declassification, decision disclosure, concealment, revocation, loss, and prior knowledge remain distinct; the profile states which resulting facts are observations.", + "order_treatment": "The profile selects total, participant-local, causal partial, simultaneous, or backend-serialized order. One linearization cannot establish a partial-order claim.", + "simultaneity_treatment": "Only declared simultaneity and visible causal frontiers are retained; timestamp equality is not simultaneity." + }, + "projection_required": true, + "relation_parameter_profile_required": true, + "direction": "unary", + "quantification": { + "states": "For every actual point in the declared carrier at which the revisioned secret predicate is true, there exists a point in the same observer information cell at which it is false.", + "traces": "Over every run and cut admitted by the selected current-, initial-, K-step, infinite-step, language, or other declared horizon profile.", + "schedulers": "Over the fixed declared scheduler class and admitted order contexts; scheduler-sensitive variants state their quantifier order.", + "strategies": "Passive profiles quantify over observations only. Active profiles quantify universally over the declared allowed adaptive participant strategies, with actual and witness runs possible under the same strategy.", + "environments": "Over the fixed declared model and environment class, including only supervisor or policy realizations admitted by the selected visibility posture.", + "observations": "Equality of declared initial information and accumulated observer-local observations, including memory across retries, replay, episodes, policy revisions, controller handoffs, and coalition sharing when selected." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "The baseline is possibilistic over the declared support. Nondeterministic or randomized supervision changes possible points but is not itself opacity evidence." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The profile fixes interleaving, step, simultaneous, causal, or other declared concurrent semantics and observer-visible order." + }, + "probability": { + "status": "outside-scope", + "treatment": "The baseline compares possibility support, not probability mass, posterior belief, entropy, leakage probability, or differential privacy. Quantitative opacity requires a separately governed relation." + }, + "time": { + "status": "parameterized", + "treatment": "The baseline is untimed and progress-insensitive. A timed profile requires a governed clock, duration and progress observation model and separately scoped evidence." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim compares declared visible causal frontiers and admitted schedules; a witness from one convenient linearization is insufficient." + } + }, + "preservation": { + "property": "The named observer never knows that the selected one-sided secret predicate is true at a protected cut because every actual secret information cell retains a possible nonsecret point.", + "proof_obligation": "For every quantified actual secret point and, for active profiles, every allowed adaptive strategy, exhibit or prove the existence of a nonsecret point with equal declared initial information and accumulated observation under the same profile, strategy, release schedule, supervisor visibility, memory, scheduler, environment, time, and order assumptions." + }, + "bounded_evidence": [ + "The SEM-231 formal specification gives four finite counterexamples covering an incomplete equal-history witness, supervisor-decision leakage, opacity without noninterference, and declassification-induced knowledge change.", + "The participant-opacity-baseline-v1 profile closes every relation coordinate and the deterministic processor exhausts exact declared finite possible-point carriers with digest-bound bounded outcomes or sanitized counterexample references.", + "implementations/python/tests/test_issue_961_participant_opacity.py covers profile and claim resolution, finite bounds, active strategies, coalition fusion, decision and omission channels, retained release knowledge, vacuity, deterministic evidence, replay, and explicit nonclaims.", + "The participant-opacity finite-state checker derives the complete reachable fixed point from an exact transition model, checks every reachable secret evaluation point, and binds catalog, profile, model, assumptions, explored coverage, tool version, result or safe counterexample, and replay evidence.", + "The committed model-check input and evidence fixtures retain the exact positive baseline model, result, digests, complete coverage, tool identity, and explicit nonclaims; invalid fixtures exercise count and partial-result promotion failures.", + "implementations/python/tests/test_issue_962_participant_opacity_model_check.py covers pair-probe incompleteness, supervisor behavior, active strategies, coalition fusion, retained memory, release changes, order and probability non-promotion, exact bounds, replay, and agreement with the bounded lane.", + "The Isabelle/HOL Participant_Opacity session kernel-checks the SEM-231 one-sided opacity definition, its information-cell knowledge characterization, and the conditional implication from a matching SEM-230 noninterference instance for an eligible predicate; checked countermodels preserve the invalid-promotion boundaries.", + "The participant-opacity-runtime-reference-v1 profile and RUN-319 crossing boundary enforce one exact finite observation inventory with safe, atomic runtime-enforcement decision bindings." + ,"The reference backend declares bounded support for the exact runtime profile; generic target conformance separately observes backend-native realization across the protected and complement points and binds the manifest, profile, configuration, tool, environment, and probe-set digests." + ], + "explicit_non_claims": [ + "Relation definition, catalog validation, claim-profile binding, and bounded finite analysis do not establish opacity of RAES, RUN-319, or any backend outside the exact admitted artifact.", + "No checker, finite-state model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance is delivered by taxonomy revision rev5.", + "Taxonomy revision rev7 adds only an in-process bounded-test checker; it does not add a model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev8 adds one exact finite-state model-check result; it does not add a mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev9 adds only the abstract conditional mathematical theorem bound to participant-opacity-theorem-v1; it does not prove opacity of RAES, a runtime, a deployment, a backend, or the finite fixture profile.", + "Taxonomy revision rev10 adds partial runtime enforcement only for participant-opacity-runtime-reference-v1; it does not establish general opacity, live model checking or proof, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev11 adds one bounded reference-backend declaration, realization, and conformance lane; it does not establish universal opacity, proof of a live execution, native realization for other backends, or cross-backend equivalence.", + "Bounded evidence authenticates only the normalized-input digest; it does not authenticate a claimed source artifact or materializer.", + "Opacity of one predicate does not imply SEM-230 policy noninterference, projected-history equivalence, epistemic indistinguishability of two selected worlds, trace inclusion or equivalence, simulation, refinement, or strong or weak bisimulation.", + "The possibilistic baseline makes no posterior-risk, entropy, probabilistic, differential-privacy, timed, progress-sensitive, or universal partial-order claim." + ], + "incompatible_claim_surfaces": [ + "Unrevisioned or untyped secret, observer, supervisor-visibility, memory, strategy, time, order, or release coordinates", + "Single equal-history pairs, finite probes, randomized behavior, runtime filters, or backend declarations promoted to universal opacity", + "Claims that erase prior knowledge through concealment, revocation, reset, rollback, or supersession" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "proved", + "checker_status": "implemented", + "model_check_status": "model-checked", + "runtime_enforcement_status": "partial", + "backend_declaration_status": "declared", + "backend_realization_status": "partial", + "backend_conformance_status": "bounded", + "evidence_refs": [ + "docs/decisions/adrs/adr-099-participant-relative-predicate-opacity.md", + "specs/formal/participant-semantics/participant-predicate-opacity.md", + "contracts/profiles/behavioral-relation/history/participant-opacity-baseline-v1-sem-231-rev2.json", + "contracts/profiles/behavioral-relation/participant-opacity-theorem-v1.json", + "contracts/schemas/formal-analysis/participant-opacity-model-check-input-v1.json", + "contracts/schemas/formal-analysis/participant-opacity-model-check-evidence-v1.json", + "contracts/fixtures/formal-analysis/participant-opacity-model-check-input-v1/valid/opaque-transition-model.json", + "contracts/fixtures/formal-analysis/participant-opacity-model-check-evidence-v1/valid/opaque-transition-model.json", + "implementations/python/packages/raes_processor/participant_opacity/_service.py", + "implementations/python/packages/raes_processor/participant_opacity/_model_check.py", + "implementations/python/tests/test_sem_231_participant_predicate_opacity.py", + "implementations/python/tests/test_issue_961_participant_opacity.py", + "implementations/python/tests/test_issue_962_participant_opacity_model_check.py", + "implementations/python/tests/test_issue_963_participant_opacity_proof.py", + "specs/formal/participant-semantics/isabelle/Participant_Opacity.thy", + "specs/formal/participant-semantics/participant-opacity-proof-evidence.json", + "tools/check_participant_opacity_proof.py", + "tools/isabelle_tool.py", + "contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json", + "implementations/python/packages/raes_contracts/participant_opacity_runtime.py", + "implementations/python/packages/raes_runtime/participant_crossing_mediation.py", + "implementations/python/tests/test_issue_964_participant_opacity_runtime.py", + "docs/decisions/issue-965-participant-opacity-backend-realization-preflight.md", + "implementations/python/packages/raes_conformance/conformance/participant_opacity_probes.py", + "implementations/python/tests/test_issue_965_participant_opacity_backend.py" + ] + }, + "source_refs": [ + "andre-lime-marinho-sun-2022", + "badouel-bednarczyk-borzyszkowski-caillaud-darondeau-2007", + "berard-mullins-sassolas-2015", + "broberg-van-delft-sands-2015", + "bryans-koutny-mazare-ryan-2008", + "cui-ma-giua-yin-2026", + "fagin-halpern-moses-vardi-1995", + "lin-2011", + "partovi-jung-hai-2020", + "saboori-hadjicostis-2012", + "schoepe-sabelfeld-2015", + "xie-yin-li-2022", + "yin-lafortune-2016" + ] + }, + "participant-projected-history-equivalence": { + "relation_id": "participant-projected-history-equivalence", + "display_name": "Participant-projected history equivalence", + "relation_class": "epistemic", + "definition": "Two finite or complete histories have equal projections for one participant under one observation-boundary revision.", + "left_carrier": "One global or backend history.", + "right_carrier": "Another global or backend history.", + "initial_states": "The compared histories share a declared participant and starting information state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Participant-visible events after projection.", + "transition_relation": "Underlying history extension relations.", + "observable_actions": "Events admitted by the participant observation boundary.", + "hidden_actions": "Events removed or redacted by the boundary.", + "stuttering_actions": "No additional stuttering assumption beyond projected history equality." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "States represented in the compared histories.", + "traces": "Finite histories unless a universal claim is separately proved.", + "schedulers": "Only schedulers represented in the histories.", + "strategies": "No strategic quantification.", + "environments": "Only the named environment/run context.", + "observations": "One participant and one policy revision." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Equality or indistinguishability of the named participant's projected histories.", + "proof_obligation": "Apply the existing participant observation boundary to both histories and compare the resulting ordered visible records." + }, + "bounded_evidence": [ + "Participant behavior-history and observation-envelope tests on named histories." + ], + "explicit_non_claims": [ + "Does not imply equality of global state, future behavior, knowledge, or strategy." + ], + "incompatible_claim_surfaces": [ + "Global-state comparison", + "Strategic equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-runtime/README.md" + ] + }, + "source_refs": [ + "fagin-halpern-moses-vardi-1995", + "milner-1980" + ] + }, + "policy-noninterference": { + "relation_id": "policy-noninterference", + "display_name": "Participant-policy noninterference", + "relation_class": "behavioral", + "definition": "For a fixed participant, episode and memory scope, model, environment class, scheduler class, order model, exact-cut policy-decision sequence, permitted declassification schedule, and low-strategy class, every low participant strategy produces equal support sets of projected participant-visible histories from low-equivalent initial states despite unauthorized high variation.", + "left_carrier": "The support set of valid labelled participant-policy runs from one low-equivalent initial state under one adaptive low strategy.", + "right_carrier": "The support set of valid labelled participant-policy runs from another low-equivalent initial state under the same adaptive low strategy.", + "initial_states": "Initial world, participant-view, delivered decision-surface history, participant memory, archival-evidence, controller, authority, marking, and policy states related by the SEM-230 low-equivalence relation at the declared initial state cut.", + "transition_signature": { + "applicability": "applicable", + "labels": "The closed SEM-230 alphabet for proposal, approval or denial, direction, intervention, handoff, override or cancellation, admission or rejection, attempt or result, disclosure or withholding, concealment, revocation, transformation, delivery, observation, policy change, evidence, and audit actions.", + "transition_relation": "The SEM-230 participant-policy crossing relation over existing world, view, local-history, archival-evidence, action, lifecycle, ordering, marking, controller, authority, policy, and provenance state.", + "observable_actions": "Labels retained for the named participant and audience by the exact-cut policy decision, marking/declassification intersection, and declared state-cut projection, including delivered decision surfaces.", + "hidden_actions": "Only labels mapped to tau by the named participant-, audience-, policy-decision-, and state-cut-relative projection; backend-internal actions are not intrinsically hidden.", + "stuttering_actions": "Finite hidden stuttering is removed by the declared tau closure; the baseline is termination- and progress-insensitive and does not claim divergence-sensitive preservation." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant and audience within one episode scope", + "policy_ref": "SEM-230 participant-information-flow policy", + "policy_revision": "The complete declared policy-decision sequence and exact state-cut bindings", + "redaction_scope": "Projection, masking, redaction, declassification, transformation, marking, loss, and weakening remain distinct and are evaluated deny-first.", + "order_treatment": "Compare occurrence-preserving visible histories under the same declared total, partial, causal, simultaneous, or backend-serialized order model; one convenient linearization is insufficient for a partial-order claim.", + "simultaneity_treatment": "Preserve declared simultaneity groups and visible order relations; timestamp equality does not establish simultaneity." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "For every pair of initial and reachable states related by low equivalence at the applicable participant-policy state cut and memory scope.", + "traces": "For all valid run support sets generated under the same low strategy, exact-cut policy decisions, and permitted declassification schedule.", + "schedulers": "For the fixed declared scheduler class; scheduler-sensitive variants must select and evidence a stronger relation.", + "strategies": "Universally over the declared class of adaptive low participant strategies mapping delivered local histories to choices or choice sets; no coalition-ability equivalence is implied.", + "environments": "For the fixed declared environment class and equal low environment inputs, allowing only the unauthorized high variation under examination.", + "observations": "Equality of support sets of occurrence-preserving histories projected for the named participant, audience, policy sequence, markings, declassification schedule, and order model." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "The baseline compares complete declared support sets of projected histories; equality of sampled or single histories is only bounded falsification evidence." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The selected sequential, total, partial, causal, simultaneous, or backend-serialized transition and visible-order model is fixed for the comparison." + }, + "probability": { + "status": "outside-scope", + "treatment": "The baseline compares support sets, not measures; probabilistic noninterference requires a separately governed probabilistic relation, kernel, bound, and evidence." + }, + "time": { + "status": "abstracted", + "treatment": "The baseline is termination- and progress-insensitive and excludes wall-clock timing; timed security requires a separately governed relation." + }, + "partial_order": { + "status": "parameterized", + "treatment": "When partial order is selected, the declared visible order relation and simultaneity groups are compared rather than one linear extension." + } + }, + "preservation": { + "property": "Unauthorized high variation does not change the support set of participant-visible histories observed by any declared adaptive low strategy, except at equal explicitly governed declassification events.", + "proof_obligation": "Prove support-set equality for every quantified low-equivalent pair and low strategy under the fixed participant, memory scope, exact-cut policy decisions, declassification schedule, model, scheduler, environment, and order assumptions, or report only the bounded counterexamples actually checked." + }, + "bounded_evidence": [ + "implementations/python/tests/test_sem_230_information_flow_control.py checks finite unauthorized-high, declassification-order, policy-revision, participant-relative hiding, deny-first, append-only-history, transformation-admission, and support-set counterexamples.", + "implementations/python/tests/test_asr_535_participant_flow_assurance.py exhausts a declared finite crossing domain for unauthorized-high purge and exact-cut declassification, and drives the shipped RUN-319 boundary for denial, withholding, redaction, governed declassification, transformation, stale or revoked policy, cross-participant leakage, participant-directed inject delivery, backend weakening, unsupported capability, and adversarial overclaim." + ], + "explicit_non_claims": [ + "The finite SEM-230 executable cases do not establish universal noninterference.", + "Projected-history equality does not establish policy noninterference without the stated low-equivalence, adaptive-strategy, memory, exact-cut policy, purge, declassification, scheduler, environment, and quantifier obligations.", + "No trace equivalence, simulation, refinement, strong or weak bisimulation, epistemic indistinguishability, timing security, probabilistic security, or backend realization is claimed.", + "The ASR-535 finite enumeration, runtime probes, and backend conformance cases are bounded falsification evidence and are not a model check or a proof; issues #810 to #813 own any stronger opacity, bisimulation, adversarial-control, or cross-backend status." + ], + "incompatible_claim_surfaces": [ + "Unrevisioned participant projection", + "Single-history or sampled-history equality", + "Undeclared scheduler, environment, timing, probability, or partial-order assumptions", + "Runtime or backend realization inferred from the definition" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-semantics/information-flow-control.md", + "implementations/python/tests/test_sem_230_information_flow_control.py", + "implementations/python/packages/raes_runtime/participant_crossing_policy.py", + "implementations/python/packages/raes_conformance/conformance/participant_policy_probes.py", + "implementations/python/tests/test_run_319_participant_flow_policy.py", + "implementations/python/tests/test_asr_535_participant_flow_assurance.py" + ] + }, + "source_refs": [ + "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "clarkson-schneider-2010", + "fagin-halpern-moses-vardi-1995", + "goguen-meseguer-1982", + "milner-1980", + "sabelfeld-sands-2009", + "van-glabbeek-1990" + ] + }, + "io-alternating-refinement": { + "relation_id": "io-alternating-refinement", + "display_name": "Input/output alternating refinement", + "relation_class": "behavioral", + "definition": "A directional concrete-to-abstract relation preserves abstract outputs and internal behavior while respecting input ownership and declared action-availability obligations against environment choices.", + "left_carrier": "A concrete backend participant I/O transition system.", + "right_carrier": "An abstract RAES participant I/O transition system.", + "initial_states": "Every concrete initial participant decision state, including decision epoch zero, relates to an abstract initial decision state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Participant proposals are inputs; participant views and observations are outputs; backend, scheduler, and environment labels retain their declared owners.", + "transition_relation": "Concrete and abstract I/O-labelled step relations under a declared refinement mapping.", + "observable_actions": "Participant-visible inputs and outputs under the named projection.", + "hidden_actions": "Only governed backend/internal labels mapped to tau by the named projection.", + "stuttering_actions": "Finite hidden concrete paths may match one abstract step only when the selected weak or branching treatment permits them." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant and audience", + "policy_ref": "participant-observation-boundary", + "policy_revision": "The exact-cut projection policy used by the claim", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Decision epochs are preserved under hidden stuttering; state cuts retain their declared order model.", + "simultaneity_treatment": "Simultaneity and partial-order frontiers are preserved only when declared by the claim." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over related reachable concrete and abstract states.", + "traces": "All concrete traces induced by quantified inputs, outputs, and environment choices.", + "schedulers": "All schedulers in the declared fairness class.", + "strategies": "All participant and environment strategies in the declared action-ownership classes.", + "environments": "All environment choices admitted by the declared alternating quantifiers.", + "observations": "Under the exact named participant projection and delivery semantics." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "Input, output, scheduler, backend, and environment choices are separately owned and quantified." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, simultaneous, step, or true-concurrency semantics must be declared." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability requires a separately governed probabilistic alternating relation." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require explicit clock, fairness, timeout, and progress semantics." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim relates declared causal frontiers rather than arbitrary linearizations." + } + }, + "preservation": { + "property": "Projected concrete traces remain abstractly admitted and declared participant inputs and outputs retain their availability and ownership obligations.", + "proof_obligation": "Supply the refinement relation, initial-state mapping, input/output ownership, availability and fairness obligations, and alternating step correspondence for every quantified choice." + }, + "bounded_evidence": [ + "Decision-surface lifecycle tests may falsify selected initiality, delivery, availability, freshness, and step-matching cases on finite models." + ], + "explicit_non_claims": [ + "Trace inclusion alone does not establish input availability or alternating refinement.", + "Successful participant loops do not establish the universal relation." + ], + "incompatible_claim_surfaces": [ + "Current bounded backend conformance reports" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md", + "implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py", + "implementations/python/tests/test_behavioral_relations.py" + ] + }, + "source_refs": [ + "alur-henzinger-kupferman-vardi-1998", + "lynch-tuttle-1989", + "lynch-vaandrager-1995" + ] + }, + "epistemic-indistinguishability": { + "relation_id": "epistemic-indistinguishability", + "display_name": "Epistemic indistinguishability", + "relation_class": "epistemic", + "definition": "Two worlds are indistinguishable to an agent when they occupy the same governed information set.", + "left_carrier": "One epistemic world/state.", + "right_carrier": "Another epistemic world/state.", + "initial_states": "Worlds in the same agent-indexed accessibility or information relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Optional temporal or action labels of the epistemic model.", + "transition_relation": "The declared interpreted-system or Kripke transition relation.", + "observable_actions": "Agent-observable propositions and events.", + "hidden_actions": "Facts excluded by the information projection.", + "stuttering_actions": "Stuttering is model-specific." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named agent", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "All worlds in the selected information relation.", + "traces": "Histories only when the interpreted-system model includes them.", + "schedulers": "All schedulers represented in the model.", + "strategies": "Strategies are outside the relation itself.", + "environments": "All environments represented by possible worlds.", + "observations": "One agent or explicitly named group." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Truth of formulas invariant over the selected information set, subject to the logic.", + "proof_obligation": "Define possible worlds, the agent-indexed indistinguishability relation, valuation, and any temporal interaction." + }, + "bounded_evidence": [ + "Equal projected finite histories may be evidence for a bounded information-state comparison." + ], + "explicit_non_claims": [ + "Does not follow from global-state equality and does not establish strategic equivalence." + ], + "incompatible_claim_surfaces": [ + "Current participant conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "fagin-halpern-moses-vardi-1995" + ] + }, + "alternating-strategic-equivalence": { + "relation_id": "alternating-strategic-equivalence", + "display_name": "Alternating or strategic equivalence", + "relation_class": "strategic", + "definition": "Two game structures preserve the abilities of named coalitions under explicit strategy and environment quantifiers.", + "left_carrier": "One concurrent or alternating game structure.", + "right_carrier": "Another concurrent or alternating game structure.", + "initial_states": "Related initial game states.", + "transition_signature": { + "applicability": "applicable", + "labels": "Joint actions, chance outcomes, and state transitions.", + "transition_relation": "Both game transition functions or relations.", + "observable_actions": "Player observations and public actions.", + "hidden_actions": "Hidden information under the named observation partitions.", + "stuttering_actions": "Stuttering, simultaneous moves, and scheduler steps are explicit." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named players or coalition", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related game states.", + "traces": "Outcome paths induced by quantified strategies.", + "schedulers": "Schedulers and chance kernels explicitly quantified.", + "strategies": "Coalitions and strategy classes universally/existentially quantified as declared.", + "environments": "Adversarial environment choices explicitly quantified.", + "observations": "Player-indexed observation partitions." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Environment and scheduler choices are explicit." + }, + "concurrency": { + "status": "supported", + "treatment": "Joint and simultaneous moves are part of the game structure." + }, + "probability": { + "status": "parameterized", + "treatment": "Chance kernels must be declared when present." + }, + "time": { + "status": "parameterized", + "treatment": "Timed strategies require an explicit clock model." + }, + "partial_order": { + "status": "parameterized", + "treatment": "Concurrent action order is part of the declared game semantics." + } + }, + "preservation": { + "property": "Coalition ability for the stated objective class.", + "proof_obligation": "Define players, legal joint actions, observations, strategy class, coalitions, chance, scheduler/fairness, objectives, and an alternating relation in both directions." + }, + "bounded_evidence": [ + "Finite recorded joint-action traces can only falsify selected cases." + ], + "explicit_non_claims": [ + "Capability declarations and shared probe outcomes do not establish strategic equivalence." + ], + "incompatible_claim_surfaces": [ + "Current multi-agent conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "alur-henzinger-kupferman-vardi-1998", + "alur-henzinger-kupferman-2002" + ] + }, + "probabilistic-bisimulation": { + "relation_id": "probabilistic-bisimulation", + "display_name": "Probabilistic bisimulation", + "relation_class": "behavioral", + "definition": "Related states match labelled probability distributions over equivalence classes under the selected probabilistic process model.", + "left_carrier": "One probabilistic labelled transition system.", + "right_carrier": "Another probabilistic labelled transition system.", + "initial_states": "Initial states belong to the probabilistic bisimulation relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Shared visible and hidden labels of the probabilistic model.", + "transition_relation": "Labelled transitions to probability distributions.", + "observable_actions": "Labels retained by the projection.", + "hidden_actions": "Declared hidden labels.", + "stuttering_actions": "Weak variants require an explicit probabilistic tau closure." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Probabilistic process observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related states.", + "traces": "All probabilistic traces or branching distributions required by the variant.", + "schedulers": "All nondeterministic schedulers explicitly quantified.", + "strategies": "Outside scope unless combined with games.", + "environments": "All admitted probabilistic environments.", + "observations": "Through the named projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Scheduler quantification is mandatory." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The process-composition semantics must be named." + }, + "probability": { + "status": "supported", + "treatment": "Probability distributions are matched over relation classes." + }, + "time": { + "status": "outside-scope", + "treatment": "Continuous or timed probability needs another variant." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The base relation uses labelled branching structure." + } + }, + "preservation": { + "property": "Probability mass over related behavior classes.", + "proof_obligation": "Exhibit a relation whose matched transitions assign equal probability to every relation-closed class under the chosen variant." + }, + "bounded_evidence": [ + "Statistical samples may refute parameters but do not prove distributional branching equivalence." + ], + "explicit_non_claims": [ + "Statistical similarity or equal sample means is not probabilistic bisimulation." + ], + "incompatible_claim_surfaces": [ + "Ordinary empirical study" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "larsen-skou-1991" + ] + }, + "statistical-similarity": { + "relation_id": "statistical-similarity", + "display_name": "Statistical similarity", + "relation_class": "empirical", + "definition": "A predeclared metric over sampled populations lies within a stated similarity criterion with uncertainty.", + "left_carrier": "One sampled population or system output distribution.", + "right_carrier": "Another sampled population, target distribution, or reference data.", + "initial_states": "The preregistered sampling frame and apparatus context.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Statistical similarity compares sampled measures, not transition systems unless a separate model binds them." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study analyst", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Sampled observations only.", + "traces": "Sampled run outcomes only.", + "schedulers": "Schedulers represented by the sampling design.", + "strategies": "Strategies represented by the sampling design.", + "environments": "The preregistered population and apparatus.", + "observations": "The named metric/estimand projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Variation is represented through the sampling model." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency matters only through measured outcomes." + }, + "probability": { + "status": "supported", + "treatment": "The sampling distribution and uncertainty method are explicit." + }, + "time": { + "status": "parameterized", + "treatment": "Sampling windows and time domains are declared." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Event partial order is not inferred from aggregate metrics." + } + }, + "preservation": { + "property": "The stated similarity criterion for the named estimand and population.", + "proof_obligation": "Predeclare population, sampling frame, metric, criterion, uncertainty method, and decision rule; then execute the study." + }, + "bounded_evidence": [ + "Experiment runs, derived measures, and uncertainty intervals." + ], + "explicit_non_claims": [ + "Does not establish behavioral, epistemic, strategic, or probabilistic bisimulation." + ], + "incompatible_claim_surfaces": [ + "Universal backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/experiment-core/README.md" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "statistical-equivalence": { + "relation_id": "statistical-equivalence", + "display_name": "Statistical equivalence", + "relation_class": "empirical", + "definition": "A preregistered equivalence test supports that a named estimand lies within a stated equivalence margin for the sampled population.", + "left_carrier": "One sampled population or treatment.", + "right_carrier": "Another sampled population, treatment, or reference.", + "initial_states": "The preregistered sampling frame, allocation, and apparatus context.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "A statistical equivalence test does not compare enabled transitions." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study analyst", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Sampled units only.", + "traces": "Sampled run outcomes only.", + "schedulers": "Schedulers represented by allocation/sampling.", + "strategies": "Strategies represented by sampled conditions.", + "environments": "The stated target population.", + "observations": "The named estimand and measurement projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Variation is handled by the statistical model." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency is only a measured covariate unless modeled." + }, + "probability": { + "status": "supported", + "treatment": "Equivalence margins, error rates, and uncertainty are explicit." + }, + "time": { + "status": "parameterized", + "treatment": "Study windows and time domains are declared." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Aggregate equivalence does not preserve event order." + } + }, + "preservation": { + "property": "Equivalence of the named estimand within the preregistered margin.", + "proof_obligation": "Specify the equivalence hypotheses, margin, error control, sampling design, and analysis before observing results." + }, + "bounded_evidence": [ + "Experiment-study analysis and derived measures." + ], + "explicit_non_claims": [ + "Statistical equivalence is not behavioral equivalence or proof of implementation conformance." + ], + "incompatible_claim_surfaces": [ + "Bisimulation claim" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/experiment-core/README.md" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "empirical-adequacy": { + "relation_id": "empirical-adequacy", + "display_name": "Empirical adequacy", + "relation_class": "empirical", + "definition": "Observed evidence supports a purpose-relative, bounded adequacy claim for a named phenomenon and intended use.", + "left_carrier": "A model, language, implementation, or method under study.", + "right_carrier": "A defined empirical target, task, or phenomenon.", + "initial_states": "The preregistered study population, tasks, and apparatus.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Empirical adequacy may study behavior but is not itself a transition-system equivalence." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study audience", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Observed study units.", + "traces": "Observed runs and tasks.", + "schedulers": "Schedulers represented by the protocol.", + "strategies": "Strategies represented by participant/task sampling.", + "environments": "The named target population and intended use.", + "observations": "The preregistered measures and coding projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Uncontrolled variation is handled as a validity limitation." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency is measured only when the protocol names it." + }, + "probability": { + "status": "parameterized", + "treatment": "Sampling and uncertainty must be reported." + }, + "time": { + "status": "parameterized", + "treatment": "Study period and temporal validity are explicit." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Adequacy does not imply partial-order preservation." + } + }, + "preservation": { + "property": "Fitness for the explicitly stated empirical purpose within the study boundary.", + "proof_obligation": "Predeclare tasks, population, measures, success/falsification criteria, analysis, limitations, and evidence lineage." + }, + "bounded_evidence": [ + "Independent parser, authoring, review, and diagnostic-recovery studies." + ], + "explicit_non_claims": [ + "Repeated bounded observations do not establish universal semantics, conformance, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Universal language equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "bueno-1997", + "wellek-2010" + ] + } + }, + "claim_surfaces": [ + { + "surface_id": "sdl-transformation", + "intended_relation_ids": [ + "structural-validity", + "semantic-validity", + "canonical-artifact-identity" + ], + "evidence_boundary": "Deterministic phase functions and finite invariant, round-trip, canonicalization, and property tests.", + "prohibited_relation_ids": [ + "data-refinement", + "forward-simulation", + "trace-equivalence", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "No universal behavioral refinement or equivalence is currently proved." + ] + }, + { + "surface_id": "backend-realization", + "intended_relation_ids": [ + "realization-envelope-membership", + "bounded-probe-success", + "trace-inclusion", + "io-alternating-refinement" + ], + "evidence_boundary": "Envelope checks and named target probes are bounded evidence; projected trace inclusion plus input/output ownership and availability under I/O alternating refinement are the intended universal actionable-participant obligations and remain deliberately unproved.", + "prohibited_relation_ids": [ + "trace-equivalence", + "backward-simulation", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "Provisioning, snapshots, witnesses, and negative probes do not prove behavioral equivalence." + ] + }, + { + "surface_id": "backend-comparison", + "intended_relation_ids": [ + "bounded-probe-success", + "statistical-similarity", + "canonical-artifact-identity" + ], + "evidence_boundary": "Only named invariants, probes, digests, populations, metrics, and uncertainty procedures are compared.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "A shared result, digest, or finite trace is not universal same behavior." + ] + }, + { + "surface_id": "participant-visible-behavior", + "intended_relation_ids": [ + "participant-projected-history-equivalence" + ], + "evidence_boundary": "Comparison is parameterized by participant, observation-boundary policy revision, redaction, order, simultaneity, and run context.", + "prohibited_relation_ids": [ + "epistemic-indistinguishability", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "Equal projected histories do not expose hidden global state or prove future knowledge or strategy equivalence." + ] + }, + { + "surface_id": "participant-information-flow-policy", + "intended_relation_ids": [ + "policy-noninterference" + ], + "evidence_boundary": "The SEM-230 relation is defined over named participant, audience, memory scope, exact-cut policy-decision sequence, low-equivalence, adaptive low-strategy class, dynamic purge, permitted declassification schedule, scheduler/environment classes, order model, and support-set semantics. Current executable evidence is limited to finite models, finite reference-runtime enforcement probes, and finite backend-conformance cases.", + "prohibited_relation_ids": [ + "participant-projected-history-equivalence", + "trace-equivalence", + "forward-simulation", + "backward-simulation", + "data-refinement", + "strong-bisimulation", + "weak-bisimulation", + "epistemic-indistinguishability", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "Definition, catalog validation, claim-policy checks, and finite counterexamples do not prove universal noninterference or runtime/backend realization.", + "Reference-runtime enforcement and passing backend-conformance probes establish neither universal noninterference nor native-backend realization." + ] + }, + { + "surface_id": "participant-opacity", + "intended_relation_ids": [ + "participant-predicate-opacity" + ], + "evidence_boundary": "Every claim binds a revisioned observer, secret predicate, possible-point carrier, initial-information and observation functions, memory and horizon, supervisor visibility, passive or active strategy domain, release schedule, scheduler and environment classes, time and order, nondeterminism and probability support, assurance axis, and evidence boundary.", + "prohibited_relation_ids": [ + "participant-projected-history-equivalence", + "policy-noninterference", + "trace-inclusion", + "trace-equivalence", + "forward-simulation", + "backward-simulation", + "data-refinement", + "strong-bisimulation", + "weak-bisimulation", + "epistemic-indistinguishability", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "A profile, finite witness, bounded probe, random choice, model check, runtime decision, or backend declaration establishes only its named assurance axis and evidence scope.", + "No current RAES runtime or backend is claimed opaque." + ] + }, + { + "surface_id": "participant-crossing-bisimulation", + "intended_relation_ids": [ + "divergence-preserving-branching-bisimulation" + ], + "evidence_boundary": "Claims bind the exact independently derived abstract and concrete model revisions and digests, initial states, complete quantified carrier and counts, closed participant/audience projection and tau partition, relation profile, source and mapping revisions, assurance axis, pinned tool provenance, result or safe counterexample, mutations, limitations, and independent reproduction.", + "prohibited_relation_ids": [ + "strong-bisimulation", + "weak-bisimulation", + "trace-equivalence", + "policy-noninterference", + "participant-predicate-opacity", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "Issue #811 defines the theorem and proof program but does not establish the formal equivalence result.", + "A formal model-check does not establish live-runtime realization, backend conformance, whole-runtime equivalence, noninterference, opacity, or a stronger timed, probabilistic, strategic, concurrent, or partial-order relation." + ] + }, + { + "surface_id": "multi-agent-interaction", + "intended_relation_ids": [ + "bounded-probe-success", + "alternating-strategic-equivalence", + "probabilistic-bisimulation" + ], + "evidence_boundary": "Current evidence is structural and finite; strategic and probabilistic relations are definitions for future governed models.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "Current joint-action, chance, simultaneous-move, and mean-field records do not prove strategic equivalence." + ] + }, + { + "surface_id": "counterfactual-necessity-validation", + "intended_relation_ids": [ + "bounded-but-for-necessity", + "bounded-probe-success" + ], + "evidence_boundary": "One revisioned claim, one immutable baseline/intervention-world pair, one typed and verified intervention, admitted proposition-truth evidence, a declared matching policy, and independently verified reset and cleanup.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "empirical-adequacy", + "statistical-equivalence" + ], + "explicit_non_claims": [ + "A supported finite but-for comparison is not universal causal proof, actual-cause attribution, sufficiency, determinism, or statistical necessity." + ] + }, + { + "surface_id": "independent-adequacy-study", + "intended_relation_ids": [ + "empirical-adequacy", + "statistical-similarity", + "statistical-equivalence" + ], + "evidence_boundary": "Claims bind to a preregistered population, task set, metric or coding scheme, uncertainty, falsification criteria, and limitations.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "Bounded observations and statistical findings cannot be promoted to universal behavioral proof." + ] + } + ], + "worked_examples": { + "finite-probe-counterexample": { + "example_id": "finite-probe-counterexample", + "purpose": "Two implementations pass the same finite visible probe a, but the left system has an additional enabled b transition that the right system cannot match.", + "left_system": { + "states": [ + "l0", + "l1", + "l2" + ], + "initial_state": "l0", + "transitions": [ + { + "source": "l0", + "action": "a", + "target": "l1" + }, + { + "source": "l0", + "action": "b", + "target": "l2" + } + ] + }, + "right_system": { + "states": [ + "r0", + "r1" + ], + "initial_state": "r0", + "transitions": [ + { + "source": "r0", + "action": "a", + "target": "r1" + } + ] + }, + "tested_visible_trace": [ + "a" + ], + "hidden_action": "tau", + "expected_strong_bisimulation": false, + "expected_weak_matching": false, + "evidence_boundary": "The shared a probe is evidence only for that finite trace; the unmatched b branch refutes strong bisimulation.", + "explicit_non_claims": [ + "This toy counterexample is not evidence about any RAES backend." + ] + }, + "hidden-action-counterexample": { + "example_id": "hidden-action-counterexample", + "purpose": "The abstract system performs visible send directly; the backend performs governed hidden tau and then send.", + "left_system": { + "states": [ + "a0", + "a1" + ], + "initial_state": "a0", + "transitions": [ + { + "source": "a0", + "action": "send", + "target": "a1" + } + ] + }, + "right_system": { + "states": [ + "b0", + "b1", + "b2" + ], + "initial_state": "b0", + "transitions": [ + { + "source": "b0", + "action": "tau", + "target": "b1" + }, + { + "source": "b1", + "action": "send", + "target": "b2" + } + ] + }, + "tested_visible_trace": [ + "send" + ], + "hidden_action": "tau", + "expected_strong_bisimulation": false, + "expected_weak_matching": true, + "evidence_boundary": "Strong matching fails on tau; weak visible-trace matching succeeds only under the declared tau-hiding projection and finite termination assumptions.", + "explicit_non_claims": [ + "The example does not declare arbitrary backend-internal work hidden and does not prove an RAES backend relation." + ] + } + } +} diff --git a/contracts/concept-authority/history/behavioral-relations-v1-rev9.json b/contracts/concept-authority/history/behavioral-relations-v1-rev9.json new file mode 100644 index 000000000..eac6cc835 --- /dev/null +++ b/contracts/concept-authority/history/behavioral-relations-v1-rev9.json @@ -0,0 +1,3121 @@ +{ + "schema_version": "behavioral-relations/v1", + "taxonomy_id": "raes-behavioral-relations", + "taxonomy_revision": "rev9", + "bibliography": [ + { + "source_id": "park-1981", + "title": "Concurrency and Automata on Infinite Sequences", + "authors": [ + "David M. R. Park" + ], + "publication_year": 1981, + "publication_venue": "Theoretical Computer Science, LNCS 104", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0017309" + } + }, + { + "source_id": "milner-1980", + "title": "A Calculus of Communicating Systems", + "authors": [ + "Robin Milner" + ], + "publication_year": 1980, + "publication_venue": "Lecture Notes in Computer Science 92", + "edition_or_version": "first edition", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/3-540-10235-3" + } + }, + { + "source_id": "van-glabbeek-1990", + "title": "The Linear Time-Branching Time Spectrum", + "authors": [ + "Rob J. van Glabbeek" + ], + "publication_year": 1990, + "publication_venue": "CONCUR 1990, LNCS 458", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0039066" + } + }, + { + "source_id": "van-glabbeek-weijland-1996", + "title": "Branching Time and Abstraction in Bisimulation Semantics", + "authors": [ + "Rob J. van Glabbeek", + "W. Peter Weijland" + ], + "publication_year": 1996, + "publication_venue": "Journal of the ACM 43(3), 555-600", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/233551.233556" + } + }, + { + "source_id": "van-glabbeek-luttik-trcka-2009", + "title": "Branching Bisimilarity with Explicit Divergence", + "authors": [ + "Rob J. van Glabbeek", + "Bas Luttik", + "Nikola Trčka" + ], + "publication_year": 2009, + "publication_venue": "Fundamenta Informaticae 93(4), 371-392", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/FI-2009-109" + } + }, + { + "source_id": "abadi-lamport-1991", + "title": "The Existence of Refinement Mappings", + "authors": [ + "Martín Abadi", + "Leslie Lamport" + ], + "publication_year": 1991, + "publication_venue": "Theoretical Computer Science 82(2)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/0304-3975(91)90224-P" + } + }, + { + "source_id": "lynch-vaandrager-1995", + "title": "Forward and Backward Simulations, Part I: Untimed Systems", + "authors": [ + "Nancy A. Lynch", + "Frits W. Vaandrager" + ], + "publication_year": 1995, + "publication_venue": "Information and Computation 121(2)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1006/inco.1995.1134" + } + }, + { + "source_id": "fagin-halpern-moses-vardi-1995", + "title": "Reasoning About Knowledge", + "authors": [ + "Ronald Fagin", + "Joseph Y. Halpern", + "Yoram Moses", + "Moshe Y. Vardi" + ], + "publication_year": 1995, + "publication_venue": "MIT Press", + "edition_or_version": "hardcover first edition", + "immutable_locator": { + "kind": "isbn", + "value": "9780262061629" + } + }, + { + "source_id": "goguen-meseguer-1982", + "title": "Security Policies and Security Models", + "authors": [ + "Joseph A. Goguen", + "José Meseguer" + ], + "publication_year": 1982, + "publication_venue": "1982 IEEE Symposium on Security and Privacy", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/SP.1982.10014" + } + }, + { + "source_id": "sabelfeld-sands-2009", + "title": "Declassification: Dimensions and Principles", + "authors": [ + "Andrei Sabelfeld", + "David Sands" + ], + "publication_year": 2009, + "publication_venue": "Journal of Computer Security 17(5)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/JCS-2009-0352" + } + }, + { + "source_id": "lynch-tuttle-1989", + "title": "An Introduction to Input/Output Automata", + "authors": [ + "Nancy A. Lynch", + "Mark R. Tuttle" + ], + "publication_year": 1989, + "publication_venue": "CWI Quarterly 2(3), 219-246", + "edition_or_version": "published journal article", + "immutable_locator": { + "kind": "report", + "value": "MIT/LCS/TM-373" + } + }, + { + "source_id": "clarkson-schneider-2010", + "title": "Hyperproperties", + "authors": [ + "Michael R. Clarkson", + "Fred B. Schneider" + ], + "publication_year": 2010, + "publication_venue": "Journal of Computer Security 18(6), 1157-1210", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/JCS-2009-0393" + } + }, + { + "source_id": "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "title": "Reactive Noninterference", + "authors": [ + "Aaron Bohannon", + "Benjamin C. Pierce", + "Vilhelm Sjöberg", + "Stephanie Weirich", + "Steve Zdancewic" + ], + "publication_year": 2009, + "publication_venue": "Proceedings of the 16th ACM Conference on Computer and Communications Security, 79-90", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/1653662.1653673" + } + }, + { + "source_id": "alur-henzinger-kupferman-vardi-1998", + "title": "Alternating Refinement Relations", + "authors": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman", + "Moshe Y. Vardi" + ], + "publication_year": 1998, + "publication_venue": "CONCUR 1998, LNCS 1466", + "edition_or_version": "published conference chapter", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/BFb0055622" + } + }, + { + "source_id": "alur-henzinger-kupferman-2002", + "title": "Alternating-Time Temporal Logic", + "authors": [ + "Rajeev Alur", + "Thomas A. Henzinger", + "Orna Kupferman" + ], + "publication_year": 2002, + "publication_venue": "Journal of the ACM 49(5)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/585265.585270" + } + }, + { + "source_id": "larsen-skou-1991", + "title": "Bisimulation Through Probabilistic Testing", + "authors": [ + "Kim G. Larsen", + "Arne Skou" + ], + "publication_year": 1991, + "publication_venue": "Information and Computation 94(1)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/0890-5401(91)90030-6" + } + }, + { + "source_id": "wellek-2010", + "title": "Testing Statistical Hypotheses of Equivalence and Noninferiority", + "authors": [ + "Stefan Wellek" + ], + "publication_year": 2010, + "publication_venue": "Chapman and Hall/CRC", + "edition_or_version": "second edition", + "immutable_locator": { + "kind": "isbn", + "value": "9781439808184" + } + }, + { + "source_id": "bueno-1997", + "title": "Empirical Adequacy: A Partial Structures Approach", + "authors": [ + "Otávio Bueno" + ], + "publication_year": 1997, + "publication_venue": "Studies in History and Philosophy of Science Part A 28(4)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/S0039-3681(97)00012-5" + } + }, + { + "source_id": "halpern-pearl-2005", + "title": "Causes and Explanations: A Structural-Model Approach. Part I: Causes", + "authors": [ + "Joseph Y. Halpern", + "Judea Pearl" + ], + "publication_year": 2005, + "publication_venue": "The British Journal for the Philosophy of Science 56(4)", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1093/bjps/axi147" + } + }, + { + "source_id": "bryans-koutny-mazare-ryan-2008", + "title": "Opacity Generalised to Transition Systems", + "authors": [ + "Jeremy W. Bryans", + "Maciej Koutny", + "Laurent Mazaré", + "Peter Y. A. Ryan" + ], + "publication_year": 2008, + "publication_venue": "International Journal of Information Security 7(6), 421-435", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/s10207-008-0058-x" + } + }, + { + "source_id": "schoepe-sabelfeld-2015", + "title": "Understanding and Enforcing Opacity", + "authors": [ + "Daniel Schoepe", + "Andrei Sabelfeld" + ], + "publication_year": 2015, + "publication_venue": "2015 IEEE Computer Security Foundations Symposium, 539-553", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/CSF.2015.41" + } + }, + { + "source_id": "lin-2011", + "title": "Opacity of Discrete Event Systems and its Applications", + "authors": [ + "Feng Lin" + ], + "publication_year": 2011, + "publication_venue": "Automatica 47(3), 496-503", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1016/j.automatica.2011.01.002" + } + }, + { + "source_id": "saboori-hadjicostis-2012", + "title": "Verification of Infinite-Step Opacity and Complexity Considerations", + "authors": [ + "Anooshiravan Saboori", + "Christoforos N. Hadjicostis" + ], + "publication_year": 2012, + "publication_venue": "IEEE Transactions on Automatic Control 57(5), 1265-1269", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2011.2173774" + } + }, + { + "source_id": "badouel-bednarczyk-borzyszkowski-caillaud-darondeau-2007", + "title": "Concurrent Secrets", + "authors": [ + "Éric Badouel", + "Marek A. Bednarczyk", + "Andrzej M. Borzyszkowski", + "Benoît Caillaud", + "Philippe Darondeau" + ], + "publication_year": 2007, + "publication_venue": "Discrete Event Dynamic Systems 17(4), 425-446", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1007/s10626-007-0020-5" + } + }, + { + "source_id": "yin-lafortune-2016", + "title": "A Uniform Approach for Synthesizing Property-Enforcing Supervisors for Partially-Observed Discrete-Event Systems", + "authors": [ + "Xiang Yin", + "Stéphane Lafortune" + ], + "publication_year": 2016, + "publication_venue": "IEEE Transactions on Automatic Control 61(8), 2140-2154", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2015.2484359" + } + }, + { + "source_id": "xie-yin-li-2022", + "title": "Opacity Enforcing Supervisory Control Using Nondeterministic Supervisors", + "authors": [ + "Yifan Xie", + "Xiang Yin", + "Shaoyuan Li" + ], + "publication_year": 2022, + "publication_venue": "IEEE Transactions on Automatic Control 67(12), 6567-6582", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/TAC.2021.3131125" + } + }, + { + "source_id": "cui-ma-giua-yin-2026", + "title": "Opacity Enforcing Supervisory Control with a Priori Unknown Supervisors", + "authors": [ + "Juntang Cui", + "Ziyue Ma", + "Alessandro Giua", + "Xiang Yin" + ], + "publication_year": 2026, + "publication_venue": "arXiv preprint arXiv:2604.04070", + "edition_or_version": "preprint version 1", + "immutable_locator": { + "kind": "doi", + "value": "10.48550/arXiv.2604.04070" + } + }, + { + "source_id": "partovi-jung-hai-2020", + "title": "Opacity of Discrete Event Systems with Active Intruder", + "authors": [ + "Alireza Partovi", + "Taeho Jung", + "Lin Hai" + ], + "publication_year": 2020, + "publication_venue": "arXiv preprint arXiv:2007.14960", + "edition_or_version": "preprint version 1", + "immutable_locator": { + "kind": "doi", + "value": "10.48550/arXiv.2007.14960" + } + }, + { + "source_id": "berard-mullins-sassolas-2015", + "title": "Quantifying Opacity", + "authors": [ + "Béatrice Bérard", + "John Mullins", + "Mathieu Sassolas" + ], + "publication_year": 2015, + "publication_venue": "Mathematical Structures in Computer Science 25(2), 361-403", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1017/S0960129513000637" + } + }, + { + "source_id": "andre-lime-marinho-sun-2022", + "title": "Guaranteeing Timed Opacity using Parametric Timed Model Checking", + "authors": [ + "Étienne André", + "Didier Lime", + "Dylan Marinho", + "Jun Sun" + ], + "publication_year": 2022, + "publication_venue": "ACM Transactions on Software Engineering and Methodology 31(4), 64:1-64:36", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/3502851" + } + }, + { + "source_id": "broberg-van-delft-sands-2015", + "title": "The Anatomy and Facets of Dynamic Policies", + "authors": [ + "Niklas Broberg", + "Bart van Delft", + "David Sands" + ], + "publication_year": 2015, + "publication_venue": "2015 IEEE Computer Security Foundations Symposium, 122-137", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/CSF.2015.16" + } + } + ], + "relations": { + "structural-validity": { + "relation_id": "structural-validity", + "display_name": "Structural validity", + "relation_class": "predicate", + "definition": "A single artifact satisfies its published closed structural schema.", + "left_carrier": "An artifact payload.", + "right_carrier": "The published schema selected by the artifact discriminator.", + "initial_states": "Not applicable; this is a unary predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Schema validity is a predicate over an artifact and schema, not a transition-system relation." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Schema validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one artifact payload and one schema revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Conformance to the declared structural shape.", + "proof_obligation": "Validate the complete payload against the named published schema." + }, + "bounded_evidence": [ + "JSON Schema and closed-model validation of named artifacts." + ], + "explicit_non_claims": [ + "Does not establish semantic validity, executability, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Backend equivalence", + "Participant strategic behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "contracts/schemas/" + ] + }, + "source_refs": [ + "milner-1980" + ] + }, + "semantic-validity": { + "relation_id": "semantic-validity", + "display_name": "Semantic validity", + "relation_class": "predicate", + "definition": "A structurally admitted artifact satisfies the named cross-reference and domain invariants.", + "left_carrier": "A parsed RAES artifact.", + "right_carrier": "The named semantic invariant set.", + "initial_states": "Not applicable; this is a unary predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Semantic validity checks a static artifact model rather than matching transitions." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Semantic validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one admitted artifact and one invariant revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "The named static semantic invariants.", + "proof_obligation": "Run every invariant in the declared semantic profile without error." + }, + "bounded_evidence": [ + "SemanticValidator results and invariant mutation tests." + ], + "explicit_non_claims": [ + "Does not establish realization, execution success, trace inclusion, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Runtime conformance", + "Backend comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes/validator/" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "capability-declaration": { + "relation_id": "capability-declaration", + "display_name": "Capability declaration", + "relation_class": "predicate", + "definition": "An apparatus declares support for governed capability and contract identifiers.", + "left_carrier": "A processor, backend, or participant manifest.", + "right_carrier": "The governed capability and contract vocabulary.", + "initial_states": "Not applicable; this is a declaration predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "A declaration is not execution behavior." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Manifest consumer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one manifest revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Portable declared support metadata.", + "proof_obligation": "Validate the manifest and resolve every governed identifier." + }, + "bounded_evidence": [ + "Manifest schema validation and capability-gap conformance cases." + ], + "explicit_non_claims": [ + "Does not prove that a declared capability works for every input." + ], + "incompatible_claim_surfaces": [ + "Universal backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes_contracts/manifest_authority.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "profile-satisfaction": { + "relation_id": "profile-satisfaction", + "display_name": "Profile satisfaction", + "relation_class": "predicate", + "definition": "An artifact bundle satisfies every required concern in a named profile revision.", + "left_carrier": "An artifact or bundle.", + "right_carrier": "A governed profile with required concerns.", + "initial_states": "Not applicable; this is a profile predicate.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Profile satisfaction aggregates named gates; it is not a behavioral matching relation." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Profile evaluator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "unary", + "quantification": { + "states": "For one artifact bundle and one profile revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "The conjunction of the profile's required concerns.", + "proof_obligation": "Evaluate every required concern with the profile's named validator." + }, + "bounded_evidence": [ + "Scientific completeness and backend-profile case results." + ], + "explicit_non_claims": [ + "Does not promote profile satisfaction to behavioral equivalence or empirical adequacy." + ], + "incompatible_claim_surfaces": [ + "Behavioral equivalence", + "Scientific adequacy" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "contracts/profiles/" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "bounded-probe-success": { + "relation_id": "bounded-probe-success", + "display_name": "Bounded fixture or probe success", + "relation_class": "empirical", + "definition": "Every named finite fixture or probe in the disclosed run produced its expected result.", + "left_carrier": "A concrete implementation run.", + "right_carrier": "A finite, enumerated fixture or probe set.", + "initial_states": "The concrete initial state selected by each named case.", + "transition_signature": { + "applicability": "applicable", + "labels": "The actions exercised by the named cases.", + "transition_relation": "Only transitions actually exercised by the finite cases.", + "observable_actions": "Case outputs and sanitized diagnostics.", + "hidden_actions": "No hidden action unless a governed projection declares one.", + "stuttering_actions": "Stuttering is explicit and relation-specific." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Case reporter", + "policy_ref": "behavioral-relations/bounded-probe-projection", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Only states reached by named cases.", + "traces": "Only enumerated finite traces.", + "schedulers": "Only schedulers exercised by the harness.", + "strategies": "Only strategies exercised by the harness.", + "environments": "Only named environments.", + "observations": "Only observations emitted by named cases." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Success of the enumerated cases.", + "proof_obligation": "Execute every named case and compare its bounded expected result." + }, + "bounded_evidence": [ + "Fixture-suite and target-probe reports with exact case identifiers." + ], + "explicit_non_claims": [ + "Does not quantify over untested transitions, schedulers, strategies, or environments.", + "Does not establish trace equivalence, simulation, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Universal conformance", + "Backend equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes_conformance/conformance.py" + ] + }, + "source_refs": [ + "park-1981", + "van-glabbeek-1990" + ] + }, + "bounded-but-for-necessity": { + "relation_id": "bounded-but-for-necessity", + "display_name": "Bounded but-for necessity", + "relation_class": "empirical", + "definition": "Within one declared finite causal boundary, a named candidate is supported as necessary for a named outcome when the admitted baseline outcome is true, a verified intervention removes or disables only that candidate, the matched counterfactual outcome is false, and evidence, reset, and cleanup gates pass.", + "left_carrier": "A named condition, weakness, control, or behavior present in the admitted baseline world.", + "right_carrier": "A governed outcome proposition evaluated independently in the baseline and intervention worlds.", + "initial_states": "The exact admitted baseline lineage and matching policy for the finite world pair.", + "transition_signature": { + "applicability": "applicable", + "labels": "The admitted baseline execution, typed candidate intervention, counterfactual execution, observation, reset, and cleanup actions.", + "transition_relation": "Only the two immutable runs and the one declared intervention admitted by the comparison case.", + "observable_actions": "Governed outcome truth, intervention evidence, matching checks, and reset or cleanup evidence.", + "hidden_actions": "No hidden action may change a held-fixed dimension; any permitted nuisance variation must be declared by the matching policy.", + "stuttering_actions": "Stuttering is relevant only when the declared time and observation models admit it." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Bounded necessity comparison audience", + "policy_ref": "behavioral-relations/bounded-but-for-necessity-projection", + "policy_revision": "rev1", + "redaction_scope": "Only governed evidence references and stable redacted diagnostics cross the comparison boundary.", + "order_treatment": "World executions are distinct immutable runs; their declared matching policy, not wall-clock order, governs comparison.", + "simultaneity_treatment": "Simultaneity is outside the binary criterion unless the matching policy and time model explicitly preserve it." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Only the admitted baseline and intervention-world initial states and reached states.", + "traces": "Only the two named finite executions.", + "schedulers": "Only schedulers admitted and matched by the declared policy.", + "strategies": "Only participant strategies represented and matched in the two runs.", + "environments": "Only the named apparatus, backend, scenario family, and permitted nuisance variation.", + "observations": "Only governed outcome, intervention, comparability, reset, and cleanup evidence for the named case." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Random streams, seeds, draws, and uncertainty are governed by the matching policy; a shared seed alone is insufficient." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Scheduling and participant concurrency must be held fixed or declared as permitted nuisance variation." + }, + "probability": { + "status": "outside-scope", + "treatment": "The binary criterion does not establish probabilistic necessity; statistical criteria require a separate governed relation or adapter." + }, + "time": { + "status": "parameterized", + "treatment": "Both worlds bind an explicit time model and comparison boundary; wall-clock proximity is not comparability." + }, + "partial_order": { + "status": "parameterized", + "treatment": "Causal or partial-order differences must be held fixed or explicitly admitted by the matching policy." + } + }, + "preservation": { + "property": "A finite binary but-for result for the exact candidate, outcome, worlds, intervention, apparatus, and matching policy.", + "proof_obligation": "Admit a true baseline outcome, verify the candidate intervention, admit a false counterfactual outcome, prove the declared matching checks for every other semantic dimension, and independently verify reset and cleanup." + }, + "bounded_evidence": [ + "Immutable experiment-run identities, proposition-truth results, intervention evidence, matching-policy checks, and reset or cleanup evidence for one named comparison case." + ], + "explicit_non_claims": [ + "Does not establish universal, actual, sufficient, probabilistic, or model-identified causation.", + "Does not treat replay, temporal order, correlation, failed execution, a no-op intervention, or incomparable outcome differences as necessity evidence.", + "Does not promote a supported finite comparison to proof or falsification-backed validation strength." + ], + "incompatible_claim_surfaces": [ + "Universal causal proof", + "Unbounded actual-cause attribution", + "Statistical or probabilistic necessity without a separate criterion" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes_conformance/necessity_evidence.py", + "implementations/python/packages/raes_conformance/necessity_types.py", + "implementations/python/packages/raes_conformance/necessity_validation.py", + "implementations/python/tests/test_necessity_validation.py" + ] + }, + "source_refs": [ + "halpern-pearl-2005" + ] + }, + "canonical-artifact-identity": { + "relation_id": "canonical-artifact-identity", + "display_name": "Canonical artifact identity", + "relation_class": "predicate", + "definition": "Two artifacts have identical canonical bytes or digest under one named serialization profile.", + "left_carrier": "One canonical artifact.", + "right_carrier": "Another canonical artifact under the same profile.", + "initial_states": "Not applicable; this is artifact identity.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Digest identity compares canonical representations, not enabled behavior." + }, + "observation_projection": { + "applicability": "identity", + "subject": "Canonical serializer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "symmetric", + "quantification": { + "states": "For the two named canonical artifacts.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Canonical byte identity under the named profile.", + "proof_obligation": "Canonicalize both artifacts with the same revision and compare bytes or collision-resistant digests." + }, + "bounded_evidence": [ + "Canonicalization and digest equality tests." + ], + "explicit_non_claims": [ + "Does not establish common provenance, equal executions, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Trace comparison", + "Backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "not-applicable", + "evidence_refs": [ + "implementations/python/packages/raes/canonical.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "realization-envelope-membership": { + "relation_id": "realization-envelope-membership", + "display_name": "Realization-envelope membership", + "relation_class": "set-relation", + "definition": "A concrete or requested point belongs to a governed realization envelope.", + "left_carrier": "A realization point.", + "right_carrier": "A closed realization-envelope set.", + "initial_states": "Not applicable; this is set membership.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Envelope membership is set-theoretic support, not a transition match." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Realization-envelope validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "left-to-right", + "quantification": { + "states": "For one point and one envelope revision.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Membership in the declared support set.", + "proof_obligation": "Evaluate every envelope dimension and closure rule for the point." + }, + "bounded_evidence": [ + "Witness and negative-probe envelope tests." + ], + "explicit_non_claims": [ + "Does not establish that execution succeeds or that behavior refines an abstract runtime." + ], + "incompatible_claim_surfaces": [ + "Runtime trace inclusion", + "Backend equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/realization/envelope-semantics.md" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "realization-envelope-subsumption": { + "relation_id": "realization-envelope-subsumption", + "display_name": "Realization-envelope subsumption", + "relation_class": "set-relation", + "definition": "Every point admitted by one realization envelope is admitted by another under the named closure rules.", + "left_carrier": "One realization-envelope set.", + "right_carrier": "Another realization-envelope set.", + "initial_states": "Not applicable; this is set inclusion.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Envelope subsumption compares support sets rather than transition systems." + }, + "observation_projection": { + "applicability": "not-applicable", + "subject": "Realization-envelope validator", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "left-to-right", + "quantification": { + "states": "Universally over points in the left envelope.", + "traces": "outside scope", + "schedulers": "outside scope", + "strategies": "outside scope", + "environments": "outside scope", + "observations": "outside scope" + }, + "dimensions": { + "nondeterminism": { + "status": "outside-scope", + "treatment": "The predicate does not quantify over branching choices." + }, + "concurrency": { + "status": "outside-scope", + "treatment": "The predicate does not compare concurrent executions." + }, + "probability": { + "status": "outside-scope", + "treatment": "The predicate does not compare probability measures." + }, + "time": { + "status": "outside-scope", + "treatment": "The predicate does not compare timed behavior." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The predicate does not compare event partial orders." + } + }, + "preservation": { + "property": "Set inclusion of declared realization support.", + "proof_obligation": "Prove or decide inclusion for every governed envelope dimension." + }, + "bounded_evidence": [ + "Finite witness and mutation tests for current envelope operators." + ], + "explicit_non_claims": [ + "Does not establish behavioral refinement, trace inclusion, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Runtime behavior", + "Participant behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "tested", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "implementations/python/packages/raes_contracts/realization_envelope.py" + ] + }, + "source_refs": [ + "abadi-lamport-1991" + ] + }, + "trace-inclusion": { + "relation_id": "trace-inclusion", + "display_name": "Projected trace inclusion", + "relation_class": "behavioral", + "definition": "Every projected concrete trace belongs to the abstract trace set under a declared projection.", + "left_carrier": "Concrete implementation transition system.", + "right_carrier": "Abstract RAES transition system.", + "initial_states": "Related concrete and abstract initial states.", + "transition_signature": { + "applicability": "applicable", + "labels": "Labels in the declared concrete and abstract alphabets.", + "transition_relation": "Concrete and abstract labelled transition relations.", + "observable_actions": "Actions retained by the governed projection.", + "hidden_actions": "Only actions explicitly hidden by the projection.", + "stuttering_actions": "Concrete stuttering must be permitted by the abstract obligation." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named observer or abstraction", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "All reachable related states.", + "traces": "Universally over projected concrete traces.", + "schedulers": "All admitted schedulers unless narrowed.", + "strategies": "Outside scope unless the systems are strategic.", + "environments": "All admitted environments unless narrowed.", + "observations": "Through the named projection only." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Abstract trace safety for projected concrete executions.", + "proof_obligation": "Show Proj(Traces_concrete) is a subset of Traces_abstract under stated fairness and divergence assumptions." + }, + "bounded_evidence": [ + "Finite target probes can falsify but cannot prove universal inclusion." + ], + "explicit_non_claims": [ + "Does not establish completeness, reverse inclusion, trace equivalence, or bisimulation." + ], + "incompatible_claim_surfaces": [ + "Current backend conformance report" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-runtime/README.md" + ] + }, + "source_refs": [ + "van-glabbeek-1990", + "abadi-lamport-1991", + "lynch-vaandrager-1995" + ] + }, + "trace-equivalence": { + "relation_id": "trace-equivalence", + "display_name": "Trace equivalence", + "relation_class": "behavioral", + "definition": "Two systems have equal projected trace sets under the same declared alphabet and projection.", + "left_carrier": "One labelled transition system.", + "right_carrier": "Another labelled transition system.", + "initial_states": "Paired initial states.", + "transition_signature": { + "applicability": "applicable", + "labels": "A shared declared label alphabet.", + "transition_relation": "The two labelled transition relations.", + "observable_actions": "Labels retained by the common projection.", + "hidden_actions": "Labels hidden by the common projection.", + "stuttering_actions": "Stuttering treatment must be identical." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named comparison observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "All reachable states contributing traces.", + "traces": "Universally over both trace sets.", + "schedulers": "All admitted schedulers.", + "strategies": "Outside scope unless strategies are encoded.", + "environments": "All declared environments.", + "observations": "Through one common projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Equality of projected trace languages.", + "proof_obligation": "Prove both projected trace inclusions under identical assumptions." + }, + "bounded_evidence": [ + "Finite trace comparison may refute but cannot establish equality." + ], + "explicit_non_claims": [ + "Does not preserve branching structure and does not imply bisimulation." + ], + "incompatible_claim_surfaces": [ + "Finite backend comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "van-glabbeek-1990" + ] + }, + "forward-simulation": { + "relation_id": "forward-simulation", + "display_name": "Forward simulation", + "relation_class": "behavioral", + "definition": "A relation maps each concrete step to an abstract matching path while preserving related states.", + "left_carrier": "Concrete implementation states.", + "right_carrier": "Abstract specification states.", + "initial_states": "Every concrete initial state relates to an abstract initial state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Concrete and abstract labels under a declared matching function.", + "transition_relation": "Concrete and abstract step relations.", + "observable_actions": "Labels exposed by the abstraction.", + "hidden_actions": "Labels mapped to hidden or stuttering abstract behavior.", + "stuttering_actions": "Explicit abstract stuttering where allowed." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Abstraction observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over related reachable states.", + "traces": "All concrete traces induced by matched steps.", + "schedulers": "All admitted concrete choices.", + "strategies": "Outside scope unless extended strategically.", + "environments": "All admitted environments.", + "observations": "Under the named abstraction." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Usually projected trace inclusion and named safety properties.", + "proof_obligation": "Supply a simulation relation and discharge initiality plus step-correspondence obligations." + }, + "bounded_evidence": [ + "Tests may exercise candidate obligations on finite models only." + ], + "explicit_non_claims": [ + "Successful probes do not establish a simulation relation." + ], + "incompatible_claim_surfaces": [ + "Current conformance results" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "lynch-vaandrager-1995", + "abadi-lamport-1991" + ] + }, + "backward-simulation": { + "relation_id": "backward-simulation", + "display_name": "Backward simulation", + "relation_class": "behavioral", + "definition": "A relation reasons from abstract successor possibilities back to concrete predecessors to establish implementation inclusion where forward simulation is insufficient.", + "left_carrier": "Concrete implementation states.", + "right_carrier": "Abstract specification states.", + "initial_states": "Initial and reachable-state coverage follow the selected backward-simulation theorem.", + "transition_signature": { + "applicability": "applicable", + "labels": "Concrete and abstract labels under the theorem's matching rule.", + "transition_relation": "Concrete and abstract step relations.", + "observable_actions": "Declared external labels.", + "hidden_actions": "Declared internal labels.", + "stuttering_actions": "History, prophecy, and stuttering treatment must be explicit." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Abstraction observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over theorem-defined related states.", + "traces": "All represented implementation behaviors.", + "schedulers": "All admitted nondeterministic choices.", + "strategies": "Outside scope unless extended strategically.", + "environments": "All admitted environments.", + "observations": "Under the named abstraction." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Implementation behavior inclusion under the cited theorem's assumptions.", + "proof_obligation": "Supply a backward simulation relation and discharge its reachability, initiality, and step obligations." + }, + "bounded_evidence": [ + "Finite model tests can exercise examples but do not prove a backend relation." + ], + "explicit_non_claims": [ + "Does not follow from result equality or a forward-only sampled trace." + ], + "incompatible_claim_surfaces": [ + "Current backend conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "lynch-vaandrager-1995" + ] + }, + "data-refinement": { + "relation_id": "data-refinement", + "display_name": "Data refinement", + "relation_class": "behavioral", + "definition": "Concrete data states represent abstract states through a retrieve relation while operations preserve that relation.", + "left_carrier": "Concrete state and operation space.", + "right_carrier": "Abstract state and operation space.", + "initial_states": "Concrete and abstract initial states related by the retrieve relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Operation invocations and observations.", + "transition_relation": "Concrete and abstract operation relations.", + "observable_actions": "Client-visible operation effects.", + "hidden_actions": "Internal representation steps.", + "stuttering_actions": "Stuttering and enabledness obligations are method-specific." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Client observation", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "All states satisfying the retrieve relation.", + "traces": "All operation histories covered by the refinement method.", + "schedulers": "All admitted operation nondeterminism.", + "strategies": "Outside scope unless strategies are modeled.", + "environments": "All client environments under stated preconditions.", + "observations": "Client-visible results only." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "The abstract operation contract and selected client-observable properties.", + "proof_obligation": "Define the retrieve relation and discharge initialization, applicability/enabledness, and correctness obligations." + }, + "bounded_evidence": [ + "Contract and operation tests are bounded evidence only." + ], + "explicit_non_claims": [ + "An SDL transformation function is not data refinement without these obligations." + ], + "incompatible_claim_surfaces": [ + "SDL phase transformation" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "abadi-lamport-1991", + "lynch-vaandrager-1995" + ] + }, + "strong-bisimulation": { + "relation_id": "strong-bisimulation", + "display_name": "Strong Park-Milner bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric relation matches every labelled step immediately in both directions.", + "left_carrier": "One labelled transition system.", + "right_carrier": "Another labelled transition system.", + "initial_states": "The two initial states belong to the bisimulation relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common label alphabet including internal labels.", + "transition_relation": "Both labelled transition relations.", + "observable_actions": "Every label is matched exactly.", + "hidden_actions": "Hidden labels are still labels and must match immediately.", + "stuttering_actions": "Only explicitly labelled stuttering steps can match." + }, + "observation_projection": { + "applicability": "identity", + "subject": "External comparison observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": false, + "direction": "symmetric", + "quantification": { + "states": "Universally over every related state pair.", + "traces": "All branching continuations.", + "schedulers": "All nondeterministic branches.", + "strategies": "Outside scope unless lifted to games.", + "environments": "All transition-system environments encoded in state.", + "observations": "Identity observation of labels." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Branching structure and modal properties under the chosen semantics.", + "proof_obligation": "Exhibit a symmetric relation closed under immediate labelled steps in both directions." + }, + "bounded_evidence": [ + "Finite algorithms can decide the relation only for supplied finite models." + ], + "explicit_non_claims": [ + "One shared trace, result, digest, or terminal observation is insufficient." + ], + "incompatible_claim_surfaces": [ + "Finite probes", + "Digest comparison" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "park-1981", + "milner-1980" + ] + }, + "weak-bisimulation": { + "relation_id": "weak-bisimulation", + "display_name": "Weak or observational bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric relation matches visible actions through closure over explicitly hidden tau steps.", + "left_carrier": "One labelled transition system with tau.", + "right_carrier": "Another labelled transition system with tau.", + "initial_states": "Initial states related after the selected tau closure.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common visible alphabet plus the declared tau label.", + "transition_relation": "Both labelled transition relations.", + "observable_actions": "Visible labels match through weak transitions.", + "hidden_actions": "Only the explicitly governed tau label is hidden.", + "stuttering_actions": "Tau closure and stuttering are explicit; divergence treatment is declared." + }, + "observation_projection": { + "applicability": "required", + "subject": "Observer that hides tau", + "policy_ref": "behavioral-relations/tau-projection", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related states under tau closure.", + "traces": "All weak traces and branching continuations.", + "schedulers": "All nondeterministic tau and visible branches.", + "strategies": "Outside scope unless lifted to games.", + "environments": "All encoded environments.", + "observations": "Through the named hiding projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Observation-preserving branching behavior under stated divergence and termination semantics.", + "proof_obligation": "Exhibit a weak bisimulation relation and discharge both directional weak-step obligations." + }, + "bounded_evidence": [ + "The catalog's hidden-action example demonstrates the definition on a finite toy model." + ], + "explicit_non_claims": [ + "Backend-internal work is not tau unless a governed projection declares it." + ], + "incompatible_claim_surfaces": [ + "Undeclared backend hiding" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "bounded", + "proof_status": "future", + "evidence_refs": [ + "contracts/concept-authority/behavioral-relations-v1.json#worked_examples" + ] + }, + "source_refs": [ + "milner-1980", + "van-glabbeek-1990" + ] + }, + "divergence-preserving-branching-bisimulation": { + "relation_id": "divergence-preserving-branching-bisimulation", + "display_name": "Divergence-preserving branching bisimulation", + "relation_class": "behavioral", + "definition": "A symmetric branching bisimulation matches visible transitions through finite closure over an explicitly governed tau set while preserving each related branching point and explicit infinite tau behavior in both directions.", + "left_carrier": "One labelled transition system with a closed visible/tau partition and explicit deadlock, termination, and divergence semantics.", + "right_carrier": "Another labelled transition system over the same projected visible alphabet and governed tau treatment.", + "initial_states": "The revisioned relation-parameter profile names both initial states and requires them to belong to the greatest fixed-point relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "A common projected visible alphabet plus only the tau labels enumerated by the revisioned relation-parameter profile.", + "transition_relation": "Both complete labelled transition relations over the profile's quantified carriers.", + "observable_actions": "Every projected visible action is matched in both directions after finite tau closure while the pre-action branching state remains related.", + "hidden_actions": "Only profile-enumerated tau actions are hidden; redacted occurrences, refusals, unsupported outcomes, errors, deadlock, termination, and divergence are not hidden by default.", + "stuttering_actions": "Finite tau stuttering is admitted at a related branching point; explicit infinite tau paths must be preserved in both directions." + }, + "observation_projection": { + "applicability": "required", + "subject": "The participant, audience, auditor, or other observer named by the closed relation-parameter profile.", + "policy_ref": "Revisioned divergence-preserving branching-bisimulation projection from the claim profile.", + "policy_revision": "The exact projection revision bound by the claim.", + "redaction_scope": "The profile enumerates every visible, redacted-occurrence, and tau label; implementation-internal or content-redacted does not imply hidden.", + "order_treatment": "The profile fixes sequence, interleaving, step, causal, or other order semantics; one linearization cannot establish a partial-order claim.", + "simultaneity_treatment": "Only simultaneity represented in the selected LTS and visible projection is preserved." + }, + "projection_required": true, + "relation_parameter_profile_required": true, + "direction": "symmetric", + "quantification": { + "states": "greatest-fixed-point relation", + "traces": "All visible and tau continuations from every related state pair, including infinite tau continuations.", + "schedulers": "Every nondeterministic branch and scheduler admitted by the closed profile.", + "strategies": "Outside scope unless the carriers explicitly encode game or adaptive-strategy state.", + "environments": "Every environment state and input admitted by the closed profile.", + "observations": "Exactly the visible alphabet after the revisioned closed projection; the tau partition remains explicit." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "Every admitted branch is matched; finite samples or selected schedules are insufficient." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The profile declares interleaving, step, true-concurrent, or other semantics and the preserved visible order." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability measures are excluded; a probabilistic relation must be named separately." + }, + "time": { + "status": "parameterized", + "treatment": "Untimed profiles erase no visible time label; timed claims require a clock and timed relation." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim requires a carrier and relation that preserve the declared causal structure." + } + }, + "preservation": { + "property": "Visible branching structure, finite governed tau stuttering, explicit termination and structural deadlock, and explicit divergence under the named projection and model dimensions.", + "proof_obligation": "Exhibit or decide the greatest symmetric relation satisfying both branching transfer clauses and both explicit-divergence clauses for the complete quantified carriers and initial states." + }, + "bounded_evidence": [ + "Issue #811 supplies an exact complete-finite theorem profile, witness family, mutation design, and pinned checker contract; it does not run the equivalence decision.", + "A finite model-check result is final only when the supplied finite carrier is the complete quantified domain and the evidence binds exact inputs, counts, tool provenance, result, and independent reproduction." + ], + "explicit_non_claims": [ + "Taxonomy revision rev6 defines this relation and the participant-crossing claim surface but does not establish a model-check or proof result.", + "The participant-crossing design does not establish live-runtime realization, backend conformance, whole-runtime equivalence, policy noninterference, or predicate opacity.", + "Depth limits, sampled traces, probes, matching digests, schema equality, and ordinary weak bisimulation are not this relation." + ], + "incompatible_claim_surfaces": [ + "Undeclared tau hiding or divergence treatment", + "Incomplete, sampled, depth-limited, or timeout-truncated carriers promoted to a complete result", + "Formal equivalence promoted to live-runtime, backend, noninterference, opacity, timed, probabilistic, strategic, concurrent, or partial-order assurance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "deliberately-unproved", + "checker_status": "not-implemented", + "model_check_status": "not-model-checked", + "runtime_enforcement_status": "not-enforced", + "backend_declaration_status": "not-declared", + "backend_realization_status": "not-realized", + "backend_conformance_status": "not-tested", + "evidence_refs": [ + "docs/decisions/adrs/adr-100-participant-crossing-bisimulation.md", + "specs/formal/participant-semantics/participant-crossing-bisimulation.md", + "docs/research/participant-bisimulation/implementation-program.json" + ] + }, + "source_refs": [ + "van-glabbeek-weijland-1996", + "van-glabbeek-luttik-trcka-2009" + ] + }, + "participant-predicate-opacity": { + "relation_id": "participant-predicate-opacity", + "display_name": "Participant-relative predicate opacity", + "relation_class": "epistemic", + "definition": "For every actual possible point at which the selected secret predicate is true, the named participant, audience, or coalition information cell induced by equal declared initial information and accumulated observations contains at least one possible point at which the predicate is false. The baseline is one-sided and possibilistic.", + "left_carrier": "One declared possible-point system whose points compose model or supervisor realization, run, evaluation cut, observer-local state and retained memory, exact-cut policy realization, and scheduler, environment, and order context.", + "right_carrier": "Not applicable; opacity is a unary property of the declared possible-point system and relation-parameter profile. A nonsecret point is a witness inside the same carrier, not a second system.", + "initial_states": "The profile fixes observer or coalition initial information, participant and audience identity, retained memory, policy and supervisor visibility, environment and scheduler classes, and the initial cut or horizon.", + "transition_signature": { + "applicability": "applicable", + "labels": "SEM-230 and participant-runtime occurrences, including proposal and control decisions, admission, attempt and result, disclosure and withholding, transformation, delivery and observation, policy or supervisor change, evidence, and audit when retained by the selected observation profile.", + "transition_relation": "Valid runs over existing participant world, view, local-history, archival-evidence, controller, authority, marking, exact-cut policy, crossing, delivery, observation, scheduler, environment, and order carriers.", + "observable_actions": "Exactly the content, occurrence, omission, decision, failure, delivery, retry, order, timing, policy-change, retrieval, evidence, or audit coordinates retained for the named observer by the revisioned relation-parameter profile.", + "hidden_actions": "Only occurrences removed by that observer-, policy-, supervisor-visibility-, cut-, memory-, time-, and order-relative observation function; hidden implementation does not imply hidden behavior.", + "stuttering_actions": "The untimed baseline removes finite unobserved stuttering and is progress- and termination-insensitive. Omission is observable only when the profile supplies an opportunity, deadline, acknowledgement, progress, or clock model." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant, audience, or explicit coalition", + "policy_ref": "SEM-231 participant-predicate-opacity observation profile", + "policy_revision": "The revisioned relation-parameter profile bound by the claim", + "redaction_scope": "Projection, redaction, declassification, decision disclosure, concealment, revocation, loss, and prior knowledge remain distinct; the profile states which resulting facts are observations.", + "order_treatment": "The profile selects total, participant-local, causal partial, simultaneous, or backend-serialized order. One linearization cannot establish a partial-order claim.", + "simultaneity_treatment": "Only declared simultaneity and visible causal frontiers are retained; timestamp equality is not simultaneity." + }, + "projection_required": true, + "relation_parameter_profile_required": true, + "direction": "unary", + "quantification": { + "states": "For every actual point in the declared carrier at which the revisioned secret predicate is true, there exists a point in the same observer information cell at which it is false.", + "traces": "Over every run and cut admitted by the selected current-, initial-, K-step, infinite-step, language, or other declared horizon profile.", + "schedulers": "Over the fixed declared scheduler class and admitted order contexts; scheduler-sensitive variants state their quantifier order.", + "strategies": "Passive profiles quantify over observations only. Active profiles quantify universally over the declared allowed adaptive participant strategies, with actual and witness runs possible under the same strategy.", + "environments": "Over the fixed declared model and environment class, including only supervisor or policy realizations admitted by the selected visibility posture.", + "observations": "Equality of declared initial information and accumulated observer-local observations, including memory across retries, replay, episodes, policy revisions, controller handoffs, and coalition sharing when selected." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "The baseline is possibilistic over the declared support. Nondeterministic or randomized supervision changes possible points but is not itself opacity evidence." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The profile fixes interleaving, step, simultaneous, causal, or other declared concurrent semantics and observer-visible order." + }, + "probability": { + "status": "outside-scope", + "treatment": "The baseline compares possibility support, not probability mass, posterior belief, entropy, leakage probability, or differential privacy. Quantitative opacity requires a separately governed relation." + }, + "time": { + "status": "parameterized", + "treatment": "The baseline is untimed and progress-insensitive. A timed profile requires a governed clock, duration and progress observation model and separately scoped evidence." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim compares declared visible causal frontiers and admitted schedules; a witness from one convenient linearization is insufficient." + } + }, + "preservation": { + "property": "The named observer never knows that the selected one-sided secret predicate is true at a protected cut because every actual secret information cell retains a possible nonsecret point.", + "proof_obligation": "For every quantified actual secret point and, for active profiles, every allowed adaptive strategy, exhibit or prove the existence of a nonsecret point with equal declared initial information and accumulated observation under the same profile, strategy, release schedule, supervisor visibility, memory, scheduler, environment, time, and order assumptions." + }, + "bounded_evidence": [ + "The SEM-231 formal specification gives four finite counterexamples covering an incomplete equal-history witness, supervisor-decision leakage, opacity without noninterference, and declassification-induced knowledge change.", + "The participant-opacity-baseline-v1 profile closes every relation coordinate and the deterministic processor exhausts exact declared finite possible-point carriers with digest-bound bounded outcomes or sanitized counterexample references.", + "implementations/python/tests/test_issue_961_participant_opacity.py covers profile and claim resolution, finite bounds, active strategies, coalition fusion, decision and omission channels, retained release knowledge, vacuity, deterministic evidence, replay, and explicit nonclaims.", + "The participant-opacity finite-state checker derives the complete reachable fixed point from an exact transition model, checks every reachable secret evaluation point, and binds catalog, profile, model, assumptions, explored coverage, tool version, result or safe counterexample, and replay evidence.", + "The committed model-check input and evidence fixtures retain the exact positive baseline model, result, digests, complete coverage, tool identity, and explicit nonclaims; invalid fixtures exercise count and partial-result promotion failures.", + "implementations/python/tests/test_issue_962_participant_opacity_model_check.py covers pair-probe incompleteness, supervisor behavior, active strategies, coalition fusion, retained memory, release changes, order and probability non-promotion, exact bounds, replay, and agreement with the bounded lane.", + "The Isabelle/HOL Participant_Opacity session kernel-checks the SEM-231 one-sided opacity definition, its information-cell knowledge characterization, and the conditional implication from a matching SEM-230 noninterference instance for an eligible predicate; checked countermodels preserve the invalid-promotion boundaries." + ], + "explicit_non_claims": [ + "Relation definition, catalog validation, claim-profile binding, and bounded finite analysis do not establish opacity of RAES, RUN-319, or any backend outside the exact admitted artifact.", + "No checker, finite-state model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance is delivered by taxonomy revision rev5.", + "Taxonomy revision rev7 adds only an in-process bounded-test checker; it does not add a model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev8 adds one exact finite-state model-check result; it does not add a mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev9 adds only the abstract conditional mathematical theorem bound to participant-opacity-theorem-v1; it does not prove opacity of RAES, a runtime, a deployment, a backend, or the finite fixture profile.", + "Bounded evidence authenticates only the normalized-input digest; it does not authenticate a claimed source artifact or materializer.", + "Opacity of one predicate does not imply SEM-230 policy noninterference, projected-history equivalence, epistemic indistinguishability of two selected worlds, trace inclusion or equivalence, simulation, refinement, or strong or weak bisimulation.", + "The possibilistic baseline makes no posterior-risk, entropy, probabilistic, differential-privacy, timed, progress-sensitive, or universal partial-order claim." + ], + "incompatible_claim_surfaces": [ + "Unrevisioned or untyped secret, observer, supervisor-visibility, memory, strategy, time, order, or release coordinates", + "Single equal-history pairs, finite probes, randomized behavior, runtime filters, or backend declarations promoted to universal opacity", + "Claims that erase prior knowledge through concealment, revocation, reset, rollback, or supersession" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "proved", + "checker_status": "implemented", + "model_check_status": "model-checked", + "runtime_enforcement_status": "not-enforced", + "backend_declaration_status": "not-declared", + "backend_realization_status": "not-realized", + "backend_conformance_status": "not-tested", + "evidence_refs": [ + "docs/decisions/adrs/adr-099-participant-relative-predicate-opacity.md", + "specs/formal/participant-semantics/participant-predicate-opacity.md", + "contracts/profiles/behavioral-relation/history/participant-opacity-baseline-v1-sem-231-rev2.json", + "contracts/profiles/behavioral-relation/participant-opacity-theorem-v1.json", + "contracts/schemas/formal-analysis/participant-opacity-model-check-input-v1.json", + "contracts/schemas/formal-analysis/participant-opacity-model-check-evidence-v1.json", + "contracts/fixtures/formal-analysis/participant-opacity-model-check-input-v1/valid/opaque-transition-model.json", + "contracts/fixtures/formal-analysis/participant-opacity-model-check-evidence-v1/valid/opaque-transition-model.json", + "implementations/python/packages/raes_processor/participant_opacity/_service.py", + "implementations/python/packages/raes_processor/participant_opacity/_model_check.py", + "implementations/python/tests/test_sem_231_participant_predicate_opacity.py", + "implementations/python/tests/test_issue_961_participant_opacity.py", + "implementations/python/tests/test_issue_962_participant_opacity_model_check.py", + "implementations/python/tests/test_issue_963_participant_opacity_proof.py", + "specs/formal/participant-semantics/isabelle/Participant_Opacity.thy", + "specs/formal/participant-semantics/participant-opacity-proof-evidence.json", + "tools/check_participant_opacity_proof.py", + "tools/isabelle_tool.py" + ] + }, + "source_refs": [ + "andre-lime-marinho-sun-2022", + "badouel-bednarczyk-borzyszkowski-caillaud-darondeau-2007", + "berard-mullins-sassolas-2015", + "broberg-van-delft-sands-2015", + "bryans-koutny-mazare-ryan-2008", + "cui-ma-giua-yin-2026", + "fagin-halpern-moses-vardi-1995", + "lin-2011", + "partovi-jung-hai-2020", + "saboori-hadjicostis-2012", + "schoepe-sabelfeld-2015", + "xie-yin-li-2022", + "yin-lafortune-2016" + ] + }, + "participant-projected-history-equivalence": { + "relation_id": "participant-projected-history-equivalence", + "display_name": "Participant-projected history equivalence", + "relation_class": "epistemic", + "definition": "Two finite or complete histories have equal projections for one participant under one observation-boundary revision.", + "left_carrier": "One global or backend history.", + "right_carrier": "Another global or backend history.", + "initial_states": "The compared histories share a declared participant and starting information state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Participant-visible events after projection.", + "transition_relation": "Underlying history extension relations.", + "observable_actions": "Events admitted by the participant observation boundary.", + "hidden_actions": "Events removed or redacted by the boundary.", + "stuttering_actions": "No additional stuttering assumption beyond projected history equality." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "States represented in the compared histories.", + "traces": "Finite histories unless a universal claim is separately proved.", + "schedulers": "Only schedulers represented in the histories.", + "strategies": "No strategic quantification.", + "environments": "Only the named environment/run context.", + "observations": "One participant and one policy revision." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Equality or indistinguishability of the named participant's projected histories.", + "proof_obligation": "Apply the existing participant observation boundary to both histories and compare the resulting ordered visible records." + }, + "bounded_evidence": [ + "Participant behavior-history and observation-envelope tests on named histories." + ], + "explicit_non_claims": [ + "Does not imply equality of global state, future behavior, knowledge, or strategy." + ], + "incompatible_claim_surfaces": [ + "Global-state comparison", + "Strategic equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-runtime/README.md" + ] + }, + "source_refs": [ + "fagin-halpern-moses-vardi-1995", + "milner-1980" + ] + }, + "policy-noninterference": { + "relation_id": "policy-noninterference", + "display_name": "Participant-policy noninterference", + "relation_class": "behavioral", + "definition": "For a fixed participant, episode and memory scope, model, environment class, scheduler class, order model, exact-cut policy-decision sequence, permitted declassification schedule, and low-strategy class, every low participant strategy produces equal support sets of projected participant-visible histories from low-equivalent initial states despite unauthorized high variation.", + "left_carrier": "The support set of valid labelled participant-policy runs from one low-equivalent initial state under one adaptive low strategy.", + "right_carrier": "The support set of valid labelled participant-policy runs from another low-equivalent initial state under the same adaptive low strategy.", + "initial_states": "Initial world, participant-view, delivered decision-surface history, participant memory, archival-evidence, controller, authority, marking, and policy states related by the SEM-230 low-equivalence relation at the declared initial state cut.", + "transition_signature": { + "applicability": "applicable", + "labels": "The closed SEM-230 alphabet for proposal, approval or denial, direction, intervention, handoff, override or cancellation, admission or rejection, attempt or result, disclosure or withholding, concealment, revocation, transformation, delivery, observation, policy change, evidence, and audit actions.", + "transition_relation": "The SEM-230 participant-policy crossing relation over existing world, view, local-history, archival-evidence, action, lifecycle, ordering, marking, controller, authority, policy, and provenance state.", + "observable_actions": "Labels retained for the named participant and audience by the exact-cut policy decision, marking/declassification intersection, and declared state-cut projection, including delivered decision surfaces.", + "hidden_actions": "Only labels mapped to tau by the named participant-, audience-, policy-decision-, and state-cut-relative projection; backend-internal actions are not intrinsically hidden.", + "stuttering_actions": "Finite hidden stuttering is removed by the declared tau closure; the baseline is termination- and progress-insensitive and does not claim divergence-sensitive preservation." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant and audience within one episode scope", + "policy_ref": "SEM-230 participant-information-flow policy", + "policy_revision": "The complete declared policy-decision sequence and exact state-cut bindings", + "redaction_scope": "Projection, masking, redaction, declassification, transformation, marking, loss, and weakening remain distinct and are evaluated deny-first.", + "order_treatment": "Compare occurrence-preserving visible histories under the same declared total, partial, causal, simultaneous, or backend-serialized order model; one convenient linearization is insufficient for a partial-order claim.", + "simultaneity_treatment": "Preserve declared simultaneity groups and visible order relations; timestamp equality does not establish simultaneity." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "For every pair of initial and reachable states related by low equivalence at the applicable participant-policy state cut and memory scope.", + "traces": "For all valid run support sets generated under the same low strategy, exact-cut policy decisions, and permitted declassification schedule.", + "schedulers": "For the fixed declared scheduler class; scheduler-sensitive variants must select and evidence a stronger relation.", + "strategies": "Universally over the declared class of adaptive low participant strategies mapping delivered local histories to choices or choice sets; no coalition-ability equivalence is implied.", + "environments": "For the fixed declared environment class and equal low environment inputs, allowing only the unauthorized high variation under examination.", + "observations": "Equality of support sets of occurrence-preserving histories projected for the named participant, audience, policy sequence, markings, declassification schedule, and order model." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "The baseline compares complete declared support sets of projected histories; equality of sampled or single histories is only bounded falsification evidence." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The selected sequential, total, partial, causal, simultaneous, or backend-serialized transition and visible-order model is fixed for the comparison." + }, + "probability": { + "status": "outside-scope", + "treatment": "The baseline compares support sets, not measures; probabilistic noninterference requires a separately governed probabilistic relation, kernel, bound, and evidence." + }, + "time": { + "status": "abstracted", + "treatment": "The baseline is termination- and progress-insensitive and excludes wall-clock timing; timed security requires a separately governed relation." + }, + "partial_order": { + "status": "parameterized", + "treatment": "When partial order is selected, the declared visible order relation and simultaneity groups are compared rather than one linear extension." + } + }, + "preservation": { + "property": "Unauthorized high variation does not change the support set of participant-visible histories observed by any declared adaptive low strategy, except at equal explicitly governed declassification events.", + "proof_obligation": "Prove support-set equality for every quantified low-equivalent pair and low strategy under the fixed participant, memory scope, exact-cut policy decisions, declassification schedule, model, scheduler, environment, and order assumptions, or report only the bounded counterexamples actually checked." + }, + "bounded_evidence": [ + "implementations/python/tests/test_sem_230_information_flow_control.py checks finite unauthorized-high, declassification-order, policy-revision, participant-relative hiding, deny-first, append-only-history, transformation-admission, and support-set counterexamples.", + "implementations/python/tests/test_asr_535_participant_flow_assurance.py exhausts a declared finite crossing domain for unauthorized-high purge and exact-cut declassification, and drives the shipped RUN-319 boundary for denial, withholding, redaction, governed declassification, transformation, stale or revoked policy, cross-participant leakage, participant-directed inject delivery, backend weakening, unsupported capability, and adversarial overclaim." + ], + "explicit_non_claims": [ + "The finite SEM-230 executable cases do not establish universal noninterference.", + "Projected-history equality does not establish policy noninterference without the stated low-equivalence, adaptive-strategy, memory, exact-cut policy, purge, declassification, scheduler, environment, and quantifier obligations.", + "No trace equivalence, simulation, refinement, strong or weak bisimulation, epistemic indistinguishability, timing security, probabilistic security, or backend realization is claimed.", + "The ASR-535 finite enumeration, runtime probes, and backend conformance cases are bounded falsification evidence and are not a model check or a proof; issues #810 to #813 own any stronger opacity, bisimulation, adversarial-control, or cross-backend status." + ], + "incompatible_claim_surfaces": [ + "Unrevisioned participant projection", + "Single-history or sampled-history equality", + "Undeclared scheduler, environment, timing, probability, or partial-order assumptions", + "Runtime or backend realization inferred from the definition" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/participant-semantics/information-flow-control.md", + "implementations/python/tests/test_sem_230_information_flow_control.py", + "implementations/python/packages/raes_runtime/participant_crossing_policy.py", + "implementations/python/packages/raes_conformance/conformance/participant_policy_probes.py", + "implementations/python/tests/test_run_319_participant_flow_policy.py", + "implementations/python/tests/test_asr_535_participant_flow_assurance.py" + ] + }, + "source_refs": [ + "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "clarkson-schneider-2010", + "fagin-halpern-moses-vardi-1995", + "goguen-meseguer-1982", + "milner-1980", + "sabelfeld-sands-2009", + "van-glabbeek-1990" + ] + }, + "io-alternating-refinement": { + "relation_id": "io-alternating-refinement", + "display_name": "Input/output alternating refinement", + "relation_class": "behavioral", + "definition": "A directional concrete-to-abstract relation preserves abstract outputs and internal behavior while respecting input ownership and declared action-availability obligations against environment choices.", + "left_carrier": "A concrete backend participant I/O transition system.", + "right_carrier": "An abstract RAES participant I/O transition system.", + "initial_states": "Every concrete initial participant decision state, including decision epoch zero, relates to an abstract initial decision state.", + "transition_signature": { + "applicability": "applicable", + "labels": "Participant proposals are inputs; participant views and observations are outputs; backend, scheduler, and environment labels retain their declared owners.", + "transition_relation": "Concrete and abstract I/O-labelled step relations under a declared refinement mapping.", + "observable_actions": "Participant-visible inputs and outputs under the named projection.", + "hidden_actions": "Only governed backend/internal labels mapped to tau by the named projection.", + "stuttering_actions": "Finite hidden concrete paths may match one abstract step only when the selected weak or branching treatment permits them." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named participant and audience", + "policy_ref": "participant-observation-boundary", + "policy_revision": "The exact-cut projection policy used by the claim", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Decision epochs are preserved under hidden stuttering; state cuts retain their declared order model.", + "simultaneity_treatment": "Simultaneity and partial-order frontiers are preserved only when declared by the claim." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Universally over related reachable concrete and abstract states.", + "traces": "All concrete traces induced by quantified inputs, outputs, and environment choices.", + "schedulers": "All schedulers in the declared fairness class.", + "strategies": "All participant and environment strategies in the declared action-ownership classes.", + "environments": "All environment choices admitted by the declared alternating quantifiers.", + "observations": "Under the exact named participant projection and delivery semantics." + }, + "dimensions": { + "nondeterminism": { + "status": "supported", + "treatment": "Input, output, scheduler, backend, and environment choices are separately owned and quantified." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, simultaneous, step, or true-concurrency semantics must be declared." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability requires a separately governed probabilistic alternating relation." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require explicit clock, fairness, timeout, and progress semantics." + }, + "partial_order": { + "status": "parameterized", + "treatment": "A partial-order claim relates declared causal frontiers rather than arbitrary linearizations." + } + }, + "preservation": { + "property": "Projected concrete traces remain abstractly admitted and declared participant inputs and outputs retain their availability and ownership obligations.", + "proof_obligation": "Supply the refinement relation, initial-state mapping, input/output ownership, availability and fairness obligations, and alternating step correspondence for every quantified choice." + }, + "bounded_evidence": [ + "Decision-surface lifecycle tests may falsify selected initiality, delivery, availability, freshness, and step-matching cases on finite models." + ], + "explicit_non_claims": [ + "Trace inclusion alone does not establish input availability or alternating refinement.", + "Successful participant loops do not establish the universal relation." + ], + "incompatible_claim_surfaces": [ + "Current bounded backend conformance reports" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md", + "implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py", + "implementations/python/tests/test_behavioral_relations.py" + ] + }, + "source_refs": [ + "alur-henzinger-kupferman-vardi-1998", + "lynch-tuttle-1989", + "lynch-vaandrager-1995" + ] + }, + "epistemic-indistinguishability": { + "relation_id": "epistemic-indistinguishability", + "display_name": "Epistemic indistinguishability", + "relation_class": "epistemic", + "definition": "Two worlds are indistinguishable to an agent when they occupy the same governed information set.", + "left_carrier": "One epistemic world/state.", + "right_carrier": "Another epistemic world/state.", + "initial_states": "Worlds in the same agent-indexed accessibility or information relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Optional temporal or action labels of the epistemic model.", + "transition_relation": "The declared interpreted-system or Kripke transition relation.", + "observable_actions": "Agent-observable propositions and events.", + "hidden_actions": "Facts excluded by the information projection.", + "stuttering_actions": "Stuttering is model-specific." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named agent", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "All worlds in the selected information relation.", + "traces": "Histories only when the interpreted-system model includes them.", + "schedulers": "All schedulers represented in the model.", + "strategies": "Strategies are outside the relation itself.", + "environments": "All environments represented by possible worlds.", + "observations": "One agent or explicitly named group." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Schedulers and branch quantifiers must be stated." + }, + "concurrency": { + "status": "parameterized", + "treatment": "Interleaving, step, or true-concurrency semantics must be stated." + }, + "probability": { + "status": "outside-scope", + "treatment": "Probability is excluded unless a probabilistic relation is named." + }, + "time": { + "status": "parameterized", + "treatment": "Timed claims require an explicit clock and time model." + }, + "partial_order": { + "status": "abstracted", + "treatment": "Default traces linearize order; partial-order claims require a separate declared model." + } + }, + "preservation": { + "property": "Truth of formulas invariant over the selected information set, subject to the logic.", + "proof_obligation": "Define possible worlds, the agent-indexed indistinguishability relation, valuation, and any temporal interaction." + }, + "bounded_evidence": [ + "Equal projected finite histories may be evidence for a bounded information-state comparison." + ], + "explicit_non_claims": [ + "Does not follow from global-state equality and does not establish strategic equivalence." + ], + "incompatible_claim_surfaces": [ + "Current participant conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "fagin-halpern-moses-vardi-1995" + ] + }, + "alternating-strategic-equivalence": { + "relation_id": "alternating-strategic-equivalence", + "display_name": "Alternating or strategic equivalence", + "relation_class": "strategic", + "definition": "Two game structures preserve the abilities of named coalitions under explicit strategy and environment quantifiers.", + "left_carrier": "One concurrent or alternating game structure.", + "right_carrier": "Another concurrent or alternating game structure.", + "initial_states": "Related initial game states.", + "transition_signature": { + "applicability": "applicable", + "labels": "Joint actions, chance outcomes, and state transitions.", + "transition_relation": "Both game transition functions or relations.", + "observable_actions": "Player observations and public actions.", + "hidden_actions": "Hidden information under the named observation partitions.", + "stuttering_actions": "Stuttering, simultaneous moves, and scheduler steps are explicit." + }, + "observation_projection": { + "applicability": "required", + "subject": "Named players or coalition", + "policy_ref": "participant-observation-boundary", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related game states.", + "traces": "Outcome paths induced by quantified strategies.", + "schedulers": "Schedulers and chance kernels explicitly quantified.", + "strategies": "Coalitions and strategy classes universally/existentially quantified as declared.", + "environments": "Adversarial environment choices explicitly quantified.", + "observations": "Player-indexed observation partitions." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Environment and scheduler choices are explicit." + }, + "concurrency": { + "status": "supported", + "treatment": "Joint and simultaneous moves are part of the game structure." + }, + "probability": { + "status": "parameterized", + "treatment": "Chance kernels must be declared when present." + }, + "time": { + "status": "parameterized", + "treatment": "Timed strategies require an explicit clock model." + }, + "partial_order": { + "status": "parameterized", + "treatment": "Concurrent action order is part of the declared game semantics." + } + }, + "preservation": { + "property": "Coalition ability for the stated objective class.", + "proof_obligation": "Define players, legal joint actions, observations, strategy class, coalitions, chance, scheduler/fairness, objectives, and an alternating relation in both directions." + }, + "bounded_evidence": [ + "Finite recorded joint-action traces can only falsify selected cases." + ], + "explicit_non_claims": [ + "Capability declarations and shared probe outcomes do not establish strategic equivalence." + ], + "incompatible_claim_surfaces": [ + "Current multi-agent conformance" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "alur-henzinger-kupferman-vardi-1998", + "alur-henzinger-kupferman-2002" + ] + }, + "probabilistic-bisimulation": { + "relation_id": "probabilistic-bisimulation", + "display_name": "Probabilistic bisimulation", + "relation_class": "behavioral", + "definition": "Related states match labelled probability distributions over equivalence classes under the selected probabilistic process model.", + "left_carrier": "One probabilistic labelled transition system.", + "right_carrier": "Another probabilistic labelled transition system.", + "initial_states": "Initial states belong to the probabilistic bisimulation relation.", + "transition_signature": { + "applicability": "applicable", + "labels": "Shared visible and hidden labels of the probabilistic model.", + "transition_relation": "Labelled transitions to probability distributions.", + "observable_actions": "Labels retained by the projection.", + "hidden_actions": "Declared hidden labels.", + "stuttering_actions": "Weak variants require an explicit probabilistic tau closure." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Probabilistic process observer", + "policy_ref": "behavioral-relations/catalog", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Universally over related states.", + "traces": "All probabilistic traces or branching distributions required by the variant.", + "schedulers": "All nondeterministic schedulers explicitly quantified.", + "strategies": "Outside scope unless combined with games.", + "environments": "All admitted probabilistic environments.", + "observations": "Through the named projection." + }, + "dimensions": { + "nondeterminism": { + "status": "parameterized", + "treatment": "Scheduler quantification is mandatory." + }, + "concurrency": { + "status": "parameterized", + "treatment": "The process-composition semantics must be named." + }, + "probability": { + "status": "supported", + "treatment": "Probability distributions are matched over relation classes." + }, + "time": { + "status": "outside-scope", + "treatment": "Continuous or timed probability needs another variant." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "The base relation uses labelled branching structure." + } + }, + "preservation": { + "property": "Probability mass over related behavior classes.", + "proof_obligation": "Exhibit a relation whose matched transitions assign equal probability to every relation-closed class under the chosen variant." + }, + "bounded_evidence": [ + "Statistical samples may refute parameters but do not prove distributional branching equivalence." + ], + "explicit_non_claims": [ + "Statistical similarity or equal sample means is not probabilistic bisimulation." + ], + "incompatible_claim_surfaces": [ + "Ordinary empirical study" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "larsen-skou-1991" + ] + }, + "statistical-similarity": { + "relation_id": "statistical-similarity", + "display_name": "Statistical similarity", + "relation_class": "empirical", + "definition": "A predeclared metric over sampled populations lies within a stated similarity criterion with uncertainty.", + "left_carrier": "One sampled population or system output distribution.", + "right_carrier": "Another sampled population, target distribution, or reference data.", + "initial_states": "The preregistered sampling frame and apparatus context.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Statistical similarity compares sampled measures, not transition systems unless a separate model binds them." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study analyst", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Sampled observations only.", + "traces": "Sampled run outcomes only.", + "schedulers": "Schedulers represented by the sampling design.", + "strategies": "Strategies represented by the sampling design.", + "environments": "The preregistered population and apparatus.", + "observations": "The named metric/estimand projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Variation is represented through the sampling model." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency matters only through measured outcomes." + }, + "probability": { + "status": "supported", + "treatment": "The sampling distribution and uncertainty method are explicit." + }, + "time": { + "status": "parameterized", + "treatment": "Sampling windows and time domains are declared." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Event partial order is not inferred from aggregate metrics." + } + }, + "preservation": { + "property": "The stated similarity criterion for the named estimand and population.", + "proof_obligation": "Predeclare population, sampling frame, metric, criterion, uncertainty method, and decision rule; then execute the study." + }, + "bounded_evidence": [ + "Experiment runs, derived measures, and uncertainty intervals." + ], + "explicit_non_claims": [ + "Does not establish behavioral, epistemic, strategic, or probabilistic bisimulation." + ], + "incompatible_claim_surfaces": [ + "Universal backend behavior" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/experiment-core/README.md" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "statistical-equivalence": { + "relation_id": "statistical-equivalence", + "display_name": "Statistical equivalence", + "relation_class": "empirical", + "definition": "A preregistered equivalence test supports that a named estimand lies within a stated equivalence margin for the sampled population.", + "left_carrier": "One sampled population or treatment.", + "right_carrier": "Another sampled population, treatment, or reference.", + "initial_states": "The preregistered sampling frame, allocation, and apparatus context.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "A statistical equivalence test does not compare enabled transitions." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study analyst", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "symmetric", + "quantification": { + "states": "Sampled units only.", + "traces": "Sampled run outcomes only.", + "schedulers": "Schedulers represented by allocation/sampling.", + "strategies": "Strategies represented by sampled conditions.", + "environments": "The stated target population.", + "observations": "The named estimand and measurement projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Variation is handled by the statistical model." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency is only a measured covariate unless modeled." + }, + "probability": { + "status": "supported", + "treatment": "Equivalence margins, error rates, and uncertainty are explicit." + }, + "time": { + "status": "parameterized", + "treatment": "Study windows and time domains are declared." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Aggregate equivalence does not preserve event order." + } + }, + "preservation": { + "property": "Equivalence of the named estimand within the preregistered margin.", + "proof_obligation": "Specify the equivalence hypotheses, margin, error control, sampling design, and analysis before observing results." + }, + "bounded_evidence": [ + "Experiment-study analysis and derived measures." + ], + "explicit_non_claims": [ + "Statistical equivalence is not behavioral equivalence or proof of implementation conformance." + ], + "incompatible_claim_surfaces": [ + "Bisimulation claim" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "partial", + "test_status": "bounded", + "proof_status": "deliberately-unproved", + "evidence_refs": [ + "specs/formal/experiment-core/README.md" + ] + }, + "source_refs": [ + "wellek-2010" + ] + }, + "empirical-adequacy": { + "relation_id": "empirical-adequacy", + "display_name": "Empirical adequacy", + "relation_class": "empirical", + "definition": "Observed evidence supports a purpose-relative, bounded adequacy claim for a named phenomenon and intended use.", + "left_carrier": "A model, language, implementation, or method under study.", + "right_carrier": "A defined empirical target, task, or phenomenon.", + "initial_states": "The preregistered study population, tasks, and apparatus.", + "transition_signature": { + "applicability": "not-applicable", + "labels": "not applicable", + "transition_relation": "not applicable", + "observable_actions": "not applicable", + "hidden_actions": "not applicable", + "stuttering_actions": "not applicable", + "not_applicable_rationale": "Empirical adequacy may study behavior but is not itself a transition-system equivalence." + }, + "observation_projection": { + "applicability": "parameterized", + "subject": "Study audience", + "policy_ref": "experiment-study-v1", + "policy_revision": "rev1", + "redaction_scope": "No redaction beyond the named projection.", + "order_treatment": "Sequence order is preserved.", + "simultaneity_treatment": "Simultaneity is preserved only when declared." + }, + "projection_required": true, + "direction": "left-to-right", + "quantification": { + "states": "Observed study units.", + "traces": "Observed runs and tasks.", + "schedulers": "Schedulers represented by the protocol.", + "strategies": "Strategies represented by participant/task sampling.", + "environments": "The named target population and intended use.", + "observations": "The preregistered measures and coding projection." + }, + "dimensions": { + "nondeterminism": { + "status": "abstracted", + "treatment": "Uncontrolled variation is handled as a validity limitation." + }, + "concurrency": { + "status": "abstracted", + "treatment": "Concurrency is measured only when the protocol names it." + }, + "probability": { + "status": "parameterized", + "treatment": "Sampling and uncertainty must be reported." + }, + "time": { + "status": "parameterized", + "treatment": "Study period and temporal validity are explicit." + }, + "partial_order": { + "status": "outside-scope", + "treatment": "Adequacy does not imply partial-order preservation." + } + }, + "preservation": { + "property": "Fitness for the explicitly stated empirical purpose within the study boundary.", + "proof_obligation": "Predeclare tasks, population, measures, success/falsification criteria, analysis, limitations, and evidence lineage." + }, + "bounded_evidence": [ + "Independent parser, authoring, review, and diagnostic-recovery studies." + ], + "explicit_non_claims": [ + "Repeated bounded observations do not establish universal semantics, conformance, or behavioral equivalence." + ], + "incompatible_claim_surfaces": [ + "Universal language equivalence" + ], + "assurance": { + "definition_status": "defined", + "implementation_status": "not-implemented", + "test_status": "not-tested", + "proof_status": "future", + "evidence_refs": [] + }, + "source_refs": [ + "bueno-1997", + "wellek-2010" + ] + } + }, + "claim_surfaces": [ + { + "surface_id": "sdl-transformation", + "intended_relation_ids": [ + "structural-validity", + "semantic-validity", + "canonical-artifact-identity" + ], + "evidence_boundary": "Deterministic phase functions and finite invariant, round-trip, canonicalization, and property tests.", + "prohibited_relation_ids": [ + "data-refinement", + "forward-simulation", + "trace-equivalence", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "No universal behavioral refinement or equivalence is currently proved." + ] + }, + { + "surface_id": "backend-realization", + "intended_relation_ids": [ + "realization-envelope-membership", + "bounded-probe-success", + "trace-inclusion", + "io-alternating-refinement" + ], + "evidence_boundary": "Envelope checks and named target probes are bounded evidence; projected trace inclusion plus input/output ownership and availability under I/O alternating refinement are the intended universal actionable-participant obligations and remain deliberately unproved.", + "prohibited_relation_ids": [ + "trace-equivalence", + "backward-simulation", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "Provisioning, snapshots, witnesses, and negative probes do not prove behavioral equivalence." + ] + }, + { + "surface_id": "backend-comparison", + "intended_relation_ids": [ + "bounded-probe-success", + "statistical-similarity", + "canonical-artifact-identity" + ], + "evidence_boundary": "Only named invariants, probes, digests, populations, metrics, and uncertainty procedures are compared.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "A shared result, digest, or finite trace is not universal same behavior." + ] + }, + { + "surface_id": "participant-visible-behavior", + "intended_relation_ids": [ + "participant-projected-history-equivalence" + ], + "evidence_boundary": "Comparison is parameterized by participant, observation-boundary policy revision, redaction, order, simultaneity, and run context.", + "prohibited_relation_ids": [ + "epistemic-indistinguishability", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "Equal projected histories do not expose hidden global state or prove future knowledge or strategy equivalence." + ] + }, + { + "surface_id": "participant-information-flow-policy", + "intended_relation_ids": [ + "policy-noninterference" + ], + "evidence_boundary": "The SEM-230 relation is defined over named participant, audience, memory scope, exact-cut policy-decision sequence, low-equivalence, adaptive low-strategy class, dynamic purge, permitted declassification schedule, scheduler/environment classes, order model, and support-set semantics. Current executable evidence is limited to finite models, finite reference-runtime enforcement probes, and finite backend-conformance cases.", + "prohibited_relation_ids": [ + "participant-projected-history-equivalence", + "trace-equivalence", + "forward-simulation", + "backward-simulation", + "data-refinement", + "strong-bisimulation", + "weak-bisimulation", + "epistemic-indistinguishability", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "Definition, catalog validation, claim-policy checks, and finite counterexamples do not prove universal noninterference or runtime/backend realization.", + "Reference-runtime enforcement and passing backend-conformance probes establish neither universal noninterference nor native-backend realization." + ] + }, + { + "surface_id": "participant-opacity", + "intended_relation_ids": [ + "participant-predicate-opacity" + ], + "evidence_boundary": "Every claim binds a revisioned observer, secret predicate, possible-point carrier, initial-information and observation functions, memory and horizon, supervisor visibility, passive or active strategy domain, release schedule, scheduler and environment classes, time and order, nondeterminism and probability support, assurance axis, and evidence boundary.", + "prohibited_relation_ids": [ + "participant-projected-history-equivalence", + "policy-noninterference", + "trace-inclusion", + "trace-equivalence", + "forward-simulation", + "backward-simulation", + "data-refinement", + "strong-bisimulation", + "weak-bisimulation", + "epistemic-indistinguishability", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "A profile, finite witness, bounded probe, random choice, model check, runtime decision, or backend declaration establishes only its named assurance axis and evidence scope.", + "No current RAES runtime or backend is claimed opaque." + ] + }, + { + "surface_id": "participant-crossing-bisimulation", + "intended_relation_ids": [ + "divergence-preserving-branching-bisimulation" + ], + "evidence_boundary": "Claims bind the exact independently derived abstract and concrete model revisions and digests, initial states, complete quantified carrier and counts, closed participant/audience projection and tau partition, relation profile, source and mapping revisions, assurance axis, pinned tool provenance, result or safe counterexample, mutations, limitations, and independent reproduction.", + "prohibited_relation_ids": [ + "strong-bisimulation", + "weak-bisimulation", + "trace-equivalence", + "policy-noninterference", + "participant-predicate-opacity", + "probabilistic-bisimulation" + ], + "explicit_non_claims": [ + "Issue #811 defines the theorem and proof program but does not establish the formal equivalence result.", + "A formal model-check does not establish live-runtime realization, backend conformance, whole-runtime equivalence, noninterference, opacity, or a stronger timed, probabilistic, strategic, concurrent, or partial-order relation." + ] + }, + { + "surface_id": "multi-agent-interaction", + "intended_relation_ids": [ + "bounded-probe-success", + "alternating-strategic-equivalence", + "probabilistic-bisimulation" + ], + "evidence_boundary": "Current evidence is structural and finite; strategic and probabilistic relations are definitions for future governed models.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation" + ], + "explicit_non_claims": [ + "Current joint-action, chance, simultaneous-move, and mean-field records do not prove strategic equivalence." + ] + }, + { + "surface_id": "counterfactual-necessity-validation", + "intended_relation_ids": [ + "bounded-but-for-necessity", + "bounded-probe-success" + ], + "evidence_boundary": "One revisioned claim, one immutable baseline/intervention-world pair, one typed and verified intervention, admitted proposition-truth evidence, a declared matching policy, and independently verified reset and cleanup.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "empirical-adequacy", + "statistical-equivalence" + ], + "explicit_non_claims": [ + "A supported finite but-for comparison is not universal causal proof, actual-cause attribution, sufficiency, determinism, or statistical necessity." + ] + }, + { + "surface_id": "independent-adequacy-study", + "intended_relation_ids": [ + "empirical-adequacy", + "statistical-similarity", + "statistical-equivalence" + ], + "evidence_boundary": "Claims bind to a preregistered population, task set, metric or coding scheme, uncertainty, falsification criteria, and limitations.", + "prohibited_relation_ids": [ + "trace-equivalence", + "strong-bisimulation", + "alternating-strategic-equivalence" + ], + "explicit_non_claims": [ + "Bounded observations and statistical findings cannot be promoted to universal behavioral proof." + ] + } + ], + "worked_examples": { + "finite-probe-counterexample": { + "example_id": "finite-probe-counterexample", + "purpose": "Two implementations pass the same finite visible probe a, but the left system has an additional enabled b transition that the right system cannot match.", + "left_system": { + "states": [ + "l0", + "l1", + "l2" + ], + "initial_state": "l0", + "transitions": [ + { + "source": "l0", + "action": "a", + "target": "l1" + }, + { + "source": "l0", + "action": "b", + "target": "l2" + } + ] + }, + "right_system": { + "states": [ + "r0", + "r1" + ], + "initial_state": "r0", + "transitions": [ + { + "source": "r0", + "action": "a", + "target": "r1" + } + ] + }, + "tested_visible_trace": [ + "a" + ], + "hidden_action": "tau", + "expected_strong_bisimulation": false, + "expected_weak_matching": false, + "evidence_boundary": "The shared a probe is evidence only for that finite trace; the unmatched b branch refutes strong bisimulation.", + "explicit_non_claims": [ + "This toy counterexample is not evidence about any RAES backend." + ] + }, + "hidden-action-counterexample": { + "example_id": "hidden-action-counterexample", + "purpose": "The abstract system performs visible send directly; the backend performs governed hidden tau and then send.", + "left_system": { + "states": [ + "a0", + "a1" + ], + "initial_state": "a0", + "transitions": [ + { + "source": "a0", + "action": "send", + "target": "a1" + } + ] + }, + "right_system": { + "states": [ + "b0", + "b1", + "b2" + ], + "initial_state": "b0", + "transitions": [ + { + "source": "b0", + "action": "tau", + "target": "b1" + }, + { + "source": "b1", + "action": "send", + "target": "b2" + } + ] + }, + "tested_visible_trace": [ + "send" + ], + "hidden_action": "tau", + "expected_strong_bisimulation": false, + "expected_weak_matching": true, + "evidence_boundary": "Strong matching fails on tau; weak visible-trace matching succeeds only under the declared tau-hiding projection and finite termination assumptions.", + "explicit_non_claims": [ + "The example does not declare arbitrary backend-internal work hidden and does not prove an RAES backend relation." + ] + } + } +} diff --git a/contracts/fixtures/artifact-transformations-v1/cases/canonicalize-portable-contract.json b/contracts/fixtures/artifact-transformations-v1/cases/canonicalize-portable-contract.json new file mode 100644 index 000000000..be7346755 --- /dev/null +++ b/contracts/fixtures/artifact-transformations-v1/cases/canonicalize-portable-contract.json @@ -0,0 +1,14 @@ +{ + "case_id": "canonicalize-portable-contract", + "operation": "canonicalize-portable-contract/v1", + "source_contract": "external-concept-bindings/v1", + "source_path": "concept-authority/external-concept-bindings-v1/valid/attack-enterprise.json", + "request": {}, + "policy": { + "allowed_loss_kinds": [] + }, + "expected": { + "status": "success", + "identity_map": [] + } +} diff --git a/contracts/fixtures/artifact-transformations-v1/cases/refuse-collision.json b/contracts/fixtures/artifact-transformations-v1/cases/refuse-collision.json new file mode 100644 index 000000000..96fe79a58 --- /dev/null +++ b/contracts/fixtures/artifact-transformations-v1/cases/refuse-collision.json @@ -0,0 +1,17 @@ +{ + "case_id": "refuse-collision", + "operation": "rename-sdl-declaration/v1", + "source_contract": "sdl-authoring-input/v1", + "source_path": "artifact-transformations-v1/sources/collision.sdl.yaml", + "request": { + "target_address": "nodes.web", + "new_local_name": "frontend" + }, + "policy": { + "allowed_loss_kinds": [] + }, + "expected": { + "status": "refused", + "identity_map": [] + } +} diff --git a/contracts/fixtures/artifact-transformations-v1/cases/remove-with-explicit-loss.json b/contracts/fixtures/artifact-transformations-v1/cases/remove-with-explicit-loss.json new file mode 100644 index 000000000..b8ae2bb1e --- /dev/null +++ b/contracts/fixtures/artifact-transformations-v1/cases/remove-with-explicit-loss.json @@ -0,0 +1,18 @@ +{ + "case_id": "remove-with-explicit-loss", + "operation": "remove-sdl-declaration/v1", + "source_contract": "sdl-authoring-input/v1", + "source_path": "artifact-transformations-v1/sources/references.sdl.yaml", + "request": { + "target_address": "nodes.peer" + }, + "policy": { + "allowed_loss_kinds": [ + "declaration-removed" + ] + }, + "expected": { + "status": "success", + "identity_map": [] + } +} diff --git a/contracts/fixtures/artifact-transformations-v1/cases/rename-composed-reference.json b/contracts/fixtures/artifact-transformations-v1/cases/rename-composed-reference.json new file mode 100644 index 000000000..d4b052c07 --- /dev/null +++ b/contracts/fixtures/artifact-transformations-v1/cases/rename-composed-reference.json @@ -0,0 +1,22 @@ +{ + "case_id": "rename-composed-reference", + "operation": "rename-sdl-declaration/v1", + "source_contract": "sdl-authoring-input/v1", + "source_path": "sdl/variation-points-v1/composition/root.yaml", + "request": { + "target_address": "nodes.shared.primary", + "new_local_name": "frontend" + }, + "policy": { + "allowed_loss_kinds": [] + }, + "expected": { + "status": "success", + "identity_map": [ + { + "before": "nodes.shared.primary", + "after": "nodes.shared.frontend" + } + ] + } +} diff --git a/contracts/fixtures/artifact-transformations-v1/cases/rename-references.json b/contracts/fixtures/artifact-transformations-v1/cases/rename-references.json new file mode 100644 index 000000000..5e4b3f58e --- /dev/null +++ b/contracts/fixtures/artifact-transformations-v1/cases/rename-references.json @@ -0,0 +1,22 @@ +{ + "case_id": "rename-references", + "operation": "rename-sdl-declaration/v1", + "source_contract": "sdl-authoring-input/v1", + "source_path": "artifact-transformations-v1/sources/references.sdl.yaml", + "request": { + "target_address": "nodes.web", + "new_local_name": "frontend" + }, + "policy": { + "allowed_loss_kinds": [] + }, + "expected": { + "status": "success", + "identity_map": [ + { + "before": "nodes.web", + "after": "nodes.frontend" + } + ] + } +} diff --git a/contracts/fixtures/artifact-transformations-v1/sources/collision.sdl.yaml b/contracts/fixtures/artifact-transformations-v1/sources/collision.sdl.yaml new file mode 100644 index 000000000..550834403 --- /dev/null +++ b/contracts/fixtures/artifact-transformations-v1/sources/collision.sdl.yaml @@ -0,0 +1,6 @@ +name: transformation-collision-case +nodes: + web: + type: switch + frontend: + type: switch diff --git a/contracts/fixtures/artifact-transformations-v1/sources/references.sdl.yaml b/contracts/fixtures/artifact-transformations-v1/sources/references.sdl.yaml new file mode 100644 index 000000000..3f46224dd --- /dev/null +++ b/contracts/fixtures/artifact-transformations-v1/sources/references.sdl.yaml @@ -0,0 +1,19 @@ +name: transformation-reference-case +nodes: + web: + type: vm + resources: + ram: 1 GiB + cpu: 1 + peer: + type: switch +content: + payload: + type: file + target: web + path: /opt/payload +relationships: + loop: + type: connects_to + source: web + target: web diff --git a/contracts/fixtures/artifact-transformations/artifact-transformation-report-v1/invalid/success-without-target.json b/contracts/fixtures/artifact-transformations/artifact-transformation-report-v1/invalid/success-without-target.json new file mode 100644 index 000000000..1ff5ec8ef --- /dev/null +++ b/contracts/fixtures/artifact-transformations/artifact-transformation-report-v1/invalid/success-without-target.json @@ -0,0 +1,32 @@ +{ + "schema_version": "artifact-transformation-report/v1", + "operation_profile": "canonicalize-portable-contract/v1", + "status": "success", + "artifact_kind": "portable-contract", + "source_profile": "external-concept-bindings/v1", + "target_profile": "external-concept-bindings/v1", + "canonicalization_profile": "rfc8785-jcs-sha256/v1", + "source_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "derivation_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "preconditions": [ + { + "check_id": "source-admitted", + "outcome": "passed", + "diagnostic_codes": [] + } + ], + "postconditions": [], + "affected_identities": [], + "identity_map": [], + "preservation": { + "profile": "canonical-artifact-identity", + "outcome": "verified", + "evidence_digests": [ + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "limitations": [] + }, + "losses": [], + "diagnostics": [] +} diff --git a/contracts/fixtures/artifact-transformations/artifact-transformation-report-v1/valid/canonical-identity.json b/contracts/fixtures/artifact-transformations/artifact-transformation-report-v1/valid/canonical-identity.json new file mode 100644 index 000000000..e537aedc8 --- /dev/null +++ b/contracts/fixtures/artifact-transformations/artifact-transformation-report-v1/valid/canonical-identity.json @@ -0,0 +1,39 @@ +{ + "schema_version": "artifact-transformation-report/v1", + "operation_profile": "canonicalize-portable-contract/v1", + "status": "success", + "artifact_kind": "portable-contract", + "source_profile": "external-concept-bindings/v1", + "target_profile": "external-concept-bindings/v1", + "canonicalization_profile": "rfc8785-jcs-sha256/v1", + "source_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "target_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "policy_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "derivation_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "preconditions": [ + { + "check_id": "source-admitted", + "outcome": "passed", + "diagnostic_codes": [] + } + ], + "postconditions": [ + { + "check_id": "canonical-identity", + "outcome": "passed", + "diagnostic_codes": [] + } + ], + "affected_identities": [], + "identity_map": [], + "preservation": { + "profile": "canonical-artifact-identity", + "outcome": "verified", + "evidence_digests": [ + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "limitations": [] + }, + "losses": [], + "diagnostics": [] +} diff --git a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/feature-support-bounded.json b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/feature-support-bounded.json index a74124ff3..4e0f0138b 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/feature-support-bounded.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/feature-support-bounded.json @@ -52,6 +52,12 @@ "operation-status-v1", "runtime-snapshot-v1" ], + "observation_capabilities": { + "forwarding-agents": { + "verification_scope": "configuration", + "observation_strength": "daemon-observed" + } + }, "constraints": {} } ], 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 d0f9f33f4..780f9673b 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -187,6 +187,18 @@ ], "support_level": "unsupported" }, + { + "constraint_refs": [], + "disclosure_refs": [ + "disclosure:participant_predicate_opacity:unsupported" + ], + "evidence_refs": [], + "feature": "participant_predicate_opacity", + "limitation_refs": [ + "limitation:participant_predicate_opacity:not-realized" + ], + "support_level": "unsupported" + }, { "constraint_refs": [], "disclosure_refs": [ @@ -368,6 +380,7 @@ "runtime-snapshot-v1" ], "domain": "runtime-realization", + "observation_capabilities": {}, "support_mode": "constrained", "supported_constraint_kinds": [ "account-feature", diff --git a/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json b/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json index f1a8ec47e..ce7beaeb4 100644 --- a/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/behavioral-relations-v1/valid/reference.json @@ -1,7 +1,7 @@ { "schema_version": "behavioral-relations/v1", "taxonomy_id": "raes-behavioral-relations", - "taxonomy_revision": "rev8", + "taxonomy_revision": "rev12", "bibliography": [ { "source_id": "park-1981", @@ -153,6 +153,67 @@ "value": "10.3233/JCS-2009-0352" } }, + { + "source_id": "denning-1976", + "title": "A Lattice Model of Secure Information Flow", + "authors": [ + "Dorothy E. Denning" + ], + "publication_year": 1976, + "publication_venue": "Communications of the ACM 19(5), 236-243", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/360051.360056" + } + }, + { + "source_id": "myers-liskov-1998", + "title": "Complete, Safe Information Flow with Decentralized Labels", + "authors": [ + "Andrew C. Myers", + "Barbara Liskov" + ], + "publication_year": 1998, + "publication_venue": "1998 IEEE Symposium on Security and Privacy, 186-197", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1109/SECPRI.1998.674834" + } + }, + { + "source_id": "myers-sabelfeld-zdancewic-2006", + "title": "Enforcing Robust Declassification and Qualified Robustness", + "authors": [ + "Andrew C. Myers", + "Andrei Sabelfeld", + "Steve Zdancewic" + ], + "publication_year": 2006, + "publication_venue": "Journal of Computer Security 14(2), 157-196", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.3233/JCS-2006-14203" + } + }, + { + "source_id": "cecchetti-myers-arden-2017", + "title": "Nonmalleable Information Flow Control", + "authors": [ + "Ethan Cecchetti", + "Andrew C. Myers", + "Owen Arden" + ], + "publication_year": 2017, + "publication_venue": "Proceedings of the 2017 ACM SIGSAC Conference on Computer and Communications Security", + "edition_or_version": "version of record", + "immutable_locator": { + "kind": "doi", + "value": "10.1145/3133956.3134054" + } + }, { "source_id": "lynch-tuttle-1989", "title": "An Introduction to Input/Output Automata", @@ -2000,13 +2061,19 @@ "implementations/python/tests/test_issue_961_participant_opacity.py covers profile and claim resolution, finite bounds, active strategies, coalition fusion, decision and omission channels, retained release knowledge, vacuity, deterministic evidence, replay, and explicit nonclaims.", "The participant-opacity finite-state checker derives the complete reachable fixed point from an exact transition model, checks every reachable secret evaluation point, and binds catalog, profile, model, assumptions, explored coverage, tool version, result or safe counterexample, and replay evidence.", "The committed model-check input and evidence fixtures retain the exact positive baseline model, result, digests, complete coverage, tool identity, and explicit nonclaims; invalid fixtures exercise count and partial-result promotion failures.", - "implementations/python/tests/test_issue_962_participant_opacity_model_check.py covers pair-probe incompleteness, supervisor behavior, active strategies, coalition fusion, retained memory, release changes, order and probability non-promotion, exact bounds, replay, and agreement with the bounded lane." + "implementations/python/tests/test_issue_962_participant_opacity_model_check.py covers pair-probe incompleteness, supervisor behavior, active strategies, coalition fusion, retained memory, release changes, order and probability non-promotion, exact bounds, replay, and agreement with the bounded lane.", + "The Isabelle/HOL Participant_Opacity session kernel-checks the SEM-231 one-sided opacity definition, its information-cell knowledge characterization, and the conditional implication from a matching SEM-230 noninterference instance for an eligible predicate; checked countermodels preserve the invalid-promotion boundaries.", + "The participant-opacity-runtime-reference-v1 profile and RUN-319 crossing boundary enforce one exact finite observation inventory with safe, atomic runtime-enforcement decision bindings." + ,"The reference backend declares bounded support for the exact runtime profile; generic target conformance separately observes backend-native realization across the protected and complement points and binds the manifest, profile, configuration, tool, environment, and probe-set digests." ], "explicit_non_claims": [ "Relation definition, catalog validation, claim-profile binding, and bounded finite analysis do not establish opacity of RAES, RUN-319, or any backend outside the exact admitted artifact.", "No checker, finite-state model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance is delivered by taxonomy revision rev5.", "Taxonomy revision rev7 adds only an in-process bounded-test checker; it does not add a model check, mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", "Taxonomy revision rev8 adds one exact finite-state model-check result; it does not add a mathematical proof, runtime enforcement, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev9 adds only the abstract conditional mathematical theorem bound to participant-opacity-theorem-v1; it does not prove opacity of RAES, a runtime, a deployment, a backend, or the finite fixture profile.", + "Taxonomy revision rev10 adds partial runtime enforcement only for participant-opacity-runtime-reference-v1; it does not establish general opacity, live model checking or proof, supervisor synthesis, backend declaration, backend realization, or backend conformance.", + "Taxonomy revision rev11 adds one bounded reference-backend declaration, realization, and conformance lane; it does not establish universal opacity, proof of a live execution, native realization for other backends, or cross-backend equivalence.", "Bounded evidence authenticates only the normalized-input digest; it does not authenticate a claimed source artifact or materializer.", "Opacity of one predicate does not imply SEM-230 policy noninterference, projected-history equivalence, epistemic indistinguishability of two selected worlds, trace inclusion or equivalence, simulation, refinement, or strong or weak bisimulation.", "The possibilistic baseline makes no posterior-risk, entropy, probabilistic, differential-privacy, timed, progress-sensitive, or universal partial-order claim." @@ -2020,17 +2087,18 @@ "definition_status": "defined", "implementation_status": "implemented", "test_status": "bounded", - "proof_status": "deliberately-unproved", + "proof_status": "proved", "checker_status": "implemented", "model_check_status": "model-checked", - "runtime_enforcement_status": "not-enforced", - "backend_declaration_status": "not-declared", - "backend_realization_status": "not-realized", - "backend_conformance_status": "not-tested", + "runtime_enforcement_status": "partial", + "backend_declaration_status": "declared", + "backend_realization_status": "partial", + "backend_conformance_status": "bounded", "evidence_refs": [ "docs/decisions/adrs/adr-099-participant-relative-predicate-opacity.md", "specs/formal/participant-semantics/participant-predicate-opacity.md", - "contracts/profiles/behavioral-relation/participant-opacity-baseline-v1.json", + "contracts/profiles/behavioral-relation/history/participant-opacity-baseline-v1-sem-231-rev2.json", + "contracts/profiles/behavioral-relation/participant-opacity-theorem-v1.json", "contracts/schemas/formal-analysis/participant-opacity-model-check-input-v1.json", "contracts/schemas/formal-analysis/participant-opacity-model-check-evidence-v1.json", "contracts/fixtures/formal-analysis/participant-opacity-model-check-input-v1/valid/opaque-transition-model.json", @@ -2039,7 +2107,19 @@ "implementations/python/packages/raes_processor/participant_opacity/_model_check.py", "implementations/python/tests/test_sem_231_participant_predicate_opacity.py", "implementations/python/tests/test_issue_961_participant_opacity.py", - "implementations/python/tests/test_issue_962_participant_opacity_model_check.py" + "implementations/python/tests/test_issue_962_participant_opacity_model_check.py", + "implementations/python/tests/test_issue_963_participant_opacity_proof.py", + "specs/formal/participant-semantics/isabelle/Participant_Opacity.thy", + "specs/formal/participant-semantics/participant-opacity-proof-evidence.json", + "tools/check_participant_opacity_proof.py", + "tools/isabelle_tool.py", + "contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json", + "implementations/python/packages/raes_contracts/participant_opacity_runtime.py", + "implementations/python/packages/raes_runtime/participant_crossing_mediation.py", + "implementations/python/tests/test_issue_964_participant_opacity_runtime.py", + "docs/decisions/issue-965-participant-opacity-backend-realization-preflight.md", + "implementations/python/packages/raes_conformance/conformance/participant_opacity_probes.py", + "implementations/python/tests/test_issue_965_participant_opacity_backend.py" ] }, "source_refs": [ @@ -2147,14 +2227,14 @@ "relation_id": "policy-noninterference", "display_name": "Participant-policy noninterference", "relation_class": "behavioral", - "definition": "For a fixed participant, episode and memory scope, model, environment class, scheduler class, order model, exact-cut policy-decision sequence, permitted declassification schedule, and low-strategy class, every low participant strategy produces equal support sets of projected participant-visible histories from low-equivalent initial states despite unauthorized high variation.", + "definition": "For a fixed participant, episode and memory scope, model, environment class, scheduler class, order model, exact-cut policy-decision sequence, SEM-233 revisioned confidentiality/integrity flow-policy profile, permitted declassification and endorsement schedule, and low-strategy class, every low participant strategy produces equal support sets of projected participant-visible histories from low-equivalent initial states despite unauthorized high variation.", "left_carrier": "The support set of valid labelled participant-policy runs from one low-equivalent initial state under one adaptive low strategy.", "right_carrier": "The support set of valid labelled participant-policy runs from another low-equivalent initial state under the same adaptive low strategy.", - "initial_states": "Initial world, participant-view, delivered decision-surface history, participant memory, archival-evidence, controller, authority, marking, and policy states related by the SEM-230 low-equivalence relation at the declared initial state cut.", + "initial_states": "Initial world, participant-view, delivered decision-surface history, participant memory, archival-evidence, controller, authority, marking, policy, immutable provenance/influence, and revisioned two-coordinate flow-label states related by the SEM-230 low-equivalence relation at the declared initial state cut.", "transition_signature": { "applicability": "applicable", "labels": "The closed SEM-230 alphabet for proposal, approval or denial, direction, intervention, handoff, override or cancellation, admission or rejection, attempt or result, disclosure or withholding, concealment, revocation, transformation, delivery, observation, policy change, evidence, and audit actions.", - "transition_relation": "The SEM-230 participant-policy crossing relation over existing world, view, local-history, archival-evidence, action, lifecycle, ordering, marking, controller, authority, policy, and provenance state.", + "transition_relation": "The SEM-230 participant-policy crossing relation, parameterized by SEM-233 sem-233/rev1 confidentiality/integrity obligation joins and final-sink predicates, over existing world, view, local-history, archival-evidence, action, lifecycle, ordering, marking, controller, authority, policy, provenance, and influence state.", "observable_actions": "Labels retained for the named participant and audience by the exact-cut policy decision, marking/declassification intersection, and declared state-cut projection, including delivered decision surfaces.", "hidden_actions": "Only labels mapped to tau by the named participant-, audience-, policy-decision-, and state-cut-relative projection; backend-internal actions are not intrinsically hidden.", "stuttering_actions": "Finite hidden stuttering is removed by the declared tau closure; the baseline is termination- and progress-insensitive and does not claim divergence-sensitive preservation." @@ -2162,9 +2242,9 @@ "observation_projection": { "applicability": "required", "subject": "Named participant and audience within one episode scope", - "policy_ref": "SEM-230 participant-information-flow policy", - "policy_revision": "The complete declared policy-decision sequence and exact state-cut bindings", - "redaction_scope": "Projection, masking, redaction, declassification, transformation, marking, loss, and weakening remain distinct and are evaluated deny-first.", + "policy_ref": "SEM-230 participant-information-flow policy parameterized by participant-boundary-flow-policy-v1", + "policy_revision": "The complete declared policy-decision sequence and exact state-cut bindings plus SEM-233 sem-233/rev1 and participant-boundary-flow-policy-v1@rev1", + "redaction_scope": "Projection, masking, redaction, confidentiality declassification, integrity endorsement, transformation, marking, loss, and weakening remain distinct and are evaluated deny-first.", "order_treatment": "Compare occurrence-preserving visible histories under the same declared total, partial, causal, simultaneous, or backend-serialized order model; one convenient linearization is insufficient for a partial-order claim.", "simultaneity_treatment": "Preserve declared simultaneity groups and visible order relations; timestamp equality does not establish simultaneity." }, @@ -2206,10 +2286,12 @@ }, "bounded_evidence": [ "implementations/python/tests/test_sem_230_information_flow_control.py checks finite unauthorized-high, declassification-order, policy-revision, participant-relative hiding, deny-first, append-only-history, transformation-admission, and support-set counterexamples.", - "implementations/python/tests/test_asr_535_participant_flow_assurance.py exhausts a declared finite crossing domain for unauthorized-high purge and exact-cut declassification, and drives the shipped RUN-319 boundary for denial, withholding, redaction, governed declassification, transformation, stale or revoked policy, cross-participant leakage, participant-directed inject delivery, backend weakening, unsupported capability, and adversarial overclaim." + "implementations/python/tests/test_asr_535_participant_flow_assurance.py exhausts a declared finite crossing domain for unauthorized-high purge and exact-cut declassification, and drives the shipped RUN-319 boundary for denial, withholding, redaction, governed declassification, transformation, stale or revoked policy, cross-participant leakage, participant-directed inject delivery, backend weakening, unsupported capability, and adversarial overclaim.", + "implementations/python/tests/test_sem_233_adversarial_boundary_flow.py checks the finite SEM-233 sem-233/rev1 two-coordinate powerset algebra, conservative possible-influence joins, missing labels and provenance/influence refs, laundering, coordinate-specific release operations, handoff and cross-episode carriage, stale cuts, and deny-first sink predicates." ], "explicit_non_claims": [ "The finite SEM-230 executable cases do not establish universal noninterference.", + "The SEM-233 definition and test-local finite model do not publish a portable contract or establish runtime enforcement, backend realization, instrumentation completeness, intentional-subversion robustness, monitor honesty, model alignment, or covert-channel control.", "Projected-history equality does not establish policy noninterference without the stated low-equivalence, adaptive-strategy, memory, exact-cut policy, purge, declassification, scheduler, environment, and quantifier obligations.", "No trace equivalence, simulation, refinement, strong or weak bisimulation, epistemic indistinguishability, timing security, probabilistic security, or backend realization is claimed.", "The ASR-535 finite enumeration, runtime probes, and backend conformance cases are bounded falsification evidence and are not a model check or a proof; issues #810 to #813 own any stronger opacity, bisimulation, adversarial-control, or cross-backend status." @@ -2227,7 +2309,9 @@ "proof_status": "deliberately-unproved", "evidence_refs": [ "specs/formal/participant-semantics/information-flow-control.md", + "specs/formal/participant-semantics/adversarial-flow-control.md", "implementations/python/tests/test_sem_230_information_flow_control.py", + "implementations/python/tests/test_sem_233_adversarial_boundary_flow.py", "implementations/python/packages/raes_runtime/participant_crossing_policy.py", "implementations/python/packages/raes_conformance/conformance/participant_policy_probes.py", "implementations/python/tests/test_run_319_participant_flow_policy.py", @@ -2236,10 +2320,14 @@ }, "source_refs": [ "bohannon-pierce-sjoberg-weirich-zdancewic-2009", + "cecchetti-myers-arden-2017", "clarkson-schneider-2010", + "denning-1976", "fagin-halpern-moses-vardi-1995", "goguen-meseguer-1982", "milner-1980", + "myers-liskov-1998", + "myers-sabelfeld-zdancewic-2006", "sabelfeld-sands-2009", "van-glabbeek-1990" ] @@ -2901,7 +2989,7 @@ "intended_relation_ids": [ "policy-noninterference" ], - "evidence_boundary": "The SEM-230 relation is defined over named participant, audience, memory scope, exact-cut policy-decision sequence, low-equivalence, adaptive low-strategy class, dynamic purge, permitted declassification schedule, scheduler/environment classes, order model, and support-set semantics. Current executable evidence is limited to finite models, finite reference-runtime enforcement probes, and finite backend-conformance cases.", + "evidence_boundary": "The SEM-230 relation is defined over named participant, audience, memory scope, exact-cut policy-decision sequence, low-equivalence, adaptive low-strategy class, dynamic purge, permitted declassification schedule, scheduler/environment classes, order model, and support-set semantics. SEM-233 sem-233/rev1 parameterizes it with participant-boundary-flow-policy-v1@rev1, independent confidentiality/integrity obligation sets, conservative provenance/influence carriage, coordinate-specific release operations, and final-sink predicates. Current SEM-233 executable evidence is a test-local finite model; existing runtime and backend probes implement SEM-230 only.", "prohibited_relation_ids": [ "participant-projected-history-equivalence", "trace-equivalence", @@ -2915,6 +3003,7 @@ ], "explicit_non_claims": [ "Definition, catalog validation, claim-policy checks, and finite counterexamples do not prove universal noninterference or runtime/backend realization.", + "SEM-233 bounded algebra tests do not establish portable-contract support, runtime enforcement, backend realization, intentional-subversion robustness, model alignment, monitor honesty, instrumentation completeness, or covert-channel control.", "Reference-runtime enforcement and passing backend-conformance probes establish neither universal noninterference nor native-backend realization." ] }, 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 63064684b..7ca1aa0a2 100644 --- a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json @@ -987,6 +987,10 @@ "title": "Participant Intervention", "description": "Backend supports governed participant intervention and supervisory control occurrences." }, + "participant_predicate_opacity": { + "title": "Participant Predicate Opacity", + "description": "Backend declares evidence-bound realization of a governed participant-relative predicate-opacity profile." + }, "participant_transformation": { "title": "Participant Transformation", "description": "Backend supports governed non-mutating participant-boundary transformations with fresh result identity." diff --git a/contracts/fixtures/participant-runtime/participant-information-state-record-v1/invalid/strong-without-profile.json b/contracts/fixtures/participant-runtime/participant-information-state-record-v1/invalid/strong-without-profile.json new file mode 100644 index 000000000..13309f628 --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-information-state-record-v1/invalid/strong-without-profile.json @@ -0,0 +1,50 @@ +{ + "event_id": "information-state-blue-43", + "schema_name": "raes.participant_runtime.information_state", + "schema_version": "1.0.0", + "event_type": "participant_information_state", + "extension_policy": "reject_unknown_required", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "occurred_at": "2026-05-26T10:20:10Z", + "recorded_at": "2026-05-26T10:20:10Z", + "ingested_at": "2026-05-26T10:20:11Z", + "clock_authority": "sim.tick", + "ordering_basis": "simulation_tick", + "actor_ref": "participants.blue.rl", + "producer_ref": "adapters.cyborg-blue.v1", + "authorization_scope": "participant:participants.blue.rl", + "information_state_ref": "information-state.blue.ep002.cut43", + "information_state_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "payload_ref": "payloads.information-state.blue.ep002.cut43", + "state_cut": { + "cut_kind": "sequence_prefix", + "cut_ref": "cut.blue.ep002.43", + "history_domain": "participant_behavior_history", + "order_model": "backend_serialized_order", + "anchor_event_ref": "evt-blue-43", + "anchor_order": 43, + "history_prefix_length": 44, + "predecessor_event_refs": [] + }, + "participant_memory_scope": "episode_local_reset", + "memory_reset_authority_ref": "episode-reset.blue.ep002", + "audience_scope_ref": "audiences.participant.blue", + "visibility_projection_ref": "projections.blue.local.telemetry.v1", + "projection_version": "projection.blue.v1", + "projection_policy_revision": "projection.blue.v1", + "redaction_policy_revision": "redaction.blue.v1", + "information_guarantee": "history_consistent", + "source_refs": [ + { + "contract_id": "participant-observation-envelope-v1", + "ref": "observations.blue.local.telemetry.43", + "relation": "observed" + } + ], + "occurrence_history_ref": "history.blue.ep002.prefix43", + "reconstruction_algorithm_id": "raes.occurrence-prefix-evidence", + "reconstruction_algorithm_version": "1.0.0", + "reconstruction_proof_ref": "proofs.information-state.blue.ep002.cut43", + "reconstructed_state_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} diff --git a/contracts/fixtures/participant-runtime/participant-information-state-record-v1/valid/history-consistent-cut.json b/contracts/fixtures/participant-runtime/participant-information-state-record-v1/valid/history-consistent-cut.json new file mode 100644 index 000000000..c1118875f --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-information-state-record-v1/valid/history-consistent-cut.json @@ -0,0 +1,63 @@ +{ + "event_id": "information-state-blue-43", + "schema_name": "raes.participant_runtime.information_state", + "schema_version": "1.0.0", + "event_type": "participant_information_state", + "extension_policy": "reject_unknown_required", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "sequence_number": 43, + "occurred_at": "2026-05-26T10:20:10Z", + "recorded_at": "2026-05-26T10:20:10Z", + "ingested_at": "2026-05-26T10:20:11Z", + "clock_authority": "sim.tick", + "ordering_basis": "simulation_tick", + "actor_ref": "participants.blue.rl", + "producer_ref": "adapters.cyborg-blue.v1", + "provenance_refs": [ + "provenance.backend-realized" + ], + "evidence_refs": [ + "evidence.information-state-blue-43" + ], + "redaction_policy_ref": "redaction.blue-observation.v1", + "authorization_scope": "participant:participants.blue.rl", + "information_state_ref": "information-state.blue.ep002.cut43", + "information_state_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "payload_ref": "payloads.information-state.blue.ep002.cut43", + "state_cut": { + "cut_kind": "sequence_prefix", + "cut_ref": "cut.blue.ep002.43", + "history_domain": "participant_behavior_history", + "order_model": "backend_serialized_order", + "anchor_event_ref": "evt-blue-43", + "anchor_order": 43, + "history_prefix_length": 44, + "predecessor_event_refs": [ + "evt-blue-42" + ] + }, + "participant_memory_scope": "episode_local_reset", + "memory_reset_authority_ref": "episode-reset.blue.ep002", + "audience_scope_ref": "audiences.participant.blue", + "visibility_projection_ref": "projections.blue.local.telemetry.v1", + "projection_version": "projection.blue.v1", + "projection_policy_revision": "projection.blue.v1", + "redaction_policy_revision": "redaction.blue.v1", + "information_guarantee": "history_consistent", + "source_refs": [ + { + "contract_id": "participant-observation-envelope-v1", + "ref": "observations.blue.local.telemetry.43", + "relation": "observed" + } + ], + "occurrence_history_ref": "history.blue.ep002.prefix43", + "reconstruction_profile_ref": "occurrence-prefix-evidence-v1", + "reconstruction_algorithm_id": "raes.occurrence-prefix-evidence", + "reconstruction_algorithm_version": "1.0.0", + "reconstruction_proof_ref": "proofs.information-state.blue.ep002.cut43", + "reconstructed_state_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loss_disclosures": [], + "predecessor_information_state_refs": [] +} diff --git a/contracts/fixtures/profiles/behavioral-relation-profile-v1/valid/participant-opacity-runtime-reference.json b/contracts/fixtures/profiles/behavioral-relation-profile-v1/valid/participant-opacity-runtime-reference.json new file mode 100644 index 000000000..0139c819e --- /dev/null +++ b/contracts/fixtures/profiles/behavioral-relation-profile-v1/valid/participant-opacity-runtime-reference.json @@ -0,0 +1,122 @@ +{ + "schema_version": "behavioral-relation-profile/v1", + "profile_id": "participant-opacity-runtime-reference-v1", + "profile_revision": "sem-231/runtime-rev1", + "taxonomy_id": "raes-behavioral-relations", + "taxonomy_revision": "rev10", + "relation_id": "participant-predicate-opacity", + "left_carrier_ref": "possible-point-carrier:runtime-reference-v1", + "observation_projection_ref": "participant-opacity-observation:runtime-reference-v1", + "observation_projection_revision": "rev1", + "finite_analysis_scope": "declared-complete-finite-carrier", + "parameters": { + "kind": "participant-predicate-opacity/v1", + "observer": { + "kind": "individual", + "participant_ref": "participant.behavior.red-agent", + "audience_ref": "audience:red-operator" + }, + "secret": { + "predicate_ref": "secret-predicate:runtime-reference-protected-state", + "predicate_revision": "rev1", + "truth_polarity": "one-sided-true" + }, + "carrier": { + "kind": "finite-possible-points", + "reachability_ref": "reachability:runtime-reference-carrier-v1", + "reachability_revision": "rev1" + }, + "initial_information": { + "projection_ref": "participant-opacity-initial-information:runtime-reference-v1", + "projection_revision": "rev1" + }, + "observation": { + "projection_ref": "participant-opacity-observation:runtime-reference-v1", + "projection_revision": "rev1", + "observable_channels": [ + "action-availability", + "decision", + "delivery", + "latency", + "order", + "participant-state", + "payload", + "policy-release", + "retry" + ], + "supervisor_decisions": "online-learned" + }, + "horizon": { + "scope": "current", + "cut_ref": "state-cut:runtime-crossing-heads-v1", + "cut_revision": "rev1", + "steps": null + }, + "memory": { + "retention": "cross-episode", + "memory_ref": "participant-memory:runtime-retained-history-v1", + "memory_revision": "rev1", + "reset_rule_ref": null, + "reset_rule_revision": null + }, + "strategy": { + "kind": "active", + "strategy_refs": [ + "participant-strategy:runtime-mediated-probes-v1" + ] + }, + "release": { + "schedule_ref": "release-schedule:runtime-exact-cut-v1", + "schedule_revision": "rev1", + "exact_cut": true, + "concealment_erases_retained_knowledge": false + }, + "scheduler_refs": [ + "scheduler:runtime-logical-single-writer" + ], + "environment_refs": [ + "environment:runtime-reference-in-process" + ], + "nondeterminism": "possibilistic-support", + "order": { + "treatment": "total-order", + "order_refs": [ + "order:runtime-logical-crossing" + ] + }, + "time": { + "model": "untimed", + "progress": "progress-insensitive", + "absence_observable": true, + "opportunity_basis_ref": "participant-opacity-opportunity:crossing-v1", + "opportunity_basis_revision": "rev1" + }, + "probability": "outside-baseline", + "bounds": { + "max_points": 4096, + "max_runs": 1024, + "max_cuts": 1024, + "max_strategies": 64, + "max_scheduler_environment_pairs": 64, + "max_order_variants": 64 + } + }, + "source_refs": [ + { + "source_ref": "docs/decisions/issue-964-participant-opacity-runtime-enforcement-preflight.md", + "source_digest": "sha256:dee2c4c0160bc1330c87d095b5aa1e0375356be655d6d044860a3198bc189e70" + } + ], + "limitations": [ + "The only enforced observation normal form is uniform denial: protected actions do not execute and participant payload or state is not released; the governed omission opportunity is recorded as withheld.", + "Support is limited to the exact finite in-process reference-runtime carrier and complete declared observation inventory.", + "Latency denotes a governed logical timing bucket only; wall-clock, timed, progress-sensitive, probabilistic, coalition, partial-order, and backend surfaces are unsupported.", + "Administrative and native-backend surfaces are outside the named participant observer boundary and do not become participant-visible by authorization alone." + ], + "explicit_non_claims": [ + "The profile does not provide a useful allowed-action or delivered-view mode; adding one requires a separately evidenced secret-independent projection strategy.", + "No general, unbounded, timed, probabilistic, quantitative, coalition, partial-order, or whole-deployment opacity is established.", + "No model checking or proof of a live execution, supervisor synthesis, backend declaration, backend realization, or backend conformance is established.", + "A runtime crossing decision establishes only bounded reference-runtime containment of the exact admitted profile and inventory." + ] +} diff --git a/contracts/fixtures/profiles/participant-information-reconstruction-profile-v1/invalid/executable-dispatch.json b/contracts/fixtures/profiles/participant-information-reconstruction-profile-v1/invalid/executable-dispatch.json new file mode 100644 index 000000000..87dfbd6ff --- /dev/null +++ b/contracts/fixtures/profiles/participant-information-reconstruction-profile-v1/invalid/executable-dispatch.json @@ -0,0 +1,22 @@ +{ + "schema_version": "participant-information-reconstruction-profile/v1", + "profile_id": "occurrence-prefix-evidence-v1", + "title": "Executable dispatch must be rejected", + "description": "An invalid profile that attempts to select runtime code.", + "algorithm_id": "raes.occurrence-prefix-evidence", + "algorithm_version": "1.0.0", + "information_state_schema_version": "1.0.0", + "projection_version": "projection.blue.v1", + "determinism_basis": "exact_occurrence_prefix_and_proof_digest", + "accepted_input_contracts": [ + "participant-observation-envelope-v1" + ], + "accepted_order_semantics": [ + "sequence_prefix" + ], + "fixture_format": "participant-information-reconstruction-fixture/v1", + "proof_artifact_format": "participant-information-reconstruction-proof/v1", + "normative_artifact_ref": "specs/formal/participant-runtime/README.md", + "normative_artifact_digest": "sha256:3927ad6c2814be339e9b021fc4f6fa6656cc3dc66d2e66bef24de63fe9830c4a", + "python_import_path": "untrusted.module:run" +} diff --git a/contracts/fixtures/profiles/participant-information-reconstruction-profile-v1/valid/occurrence-prefix-evidence-v1.json b/contracts/fixtures/profiles/participant-information-reconstruction-profile-v1/valid/occurrence-prefix-evidence-v1.json new file mode 100644 index 000000000..410304100 --- /dev/null +++ b/contracts/fixtures/profiles/participant-information-reconstruction-profile-v1/valid/occurrence-prefix-evidence-v1.json @@ -0,0 +1,22 @@ +{ + "schema_version": "participant-information-reconstruction-profile/v1", + "profile_id": "occurrence-prefix-evidence-v1", + "title": "Occurrence-prefix evidence reconstruction", + "description": "Validates an exact participant-visible occurrence prefix and a separately governed proof digest.", + "algorithm_id": "raes.occurrence-prefix-evidence", + "algorithm_version": "1.0.0", + "information_state_schema_version": "1.0.0", + "projection_version": "projection.blue.v1", + "determinism_basis": "exact_occurrence_prefix_and_proof_digest", + "accepted_input_contracts": [ + "participant-observation-envelope-v1" + ], + "accepted_order_semantics": [ + "sequence_prefix", + "causal_frontier" + ], + "fixture_format": "participant-information-reconstruction-fixture/v1", + "proof_artifact_format": "participant-information-reconstruction-proof/v1", + "normative_artifact_ref": "specs/formal/participant-runtime/README.md", + "normative_artifact_digest": "sha256:3927ad6c2814be339e9b021fc4f6fa6656cc3dc66d2e66bef24de63fe9830c4a" +} diff --git a/contracts/fixtures/snapshots/runtime-snapshot-v1/invalid/realization-observation-missing-strength.json b/contracts/fixtures/snapshots/runtime-snapshot-v1/invalid/realization-observation-missing-strength.json new file mode 100644 index 000000000..a7bed33e3 --- /dev/null +++ b/contracts/fixtures/snapshots/runtime-snapshot-v1/invalid/realization-observation-missing-strength.json @@ -0,0 +1,14 @@ +{ + "schema_version": "runtime-snapshot/v1", + "entries": {}, + "realization_observations": [ + { + "address": "provision.node.web", + "field_path": "nodes.web.runtime.forwarding_agents", + "domain": "runtime-realization", + "requirement_kind": "forwarding-agents", + "verification_scope": "configuration" + } + ], + "metadata": {} +} diff --git a/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/realization-provenance.json b/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/realization-provenance.json index 96c5163d4..28fc13876 100644 --- a/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/realization-provenance.json +++ b/contracts/fixtures/snapshots/runtime-snapshot-v1/valid/realization-provenance.json @@ -26,5 +26,15 @@ "provenance": "author-declared" } ], + "realization_observations": [ + { + "address": "provision.node.web", + "field_path": "nodes.web.runtime.forwarding_agents", + "domain": "runtime-realization", + "requirement_kind": "forwarding-agents", + "verification_scope": "configuration", + "observation_strength": "daemon-observed" + } + ], "metadata": {} } diff --git a/contracts/profiles/behavioral-relation/history/participant-opacity-runtime-reference-v1-sem-231-runtime-rev1.json b/contracts/profiles/behavioral-relation/history/participant-opacity-runtime-reference-v1-sem-231-runtime-rev1.json new file mode 100644 index 000000000..0139c819e --- /dev/null +++ b/contracts/profiles/behavioral-relation/history/participant-opacity-runtime-reference-v1-sem-231-runtime-rev1.json @@ -0,0 +1,122 @@ +{ + "schema_version": "behavioral-relation-profile/v1", + "profile_id": "participant-opacity-runtime-reference-v1", + "profile_revision": "sem-231/runtime-rev1", + "taxonomy_id": "raes-behavioral-relations", + "taxonomy_revision": "rev10", + "relation_id": "participant-predicate-opacity", + "left_carrier_ref": "possible-point-carrier:runtime-reference-v1", + "observation_projection_ref": "participant-opacity-observation:runtime-reference-v1", + "observation_projection_revision": "rev1", + "finite_analysis_scope": "declared-complete-finite-carrier", + "parameters": { + "kind": "participant-predicate-opacity/v1", + "observer": { + "kind": "individual", + "participant_ref": "participant.behavior.red-agent", + "audience_ref": "audience:red-operator" + }, + "secret": { + "predicate_ref": "secret-predicate:runtime-reference-protected-state", + "predicate_revision": "rev1", + "truth_polarity": "one-sided-true" + }, + "carrier": { + "kind": "finite-possible-points", + "reachability_ref": "reachability:runtime-reference-carrier-v1", + "reachability_revision": "rev1" + }, + "initial_information": { + "projection_ref": "participant-opacity-initial-information:runtime-reference-v1", + "projection_revision": "rev1" + }, + "observation": { + "projection_ref": "participant-opacity-observation:runtime-reference-v1", + "projection_revision": "rev1", + "observable_channels": [ + "action-availability", + "decision", + "delivery", + "latency", + "order", + "participant-state", + "payload", + "policy-release", + "retry" + ], + "supervisor_decisions": "online-learned" + }, + "horizon": { + "scope": "current", + "cut_ref": "state-cut:runtime-crossing-heads-v1", + "cut_revision": "rev1", + "steps": null + }, + "memory": { + "retention": "cross-episode", + "memory_ref": "participant-memory:runtime-retained-history-v1", + "memory_revision": "rev1", + "reset_rule_ref": null, + "reset_rule_revision": null + }, + "strategy": { + "kind": "active", + "strategy_refs": [ + "participant-strategy:runtime-mediated-probes-v1" + ] + }, + "release": { + "schedule_ref": "release-schedule:runtime-exact-cut-v1", + "schedule_revision": "rev1", + "exact_cut": true, + "concealment_erases_retained_knowledge": false + }, + "scheduler_refs": [ + "scheduler:runtime-logical-single-writer" + ], + "environment_refs": [ + "environment:runtime-reference-in-process" + ], + "nondeterminism": "possibilistic-support", + "order": { + "treatment": "total-order", + "order_refs": [ + "order:runtime-logical-crossing" + ] + }, + "time": { + "model": "untimed", + "progress": "progress-insensitive", + "absence_observable": true, + "opportunity_basis_ref": "participant-opacity-opportunity:crossing-v1", + "opportunity_basis_revision": "rev1" + }, + "probability": "outside-baseline", + "bounds": { + "max_points": 4096, + "max_runs": 1024, + "max_cuts": 1024, + "max_strategies": 64, + "max_scheduler_environment_pairs": 64, + "max_order_variants": 64 + } + }, + "source_refs": [ + { + "source_ref": "docs/decisions/issue-964-participant-opacity-runtime-enforcement-preflight.md", + "source_digest": "sha256:dee2c4c0160bc1330c87d095b5aa1e0375356be655d6d044860a3198bc189e70" + } + ], + "limitations": [ + "The only enforced observation normal form is uniform denial: protected actions do not execute and participant payload or state is not released; the governed omission opportunity is recorded as withheld.", + "Support is limited to the exact finite in-process reference-runtime carrier and complete declared observation inventory.", + "Latency denotes a governed logical timing bucket only; wall-clock, timed, progress-sensitive, probabilistic, coalition, partial-order, and backend surfaces are unsupported.", + "Administrative and native-backend surfaces are outside the named participant observer boundary and do not become participant-visible by authorization alone." + ], + "explicit_non_claims": [ + "The profile does not provide a useful allowed-action or delivered-view mode; adding one requires a separately evidenced secret-independent projection strategy.", + "No general, unbounded, timed, probabilistic, quantitative, coalition, partial-order, or whole-deployment opacity is established.", + "No model checking or proof of a live execution, supervisor synthesis, backend declaration, backend realization, or backend conformance is established.", + "A runtime crossing decision establishes only bounded reference-runtime containment of the exact admitted profile and inventory." + ] +} diff --git a/contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json b/contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json new file mode 100644 index 000000000..fa71a475c --- /dev/null +++ b/contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json @@ -0,0 +1,128 @@ +{ + "schema_version": "behavioral-relation-profile/v1", + "profile_id": "participant-opacity-runtime-reference-v1", + "profile_revision": "sem-231/runtime-rev2", + "taxonomy_id": "raes-behavioral-relations", + "taxonomy_revision": "rev11", + "relation_id": "participant-predicate-opacity", + "left_carrier_ref": "possible-point-carrier:runtime-reference-v1", + "observation_projection_ref": "participant-opacity-observation:runtime-reference-v1", + "observation_projection_revision": "rev1", + "finite_analysis_scope": "declared-complete-finite-carrier", + "parameters": { + "kind": "participant-predicate-opacity/v1", + "observer": { + "kind": "individual", + "participant_ref": "participant.behavior.red-agent", + "audience_ref": "audience:red-operator" + }, + "secret": { + "predicate_ref": "secret-predicate:runtime-reference-protected-state", + "predicate_revision": "rev1", + "truth_polarity": "one-sided-true" + }, + "carrier": { + "kind": "finite-possible-points", + "reachability_ref": "reachability:runtime-reference-carrier-v1", + "reachability_revision": "rev1" + }, + "initial_information": { + "projection_ref": "participant-opacity-initial-information:runtime-reference-v1", + "projection_revision": "rev1" + }, + "observation": { + "projection_ref": "participant-opacity-observation:runtime-reference-v1", + "projection_revision": "rev1", + "observable_channels": [ + "action-availability", + "decision", + "delivery", + "latency", + "order", + "participant-state", + "payload", + "policy-release", + "retry" + ], + "supervisor_decisions": "online-learned" + }, + "horizon": { + "scope": "current", + "cut_ref": "state-cut:runtime-crossing-heads-v1", + "cut_revision": "rev1", + "steps": null + }, + "memory": { + "retention": "cross-episode", + "memory_ref": "participant-memory:runtime-retained-history-v1", + "memory_revision": "rev1", + "reset_rule_ref": null, + "reset_rule_revision": null + }, + "strategy": { + "kind": "active", + "strategy_refs": [ + "participant-strategy:runtime-mediated-probes-v1" + ] + }, + "release": { + "schedule_ref": "release-schedule:runtime-exact-cut-v1", + "schedule_revision": "rev1", + "exact_cut": true, + "concealment_erases_retained_knowledge": false + }, + "scheduler_refs": [ + "scheduler:runtime-logical-single-writer" + ], + "environment_refs": [ + "environment:runtime-reference-in-process" + ], + "nondeterminism": "possibilistic-support", + "order": { + "treatment": "total-order", + "order_refs": [ + "order:runtime-logical-crossing" + ] + }, + "time": { + "model": "untimed", + "progress": "progress-insensitive", + "absence_observable": true, + "opportunity_basis_ref": "participant-opacity-opportunity:crossing-v1", + "opportunity_basis_revision": "rev1" + }, + "probability": "outside-baseline", + "bounds": { + "max_points": 4096, + "max_runs": 1024, + "max_cuts": 1024, + "max_strategies": 64, + "max_scheduler_environment_pairs": 64, + "max_order_variants": 64 + } + }, + "source_refs": [ + { + "source_ref": "docs/decisions/issue-964-participant-opacity-runtime-enforcement-preflight.md", + "source_digest": "sha256:dee2c4c0160bc1330c87d095b5aa1e0375356be655d6d044860a3198bc189e70" + }, + { + "source_ref": "docs/decisions/issue-965-participant-opacity-backend-realization-preflight.md", + "source_digest": "sha256:29262666ba8293d304563e29311b8589a94d1be5e93338089bf6aa6249b3aab7" + } + ], + "limitations": [ + "The only enforced observation normal form is uniform denial: protected actions do not execute and participant payload or state is not released; the governed omission opportunity is recorded as withheld.", + "Support is limited to the exact finite in-process reference-runtime carrier and complete declared observation inventory.", + "Latency denotes a governed logical timing bucket only; wall-clock, timed, progress-sensitive, probabilistic, coalition, and partial-order surfaces are unsupported.", + "Backend conformance is bounded to an exact manifest, profile, configuration, tool, environment, and probe-set digest plus the complete named observation transcript.", + "Administrative and native-backend surfaces are outside the named participant observer boundary and do not become participant-visible by authorization alone." + ], + "explicit_non_claims": [ + "The profile does not provide a useful allowed-action or delivered-view mode; adding one requires a separately evidenced secret-independent projection strategy.", + "No general, unbounded, timed, probabilistic, quantitative, coalition, partial-order, or whole-deployment opacity is established.", + "No model checking or proof of a live execution, supervisor synthesis, universal backend opacity, or cross-backend equivalence is established.", + "A declaration, backend realization, and observed backend conformance are three independent assurance claims; none implies either of the others.", + "A runtime crossing decision establishes only bounded reference-runtime containment of the exact admitted profile and inventory." + ] +} diff --git a/contracts/profiles/participant-information-reconstruction/occurrence-prefix-evidence-v1.json b/contracts/profiles/participant-information-reconstruction/occurrence-prefix-evidence-v1.json new file mode 100644 index 000000000..205699aef --- /dev/null +++ b/contracts/profiles/participant-information-reconstruction/occurrence-prefix-evidence-v1.json @@ -0,0 +1,22 @@ +{ + "schema_version": "participant-information-reconstruction-profile/v1", + "profile_id": "occurrence-prefix-evidence-v1", + "title": "Occurrence-prefix evidence reconstruction", + "description": "Validates an exact participant-visible occurrence prefix and a separately governed proof digest without loading caller-selected code.", + "algorithm_id": "raes.occurrence-prefix-evidence", + "algorithm_version": "1.0.0", + "information_state_schema_version": "1.0.0", + "projection_version": "projection.blue.v1", + "determinism_basis": "exact_occurrence_prefix_and_proof_digest", + "accepted_input_contracts": [ + "participant-observation-envelope-v1" + ], + "accepted_order_semantics": [ + "sequence_prefix", + "causal_frontier" + ], + "fixture_format": "participant-information-reconstruction-fixture/v1", + "proof_artifact_format": "participant-information-reconstruction-proof/v1", + "normative_artifact_ref": "specs/formal/participant-runtime/README.md", + "normative_artifact_digest": "sha256:3927ad6c2814be339e9b021fc4f6fa6656cc3dc66d2e66bef24de63fe9830c4a" +} diff --git a/contracts/provenance/sdl-lineage-ledger-v1.json b/contracts/provenance/sdl-lineage-ledger-v1.json index f770c7c5a..a46af9db1 100644 --- a/contracts/provenance/sdl-lineage-ledger-v1.json +++ b/contracts/provenance/sdl-lineage-ledger-v1.json @@ -1,6 +1,6 @@ { "schema_version": "sdl-lineage-ledger/v1", - "reviewed_on": "2026-07-27", + "reviewed_on": "2026-08-01", "citations": [ { "citation_id": "ocr-sdl-v0.21.2", @@ -440,6 +440,156 @@ "canonical_url": "https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-metrics.md", "verified_on": "2026-07-27", "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-899-scoped-resource-governance-review" + }, + { + "citation_id": "denning-1976", + "title": "A Lattice Model of Secure Information Flow", + "authors_or_maintainer": [ + "Dorothy E. Denning" + ], + "year": 1976, + "container_title": "Communications of the ACM 19(5)", + "doi": "10.1145/360051.360056", + "canonical_url": "https://doi.org/10.1145/360051.360056", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "myers-liskov-1998", + "title": "Complete, Safe Information Flow with Decentralized Labels", + "authors_or_maintainer": [ + "Andrew C. Myers", + "Barbara Liskov" + ], + "year": 1998, + "container_title": "1998 IEEE Symposium on Security and Privacy", + "doi": "10.1109/SECPRI.1998.674834", + "canonical_url": "https://doi.org/10.1109/SECPRI.1998.674834", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "myers-sabelfeld-zdancewic-2006", + "title": "Enforcing Robust Declassification and Qualified Robustness", + "authors_or_maintainer": [ + "Andrew C. Myers", + "Andrei Sabelfeld", + "Steve Zdancewic" + ], + "year": 2006, + "container_title": "Journal of Computer Security 14(2)", + "doi": "10.3233/JCS-2006-14203", + "canonical_url": "https://doi.org/10.3233/JCS-2006-14203", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "cecchetti-myers-arden-2017", + "title": "Nonmalleable Information Flow Control", + "authors_or_maintainer": [ + "Ethan Cecchetti", + "Andrew C. Myers", + "Owen Arden" + ], + "year": 2017, + "container_title": "ACM SIGSAC Conference on Computer and Communications Security", + "doi": "10.1145/3133956.3134054", + "canonical_url": "https://doi.org/10.1145/3133956.3134054", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "histar-2006", + "title": "Making Information Flow Explicit in HiStar", + "authors_or_maintainer": [ + "Nickolai Zeldovich", + "Silas Boyd-Wickizer", + "Eddie Kohler", + "David Mazières" + ], + "year": 2006, + "container_title": "7th USENIX Symposium on Operating Systems Design and Implementation", + "canonical_url": "https://www.usenix.org/conference/osdi-06/making-information-flow-explicit-histar", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "flume-2007", + "title": "Information Flow Control for Standard OS Abstractions", + "authors_or_maintainer": [ + "Maxwell Krohn", + "Alexander Yip", + "Micah Brodsky", + "Natan Cliffer", + "M. Frans Kaashoek", + "Eddie Kohler", + "Robert Morris" + ], + "year": 2007, + "container_title": "21st ACM Symposium on Operating Systems Principles", + "canonical_url": "https://pdos.csail.mit.edu/papers/flume-sosp07.pdf", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "nist-sp-800-53r5-ac4", + "title": "Security and Privacy Controls for Information Systems and Organizations: AC-4 Information Flow Enforcement", + "authors_or_maintainer": [ + "National Institute of Standards and Technology" + ], + "year": 2020, + "container_title": "NIST Special Publication 800-53 Revision 5", + "canonical_url": "https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "nsa-cross-domain-2026", + "title": "National Cross Domain Strategy and Management Office", + "authors_or_maintainer": [ + "National Security Agency" + ], + "year": 2026, + "canonical_url": "https://www.nsa.gov/Cybersecurity/Partnership/National-Cross-Domain-Strategy-Management-Office/", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "taintdroid-2010", + "title": "TaintDroid: An Information-Flow Tracking System for Realtime Privacy Monitoring on Smartphones", + "authors_or_maintainer": [ + "William Enck", + "Peter Gilbert", + "Byung-Gon Chun", + "Landon P. Cox", + "Jaeyeon Jung", + "Patrick McDaniel", + "Anmol N. Sheth" + ], + "year": 2010, + "container_title": "9th USENIX Symposium on Operating Systems Design and Implementation", + "canonical_url": "https://www.usenix.org/conference/osdi10/taintdroid-information-flow-tracking-system-realtime-privacy-monitoring", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" + }, + { + "citation_id": "pasquier-et-al-2017", + "title": "Practical Whole-System Provenance Capture", + "authors_or_maintainer": [ + "Thomas Pasquier", + "Xueyuan Han", + "Mark Goldstein", + "Thomas Moyer", + "David Eyers", + "Margo Seltzer", + "Jean Bacon" + ], + "year": 2017, + "container_title": "ACM Symposium on Cloud Computing", + "doi": "10.1145/3127479.3129249", + "canonical_url": "https://doi.org/10.1145/3127479.3129249", + "verified_on": "2026-08-01", + "verification_evidence": "docs/research/lineage/source-audit-2026-07-12.md#issue-1001-boundary-flow-control-sources" } ], "sources": [ @@ -737,6 +887,88 @@ "canonical_url": "https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-metrics.md", "maintaining_body": "OpenTelemetry", "citation_ref": "opentelemetry-genai-metrics-2026" + }, + { + "source_id": "denning-1976", + "kind": "publication", + "title": "A Lattice Model of Secure Information Flow", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.1145/360051.360056", + "citation_ref": "denning-1976" + }, + { + "source_id": "myers-liskov-1998", + "kind": "publication", + "title": "Complete, Safe Information Flow with Decentralized Labels", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.1109/SECPRI.1998.674834", + "citation_ref": "myers-liskov-1998" + }, + { + "source_id": "myers-sabelfeld-zdancewic-2006", + "kind": "publication", + "title": "Enforcing Robust Declassification and Qualified Robustness", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.3233/JCS-2006-14203", + "citation_ref": "myers-sabelfeld-zdancewic-2006" + }, + { + "source_id": "cecchetti-myers-arden-2017", + "kind": "publication", + "title": "Nonmalleable Information Flow Control", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.1145/3133956.3134054", + "citation_ref": "cecchetti-myers-arden-2017" + }, + { + "source_id": "histar-2006", + "kind": "publication", + "title": "Making Information Flow Explicit in HiStar", + "version_or_edition": "OSDI 2006 publication", + "canonical_url": "https://www.usenix.org/conference/osdi-06/making-information-flow-explicit-histar", + "citation_ref": "histar-2006" + }, + { + "source_id": "flume-2007", + "kind": "publication", + "title": "Information Flow Control for Standard OS Abstractions", + "version_or_edition": "SOSP 2007 publication", + "canonical_url": "https://pdos.csail.mit.edu/papers/flume-sosp07.pdf", + "citation_ref": "flume-2007" + }, + { + "source_id": "nist-sp-800-53r5-ac4", + "kind": "standard", + "title": "NIST SP 800-53 Revision 5 AC-4 Information Flow Enforcement", + "version_or_edition": "Revision 5 Update 1", + "canonical_url": "https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final", + "maintaining_body": "National Institute of Standards and Technology", + "citation_ref": "nist-sp-800-53r5-ac4" + }, + { + "source_id": "nsa-cross-domain-2026", + "kind": "standard", + "title": "National Cross Domain Strategy and Management Office", + "version_or_edition": "program guidance reviewed 2026-08-01", + "canonical_url": "https://www.nsa.gov/Cybersecurity/Partnership/National-Cross-Domain-Strategy-Management-Office/", + "maintaining_body": "National Security Agency", + "citation_ref": "nsa-cross-domain-2026" + }, + { + "source_id": "taintdroid-2010", + "kind": "publication", + "title": "TaintDroid: An Information-Flow Tracking System for Realtime Privacy Monitoring on Smartphones", + "version_or_edition": "OSDI 2010 publication", + "canonical_url": "https://www.usenix.org/conference/osdi10/taintdroid-information-flow-tracking-system-realtime-privacy-monitoring", + "citation_ref": "taintdroid-2010" + }, + { + "source_id": "pasquier-et-al-2017", + "kind": "publication", + "title": "Practical Whole-System Provenance Capture", + "version_or_edition": "version of record", + "canonical_url": "https://doi.org/10.1145/3127479.3129249", + "citation_ref": "pasquier-et-al-2017" } ], "subjects": [ @@ -3309,6 +3541,95 @@ ], "compatibility_direction": "aces_relative_to_source" }, + { + "plane": "semantics", + "classification": "adapted", + "source_refs": [ + "denning-1976", + "myers-liskov-1998", + "myers-sabelfeld-zdancewic-2006", + "cecchetti-myers-arden-2017", + "histar-2006", + "flume-2007", + "nist-sp-800-53r5-ac4", + "nsa-cross-domain-2026", + "taintdroid-2010", + "pasquier-et-al-2017" + ], + "aces_boundaries": [ + { + "artifact": "contracts/concept-authority/behavioral-relations-v1.json", + "symbol_or_pointer": "#/relations/policy-noninterference" + }, + { + "artifact": "specs/formal/participant-semantics/adversarial-flow-control.md", + "symbol_or_pointer": "Exact Revision-1 Flow Algebra" + } + ], + "source_boundaries": [ + { + "artifact": "A Lattice Model of Secure Information Flow", + "symbol_or_pointer": "lattice order and conservative join" + }, + { + "artifact": "Complete, Safe Information Flow with Decentralized Labels", + "symbol_or_pointer": "principal-relative policies and authority-bounded declassification" + }, + { + "artifact": "Enforcing Robust Declassification and Qualified Robustness", + "symbol_or_pointer": "robust declassification and endorsement duality" + }, + { + "artifact": "Nonmalleable Information Flow Control", + "symbol_or_pointer": "confidentiality/integrity downgrade interaction and attacker influence" + }, + { + "artifact": "Making Information Flow Explicit in HiStar", + "symbol_or_pointer": "decentralized labels, privilege, and trusted declassifier boundary" + }, + { + "artifact": "Information Flow Control for Standard OS Abstractions", + "symbol_or_pointer": "practical decentralized IFC and conservative propagation" + }, + { + "artifact": "NIST SP 800-53 Revision 5 AC-4", + "symbol_or_pointer": "controlled-interface information-flow enforcement" + }, + { + "artifact": "National Cross Domain Strategy and Management Office", + "symbol_or_pointer": "cross-domain controlled-interface lifecycle and assurance" + }, + { + "artifact": "TaintDroid", + "symbol_or_pointer": "dynamic explicit-flow tracking and coverage limitations" + }, + { + "artifact": "Practical Whole-System Provenance Capture", + "symbol_or_pointer": "whole-system source/derivation capture and operational limitations" + } + ], + "divergence": "RAES composes the lattice, decentralized-label, robust-downgrade, deployed-IFC, cross-domain-boundary, dynamic-tracking, and provenance precedents into the ACES-native SEM-233 sem-233/rev1 two-coordinate obligation powerset over existing participant carriers. It separates immutable provenance/influence evidence from label obligations and authority; preserves labels across handoff/replay; and adds an exact-cut final-sink predicate. RAES does not adopt a source label vocabulary, operating-system API, guard product profile, dynamic-taint coverage claim, provenance-as-authorization rule, wire format, certification, runtime implementation, or covert-channel guarantee.", + "compatibility": "none", + "citation_refs": [ + "denning-1976", + "myers-liskov-1998", + "myers-sabelfeld-zdancewic-2006", + "cecchetti-myers-arden-2017", + "histar-2006", + "flume-2007", + "nist-sp-800-53r5-ac4", + "nsa-cross-domain-2026", + "taintdroid-2010", + "pasquier-et-al-2017" + ], + "internal_authority_refs": [ + "docs/decisions/adrs/adr-101-adversarial-participant-flow-control.md", + "specs/formal/participant-semantics/adversarial-flow-control.md", + "implementations/python/tests/test_sem_233_adversarial_boundary_flow.py", + "docs/research/adversarial-participant-control/current-state-assessment.md" + ], + "compatibility_direction": "aces_relative_to_source" + }, { "plane": "semantics", "classification": "adapted", diff --git a/contracts/schema-publication/entries/artifact-transformation-report-v1.json b/contracts/schema-publication/entries/artifact-transformation-report-v1.json new file mode 100644 index 000000000..b4129e193 --- /dev/null +++ b/contracts/schema-publication/entries/artifact-transformation-report-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "artifact-transformation-report-v1", + "schema_path": "contracts/schemas/artifact-transformations/artifact-transformation-report-v1.json", + "stability": "draft", + "content_hash": "d645595e96c86e16de9c772b26504e751ba49deed2dda591d094246cbeefc417", + "last_change": { + "summary": "Initial publication for safe deterministic artifact transformations under the OpenRAE schema namespace in issue #265.", + "content_hash": "d645595e96c86e16de9c772b26504e751ba49deed2dda591d094246cbeefc417" + } +} diff --git a/contracts/schema-publication/entries/backend-manifest-v2.json b/contracts/schema-publication/entries/backend-manifest-v2.json index 0e32a64f7..9af24779b 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": "4e9f3694ba9be9dff9236ad94b895591f28863f295724d013c243b6d290ae37f", + "content_hash": "fe27901e088649ac3b9f7a53e4cc43474bd1ac0fbef28e7cb2545e33fd703c57", "last_change": { - "summary": "Rebound the published schema namespace to the renamed OpenRAE GitHub organization for issue #963.", - "content_hash": "4e9f3694ba9be9dff9236ad94b895591f28863f295724d013c243b6d290ae37f" + "summary": "Added concern-keyed realization observation capabilities for issue #1043.", + "content_hash": "fe27901e088649ac3b9f7a53e4cc43474bd1ac0fbef28e7cb2545e33fd703c57" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json b/contracts/schema-publication/entries/instantiated-scenario-snapshot-v1.json index bb7903209..0d2a735ba 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": "ebbb182be917c3d2b473f25df2374ab633bd2a73dc13af786e3c06f38d5f0ab6", + "content_hash": "470d7e2a8818f8000d2df8e0d1dddc9ef5120039f91dd7b6f80e198a531674f0", "last_change": { - "summary": "Added the service search-index field-schema profile and rebound the namespace to OpenRAE for issues #1011 and #963.", - "content_hash": "ebbb182be917c3d2b473f25df2374ab633bd2a73dc13af786e3c06f38d5f0ab6" + "summary": "Added forwarding-agent ownership roles for issue #1043.", + "content_hash": "470d7e2a8818f8000d2df8e0d1dddc9ef5120039f91dd7b6f80e198a531674f0" } } diff --git a/contracts/schema-publication/entries/instantiated-scenario-v1.json b/contracts/schema-publication/entries/instantiated-scenario-v1.json index ef91d649f..fafe25ae0 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": "7560409fe1e4fcaa3a9f957a997ca1b019c6b13b78b5210778763bc41b4a5a88", + "content_hash": "d4a810ad9cd6fabd884e7422e425b670cb5efa0e2652d34b9db1ebb8a3a8f74d", "last_change": { - "summary": "Added the service search-index field-schema profile and rebound the namespace to OpenRAE for issues #1011 and #963.", - "content_hash": "7560409fe1e4fcaa3a9f957a997ca1b019c6b13b78b5210778763bc41b4a5a88" + "summary": "Added forwarding-agent ownership roles for issue #1043.", + "content_hash": "d4a810ad9cd6fabd884e7422e425b670cb5efa0e2652d34b9db1ebb8a3a8f74d" } } diff --git a/contracts/schema-publication/entries/participant-context-view-v1.json b/contracts/schema-publication/entries/participant-context-view-v1.json index 32e4c8c96..e0c349046 100644 --- a/contracts/schema-publication/entries/participant-context-view-v1.json +++ b/contracts/schema-publication/entries/participant-context-view-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-context-view-v1", "schema_path": "contracts/schemas/control-plane/participant-context-view-v1.json", "stability": "draft", - "content_hash": "55dcb760c374a325496d41d3049de13db1974287dd14d3458a6e9d3c8733f962", + "content_hash": "8f8ab8309c7d00ea83982b284832e9618abc239586b004c601519485f451def5", "last_change": { - "summary": "Rebound the published schema namespace to the renamed OpenRAE GitHub organization for issue #963.", - "content_hash": "55dcb760c374a325496d41d3049de13db1974287dd14d3458a6e9d3c8733f962" + "summary": "Added the closed participant-information-state source layer for ACT-604 derived context joins.", + "content_hash": "8f8ab8309c7d00ea83982b284832e9618abc239586b004c601519485f451def5" } } diff --git a/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json b/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json index 8cd8a8c31..1d5d4f03b 100644 --- a/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json +++ b/contracts/schema-publication/entries/participant-crossing-occurrence-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-crossing-occurrence-v1", "schema_path": "contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json", "stability": "draft", - "content_hash": "c6650ca87d98bfcf6a99cdbd6340cfbc869b0ac74708812a94b79c1c5acc0c3a", + "content_hash": "e2757c45b507fbdf677709bf8148ab634d5de7f2695561724490eb86dfd405b5", "last_change": { - "summary": "Rebound the published schema namespace to the renamed OpenRAE GitHub organization for issue #963.", - "content_hash": "c6650ca87d98bfcf6a99cdbd6340cfbc869b0ac74708812a94b79c1c5acc0c3a" + "summary": "Added the optional closed participant-opacity runtime-enforcement binding to exact-cut crossing decisions for issue #964.", + "content_hash": "e2757c45b507fbdf677709bf8148ab634d5de7f2695561724490eb86dfd405b5" } } diff --git a/contracts/schema-publication/entries/participant-information-reconstruction-profile-v1.json b/contracts/schema-publication/entries/participant-information-reconstruction-profile-v1.json new file mode 100644 index 000000000..786cc6797 --- /dev/null +++ b/contracts/schema-publication/entries/participant-information-reconstruction-profile-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-information-reconstruction-profile-v1", + "schema_path": "contracts/schemas/profiles/participant-information-reconstruction-profile-v1.json", + "stability": "draft", + "content_hash": "003a393e84a849a0c1893cec4cc48e321fcad50f0d89f6a1c45eebc7e9240598", + "last_change": { + "summary": "Published the ACT-604 closed immutable participant information reconstruction profile contract.", + "content_hash": "003a393e84a849a0c1893cec4cc48e321fcad50f0d89f6a1c45eebc7e9240598" + } +} diff --git a/contracts/schema-publication/entries/participant-information-state-record-v1.json b/contracts/schema-publication/entries/participant-information-state-record-v1.json new file mode 100644 index 000000000..5317e4a9a --- /dev/null +++ b/contracts/schema-publication/entries/participant-information-state-record-v1.json @@ -0,0 +1,10 @@ +{ + "contract_id": "participant-information-state-record-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-information-state-record-v1.json", + "stability": "draft", + "content_hash": "b92865f10d1136d66ee045591fcb28d2b44943369e466d7d428ec755db5bdd40", + "last_change": { + "summary": "Published the ACT-604 immutable participant information-state record at one exact cut.", + "content_hash": "b92865f10d1136d66ee045591fcb28d2b44943369e466d7d428ec755db5bdd40" + } +} diff --git a/contracts/schema-publication/entries/participant-observation-envelope-v1.json b/contracts/schema-publication/entries/participant-observation-envelope-v1.json index 093088d41..85ca70fe7 100644 --- a/contracts/schema-publication/entries/participant-observation-envelope-v1.json +++ b/contracts/schema-publication/entries/participant-observation-envelope-v1.json @@ -2,9 +2,9 @@ "contract_id": "participant-observation-envelope-v1", "schema_path": "contracts/schemas/participant-runtime/participant-observation-envelope-v1.json", "stability": "draft", - "content_hash": "2abcc6fb611f25dd99b453b0086387ec286b75748a5e56cc97bf67b69a039542", + "content_hash": "d0b27daf3dec522564f8da5620fb5b37f852aae245fb789ee1bfb7b908fb63c3", "last_change": { - "summary": "Rebound the published schema namespace to the renamed OpenRAE GitHub organization for issue #963.", - "content_hash": "2abcc6fb611f25dd99b453b0086387ec286b75748a5e56cc97bf67b69a039542" + "summary": "Required reconstruction authority for strong ACT-604 information guarantees and explicit loss disclosure for lossy projections.", + "content_hash": "d0b27daf3dec522564f8da5620fb5b37f852aae245fb789ee1bfb7b908fb63c3" } } diff --git a/contracts/schema-publication/entries/runtime-snapshot-v1.json b/contracts/schema-publication/entries/runtime-snapshot-v1.json index 21a8643c1..b11a53dbc 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": "81ad3650fab82ddcecc17f9905319243605d3b64168f12cff75e4890c0180018", + "content_hash": "0dc5926aa421eac52576a6ad7235b2fc99a62466a023ac1e4524aacab5a84417", "last_change": { - "summary": "Rebound the published schema namespace to the renamed OpenRAE GitHub organization for issue #963.", - "content_hash": "81ad3650fab82ddcecc17f9905319243605d3b64168f12cff75e4890c0180018" + "summary": "Added value-free realization observation disclosures for issue #1043.", + "content_hash": "0dc5926aa421eac52576a6ad7235b2fc99a62466a023ac1e4524aacab5a84417" } } diff --git a/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json b/contracts/schema-publication/entries/scenario-satisfiability-evidence-v1.json index 422ea3fcf..68e69dee7 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": "6e0c1ee6fb1752832a07c220d6762dfbdfd71d39effe38bc2e44ea0c0d3b8e7c", + "content_hash": "9d6d499a20736a20d0a931d3c5a473eca4388fdb994ee995e9ca9d7464e2885f", "last_change": { - "summary": "Added the service search-index field-schema profile and rebound the namespace to OpenRAE for issues #1011 and #963.", - "content_hash": "6e0c1ee6fb1752832a07c220d6762dfbdfd71d39effe38bc2e44ea0c0d3b8e7c" + "summary": "Propagated forwarding-agent ownership roles for issue #1043.", + "content_hash": "9d6d499a20736a20d0a931d3c5a473eca4388fdb994ee995e9ca9d7464e2885f" } } diff --git a/contracts/schema-publication/entries/sdl-authoring-input-v1.json b/contracts/schema-publication/entries/sdl-authoring-input-v1.json index 245700fe1..71a55f404 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": "74b3e5310b2368e0463bae298b153fc5d7311b0a421df9559259374411540e28", + "content_hash": "b06b178b3611f9ce587968b1dc7d73e50b500cfe3cbb367b641e77cfcea0b6a0", "last_change": { - "summary": "Added the service search-index field-schema profile and rebound the namespace to OpenRAE for issues #1011 and #963.", - "content_hash": "74b3e5310b2368e0463bae298b153fc5d7311b0a421df9559259374411540e28" + "summary": "Added forwarding-agent ownership roles for issue #1043.", + "content_hash": "b06b178b3611f9ce587968b1dc7d73e50b500cfe3cbb367b641e77cfcea0b6a0" } } diff --git a/contracts/schemas/artifact-transformations/artifact-transformation-report-v1.json b/contracts/schemas/artifact-transformations/artifact-transformation-report-v1.json new file mode 100644 index 000000000..403137ff7 --- /dev/null +++ b/contracts/schemas/artifact-transformations/artifact-transformation-report-v1.json @@ -0,0 +1,432 @@ +{ + "$defs": { + "ArtifactTransformationCheckModel": { + "additionalProperties": false, + "properties": { + "check_id": { + "maxLength": 128, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "title": "Check Id", + "type": "string" + }, + "diagnostic_codes": { + "default": [], + "items": { + "type": "string" + }, + "title": "Diagnostic Codes", + "type": "array" + }, + "outcome": { + "$ref": "#/$defs/TransformationCheckOutcome" + } + }, + "required": [ + "check_id", + "outcome" + ], + "title": "ArtifactTransformationCheckModel", + "type": "object" + }, + "ArtifactTransformationIdentityMapModel": { + "additionalProperties": false, + "properties": { + "after": { + "minLength": 1, + "title": "After", + "type": "string" + }, + "before": { + "minLength": 1, + "title": "Before", + "type": "string" + }, + "declaration_kind": { + "minLength": 1, + "title": "Declaration Kind", + "type": "string" + } + }, + "required": [ + "declaration_kind", + "before", + "after" + ], + "title": "ArtifactTransformationIdentityMapModel", + "type": "object" + }, + "ArtifactTransformationKind": { + "enum": [ + "sdl-authoring", + "portable-contract" + ], + "title": "ArtifactTransformationKind", + "type": "string" + }, + "ArtifactTransformationLossKind": { + "enum": [ + "declaration-removed" + ], + "title": "ArtifactTransformationLossKind", + "type": "string" + }, + "ArtifactTransformationLossModel": { + "additionalProperties": false, + "properties": { + "affected_identity": { + "minLength": 1, + "title": "Affected Identity", + "type": "string" + }, + "diagnostic": { + "$ref": "#/$defs/DiagnosticModel" + }, + "kind": { + "$ref": "#/$defs/ArtifactTransformationLossKind" + } + }, + "required": [ + "kind", + "affected_identity", + "diagnostic" + ], + "title": "ArtifactTransformationLossModel", + "type": "object" + }, + "ArtifactTransformationPreservationModel": { + "additionalProperties": false, + "properties": { + "evidence_digests": { + "default": [], + "items": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "type": "string" + }, + "title": "Evidence Digests", + "type": "array" + }, + "limitations": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Limitations", + "type": "array" + }, + "outcome": { + "$ref": "#/$defs/PreservationOutcome" + }, + "profile": { + "minLength": 1, + "title": "Profile", + "type": "string" + } + }, + "required": [ + "profile", + "outcome" + ], + "title": "ArtifactTransformationPreservationModel", + "type": "object" + }, + "ArtifactTransformationStatus": { + "enum": [ + "success", + "refused" + ], + "title": "ArtifactTransformationStatus", + "type": "string" + }, + "DiagnosticModel": { + "additionalProperties": false, + "description": "Closed portable diagnostic shape for published contracts.", + "properties": { + "address": { + "maxLength": 4096, + "pattern": "^(?:/(?:[^~/]|~[01])*)*$", + "title": "Address", + "type": "string" + }, + "code": { + "maxLength": 128, + "pattern": "^[a-z0-9]+(?:[.-][a-z0-9]+)*$", + "title": "Code", + "type": "string" + }, + "domain": { + "maxLength": 64, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "title": "Domain", + "type": "string" + }, + "message": { + "maxLength": 512, + "minLength": 1, + "title": "Message", + "type": "string" + }, + "severity": { + "$ref": "#/$defs/Severity", + "default": "error" + } + }, + "required": [ + "code", + "domain", + "address", + "message" + ], + "title": "DiagnosticModel", + "type": "object" + }, + "PreservationOutcome": { + "enum": [ + "verified", + "failed", + "not-applicable" + ], + "title": "PreservationOutcome", + "type": "string" + }, + "Severity": { + "description": "Diagnostic severity level.", + "enum": [ + "error", + "warning", + "info" + ], + "title": "Severity", + "type": "string" + }, + "TransformationCheckOutcome": { + "enum": [ + "passed", + "failed", + "not-applicable" + ], + "title": "TransformationCheckOutcome", + "type": "string" + } + }, + "$id": "https://openrae.github.io/rae/schemas/artifact-transformation-report-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "status": { + "const": "success" + } + }, + "required": [ + "status" + ] + }, + "then": { + "properties": { + "target_digest": { + "type": "string" + } + }, + "required": [ + "target_digest" + ] + } + }, + { + "if": { + "properties": { + "status": { + "const": "refused" + } + }, + "required": [ + "status" + ] + }, + "then": { + "properties": { + "diagnostics": { + "minItems": 1 + }, + "target_digest": { + "type": "null" + } + } + } + } + ], + "description": "Deterministic all-or-none report for one semantic operation.", + "properties": { + "affected_identities": { + "default": [], + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Affected Identities", + "type": "array" + }, + "artifact_kind": { + "$ref": "#/$defs/ArtifactTransformationKind" + }, + "canonicalization_profile": { + "minLength": 1, + "title": "Canonicalization Profile", + "type": "string" + }, + "derivation_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Derivation Digest", + "type": "string" + }, + "diagnostics": { + "default": [], + "items": { + "$ref": "#/$defs/DiagnosticModel" + }, + "title": "Diagnostics", + "type": "array" + }, + "identity_map": { + "default": [], + "items": { + "$ref": "#/$defs/ArtifactTransformationIdentityMapModel" + }, + "title": "Identity Map", + "type": "array" + }, + "losses": { + "default": [], + "items": { + "$ref": "#/$defs/ArtifactTransformationLossModel" + }, + "title": "Losses", + "type": "array" + }, + "operation_profile": { + "maxLength": 128, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*/v[1-9][0-9]*$", + "title": "Operation Profile", + "type": "string" + }, + "policy_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Policy Digest", + "type": "string" + }, + "postconditions": { + "default": [], + "items": { + "$ref": "#/$defs/ArtifactTransformationCheckModel" + }, + "title": "Postconditions", + "type": "array" + }, + "preconditions": { + "items": { + "$ref": "#/$defs/ArtifactTransformationCheckModel" + }, + "minItems": 1, + "title": "Preconditions", + "type": "array" + }, + "preservation": { + "$ref": "#/$defs/ArtifactTransformationPreservationModel" + }, + "schema_version": { + "const": "artifact-transformation-report/v1", + "default": "artifact-transformation-report/v1", + "title": "Schema Version", + "type": "string" + }, + "source_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Source Digest", + "type": "string" + }, + "source_profile": { + "minLength": 1, + "title": "Source Profile", + "type": "string" + }, + "status": { + "$ref": "#/$defs/ArtifactTransformationStatus" + }, + "target_digest": { + "anyOf": [ + { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Target Digest" + }, + "target_profile": { + "minLength": 1, + "title": "Target Profile", + "type": "string" + } + }, + "required": [ + "operation_profile", + "status", + "artifact_kind", + "source_profile", + "target_profile", + "canonicalization_profile", + "source_digest", + "policy_digest", + "derivation_digest", + "preconditions", + "preservation" + ], + "title": "RAES Artifact Transformation Report v1", + "type": "object", + "x-raes-invariants": [ + { + "description": "Success carries a complete target digest; refusal carries no target and at least one bounded diagnostic.", + "id": "artifact-transformation-all-or-none", + "inputs": [ + { + "contract_id": "artifact-transformation-report-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ArtifactTransformationReportModel.model_validate" + }, + { + "description": "Checks, identities, mappings, losses, diagnostics, and evidence use stable closed ordering.", + "id": "artifact-transformation-deterministic-order", + "inputs": [ + { + "contract_id": "artifact-transformation-report-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.ArtifactTransformationReportModel.model_validate" + } + ], + "x-raes-semantic-profile": { + "contract_id": "artifact-transformation-report-v1", + "entry_schema_contract_id": "raes-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/RaesSemanticInvariantEntryModel", + "id": "raes-semantic-invariants-v1", + "keyword": "x-raes-invariants", + "required": true, + "uri": "https://openrae.github.io/rae/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/backend-manifest/backend-manifest-v2.json b/contracts/schemas/backend-manifest/backend-manifest-v2.json index 5c84d1702..a17302166 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -718,6 +718,17 @@ "title": "ObservationCapabilitiesModel", "type": "object" }, + "ObservationStrength": { + "description": "Strongest evidence a backend configuration emits for one concern.", + "enum": [ + "none", + "driver-reported", + "daemon-observed", + "guest-observed" + ], + "title": "ObservationStrength", + "type": "string" + }, "OrchestratorCapabilitiesModel": { "additionalProperties": false, "allOf": [ @@ -976,6 +987,7 @@ "participant_egress_projection", "participant_ingress_admission", "participant_intervention", + "participant_predicate_opacity", "participant_transformation" ] }, @@ -1013,6 +1025,7 @@ "participant_egress_projection", "participant_ingress_admission", "participant_intervention", + "participant_predicate_opacity", "participant_transformation" ] }, @@ -1046,6 +1059,7 @@ "participant_egress_projection", "participant_ingress_admission", "participant_intervention", + "participant_predicate_opacity", "participant_transformation" ] }, @@ -1957,6 +1971,27 @@ "title": "RealizationEnvelopeIdentityModel", "type": "object" }, + "RealizationObservationCapabilityModel": { + "additionalProperties": false, + "description": "Concern-specific scope and source of backend corroboration.", + "properties": { + "observation_strength": { + "$ref": "#/$defs/ObservationStrength", + "not": { + "const": "none" + } + }, + "verification_scope": { + "$ref": "#/$defs/RealizationVerificationScope" + } + }, + "required": [ + "verification_scope", + "observation_strength" + ], + "title": "RealizationObservationCapabilityModel", + "type": "object" + }, "RealizationSupportDeclarationModel": { "additionalProperties": false, "allOf": [ @@ -2039,6 +2074,16 @@ "title": "Domain", "type": "string" }, + "observation_capabilities": { + "additionalProperties": { + "$ref": "#/$defs/RealizationObservationCapabilityModel" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Observation Capabilities", + "type": "object" + }, "support_mode": { "$ref": "#/$defs/RealizationSupportMode" }, @@ -2077,6 +2122,15 @@ "title": "RealizationSupportMode", "type": "string" }, + "RealizationVerificationScope": { + "description": "Closed scope at which an inventory realization was corroborated.", + "enum": [ + "presence", + "configuration" + ], + "title": "RealizationVerificationScope", + "type": "string" + }, "TimeCapabilitiesModel": { "additionalProperties": false, "description": "Backend support for the API-421 portable shared-time contract family.", diff --git a/contracts/schemas/control-plane/participant-context-view-v1.json b/contracts/schemas/control-plane/participant-context-view-v1.json index 214569307..ff41ecd13 100644 --- a/contracts/schemas/control-plane/participant-context-view-v1.json +++ b/contracts/schemas/control-plane/participant-context-view-v1.json @@ -158,6 +158,7 @@ "enum": [ "source_snapshot", "participant_observation", + "participant_information_state", "participant_behavior_history", "participant_episode_state", "participant_status_view", diff --git a/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json b/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json index d6929db91..64f4f3637 100644 --- a/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json +++ b/contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json @@ -1,5 +1,217 @@ { "$defs": { + "BehavioralClaimBindingModel": { + "additionalProperties": false, + "description": "A bounded claim tied to one revisioned behavioral-relation definition.\n\nThis is deliberately a claim *binding*, not another relation registry. The\ncatalog owns relation meaning; consumers supply the subject, carriers,\nquantifier/evidence boundary, assurance state, and explicit limitations.", + "properties": { + "assurance_axis": { + "anyOf": [ + { + "enum": [ + "definition", + "checker", + "bounded-test", + "model-check", + "proof", + "runtime-enforcement", + "backend-declaration", + "backend-realization", + "backend-conformance" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Assurance Axis" + }, + "assurance_status": { + "enum": [ + "defined", + "implemented", + "tested", + "model-checked", + "proved", + "deliberately-unproved", + "future", + "enforced", + "declared", + "realized", + "conformant" + ], + "title": "Assurance Status", + "type": "string" + }, + "evidence_boundary": { + "minLength": 1, + "title": "Evidence Boundary", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "evidence_scope": { + "enum": [ + "structural", + "finite", + "statistical", + "model-check", + "proof" + ], + "title": "Evidence Scope", + "type": "string" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "left_carrier_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Left Carrier Ref" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "observation_projection_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Projection Ref" + }, + "observation_projection_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Projection Revision" + }, + "quantifier_scope": { + "enum": [ + "single-artifact", + "finite-cases", + "sampled-population", + "all-admitted-inputs", + "all-traces", + "all-strategies" + ], + "title": "Quantifier Scope", + "type": "string" + }, + "relation_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Relation Id", + "type": "string" + }, + "relation_parameter_profile_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Relation Parameter Profile Ref" + }, + "relation_parameter_profile_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Relation Parameter Profile Revision" + }, + "right_carrier_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Right Carrier Ref" + }, + "subject": { + "minLength": 1, + "title": "Subject", + "type": "string" + }, + "taxonomy_id": { + "minLength": 1, + "title": "Taxonomy Id", + "type": "string" + }, + "taxonomy_revision": { + "pattern": "^[a-z0-9][a-z0-9.-]*$", + "title": "Taxonomy Revision", + "type": "string" + } + }, + "required": [ + "taxonomy_id", + "taxonomy_revision", + "relation_id", + "subject", + "quantifier_scope", + "evidence_scope", + "evidence_boundary", + "assurance_status", + "limitations", + "explicit_non_claims" + ], + "title": "BehavioralClaimBindingModel", + "type": "object" + }, "EventClassificationModel": { "additionalProperties": false, "description": "RAES-native normalized event classification tuple (ADR-054).", @@ -310,6 +522,17 @@ "title": "Loss And Limitations", "type": "array" }, + "opacity_enforcement": { + "anyOf": [ + { + "$ref": "#/$defs/ParticipantOpacityRuntimeEnforcementBindingModel" + }, + { + "type": "null" + } + ], + "default": null + }, "order_model": { "enum": [ "total_order", @@ -1512,6 +1735,211 @@ "title": "ParticipantCrossingTransformationModel", "type": "object" }, + "ParticipantOpacityRuntimeEnforcementBindingModel": { + "additionalProperties": false, + "description": "Safe finite runtime-enforcement binding owned by an API-423 decision.", + "properties": { + "assurance_axis": { + "const": "runtime-enforcement", + "title": "Assurance Axis", + "type": "string" + }, + "carrier_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Carrier Digest", + "type": "string" + }, + "carrier_ref": { + "minLength": 1, + "title": "Carrier Ref", + "type": "string" + }, + "claim": { + "$ref": "#/$defs/BehavioralClaimBindingModel" + }, + "enforcement_rule_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Enforcement Rule Digest", + "type": "string" + }, + "enforcement_rule_ref": { + "minLength": 1, + "title": "Enforcement Rule Ref", + "type": "string" + }, + "enforcement_rule_revision": { + "minLength": 1, + "title": "Enforcement Rule Revision", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "materializer_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Materializer Digest", + "type": "string" + }, + "materializer_ref": { + "minLength": 1, + "title": "Materializer Ref", + "type": "string" + }, + "materializer_revision": { + "minLength": 1, + "title": "Materializer Revision", + "type": "string" + }, + "memory_ref": { + "minLength": 1, + "title": "Memory Ref", + "type": "string" + }, + "memory_revision": { + "minLength": 1, + "title": "Memory Revision", + "type": "string" + }, + "observation_inventory_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Observation Inventory Digest", + "type": "string" + }, + "observation_inventory_ref": { + "minLength": 1, + "title": "Observation Inventory Ref", + "type": "string" + }, + "observation_inventory_revision": { + "minLength": 1, + "title": "Observation Inventory Revision", + "type": "string" + }, + "predicate_ref": { + "minLength": 1, + "title": "Predicate Ref", + "type": "string" + }, + "predicate_revision": { + "minLength": 1, + "title": "Predicate Revision", + "type": "string" + }, + "profile_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Profile Digest", + "type": "string" + }, + "profile_id": { + "minLength": 1, + "title": "Profile Id", + "type": "string" + }, + "profile_revision": { + "minLength": 1, + "title": "Profile Revision", + "type": "string" + }, + "relation_id": { + "const": "participant-predicate-opacity", + "title": "Relation Id", + "type": "string" + }, + "release_ref": { + "minLength": 1, + "title": "Release Ref", + "type": "string" + }, + "release_revision": { + "minLength": 1, + "title": "Release Revision", + "type": "string" + }, + "state_cut_ref": { + "minLength": 1, + "title": "State Cut Ref", + "type": "string" + }, + "state_cut_revision": { + "minLength": 1, + "title": "State Cut Revision", + "type": "string" + }, + "taxonomy_id": { + "const": "raes-behavioral-relations", + "title": "Taxonomy Id", + "type": "string" + }, + "taxonomy_revision": { + "minLength": 1, + "title": "Taxonomy Revision", + "type": "string" + } + }, + "required": [ + "taxonomy_id", + "taxonomy_revision", + "relation_id", + "profile_id", + "profile_revision", + "profile_digest", + "predicate_ref", + "predicate_revision", + "carrier_ref", + "carrier_digest", + "materializer_ref", + "materializer_revision", + "materializer_digest", + "observation_inventory_ref", + "observation_inventory_revision", + "observation_inventory_digest", + "enforcement_rule_ref", + "enforcement_rule_revision", + "enforcement_rule_digest", + "state_cut_ref", + "state_cut_revision", + "memory_ref", + "memory_revision", + "release_ref", + "release_revision", + "assurance_axis", + "claim", + "evidence_refs", + "limitations", + "explicit_non_claims" + ], + "title": "ParticipantOpacityRuntimeEnforcementBindingModel", + "type": "object" + }, "RawDataIntegrityModel": { "additionalProperties": false, "description": "Hash, size, and truncation facts for raw data behind a runtime claim.", diff --git a/contracts/schemas/participant-runtime/participant-information-state-record-v1.json b/contracts/schemas/participant-runtime/participant-information-state-record-v1.json new file mode 100644 index 000000000..cfabfb293 --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-information-state-record-v1.json @@ -0,0 +1,1187 @@ +{ + "$defs": { + "EventClassificationModel": { + "additionalProperties": false, + "description": "RAES-native normalized event classification tuple (ADR-054).", + "properties": { + "activity_id": { + "title": "Activity Id", + "type": "integer" + }, + "activity_name": { + "minLength": 1, + "title": "Activity Name", + "type": "string" + }, + "category_name": { + "minLength": 1, + "title": "Category Name", + "type": "string" + }, + "category_uid": { + "title": "Category Uid", + "type": "integer" + }, + "class_name": { + "minLength": 1, + "title": "Class Name", + "type": "string" + }, + "class_uid": { + "title": "Class Uid", + "type": "integer" + }, + "severity": { + "minLength": 1, + "title": "Severity", + "type": "string" + }, + "severity_id": { + "title": "Severity Id", + "type": "integer" + }, + "type_name": { + "minLength": 1, + "title": "Type Name", + "type": "string" + }, + "type_uid": { + "title": "Type Uid", + "type": "integer" + } + }, + "required": [ + "category_uid", + "category_name", + "class_uid", + "class_name", + "activity_id", + "activity_name", + "type_uid", + "type_name", + "severity_id", + "severity" + ], + "title": "EventClassificationModel", + "type": "object" + }, + "ParticipantDecisionSurfaceCausalCutModel": { + "additionalProperties": false, + "description": "A downward-closed causal frontier for a partially ordered realization.", + "properties": { + "cut_kind": { + "const": "causal_frontier", + "title": "Cut Kind", + "type": "string" + }, + "cut_ref": { + "minLength": 1, + "title": "Cut Ref", + "type": "string" + }, + "frontier_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Frontier Event Refs", + "type": "array" + }, + "history_domain": { + "minLength": 1, + "title": "History Domain", + "type": "string" + }, + "order_model": { + "const": "causal_partial_order", + "title": "Order Model", + "type": "string" + }, + "predecessor_closure_ref": { + "minLength": 1, + "title": "Predecessor Closure Ref", + "type": "string" + } + }, + "required": [ + "cut_kind", + "cut_ref", + "history_domain", + "order_model", + "frontier_event_refs", + "predecessor_closure_ref" + ], + "title": "ParticipantDecisionSurfaceCausalCutModel", + "type": "object" + }, + "ParticipantDecisionSurfaceSequenceCutModel": { + "additionalProperties": false, + "description": "A complete prefix ending at one event in a declared total order.", + "properties": { + "anchor_event_ref": { + "minLength": 1, + "title": "Anchor Event Ref", + "type": "string" + }, + "anchor_order": { + "minimum": 0, + "title": "Anchor Order", + "type": "integer" + }, + "cut_kind": { + "const": "sequence_prefix", + "title": "Cut Kind", + "type": "string" + }, + "cut_ref": { + "minLength": 1, + "title": "Cut Ref", + "type": "string" + }, + "history_domain": { + "enum": [ + "participant_episode_lifecycle", + "participant_behavior_history" + ], + "title": "History Domain", + "type": "string" + }, + "history_prefix_length": { + "minimum": 1, + "title": "History Prefix Length", + "type": "integer" + }, + "order_model": { + "enum": [ + "control_plane_order", + "backend_serialized_order", + "behavior_history_order" + ], + "title": "Order Model", + "type": "string" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + } + }, + "required": [ + "cut_kind", + "cut_ref", + "history_domain", + "order_model", + "anchor_event_ref", + "anchor_order", + "history_prefix_length" + ], + "title": "ParticipantDecisionSurfaceSequenceCutModel", + "type": "object" + }, + "ParticipantInformationStateSourceRefModel": { + "additionalProperties": false, + "description": "One closed typed relation to an incumbent information source.", + "properties": { + "contract_id": { + "enum": [ + "participant-observation-envelope-v1", + "participant-context-view-v1", + "participant-shared-state-record-v1", + "participant-behavior-history-event-stream-v1", + "participant-episode-state-envelope-v1" + ], + "title": "Contract Id", + "type": "string" + }, + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" + }, + "relation": { + "enum": [ + "authored_initial", + "observed", + "derived", + "disclosed", + "shared_state_projection" + ], + "title": "Relation", + "type": "string" + } + }, + "required": [ + "contract_id", + "ref", + "relation" + ], + "title": "ParticipantInformationStateSourceRefModel", + "type": "object" + }, + "RawDataIntegrityModel": { + "additionalProperties": false, + "description": "Hash, size, and truncation facts for raw data behind a runtime claim.", + "properties": { + "raw_data_hash": { + "anyOf": [ + { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Hash" + }, + "raw_data_hash_algorithm": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Hash Algorithm" + }, + "raw_data_is_truncated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Is Truncated" + }, + "raw_data_size": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Size" + }, + "raw_data_untruncated_size": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Untruncated Size" + } + }, + "title": "RawDataIntegrityModel", + "type": "object" + }, + "SourcePipelineModel": { + "additionalProperties": false, + "description": "Source product, identity, and pipeline-time facts for a mapped record.", + "properties": { + "correlation_uid": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Correlation Uid" + }, + "log_name": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Name" + }, + "log_provider": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Provider" + }, + "log_source": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Source" + }, + "logged_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logged Time" + }, + "original_event_uid": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Original Event Uid" + }, + "original_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Original Time" + }, + "processed_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Processed Time" + }, + "product_ref": { + "minLength": 1, + "title": "Product Ref", + "type": "string" + }, + "product_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Product Version" + }, + "sequence": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence" + }, + "transmit_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Transmit Time" + } + }, + "required": [ + "product_ref" + ], + "title": "SourcePipelineModel", + "type": "object" + }, + "SourceStatusModel": { + "additionalProperties": false, + "description": "Normalized source status claim for one participant runtime record.", + "properties": { + "source_status_label": { + "minLength": 1, + "title": "Source Status Label", + "type": "string" + }, + "source_status_mapping": { + "minLength": 1, + "title": "Source Status Mapping", + "type": "string" + }, + "status": { + "minLength": 1, + "title": "Status", + "type": "string" + }, + "status_code": { + "minLength": 1, + "title": "Status Code", + "type": "string" + }, + "status_detail": { + "minLength": 1, + "title": "Status Detail", + "type": "string" + }, + "status_id": { + "title": "Status Id", + "type": "integer" + } + }, + "required": [ + "status_id", + "status", + "status_code", + "status_detail", + "source_status_label", + "source_status_mapping" + ], + "title": "SourceStatusModel", + "type": "object" + } + }, + "$id": "https://openrae.github.io/rae/schemas/participant-information-state-record-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "information_guarantee": { + "enum": [ + "history_consistent", + "perfect_recall" + ] + } + }, + "required": [ + "information_guarantee" + ] + }, + "then": { + "properties": { + "occurrence_history_ref": { + "minLength": 1, + "type": "string" + }, + "reconstructed_state_digest": { + "minLength": 1, + "type": "string" + }, + "reconstruction_algorithm_id": { + "minLength": 1, + "type": "string" + }, + "reconstruction_algorithm_version": { + "minLength": 1, + "type": "string" + }, + "reconstruction_profile_ref": { + "minLength": 1, + "type": "string" + }, + "reconstruction_proof_ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "occurrence_history_ref", + "reconstruction_profile_ref", + "reconstruction_algorithm_id", + "reconstruction_algorithm_version", + "reconstruction_proof_ref", + "reconstructed_state_digest" + ] + } + }, + { + "if": { + "properties": { + "information_guarantee": { + "const": "perfect_recall" + } + }, + "required": [ + "information_guarantee" + ] + }, + "then": { + "properties": { + "occurrence_order_witness_ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "occurrence_order_witness_ref" + ] + } + }, + { + "if": { + "properties": { + "information_guarantee": { + "const": "lossy_projection" + } + }, + "required": [ + "information_guarantee" + ] + }, + "then": { + "properties": { + "loss_disclosures": { + "minItems": 1 + } + }, + "required": [ + "loss_disclosures" + ] + } + } + ], + "description": "Immutable participant-relative information state at one exact cut.", + "properties": { + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", + "type": "string" + }, + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authorization_scope": { + "minLength": 1, + "title": "Authorization Scope", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "confidence": { + "anyOf": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "event_classification": { + "anyOf": [ + { + "$ref": "#/$defs/EventClassificationModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "event_type": { + "const": "participant_information_state", + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "extension_policy": { + "minLength": 1, + "title": "Extension Policy", + "type": "string" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" + }, + "information_guarantee": { + "enum": [ + "observation_only", + "history_consistent", + "perfect_recall", + "lossy_projection", + "unknown", + "unsupported" + ], + "title": "Information Guarantee", + "type": "string" + }, + "information_state_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Information State Digest", + "type": "string" + }, + "information_state_ref": { + "minLength": 1, + "title": "Information State Ref", + "type": "string" + }, + "ingested_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Ingested At", + "type": "string" + }, + "logical_order_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logical Order Ref" + }, + "loss_disclosures": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Loss Disclosures", + "type": "array" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array" + }, + "memory_reset_authority_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Memory Reset Authority Ref" + }, + "object_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Object Marking Refs", + "type": "array" + }, + "occurred_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Occurred At", + "type": "string" + }, + "occurrence_history_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Occurrence History Ref" + }, + "occurrence_order_witness_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Occurrence Order Witness Ref" + }, + "ordering_basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Ordering Basis", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "participant_memory_scope": { + "enum": [ + "episode_local_reset", + "persistent_across_episodes" + ], + "title": "Participant Memory Scope", + "type": "string" + }, + "payload_ref": { + "minLength": 1, + "title": "Payload Ref", + "type": "string" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "predecessor_information_state_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Information State Refs", + "type": "array" + }, + "producer_ref": { + "minLength": 1, + "title": "Producer Ref", + "type": "string" + }, + "projection_policy_revision": { + "minLength": 1, + "title": "Projection Policy Revision", + "type": "string" + }, + "projection_version": { + "minLength": 1, + "title": "Projection Version", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "reconstructed_state_digest": { + "anyOf": [ + { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstructed State Digest" + }, + "reconstruction_algorithm_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstruction Algorithm Id" + }, + "reconstruction_algorithm_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstruction Algorithm Version" + }, + "reconstruction_profile_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstruction Profile Ref" + }, + "reconstruction_proof_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstruction Proof Ref" + }, + "recorded_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Recorded At", + "type": "string" + }, + "redaction_policy_ref": { + "minLength": 1, + "title": "Redaction Policy Ref", + "type": "string" + }, + "redaction_policy_revision": { + "minLength": 1, + "title": "Redaction Policy Revision", + "type": "string" + }, + "schema_name": { + "const": "raes.participant_runtime.information_state", + "title": "Schema Name", + "type": "string" + }, + "schema_version": { + "const": "1.0.0", + "title": "Schema Version", + "type": "string" + }, + "sequence_number": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence Number" + }, + "source_pipeline": { + "anyOf": [ + { + "$ref": "#/$defs/SourcePipelineModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_raw_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Raw Ref" + }, + "source_record_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Record Ref" + }, + "source_refs": { + "items": { + "$ref": "#/$defs/ParticipantInformationStateSourceRefModel" + }, + "minItems": 1, + "title": "Source Refs", + "type": "array" + }, + "source_status": { + "anyOf": [ + { + "$ref": "#/$defs/SourceStatusModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_system_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source System Ref" + }, + "state_cut": { + "discriminator": { + "mapping": { + "causal_frontier": "#/$defs/ParticipantDecisionSurfaceCausalCutModel", + "sequence_prefix": "#/$defs/ParticipantDecisionSurfaceSequenceCutModel" + }, + "propertyName": "cut_kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/ParticipantDecisionSurfaceSequenceCutModel" + }, + { + "$ref": "#/$defs/ParticipantDecisionSurfaceCausalCutModel" + } + ], + "title": "State Cut" + }, + "supersedes_information_state_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Supersedes Information State Ref" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" + }, + "visibility_projection_ref": { + "minLength": 1, + "title": "Visibility Projection Ref", + "type": "string" + } + }, + "required": [ + "event_id", + "schema_name", + "schema_version", + "event_type", + "extension_policy", + "participant_address", + "episode_id", + "occurred_at", + "recorded_at", + "ingested_at", + "clock_authority", + "ordering_basis", + "actor_ref", + "producer_ref", + "redaction_policy_ref", + "authorization_scope", + "information_state_ref", + "information_state_digest", + "payload_ref", + "state_cut", + "participant_memory_scope", + "audience_scope_ref", + "visibility_projection_ref", + "projection_version", + "projection_policy_revision", + "redaction_policy_revision", + "information_guarantee", + "source_refs" + ], + "title": "ParticipantInformationStateRecordModel", + "type": "object", + "x-raes-invariants": [ + { + "description": "Strong information-state claims resolve one profile, occurrence history, typed source set, proof digest, and exact participant/episode/cut/projection/memory coordinate.", + "id": "participant-information-state-context-resolution", + "inputs": [ + { + "contract_id": "participant-information-state-record-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.validate_participant_information_state_context" + } + ], + "x-raes-semantic-profile": { + "contract_id": "participant-information-state-record-v1", + "entry_schema_contract_id": "raes-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/RaesSemanticInvariantEntryModel", + "id": "raes-semantic-invariants-v1", + "keyword": "x-raes-invariants", + "required": true, + "uri": "https://openrae.github.io/rae/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/participant-runtime/participant-observation-envelope-v1.json b/contracts/schemas/participant-runtime/participant-observation-envelope-v1.json index 74e9cbd8b..d892f43c1 100644 --- a/contracts/schemas/participant-runtime/participant-observation-envelope-v1.json +++ b/contracts/schemas/participant-runtime/participant-observation-envelope-v1.json @@ -412,6 +412,71 @@ "$id": "https://openrae.github.io/rae/schemas/participant-observation-envelope-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "information_guarantee": { + "enum": [ + "history_consistent", + "perfect_recall" + ] + } + }, + "required": [ + "information_guarantee" + ] + }, + "then": { + "properties": { + "action_observation_history_ref": { + "minLength": 1, + "type": "string" + }, + "information_state_ref": { + "minLength": 1, + "type": "string" + }, + "reconstruction_algorithm_ref": { + "minLength": 1, + "type": "string" + }, + "reconstruction_proof_ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "action_observation_history_ref", + "information_state_ref", + "reconstruction_algorithm_ref", + "reconstruction_proof_ref" + ] + } + }, + { + "if": { + "properties": { + "information_guarantee": { + "const": "lossy_projection" + } + }, + "required": [ + "information_guarantee" + ] + }, + "then": { + "properties": { + "loss_descriptor": { + "type": "object" + } + }, + "required": [ + "loss_descriptor" + ] + } + } + ], "description": "SEM-210 participant-visible observation record with explicit guarantees.", "properties": { "action_observation_history_ref": { diff --git a/contracts/schemas/profiles/participant-information-reconstruction-profile-v1.json b/contracts/schemas/profiles/participant-information-reconstruction-profile-v1.json new file mode 100644 index 000000000..c52b9b9b9 --- /dev/null +++ b/contracts/schemas/profiles/participant-information-reconstruction-profile-v1.json @@ -0,0 +1,120 @@ +{ + "$id": "https://openrae.github.io/rae/schemas/participant-information-reconstruction-profile-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "One immutable, non-executable reconstruction profile declaration.", + "properties": { + "accepted_input_contracts": { + "items": { + "enum": [ + "participant-observation-envelope-v1", + "participant-context-view-v1", + "participant-shared-state-record-v1", + "participant-behavior-history-event-stream-v1", + "participant-episode-state-envelope-v1" + ], + "type": "string" + }, + "minItems": 1, + "title": "Accepted Input Contracts", + "type": "array" + }, + "accepted_order_semantics": { + "items": { + "enum": [ + "sequence_prefix", + "causal_frontier" + ], + "type": "string" + }, + "minItems": 1, + "title": "Accepted Order Semantics", + "type": "array" + }, + "algorithm_id": { + "minLength": 1, + "title": "Algorithm Id", + "type": "string" + }, + "algorithm_version": { + "minLength": 1, + "title": "Algorithm Version", + "type": "string" + }, + "description": { + "minLength": 1, + "title": "Description", + "type": "string" + }, + "determinism_basis": { + "const": "exact_occurrence_prefix_and_proof_digest", + "title": "Determinism Basis", + "type": "string" + }, + "fixture_format": { + "minLength": 1, + "title": "Fixture Format", + "type": "string" + }, + "information_state_schema_version": { + "minLength": 1, + "title": "Information State Schema Version", + "type": "string" + }, + "normative_artifact_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Normative Artifact Digest", + "type": "string" + }, + "normative_artifact_ref": { + "minLength": 1, + "title": "Normative Artifact Ref", + "type": "string" + }, + "profile_id": { + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*-v[0-9]+$", + "title": "Profile Id", + "type": "string" + }, + "projection_version": { + "minLength": 1, + "title": "Projection Version", + "type": "string" + }, + "proof_artifact_format": { + "minLength": 1, + "title": "Proof Artifact Format", + "type": "string" + }, + "schema_version": { + "const": "participant-information-reconstruction-profile/v1", + "title": "Schema Version", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + } + }, + "required": [ + "schema_version", + "profile_id", + "title", + "description", + "algorithm_id", + "algorithm_version", + "information_state_schema_version", + "projection_version", + "determinism_basis", + "accepted_input_contracts", + "accepted_order_semantics", + "fixture_format", + "proof_artifact_format", + "normative_artifact_ref", + "normative_artifact_digest" + ], + "title": "ParticipantInformationReconstructionProfileModel", + "type": "object" +} diff --git a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json index 683d5c941..39f50052e 100644 --- a/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json +++ b/contracts/schemas/satisfiability/scenario-satisfiability-evidence-v1.json @@ -13652,6 +13652,18 @@ "title": "Name", "type": "string" }, + "ownership_role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgentOwnershipRole" + }, + { + "type": "string" + } + ], + "default": "system_under_test", + "title": "Ownership Role" + }, "reload_channels": { "items": { "$ref": "#/$defs/RuntimeForwardingReloadChannel" @@ -13729,6 +13741,15 @@ "title": "RuntimeForwardingAgentKind", "type": "string" }, + "RuntimeForwardingAgentOwnershipRole": { + "description": "Closed ownership of an agent relative to the system under test.", + "enum": [ + "system_under_test", + "measurement_apparatus" + ], + "title": "RuntimeForwardingAgentOwnershipRole", + "type": "string" + }, "RuntimeForwardingBufferCrypto": { "description": "Portable at-rest / in-transit crypto of a buffer policy (OPEN).", "enum": [ diff --git a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json index 91398fc57..c5b46c4d9 100644 --- a/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json @@ -16558,6 +16558,21 @@ "title": "Name", "type": "string" }, + "ownership_role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgentOwnershipRole" + }, + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "system_under_test", + "title": "Ownership Role" + }, "reload_channels": { "items": { "$ref": "#/$defs/RuntimeForwardingReloadChannel" @@ -16638,6 +16653,15 @@ "title": "RuntimeForwardingAgentKind", "type": "string" }, + "RuntimeForwardingAgentOwnershipRole": { + "description": "Closed ownership of an agent relative to the system under test.", + "enum": [ + "system_under_test", + "measurement_apparatus" + ], + "title": "RuntimeForwardingAgentOwnershipRole", + "type": "string" + }, "RuntimeForwardingBufferCrypto": { "description": "Portable at-rest / in-transit crypto of a buffer policy (OPEN).", "enum": [ diff --git a/contracts/schemas/sdl/instantiated-scenario-v1.json b/contracts/schemas/sdl/instantiated-scenario-v1.json index ee4f3a83a..fd8ec54be 100644 --- a/contracts/schemas/sdl/instantiated-scenario-v1.json +++ b/contracts/schemas/sdl/instantiated-scenario-v1.json @@ -15915,6 +15915,21 @@ "title": "Name", "type": "string" }, + "ownership_role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgentOwnershipRole" + }, + { + "not": { + "pattern": "\\$\\{((?:(?:[a-z0-9][a-z0-9_-]{0,63}|__private)\\.)*[a-z0-9][a-z0-9_-]{0,63})\\}" + }, + "type": "string" + } + ], + "default": "system_under_test", + "title": "Ownership Role" + }, "reload_channels": { "items": { "$ref": "#/$defs/RuntimeForwardingReloadChannel" @@ -15995,6 +16010,15 @@ "title": "RuntimeForwardingAgentKind", "type": "string" }, + "RuntimeForwardingAgentOwnershipRole": { + "description": "Closed ownership of an agent relative to the system under test.", + "enum": [ + "system_under_test", + "measurement_apparatus" + ], + "title": "RuntimeForwardingAgentOwnershipRole", + "type": "string" + }, "RuntimeForwardingBufferCrypto": { "description": "Portable at-rest / in-transit crypto of a buffer policy (OPEN).", "enum": [ diff --git a/contracts/schemas/sdl/sdl-authoring-input-v1.json b/contracts/schemas/sdl/sdl-authoring-input-v1.json index 84aab29e0..389b9b27d 100644 --- a/contracts/schemas/sdl/sdl-authoring-input-v1.json +++ b/contracts/schemas/sdl/sdl-authoring-input-v1.json @@ -13537,6 +13537,18 @@ "title": "Name", "type": "string" }, + "ownership_role": { + "anyOf": [ + { + "$ref": "#/$defs/RuntimeForwardingAgentOwnershipRole" + }, + { + "type": "string" + } + ], + "default": "system_under_test", + "title": "Ownership Role" + }, "reload_channels": { "items": { "$ref": "#/$defs/RuntimeForwardingReloadChannel" @@ -13614,6 +13626,15 @@ "title": "RuntimeForwardingAgentKind", "type": "string" }, + "RuntimeForwardingAgentOwnershipRole": { + "description": "Closed ownership of an agent relative to the system under test.", + "enum": [ + "system_under_test", + "measurement_apparatus" + ], + "title": "RuntimeForwardingAgentOwnershipRole", + "type": "string" + }, "RuntimeForwardingBufferCrypto": { "description": "Portable at-rest / in-transit crypto of a buffer policy (OPEN).", "enum": [ diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index a5b986855..57e316580 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -265,6 +265,218 @@ "title": "ArtifactSatisfactionDisclosureModel", "type": "object" }, + "BehavioralClaimBindingModel": { + "additionalProperties": false, + "description": "A bounded claim tied to one revisioned behavioral-relation definition.\n\nThis is deliberately a claim *binding*, not another relation registry. The\ncatalog owns relation meaning; consumers supply the subject, carriers,\nquantifier/evidence boundary, assurance state, and explicit limitations.", + "properties": { + "assurance_axis": { + "anyOf": [ + { + "enum": [ + "definition", + "checker", + "bounded-test", + "model-check", + "proof", + "runtime-enforcement", + "backend-declaration", + "backend-realization", + "backend-conformance" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Assurance Axis" + }, + "assurance_status": { + "enum": [ + "defined", + "implemented", + "tested", + "model-checked", + "proved", + "deliberately-unproved", + "future", + "enforced", + "declared", + "realized", + "conformant" + ], + "title": "Assurance Status", + "type": "string" + }, + "evidence_boundary": { + "minLength": 1, + "title": "Evidence Boundary", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "evidence_scope": { + "enum": [ + "structural", + "finite", + "statistical", + "model-check", + "proof" + ], + "title": "Evidence Scope", + "type": "string" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "left_carrier_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Left Carrier Ref" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "observation_projection_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Projection Ref" + }, + "observation_projection_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Projection Revision" + }, + "quantifier_scope": { + "enum": [ + "single-artifact", + "finite-cases", + "sampled-population", + "all-admitted-inputs", + "all-traces", + "all-strategies" + ], + "title": "Quantifier Scope", + "type": "string" + }, + "relation_id": { + "pattern": "^[a-z][a-z0-9-]*$", + "title": "Relation Id", + "type": "string" + }, + "relation_parameter_profile_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Relation Parameter Profile Ref" + }, + "relation_parameter_profile_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Relation Parameter Profile Revision" + }, + "right_carrier_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Right Carrier Ref" + }, + "subject": { + "minLength": 1, + "title": "Subject", + "type": "string" + }, + "taxonomy_id": { + "minLength": 1, + "title": "Taxonomy Id", + "type": "string" + }, + "taxonomy_revision": { + "pattern": "^[a-z0-9][a-z0-9.-]*$", + "title": "Taxonomy Revision", + "type": "string" + } + }, + "required": [ + "taxonomy_id", + "taxonomy_revision", + "relation_id", + "subject", + "quantifier_scope", + "evidence_scope", + "evidence_boundary", + "assurance_status", + "limitations", + "explicit_non_claims" + ], + "title": "BehavioralClaimBindingModel", + "type": "object" + }, "ClockTransitionEventModel": { "additionalProperties": false, "properties": { @@ -588,6 +800,17 @@ "title": "ExplicitnessProvenance", "type": "string" }, + "ObservationStrength": { + "description": "Strongest evidence a backend configuration emits for one concern.", + "enum": [ + "none", + "driver-reported", + "daemon-observed", + "guest-observed" + ], + "title": "ObservationStrength", + "type": "string" + }, "OutcomeInterpretationSourceLayer": { "description": "Semantic layers that may feed a SEM-215 interpretation rule.", "enum": [ @@ -2738,6 +2961,17 @@ "title": "Loss And Limitations", "type": "array" }, + "opacity_enforcement": { + "anyOf": [ + { + "$ref": "#/$defs/ParticipantOpacityRuntimeEnforcementBindingModel" + }, + { + "type": "null" + } + ], + "default": null + }, "order_model": { "enum": [ "total_order", @@ -4344,6 +4578,123 @@ "title": "ParticipantCrossingTransformationModel", "type": "object" }, + "ParticipantDecisionSurfaceCausalCutModel": { + "additionalProperties": false, + "description": "A downward-closed causal frontier for a partially ordered realization.", + "properties": { + "cut_kind": { + "const": "causal_frontier", + "title": "Cut Kind", + "type": "string" + }, + "cut_ref": { + "minLength": 1, + "title": "Cut Ref", + "type": "string" + }, + "frontier_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Frontier Event Refs", + "type": "array" + }, + "history_domain": { + "minLength": 1, + "title": "History Domain", + "type": "string" + }, + "order_model": { + "const": "causal_partial_order", + "title": "Order Model", + "type": "string" + }, + "predecessor_closure_ref": { + "minLength": 1, + "title": "Predecessor Closure Ref", + "type": "string" + } + }, + "required": [ + "cut_kind", + "cut_ref", + "history_domain", + "order_model", + "frontier_event_refs", + "predecessor_closure_ref" + ], + "title": "ParticipantDecisionSurfaceCausalCutModel", + "type": "object" + }, + "ParticipantDecisionSurfaceSequenceCutModel": { + "additionalProperties": false, + "description": "A complete prefix ending at one event in a declared total order.", + "properties": { + "anchor_event_ref": { + "minLength": 1, + "title": "Anchor Event Ref", + "type": "string" + }, + "anchor_order": { + "minimum": 0, + "title": "Anchor Order", + "type": "integer" + }, + "cut_kind": { + "const": "sequence_prefix", + "title": "Cut Kind", + "type": "string" + }, + "cut_ref": { + "minLength": 1, + "title": "Cut Ref", + "type": "string" + }, + "history_domain": { + "enum": [ + "participant_episode_lifecycle", + "participant_behavior_history" + ], + "title": "History Domain", + "type": "string" + }, + "history_prefix_length": { + "minimum": 1, + "title": "History Prefix Length", + "type": "integer" + }, + "order_model": { + "enum": [ + "control_plane_order", + "backend_serialized_order", + "behavior_history_order" + ], + "title": "Order Model", + "type": "string" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + } + }, + "required": [ + "cut_kind", + "cut_ref", + "history_domain", + "order_model", + "anchor_event_ref", + "anchor_order", + "history_prefix_length" + ], + "title": "ParticipantDecisionSurfaceSequenceCutModel", + "type": "object" + }, "ParticipantDenialOccurrenceModel": { "additionalProperties": false, "description": "Denial of exactly one proposal revision.", @@ -5174,52 +5525,759 @@ } ], "default": null, - "title": "Reason Ref" + "title": "Reason Ref" + }, + "resulting_controller_state_ref": { + "minLength": 1, + "title": "Resulting Controller State Ref", + "type": "string" + }, + "resulting_state_revision": { + "minimum": 1, + "title": "Resulting State Revision", + "type": "integer" + }, + "valid_from_order": { + "minimum": 0, + "title": "Valid From Order", + "type": "integer" + }, + "valid_until_order": { + "minimum": 0, + "title": "Valid Until Order", + "type": "integer" + } + }, + "required": [ + "declaration_ref", + "controller_ref", + "controller_state_ref", + "authority_basis_refs", + "controlled_scope_refs", + "behavior_specification_ref", + "mixed_control_policy_ref", + "policy_revision", + "expected_state_revision", + "effective_order", + "valid_from_order", + "valid_until_order", + "occurrence_revision", + "disposition", + "limitation_refs", + "kind", + "prior_controller_state_ref", + "resulting_controller_state_ref", + "resulting_state_revision", + "completion_evidence_ref" + ], + "title": "ParticipantHandoffOccurrenceModel", + "type": "object" + }, + "ParticipantInformationStateRecordModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "information_guarantee": { + "enum": [ + "history_consistent", + "perfect_recall" + ] + } + }, + "required": [ + "information_guarantee" + ] + }, + "then": { + "properties": { + "occurrence_history_ref": { + "minLength": 1, + "type": "string" + }, + "reconstructed_state_digest": { + "minLength": 1, + "type": "string" + }, + "reconstruction_algorithm_id": { + "minLength": 1, + "type": "string" + }, + "reconstruction_algorithm_version": { + "minLength": 1, + "type": "string" + }, + "reconstruction_profile_ref": { + "minLength": 1, + "type": "string" + }, + "reconstruction_proof_ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "occurrence_history_ref", + "reconstruction_profile_ref", + "reconstruction_algorithm_id", + "reconstruction_algorithm_version", + "reconstruction_proof_ref", + "reconstructed_state_digest" + ] + } + }, + { + "if": { + "properties": { + "information_guarantee": { + "const": "perfect_recall" + } + }, + "required": [ + "information_guarantee" + ] + }, + "then": { + "properties": { + "occurrence_order_witness_ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "occurrence_order_witness_ref" + ] + } + }, + { + "if": { + "properties": { + "information_guarantee": { + "const": "lossy_projection" + } + }, + "required": [ + "information_guarantee" + ] + }, + "then": { + "properties": { + "loss_disclosures": { + "minItems": 1 + } + }, + "required": [ + "loss_disclosures" + ] + } + } + ], + "description": "Immutable participant-relative information state at one exact cut.", + "properties": { + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", + "type": "string" + }, + "audience_scope_ref": { + "minLength": 1, + "title": "Audience Scope Ref", + "type": "string" + }, + "authorization_scope": { + "minLength": 1, + "title": "Authorization Scope", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "confidence": { + "anyOf": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "event_classification": { + "anyOf": [ + { + "$ref": "#/$defs/EventClassificationModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "event_type": { + "const": "participant_information_state", + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "extension_policy": { + "minLength": 1, + "title": "Extension Policy", + "type": "string" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" + }, + "information_guarantee": { + "enum": [ + "observation_only", + "history_consistent", + "perfect_recall", + "lossy_projection", + "unknown", + "unsupported" + ], + "title": "Information Guarantee", + "type": "string" + }, + "information_state_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Information State Digest", + "type": "string" + }, + "information_state_ref": { + "minLength": 1, + "title": "Information State Ref", + "type": "string" + }, + "ingested_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Ingested At", + "type": "string" + }, + "logical_order_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logical Order Ref" + }, + "loss_disclosures": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Loss Disclosures", + "type": "array" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array" + }, + "memory_reset_authority_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Memory Reset Authority Ref" + }, + "object_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Object Marking Refs", + "type": "array" + }, + "occurred_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Occurred At", + "type": "string" + }, + "occurrence_history_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Occurrence History Ref" + }, + "occurrence_order_witness_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Occurrence Order Witness Ref" + }, + "ordering_basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Ordering Basis", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "participant_memory_scope": { + "enum": [ + "episode_local_reset", + "persistent_across_episodes" + ], + "title": "Participant Memory Scope", + "type": "string" + }, + "payload_ref": { + "minLength": 1, + "title": "Payload Ref", + "type": "string" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "predecessor_information_state_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Information State Refs", + "type": "array" + }, + "producer_ref": { + "minLength": 1, + "title": "Producer Ref", + "type": "string" + }, + "projection_policy_revision": { + "minLength": 1, + "title": "Projection Policy Revision", + "type": "string" + }, + "projection_version": { + "minLength": 1, + "title": "Projection Version", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "reconstructed_state_digest": { + "anyOf": [ + { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstructed State Digest" + }, + "reconstruction_algorithm_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstruction Algorithm Id" + }, + "reconstruction_algorithm_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstruction Algorithm Version" + }, + "reconstruction_profile_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstruction Profile Ref" + }, + "reconstruction_proof_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reconstruction Proof Ref" + }, + "recorded_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Recorded At", + "type": "string" + }, + "redaction_policy_ref": { + "minLength": 1, + "title": "Redaction Policy Ref", + "type": "string" + }, + "redaction_policy_revision": { + "minLength": 1, + "title": "Redaction Policy Revision", + "type": "string" + }, + "schema_name": { + "const": "raes.participant_runtime.information_state", + "title": "Schema Name", + "type": "string" + }, + "schema_version": { + "const": "1.0.0", + "title": "Schema Version", + "type": "string" + }, + "sequence_number": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence Number" + }, + "source_pipeline": { + "anyOf": [ + { + "$ref": "#/$defs/SourcePipelineModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_raw_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Raw Ref" + }, + "source_record_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Record Ref" + }, + "source_refs": { + "items": { + "$ref": "#/$defs/ParticipantInformationStateSourceRefModel" + }, + "minItems": 1, + "title": "Source Refs", + "type": "array" + }, + "source_status": { + "anyOf": [ + { + "$ref": "#/$defs/SourceStatusModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_system_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source System Ref" + }, + "state_cut": { + "discriminator": { + "mapping": { + "causal_frontier": "#/$defs/ParticipantDecisionSurfaceCausalCutModel", + "sequence_prefix": "#/$defs/ParticipantDecisionSurfaceSequenceCutModel" + }, + "propertyName": "cut_kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/ParticipantDecisionSurfaceSequenceCutModel" + }, + { + "$ref": "#/$defs/ParticipantDecisionSurfaceCausalCutModel" + } + ], + "title": "State Cut" + }, + "supersedes_information_state_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Supersedes Information State Ref" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" }, - "resulting_controller_state_ref": { + "visibility_projection_ref": { "minLength": 1, - "title": "Resulting Controller State Ref", + "title": "Visibility Projection Ref", + "type": "string" + } + }, + "required": [ + "event_id", + "schema_name", + "schema_version", + "event_type", + "extension_policy", + "participant_address", + "episode_id", + "occurred_at", + "recorded_at", + "ingested_at", + "clock_authority", + "ordering_basis", + "actor_ref", + "producer_ref", + "redaction_policy_ref", + "authorization_scope", + "information_state_ref", + "information_state_digest", + "payload_ref", + "state_cut", + "participant_memory_scope", + "audience_scope_ref", + "visibility_projection_ref", + "projection_version", + "projection_policy_revision", + "redaction_policy_revision", + "information_guarantee", + "source_refs" + ], + "title": "ParticipantInformationStateRecordModel", + "type": "object", + "x-raes-invariants": [ + { + "description": "Strong information-state claims resolve one profile, occurrence history, typed source set, proof digest, and exact participant/episode/cut/projection/memory coordinate.", + "id": "participant-information-state-context-resolution", + "inputs": [ + { + "contract_id": "participant-information-state-record-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "raes_contracts.contracts.validate_participant_information_state_context" + } + ] + }, + "ParticipantInformationStateSourceRefModel": { + "additionalProperties": false, + "description": "One closed typed relation to an incumbent information source.", + "properties": { + "contract_id": { + "enum": [ + "participant-observation-envelope-v1", + "participant-context-view-v1", + "participant-shared-state-record-v1", + "participant-behavior-history-event-stream-v1", + "participant-episode-state-envelope-v1" + ], + "title": "Contract Id", "type": "string" }, - "resulting_state_revision": { - "minimum": 1, - "title": "Resulting State Revision", - "type": "integer" - }, - "valid_from_order": { - "minimum": 0, - "title": "Valid From Order", - "type": "integer" + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" }, - "valid_until_order": { - "minimum": 0, - "title": "Valid Until Order", - "type": "integer" + "relation": { + "enum": [ + "authored_initial", + "observed", + "derived", + "disclosed", + "shared_state_projection" + ], + "title": "Relation", + "type": "string" } }, "required": [ - "declaration_ref", - "controller_ref", - "controller_state_ref", - "authority_basis_refs", - "controlled_scope_refs", - "behavior_specification_ref", - "mixed_control_policy_ref", - "policy_revision", - "expected_state_revision", - "effective_order", - "valid_from_order", - "valid_until_order", - "occurrence_revision", - "disposition", - "limitation_refs", - "kind", - "prior_controller_state_ref", - "resulting_controller_state_ref", - "resulting_state_revision", - "completion_evidence_ref" + "contract_id", + "ref", + "relation" ], - "title": "ParticipantHandoffOccurrenceModel", + "title": "ParticipantInformationStateSourceRefModel", "type": "object" }, "ParticipantInteractionClass": { @@ -6035,6 +7093,211 @@ "title": "ParticipantObservationStatus", "type": "string" }, + "ParticipantOpacityRuntimeEnforcementBindingModel": { + "additionalProperties": false, + "description": "Safe finite runtime-enforcement binding owned by an API-423 decision.", + "properties": { + "assurance_axis": { + "const": "runtime-enforcement", + "title": "Assurance Axis", + "type": "string" + }, + "carrier_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Carrier Digest", + "type": "string" + }, + "carrier_ref": { + "minLength": 1, + "title": "Carrier Ref", + "type": "string" + }, + "claim": { + "$ref": "#/$defs/BehavioralClaimBindingModel" + }, + "enforcement_rule_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Enforcement Rule Digest", + "type": "string" + }, + "enforcement_rule_ref": { + "minLength": 1, + "title": "Enforcement Rule Ref", + "type": "string" + }, + "enforcement_rule_revision": { + "minLength": 1, + "title": "Enforcement Rule Revision", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "explicit_non_claims": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Explicit Non Claims", + "type": "array" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "materializer_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Materializer Digest", + "type": "string" + }, + "materializer_ref": { + "minLength": 1, + "title": "Materializer Ref", + "type": "string" + }, + "materializer_revision": { + "minLength": 1, + "title": "Materializer Revision", + "type": "string" + }, + "memory_ref": { + "minLength": 1, + "title": "Memory Ref", + "type": "string" + }, + "memory_revision": { + "minLength": 1, + "title": "Memory Revision", + "type": "string" + }, + "observation_inventory_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Observation Inventory Digest", + "type": "string" + }, + "observation_inventory_ref": { + "minLength": 1, + "title": "Observation Inventory Ref", + "type": "string" + }, + "observation_inventory_revision": { + "minLength": 1, + "title": "Observation Inventory Revision", + "type": "string" + }, + "predicate_ref": { + "minLength": 1, + "title": "Predicate Ref", + "type": "string" + }, + "predicate_revision": { + "minLength": 1, + "title": "Predicate Revision", + "type": "string" + }, + "profile_digest": { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "title": "Profile Digest", + "type": "string" + }, + "profile_id": { + "minLength": 1, + "title": "Profile Id", + "type": "string" + }, + "profile_revision": { + "minLength": 1, + "title": "Profile Revision", + "type": "string" + }, + "relation_id": { + "const": "participant-predicate-opacity", + "title": "Relation Id", + "type": "string" + }, + "release_ref": { + "minLength": 1, + "title": "Release Ref", + "type": "string" + }, + "release_revision": { + "minLength": 1, + "title": "Release Revision", + "type": "string" + }, + "state_cut_ref": { + "minLength": 1, + "title": "State Cut Ref", + "type": "string" + }, + "state_cut_revision": { + "minLength": 1, + "title": "State Cut Revision", + "type": "string" + }, + "taxonomy_id": { + "const": "raes-behavioral-relations", + "title": "Taxonomy Id", + "type": "string" + }, + "taxonomy_revision": { + "minLength": 1, + "title": "Taxonomy Revision", + "type": "string" + } + }, + "required": [ + "taxonomy_id", + "taxonomy_revision", + "relation_id", + "profile_id", + "profile_revision", + "profile_digest", + "predicate_ref", + "predicate_revision", + "carrier_ref", + "carrier_digest", + "materializer_ref", + "materializer_revision", + "materializer_digest", + "observation_inventory_ref", + "observation_inventory_revision", + "observation_inventory_digest", + "enforcement_rule_ref", + "enforcement_rule_revision", + "enforcement_rule_digest", + "state_cut_ref", + "state_cut_revision", + "memory_ref", + "memory_revision", + "release_ref", + "release_revision", + "assurance_axis", + "claim", + "evidence_refs", + "limitations", + "explicit_non_claims" + ], + "title": "ParticipantOpacityRuntimeEnforcementBindingModel", + "type": "object" + }, "ParticipantOutcomeInterpretationRecordModel": { "additionalProperties": false, "properties": { @@ -9176,6 +10439,56 @@ "title": "RealizationEnvelopeIdentityModel", "type": "object" }, + "RealizationObservationDisclosureModel": { + "additionalProperties": false, + "description": "Value-free corroboration metadata for one realized inventory concern.", + "properties": { + "address": { + "maxLength": 2048, + "minLength": 3, + "not": { + "pattern": "[^a-z0-9_.-]" + }, + "pattern": "^(?:[a-z0-9][a-z0-9_-]{0,63}|__private)(?:\\.(?:[a-z0-9][a-z0-9_-]{0,63}|__private))+$", + "title": "Address", + "type": "string" + }, + "domain": { + "minLength": 1, + "title": "Domain", + "type": "string" + }, + "field_path": { + "minLength": 1, + "title": "Field Path", + "type": "string" + }, + "observation_strength": { + "$ref": "#/$defs/ObservationStrength", + "not": { + "const": "none" + } + }, + "requirement_kind": { + "minLength": 1, + "title": "Requirement Kind", + "type": "string" + }, + "verification_scope": { + "$ref": "#/$defs/RealizationVerificationScope" + } + }, + "required": [ + "address", + "field_path", + "domain", + "requirement_kind", + "verification_scope", + "observation_strength" + ], + "title": "RealizationObservationDisclosureModel", + "type": "object" + }, "RealizationProvenanceEntryModel": { "additionalProperties": false, "description": "SEM-218 invariant I5: provenance for one realized realization concern.\n\nDistinguishes ``author-declared`` / ``processor-derived`` / ``backend-realized``\norigins for a realized concern recorded on the snapshot's result / history\nsurfaces. Carries field-path and kind references only (never the realized\nvalue, per the SEM-218 host-exposure gate). Kept distinct from ADR-054\nlifecycle ``phase_realization`` and API-407 participant feature support.", @@ -9242,6 +10555,15 @@ "title": "RealizationProvenanceEntryModel", "type": "object" }, + "RealizationVerificationScope": { + "description": "Closed scope at which an inventory realization was corroborated.", + "enum": [ + "presence", + "configuration" + ], + "title": "RealizationVerificationScope", + "type": "string" + }, "RuntimeClockStateModel": { "additionalProperties": false, "properties": { @@ -10036,6 +11358,16 @@ "title": "Evaluation Results", "type": "object" }, + "information_state_history": { + "additionalProperties": { + "items": { + "$ref": "#/$defs/ParticipantInformationStateRecordModel" + }, + "type": "array" + }, + "title": "Information State History", + "type": "object" + }, "joint_action_records": { "additionalProperties": { "$ref": "#/$defs/ParticipantJointActionRecordModel" @@ -10165,6 +11497,13 @@ ], "default": null }, + "realization_observations": { + "items": { + "$ref": "#/$defs/RealizationObservationDisclosureModel" + }, + "title": "Realization Observations", + "type": "array" + }, "realization_provenance": { "items": { "$ref": "#/$defs/RealizationProvenanceEntryModel" diff --git a/docs/conformance/libvirt-qemu.provisioning-only.report.json b/docs/conformance/libvirt-qemu.provisioning-only.report.json index 1439b12a5..2af6bbbba 100644 --- a/docs/conformance/libvirt-qemu.provisioning-only.report.json +++ b/docs/conformance/libvirt-qemu.provisioning-only.report.json @@ -215,6 +215,15 @@ "passed": true, "diagnostic_codes": [] }, + { + "name": "realization-observation-missing-strength", + "contract_name": "runtime-snapshot-v1", + "valid": false, + "passed": true, + "diagnostic_codes": [ + "conformance.schema-invalid" + ] + }, { "name": "realization-provenance-missing-provenance", "contract_name": "runtime-snapshot-v1", diff --git a/docs/decisions/adrs/adr-050-forwarding-agent-runtime-inventory.md b/docs/decisions/adrs/adr-050-forwarding-agent-runtime-inventory.md index 1e4c6f6cb..4022313a5 100644 --- a/docs/decisions/adrs/adr-050-forwarding-agent-runtime-inventory.md +++ b/docs/decisions/adrs/adr-050-forwarding-agent-runtime-inventory.md @@ -91,6 +91,38 @@ service is then resolved on that node. `RelationshipForwardingEdge.forwarder_ref resolves across both node-hosted and scenario-level forwarding-agent registries. `forwarding_agent_id` values must be unique across both registries. +### 5. Define realization as corroborated inventory, not behavior + +For the SEM-218 `forwarding-agents` concern, realization means that the +agent's identity, placement, implementation, and declared configuration +projection are present and independently corroborated. The existing +`project_forwarding_agents` projection remains the exact comparison surface. +It does not prove that the agent shipped an event, applied a transform, reached +the declared destination, or caused a reload. + +Backend manifests disclose concern-keyed observation capability using the +closed verification scopes `presence` and `configuration`, paired with the +existing observation-strength source axis. Runtime snapshots disclose the +scope and source actually used without copying realized values. An exact +declaration fails closed when the capability or returned disclosure is missing +or weaker than the authored scope; equality with an echoed plan payload is not +corroboration. + +### 6. Classify agent ownership and bind apparatus evidence inward + +Each forwarding agent has a closed `ownership_role`: + +- `system_under_test` means the agent is scenario state; and +- `measurement_apparatus` means the agent exists to collect experiment + evidence. + +The role is authored ownership, not an execution mode, visibility claim, or +backend configuration field. A measurement-apparatus agent must be named by an +inbound apparatus-class `EvidenceRequirement.source_refs` binding. The same +binding is invalid for an agent marked `system_under_test`. Forwarding agents +do not carry evidence destinations, credentials, capture payloads, or reverse +evidence-reference fields. + ## Security and Validation Gates - Parser/model gate: stable agent and child ids are concrete symbols, not @@ -164,3 +196,4 @@ resolves across both node-hosted and scenario-level forwarding-agent registries. | Date | Commit/PR | Summary | |------|-----------|---------| | 2026-05-31 | e815f27 | Added scenario-level forwarding agents (top-level `Scenario.forwarding_agents`) with cross-registry ref resolution and uniqueness. | +| 2026-08-01 | #1043 | Defined corroborated inventory realization, closed ownership roles, and evidence-plane binding for forwarding agents. | diff --git a/docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md b/docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md index 68d70b4cc..559445cdc 100644 --- a/docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md +++ b/docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md @@ -44,6 +44,14 @@ The common invariant is: - the shared secret-name vocabulary is helper logic, not a source of credential values. +Realization corroboration follows the same exposure boundary. A runtime +observation disclosure carries only the concern address, field path, kind, +verification scope, and observation strength. It MUST NOT repeat a forwarding +setting, enrollment material, native inspect object, evidence location, or +credential. Secret-safe commitments remain the comparison form for permitted +forwarding settings; observation metadata does not become a second value +carrier. + This invariant is implemented as a shared helper in `runtime_values.py`. Family models call that helper only where the semantics are actually shared. Family models still own their ids, refs, scopes, provenance enums, and closed/open @@ -138,3 +146,4 @@ Rejected alternatives: |------|-----------|---------| | 2026-07-13 | #417 | Narrowed the authored SDL boundary: health results, generated network identity, scanner-derived package findings, and scanner capture provenance use evidence/derived carriers rather than `Node.runtime`; the shared redaction invariant continues to govern the remaining declarative runtime fields. | | 2026-07-29 | #956 | Removed the obsolete platform-kind profile-guard claim; platform capability and legacy category/content semantics remain family-local while shared explicit-redaction handling is unchanged. | +| 2026-08-01 | #1043 | Applied the raw-value omission boundary to forwarding-agent realization observation disclosures. | diff --git a/docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md b/docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md index 107b4ef4a..4ecd84615 100644 --- a/docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md +++ b/docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md @@ -138,6 +138,22 @@ runtime, evidence, or provenance carriers. It must not hide in `RuntimeSnapshot.metadata`, evaluator details, diagnostics, audit blobs, backend DTOs, or raw logs. +### 7. Forwarding-agent ownership does not collapse the planes + +A forwarding agent marked `system_under_test` remains scenario-native +observability. A forwarding agent marked `measurement_apparatus` remains an +authored inventory declaration for experiment apparatus; it does not become +captured evidence merely by existing. The ownership role is not an execution +or visibility claim. + +The authored evidence-requirement plane owns the binding direction. An +apparatus-class `EvidenceRequirement` names the measurement forwarding agent +in `source_refs`; the agent does not name evidence records, capture locations, +or storage credentials. Environment-visible or comparability-relevant +apparatus still requires the existing run-level augmentation disclosure. +Operational delivery or failure claims remain proposition/probe/truth/evidence +claims rather than fields on the inventory or realization observation. + ## Required Boundaries - A backend log is not a participant observation unless a participant @@ -237,3 +253,4 @@ participant-visible, or comparability-relevant. | Date | Commit/PR | Summary | |------|-----------|---------| | 2026-06-23 | #335 | Implemented SEM-225 run-level augmentation disclosures in `experiment-run-v1`, including separate environment-visible, participant-visible, and comparability-relevant validation. | +| 2026-08-01 | #1043 | Separated forwarding-agent ownership, realization corroboration, evidence binding, and operational behavior claims. | diff --git a/docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md b/docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md index 5b26cd746..190a0325d 100644 --- a/docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md +++ b/docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md @@ -25,6 +25,11 @@ services while evaluated participants operate. Deterministic activity is still participant behavior: its implementation may be a script, agent, simulator, or external service, but determinism does not create a new actor ontology. +ACT-605's declarative baseline and background behavior profiles are this same +capability: ordinary user, automation, and ambient scenario activity expressed +through autonomous participant policy. The requirement names a scenario +purpose, not a separate actor, scheduler, clock, or execution plane. + The prior live-activity prototype introduced parallel actor, scheduler, and historical-data concepts. That would duplicate participant semantics, bypass the shared time authority, and make scenario-pack control conventions part of @@ -39,7 +44,8 @@ Autonomous benign activity is authored under `ParticipantBehaviorSpecification.autonomous_execution`. It resolves existing agents, participant action contracts, observation boundaries, implementation selection, and behavior history. Non-evaluated autonomous participants must -have the existing `green` role. +have the existing `green` role. This is the normative authoring and realization +path for ACT-605 baseline behavior profiles. ### 2. Separate evaluation authority @@ -244,3 +250,4 @@ evidence does not pass. | 2026-07-24 | #861 | Required exact action provenance and capability-specific atomic participant batching. | | 2026-07-26 | #897 | Kept v1 stable and governed richer within-run timing, weighted selection, lifecycle state, and provenance as a versioned autonomous-execution profile. | | 2026-07-26 | #898 | Added exact native action-to-target bindings, bounded concurrent execution, generation-fenced lifecycle control, typed service readback, and conditional live conformance. | +| 2026-08-01 | #213 | Identified the existing autonomous-participant policy and execution path as the implementation of ACT-605 baseline behavior profiles. | diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 9ae3eb805..4595da705 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -248,11 +248,14 @@ adrs: summary: "Replaced product-category completeness profiles with composable provider-neutral capabilities and retained legacy product/category content fields as deprecated compatible input." - id: ADR-050 path: docs/decisions/adrs/adr-050-forwarding-agent-runtime-inventory.md - pin: 8eaecd0c498353822c5fec91f83fa6af6b7a7bb32451025f97661673cc1b832c + pin: 00f8711cf54877551ad990c4f6f17cc4f355ecaf74e8a1282cb3e56eb0877dc5 amendments: - date: 2026-05-31 ref: e815f27 summary: "Added scenario-level forwarding agents (top-level `Scenario.forwarding_agents`) with cross-registry ref resolution and uniqueness." + - date: 2026-08-01 + ref: "#1043" + summary: "Defined corroborated inventory realization, closed ownership roles, and evidence-plane binding for forwarding agents." - id: ADR-051 path: docs/decisions/adrs/adr-051-orchestration-authority-runtime-inventory.md pin: 080e29f079007f86d1be96f5bf94a29d44cf138778b0adf43ddefa16bafdd099 @@ -288,7 +291,7 @@ adrs: summary: "ADR-074 realizes the explicit draft/authoring surface anticipated in the Risks section as experiment-authoring-input-v1; the archival contracts are unchanged." - id: ADR-056 path: docs/decisions/adrs/adr-056-runtime-observed-values-and-credential-posture.md - pin: c2ff94d295a9b22dc72d8654a3fcd6bf9bb3fb4f5e4ffc411341c3e3ab164617 + pin: fd042c405c3bf9994d998b827e817c86f0ff35245b5a071f9fe86754d281221b amendments: - date: 2026-07-13 ref: "#417" @@ -296,6 +299,9 @@ adrs: - date: 2026-07-29 ref: "#956" summary: "Removed the obsolete platform-kind profile-guard claim while retaining family-local capability/category/content semantics and shared explicit-redaction handling." + - date: 2026-08-01 + ref: "#1043" + summary: "Applied the raw-value omission boundary to forwarding-agent realization observation disclosures." - id: ADR-057 path: docs/decisions/adrs/adr-057-runtime-secret-name-classifier-boundaries.md pin: 44c638bf47df8545b8af8a3850476fabc46a4ea42b3a8003b4f9ea1e89f72f49 @@ -322,11 +328,14 @@ adrs: pin: 69f90581b2bcedc12bfb7ed8aa688787d7a0d9aa4cc49a90f8c631ecbae8a356 - id: ADR-066 path: docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md - pin: c2a8cf0bafdf53007df8a6c40f60bd63114656887b3c2feaca6f678ef6ae6d65 + pin: 5d061a0667b2505d311803adf94e507781c4ba70550892bbfbe50cc82c4120ac amendments: - date: 2026-06-23 ref: "#335" summary: "Recorded SEM-225 run-level augmentation disclosure implementation coverage." + - date: 2026-08-01 + ref: "#1043" + summary: "Separated forwarding-agent ownership, realization corroboration, evidence binding, and operational behavior claims." - id: ADR-068 path: docs/decisions/adrs/adr-068-experiment-trials-replication-and-replay-claims.md pin: 12e6d644f7a90d50956964d8f0608737f9b4d8c7627819419a69974184c2781f @@ -436,7 +445,7 @@ adrs: pin: c2a0e6ac9fb87aa10fbb571b0f70efe0b99520b806f6ffe706f1556b0ec7e84b - id: ADR-092 path: docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md - pin: adf8a99f99a941a2a24b781df017f03e155b601ded2ecd170dd605549456521b + pin: 3ef5777be2b8d10e7b1e1a8ff55dc999bc7254210aa8800b317c9f87165e3fea amendments: - date: 2026-07-24 ref: "#861" @@ -447,6 +456,9 @@ adrs: - date: 2026-07-26 ref: "#898" summary: "Added exact native action-to-target bindings, bounded concurrent execution, generation-fenced lifecycle control, typed service readback, and conditional live conformance." + - date: 2026-08-01 + ref: "#213" + summary: "Identified the existing autonomous-participant policy and execution path as the implementation of ACT-605 baseline behavior profiles." - id: ADR-094 path: docs/decisions/adrs/adr-094-authoritative-cross-plane-experiment-bindings.md pin: 90578754323795ca8775c47dd6095752ccb5a769a0f932ebe60abbe1d8954246 diff --git a/docs/decisions/issue-1001-sem-233-boundary-flow-semantics-preflight.md b/docs/decisions/issue-1001-sem-233-boundary-flow-semantics-preflight.md new file mode 100644 index 000000000..7de77332e --- /dev/null +++ b/docs/decisions/issue-1001-sem-233-boundary-flow-semantics-preflight.md @@ -0,0 +1,415 @@ +# Issue #1001 — SEM-233 Boundary-Flow Semantics Architecture Preflight + +Date: 2026-08-01 + +Issue: #1001. + +Primary requirement: `SEM-233`. + +Reused requirements: `SEM-230`, `ACT-617`, `API-409`, and `API-423`. + +This note records architecture guardrails for publishing the first revisioned +SEM-233 semantic authority. It is guidance only. It does not publish the +profile, label algebra, carrier contract, schema, runtime resolver, final-sink +enforcement, backend capability, conformance result, or behavioral claim. + +## Decisive Current-State Finding + +Issue #1001 must sharpen the semantic authority already selected by ADR-101; +it must not start a second information-flow subsystem. + +- The requirement payload supplied to preflight is active `API-423`, an + already delivered crossing-contract dependency, while issue #1001 is the + first delivery for DRAFT `SEM-233`. Preserve API-423's existing contract, + schema, validation, migration, and traceability ownership; do not use it as + the ownership UID for #1001. Because branch + `1001-boundary-flow-semantics` contains no requirement UID, repository + workflow commands use `RAES_REQUIREMENT_UID=SEM-233`. +- ADR-101 already fixes independent confidentiality and integrity, conservative + derivation, distinct authority operations, typed carriage, and the last + RAES-controlled sink. No new ADR is justified unless implementation discovers + a conflict with that accepted decision. +- `specs/formal/participant-semantics/adversarial-flow-control.md` is the + focused design authority, but its current phrases “least upper bound” and + “conservative influence union” do not yet define an exact carrier, order, + join, source default, or scoped release transition. That is the design gap + #1001 closes. +- API-423 already owns crossing stages, exact policy/cut references, + transformation identity, marking preservation, declassification basis, and + cross-record validation. Runtime facts already own typed sources, scope, + sensitivity, freshness, provenance, and action-input sinks. Neither is the + complete SEM-233 two-coordinate label algebra. +- The behavioral catalog already owns `policy-noninterference` and the + `participant-information-flow-policy` claim surface. SEM-233 parameterizes + that existing relation; it does not justify an + `adversarial-noninterference`, `flow-safe`, or similarly overlapping relation. +- Portable flow-policy and carrier DTOs are owned by #1002. Final-sink runtime + enforcement and persistence changes are owned by #1003. Issue #1001 may use + a test-local finite model as bounded falsification evidence, but it must not + add production contracts or runtime code in anticipation of those issues. + +The smallest coherent delivery is therefore one exact `sem-233/rev1` formal +profile, a typed mapping to incumbent carrier identities, a narrowly evolved +behavioral claim surface, lineage/assurance bookkeeping, and bounded synthetic +counterexamples. ADR-101, SEM-230, and the existing runtime boundaries remain +the owners of their current concepts. + +## Architecture Decisions And Guardrails + +### Publish an exact formal profile without publishing a wire contract + +The focused SEM-233 specification is the authority for +`participant-boundary-flow-policy-v1@rev1`. It must name its authority revision +separately from the profile revision and distinguish these statuses: + +- the formal definition is published; +- portable contract and schema support are not implemented; +- runtime and backend enforcement are not implemented; +- finite counterexamples are bounded tests, not a model check or proof; and +- `SEM-233` remains DRAFT while its downstream positive obligations remain + open. + +The profile is not a GOV-920 `SemanticProfileModel`: that incumbent describes +phase-wise contract, concept-family, binding, and behavior compatibility. It is +also not a `BehavioralRelationProfileModel`: that incumbent closes the +parameters of a behavioral claim or analysis. A future claim profile may +reference the SEM-233 flow-policy profile, but claim configuration must not +become the operative flow policy. Issue #1001 must not add either portable +profile variant; #1002 owns that contract decision. + +### Require one exact two-coordinate algebra + +The formal profile must select one exact, non-scalar revision-1 algebra. This +preflight deliberately does not choose its carrier representation. The +normative definition must close, for each independent coordinate: + +- the carrier domain and equality/normalization rule; +- the partial-order direction and the meaning of incomparable values; +- the join and every extremal or explicitly absent extremal value; +- unknown-source, missing-label, conflict, and unsupported behavior; +- the sink-satisfaction predicate; and +- the authority-bounded operation that may change that coordinate on a fresh + result. + +The definition must state and exercise the algebraic laws required for stable +composition: closure, associativity, commutativity, idempotence, monotonicity, +and traversal-order independence. It must make mechanically clear that adding +a confidentiality restriction or possible integrity influence cannot widen a +release. An unresolved source, label, authority, profile revision, or join +must yield the profile's deny-equivalent result or a typed unsupported result; +it never becomes empty, public, or trusted by default. + +Authentication, signatures, hashes, sensitivity, markings, confidence, and +monitor scores may be evidence used by a resolver; none is either coordinate +by itself. The integrity value coordinate records conservative possible +influence, while a sink requirement is a predicate over that value; those are +not two meanings to collapse into one trust score. The chosen revision-1 +domain must preserve mutually distrustful and incomparable principals or +audiences rather than assuming one global classification ladder. + +### Keep provenance, label state, and authority transitions separate + +For a derivation `d`, the conservative default is: + +```text +L_phi(d) = join_phi { L_phi(x) | x may influence d } +``` + +The possible-input set includes participant context and retained memory plus +the participant, tool, service, transformation, monitor, or apparatus source +when that source can influence the result. Copying, summarizing, redacting, +prompting, parsing, editing, or changing participant/controller does not remove +an input. A typed transformation may exclude an influence only through a +closed, revisioned non-influence relation with evidence; absence from a +caller-supplied list is not proof. + +Provenance remains an immutable graph of source and derivation refs. It is +evidence for label resolution and replay, not authorization. Labels are the +policy obligations derived from that graph at a named profile/cut. Do not put +the graph inside the label or treat possession of provenance as permission. + +Declassification and integrity endorsement are coordinate-specific rewrites: + +- declassification may remove or replace only named confidentiality + obligations; +- endorsement may replace only named integrity obligations while retaining + their immutable influence refs; it changes the sink-admission predicate, not + the recorded possible-writer history; +- each operation binds source and fresh result identities, unchanged + coordinate, exact obligation delta, destination/sink, authority basis, + profile/revision, decision/cut, predecessors, and safe evidence; and +- neither mutates the source label or provenance history. + +Redaction and ordinary transformation retain both coordinates unless their +governed relation says otherwise. Approval, authentication, authorization, +admission, handoff, and trusted editing imply neither rewrite. Trusted editing +creates a new result and re-enters ordinary validation and policy gates. + +### Reuse the exact-cut crossing decision instead of adding a policy engine + +Use a final predicate named distinctly from declassification, for example: + +```text +MayFlowAtSink_phi(x, sink, destination, cut) = + ConfidentialityObligationsSatisfied(...) + and IntegrityObligationsSatisfied(...) + and existing caller/target/participant authority gates + and existing action admission + and existing API-407 effective capability gate + and existing API-423 transformation/projection gates + and FreshExpectedHistoryHeads(cut) +``` + +Every conjunct is deny-first. “Release” must not ambiguously mean both the +declassification operation and this final composite decision. The SEM-230 +participant/audience projection and API-423 `MayCross` relation remain +incumbents; SEM-233 adds the two obligation checks at the same exact cut. + +Policy changes never reinterpret a historical decision. Source labels, +derivations, declassification/endorsement operations, and decisions retain +their original profile, policy, and cut refs. Reuse under a later policy or +episode requires a fresh sink decision. Episode reset, handoff, controller +change, participant change, replay, or snapshot reload never clears labels or +provenance. + +### Map semantics to incumbent carriers by reference + +Issue #1001 must publish a complete carrier/derivation table but must not edit +these contracts. The mapping is semantic and reference-based: + +| Flow stage | Canonical incumbent | SEM-233 mapping boundary | +| --- | --- | --- | +| observations, tool results, retrieved values, derived facts | `ParticipantObservationEnvelopeModel`; runtime-fact declaration/version models and `RuntimeFactBindingPlane` | source and derivation refs resolve to an effective label; `RuntimeFactSensitivity` is only one input to that resolution | +| participant context, information state, and memory | participant context/history/information-state/episode carriers and SEM-230 memory scope | every retained or replayed value preserves source, label-profile, derivation, and release-history refs | +| proposals and action arguments | participant decision surfaces, `ParticipantActionAdmissionRequest`, action argument definitions, and runtime-fact sinks | structural/action admission composes with label resolution; it does not duplicate it or imply flow permission | +| control and handoff | API-409 `ParticipantControlOccurrenceModel` and contextual validator | controller/authority changes remain separate from declassification, endorsement, and action admission | +| crossing, transformation, and disclosure | API-423 `ParticipantCrossingOccurrenceModel`, typed subjects, predecessor stages, and contextual validator | later contracts reference label/derivation decisions; they do not copy source payloads or invent a second crossing history | +| output, delivery, observation, and errors | API-423 disclosure/delivery/observation stages and participant-facing views | every participant/external serialization is a sink; disclosure, delivery, and observation remain distinct | +| persistence and replay | `RuntimeSnapshot`, participant histories, operation/idempotency records, and expected heads | downstream enforcement uses the existing atomic transition; #1001 makes no persistence change | + +The mapping must include cross-participant handoff, shared/joint state, and +cross-episode replay examples. It must name which values can influence each +derived carrier, including error branches and destination/tool arguments. An +open `taint`, `security_labels`, `context`, `metadata`, or backend-options bag +is not a typed mapping. + +### Evolve the existing claim authority without strengthening assurance + +`policy-noninterference` remains the relation. The SEM-233 profile supplies a +revisioned classification/flow-policy parameter to SEM-230 low equivalence, +dynamic purge, declassification schedule, projection, and adaptive-strategy +quantification. It does not replace those definitions or make a separate +behavioral relation. + +If #1001 changes the current catalog, it must evolve it once from current +`rev11`: + +- preserve byte-for-byte `rev11` under + `contracts/concept-authority/history/` before advancing the current file; +- register `rev11` in `_HISTORICAL_CATALOG_PATHS` so stored bindings remain + resolvable; +- update the existing `participant-information-flow-policy` claim surface + rather than creating an overlapping surface; +- keep the catalog schema at `behavioral-relations/v1` when its closed shape is + unchanged; and +- move every current revision literal, fixture, claim producer, publication + entry/hash, and catalog test together. + +The catalog must not report SEM-233 runtime enforcement, backend declaration, +realization, conformance, model checking, or proof. Existing SEM-230 evidence +does not implement the stronger SEM-233 label propagation. A #1001 finite model +is test-local bounded falsification evidence and must bind finite scope and +explicit nonclaims; a successful enumeration is not universal +noninterference, intentional-subversion robustness, or backend behavior. + +## Canonical Incumbents To Reuse + +| Concern | Canonical incumbent and required use | +| --- | --- | +| participant-relative policy and relation | ADR-085/095 and `information-flow-control.md`: reuse exact cuts, projection, hiding, memory, adaptive strategies, dynamic purge, declassification, order, and `policy-noninterference` | +| SEM-233 design decision | ADR-101 and the existing adversarial-control research record; amend the focused formal spec, not the accepted ADR, unless a real decision conflict appears | +| action and admission | SEM-211, participant decision surfaces, `ParticipantActionAdmissionRequest`, action argument definitions, and incumbent admission validators | +| runtime facts and sinks | runtime-fact declaration/version/sink/binding models, `RuntimeFactBindingPlane`, `validate_binding()`, dispatch, secret references, freshness, and projection redaction | +| control and handoff | ACT-617, API-409 participant-control occurrence models, contextual validation, controller/authority binding, and control history | +| crossings | API-423 models/vocabulary/context validator, `ParticipantCrossingIntent`, `ParticipantCrossingPolicyResolver`, exact policy refs, fresh transformation identity, and crossing history | +| capability | API-407 manifests/profiles, required-contract maps, and `resolve_participant_feature_support()`; semantic definition is not a capability declaration | +| persistence | `RuntimeSnapshot`, `ControlPlaneStore.commit_participant_transition()`, `LocalControlPlaneStore`, operation records, semantic fingerprints, idempotency, expected heads, and `AuditEvent` | +| claims | `behavioral-relations-v1.json`, historical catalogs, `BehavioralRelationCatalogModel`, `BehavioralClaimBindingModel`, `validate_behavioral_claim_binding()`, and `check_behavioral_relation_claims.py` | +| validation and diagnostics | `ContractModel(extra="forbid")`, contextual validators, `Diagnostic`, `Severity`, operation envelopes, sanitized failures, and the generic redacted HTTP 500 response | +| formal assurance | ADR-007/018, FM3 participant-semantics fulfillment, the SEM-230 test-local model pattern, and bounded counterexample/nonclaim discipline | +| lineage and workflow | SDL lineage ledger/model/checker, source audit, `.ground-control.yaml`, `.gc/plan-rules.md`, canonical nox/policy/verification commands, and requirement traceability | + +No new relation registry, semantic-profile family, policy engine, source or +sink registry, action/crossing hierarchy, validator stack, exception hierarchy, +logger, audit stream, store, workflow script, or issue-local schema authority +is justified. + +## Cross-Cutting Layers And Security Posture + +Issue #1001 is definition-only, so it traverses repository authority and +claim-validation layers, not live transport or execution. That distinction is +part of the assurance boundary. + +1. **Formal authority and revision gate.** The focused spec names + `sem-233/rev1`, the flow-profile id/revision, all algebraic domains and + laws, exact-cut semantics, carrier mappings, limitations, and nonclaims. + ADR-101 remains the decision owner and SEM-230 remains the relation owner. +2. **Concept/catalog shape gate.** Any behavioral catalog change validates + through the closed `BehavioralRelationCatalogModel`, exact revision loader, + publication fixture/entry, JSON artifact checks, concept-authority checks, + and historical-revision tests. Free-form prose cannot substitute for a + governed claim surface. +3. **Claim-strength gate.** `validate_behavioral_claim_binding()` and + `tools/check_behavioral_relation_claims.py` continue to require exact + taxonomy/relation/projection/evidence boundaries. Definition and bounded + examples cannot advance implementation, runtime, backend, model-check, or + proof axes. +4. **Formal-assurance gate.** Participant semantics remains FM3. The invariant + list, test-local bounded model, unit/property counterexamples, abstract + transition semantics, and existing typed-contract waiver remain honest. + Issue #1001 must not claim that prose or a test dataclass closes #1002's + typed-contract obligation. +5. **Lineage and governance gate.** New normative derivations update the + existing lineage authority and source audit; explanatory repetition does + not. Repository policy, requirement governance, assurance, concept, + lineage, JSON, documentation, and full verification stay in the canonical + workflow. `SEM-233`, not the supplied API-423 payload, is the workflow UID. +6. **Secret-handling gate.** Formal examples and tests use synthetic bounded + values and safe refs only. No raw confidential value, prompt, credential, + private state, hidden objective, policy body, rejected input, or secret- + derived digest enters docs, fixtures, diagnostics, or test output. Hashing a + secret does not make it safe evidence. +7. **Config and environment gate.** The semantic delivery adds no runtime + configuration, environment variable, secret loader, feature flag, CLI + option, endpoint, or caller-selectable profile root. The only environment + binding is the existing workflow UID. Policy/profile content never comes + from environment or open metadata. +8. **OS/process exposure gate.** The bounded semantic model is in-process and + deterministic. It adds no network access, subprocess, socket, daemon, + sidecar, host path, temporary secret file, or privileged operation. Labels, + values, policies, and witnesses do not enter argv, environment, filenames, + stdout/stderr, or host logs. +9. **Exception, logging, and error-envelope gate.** Test-local invalid cases use + assertions or incumbent validation failures. Do not add a SEM-233 exception + hierarchy or logger. Because #1001 exposes no API, it does not traverse HTTP + envelopes; consequently it makes no error-redaction claim. #1003 must reuse + stable `Diagnostic` codes, safe `AuditEvent` fields, operation envelopes, + and `{\"detail\":\"internal server error\"}` for unexpected HTTP failures. +10. **Auth, final-sink, and persistence boundary.** #1001 does not traverse + `ControlPlaneSecurityConfig.strict_defaults()`, bearer/proxy identity, + target/role/participant/controller/audience binding, request-size guards, + `RuntimeControlPlane`, `RuntimeTarget`, capability admission, atomic commit, + or replay. The formal predicate must name every one as a conjunct or + downstream realization obligation, but must not claim it has passed them. + +## Whole-Repository Surfaces In Scope + +- **Normative semantics:** ADR-101, SEM-230, the focused SEM-233 formal spec, + participant-semantics README, formal-assurance fulfillment, and any clause + mapping derived from them. +- **Concept and claims:** current and historical behavioral catalogs, the + `participant-information-flow-policy` claim surface, catalog loader and + validators, claim bindings, publication metadata, and claim-policy tests. +- **Lineage:** the existing SDL lineage explanation, machine-readable lineage + ledger, primary-source audit, and their checkers. The currently edited + lineage/research files remain independent working-tree content and must not + be overwritten by the #1001 implementation. +- **Bounded evidence:** one test-local SEM-233 model and focused algebra, + missing-label, laundering, stale-cut, cross-participant, cross-episode, and + release-operation-conflation counterexamples. Production packages are out of + scope. +- **Incumbent carrier inventory:** action/admission, observation, context, + memory, runtime-fact, control/handoff, crossing, transformation, delivery, + snapshot/history, capability, diagnostics, audit, and operation surfaces are + mapped but unchanged. +- **Verification:** policy, requirement governance, concept authority, + behavioral claims, lineage, assurance policy, JSON/schema publication when + applicable, focused tests, docs, and `tools/verify_all.py`. + +## Extensibility Seam + +The semantic seam is the tuple: + +```text +(profile_ref, + profile_revision, + source_label_resolver_ref, + derivation_rule_ref, + sink_policy_ref, + authority_resolver_ref, + participant, + audience, + direction, + interaction_kind, + destination, + sink_class, + exact_cut) +``` + +The profile owns the confidentiality/integrity obligation domains, order, +join, unknown defaults, release/endorsement rewrite rules, and sink predicates. +Resolvers bind existing carrier/source identities to that profile. Existing +carriers later reference label, derivation, and decision identities rather than +embedding full policies or acquiring apparatus-specific branches. + +This seam permits the next source kind, sink class, owner policy, participant, +apparatus, backend, or episode scope to add a revisioned resolver/profile rule +without editing every carrier. A third independent policy dimension would be a +new profile revision and algebraic decision, not an overloaded integrity or +confidentiality field. Timed, probabilistic, quantitative leakage, covert-flow, +or stronger behavioral properties require their own governed relation/profile +decision; configuration cannot disguise a changed theorem. + +## Gotchas And Anti-Patterns + +Avoid: + +- implementing #1001 under `API-423`, changing API-423's active contract + authority, or treating its supplied traceability list as SEM-233 completion; +- publishing `sem-233/rev1` while leaving the actual order, join, equality, + unknown element, or release rewrite implicit; +- a single ordered security level, `trusted` flag, sensitivity, marking, + confidence, signature, checksum, role, or monitor score standing in for both + coordinates; +- treating source authenticity, content integrity, provenance availability, + action authorization, confidentiality, and admitted origin trust as the same + concept; +- letting empty labels, absent provenance, an unknown source, unresolved + profile, missing authority, or ambiguous join mean public or trusted; +- omitting participant context, retained memory, tool arguments, destination, + error output, monitor inputs, transformation apparatus, shared state, + handoff, replay, or episode history from the possible-influence set; +- treating redaction, sanitization, summarization, projection, parsing, + trusted editing, approval, or handoff as automatic declassification or + endorsement; +- removing provenance when endorsement changes the admitted integrity + coordinate, or mutating a historical label after policy change; +- overloading API-423 declassification to represent endorsement, or adding + caller-authored policy/cut/label fields to headers, query parameters, + metadata, or backend options; +- creating a new relation or claim surface beside + `policy-noninterference`/`participant-information-flow-policy`; +- advancing the behavioral catalog without archiving/resolving rev11 and + moving all live producers and fixtures together; +- reporting bounded algebra tests as universal noninterference, model checking, + proof, final-sink enforcement, backend realization, covert-channel control, + monitor honesty, or intentional-subversion robustness; and +- implementing #1002 contracts, #1003 runtime/store changes, #1004 backend + capability, or #1007 adversarial evaluation as “helpful” work in #1001. + +## Non-Goals And Implementation Boundaries + +- No portable DTO, JSON Schema, public API, SDL syntax, config surface, or + runtime policy loader. +- No final-sink enforcement, external call, participant delivery, streaming, + persistence, idempotency, audit, or error-envelope change. +- No backend declaration, apparatus integration, quarantine mechanism, + monitor service, gateway, prompt policy, agent framework, or trajectory + store. +- No replacement or modification of API-409/API-423, runtime-fact, action, + observation, history, snapshot, experiment, evidence, or conformance carrier + ownership. +- No model alignment, chain-of-thought or private-state safety, monitor honesty, + covert-channel protection, intentional-subversion robustness, universal + noninterference, bisimulation, opacity, model-check, proof, runtime + realization, or backend-conformance claim. diff --git a/docs/decisions/issue-1043-forwarding-agent-realization-posture-preflight.md b/docs/decisions/issue-1043-forwarding-agent-realization-posture-preflight.md new file mode 100644 index 000000000..062f7e0c0 --- /dev/null +++ b/docs/decisions/issue-1043-forwarding-agent-realization-posture-preflight.md @@ -0,0 +1,147 @@ +# Issue 1043: Forwarding-Agent Realization and Posture Preflight + +## Decision guidance + +`runtime.forwarding_agents` remains a non-executable, typed runtime inventory. +For SEM-218, realizing that inventory means materializing and independently +corroborating the agent's **identity, placement, implementation, and declared +configuration projection**. It does not mean proving that a source is currently +shipping, that an IOC transform ran, or that a reload was consumed. + +Consequently, an exact forwarding-agent declaration is realized exactly when +the canonical `forwarding-agents` configuration projection agrees with the +backend's independently obtained readback at the declared verification scope. +A digest-locked sidecar or installed unit may therefore satisfy the concern; +the reference backend must not report it absent merely because it does not run +the forwarding workload. Conversely, a planned payload echoed as an observation +is not readback and cannot discharge the gate. + +## Keep three claims separate + +| Claim | Owner / canonical carrier | Meaning | +| --- | --- | --- | +| Agent inventory and configuration | ADR-050 model, SEM-218 `forwarding-agents` descriptor and projection | The agent and its declared configuration are present as declared. | +| Corroboration capability and result | `RealizationSupportDeclaration`, the backend-return gate, and a concern-keyed observation profile | What independent evidence supports the inventory/configuration claim. | +| Operational forwarding posture | ADR-079 proposition/assertion, probe binding, truth result, and evidence record | Whether the agent actually forwards, fails to forward, routes to an intended/different destination, or produces a rule/reload effect during a governed window. | + +Do not use any one of these carriers as a substitute for another. In +particular, a configured `ship_target` is not evidence of delivery, and a +failed forwarding proposition does not make an intentionally configured agent +unrealized. + +The existing `ObservationStrength` in the realization-envelope carrier remains +the provenance/source axis (`driver-reported`, `daemon-observed`, or +`guest-observed`). It must not be relabelled as `presence`, `configuration`, or +`behavior`: those are the scope of the claim. Extend the existing realization +support/disclosure path with a closed, concern-keyed **verification scope** +(`presence`, `configuration`; `behavior` is reserved for a future concern that +actually owns behavior), paired with the existing strength. An exact inventory +requirement may only be accepted when its declared required scope is met by the +backend's independently observed scope. This is a capability/observation +qualification on the existing SEM-218 descriptor and manifest declaration, not +a second realization taxonomy, envelope dialect, or a new `RealizationConcern` +value in ADR-070's carrier. + +The initial forwarding-agent exact projection must be the present +configuration projection already owned by `project_forwarding_agents`, with +the existing keyed ordering, annotation removal, secret-safe setting +commitments, and `validate_forwarding_agents_observation` boundary. It must +not add a `forwarding`, `healthy`, `delivery_status`, `last_shipped`, or +runtime-log field to that projection. + +## Endogenous and exogenous agents + +Add a closed authored role on a forwarding-agent inventory entry: + +- `system_under_test`: the agent is scenario state; its configuration may be a + variation target and its operational posture may be an observed proposition. +- `measurement_apparatus`: the agent exists to collect experiment evidence. It + is not a scenario factor or a substitute for a scenario-native observation. + +The role is an ownership classification, not an execution mode and not a +visibility claim. A measurement-apparatus entry that is environment-visible or +comparability-relevant must also be disclosed through the existing +`ExperimentAugmentationDisclosureModel` on the run, using its existing +classification, carrier references, affected references, evidence references, +and marking requirements. `apparatus_only` continues to use the established +operational-observability boundary. + +The evidence plane is the authoritative direction for the binding. An +`EvidenceRequirement` (and, when executable, its capture specification) names +the forwarding-agent qualified reference as its `source_refs`, with its +channel, artifact role, sensitivity, redaction, integrity, retention, and loss +requirements. It describes the abstract evidence destination; no forwarding +agent field may contain a host path, evidence-pack URI, storage credential, or +capture payload. Semantic validation must require at least one such inbound +evidence binding for `measurement_apparatus` and reject a binding that targets +an agent marked `system_under_test` as measurement apparatus. Keep this +relation single-directional: do not duplicate evidence requirement references +on the agent. + +This is the first application of an agent-role pattern, not justification for a +universal runtime-agent base model. A future EDR or comparable family should +reuse the same closed role vocabulary, evidence-direction rule, augmentation +disclosure, and proposition boundary only after it has the same semantics. + +## Required gates and invariants + +- Preserve `RuntimeForwardingAgent` profile validation, stable ids, target + resolution, the closed enrollment posture, and ADR-056 secret-value + redaction. Role and evidence binding must use `SDLModel`, `extra="forbid"`, + existing enum/variable parsing, and `SemanticValidator`; compiler code must + not parse or revalidate raw YAML. +- Preserve `raes.explicitness`, realization designation, instantiation + provenance, and concrete revalidation. Exactness remains aggregate + configuration exactness; it is never inferred from serialization or a + backend snapshot. +- Extend only `raes_processor.semantics.realization_concerns` and its one + descriptor/projection/observation-validator route. Compilation, envelope + admission, backend capability matching, `realization_disclosure()`, and + `_call_backend_apply()` must consume that route. A non-matching or + under-observed exact declaration fails closed as + `runtime.backend-contract-invalid` before snapshot persistence. +- Reuse `RealizationSupportDeclaration` / `RealizationSupportDeclarationModel` + and the backend-manifest-v2 concept/schema/fixture path for observation-scope + support. Do not add per-backend booleans, a forwarding-specific manifest, or + a second capability registry. +- Reuse the existing snapshot serializers and + `RuntimeSnapshotEnvelopeModel`; validate every backend-returned forwarding + observation before storage or API conversion. `RealizationProvenanceEntry` + still records no realized value and must not become the observation carrier. +- A behavioral probe must be a closed typed proposition and capability-bound + probe binding under ADR-079. It requires governed evidence and reports + `true`, `false`, `unknown`, or `unsupported`; lack of a delivery observation + is `unknown`, not `false`. It must not be an arbitrary command, healthcheck, + manager-log heuristic, or a backdoor field in the realization snapshot. + +## Security and exposure + +The change passes the existing parser/model, semantic-reference, +instantiation, manifest/configuration, backend-return, persistence/schema, +control-plane authorization, diagnostic, audit, and OS/process-exposure gates. +No raw forwarding setting, enrollment material, evidence-store credential, +native inspect object, probe payload, or evidence location may enter a +snapshot, provenance entry, diagnostic, audit event, fixture, exception, or +process argv. Continue using safe commitments for permitted setting equality, +the existing redacted error envelope, authenticated snapshot reads, role checks, +request limits, audit recording, and backend fixed-argv/no-shell execution. + +## Explicit non-goals + +- Implementing shipping, capture storage, rule generation, reload execution, + liveness, or delivery monitoring. +- Treating image attestation or configuration readback as behavioral proof. +- Redesigning ADR-070 realization-envelope concerns, ADR-064 evidence records, + ADR-066 planes, ADR-079 truth algebra, or service materialization. +- Adding a universal runtime-agent schema, evidence-path fields, secret + resolver, new endpoint, sidecar persistence store, exception hierarchy, or + backend-specific forwarding dialect. + +## Follow-on ADR handling + +Before implementation changes public SDL/contracts, promote these choices by +an ADR-050 amendment plus any necessary ADR-056/ADR-066/SEM-218 amendment, +recording ADR-059 amendment rows and `adr-index.yaml` pins in the same change. +The implementation must update model/schema parity, publication governance, +fixtures, semantic and conformance tests together; this preflight adds none of +those executable artifacts. diff --git a/docs/decisions/issue-212-act-604-dynamic-knowledge-environment-state-preflight.md b/docs/decisions/issue-212-act-604-dynamic-knowledge-environment-state-preflight.md new file mode 100644 index 000000000..55f751e82 --- /dev/null +++ b/docs/decisions/issue-212-act-604-dynamic-knowledge-environment-state-preflight.md @@ -0,0 +1,346 @@ +# Issue #212 — ACT-604 Dynamic Knowledge And Environment-State Preflight + +Date: 2026-07-31 + +Issue: #212. + +Requirement: ACT-604. + +This note records the architecture boundary for closing ACT-604 against the +current repository. It is implementation guidance only: it does not add a +contract, change SDL, modify runtime state, claim backend support, define the +ACT-615 holdings taxonomy, or implement reconstruction. + +## Finding + +The current architecture satisfies ACT-604's authored-state, observation, +participant-history, derived-context, shared-state, information-flow, and +exact-cut requirements. It does not satisfy ACT-604 end to end. + +`participant-observation-envelope-v1` and +`participant-decision-surface-v2` carry `information_state_ref` and related +history/reconstruction/proof refs. The formal participant-runtime authority +defines exact-cut reconstruction and guarantee obligations. No published +portable information-state record or governed reconstruction profile resolves +those refs, and the current structural validators do not enforce the +conditions attached to strong information guarantees. A clause-only +conformance profile would therefore preserve dangling references rather than +close them. + +The smallest RAES-native closure is: + +- one immutable, participant-relative + `participant-information-state-record-v1` at an exact episode/state cut; +- one closed `participant-information-reconstruction-profile-v1` contract, + whose immutable named profile corpus realizes the formal reconstruction + registry without creating a plugin mechanism; +- one contextual join validator over those records and the existing + occurrence-history, observation, decision-surface, projection, evidence, + and profile authorities; and +- first-class append-only runtime carriage for the record, using the existing + snapshot, atomic transition, persistence, retrieval, capability, and + conformance paths. + +No new ADR is warranted. Accepted ADR-022, ADR-054, ADR-083, ADR-085, and +ADR-095 already decide the world/view/history/evidence separation, runtime +guarantee model, participant decision surface, governed information crossing, +and exact-cut/memory-scope semantics. This note fixes the missing contract +binding under those authorities. + +## Concept And Ownership Boundaries + +| Concept | Canonical owner | ACT-604 use | +| --- | --- | --- | +| Authored initial framing | `agents.*.initial_knowledge`, `starting_accounts`, and `starting_assertions` plus existing SDL validators/compiler addresses | Source refs only; never dynamic truth or runtime evidence | +| Hidden world/backend/evaluator truth | ADR-022 participant semantics and authorized evidence | Never serialized as participant information merely because a backend can inspect it | +| Participant-visible occurrence | `participant-observation-envelope-v1`, behavior history, observation boundaries, and action-result transitions | Occurrence-preserving reconstruction input | +| Derived participant context or belief support | `participant-context-view-v1` and its governed transformations | Derived view, not a second fact store and not truth promotion | +| Shared operational state | `participant-shared-state-record-v1` and RUN-307 history/concurrency rules | A participant may know only an authorized projection or disclosure of a record | +| Participant decision cut | `participant-decision-surface-v2` sequence/causal cut and ADR-095 memory scope | Reused exactly; no second cut vocabulary | +| Crossing and audience policy | ADR-085, SEM-230, RUN-319 crossing policy/evidence, and API-408 retrieval | Governs participant-facing release and records it before return | +| Archival evidence and provenance | Existing evidence refs, provenance refs, markings, redaction, and audit records | Supports claims without becoming participant-visible state | +| Operational holdings kinds and lifecycle | ACT-615 | Typed refs only; ACT-604 does not define credentials, sessions, footholds, privileges, alerts, workload, assets, or liabilities | + +An information-state record is a versioned claim about what one participant's +portable information state is at one exact cut under one declared projection, +memory scope, and guarantee. It is not a mutable knowledge map, a ledger of +free-form facts, a hidden-world snapshot, or an agent's private cognitive +implementation. + +The record may bind a canonical state digest and an authorized payload ref, +but it must not inline an untyped fact bag. Its sources are typed refs to the +existing carriers. Source classification follows the resolved target contract +and governed relation, not an untrusted `source_kind` label. Repeated, +contradictory, deceptive, redacted, superseded, or revoked occurrences remain +distinct in history; a final-value map cannot replace them. + +## Contract Guardrails + +The information-state record must reuse `ParticipantRuntimeBaseEnvelopeModel` +and the existing participant address, episode identity, evidence, provenance, +marking, authorization, visibility, redaction, and schema-version conventions. +Its semantic coordinates must include: + +- `information_state_ref` and a canonical digest or content-addressed payload + ref; +- participant and episode identity; +- the existing sequence-cut or causal-cut shape, without a wire-incompatible + copy; +- participant memory scope and reset authority where cross-episode history is + in scope; +- occurrence-history/source refs and the applicable projection, visibility, + redaction, and policy revisions; +- the existing `information_guarantee` vocabulary; +- reconstruction profile identity, algorithm identity/version, proof/evidence + refs, and disclosed loss or limitations as applicable; and +- predecessor/supersession identity sufficient for append-only history without + implying that persistence order is semantic delivery or causal order. + +The reconstruction profile contract is a static profile authority, not a +runtime envelope. Follow the existing random-stream profile pattern: a +published schema plus immutable named JSON profiles loaded through the +contract-corpus resolver. Each profile is closed and versioned, and binds the +formal registry key `(algorithm_id, algorithm_version, schema_version, +projection_version)`, determinism basis, accepted input/order semantics, +fixture format, proof-artifact format, and normative artifact ref/digest. + +Profiles must not contain Python import paths, shell commands, expressions, +URLs fetched at validation time, embedded credentials, or arbitrary callables. +Selection dispatches through closed trusted code. There is no `latest`, version +range, dynamic plugin discovery, or caller-selected executable. + +`history_consistent` and `perfect_recall` are strong assertions. A published +record carrying either value must resolve all required state, history, +projection, reconstruction-profile, and proof/evidence refs at exactly the +same participant, episode, cut, memory scope, and policy/revision coordinates. +The reconstructed digest must equal the record's claimed digest. +`perfect_recall` additionally requires the governed prefix/occurrence-identity +and order witness defined by ADR-054 and the formal runtime model. + +An unresolved profile, missing proof, mismatched coordinate, unauditable +algorithm, collapsed occurrence history, or failed reconstruction makes a +strong record invalid. A producer may publish a weaker truthful claim instead; +a validator must never silently rewrite or accept the false strong claim. +`observation_only`, `lossy_projection`, `unknown`, and `unsupported` retain +their existing distinct meanings. The weakest generally portable positive +claim is `observation_only`; a backend that cannot materialize even the +portable record declares the capability unsupported rather than synthesizing +an empty state. + +The observation-envelope structural/model validation must enforce the +field-level conditions associated with its selected guarantee. The contextual +validator then resolves refs and evaluates cross-record conditions once. Do +not duplicate the same joins in API DTOs, runtime services, and backend +adapters. A decision surface's `information_state_ref` must resolve to a record +with the same participant, episode, exact cut, projection/redaction revisions, +and memory scope. If a context view names an information state as a source, it +uses a closed source-layer value and ref; it does not copy the information-state +payload. + +## Runtime, Persistence, And Retrieval Boundary + +Information-state records belong in one first-class append-only +`RuntimeSnapshot` history. A list's physical order is persistence order only; +semantic order comes from the record's existing exact-cut coordinates and +visible occurrence relation. Records have stable unique refs and cannot be +deleted, rewritten, or replaced by an issue-local "current knowledge" map. + +Runtime integration must remain within the canonical snapshot authority: + +- `RuntimeSnapshot`, `_SNAPSHOT_UPDATE_KEYS`, `with_entries()`, + `RuntimeSnapshotEnvelopeModel`, `_snapshot_payload()`, and + `_snapshot_from_payload()` stay shape-compatible; +- the reserved-metadata-key checks include the first-class history so metadata + cannot smuggle a parallel state stack; +- `participant_result_contracts.py` performs the aggregate snapshot and + transition validation; +- `ControlPlaneStore.commit_participant_transition()` persists the state, + expected predecessor head, transition/evidence record, and audit outcome + atomically in both in-memory and local stores; and +- local-store restart arbitration counts the information-state history in + `_participant_transition_count()`. Omitting it can cause the loader to choose + a stale legacy snapshot after restart even when the new history committed. + +Do not append an information-state record through a separate repository call +after a decision, observation, or crossing has already committed. That creates +a crash window and an exact-cut claim with no atomic source state. Extend the +existing expected-head seam and commit protocol instead of adding another +lock, journal, repository, or transaction abstraction. + +Administrative/control-plane carriage uses the existing snapshot envelope and +`/snapshot` path. Participant-facing use should normally remain indirect: +decision surfaces and API-408 context/history/status views carry governed refs +or authorized derived projections. A new raw ACT-604 endpoint is not required +to satisfy the requirement. + +If direct participant-facing record retrieval is later justified, it is a new +closed RUN-319 carrier kind passed through +`ParticipantCrossingPolicyResolver`, audience-subject binding, markings, +projection/redaction, `serialize_participant_view()`, and commit-before-return. +It must fail closed when no governed resolver is configured; the legacy API-408 +fallback is not an authorization path for a new sensitive carrier. + +## Cross-Cutting Layers That Must Be Reused + +### Contract and validation authority + +- ADR-009/019 and `contracts/README.md`: hand-governed published schemas remain + normative. Pydantic models must reproduce them through `schema_bundle()`; + generated output is not a substitute authority. +- ADR-061 and `contracts/schema-publication/entries/`: schema publication, + hashes, compatibility classification, and `last_change` are updated through + the existing manifest path. +- `raes_contracts` strict models: closed enums, forbidden extra fields, + portable refs, exact shapes, and model/schema parity. Do not add an API-only + DTO with different semantics. +- `raes_conformance.conformance.validators._MODEL_VALIDATORS`: one structural + validator registration per new contract, plus a contextual validator for the + multi-record join. Use `sanitized_failure_message()` for untrusted validation + failures. +- Existing valid/invalid contract fixtures, backend conformance targets, and + invariant oracles: positive publication and each negative guarantee/ref/cut/ + audience path exercise production validators, not a test-only helper. + +`implementations/python/tests/sem230_information_flow_model.py` is a bounded +test model. It is not a production contract, reconstruction engine, or state +store and must not be promoted into one. + +### SDL and semantic compilation + +The existing `raes.agents` fields and the content-objective/proposition +validators own authored starting state. `SDLModel`, `SemanticValidator`, the +processor compiler, planning diagnostics, and canonical address resolution +remain the only SDL validation/compilation path. ACT-604 does not need new SDL +syntax. If a future authored binding is genuinely required, it must compile to +the same typed runtime refs and pass the existing parse, shape, semantic, +compile, and diagnostic layers; no second parser or validator is permitted. + +### Capability and backend honesty + +Add any reconstruction-support term to the existing participant-runtime +behavior-feature vocabulary and map it through +`PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS`, +`BACKEND_SUPPORTED_CONTRACT_IDS`, manifest validation, feature admission, and +backend conformance. Reuse the existing `exact`, `bounded`, `disclosed_weak`, +and `unsupported` support scale. + +Capability strength and a record's `information_guarantee` are independent: +backend capability says what the implementation can support; the per-record +guarantee says what this state instance evidences. Exact backend support does +not promote a record to perfect recall. Do not upgrade the reference backend's +manifest until its production path and evidence actually satisfy the new +contract. + +### Authentication, policy, and egress + +`ControlPlaneSecurityConfig.strict_defaults()` remains the default-deny +configuration. `_ControlPlaneApiAuth.read_identity()` owns bearer or trusted- +proxy authentication and backend/operator/auditor role checks. +`ParticipantAudienceSubjectBinding` separately binds the semantic audience; +an operator role is not participant identity. + +Participant-sensitive retrieval must resolve authentication, target role, +participant/audience binding, crossing policy/revision, state cut, markings, +visibility, redaction, and transformation independently. No one successful +gate widens another. Resolve the governed audience before disclosing whether a +participant, episode, or information-state ref exists. + +The existing request-body/content-length middleware enforces +`max_request_bytes`, but GET path, query, and header values are not covered by +that body limit. ACT-604 should add no new caller-controlled input on the +existing retrieval path. Any later direct route must apply the shared bounded- +value validation to participant, episode, audience, ref, and idempotency values +before lookup, audit, fingerprinting, or persistence. + +### Secrets, host exposure, errors, and observability + +ACT-604 needs no environment variable, secret-file lookup, CLI, subprocess, +socket, daemon, or host-service change. Tokens, credentials, private prompts, +private model memory, hidden truth, and raw evidence must never enter process +arguments, environment values, filenames, refs, digests, exception text, +stdout/stderr, audit events, or logs. State carries authorized refs/digests, +markings, redaction, and provenance; operational credentials remain behind +their existing secret/auth boundary. + +Reuse the existing error boundary: model/semantic failures become closed +`Diagnostic` values or sanitized conformance failures; expected access/conflict +failures use the existing `PermissionError`/`ValueError` mapping and fixed safe +HTTP details; unexpected exceptions return only +`{"detail":"internal server error"}` while audit records the exception type, +not attacker-controlled text. Do not create a participant-information-state +exception hierarchy or return Pydantic inputs/errors verbatim. + +Observability is the existing `Diagnostic`, `OperationReceipt`/status, +`AuditEvent`, crossing evidence, evidence refs, provenance refs, and append-only +history. Do not add a parallel logger or emit state payloads for debugging. + +### Repository workflow + +`.ground-control.yaml`, `.gc/plan-rules.md`, the canonical repository-policy +and requirement-governance checks, schema publication tooling, contract +fixtures, and the `verify` session own delivery. The implementation branch must +bind `RAES_REQUIREMENT_UID=ACT-604` because `212-dynamic-knowledge-state` does +not contain the requirement UID. No issue-local validation script, generated +schema shortcut, manual version edit, or manual changelog entry is needed. + +## Extensibility Seam + +The stable seam is the record/profile key, not a generic knowledge abstraction: + +`(participant, episode, exact_state_cut, projection_revision, +redaction_revision, memory_scope, reconstruction_profile, guarantee)`. + +The reconstruction profile resolves the closed algorithm tuple +`(algorithm_id, algorithm_version, schema_version, projection_version)`. +Future algorithms, partial-order strategies, proof formats, or bounded +realizations add immutable profiles and closed trusted dispatch entries without +editing the information-state record shape or weakening historical meaning. +Future source carriers add a typed contract/ref relation and contextual join; +they do not add arbitrary dictionaries or executable registry entries. + +ACT-615 holdings fit this seam as typed source refs after their owner publishes +the kinds and lifecycle. A holding becomes participant information only through +an authorized observation, disclosure, or governed reconstruction at a cut. +This allows new holding kinds without reopening ACT-604 or confusing possession +with knowledge. + +## Gotchas And Anti-Patterns + +- Do not equate world state, centralized-training state, backend debug state, + evaluator truth, participant view, belief/inference, shared state, holdings, + and archival evidence. +- Do not infer participant knowledge from shared-state read permission. A + visible/delivered occurrence at the relevant cut is required. +- Do not collapse repeated or contradictory occurrences by value, timestamp, + or digest. Occurrence identity and declared order are semantic. +- Do not use wall-clock timestamps, list position, database revision, or + last-writer-wins as a portable causal/decision cut. +- Do not silently promote initial knowledge, disclosed assertions, inferred + beliefs, deceptive observations, stale context, or holdings to truth. +- Do not erase history on concealment, revocation, supersession, reset, or + redaction. Append the governed transition and change future visibility. +- Do not turn the reconstruction profile corpus into a plugin system, planner, + reasoner, knowledge graph, or dynamic code-loading surface. +- Do not duplicate exact-cut, guarantee, marking, redaction, provenance, + capability, diagnostic, exception, snapshot, repository, locking, audit, or + egress abstractions. +- Do not validate only the edited model or a test helper. Structural validity + without cross-ref, exact-cut, policy, reconstruction, and persistence + validation is insufficient. +- Do not advertise perfect recall, history consistency, replay fidelity, + convergence, epistemic equivalence, or causal truth from schema validity or + a successful sample reconstruction. + +## Explicit Non-Goals + +- A generic mutable `knowledge`, `beliefs`, `holdings`, or + `environment_state` dictionary. +- A universal BDI state, private chain-of-thought/cognitive-state carrier, + planner, POMDP solver, epistemic reasoner, knowledge graph, or CRDT. +- A new observation, context, shared-state, decision-cut, evidence, capability, + authentication, storage, exception, logging, or workflow stack. +- The ACT-615 operational-holdings taxonomy or lifecycle. +- A raw participant information-state API when existing governed retrieval and + typed refs suffice. +- Backend realization or strong semantic claims unsupported by governed + production evidence. diff --git a/docs/decisions/issue-213-act-605-baseline-behavior-profiles-preflight.md b/docs/decisions/issue-213-act-605-baseline-behavior-profiles-preflight.md new file mode 100644 index 000000000..21cc584c6 --- /dev/null +++ b/docs/decisions/issue-213-act-605-baseline-behavior-profiles-preflight.md @@ -0,0 +1,263 @@ +# Issue #213 — ACT-605 Baseline Behavior Profiles Preflight + +Date: 2026-08-01 + +Issue: #213. + +Requirement: ACT-605. + +This note records the architecture boundary for reconciling ACT-605 with the +current repository. It is implementation guidance only: it does not amend +ADR-092, change the formal specification or SDL, modify a published contract, +claim a backend realization, change Ground Control traceability, or transition +ACT-605 from `DRAFT`. + +## Finding + +The current architecture already satisfies ACT-605 end to end. Ordinary user, +automation, and ambient scenario activity is authored as +`ParticipantBehaviorSpecification.autonomous_execution` and executed by +ordinary participants under ADR-092 and the shared-time model. A +non-evaluated autonomous participant is an existing `green` participant, not a +background actor. + +The remaining gap is reconciliation only: + +- ADR-092 and + `specs/formal/participant-semantics/autonomous-execution.md` define the + required behavior but do not identify ACT-605 explicitly; +- `docs/explain/sdl/limitations.md` still lists “User behavior profiles” as an + SDL expressiveness gap even though the v2/v3 autonomous policy surface is + delivered; and +- the supplied Ground Control inventory contains issue-level `DOCUMENTS` + links but no current code-level `IMPLEMENTS` or test-level `TESTS` evidence, + while ACT-605 remains `DRAFT`. + +No new ADR, schema, model, validator, compiler path, runtime service, backend +protocol, persistence carrier, API route, or test framework is warranted. If +the focused verification remains green, the implementation boundary is an +in-band ADR-092 amendment, a formal-specification clarification, correction of +the stale limitation, and external traceability/status reconciliation. + +## Concept And Authority Boundaries + +| Concern | Canonical owner | ACT-605 boundary | +| --- | --- | --- | +| Authored behavior | `ParticipantBehaviorSpecification.autonomous_execution` and `ParticipantAutonomousExecutionPolicyV1/V2/V3` | No `baseline_behavior`, `background_activity`, or pack-local profile root | +| Participant identity and role | Existing `agents`, participant roles, episodes, and implementation selection | `evaluation_authority.mode: none` requires existing `green`; “ambient” does not create another actor kind | +| Actions and observations | Existing participant action contracts, observation boundaries, native admission requests, and behavior history | A profile selects already-authorized actions; it does not widen action, target, observation, or evaluation authority | +| Time | ADR-090/091 shared clocks, progression policies, constraints, lifecycle, and provenance | No private clock, cron authority, host calendar, or background scheduler | +| Baseline policy | V1 fixed cadence and ordered cycle; v2 work/pause windows, bounded timing, weighted candidates, dependencies, retries, cooldowns, bursts, and finite limits | Profile version changes are explicit; v1 meaning and persisted digests remain stable | +| Resource governance | V3 scoped resource budget and ADR-097 | Resource priority/fairness is explicit and role-neutral; it is not inferred from `green` or “background” wording | +| Evaluation | `ParticipantEvaluationAuthority`, objectives, evaluator-plane contracts, ADR-073, and ADR-092 | Action execution never implies score, proof, receipt, adjudication, or outcome authority | +| Native realization | Exact execution bindings, participant-runtime capability/admission, lifecycle/readback, and conditional live conformance | Capability declarations permit admission but do not prove activity, service fidelity, or evidence | +| Durable evidence | Typed scheduler state, execution-service state, behavior history, shared-time state/provenance, resource state/events, and `RuntimeSnapshot` | Logs, metadata, timestamps, and control-operation success are not behavior evidence | + +“Baseline profile” is requirement language for an autonomous participant +policy, not a new contract-family name. “Background” describes its scenario +purpose, not an execution plane, priority class, daemon, or hidden actor. +Service-internal maintenance that is not participant behavior remains owned by +the applicable service/runtime contract; exercise injects remain orchestration. + +## Reconciliation Guardrails + +ADR-092 is accepted and acceptance-content pinned. Any ACT-605 wording change +must follow ADR-059 in the same change: add a `#213` row to the ADR's +`## Amendments` table, add the matching amendment entry in +`docs/decisions/adrs/adr-index.yaml`, and update the canonical-content pin. +Editing the ADR without all three is invalid even when the prose change is +small. + +The ADR and formal specification should identify ACT-605 as covered by the +existing autonomous-participant decision and invariants. They must not give +ACT-605 separate normative semantics or restate the policy in a way that can +drift from the v1/v2/v3 contract. The limitations table should remove the +false expressiveness gap. Any residual human-realism or production-fidelity +limitation belongs with the existing nonclaims; it is not a reason to retain +“User behavior profiles” as a missing SDL surface. + +No published schema changes are implied. In particular, do not touch the +hand-governed SDL, backend-manifest, runtime-snapshot, behavior-history, or +control-plane schemas, their generated `schema_bundle()` counterparts, or the +schema-publication manifest unless focused verification first proves a real +contract mismatch. Documentation must describe the shipped contract rather +than change it to manufacture ACT-605-specific vocabulary. + +Ground Control should point ACT-605 at the current canonical repository +identity (`OpenRAE/rae#213` and current repository paths), while preserving +historical issue links only as lineage. `DOCUMENTS`, `IMPLEMENTS`, and `TESTS` +must remain distinct: an issue or ADR is not an implementation link, and a code +file is not test evidence. `ACTIVE` is justified only after the repository +artifacts, focused verification, and those link sets agree. + +## Canonical Cross-Cutting Incumbents + +| Layer | Incumbent to reuse | +| --- | --- | +| Source ingress | `parse_sdl`, `load_sdl_yaml`, YAML 1.2 core resolution, duplicate/merge-key checks, `SDLParserLimits`, import/composition limits, and closed `SDLModel` shapes | +| Authored policy | `raes.participant_execution`, `ParticipantBehaviorSpecification.autonomous_execution`, and the existing v1/v2/v3 discriminated policy union | +| Semantic validation | `raes.semantics.participant_behavior`, `raes.validator._participant_execution_renderers`, `raes.validator._time_model`, and `_participant_resource_budget_owners` | +| Compilation | `_compile_autonomous_execution`, `ParticipantAutonomousExecutionRuntime`, `ParticipantExecutionBindingRuntime`, canonical address helpers, and canonical contract digests | +| Shared time and randomness | Existing time models/compiler/runtime controls; `RandomStreamControlBindingModel`, admitted `agent-policy` stochastic control, governed profile/corpus, stateless engine, and bounded-integer transform | +| Planning and manifest admission | `ParticipantRuntimeCapabilitiesModel`, `ParticipantRuntimeCapabilities`, manifest round-trip adapters, `PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS`, `participant_autonomous_execution_capability_gaps()`, resource-budget admission, and planner diagnostics | +| Runtime target and execution | `RuntimeTarget`, `BackendRegistry`, capability-specific participant/time protocols, `ParticipantScheduler`, `RuntimeParticipantExecutionMixin`, `ParticipantClockDriver`, `ParticipantActionApplyResult`, and `autonomous_action_result_violation()` | +| Lifecycle and control | Existing participant-execution bindings, generation-fenced control/readback contracts, `RuntimeControlPlane`, operation receipts/status, idempotency records, and coordinated reset protocols | +| Persistence | `RuntimeSnapshot.participant_autonomous_execution_states`, execution-service/resource carriers, participant behavior history, time-model state, `require_participant_autonomous_runtime_snapshot()`, `ControlPlaneStore`, and local/in-memory stores | +| Errors and observability | `SDLParseError`, `SDLValidationError`, bounded `Diagnostic`, `AuditEvent`, typed operation/snapshot/history/evidence records, and the redacted HTTP 500 envelope | +| Conformance | Participant execution live probes, snapshot semantics, backend profiles/manifests, published fixtures, and the focused DSL-437/#898/#899 negative and lifecycle tests | +| Publication and workflow | ADR-059 pin gate, `.ground-control.yaml`, `.gc/plan-rules.md`, schema/publication policy, canonical nox `verify`, repository-policy and requirement-governance checks, Ground Control traceability, and release-please | + +These incumbents already own the concern. Do not duplicate them as an ACT-605 +DTO, validator, exception hierarchy, scheduler, controller, repository, audit +stream, logger, schema generator, fixture runner, or workflow script. + +## Whole-Path Security And Validation Gates + +1. **SDL source and shape.** An authored baseline policy continues through + byte/scalar/depth/node/alias/import/composition limits, safe YAML + construction, duplicate/merge-key policy, the closed Pydantic model, and + semantic validation. Unknown “background” fields fail rather than becoming + extensions or metadata. +2. **Reference and authority validation.** Participant, role, action, + observation, clock, progression, window/cadence, stochastic-control, + resource-owner, objective, implementation, and target refs resolve through + existing registries and canonical addresses. Non-evaluated role and + authority-widening checks remain fail closed. +3. **Configuration and secrets.** Participant implementation configuration + stays behind `ConfigurationTargetRegistryModel`, literal versus + `secret-reference` validation, selection/configuration digests, and the + authorized sink. SDL policies, manifests, snapshots, histories, + diagnostics, and provenance carry safe refs/digests, never resolved + credentials or raw entropy. ACT-605 requires no new environment binding or + secret resolver. +4. **Manifest and backend admission.** Closed manifest models, controlled + vocabularies, exact profile/feature/strategy/action/observation/target/time/ + random/resource support, relational execution bindings, required contract + ids, and positive finite limits must agree. Booleans, installed libraries, + method presence, or separate action/target lists are insufficient. +5. **Runtime target and native commit.** Registry probes require claimed + capability-specific methods. Each action is generation fenced, invokes the + selected native binding, returns a typed terminal outcome, appends the exact + ordered behavior history, and passes snapshot/history/state validation + before durable commit. Control-operation success is not action success. +6. **Authentication and egress.** ACT-605 adds no endpoint or caller-controlled + input. Existing execution control and readback stay within + `create_control_plane_app()`, `ControlPlaneSecurityConfig.strict_defaults()`, + bearer or verified-proxy identity, role/target checks, request-size guards, + request fingerprints, idempotency, and audit. Policy presence grants no + control, participant-subject, evaluator, or secret-read authority. +7. **Persistence and conformance.** Save, load, API projection, and conformance + reuse the canonical autonomous-state, clock-segment, episode, lifecycle, + generation, accounting, and append-only history invariants. No state belongs + in `RuntimeSnapshot.metadata`, operation details, log text, or a new store. +8. **OS and process exposure.** The reference scheduler and random engine stay + in process. A backend adapter remains an impure leaf using preconfigured + targets, fixed argv when a process is unavoidable, no shell, bounded + time/input/output, controlled working directory/environment, and no + credentials, policy payloads, entropy, or action data in argv. ACT-605 adds + no CLI, subprocess, socket, daemon, host service, environment variable, or + temporary-file surface. +9. **Errors and observability.** Authoring failures use the existing SDL error + types; planner/runtime failures use bounded diagnostics and typed operation + records; unexpected HTTP failures retain the generic redacted 500 response. + Do not expose raw Pydantic inputs, candidate sets, weights, entropy, + credentials, adapter errors, backend reprs, paths, command output, + environment dumps, or tracebacks. Typed history/snapshot/evidence is the + scientific observability surface; logs and audit carry only safe, + low-cardinality identifiers, codes, counts, generations, and durations. + +The reconciliation itself should not cross any runtime, auth, secret, config, +or OS boundary; it documents the boundary already enforced by those layers. + +## Evidence And Traceability Boundary + +The primary implementation links should identify the existing authored model, +semantic validator, compiler, capability admission, scheduler/native-result +validation, and durable-state validator/store. The primary test links are: + +- `test_dsl_437_benign_participant_execution.py` for parsing, semantic + rejection, v1/v2 compilation, exact admission, shared-time execution, + retries, cooldowns, bursts, reset, native outcome, and wall-driver cases; +- `test_dsl_437_evaluation_authority.py` for evaluator-plane separation; +- `test_dsl_437_snapshot_durability_conformance.py` for save/load/API/ + conformance and clock/episode contradictions; +- `test_issue_898_participant_execution_control.py` for relational bindings, + lifecycle/readback, generation fencing, authentication, concurrency, and + conditional live conformance; and +- `test_issue_899_participant_resource_budgets.py` for the v3 policy, + admission, resource accounting, reset reconciliation, and isolation. + +Preflight verification passed 112 focused cases across those files. The HTTP +case `test_control_plane_exposes_authenticated_generation_bound_execution_control` +initially timed out while entering Starlette `TestClient`, before it issued its +first request or reached an ACT-605 assertion. Its isolated rerun passed, and +the subsequent focused suite passed all 113 cases. The canonical completion +suite also passed before publication. + +Reference-backend tests prove portable protocol behavior, not a production +adapter, realistic human behavior, workload fidelity, or actual scenario +activity. Ground Control links and public prose must preserve that claim +boundary. + +## Extensibility Seam + +The stable extension seam is the versioned autonomous policy profile plus +stable keyed action candidates and exact backend execution bindings. A new +timing or selection meaning mints a new profile/capability term while preserving +v1/v2/v3 semantics. A new product or service adds an admitted action-to-target +binding and evidence behind the incumbent participant-runtime protocol. A +future civil-calendar constraint extends shared-time authority, not the +participant scheduler. + +This seam lets additional ordinary user, automation, or ambient activities +reuse the same participant/action/observation/time/evidence path without +reopening ACT-605 or editing a central “baseline activity” catalog. If a future +variation cannot be expressed by the existing bounded-integer transform or +contract version, it receives a separately versioned governed profile rather +than an optional field that changes historical meaning. + +## Gotchas And Anti-Patterns + +- Do not equate `green`, non-evaluated, background, low-priority, hidden, + harmless, deterministic, or non-adversarial. These are different concerns. +- Do not turn “ambient scenario activity” into host/runtime noise, a service + daemon ontology, exercise injects, historical files, or pack-local workflow. +- Do not infer action authority, target access, observation scope, evaluation + authority, or backend support from the word “baseline” or from participant + color. +- Do not introduce a parallel actor, live-activity service, scheduler, clock, + calendar, action schema, behavior profile root, snapshot map, or evidence + stream. +- Do not change v1 defaults or digests, silently treat v2/v3 as compatible, or + use optional fields to alter an existing profile's meaning. +- Do not use cron, wall time, host locale/time zone, sleep, process order, map + order, mutable RNG state, floating weights, retry-driven draws, or backend + logs as semantic authority. +- Do not treat control receipt, health, readiness, lifecycle, native outcome, + scheduler state, participant episode, and behavior evidence as synonyms. +- Do not claim native execution, human realism, fidelity, causality, rollback, + or exactly-once side effects from schema validity, capability flags, or + reference tests. +- Do not add code or regenerate schemas merely to create ACT-605-named + artifacts; trace the requirement to the canonical artifacts that already + implement it. +- Do not update an accepted ADR without its amendment row, manifest entry, and + pin, or mark ACT-605 `ACTIVE` before `DOCUMENTS`/`IMPLEMENTS`/`TESTS` evidence + and verification agree. + +## Explicit Non-Goals + +- A new background-actor, baseline-profile, ambient-activity, scheduler, time, + calendar, workflow, daemon, or evaluation ontology. +- New SDL fields, contract versions, backend capabilities, API routes, + configuration/env shapes, secret resolution, persistence, logging, + exception, or conformance frameworks. +- Product-, pack-, workload-, service-, or historical-content-specific + semantics. +- Scoring, objectives, proof or receipt authority, experiment allocation, + scenario-family variation, inject delivery, or service lifecycle redesign. +- A guarantee of human likeness, stochastic unpredictability, workload + realism, production throughput, backend fidelity, native rollback, causal + attribution, or golden-range equivalence. +- A Ground Control status change or external traceability mutation during this + architecture preflight. diff --git a/docs/decisions/issue-265-aut-810-artifact-transformations-preflight.md b/docs/decisions/issue-265-aut-810-artifact-transformations-preflight.md new file mode 100644 index 000000000..6b3a8e67f --- /dev/null +++ b/docs/decisions/issue-265-aut-810-artifact-transformations-preflight.md @@ -0,0 +1,407 @@ +# Issue #265 / AUT-810 — Safe artifact transformations preflight + +Date: 2026-07-31 + +Issue: #265. + +Requirement status: requirement-free run; the issue is the delivery contract. + +This note fixes the architecture boundary for pure, deterministic refactoring +and transformation of canonical RAES SDL and portable contracts. It is +non-normative guidance only. It does not implement an operation, publish a +schema, change SDL or contract meaning, migrate an artifact, or add a workflow. + +## Decisive current-state findings + +- `raes` already owns SDL models, parsing, semantic validation, composition, + instantiation, canonical identity, declarations, references, and phase + provenance. The public semantic transformation facade belongs there. +- The existing `raes_operations` package is the backend-evidence orchestration + layer. Its allowed imports include runtime and libvirt surfaces. Despite its + name, it is not the home of a pure artifact-transformation kernel. +- `raes_contracts` owns dependency-neutral closed portable models, diagnostics, + canonical JSON, published-schema generation, and portable contract adapters. + A portable transformation report belongs there if it is published. It must + not import SDL implementation details to perform an operation. +- `canonical_sdl_digest()`, `canonical_instantiated_sdl_digest()`, and + `canonical_contract_digest()` already define canonical identities. The + `canonical-artifact-identity` relation explicitly proves canonical byte + identity only. A rename normally changes the digest, so digest inequality is + not loss and digest equality cannot be the rename-preservation test. +- `composition.py` already contains the namespace, reference-rewrite, merge, + budget, and phase-provenance behavior used by imports. A second rewrite table + would drift. The pure namespace/rewrite/merge core must be shared with that + incumbent, while file, OCI, lock, signature, trust, and resolver behavior + stays at the existing import boundary. +- `specs/sdl/references.md`, `DeclarationIndex`, `_module_symbols.py`, and the + semantic validators together own reference meaning. Language-service + completion and occurrence metadata are presentation aids, not an exhaustive + semantic rename engine. `apply_structured_edit()` intentionally returns + edited text even when the edit has diagnostics; that is not the atomic + transformation contract required here. +- ADR-075 and the normative evolution specification already require automated + migration to be deterministic, idempotent, source-preserving, explicit about + ambiguity/loss, and fail-closed. They reject a universal migration service. +- This work is FM2: rename is an FM1 static-semantic change, but composition, + extraction, dependency closure, and reference transport are graph semantics. + The FM2 floor requires an invariant list, unit tests, typed contract + coverage, and property-based or differential tests. + +ADR-036, ADR-053, ADR-061, ADR-075, ADR-076, ADR-078, ADR-080, and the existing +normative specifications settle the durable architecture. No new ADR is needed +unless implementation discovers a requirement incompatible with those +authorities. + +## Architecture decisions and boundaries + +### One pure kernel, operation-specific entry points + +Expose a small public facade from `raes`, backed by operation-specific pure +functions. Inputs include the admitted artifact, exact operation request, +closed policy, and any linked artifacts or resolution context needed to make +the result unambiguous. There is no filesystem, network, process, environment, +clock, random, singleton-registry, UI, or persistence input hidden behind an +entry point. + +Do not create a generic object-tree transformation language or callback/plugin +executor. Rename, compose, extract, and exact source/target migration pairs +have different preconditions and preservation obligations. They may share one +closed report vocabulary and common admission/comparison helpers without +pretending to be the same algorithm. + +Portable contract migration adapters stay with their owning contracts. The +`raes` facade may dispatch to dependency-neutral `raes_contracts` adapters; +`raes_contracts` must not import the SDL kernel. Dispatch is by exact supported +source and target contract/profile, never by a mutable notion of "current" or +"latest". + +### Transactional value semantics, not filesystem atomicity + +SDL models are not generally frozen. An operation must reconstruct an isolated +candidate from typed data, transform only that candidate, and pass the complete +candidate through its normal structural, semantic, phase, and canonicalization +gates before exposing it. A shallow copy, Pydantic `model_copy()` without +readmission, or temporary mutation followed by rollback is insufficient. + +The public result has exactly one of these shapes: + +- success: complete admitted typed output plus a complete report; or +- refusal: no output plus a complete report containing the failed condition + and typed diagnostics. + +Expected ambiguity, loss-policy, precondition, or postcondition failure is a +machine-readable refusal, not a partially useful artifact and not a new +exception family. Existing `SDLParseError`, `SDLValidationError`, +`SDLInstantiationError`, Pydantic validation, and `StrictJsonIngressError` +remain the malformed-input/programming boundaries. The caller's input object +and canonical bytes must remain unchanged for success and refusal alike. + +"Atomic" in this package means all-or-none in-memory output. Atomic file +writes, rollback, pack transactions, and repository commits belong to the +consumer. + +### One closed, bounded transformation report + +If a portable report is published, define it once as a frozen, closed +`ContractModel` in `raes_contracts` and use the existing `DiagnosticModel` for +its general diagnostics. The report must carry, in deterministic order: + +- exact operation profile/revision and result status; +- source and requested target artifact kind, lifecycle/contract profile, and + canonicalization profile, with a source digest always and a target digest + only for admitted output; +- machine-readable precondition and postcondition check ids and outcomes; +- affected declaration identities and an explicit before-to-after identity + map; +- the named preservation/comparison profile, outcome, and bounded evidence; +- stable derivation from input digest(s), operation profile, and canonical + policy digest; and +- typed loss/ambiguity records with stable kind, affected identity/address, + and safe diagnostic. + +The report is not an artifact bundle. It references inputs and outputs by typed +identity and digest; operation-specific Python results carry the actual typed +artifacts. Do not embed arbitrary payloads, raw source, open `metadata`, caller +objects, timestamps, UUIDs, usernames, host paths, or backend evidence. + +A small governed loss-kind vocabulary is justified by the issue. It extends, +rather than replaces, `DiagnosticModel`. Do not reuse participant-crossing +loss or external-concept approximation types for unrelated artifact loss, and +do not introduce another general severity, diagnostic, or exception hierarchy. +Default loss and ambiguity policy is reject. An explicit policy may authorize +named, reported loss; it cannot authorize guessing. Ambiguous identity lookup +must be resolved by an exact canonical address or explicit complete mapping, +never first/last match, set collapse, or `force=True`. + +### Preservation is a named relation, not a boolean + +Every successful operation declares the artifact stage and comparison profile +it actually checks. `semantic_preserved: true` without a governed relation and +evidence is forbidden. + +- Exact/no-op transformations may use `canonical-artifact-identity` with the + same named canonicalization profile. +- Rename requires static semantic identity transport: a bijection maps the + changed declaration address and every derived canonical address, and the + resolved declaration/reference graph plus non-identity semantic fields must + be isomorphic under that map. Compare resolved targets, not authored string + spelling. If this relation is made public, add a narrowly scoped relation to + the existing behavioral-relation catalog with explicit nonclaims; do not + create a local relation registry. +- Extraction is preservation of the collective result. Recompose the returned + root and extracted module using the declared namespace/import coordinate and + require canonical identity with the original artifact (or the named identity + transport relation). Validating the two files independently is not a + preservation proof. +- Composition names every supplied module, namespace, export set, binding, and + provenance input. Its postcondition compares the expanded result against the + existing ADR-053 composition semantics; it does not claim that one input was + individually preserved as the whole output. +- Contract migration uses exact source/target-version invariants, source and + target admission, and round-trip evidence when the mapping is reversible. + A lossy migration cannot report unqualified full preservation; it may verify + only an explicitly named unaffected projection while still reporting every + policy-authorized loss. Successful parsing at the target version is not a + meaning-preservation result. + +Verification over the supplied artifacts is finite verification, not a +universal proof. Neither canonical identity nor identity transport establishes +behavioral, observational, epistemic, strategic, backend, or scientific +equivalence. Any stronger public claim must use the existing governed +behavioral-relation claim machinery and meet that relation's obligations. + +### Reuse the canonical identity and reference machinery + +Declaration selection starts with `build_declaration_index()` and an exact +canonical address from ADR-076. The transformation must preserve declaration +kind and reject collisions, dangling references, new ambiguity, illegal target +kind, or an address map that is not injective. + +The rewrite inventory must be the same semantic seam used by module +composition. Refactor dependency-neutral helpers out of `composition.py` when +necessary so composition and transformations share: + +- `_module_symbols.HASHMAP_SECTIONS`, `FORWARDING_AGENTS_SECTION`, + `symbol_index()`, exports, nested service/runtime/content aliases, and + variable-token handling; +- the namespace/reference rewrite and section-merge behavior in + `composition.py`; +- `_composition_provenance.py` for rebasing existing phase provenance; and +- the reference-edge obligations and owning validators enumerated by + `specs/sdl/references.md`. + +Do not create a third top-level-section catalog. `_module_symbols.HASHMAP_SECTIONS` +owns composition/declaration identities; `_mapping_scopes.HASHMAP_SECTIONS` +owns authored mapping-key interpretation. Keep those distinct. List-valued +`forwarding_agents` use a stable id while model/provenance paths are positional, +so extraction and rename must not confuse list position with identity. + +Generic language-service occurrence search, JSON Pointer editing, global +string replacement, dotted-string splitting, aliases, labels, filenames, and +array positions do not establish semantic reference identity. The complete +post-transform `SemanticValidator` pass remains authoritative even after a +rewrite helper reports success. + +### Keep composition/extraction pure without weakening import security + +A pure compose/extract call accepts already supplied, admitted typed artifacts, +logical namespaces/export selections/bindings, and any exact resolved-import +provenance it is entitled to preserve. It does not resolve `ImportDecl.source`, +discover adjacent files, read a lockfile, fetch OCI, choose a pack path, verify +a signature, or infer trust. + +The caller supplies the logical import coordinate needed by an extracted root; +RAES must not invent a path or pack layout. Artifact-native +`ExpansionProvenance` retains only facts actually established by the existing +resolver/trust boundary. Transformation derivation lives in the accompanying +report and must not masquerade as signature, lock, or trust evidence. + +Existing `parse_sdl_file()` composition continues to enforce base confinement, +source budgets, resolver allowlists, version and digest pins, lock records, +signature/trust policy, exports, namespaces, cycles, bindings, and aggregate +composition budgets. Extracting the pure rewrite/merge core must not move, +duplicate, or bypass those gates. + +### Treat concept bindings as linked artifacts, not SDL fields + +`ExternalConceptSubjectModel` binds a canonical declaration reference to the +owning artifact digest. A rename changes the renamed canonical reference and +the whole artifact digest. Therefore every supplied binding subject for that +artifact needs its digest updated, not only bindings naming the renamed +declaration. + +An operation may atomically transform explicitly supplied +`ExternalConceptBindingDocumentModel` values and then rerun their structural +and contextual semantic admission. If binding documents are not inputs, the +report exposes the identity/digest map needed by orchestration; the kernel does +not discover them. A binding cannot select a rename target, override +declaration ambiguity, or become identity authority. No concept catalog, +scheme snapshot, URI, or network lookup is implicit. + +## Canonical cross-cutting concerns to reuse + +| Concern | Canonical incumbent and required use | +| --- | --- | +| Package ownership | ADR-036 and `tools/policy/adr_policy.yaml`; SDL kernel in `raes`, portable contracts in `raes_contracts`, conformance in `raes_conformance`, presentation only in CLI/MCP. Keep semantic code out of `raes_operations`, processor, runtime, Hub, and pack adapters. | +| SDL source admission | `SDLParserLimits`, `_yaml_loader.py`, `_source_validation.py`, `_source_profile.py`, `parse_sdl*()`, and closed `SDLModel` types. Preserve UTF-8, byte/scalar/depth/node/alias/composition bounds, YAML 1.2 Core, tag/directive, duplicate/normalized-key, finite-JSON-domain, and explicit `SDLMigrationPolicy` gates. | +| Portable JSON ingress | `parse_bounded_json_object()` and `StrictJsonIngressError`, followed by the exact closed source `ContractModel`. Inspect the raw source shape before model defaults/unknown-field rejection can erase migration evidence. Never `json.loads()` then silently project known fields. | +| Static semantics | `SemanticValidator`, `DeclarationIndex`, ADR-076 identifiers, `specs/sdl/references.md`, `specs/sdl/diagnostics.md`, and the existing domain validators. Run full admission before and after; do not duplicate validation in operation code. | +| Phase and canonical identity | ADR-078, `phase_contracts.py`, `admit_instantiated_scenario()`, `canonical_sdl_*`, `canonical_instantiated_sdl_*`, and dependency-neutral `raes_contracts.canonical`. Preserve lifecycle distinctions and use the canonicalizer owned by the artifact type. | +| Composition and provenance | ADR-053, `composition.py`, `_module_symbols.py`, `_composition_provenance.py`, resolver/lock/trust modules, and composition-budget checks. Share the pure core; leave I/O and trust orchestration at the incumbent boundary. | +| Portable evolution | ADR-061, ADR-075, `specs/evolution/versioning-deprecation-and-migration.md`, exact contract discriminators in `raes_contracts.versions`, and owning source/target models. No universal registry or cross-surface version semantics. | +| Diagnostics/errors | `Diagnostic`, `DiagnosticModel`, `Severity`, the existing SDL error hierarchy, `specs/sdl/diagnostics.md`, `_model_diagnostics.py`, and `sanitized_failure_message()`. Use stable bounded codes and safe locations; add only the transformation-specific loss vocabulary. | +| External concepts | `ExternalConceptBindingDocumentModel`, `ExternalConceptSubjectModel`, and `external_concept_subjects()`. Recompute exact refs/digests for supplied documents and rerun existing contextual resolution; do not redefine concepts or fetch schemes. | +| Schemas and fixtures | `ContractModel(extra="forbid")`, `contracts/schemas/`, `schema_bundle()`, `_MODEL_VALIDATORS` / `_STRUCTURAL_ONLY_VALIDATORS`, `contracts/fixtures/`, `raes_contracts.corpus`, `run_fixture_suite()`, ADR-061 publication records, and generated-schema checks. Extend these once if a report is published; do not duplicate schema validation or corpus loading. | +| Assurance and claims | `specs/formal/assurance-policy.yaml`, the classification/fulfillment ledgers, the behavioral-relation catalog, claim validator, and `tools/check_assurance_policy.py`. Record truthful FM2 evidence and keep finite verification distinct from proof. | +| Workflow | `.ground-control.yaml`, `.gc/plan-rules.md`, `.pre-commit-config.yaml`, `noxfile.py`, `tools/check_repo_policy.py`, `tools/check_requirement_governance.py`, `tools/verify_all.py`, Sphinx, private-key detection, and gitleaks. Wire checks into the existing graph, never a parallel workflow. | + +Conformance artifacts should reuse the owning fixture families. Pair source, +request, expected output/report, and refusal cases by stable case id in the +test/conformance harness instead of copying them into a generic migration or +transformation bundle. Required coverage includes composed/private/exported +references, collisions and ambiguous aliases, workflow/variation/runtime +references, list-valued forwarding agents, concept-binding digest retargeting, +recomposition, allowed and rejected loss, failure immutability, idempotent +migration, and representative portable contracts. Property/differential tests +must vary declaration order, map insertion order, canonical-equivalent inputs, +and repeated execution. + +The transformation case executor belongs in `raes_conformance` as a focused +sibling to the backend-profile fixture suite. It composes the existing corpus, +contract validators, sanitized diagnostics, deterministic case ordering, and +report conventions; it must not turn `run_fixture_suite()` into a semantic +operation engine or fork another JSON Schema validator. + +## Cross-cutting validation, security, and operational layers + +The intended design passes these layers even though the kernel itself is +offline: + +1. **Ingress and shape.** Raw SDL uses the production bounded safe loader; raw + portable JSON uses `parse_bounded_json_object()`. Exact closed source models + validate before interpretation and exact target models validate afterward. + Unknown or duplicate data is refused or reported under an explicit loss + policy, never dropped by projection. +2. **Semantic and graph admission.** `SemanticValidator`, declaration/reference + resolution, uniqueness, ambiguity, cycles, target kinds, dependency closure, + and phase admission run before and after. A local rewrite check cannot bypass + an owning validator. +3. **Canonical and preservation check.** The owning canonicalizer binds both + endpoints. The named identity, identity-transport, recomposition, or + version-pair predicate runs after target admission. Unordered implementation + containers are sorted by governed identity unless order is semantic. +4. **Composition/trust.** Existing file/OCI resolver, lock, digest, signature, + allowlist, confinement, cycle, export, and budget checks remain outside the + pure core. A transformation report cannot mint or strengthen their evidence. +5. **Contract/schema/conformance.** A published report passes `ContractModel`, + checked-in JSON Schema, `schema_bundle()` parity, registered conformance + validation, positive/negative fixtures, publication manifest/hash/record, + and compatibility classification. Schema success remains structural unless + contextual semantics are separately checked. +6. **Authentication/authorization.** The library call adds no auth surface and + grants no authority. A later HTTP adapter must reuse + `ControlPlaneSecurityConfig.strict_defaults()`, verified identity, + target/role checks, request size, idempotency/fingerprint, audit, and redacted + unexpected-error behavior. An MCP adapter reuses its 64 KiB input guard and + `json_response()` envelope. Neither adapter may let caller identity or role + alter pure semantics. +7. **Config and environment.** Operation profile, source/target profile, + selection, namespace, and loss policy are explicit closed inputs. Add no + environment switch, feature flag, config file, service locator, mutable + registry, or ambient "latest" version. Workflow requirement context is not + an artifact field. +8. **Secrets and OS exposure.** The kernel performs no file, network, + subprocess, shell, privilege, or temporary-file action. Reports, + diagnostics, logs, filenames, argv, and environment captures contain no raw + SDL values, credentials, tokens, private keys, parameter maps, URI userinfo, + source bodies, backend objects, absolute host paths, or environment dumps. + A digest is identity, not redaction. A future CLI accepts bounded file/stdin + input and separate output paths; sensitive artifacts are not command-line + arguments. +9. **Error envelopes and observability.** Expected refusals use the portable + report and existing diagnostics. Conformance/adapters use + `sanitized_failure_message()` rather than `str(ValidationError)` or backend + exceptions. The pure kernel adds no logger, telemetry stream, metrics + registry, or audit store. Adapters may log only safe operation profile, + digests, counts, stable codes, and outcome. +10. **Persistence.** No controller, service, repository, `ControlPlaneStore`, + runtime snapshot, database, cache, or audit event stores transformations. + Consumers own atomic writes and pack/repository transactions after a + successful result. A failed operation has nothing to persist as an output + artifact. + +## Extensibility seam + +The stable dispatch/comparison coordinate is: + +```text +(operation_profile, artifact_kind, lifecycle_phase, + source_contract_or_canonical_profile, target_contract_or_canonical_profile, + preservation_profile, explicit_policy) +``` + +The obvious next version or artifact type adds one exact adapter and one +governed comparison profile at that coordinate; it does not edit a universal +`if current_version` branch. The SDL-specific extension seam is the single +shared declaration/reference rewrite catalog used by composition and +transformations. Adding a new SDL reference-bearing field extends that catalog, +`specs/sdl/references.md`, its owning validator, composition, language tooling, +and conformance tests together—not a per-operation field list. + +Extraction additionally parameterizes explicit selection/dependency-closure +policy and caller-supplied logical namespace/import coordinate. Those are data, +not filesystem discovery hooks. Caller callbacks or arbitrary policy code are +not part of the portable seam. + +## Gotchas and anti-patterns + +Avoid: + +- placing semantic transformations in `raes_operations`, CLI/MCP adapters, + processor, runtime, Hub, or env-packs; +- treating an editor JSON-Pointer operation or language-service occurrence list + as semantic rename; +- comparing only pre/post digests for rename, or calling digest equality + behavioral equivalence; +- comparing an extracted fragment and root independently instead of proving + recomposition; +- mutating an input and rolling back on error, returning invalid output beside + diagnostics, or writing files before all postconditions pass; +- a global string replacement, substring/dotted-name rewrite, alias-selected + target, first/last winner, unordered-set output, or field list copied from one + validator; +- a second declaration/reference index, section catalog, canonical JSON + serializer, generic diagnostics envelope, exception hierarchy, schema + registry, migration registry, fixture runner, logger, or persistence store; +- silently dropping unknown JSON members by validating into the target model, + defaulting absent source fields before classification, or interpreting target + parse success as preservation; +- a broad `allow_loss`, `force`, or `allow_ambiguous` boolean; policy names exact + accepted loss kinds and ambiguity is resolved by explicit identity; +- embedding artifacts in the report, using an open metadata bag, or recording + clock time/random ids that make a pure result nondeterministic; +- rewriting only the renamed external-concept subject while leaving other + subjects pinned to the old whole-artifact digest; +- forging `ExpansionProvenance`, lock/signature/trust facts, or pack source + paths inside a pure operation; +- allowing a new SDL section or reference field to land without updating the + shared rewrite/reference/conformance seam; or +- claiming proof, behavioral equivalence, runtime support, or pack-level + transactional safety from finite transformation fixtures. + +## Non-goals + +- Pack layout, file discovery, multi-file transaction management, publication, + repository migration, or automatic linked-artifact discovery. +- Browser/editor journeys, a pack CLI workflow, MCP presentation, HTTP routes, + controllers, backend lifecycle, runtime execution, or experiment execution. +- A generic patch language, arbitrary transformation plugin system, universal + artifact bundle, universal version/migration registry, or new canonical + serialization format. +- Automatic resolution of ambiguous identities, automatic acceptance of loss, + implicit network/schema/concept lookup, or inference of missing provenance. +- Redefining SDL semantics, concept authority, existing portable contracts, + module trust, diagnostic severity, behavioral relations, or compatibility + guarantees merely to make a transformation succeed. +- Filesystem atomicity or persistence inside RAES; the kernel's guarantee ends + at a complete admitted typed result and deterministic report. diff --git a/docs/decisions/issue-50-libvirt-workflow-package-split-preflight.md b/docs/decisions/issue-50-libvirt-workflow-package-split-preflight.md new file mode 100644 index 000000000..000c159ce --- /dev/null +++ b/docs/decisions/issue-50-libvirt-workflow-package-split-preflight.md @@ -0,0 +1,314 @@ +# Issue 50 Libvirt And Workflow Package Split Preflight + +Date: 2026-08-01 + +Issue: #50. + +Requirement: none. The GitHub issue is the delivery contract. This note records +architecture guardrails only; it does not implement the split, drain the +oversized allowlist, alter tests, or add release content. + +## Binding Decisions + +- The four live targets are + `raes_backend_libvirt.drivers.libvirt` (555 lines), + `raes_backend_libvirt.realization` (571 lines), + `raes_backend_libvirt.techvault_native` (563 lines), and + `raes_contracts.workflow` (688 lines). Each becomes a same-named package with + a thin `__init__.py`; every Python file in the replacement packages, + including each facade, must remain at or below ADR-015's 500-line cap. +- Child modules are implementation details. Existing callers continue to use + the four facade paths, and no absolute or relative external import line that + targets those paths changes. +- This is an atomic file-to-package conversion. A target `.py` and same-named + package must never coexist, even transiently in the submitted tree. +- Remove only the four deleted paths from + `tools/policy/oversized_allowlist.yaml`. The fixed historical set in + `tools/policy/repo_policy.py` remains unchanged. +- Release-please owns `CHANGELOG.md`. Although the issue asks for a changelog + entry, `.gc/plan-rules.md` and `docs/DEVELOPMENT_WORKFLOW.md` prohibit manual + edits and fragments. Leave `CHANGELOG.md` untouched and use a + non-behavior-changing Conventional Commit / PR title such as `refactor:`. +- No new ADR is required. ADR-015 already decides the package split and size + gate; ADR-036 already decides DTO, processor, runtime, and backend ownership. + +## Architecture Boundaries + +### Generic libvirt driver + +Keep `LibvirtDeploymentDriver` as one concrete driver, not a controller plus a +new service/repository hierarchy. Internal ownership can follow the existing +call graph: native identity/lookup/stop semantics, driver realization and +teardown, and small connection/name/diagnostic helpers. Existing XML and seed +owners remain `drivers._libvirt_xml`, `drivers.seed`, and `cloudinit`; do not +copy them into the package. + +The split must preserve the security-relevant distinction between `_lookup()` +on define/convergence paths and `_find_native()` on teardown paths, the stable +libvirt error-code classification, deterministic per-address UUIDs, nwfilter +owner UUIDs, ownership-conflict refusal, stop-before-undefine order, +created-only rollback, and verified fail-closed teardown. Do not turn native +absence, permission failure, connection failure, and foreign-name collision +into one generic lookup result. + +### Pure plan realization + +Keep `interpret_provisioning_plan()` pure and preserve its current dependency +direction: typed plan/capability DTOs and payload projections feed network/node +spec construction, placement/cloud-init aggregation, ACL realization, and +ordered diagnostics. Driver calls, filesystem access, host inspection, and +snapshot persistence do not belong in this package. + +The existing concepts stay distinct: capability-envelope admission is not +payload-shape admission; node/network specs are not portable plan DTOs; +placement-to-cloud-init translation is not TechVault appliance admission; and +`Realization.placement_targets` is a refresh/reconciliation aid, not a second +planner or persistence ledger. Preserve resource sorting, diagnostic order, +placement binding behavior, OS-dialect routing, path-component sanitization, +password locking, argv-list `runcmd`, and exact default/coercion behavior. + +### TechVault native driver + +Keep `TechVaultNativeLibvirtDriver` as the concrete lifecycle coordinator over +the existing `techvault_*`, `_techvault_native_*`, envelope, appliance, +observation, and probe modules. Do not split the class into mixins, create a +second native lifecycle service, or move low-level logic back out of its +current canonical sibling modules merely to populate the new package. + +Preserve the stage order: envelope/spec admission before host mutation; +network definition/readback before domain definition/readback; daemon +observation validation before the optional guest stage; evidence binding before +success; ownership-checked rollback on every failed later stage; and snapshot +publication only after all gates pass. The subclass hooks +`_admission_diagnostics`, `_build_matrix`, `_render_domain_xml`, `_guest_stage`, +`destroy`, and `_cleanup_artifacts`, plus the test-patched `_material_binding` +method seam, must keep their method-resolution and override behavior for +`GuestCertifiedLibvirtDriver`. If the class module needs headroom, extract only +a narrow pure helper behind the same method seam; do not introduce a framework. + +### Workflow contracts + +Keep one dependency direction inside `raes_contracts.workflow`: workflow enums +and attempt provenance; result/execution contract parsing and validation; +history and execution-state payload normalization/validation; then the facade. +Child modules import exact owners and never import the facade, avoiding +partially initialized package cycles. + +These dataclasses are normalized runtime/backend DTOs. They are not the +normative Pydantic publication models in +`raes_contracts.contracts.execution_state`, and neither set replaces or wraps +the other. `raes.semantics.workflow.WorkflowStepSemanticContract` and +`validate_workflow_step_result()` remain the SDL-semantic authorities. Retain +`raes_contracts._validation` as the shared primitive validator set. Do not +duplicate schemas, enum coercion, semantic checks, or exception types in child +modules. + +Preserve dataclass field order/defaults, enum identity, mutable-versus-immutable +field behavior, `from_mapping()` / `from_payload()` coercions, `to_payload()` +shapes, `__post_init__` validation order, exact `TypeError`/`ValueError` +conditions and text, and mapping/list iteration order. In particular, do not +"tighten" iterable handling, filtered compensation-failure parsing, empty +details fallback, timeout coercion, or the currently unvalidated cancellation +request during this refactor. + +## Facade Compatibility Inventory + +`raes_contracts.workflow.__all__` remains the exact ordered 13-name tuple now +declared by the module. Re-export the same objects so +`raes_processor.models` continues to expose object-identical neutral DTOs. + +`raes_backend_libvirt.techvault_native.__all__` remains the exact ordered +seven-name list now declared by the module. The facade must additionally retain +`DriverResult` and `_artifact_token`, which +`guest_certified_driver.py` imports from that path even though they are absent +from `__all__`. + +The other two modules currently declare no `__all__`; do not add a restrictive +one that changes star-import behavior. Preserve at least every observed facade +seam: + +- `drivers.libvirt`: `LibvirtDeploymentDriver`, `Connector`, `_error_code`, + `_existing_uuid`, `_raes_uuid`, and `_filter_owner_uuid`; +- `realization`: `Realization`, `interpret_provisioning_plan`, `_image_ref`, + `_infrastructure_spec`, `_memory_mib`, `_node_resources`, `_resource_name`, + `_services`, and `_vcpus`. + +The issue's absolute-import grep does not find the backend's relative imports. +Those are compatibility consumers too: `target.py`, `provisioner.py`, +`techvault_lifecycle.py`, `techvault_matrix.py`, +`techvault_plan_admission.py`, `_techvault_native_ops.py`, +`guest_certified_driver.py`, and the backend root `__init__.py` must keep their +current facade imports unchanged. + +Use explicit re-exports, not wildcard imports, dynamic `__getattr__`, import +scanning, or registration side effects. Do not rewrite class/function +`__module__` metadata or add pickle shims without a demonstrated compatibility +contract. Preserve the Sphinx target `raes_contracts.workflow` and the public +prefix `raes_backend_libvirt.techvault_native` in ADR-036 policy. + +## Canonical Incumbents To Reuse + +- **Plans, capabilities, and snapshots:** `raes_contracts.planning`, + `raes_contracts.runtime_state`, `raes_backend_protocols.capabilities`, + `raes_backend_libvirt.manifest`, `capability_envelope_diagnostics()`, + `LibvirtProvisioner`, and its existing snapshot reconciliation remain the + authorities. No libvirt-specific replacement DTO/schema/store is needed. +- **Target and config shape:** `raes_backend_libvirt.target._CONFIG_KEYS`, + `_validate_config_keys()`, `_driver_config()`, `LibvirtDriverMode`, manifest + and realization-envelope pairing, and the existing injected driver/connector + constructors remain the only backend configuration path. Add no environment + fallback or second config model. +- **Native security and lifecycle:** `provider_resource_name()`, deterministic + owner UUIDs, `techvault_lifecycle`, `_techvault_native_ops`, structured XML + builders, `drivers.seed`, `techvault_appliance`, and the observation/probe + modules remain canonical. Do not fork lookup, ownership, cleanup, XML, + artifact-token, digest, or diagnostic logic. +- **Guest realization:** `cloudinit`, `dialects`, `acls`, and + `techvault_concerns` own safe path components, argv-list commands, + OS-specific emission, ACL translation, bounded guest paths/names, exact + concern admission, and daemon/guest observation comparison. +- **Workflow contracts and schemas:** `raes_contracts._validation`, + `raes.semantics.workflow`, `raes_contracts.versions`, + `raes_contracts.contracts.execution_state`, `schema_bundle()`, and the + checked-in published schemas remain their separate authorities. +- **Runtime errors and persistence:** `raes_runtime.backend_calls` owns + deep-copy/fail-closed backend invocation and `runtime.backend-contract-invalid`; + `workflow_result_contract_context` and + `workflow_result_contract_checks` own compiled-contract/result validation; + `RuntimeControlPlane` and `ControlPlaneStore` own operations, snapshots, + idempotency, audit, and persistence. The four replacement packages add no + store, audit sink, or error envelope. +- **Repository workflow:** ADR-015, ADR-036, + `tools/policy/adr_policy.yaml`, `tools/check_repo_policy.py`, the locked + oversized reference set, Hatch's existing nested-package discovery, + `implementations/python/pyproject.toml`, generated-schema drift checks, the + optional real-daemon smoke harness, and the pinned nox `verify` session are + the completion graph. + +## Cross-Cutting And Security Layers + +- **Authoring, compilation, and plan shape:** normal input still passes the SDL + parser/validators, compiler, planner, typed `ProvisioningPlan` construction, + manifest capability checks, and realization-envelope checks. Direct plans + still pass `LibvirtProvisioner.validate()` / `apply()` and + `capability_envelope_diagnostics()` before reconciliation or driver IO. The + split adds no raw SDL or unvalidated dictionary ingress. +- **TechVault admission and observation:** direct driver callers still pass + constructor URI/flag/name validation, `load_libvirt_realization_envelope()`, + `techvault_spec_diagnostics()`, native-name/ownership checks, exact daemon + readback, `techvault_observation_diagnostics()`, optional challenge-bound + guest observation, digest binding, and verified cleanup. No stage may be + bypassed by importing a child module directly. +- **Workflow shape and semantics:** payloads still pass the same dataclass + `from_*` shape checks, shared primitive validators, semantic step-result + validator, runtime compiled-contract normalization, ordered result/history/ + compensation checks, and separately the closed Pydantic publication models + where schema validation is required. Moving a definition must not weaken, + duplicate, or reorder these gates. +- **Authentication and authorization:** none of the four modules authenticates + a caller. HTTP-triggered work remains behind the existing control-plane API + bearer-token, verified-identity, role, target-binding, request-size, + idempotency, and audit gates. Do not add a libvirt or workflow child-module + endpoint that bypasses `RuntimeControlPlane`. +- **Secrets and configuration:** TechVault continues to reject connection URIs + carrying user information or passwords. Generic libvirt configuration stays + explicit and is passed to the Python libvirt API, not inferred from ambient + environment. Cloud-init may contain authored content and SSH public keys; + retain password locking, private source/ISO modes, owner/symlink checks, and + redaction. Never place URI credentials, content, keys, challenge values, + environment values, or native exception text into diagnostics, audit, or + portable snapshots. +- **OS/process/filesystem exposure:** preserve lazy `libvirt` import; structured + XML calls; deterministic, ownership-checked host mutations; scoped artifact + cleanup; safe path components; fixed argv, no shell, bounded timeout, and + discarded output in `GenisoimageSeedBuilder`; and the existing bounded + initramfs builder. The guest challenge remains a non-credential correlation + value on the kernel command line. The split adds no subprocess, argv, + environment, network, filesystem, privilege, or daemon surface. +- **Errors and observability:** preserve `Diagnostic`/`Severity`, every stable + libvirt/TechVault/runtime code, message redaction, multiplicity and order, + `DriverResult` observations, `last_snapshot` evidence binding, and upstream + control-plane audit behavior. These modules have no logger; do not add a + parallel logger, telemetry channel, exception hierarchy, raw traceback, XML + dump, subprocess output, or payload-bearing error. +- **Persistence:** generic driver name/realized/seed/filter maps and TechVault + artifact maps are private live-driver state. `RuntimeSnapshot`, control-plane + records, and validated run artifacts remain the portable/durable surfaces. + Workflow contract dataclasses remain data-only. Do not introduce a cache, + database, repository, native-state ledger, or child-module global registry. +- **Import and policy gates:** child modules stay within the ADR-036 dependency + directions: `raes_backend_libvirt` consumes only its allowed protocol, + contract, and public runtime registry surfaces; `raes_contracts` consumes + only `raes`. Every new file passes module-boundary, path-safety, + secret/private-key, Ruff, type, coverage, and 500-line checks. + +## Extensibility Seams + +The existing seams are sufficient and must remain usable after the split: + +- generic backend variation remains the explicit `provisioner_capabilities` + parameter plus target/driver `connection_uri`, connector, name prefix, + workspace, and `SeedBuilder` injection; +- plan-to-guest variation remains `GuestDialect` plus the manifest capability + envelope, not conditionals scattered through child modules; +- TechVault/guest-certified variation remains `driver_mode`, realization + envelope, `InitramfsBuilder`, connector, and the existing subclass hooks; +- a future workflow field or rule belongs with its owning DTO/validator, is + deliberately re-exported if public, and joins the existing runtime check + ordering and separate publication model explicitly. + +A future remote libvirt target, seed transport, bounded appliance mode, or +workflow contract field must not require changes to `RuntimeManager`, the +control-plane API, published import paths, or an unrelated child module. Do not +replace these explicit parameters with environment binding, discovery, +registries, plugins, service containers, or generic validator frameworks. + +## Gotchas And Anti-Patterns + +- Do not modify external import lines, including relative backend imports, or + expose child-module paths as a new supported API. +- Do not import a package facade from one of its child modules. In particular, + avoid cycles among the workflow DTO families and among libvirt ownership, + TechVault lifecycle, and driver modules. +- Do not preserve compatibility with wildcard re-exports. Conversely, do not + omit the observed private facade seams merely because their names begin with + `_` or are absent from `__all__`. +- Do not merge the generic and TechVault drivers, treat TechVault's bounded + appliance proof as generic libvirt realization, or conflate daemon-observed + facts with guest-observed service/application facts. +- Do not move `techvault_plan_admission`'s realization projections into a new + schema or copy them; its facade imports are an existing coupling to preserve + during this mechanical issue, not permission to create another payload + authority. +- Do not merge workflow runtime dataclasses with Pydantic publication models, + workflow SDL semantics, runtime result-check contexts, or control-plane + workflow coordination. They have different owners and validation roles. +- Do not reorder diagnostics, sorting, mutation/rollback, observation stages, + payload conversion, validation, or cleanup. Do not "fix" broad exception + handling, coercion quirks, mutable defaults created by factories, unused + helpers, or comments as incidental cleanup. +- Do not broaden Ruff suppressions to whole new packages. Use explicit + same-name re-exports where possible; if a suppression is truly necessary, + scope it to the facade and rule that require it. +- Do not hand-edit generated schemas, policy code, the locked oversized set, + versions, `CHANGELOG.md`, or pre-existing tests to absorb refactor drift. + Existing tests and the optional real-daemon harness remain behavior oracles. + +## Non-Goals + +- No libvirt, QEMU, cloud-init, TechVault, guest-certified, workflow, + compensation, validation, diagnostic, observation, rollback, cleanup, + snapshot, persistence, API, auth, audit, or security behavior change. +- No new SDL fields, contract/schema/profile/vocabulary changes, generated + schema churn, backend capability claim, realization concern, driver mode, + workflow state field, history event, or cancellation behavior. +- No new public child-module API, compatibility namespace, DTO, schema, + validator framework, exception hierarchy, logger, telemetry path, + repository, cache, service, registry/plugin system, config/environment + surface, CLI command, HTTP endpoint, or host integration. +- No movement of contract ownership into runtime/backend code, backend-native + state into portable contracts, pure realization into driver IO, or concrete + backend logic into `raes_contracts`, `raes_processor`, `raes_runtime`, or the + legacy compatibility tree. +- No test relaxation, behavior-motivated test edit, policy redesign, + performance rewrite, dead-code cleanup, version edit, or manual release note. diff --git a/docs/decisions/issue-51-security-semantics-evidence-package-split-preflight.md b/docs/decisions/issue-51-security-semantics-evidence-package-split-preflight.md new file mode 100644 index 000000000..9a8d64d11 --- /dev/null +++ b/docs/decisions/issue-51-security-semantics-evidence-package-split-preflight.md @@ -0,0 +1,236 @@ +# Issue 51 Security, Semantics, And Evidence Package Split Preflight + +Date: 2026-08-01 + +Issue: #51. + +Requirement: none. The GitHub issue is the delivery contract. This note records +architecture guardrails only; it does not implement the split, change behavior, +drain the oversized allowlist, or add release content. + +## Binding Decisions + +- Convert only the four current targets to same-named packages. Every Python + file in each package, including `__init__.py`, must remain at or below the + ADR-015 500-line cap. +- Keep the four original dotted module names as the only supported facades. + Child modules are package-private implementation details; no existing caller + moves to a child import. +- Remove only the four deleted paths from + `tools/policy/oversized_allowlist.yaml`. The fixed historical set in + `tools/policy/repo_policy.py` is policy evidence and must not change. +- The issue asks for a `CHANGELOG.md` entry, but `.gc/plan-rules.md` makes + release-please the sole owner of that file. Leave `CHANGELOG.md` untouched + and use a non-behavior-changing Conventional Commit / PR title such as + `refactor:`. +- ADR-015, ADR-016, and ADR-036 already decide the durable layering and + coverage model. No new ADR, schema, contract, registry, DTO family, service, + repository, exception hierarchy, or logging abstraction is needed. + +## Package Boundaries + +### Runtime security monitoring + +Preserve the exact ordered `__all__`. It is consumed by +`_runtime_service_family_registry.py`, which treats the module object and its +exports as the canonical runtime-family registration surface and installs +those same objects into `runtime_configuration.py` and `nodes.py`. Continue to +re-export the detection-definition identities from the existing +`runtime_security_monitoring_definitions.py`; do not copy or absorb that +already-separated concern. + +The cohesive internal boundaries are vocabulary, listener/component/agent and +content/setting inventory models, and manager aggregation/local-id validation. +They must continue to use `SDLModel`, `_base.parse_int_or_var`, and the shared +`runtime_values` parsers and redaction helpers. The manager's duplicate-id +check remains one cross-child invariant and must not be distributed into the +individual child models. + +Package conversion currently weakens two invariant tests unless their discovery +is made package-aware: `test_runtime_modules_do_not_redeclare_shared_validation_helpers` +only scans flat `runtime_*.py` files, and `_runtime_family_enums()` imports a +package but only recognizes enums defined directly on that facade. Preserve +the same recursive enforcement for child modules; do not let the split make +shared-helper or open/closed-enum drift invisible. + +### Objective semantics + +Keep `raes.semantics.objective_semantics` as the single name-level objective +authority used by the validator and compiler. Preserve the public role +constants, catalogs, analysis/reference/issue records, and the two public +functions. The existing test patches +`OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES` on the facade and then calls +`partition_objective_dependencies`; moving that function behind a simple +re-export would make it read an unpatched child-module global. Keep that +observable facade seam or use an equally narrow explicit indirection. + +Internal boundaries may separate records/catalogs, actor and target resolution, +success/window/dependency resolution, and ordered analysis coordination. Do not +duplicate `analyze_objective_window`, `AssessmentResourceKind`, target indexes, +or compiler dependency logic. Preserve reference and issue ordering, stable +deduplication, kind-qualified names, dependency-cycle behavior, and the exact +role allocation. The existing `AssessmentResourceCatalog`, +`WindowResourceCatalog`, per-category role constants, and +`partition_objective_dependencies` are the extension seams for a future +resource kind or role change. + +### Participant behavior semantics + +This package remains distinct from `raes.participant_behavior`. It owns +name-level analysis, not authored DTOs, runtime history, or processor models. +Keep the three public frozen records and `analyze_participant_behavior` at the +facade. Cohesive child concerns are base action/boundary references, observation +visibility, behavior-specification registries and governed vocabularies, +autonomous-execution checks, tool-affordance checks, and one ordered coordinator. + +`_ParticipantBehaviorSemanticRegistries.from_keywords` remains the fail-closed +shape gate for the six injected registries. Do not replace it with a permissive +bag, service locator, or global registry. Preserve `is_unresolved` as the +caller-supplied placeholder-policy seam. Continue to reuse +`raes_contracts.controlled_vocabularies`, +`raes_contracts.manifest_authority`, and +`participant_behavior_specification.tool_affordance_reference`; do not fork +their vocabularies, supported-contract ids, or canonical references. + +Diagnostic multiplicity and order are behavior. The coordinator must continue +to aggregate participant action/boundary issues, action interaction issues, +visibility issues, then behavior-specification issues; the validator remains +the sole renderer from `ParticipantBehaviorIssue.code` to authoring errors. + +### Evidence-run artifact + +The leading-underscore module becomes a leading-underscore package without +changing `raes_operations._evidence_run_artifact`. Keep +`EVIDENCE_RUN_SCHEMA` and `assemble_artifact(EvidenceArtifactInputs)` available +there. Partition section builders by backend/scenario provenance, realization +and topology, participant/observation evidence, evaluation/disclosures, and +redaction/invariant-ledger metadata; retain one assembly coordinator and the +current section order. + +`_evidence_run_types.py` remains the structural DTO/protocol boundary that +avoids a forbidden `raes_processor` dependency. Continue to use the canonical +`backend_manifest_payload`, capability-gap helpers, published +`raes_contracts` models, `expected_surface`, and `portable_artifact_ref`. +Never turn the artifact's duck-typed read surface into duplicate concrete +processor DTOs. + +Assembly is not the security or persistence gate. The existing producer must +still pass every artifact through +`validate_libvirt_evidence_run_artifact()`—schema/required sections, embedded +Pydantic contracts, redaction, participant boundary, and realization-source +checks—before `run_artifact_path()` and `atomic_write_json_artifact()` may +persist it. Preserve the fail-closed no-write outcome and bounded diagnostic +messages. + +## Compatibility And Repository Guardrails + +- Replace each `.py` and package atomically; never leave both import candidates. + Preserve every absolute and relative external import line, callable + signature/default/annotation, dataclass field order/default, enum identity, + `__all__` order, Pydantic field/validator behavior, and module-level + monkeypatch seam. +- The issue's absolute-import grep is necessary but not sufficient. Preserve + relative imports from the validator and runtime siblings, the module-object + imports in `runtime_configuration.py` and + `_runtime_service_family_registry.py`, and the transitive public identities + installed into `raes.nodes`. +- `objective_semantics.py`, `participant_behavior.py`, and + `_evidence_run_artifact.py` currently have no `__all__`. Do not add a + restrictive one casually; expose every observed import plus the original + module's intentional public definitions, without wildcard or dynamic export + discovery. +- A re-export preserves an import statement but can change `__module__`, repr, + pickle targets, introspection, and generated-schema details. Keep public + definitions in the facade where metadata or mutable facade globals are an + observed contract; otherwise prove facade object identity and schema parity. + Do not rewrite `__module__` as a blanket compatibility trick. +- Child modules import exact foundations/owners, not their package facade. + Keep one acyclic direction from records and shared helpers through domain + checks/builders to the coordinator and facade. Account for the extra package + depth in relative imports, and do not create child names that shadow the + existing `raes.semantics.objectives`, `raes.participant_behavior`, or + `raes.runtime_security_monitoring_definitions` authorities. +- Hatch's existing package-root configuration already discovers nested + packages. Do not add a distribution, entry point, import hook, or packaging + configuration. +- Deleted `.py` paths are referenced by the live semantic coverage table, + SDL/formal/reference documents, the runtime inventory gap analysis, and + `tools/check_sdl_catalog_parity.py`. Retarget live implementation links to + the package facade (normally + `...//__init__.py`) and update the parity check's expected links. + Historical decision provenance need not be rewritten. +- Generated SDL schemas include `RuntimeConfiguration` and the security + monitoring models. `schema_bundle()` and all published schema files must be + byte/content equivalent after the refactor. Schema drift is a refactor defect, + not authorization to edit published schemas or the publication manifest. + +## Cross-Cutting Layers + +- **Repository/import policy:** ADR-015/ADR-036 and + `tools/policy/adr_policy.yaml` retain the 500-line and package-direction + gates. `raes` may use only its existing `raes_contracts` dependency; + `raes_operations` retains only its currently allowed public import prefixes. +- **SDL structural validation:** `SDLModel(extra="forbid")`, Pydantic field and + model validators, variable-placeholder parsing, enum/bool/int coercion, + portable symbols, absolute paths, and duplicate-id checks remain canonical. +- **SDL semantic validation:** `SemanticValidator`, objective-window analysis, + named-reference indexes, controlled vocabularies, manifest authority, and + the existing issue-code renderers remain the only semantic/error boundary. +- **Runtime secret handling:** `RuntimeSensitivityClassification` and + `enforce_observed_value_redaction` still reject unredacted values in secret + classifications. The split must not log or surface setting values. +- **Evidence security:** allowlisted section builders feed the existing + redaction and participant/evaluator-boundary validator. Raw libvirt XML, + UUIDs, QEMU command lines, host paths, connection URIs, credentials, private + keys, and backend-private snapshots remain excluded. +- **Persistence and OS exposure:** the semantic and runtime-model packages stay + pure in-process code. Evidence assembly retains only its existing scenario + file read/hash; validated output still uses safe run-id containment and the + atomic JSON writer. No environment binding, subprocess/argv value, socket, + credential lookup, database, cache, or new filesystem surface is introduced. +- **Configuration and environment shapes:** keep + `LibvirtEvidenceRunConfig`, `EvidenceArtifactInputs`, and the Pydantic + scenario/runtime models as the explicit inputs. Backend connection-URI + credential rejection remains with + `TechVaultNativeLibvirtDriver._validate_connection_uri`; no child module may + add environment fallbacks or place a URI, token, path, or payload in process + arguments. +- **Authentication:** none of the four facades owns authentication or + authorization. Runtime control-plane and backend connection policy remain in + their existing owners; the split adds no token or identity surface. +- **Errors and observability:** preserve Pydantic `ValidationError`/underlying + `ValueError`, frozen semantic issue records rendered by the validator, and + evidence validation problem lists/gating checks. Add no logger, traceback + envelope, exception translation, audit stream, or payload dump. + +The canonical repository gates remain `tools/check_repo_policy.py`, +`tools/check_requirement_governance.py`, `tools/verify_all.py`, semantic +coverage, SDL catalog parity, generated-schema drift, and the pinned nox +`verify` session in `.ground-control.yaml`. + +## Gotchas And Anti-Patterns + +- Do not split by line ranges, create a generic “semantic rule” framework, or + merge objective, participant, runtime inventory, and evidence concepts. +- Do not move validator rendering into semantic helpers or mix semantic issue + DTOs with Pydantic contract models. +- Do not duplicate runtime parsers/redaction, objective window logic, + controlled vocabularies, manifest contract ids, evidence redaction patterns, + artifact schemas, persistence, or exception types. +- Do not reorder validation, issue aggregation, artifact sections, references, + diagnostics, mappings, or deduplication; do not sort data that is currently + insertion ordered. +- Do not import `raes_processor` from `raes` or `raes_operations`, and do not + route private child modules into callers as a shortcut around facade cycles. +- Do not weaken invariant or semantic-coverage discovery because a file became + a directory. Strengthening package-aware discovery is not test relaxation. +- Do not modify pre-existing behavioral tests to accept changed output, and do + not use this refactor to fix incidental semantics. + +## Non-Goals + +No behavior, SDL grammar, schema, contract, vocabulary, semantic rule, issue +code/message/order, compiler/planner behavior, runtime inventory meaning, +redaction policy, evidence claim, artifact format, auth policy, secret handling, +configuration, persistence, logging, packaging entry point, compatibility +namespace, published schema, version, or release-note change is in scope. diff --git a/docs/decisions/issue-713-planned-resource-payload-accessors-preflight.md b/docs/decisions/issue-713-planned-resource-payload-accessors-preflight.md new file mode 100644 index 000000000..a5818b46d --- /dev/null +++ b/docs/decisions/issue-713-planned-resource-payload-accessors-preflight.md @@ -0,0 +1,137 @@ +# Issue 713 PlannedResource Payload Accessors Preflight + +Date: 2026-08-01 + +Requirement: none. GitHub issue #713 is the authoritative contract. + +This note records the boundary for a public Python convenience API over +`PlannedResource.payload`. It is guidance only: it does not add a portable +semantic, schema, fixture, profile, backend capability, or production-backend +claim, and it is not an implementation plan. + +No ADR is needed. ADR-009, ADR-036, and ADR-063 already establish normative +authority, package ownership, and the repository-owned reference-backend +boundary. + +## Binding Sources + +- `raes_contracts.planning.PlannedResource`, `ProvisioningPlan`, + `RuntimeDomain`, `ChangeAction`, and `require_plan_operation_identity()` are + the neutral planning DTO and shape authority. +- `raes_processor.models.runtime_model.resource_payload()` is the compiler-side + producer of the stable planner payload; it is not a backend reader API. +- `raes_reference_backend.realization` and `raes_backend_libvirt.realization` + are the two consumer migrations. Their pure interpretation and existing + package-local `Diagnostic` behavior remain authoritative for realization + failure reporting. +- `raes_backend_libvirt._payload` is the immediate duplication to retire or + reduce to backend-only concerns. `capability_envelope_diagnostics()` remains + the backend capability gate, not an accessor responsibility. +- `provider_resource_name()` is the incumbent backend-native name normalizer. + `Diagnostic`, `Severity`, `RuntimeSnapshot`, `ApplyResult`, and the runtime + backend-call gates remain the public error and persistence envelopes. +- ADR-009/019/061 and the corpus/publication checks govern normative artifacts; + ADR-036 and `tools/policy/adr_policy.yaml` govern this import direction. + +## Decision And Guardrails + +Place one small, public, dependency-free accessor surface alongside +`PlannedResource` in `raes_contracts.planning` (or a directly public +`raes_contracts.planning` submodule re-exported there). It accepts only a +`PlannedResource`; it must not import a backend, processor implementation, +runtime, schema model, or provider naming helper. + +The surface is a **total read/normalization convenience API**, not validation. +It must provide mapping-shaped access to the top-level payload and the node +`spec`, `infrastructure`, `resources`, and `source` surfaces, plus a stable +authored-name fallback. A missing optional subtree, a non-mapping payload, a +non-provisioning resource, or a non-node resource must produce the documented +safe empty/absent result for the relevant accessor; it must not throw, coerce, +guess, mutate the payload, or leak its contents. Source should retain its +existing string-or-mapping distinction rather than inventing an image/source +DTO. The name accessor may fall back to the stable planned address; a backend +that needs a provider-safe runtime name continues to apply +`provider_resource_name()` at its own boundary. + +Use one consistent absent-result convention across all accessors (for example, +`None` for an unavailable scalar/subtree and an empty mapping only where the +API explicitly promises a mapping). Document the resource/domain precondition +and the absent-result behavior. Do not make `None` mean a malformed plan is +valid: realization collectors must retain their existing resource-type/domain +checks and their distinct `invalid-payload` versus `unsupported-resource` +diagnostics before using node-specific reads. + +The helper must expose existing payload projections only. It must not validate +SDL meaning, enforce capability allowlists, resolve network or placement +references, convert RAM/CPU units, normalize service entries, choose an image, +or supply defaults with operational meaning. Those decisions stay respectively +with the parser/compiler/planner, `capability_envelope_diagnostics()`, and the +backend realization/driver layers. + +Migrate both realization modules for every covered node read. Do not leave +parallel local traversal helpers for source, resources, infrastructure/spec, or +authored name. Shared generic access does not require migrating backend-only +placement, account, feature, ACL, cloud-init, or capability-envelope traversal +in this issue; those must continue to use their current local validation until +they have an equally clear shared contract. + +## Cross-Cutting And Security Gates + +| Layer | Required treatment | +| --- | --- | +| SDL, schemas, and compiler | No new input path or schema. Existing closed SDL/Pydantic validation and `resource_payload()` remain the sole producer/validator of plan payload meaning. | +| Plan DTO shape | Keep `PlannedResource` address/dependency checks and plan-domain admission unchanged. The accessor handles hostile or hand-built payload shapes defensively without changing DTO validation. | +| Capability and realization | Keep `ProvisionerCapabilities`, capability-envelope diagnostics, supported-resource checks, and pure realization diagnostics in the backends. The helper neither authorizes nor realizes anything. | +| Runtime/error envelope | Do not add exceptions, logs, or diagnostics in `raes_contracts`. Existing package-local `Diagnostic` codes and runtime `_call_backend_apply()`/snapshot validation continue to envelope failures. | +| Secrets and observability | The helper performs no IO, parsing of environment/config, logging, persistence, subprocess invocation, or serialization. Callers must not include returned payload values in diagnostic messages; existing redaction tests remain relevant. | +| Host/OS exposure | None: this layer must never reach driver configuration, libvirt, OCI, filesystem, process argv, or environment. Provider-safe naming remains in the backend protocol boundary. | +| Packaging and policy | Keep the public API within the already packaged `raes_contracts` root and the ADR-036-approved dependency direction. Run repository policy, requirement governance with the existing requirement-free setting, and hermetic verification. | + +## Evidence Required From The Implementation + +Add focused contract tests for valid node source/resources/infrastructure/name +reads; omitted optional fields; non-mapping payloads; and wrong resource type +or runtime domain. Include the fallback-name and string-versus-mapping-source +cases. Keep the existing reference and libvirt realization tests as migration +evidence: malformed payloads must still yield their existing redacted backend +diagnostic codes, and valid plans must preserve current realization output. + +## Extensibility Boundary + +The seam is a small family of `PlannedResource`-only accessors, with explicit +resource/domain applicability rather than a generic dotted-path evaluator or a +backend parameter. The next reasonable addition (for example node services or +network infrastructure) can add one named accessor with the same absent-result +contract. It must not require re-editing schemas, planner payload production, +runtime control, or either driver. If a later use needs semantic validation or a +typed portable value, that is a separate contract-evolution decision rather +than a widening of this convenience layer. + +## Gotchas And Anti-Patterns + +Avoid: + +- a second payload schema, Pydantic model, source/image DTO, exception class, + validation profile, diagnostic code set, or backend capability registry; +- a `dict`/deep-copy/serialization conversion that changes identity, accepts + arbitrary mapping-like objects inconsistently, or makes the accessor a + persistence boundary; +- treating wrong resource/domain or malformed payload as a valid empty node in + backend control flow; retain the collectors' diagnostic gates; +- provider name sanitization, network/placement resolution, resource sizing, + default-image selection, or cloud-init policy in `raes_contracts`; +- importing `raes_backend_*`, `raes_processor`, `raes_runtime`, or the legacy + `raes.*` compatibility tree from the shared helper; +- exposing raw payloads or source/build data through logs, diagnostics, + snapshots, control-plane records, or error messages. + +## Non-Goals + +- Changing normative portable semantics, schemas, fixtures, profiles, or + conformance claims. +- Making the reference or libvirt backend a production backend, transferring + ownership to LilRAE/BigRAE, or changing ADR-063's boundary. +- Refactoring all payload traversal across every backend concern, redesigning + planning DTOs, or changing planner reconciliation. +- Adding a persistence, configuration, authentication, authorization, logging, + IO, driver, or API surface. diff --git a/docs/decisions/issue-964-participant-opacity-runtime-enforcement-preflight.md b/docs/decisions/issue-964-participant-opacity-runtime-enforcement-preflight.md new file mode 100644 index 000000000..5742f52b9 --- /dev/null +++ b/docs/decisions/issue-964-participant-opacity-runtime-enforcement-preflight.md @@ -0,0 +1,535 @@ +# Issue #964 — Participant Opacity Runtime Enforcement Preflight + +Date: 2026-08-01 + +Issue: #964. + +Requirements: `SEM-231`, `RUN-319`. + +This note records repository-wide architecture guardrails for the bounded +reference-runtime enforcement lane. It is guidance only. It does not publish a +runtime profile or schema, change runtime behavior, advance catalog assurance, +establish opacity, or make a backend-realization or conformance claim. + +## Decisive Current-State Finding + +Issue #964 must close and constrain the existing SEM-231/RUN-319 seam. It must +not create an opacity monitor beside the participant crossing boundary. + +- ADR-099 and the SEM-231 formal authority already own the one-sided opacity + relation, observer information cell, secret predicate, memory, active + strategy, supervisor visibility, release, order, time, probability, and + relation boundaries. +- `BehavioralRelationProfileModel`, the corpus-backed profile loader, + `BehavioralClaimBindingModel`, and `validate_behavioral_claim_binding()` + already own profile identity and claim admission. The claim model already + has the `runtime-enforcement/enforced` axis, and deliberately prevents finite + runtime evidence from carrying a universal quantifier. +- The current `participant-opacity-baseline-v1@sem-231/rev3` artifact is not a + live-runtime profile. Its carrier, observer, predicate, scheduler, + environment, and cuts are fixture identities. Relabeling it, changing its + evidence axis, or applying it to arbitrary runtime state would be a false + join. Earlier bounded, model-check, and proof evidence must retain their exact + historical catalog/profile coordinates. +- RUN-319 already supplies authenticated subject and audience binding, + deny-first exact-cut crossing decisions, capability admission, governed + transformation, scoped idempotency, append-only API-423 history, + expected-head commits, restart validation, and participant-view projection + before serialization. +- API-423 already distinguishes requested, decided, transformed, disclosed, + delivery-attempted, delivered, observed, and audited facts. The runtime + currently emits only requested, decided, and sometimes transformed crossing + facts. Delivery, observation, omission/opportunity, retry, policy-change, + evidence, and audit visibility are therefore not established merely because + their portable stage types exist. +- The live crossing path currently covers ordinary action admission, API-409 + participant control, and governed status/history/context view serialization. + Execution-service control/readback, episode initialize/reset/restart/ + terminate, the v2 decision-surface selection call, autonomous scheduler and + clock activity, operation status, administrative snapshot/apparatus reads, + `audit_log()`, direct backend calls, and observable error/status/timing + behavior are separate reachable surfaces. A supported opacity declaration + must either mediate each surface retained by its observer profile or prove it + unreachable and fail admission when that fact changes. +- `commit_participant_transition()` is the correct persistence owner, but its + current history-head cut covers behavior, control, and crossing streams only. + A runtime-opacity cut cannot ignore episode, scheduler/time, operation, + delivery, policy/profile, or evidence state when one of those coordinates + affects the declared observation. + +The bounded meaning of `runtime-enforcement` for this issue is therefore: + +> For one exact, finite, revisioned runtime profile and carrier, every admitted +> in-scope observation is produced through the shared crossing boundary under +> one fresh trusted state cut, and any unknown, stale, out-of-carrier, or +> unmediated case is refused before observation or is converted by an already +> authorized weakening into an explicit nonclaim. + +This is runtime containment of an admitted finite opacity profile. It is not a +live computation of arbitrary possible worlds, an online proof of the SEM-231 +hyperproperty, or a claim about an unbounded deployment. + +## Architecture Decisions And Guardrails + +### Bind one runtime support declaration to the shared profile and claim seams + +The runtime needs one closed support declaration that composes, rather than +copies: + +- exact catalog id/revision and `participant-predicate-opacity`; +- exact relation-profile id/revision/digest; +- exact finite normalized carrier/materializer identity and digest; +- observer participant or coalition and audience identity; +- secret-predicate ref/revision, never its raw value or evaluator body; +- an exact observation-surface inventory ref/revision/digest; +- trusted state-cut, projection, release, memory, opportunity, order, and + enforcement-rule refs/revisions; +- limitations and explicit nonclaims; and +- one validated `BehavioralClaimBindingModel` with + `assurance_axis=runtime-enforcement`, `assurance_status=enforced`, and a + finite, non-universal evidence boundary. + +The declaration is an axis-specific support/evidence join, not another +relation definition, policy binding, backend manifest, or claim DTO. If a +portable contract is needed, it belongs in `raes_contracts`, remains closed +under `ContractModel(extra="forbid")`, embeds or references the incumbent +claim/profile authorities, and follows normal schema publication. Runtime code +must not encode this declaration as an open dictionary, SDL metadata, +`RuntimeSnapshot.metadata`, an environment-variable bundle, or a set of +booleans. + +Do not mutate the fixture-bound baseline profile into a runtime profile. Add +an exact runtime profile through the existing behavioral-relation profile +family and closed discriminator seam. Preserve the current profile and every +earlier evidence revision. If the behavioral catalog advances, archive the +exact prior catalog and keep historical resolution working; do not relabel old +bounded/model-check/proof evidence with the new revision. + +The #961 `ParticipantOpacityAnalysisEvidenceModel` may be cited as bounded +evidence, but its `normalized-input-only` provenance explicitly does not +authenticate the source or materializer. It cannot authorize a runtime by +itself. A trusted runtime-owned materializer/resolver must recompute the exact +profile/carrier/cut joins or supply independently admitted immutable joins. +Changed profile, carrier, materializer, policy, inventory, or enforcement-rule +identity invalidates admission. + +### Enforce through the existing crossing resolver and operation boundary + +Compose runtime-opacity admission and decision data into the existing +`ParticipantCrossingPolicyResolver` / `ParticipantCrossingPolicyResolution` +seam. `RuntimeControlPlane`, its public in-process methods, and its HTTP adapter +must converge on the same operation-bound mediation. The runtime supplies the +state cut and completed decision; adapters supply only bounded intent and an +authenticated identity. + +Do not add an opacity gateway, response-filter middleware, detached recorder, +belief-state store, world-state store, background monitor, or backend-local +policy engine. Do not run the bounded checker on each request. The finite +checker admits or falsifies an exact carrier; the runtime boundary keeps the +executed observation inside that admitted carrier and records the result. + +The trusted resolver may evaluate a secret predicate internally only through a +revisioned injected evaluator bound by the profile. Raw truth values, +alternative points, information-cell keys, participant memory, policy bodies, +or supervisor internals never cross into API-423, operation, audit, diagnostic, +log, filename, or process surfaces. + +### Treat observation coverage as a closed admission invariant + +The observation inventory is exhaustive for the named observer, not a list of +the routes edited by #964. Each entry needs a stable surface id, owning carrier +or method, observer/audience, occurrence/content/absence treatment, projection +and order basis, opportunity or timing basis where applicable, and one of: +mediated, provably unreachable under the admitted configuration, or +unsupported. Unknown entries and reachable unsupported entries reject the +positive claim. + +The repository surfaces that must be classified include: + +| Surface family | Canonical owner and guardrail | +| --- | --- | +| Action and active probes | `ParticipantControlMixin.admit_participant_action()`, v1/v2 decision-surface selection, SEM-211 admission, and autonomous scheduler action paths. Every allowed probe uses the same crossing path; a passive profile must make all participant-driven probes unreachable rather than ignore them. | +| Supervisor/control | API-409 control occurrences and `ParticipantCrossingControlIngressMixin`. Approval, denial, edit/direction, intervention, handoff, override, cancellation, deferral, and occurrence/content visibility remain distinct observations. | +| Episode and execution lifecycle | initialize, reset, restart, terminate, execution-service control/readback, participant clock and scheduler reset. A runtime reset is a state transition, never participant forgetting unless the profile names a trusted reset rule covering every retained channel. | +| Participant egress | `ParticipantRetrievalMixin`, `serialize_participant_view()`, status/history/context views, directed inject delivery, SEM-226 exposure, and API-423 disclosure/delivery/observation stages. Projection precedes serialization and the required durable stages precede return. Delivery is not inferred to be observation. | +| Failure and omission | Operation receipts/status, rejection/error envelopes, withheld/failed/unsupported delivery attempts, retries, acknowledgements, timeouts, and declared opportunities. Silence is not evidence: observable omission is represented by a durable incumbent occurrence at a governed opportunity cut. | +| Time and order | Existing time-model, scheduler, logical/causal order, delivery order, and policy-cut authorities. The baseline may use governed logical opportunity or discrete timing labels; wall-clock latency, progress-sensitive behavior, jitter, and timed opacity remain unsupported. | +| Policy/release change | Exact-cut API-423 policy refs, SEM-226/230 release and declassification, projection refresh, controller/authority changes, and visible effects. Hiding a revision identifier does not hide changed behavior. | +| Retrieval/evidence/audit | Operation reads, `/snapshot`, apparatus summary, `audit_log()`, crossing/control histories, evidence refs, and any future evidence endpoint. Administrative authorization is not participant visibility. If such a reader is the observer or coalition member, the surface belongs in that profile. | +| External effects | Backend calls, target readback, participant execution, resource/scheduler effects, and retry/replay behavior. A reference-runtime claim stops at the declared boundary and cannot silently include direct adapter/native-backend use. | + +An observation inventory that names only payload, decision, delivery, retry, +latency, and order at a coarse semantic level is insufficient for runtime +enforcement. The runtime support declaration binds the concrete surface +inventory while the SEM-231 profile continues to own semantic observation +meaning. Do not duplicate relation coordinates in each route or service. + +### Represent omissions and timing without inventing knowledge + +An omission is observable only when a profile binds an existing schedule, +opportunity, acknowledgement, logical deadline, or progress basis. Reuse +API-423 delivery-attempted dispositions and the incumbent scheduler/time or +owning occurrence reference where they express the fact. Emit a positive +durable withheld/failed/unsupported fact at the declared cut; absence of a log +entry cannot prove that an opportunity was observed and missed. + +The current SEM-231 baseline is untimed and progress-insensitive. A discrete +logical timing bucket may be an observation label only when its bucket rule, +clock authority, cut, and order are governed and digest-bound. Do not sleep, +add random delay, read ambient wall time, or treat nondeterministic scheduling +as opacity evidence. Quantitative latency, deadlines based on elapsed host +time, progress-sensitive opacity, and probability-bearing claims require a +different governed relation/profile and evidence. + +### Keep the durable claim on the owning API-423 decision + +The durable crossing decision is the portable owner of the runtime-enforcement +fact. It must carry a compact, safe, typed binding to relation, exact profile, +secret-predicate ref/revision, observation-inventory ref/revision/digest, and +`assurance_axis=runtime-enforcement`, plus the claim/evidence reference and +limitations. It must not carry the predicate result, raw secret world, cell, +witness, memory, policy body, supervisor state, or alternative execution. + +If API-423 cannot carry that binding without ambiguity, evolve its existing +decision component through normal compatible schema publication and contextual +validation. Do not place the only binding in `AuditEvent.details`, operation +diagnostics, `result_payload`, snapshot metadata, a parallel opacity history, +or a new database. Audit receives safe correlation refs only and remains an +authorized evidence surface, not semantic authority. + +The API-423 context validator remains the single cross-record join owner. It +must resolve the compact binding against the exact claim, catalog, profile, +predicate authority, inventory, policy/cut, evidence, predecessor, participant, +episode, audience, and order indexes. Local Pydantic validators own only +closed-shape and single-record invariants. Routes, mediation, stores, replay, +and tests must not each reimplement this join. + +### Extend the existing atomic cut; do not add persistence + +`RuntimeSnapshot`, `ControlPlaneStore`, `InMemoryControlPlaneStore`, and +`LocalControlPlaneStore` remain the only state owners. Reuse +`commit_participant_transition()` and its expected-head write-set semantics so +the applicable crossing stages, operation state/result, safe audit +correlation, profile/inventory cut, and any participant-visible result become +durable atomically before output. + +The commit precondition must cover every observation-affecting head or digest +named by the profile, not just the three current participant history heads. +Episode state, policy/profile/inventory revision, release, scheduler/time, +delivery/retry/opportunity, operation status, controller/authority, evidence, +and relevant backend support are part of the cut when the profile retains +them. A changed coordinate causes a fresh decision; it is not repaired by +updating the fingerprint after resolution. + +Intermediate `RUNNING`, authorization, delivery-attempted, failure, retry, and +commit-conflict states are observations when the profile retains them. A +two-commit action path must not expose an unclassified intermediate result. +Backend dispatch or participant serialization cannot precede the durable +authorization stage. If an external backend effect cannot be rolled back when +final persistence fails, that path is outside the bounded reference-runtime +claim and must be refused or disclosed as a weakening that removes the opacity +claim. + +Restart first parses closed snapshot models, validates append-only prefixes, +and re-resolves API-423 plus runtime-opacity context before serving or mutating +state. Missing legacy crossing/profile/inventory history, unresolved historical +profiles, stale digests, truncated state, or a reset that merely clears +runtime-local caches fails closed. The issue #802 legacy-presence distinction +must not be collapsed into “empty means no prior knowledge.” + +Idempotency reuses the incumbent scoped key, semantic fingerprint, operation +record, and result-history cut. The fingerprint binds every profile, +predicate, observer/audience, inventory, policy/release, rule, subject, +marking, opportunity, order, capability, and expected-head coordinate that can +affect observation. An exact retry returns the same durable result without a +new decision. A stale retry, replay, handoff, reset, policy revision, or +inventory change conflicts and discloses no protected detail. + +The local store remains a single-process, single-writer reference +implementation. Atomic replacement and an in-process lock do not establish +multi-process transactions, distributed linearizability, or crash atomicity +with native external effects. + +### Fail closed or remove the claim under explicit weakening + +Malformed, incomplete, unsupported, stale, cross-cut, out-of-carrier, or +unmediated profile coordinates fail before a participant-visible result. The +only alternative is an already policy-authorized disclosed weakening that: + +- records the exact safe limitation/loss and authorization basis; +- does not retain or emit the positive opacity runtime-enforcement binding; +- does not mutate an earlier decision or erase retained observation; and +- does not treat API-407 backend weakening as authority to weaken SEM-231. + +No “best effort opacity,” warning-only bypass, implicit default profile, +`latest` revision, current-snapshot fallback, or randomized response is +permitted. + +### Advance assurance narrowly + +One exact supported runtime profile may carry a finite +`runtime-enforcement/enforced` claim. The catalog-level +`runtime_enforcement_status` becomes at most `partial` because #964 supports a +bounded subset. `implementation_status` remains consistent with the positive +checker/runtime axes. Backend declaration, realization, and conformance remain +negative until #965 supplies their separate evidence. + +Runtime tests and mediation do not change the result relation to +policy-noninterference, projected-history equality, epistemic +indistinguishability, trace equivalence, simulation, refinement, or +bisimulation. They do not relabel bounded checking as model checking or the +Isabelle theorem as a concrete runtime proof. + +## Canonical Incumbents To Reuse + +| Concern | Canonical incumbent and required use | +| --- | --- | +| Opacity semantics | ADR-099 and `specs/formal/participant-semantics/participant-predicate-opacity.md`; do not redefine the information cell, possible points, memory, strategy, release, supervisor, or relation boundary in runtime code. | +| Shared profile and claim | `BehavioralRelationProfileModel`, corpus-backed exact-revision loaders, `BehavioralClaimBindingModel`, `BehavioralRelationCatalogModel`, `validate_behavioral_claim_binding()`, and RFC 8785 canonical digests. Add no runtime-only relation registry or claim model. | +| Bounded admission evidence | `ParticipantOpacityAnalysisInputModel`, `ParticipantOpacityAnalysisEvidenceModel`, the #961 deterministic checker/replay, and their authenticity nonclaim. Recompute trusted joins; never treat a passing fixture as runtime authority. | +| Information-flow and crossing | ADR-085/095, SEM-230, API-423 `ParticipantCrossingOccurrenceModel`, `validate_participant_crossing_occurrence_context()`, and all distinct crossing stages. Reference carriers; do not copy payloads or collapse stages. | +| Runtime mediation | `RuntimeControlPlane`, `ParticipantCrossingPolicyResolver`, `ParticipantCrossingPolicyResolution`, `prepare_participant_crossing()`, crossing ingress/egress/control boundaries, SEM-211 admission, and SEM-226 exposure. Extend this seam instead of adding a monitor. | +| Identity and authorization | `ControlPlaneSecurityConfig.strict_defaults()`, `_ControlPlaneApiAuth`, `ControlPlaneIdentity`, `ControlPlaneRole`, target binding, and separate participant controller/audience bindings. Caller role never establishes participant visibility or opacity. | +| Capability | `PARTICIPANT_RUNTIME_POLICY_FEATURES`, required-contract mappings, and `resolve_participant_feature_support()`. Capability is a gate and posture, not relation evidence or weakening authority. | +| Runtime carriers | Participant episode, behavior, control, crossing, delivery/observation, execution-service, scheduler/time, resource, operation, evidence, and audit carriers. Reuse their lifecycle and append-only validators; do not create opacity copies. | +| Persistence | `RuntimeSnapshot`, `ControlPlaneOperationRecord`, `AuditEvent`, `ControlPlaneStore.commit_participant_transition()`, both shipped stores, atomic replacement, expected-head checks, restart parsing, and append-only history validators. Add no opacity side store. | +| Diagnostics and HTTP | `Diagnostic`, `Severity`, `OperationReceipt`/`OperationStatus`, request-size guards, `_request_fingerprint()`, idempotency, governed retrieval response models, and the exact redacted 500 envelope. Add no exception hierarchy or logger. | +| Contract publication | Hand-governed `contracts/schemas/`, valid/invalid fixtures, `schema_bundle()`, schema-publication entries/hashes, compatibility classification, `tools/check_generated_schemas.py`, and `tools/check_schema_publication.py`. | +| Claim/concept governance | `contracts/concept-authority/behavioral-relations-v1.json`, its historical revisions and fixtures, `tools/check_behavioral_relation_claims.py`, concept-authority gates, and all claim-bearing consumers. Advance the current revision once; do not rewrite historical evidence. | +| Workflow | `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, repository/requirement policy, JSON/schema/concept/docs gates, `tools/verify_all.py`, and `RAES_REQUIREMENT_UID=SEM-231` on this issue-number branch. Add no issue-local runner. | + +Package ownership remains: `specs` owns semantics; `raes_contracts` owns +portable profiles, claims, and crossing/evidence shapes; `raes_processor` owns +pure bounded analysis and trusted compilation/materialization; `raes_runtime` +owns live mediation/security/state/persistence; `raes_backend_protocols` owns +capability declarations; and `raes_conformance` owns backend probes. #964 must +not move runtime policy into the checker or backend claims into runtime state. + +## Cross-Cutting Layers And Security Posture + +1. **Profile/config ingress.** Runtime support resolves exact grammar-checked + ids through the canonical corpus loader or receives an explicitly trusted + typed in-process object. Bounded UTF-8 JSON rejects duplicate keys, + non-finite numbers, unknown fields, path traversal, caller-selected roots, + arbitrary imports, URLs, expressions, and `latest` aliases. No profile or + policy comes from a request, snapshot metadata, or ambient environment. +2. **Closed shape and semantic join.** Published schema, `ContractModel`, local + validators, the behavioral claim/profile/catalog validator, and the API-423 + contextual validator agree on exact revisions, digests, observer/audience, + predicate, carrier, inventory, projection, policy/cut, order, evidence, and + axis. A valid string ref or Pydantic shape alone is insufficient. +3. **Runtime support admission.** The trusted materializer and enforcement + rule match the exact finite carrier; the inventory is complete; every + enabled participant surface is mediated or unreachable; every unsupported + dimension is explicit; and the finite claim uses no universal quantifier. + Vacuity, incomplete enumeration, untrusted #961 source assertions, or a + stale digest cannot admit enforcement. +4. **HTTP size and value bounds.** `request_size_guard_response()` bounds body + bytes before parsing, then closed request DTOs validate the body. The + current guard does not bound path, query, or header values, and FastAPI's + default request-validation response can echo rejected input. Every touched + participant path/query/idempotency value needs the incumbent bounded-value + rules and a sanitized validation envelope before it belongs to a supported + profile. +5. **Authentication and subject binding.** Bearer or verified-proxy identity + passes `_ControlPlaneApiAuth`, role and exact target checks, then separate + controller or audience binding. Tokens and identity headers never enter + fingerprints, crossings, claims, diagnostics, audit details, or logs. + Operator/auditor/backend roles do not imply participant authority or + observer membership. +6. **Deny-first policy and capability.** Participant authority, admission, + visibility, markings, declassification, transformation, backend support, + opacity-profile admission, and observation-surface coverage are independent + gates at the same state cut. `NOT_APPLICABLE`, unknown, stale, missing, or + unsupported at a required gate denies. +7. **Projection, delivery, and omission.** Only a validated governed carrier + is serialized. Required disclosure, delivery-attempted, delivered, + observed, retry, and audited facts remain distinct and are committed only + with their owning evidence. An omission has an explicit opportunity basis; + delivery never proves observation. +8. **Persistence, restart, and concurrency.** Complete candidate state passes + snapshot, append-only transition, API-423 context, and runtime-opacity joins + before the expected-head atomic write set commits. Restart repeats those + joins before service. Concurrent or stale cuts conflict without output; + local atomicity is not a distributed claim. +9. **Expected-error envelope.** Denials and unsupported outcomes use stable, + bounded, value-independent `Diagnostic` codes/messages and uniform public + status/detail behavior for protected existence. Do not expose `str(exc)`, + Pydantic `input`/`input_value`, participant/episode existence, hidden refs, + gate differences, policy inventory, traceback, or backend objects. Existing + route paths that interpolate exception or unknown participant text cannot be + reused unchanged for a supported observer. +10. **Unexpected-error envelope.** The HTTP adapter retains exactly + `{"detail":"internal server error"}` while audit records only a safe + exception class/code and correlation. Failure latency and status are still + observations when the profile retains them; redacted content alone does + not close the channel. +11. **Secret, audit, and logging boundary.** Profiles, crossings, snapshots, + operations, audit, diagnostics, tests, evidence, docs, stdout/stderr, and + host logs carry safe refs, digests, codes, markings, counts, limitations, + and nonclaims only. Raw predicate values/worlds, cell/witness data, + participant private memory, policy/supervisor internals, payloads, + credentials, rejected values, environment dumps, and tracebacks are + excluded. Hashing secret-bearing content does not make it safe metadata. +12. **OS/process exposure.** #964 needs no new environment binding, secret + loader, CLI policy/profile argument, subprocess, shell, daemon, socket, + privilege, or host file. Typed config and corpus artifacts are injected + in-process; no token, policy, profile contents, secret, witness, evidence + payload, or full result enters argv, environment variables, filenames, + shell history, stdout, stderr, or host logs. The existing HTTP socket and + local-store directory remain their already governed deployment surfaces. +13. **Governance/publication.** Any changed portable contract moves with its + hand-governed schema, fixtures, publication manifest/hash, generated bundle, + compatibility review, package exports, packaged-corpus tests, and all + embedding consumers. Catalog/profile history, claim policy, docs, and + requirement traceability advance together through the canonical nox and + policy graph. + +## Whole-Repository Surfaces In Scope + +- **Normative and concept authority:** ADR-099, SEM-231, SEM-230/RUN-319 + composition, the current and historical behavioral catalog, the shared + relation profile family, claim validation, assurance status, and explicit + nonclaims. +- **Published contracts:** relation profile and any narrow runtime-support + declaration, API-423 decision/stage shapes, runtime snapshot and operation + envelopes when changed, their schemas, fixtures, publication records, and + packaged-corpus parity. +- **Runtime:** control-plane construction, crossing resolver/mediation, + ingress/control/egress, participant lifecycle and execution-service paths, + decision surfaces, directed injects, scheduler/time/retry paths, operation + and administrative reads, snapshot, audit, store, restart, and concurrency. +- **Security and observability:** HTTP guards/auth, participant controller and + audience bindings, error/status/latency behavior, safe diagnostics, + idempotency/fingerprints, evidence/audit visibility, operational summaries, + and the absence of a new logging channel. +- **Analysis and claims:** exact #961 profile/input/evidence resolution, + runtime-owned materializer authenticity, finite claim binding, catalog + partial-enforcement evidence, replay, and historical evidence preservation. +- **Verification:** profile/admission negatives, boundary bypass tests for each + enabled surface, decision/content/omission/timing/retry/reset/policy-change/ + audit cases, atomic/idempotent/restart/concurrency tests, sanitization and + existence-leak tests, claim/catalog/profile joins, schema/concept/docs gates, + and canonical repository verification. +- **Host/runtime:** the existing in-process reference runtime, HTTP adapter, + local single-writer store, optional backend call boundary, filesystem + containment, and no additional ambient secrets, network services, + subprocesses, or privileges. + +## Extensibility Seam + +The stable seam is: + +```text +catalog relation + exact relation profile + -> finite admitted carrier + trusted runtime materializer + -> observation-surface inventory + enforcement rule + -> existing crossing resolver at one exact state cut + -> API-423 stages + atomic operation/audit evidence + -> one finite runtime-enforcement claim +``` + +It is parameterized by exact profile/predicate, observer/audience, inventory, +materializer/rule, policy/release/memory, opportunity, order, expected heads, +and evidence identities. A second safe predicate, participant, audience, +logical opportunity class, or newly governed carrier plugs into those closed +artifacts and resolver indexes rather than adding route branches or fields to +every participant DTO. + +A new surface kind extends the common inventory vocabulary and mapping once. +A durable multi-writer store implements the same expected-cut write-set seam. +Active strategies require every allowed action path and same-strategy carrier +mapping. Coalitions require an explicit fused-observation/memory resolver. +Timed, progress-sensitive, probabilistic, quantitative, partial-order, or +mathematically different opacity is not hidden behind this seam; it requires +the corresponding governed relation/profile and independent evidence. + +## Gotchas And Anti-Patterns + +Avoid: + +- relabeling `participant-opacity-baseline-v1@sem-231/rev3`, the #961 bounded + outcome, #962 model check, or #963 theorem as runtime evidence; +- reporting runtime enforcement for arbitrary live state when the admitted + carrier is finite, fixture-bound, stale, vacuous, incomplete, or not + authentically materialized; +- implementing an online possible-world/belief-state engine or storing raw + secret truth, information cells, witnesses, or participant memory; +- creating an opacity policy engine, resolver stack, gateway, middleware-only + filter, side store, history, audit stream, exception hierarchy, logger, + schema registry, claim DTO, backend manifest block, or workflow; +- putting runtime meaning into a consumer-local enum, open mapping, metadata, + audit details, error prose, route name, or method-presence check; +- inventorying only payloads while omitting decision occurrence/content, + denial/withholding/failure, action availability, lifecycle, delivery, + acknowledgement, omission, retry, order, timing labels, release/policy + effects, retrieval, evidence, audit, operation status, or external effects; +- treating a disabled HTTP route as proof that an in-process method, scheduler, + backend adapter, retry/replay path, administrative read, or error side channel + is unreachable; +- permitting a passive profile while any participant-controlled probe path is + enabled outside mediation, or comparing active actual/witness points under + different strategies; +- treating a response payload filter, generic 403, random delay, jitter, + nondeterministic supervisor, or a hidden policy body as opacity; +- treating silence as an observable omission without a governed opportunity, + or treating wall-clock latency buckets as untimed baseline evidence; +- treating delivery as observation, audit retention as participant delivery, + authorization as visibility, redaction as declassification, reset as + forgetting, revocation as erasure, or backend downgrade as relation + weakening authority; +- allowing v2 selection, lifecycle/execution control, autonomous scheduling, + operation/snapshot/apparatus retrieval, or direct backend use to bypass the + supported crossing path; +- binding the decision to current/final snapshot state, timestamp, receipt + order, or only the crossing-history head instead of every relevant exact-cut + coordinate; +- independent snapshot/operation/audit writes, output before commit, + last-writer-wins, retry across an advanced cut, or a claim of distributed + atomicity from file replacement and an in-process lock; +- retaining a positive opacity claim after fail-open behavior, partial + coverage, unsupported dimensions, disclosed weakening, external side effect + without durable finalization, or missing restart context; +- emitting universal quantifiers from finite runtime evidence or setting the + catalog runtime status to generally `enforced` for one supported subset; +- exposing raw `str(exc)`, FastAPI/Pydantic rejected input, participant + existence, secret-bearing refs/digests, policy/gate inventories, backend + diagnostics, tracebacks, tokens, headers, or environment/process data; and +- advancing only Python, only the schema, only the profile, only the catalog, + or only one embedded claim producer while leaving publication/history and + consumers stale. + +## Non-Goals And Implementation Boundary + +Issue #964 may add one exact finite runtime-support/profile declaration, +compose its trusted admission and observation inventory into RUN-319, extend +the existing API-423 decision/stage and expected-cut persistence seams only as +needed, mediate the explicitly supported reference-runtime surfaces, and +publish finite boundary/security evidence plus one exact +runtime-enforcement claim. + +It does not: + +- establish general, unbounded, timed, progress-sensitive, probabilistic, + quantitative, coalition, all-strategy, all-schedule, partial-order, or + whole-deployment opacity; +- prove opacity, synthesize a supervisor, run the finite checker as a live + monitor, or authenticate arbitrary #961 source/materializer assertions; +- implement backend-native realization, backend declaration or conformance, + cross-backend equivalence, distributed ordering, or external-effect + transactions; +- add SDL syntax, a secret-predicate expression language, policy programming + language, participant gateway, new transport, UI, credential broker, + provider integration, arbitrary plugin/callable loading, or generic + evidence service; +- replace or duplicate SEM-211 admission, SEM-226 exposure, SEM-230 policy, + API-409 control, API-423 crossings, RUN-319 mediation, API-407 capability, + participant lifecycle/history, scheduler/time, evidence/provenance, audit, + or persistence authorities; +- expose raw secrets, possible worlds, belief state, participant private + memory, policy/supervisor internals, hidden backend state, rejected input, or + backend-private objects in portable or observable records; or +- claim policy noninterference, projected-history equality, epistemic + indistinguishability, trace inclusion/equivalence, simulation, refinement, + strong/weak/branching bisimulation, erasure, differential privacy, or + quantitative leakage bounds. diff --git a/docs/decisions/issue-965-participant-opacity-backend-realization-preflight.md b/docs/decisions/issue-965-participant-opacity-backend-realization-preflight.md new file mode 100644 index 000000000..3f44e9913 --- /dev/null +++ b/docs/decisions/issue-965-participant-opacity-backend-realization-preflight.md @@ -0,0 +1,372 @@ +# Issue 965 participant opacity backend realization preflight + +Date: 2026-08-01 + +Issue: #965. + +Requirements: `SEM-231`, `API-407`, `ASR-535`. + +This note records repository-wide architecture guardrails for declaring and +falsifying backend support for the bounded participant-opacity profile. It is +guidance only. It does not add a feature term, contract, probe, backend +behavior, evidence, or assurance claim, and it is not an implementation plan. + +No new ADR is required. ADR-060 already owns API-407 participant feature +support, ADR-081 owns behavioral claims, and ADR-099 owns participant-relative +predicate opacity and its independent assurance axes. + +## Decisive current-state finding + +Issue #965 belongs at the composition of three existing seams, not in a new +opacity subsystem: + +- `ParticipantFeatureSupport`, the governed participant-runtime vocabularies, + `PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS`, and + `resolve_participant_feature_support()` own declaration, strength, required + contracts, evidence, limitations, disclosures, and authorized downgrade. +- `participant-opacity-runtime-reference-v1`, + `ParticipantOpacityRuntimeSupportModel`, and + `validate_participant_opacity_runtime_enforcement()` own the exact supported + SEM-231 profile, carrier/materializer, observer, predicate reference, + observation inventory, rule, and finite runtime-enforcement join. +- `run_target_conformance()`, `ConformanceCaseResult`, + `BackendConformanceReport`, `BehavioralClaimBindingModel`, and the report + validator/serializer own finite target evidence and claim honesty. + +The missing distinction is orthogonal to the existing execution basis: + +```text +execution basis: fixture-only | hermetic-live | native-live +realization owner: declaration-only | runtime-mediated | backend-native +``` + +`native-live` currently describes an execution substrate. It does not prove +that opacity was realized by backend-owned behavior. Conversely, an in-process +reference backend can execute backend-owned code without a native daemon. The +report must therefore retain a separate realization-owner coordinate; target +name, method presence, backend invocation, or `native_conformance=True` cannot +stand in for it. + +A positive backend-native result must be sensitive to backend behavior: an +adversarial backend that leaks or fabricates the same case must fail while the +runtime configuration is held fixed. If replacing the backend with a no-op or +dishonest implementation leaves every case passing because RUN-319 refused or +normalized the observation first, the evidence establishes runtime mediation +only. ADR-099 consequently forbids a positive `backend-conformance` opacity +claim until the exact profile has at least partial backend realization. + +## Architecture decisions and guardrails + +### Extend API-407 once, without turning opacity into policy + +Use one governed participant-runtime behavior feature for the relation family, +`participant_predicate_opacity`. The feature id is a capability selector, not +the secret, observer, profile, relation definition, or assurance result. Exact +support remains bounded by the referenced SEM-231 profile and claim bindings. + +Do not add opacity to `PARTICIPANT_RUNTIME_POLICY_FEATURES`: opacity is a +relation assurance concern, not a participant policy operation. Keep policy +membership separate from the incumbent rule that selected features require an +explicit declaration, evidence, limitations, disclosures, and fail-closed +admission. Generalize that evidence-required predicate once or add a distinct +relation-feature set in manifest authority; do not fork `ParticipantFeatureSupport` +or its Pydantic/dataclass validators. + +The canonical term-level minimum contract set remains +`PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS`. It must cover every +portable carrier actually used by the named profile's mediated positive and +adversarial cases. For the #964 reference profile that closure includes the +incumbent operation receipt/status, runtime snapshot, participant episode +state/history, participant behavior history, participant control occurrence, +participant crossing occurrence, and participant observation contracts. Exact +profile coverage is still resolved from the shared runtime support binding and +concrete observation inventory. Do not copy that inventory into the manifest, +infer support from the full backend contract list, or create an +opacity-specific backend profile under `contracts/profiles/backend/`. + +Below-exact or authorized weakening uses the existing support-level, +constraint, limitation, disclosure, policy-ref, and provenance-ref rules. A +weakening removes the stronger opacity claim; it never changes SEM-231 meaning, +edits a prior result, or turns a runtime-mediated result into backend-native +realization. + +### Reuse the exact runtime profile and shared claim model + +Backend artifacts reference the exact catalog/profile and the compact safe +coordinates already admitted by #964. They do not restate the predicate body, +possible points, observer memory, policy/supervisor implementation, alternative +worlds, observation semantics, or relation formula. + +Use three independently validated `BehavioralClaimBindingModel` values: + +- `backend-declaration/declared/structural` for the exact manifest feature + entry; +- `backend-realization/realized` with `structural` or `finite` scope for + backend-owned implementation evidence; and +- `backend-conformance/conformant/finite` for the named executed cases. + +All three must join to the same catalog revision, relation, profile id/revision, +carrier, observation projection, backend implementation/configuration identity, +and safe evidence lineage. A declaration does not imply realization; a +realization does not imply conformance; a conformance run cannot manufacture a +missing declaration or realization. + +The report-level relation remains `bounded-probe-success`. The SEM-231 axis +bindings describe the obligation and assurance lane within the report; they do +not promote the finite report itself to universal predicate opacity. Keep the +finite quantifier, limitations, failed/unsupported cases, and nonclaims visible. + +Do not overload the SEM-230-specific `ParticipantPolicyBinding` or add an +opacity-local claim class. If the current case shape cannot carry the three +bindings, extend the existing report family with the smallest generic +collection of incumbent claim bindings plus exact relation-profile and +backend/tool/environment digests. Do not reuse `envelope_digest` for a relation +profile or hide required coordinates in case names and prose. + +### Keep one backend-neutral conformance runner + +The extension seam is `_TargetConformanceOptions` and an injected, typed, +in-process probe harness, following the ASR-535 participant-policy and ASR-519 +realization-honesty patterns. The harness supplies exact profile-bound inputs, +safe case labels, expected observations, and a deterministic probe-set digest. +The runner constructs or invokes the target, observes effects, and owns the +verdict. A harness must not return a pass boolean, prebuilt report, claimed +observation, arbitrary callable/module path, or backend-private object. + +Backend-specific setup and observation stay behind that structural harness. +`raes_conformance` remains backend-neutral and must not import the reference or +libvirt backend. A conformance-owned direct backend probe may establish a +backend-native owner, but it is not a new public participant route and does not +change #964's runtime reachability inventory. Production dishonest modes, +backend-name dispatch, monkeypatching the gate under test, and a second runner +are prohibited. + +The finite matrix must compare secret and nonsecret cases under the same +profile and active strategy, and must observe the complete declared transcript, +not payload alone. At minimum it preserves distinct checks for decisions and +failures, action availability, delivery and governed omission opportunities, +retry/replay, logical order and timing buckets, policy/release effects, and +participant-visible external effects. Omission requires the profile's explicit +opportunity basis. Untimed support uses governed logical buckets; sleeps, +ambient wall time, jitter, or randomized response are not opacity evidence. + +### Validate and finalize one report before persistence + +`validate_backend_conformance_report()` remains the cross-field honesty seam. +It validates every nested claim against the catalog and profile. It requires +each cited case to be present. It also binds exact backend, manifest, profile, +tool, environment, and probe-set digests. It rejects axis mismatches and +enforces this progression: + +```text +declared -> at least partially backend-realized -> finitely conformant +``` + +Failed, unsupported, skipped, weakened, and counterexample cases remain in the +claim boundary. A declared profile that was not exercised is non-passing. +Runtime-mediated and backend-native cases remain separately labelled even when +both appear in one report. + +Reports carry allowlisted safe refs, digests of already nonsecret canonical +material, bounded counts, result codes, limitations, and sanitized +counterexample refs. Hashing a secret value, raw witness, policy body, memory, +environment dump, or backend object does not make it safe metadata. + +## Canonical incumbents to reuse + +| Concern | Canonical incumbent and required use | +| --- | --- | +| Opacity authority | ADR-099, `participant-predicate-opacity.md`, the current/historical behavioral catalog, and `participant-opacity-runtime-reference-v1`; reference exact revisions instead of redefining semantics. | +| Profile/runtime binding | `BehavioralRelationProfileModel`, exact-revision loaders, `ParticipantOpacityRuntimeSupportModel`, `ParticipantOpacityRuntimeEnforcementBindingModel`, and `validate_participant_opacity_runtime_enforcement()`. | +| API-407 declaration | `ParticipantFeatureSupport` and `ParticipantFeatureSupportModel`, participant feature vocabularies, canonical required-contract map, manifest dataclass/model parity, and `resolve_participant_feature_support()`. | +| Claims | `BehavioralClaimBindingModel`, `validate_behavioral_claim_binding()`, ADR-081 assurance rules, and the behavioral-claim policy checker. | +| Conformance | `run_fixture_suite()`, `run_target_conformance()`, `_TargetConformanceOptions`, `ConformanceCaseResult`, `BackendConformanceReport`, `_bounded_conformance_claim()`, and `validate_backend_conformance_report()`. | +| Runtime boundary | `RuntimeControlPlane`, `RuntimeTarget`, RUN-319 crossing mediation, API-423 occurrences, participant controller/audience binding, operation records, and backend-call accounting. | +| Diagnostics | `Diagnostic`, `Severity`, stable `conformance.*` codes, and `sanitized_failure_message()`; expected failures are values, not a new exception hierarchy. | +| Persistence | `backend_conformance_report_payload()`, `redaction_violations()`, `write_backend_conformance_report()`, `run_artifact_path()`, and `atomic_write_json_artifact()`. No new store or ledger. | +| Publication | `ContractModel(extra="forbid")`, `schema_bundle()`, hand-governed schemas, valid/invalid fixtures, publication entries/hashes, packaged-corpus parity, and compatibility gates. | +| Workflow | `.ground-control.yaml`, `.gc/plan-rules.md`, canonical nox sessions, repo policy, requirement governance, schema/concept/claim checks, and `tools/verify_all.py` with `RAES_REQUIREMENT_UID=SEM-231` on this issue-number branch. | + +## Cross-cutting layers and security posture + +1. **Manifest/config shape.** External manifests retain the existing bounded + `--manifest` file path, UTF-8 JSON-object parse, + `BackendManifestV2Model.model_validate()`, and + `backend_manifest_from_v2_model()` reconstruction; native factories produce + the same typed manifest and canonical projection. The dataclass validators, + published schema, governed feature vocabulary, required-contract map, and + concept bindings all still apply. No opacity fragment, boolean, + environment-variable bundle, or unvalidated mapping bypasses those layers. + Exact profile ids resolve through grammar-checked, root-confined corpus + loaders with pinned revision and digest; no `latest`, caller-selected root, + path, URL, open metadata, or environment-selected profile is admitted. +2. **Relation/profile join.** Each axis binding passes + `validate_behavioral_claim_binding()` against the exact catalog and profile. + The separate #964 runtime-support binding passes + `validate_participant_opacity_runtime_enforcement()` before it is cited as + prerequisite evidence; backend-axis claims are not relabeled runtime + bindings. Profile, predicate ref, carrier/materializer, observer/audience, + inventory, projection, memory, release, order, opportunity, rule, and + evidence coordinates agree. Shape validity or a non-empty ref is + insufficient. +3. **Authentication and authority.** In-process conformance makes no HTTP-auth + claim, but runtime-mediated cases still use `ControlPlaneIdentity`, exact + target role, controller/audience binding, deny-first policy, and API-407 + admission. If an existing HTTP path is exercised it retains + `ControlPlaneSecurityConfig.strict_defaults()`, + `request_size_guard_response()`, `_ControlPlaneApiAuth`, role/target checks, + and separate participant controller/audience subject bindings. A direct + backend probe is a trusted test-composition boundary, not an unauthenticated + endpoint or participant authority bypass. Caller/backend authorization does + not establish observer membership, visibility, declassification, or + opacity. +4. **Runtime/backend boundary.** Runtime refusal, normalization, durable + crossing evidence, backend invocation, backend-owned observation, and + participant-visible serialization are recorded separately. A positive + backend-native case proves backend code was causally exercised and that an + adversarial backend is detectable; method presence or an uncalled adapter + fails this gate. +5. **Observation completeness.** Cases cover every enabled observable channel + retained by the exact profile, including occurrence/content, errors, + omissions, retry, order, logical timing, release/policy changes, and external + effects. Unknown, reachable-unmediated, stale, or unsupported channels fail + or remove the claim; payload redaction alone never passes. +6. **Report validation and error envelope.** The existing report finalizer + validates claim/case/axis consistency before serialization. Expected + backend, Pydantic, file, or harness failures pass through + `sanitized_failure_message()` and stable bounded `Diagnostic` codes and + messages. Unexpected HTTP failures retain exactly + `{"detail":"internal server error"}` with only safe class/code and + correlation data in authorized audit. Never emit `str(exc)`, rejected input, + host paths, target internals, policy/gate inventory, backend error text, + tracebacks, stdout/stderr, or participant existence details. Status and + logical timing remain profile observations even when content is redacted. + Expected refusals use uniform, value-independent status and detail behavior + for protected existence. +7. **Runtime persistence and ordering.** Runtime-mediated cases reuse + `RuntimeSnapshot`, operation records, audit, append-only API-423 histories, + `ControlPlaneStore.commit_participant_transition()`, expected history heads, + idempotency, restart validation, and conflict behavior. A native side effect + that cannot be reconciled with durable finalization is outside the positive + claim. No harness or report becomes a second runtime state store. +8. **Report persistence/redaction.** The validated report is projected once, + passed through the shared redaction gate, root-confined by safe run id, and + written atomically. API-423/runtime state remains in incumbent stores and is + cited by safe ref; no opacity report database, raw transcript, witness + archive, or duplicate audit stream is added. +9. **Secrets and OS/process exposure.** The reference path is typed and + in-process. Secret values, predicate bodies/results, worlds, memories, + policy/supervisor bodies, credentials, tokens, raw observations, and full + environment/configuration never enter CLI arguments, process argv, + environment variables, filenames, shell history, logs, diagnostics, + reports, or test ids. Environment digests are computed from an explicit + nonsecret allowlist, not from `os.environ` or an environment dump. No new + subprocess, socket, daemon, privilege, or host-file dependency is justified. +10. **Schema/concept governance.** Any portable field or feature-term change + moves with the current and historical concept authority, schema, fixture, + publication-manifest hash, generated bundle, package exports, report + consumers, and compatibility review. Historical #961-#964 evidence retains + its original taxonomy/profile coordinates. + +## Whole-repository surfaces in scope + +- **Authority:** ADR-060/081/099, SEM-231 formal semantics, current and + historical behavioral catalogs, controlled vocabularies, relation profiles, + assurance aggregates, and claim validation. +- **Contracts/manifests:** backend manifest dataclass and Pydantic projections, + participant feature support, required-contract mapping, API-423 opacity + bindings, schemas, fixtures, publication records, and packaged corpus. +- **Runtime/backend:** reference target manifest and participant runtime, + runtime opacity support, crossing resolver/mediation, backend calls, + operation/audit evidence, and exact participant-visible observation paths. +- **Conformance/operations:** target options, injected harness boundary, case + and report projection/validation, diagnostic sanitizer, redaction gate, + root-confined atomic writer, and adversarial test fixtures. +- **Host/workflow:** in-process reference execution, no ambient secrets or new + OS services, repository policy, requirement governance, schema/concept/claim + checks, traceability, and full verification. + +## Extensibility seam + +The stable seam is: + +```text +API-407 family feature + exact support strength + -> SEM-231 catalog/profile + #964 safe runtime support binding + -> realization owner + backend/config/tool/environment identity + -> injected finite case policy and independent observation + -> three axis-specific shared claim bindings + -> one validated backend conformance report +``` + +It is parameterized by profile id, revision, and digest. It also binds backend +implementation, configuration digest, realization owner, execution basis, and +observer or audience. The remaining parameters are the finite case/probe-set +digest, tool/observer version, and evidence refs. + +A second supported profile, backend, observer, strategy set, or logical +opportunity class changes those values and its typed case policy. It does not +add a feature boolean, backend-name branch, report family, claim type, or +relation copy. + +Timed, quantitative, probabilistic, progress-sensitive, coalition, or +partial-order opacity does not fit by changing a string. It requires the +corresponding governed relation/profile and independent evidence. + +## Gotchas and anti-patterns + +Avoid: + +- treating `participant_predicate_opacity` as a policy operation or copying it + into `PARTICIPANT_RUNTIME_POLICY_FEATURES`; +- copying the runtime observation inventory, secret predicate, observer model, + possible worlds, relation formula, or policy body into a manifest/report; +- inferring support from a boolean, feature list membership, method presence, + importability, contract-list breadth, or a green schema round trip; +- using `ParticipantPolicyBinding`, `envelope_digest`, `native-live`, or + `native_conformance` to mean backend-owned opacity realization; +- passing backend conformance when only RUN-319 was exercised, when the backend + was never called, or when an adversarial backend is masked by runtime denial; +- treating one equal projected history, payload-redaction pair, randomized + result, hidden supervisor body, or finite probe set as predicate opacity; +- omitting supervisor decisions, failure/status shape, observable silence, + retry, timing, order, release/policy effects, or external behavior from the + observed transcript; +- comparing active actual and alternative cases under different strategies, + treating reset/revocation as forgetting, or treating delivery as observation; +- silently skipping an unexercised declaration, filtering failed/unsupported + cases, or retaining a strong claim after authorized weakening; +- adding an opacity backend profile, runner, report/schema, claim DTO, registry, + gateway, public probe endpoint, policy engine, store, exception hierarchy, + logger, environment-variable bundle, or workflow; +- exposing raw exception text, rejected values, secrets or secret-derived + digests, raw witnesses/transcripts, policy/memory/backend objects, host paths, + credentials, headers, environment dumps, or tracebacks; and +- advancing only the Python model, only the schema, only the current catalog, + or only a reference manifest while leaving historical resolution, fixtures, + publication hashes, serializers, stub/reference expectations, and report + validators inconsistent. + +## Non-goals and implementation boundary + +Issue #965 may add one governed API-407 opacity family feature. It may declare +bounded support for exact profiles and record backend-owned realization +evidence. It may also add finite adversarial cases and separate axis claim +bindings in the existing report family. + +It does not: + +- redefine SEM-231, add a secret language, copy or widen the #964 profile, or + claim unnamed profile support; +- establish universal backend opacity, proof, model checking, cross-backend + equivalence, timed/quantitative/probabilistic opacity, or behavior outside the + named backend/profile/tool/environment/cases; +- make runtime mediation backend-native, make declaration conformance, or make + finite conformance mathematical proof; +- expose a participant-facing direct backend path, add authentication/config + surfaces, persist raw secret-capable evidence, or require a daemon/network/ + privileged host integration; or +- claim policy noninterference, projected-history equality, epistemic + indistinguishability, trace inclusion/equivalence, simulation, refinement, + strong/weak/branching bisimulation, erasure, differential privacy, or a + quantitative leakage bound. diff --git a/docs/explain/reference/backend-conformance.md b/docs/explain/reference/backend-conformance.md index 1b2f0b36f..523dbe615 100644 --- a/docs/explain/reference/backend-conformance.md +++ b/docs/explain/reference/backend-conformance.md @@ -347,6 +347,28 @@ backed only by finite evidence, refuses a `native_conformance` flag with no natively-executed case, and refuses a claim whose cited cases — including failed, unsupported, and counterexample cases — are not all present. +## Participant-Opacity Backend Probes + +The `participant_predicate_opacity` family feature follows the same generic +target runner but is intentionally separate from participant-policy +operations. A typed in-process harness supplies only exact profile-bound case +inputs and an observation adapter. The runner instruments the target, invokes +both governed possible points, compares the complete closed transcript, and +owns the verdict. A declaration with no harness is an explicit unsupported +case; a harness that never calls the backend cannot claim backend-native +realization. + +Each passing case carries three independent catalog-validated bindings: +`backend-declaration/declared/structural`, +`backend-realization/realized/finite`, and +`backend-conformance/conformant/finite`. The case separately records the +realization owner and execution basis, plus exact profile, manifest, +configuration, tool, environment, and probe-set digests. Report-level meaning +remains `bounded-probe-success`. A method, boolean, payload-redaction pair, +runtime-mediated denial, or `native-live` label cannot substitute for the +observed backend transcript, and finite conformance is not proof or universal +opacity. + ## Non-Goals This preflight does not implement `ASR-502`, change requirement status, add new diff --git a/docs/explain/reference/shared-semantic-integrity.md b/docs/explain/reference/shared-semantic-integrity.md index 48515be46..84259978d 100644 --- a/docs/explain/reference/shared-semantic-integrity.md +++ b/docs/explain/reference/shared-semantic-integrity.md @@ -233,10 +233,10 @@ so they are tracked by their own requirements, not here. | Deterministic module composition and canonical-identity stability across expansion | DSL-103, SEM-205 | authoring, validation, compilation | `implementations/python/packages/raes/composition.py`, `implementations/python/packages/raes/module_registry/__init__.py`, `specs/formal/composition-readiness.md`, `implementations/python/tests/test_sdl_module_registry.py` | active | | Instantiation, closed portable phase contracts, and revalidation of concrete scenarios | RUN-301 | instantiation, validation | `docs/decisions/adrs/adr-078-closed-sdl-phase-contracts-and-portable-derivation-evidence.md`, `specs/formal/sdl-phases/README.md`, `contracts/schemas/sdl/instantiated-scenario-v1.json`, `contracts/schemas/sdl/instantiated-scenario-snapshot-v1.json`, `implementations/python/packages/raes/phase_contracts.py`, `implementations/python/packages/raes/instantiate.py`, `implementations/python/tests/test_sdl_phase_contracts.py`, `implementations/python/tests/test_instantiated_scenario_schema.py`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_run_300_lifecycle.py` | active | | Objective windows, referenced scopes, reachability, and refresh | SEM-202 | validation, compilation, planning | `implementations/python/packages/raes/semantics/objectives.py`, `specs/formal/objectives/README.md`, `specs/formal/objectives/window-consistency.md`, `implementations/python/tests/test_semantics_objectives.py`, `implementations/python/tests/test_fm2_semantics.py` | active | -| Declarative objective actor binding, target resolution, success interpretation, and dependency ordering | DSL-112, SEM-207 | authoring, validation, instantiation, compilation, planning | `implementations/python/packages/raes/objectives.py`, `implementations/python/packages/raes/semantics/objective_semantics.py`, `implementations/python/packages/raes/validator/__init__.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `specs/formal/objectives/README.md`, `specs/formal/objectives/declarative-objective-semantics.md`, `implementations/python/tests/test_semantics_objectives.py`, `implementations/python/tests/test_fm2_semantics.py`, `implementations/python/tests/test_sdl_validator.py` | active | +| Declarative objective actor binding, target resolution, success interpretation, and dependency ordering | DSL-112, SEM-207 | authoring, validation, instantiation, compilation, planning | `implementations/python/packages/raes/objectives.py`, `implementations/python/packages/raes/semantics/objective_semantics/__init__.py`, `implementations/python/packages/raes/validator/__init__.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `specs/formal/objectives/README.md`, `specs/formal/objectives/declarative-objective-semantics.md`, `implementations/python/tests/test_semantics_objectives.py`, `implementations/python/tests/test_fm2_semantics.py`, `implementations/python/tests/test_sdl_validator.py` | active | | Workflow control semantics (branching, joins, calling, retry, completion, history) | DSL-113, SEM-203 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/raes/orchestration.py`, `implementations/python/packages/raes/semantics/workflow.py`, `specs/formal/workflows/README.md`, `specs/formal/workflows/state-machine.md`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_runtime_models.py`, `implementations/python/tests/test_sdl_models.py` | active | | Workflow compensation semantics (registration, triggering, ordering, observation) | SEM-204 | validation, compilation, execution, observation | `implementations/python/packages/raes/semantics/workflow.py`, `specs/formal/workflows/compensation.md`, `implementations/python/tests/test_sdl_validator.py`, `implementations/python/tests/test_runtime_manager.py` | partial | -| Backend-neutral proposition truth and assertion-composed objective success (probe conditions remain separate implementation bindings; SDL scoring pipeline removed per ADR-073) | DSL-110, SEM-206 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/raes/propositions.py`, `implementations/python/packages/raes/conditions.py`, `implementations/python/packages/raes/semantics/propositions.py`, `implementations/python/packages/raes/semantics/objective_semantics.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `specs/formal/objectives/proposition-and-assertion-semantics.md`, `implementations/python/tests/test_proposition_semantics.py`, `implementations/python/tests/test_truth_result_contracts.py`, `implementations/python/tests/test_fm2_semantics.py` | active | +| Backend-neutral proposition truth and assertion-composed objective success (probe conditions remain separate implementation bindings; SDL scoring pipeline removed per ADR-073) | DSL-110, SEM-206 | authoring, validation, compilation, planning, execution, observation | `implementations/python/packages/raes/propositions.py`, `implementations/python/packages/raes/conditions.py`, `implementations/python/packages/raes/semantics/propositions.py`, `implementations/python/packages/raes/semantics/objective_semantics/__init__.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `specs/formal/objectives/proposition-and-assertion-semantics.md`, `implementations/python/tests/test_proposition_semantics.py`, `implementations/python/tests/test_truth_result_contracts.py`, `implementations/python/tests/test_fm2_semantics.py` | active | | Runtime compiled representation and canonical addresses | RUN-302 | compilation | `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/tests/test_runtime_models.py`, `implementations/python/tests/test_fm2_semantics.py` | active | | Planner dependency, ordering, refresh, and applicability semantics | RUN-303 | planning | `implementations/python/packages/raes_processor/semantics/planner.py`, `implementations/python/packages/raes_processor/planner/__init__.py`, `specs/formal/planner/README.md`, `specs/formal/planner/dependency-ordering.md`, `implementations/python/tests/test_semantics_planner.py`, `implementations/python/tests/test_runtime_planner.py` | active | | Live execution state and lifecycle (snapshots, results, history) | RUN-304, API-402 | execution, observation | `implementations/python/packages/raes_runtime/manager.py`, `implementations/python/packages/raes_runtime/result_contracts.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/tests/test_runtime_manager.py`, `implementations/python/tests/test_runtime_models.py` | active | @@ -246,18 +246,19 @@ so they are tracked by their own requirements, not here. | Concept authority, controlled vocabularies, reference models, and semantic profiles (meta-layer) | GOV-920 | authoring, validation, compilation, planning, execution | `specs/concept-authority/concept-authority.md`, `specs/concept-authority/semantic-profiles.md`, `implementations/python/packages/raes_contracts/semantic_profiles.py`, `implementations/python/packages/raes_contracts/controlled_vocabularies.py`, `implementations/python/packages/raes_contracts/reference_models.py`, `docs/explain/reference/shared-concept-model.md`, `implementations/python/tests/test_concept_authority.py`, `implementations/python/tests/test_semantic_profiles.py` | active | | Participant episode lifecycle boundaries (initialization, reset, completion, timeout, truncation, interruption) | RUN-311, SEM-222 | execution, observation | `docs/decisions/adrs/adr-013-participant-episode-lifecycle-boundaries.md`, `implementations/python/tests/test_run_311_participant_episode_lifecycle.py` | partial | | Declarative participant framing (identity, role, starting conditions, authority anchors, operating scope) | ACT-601 | authoring, validation | `implementations/python/packages/raes/agents.py`, `implementations/python/packages/raes/validator/__init__.py`, `docs/decisions/adrs/adr-020-declarative-participant-framing-boundaries.md`, `implementations/python/tests/test_sdl_models.py`, `implementations/python/tests/test_sdl_validator.py` | active | -| Participant behavior semantics (actions, observations, state transitions) | ACT-602, SEM-208 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/raes/participant_behavior.py`, `implementations/python/packages/raes/semantics/participant_behavior.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | -| Multi-participant interaction and participant-local histories | SEM-209 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/raes/participant_behavior.py`, `implementations/python/packages/raes/semantics/participant_behavior.py`, `implementations/python/packages/raes/validator/__init__.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_runtime_conformance.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | -| Visibility and information-boundary semantics | SEM-210 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/raes/participant_behavior.py`, `implementations/python/packages/raes/semantics/participant_behavior.py`, `implementations/python/packages/raes/validator/__init__.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | +| Participant behavior semantics (actions, observations, state transitions) | ACT-602, SEM-208 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/raes/participant_behavior.py`, `implementations/python/packages/raes/semantics/participant_behavior/__init__.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | +| Multi-participant interaction and participant-local histories | SEM-209 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/raes/participant_behavior.py`, `implementations/python/packages/raes/semantics/participant_behavior/__init__.py`, `implementations/python/packages/raes/validator/__init__.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_runtime_conformance.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | +| Visibility and information-boundary semantics | SEM-210 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/raes/participant_behavior.py`, `implementations/python/packages/raes/semantics/participant_behavior/__init__.py`, `implementations/python/packages/raes/validator/__init__.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | +| Participant information-flow policy, finite predicate-opacity runtime enforcement, and bounded reference-backend conformance | SEM-230, SEM-231, RUN-319, API-407, ASR-535 | validation, execution, observation | `specs/formal/participant-semantics/information-flow-control.md`, `specs/formal/participant-semantics/participant-predicate-opacity.md`, `docs/decisions/adrs/adr-085-participant-information-flow-and-control.md`, `docs/decisions/adrs/adr-099-participant-relative-predicate-opacity.md`, `contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json`, `contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json`, `implementations/python/packages/raes_contracts/participant_opacity_runtime.py`, `implementations/python/packages/raes_runtime/participant_crossing_mediation.py`, `implementations/python/packages/raes_conformance/conformance/participant_opacity_probes.py`, `implementations/python/tests/test_issue_964_participant_opacity_runtime.py`, `implementations/python/tests/test_issue_965_participant_opacity_backend.py` | partial | | Participant preconditions, effects, failure, causality, and attribution semantics | SEM-211, SEM-212 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/raes/participant_action_semantics.py`, `implementations/python/packages/raes/participant_attribution_semantics.py`, `implementations/python/packages/raes/participant_behavior.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py`, `implementations/python/packages/raes_contracts/contracts/__init__.py`, `implementations/python/tests/test_sem_211_participant_action_semantics.py`, `implementations/python/tests/test_sem_212_participant_attribution_semantics.py`, `implementations/python/tests/test_runtime_conformance.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | | Participant temporal semantics | SEM-213 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | -| Participant tool/affordance, decision-surface, exact-cut exposure, delivery, and visibility-boundary semantics | SEM-219, SEM-220, SEM-226 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `specs/formal/participant-semantics/information-flow-control.md`, `docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md`, `docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md`, `docs/decisions/issue-119-sem-219-220-226-participant-decision-surface-preflight.md`, `docs/decisions/issue-294-sem-219-participant-tool-affordance-preflight.md`, `contracts/schemas/control-plane/participant-decision-surface-v1.json`, `contracts/schemas/control-plane/participant-decision-surface-v2.json`, `implementations/python/packages/raes/participant_behavior_specification.py`, `implementations/python/packages/raes/semantics/participant_behavior.py`, `implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py`, `implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2.py`, `implementations/python/packages/raes_contracts/contracts/participant_decision_surface_exposure_v2.py`, `implementations/python/packages/raes_contracts/participant_decision_surface_delivery.py`, `implementations/python/packages/raes_processor/compiler/participant_behaviors.py`, `implementations/python/packages/raes_processor/models/behavior_resources.py`, `implementations/python/packages/raes_processor/models/decision_surface.py`, `implementations/python/packages/raes_processor/models/decision_surface_v2.py`, `implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py`, `implementations/python/packages/raes_processor/models/participant_exposure_v2.py`, `implementations/python/packages/raes_runtime/participant_control.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_sem_220_participant_decision_surface.py`, `implementations/python/tests/test_sem_220_participant_decision_surface_v2.py`, `implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | +| Participant tool/affordance, decision-surface, exact-cut exposure, delivery, and visibility-boundary semantics | SEM-219, SEM-220, SEM-226 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `specs/formal/participant-semantics/information-flow-control.md`, `docs/decisions/adrs/adr-083-participant-tool-decision-surface-and-exposure-semantics.md`, `docs/decisions/adrs/adr-095-participant-decision-epoch-state-cut-and-delivery-semantics.md`, `docs/decisions/issue-119-sem-219-220-226-participant-decision-surface-preflight.md`, `docs/decisions/issue-294-sem-219-participant-tool-affordance-preflight.md`, `contracts/schemas/control-plane/participant-decision-surface-v1.json`, `contracts/schemas/control-plane/participant-decision-surface-v2.json`, `implementations/python/packages/raes/participant_behavior_specification.py`, `implementations/python/packages/raes/semantics/participant_behavior/__init__.py`, `implementations/python/packages/raes_contracts/contracts/participant_decision_surface.py`, `implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2.py`, `implementations/python/packages/raes_contracts/contracts/participant_decision_surface_exposure_v2.py`, `implementations/python/packages/raes_contracts/participant_decision_surface_delivery.py`, `implementations/python/packages/raes_processor/compiler/participant_behaviors.py`, `implementations/python/packages/raes_processor/models/behavior_resources.py`, `implementations/python/packages/raes_processor/models/decision_surface.py`, `implementations/python/packages/raes_processor/models/decision_surface_v2.py`, `implementations/python/packages/raes_processor/models/decision_surface_anchor_v2.py`, `implementations/python/packages/raes_processor/models/participant_exposure_v2.py`, `implementations/python/packages/raes_runtime/participant_control.py`, `implementations/python/tests/test_sem_208_participant_behavior.py`, `implementations/python/tests/test_sem_220_participant_decision_surface.py`, `implementations/python/tests/test_sem_220_participant_decision_surface_v2.py`, `implementations/python/tests/test_sem_220_participant_decision_surface_v2_runtime.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | | Participant reference trajectories, demonstrations, budgets, and quota/exhaustion semantics | SEM-221, SEM-223 | — | — | planned | | Participant outcome interpretation | SEM-215 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/raes/participant_outcome_semantics.py`, `implementations/python/packages/raes/semantics/participant_outcome.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_contracts/contracts/__init__.py`, `implementations/python/tests/test_sem_215_participant_outcome_interpretation.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | | Derived operational context views (portable meaning and comparability) | SEM-214 | execution, observation | `specs/formal/participant-semantics/README.md`, `specs/formal/runtime-contracts/participant-backend-contracts.md`, `implementations/python/packages/raes_contracts/contracts/__init__.py`, `implementations/python/packages/raes_runtime/participant_retrieval.py`, `implementations/python/packages/raes_runtime/control_plane_api_participant_retrieval.py`, `contracts/schemas/control-plane/participant-context-view-v1.json`, `implementations/python/tests/test_participant_backend_contracts.py`, `implementations/python/tests/test_runtime_control_plane.py`, `implementations/python/tests/test_runtime_control_plane_api.py` | active | | Boundary semantics for runtime-observable state, captured evidence, derived evaluations, analysis outputs, and audience-specific views | SEM-216 | execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md`, `implementations/python/packages/raes_contracts/contracts/__init__.py`, `contracts/schemas/control-plane/participant-context-view-v1.json`, `contracts/schemas/experiment-core/experiment-evidence-record-v1.json`, `implementations/python/tests/test_sem_216_boundary_semantics.py`, `implementations/python/tests/test_participant_backend_contracts.py`, `implementations/python/tests/test_runtime_contracts.py` | active | -| Evidence, evaluation, view-boundary, and observability-plane semantics | SEM-224, SEM-225, DSL-123, DSL-124 | authoring, validation, execution, observation | `docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md`, `specs/formal/observability-evidence-plane.md`, `specs/sdl/observability-and-evidence.md` | partial | +| Evidence, evaluation, view-boundary, and observability-plane semantics | SEM-224, SEM-225, DSL-123, DSL-124 | authoring, validation, execution, observation | `docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md`, `specs/formal/observability-evidence-plane.md`, `specs/sdl/observability-and-evidence.md`, `implementations/python/tests/test_issue_1043_forwarding_agent_posture.py` | partial | | External knowledge bindings semantics | SEM-217 | validation, execution | `specs/formal/participant-semantics/README.md`, `docs/explain/reference/shared-concept-model.md`, `implementations/python/packages/raes_contracts/semantic_binding_effects.py`, `implementations/python/tests/test_sem_217_knowledge_bindings.py` | active | -| Explicitness and realization semantics (binding declarations vs processor/backend realization) | SEM-218 | authoring, validation, instantiation, compilation, planning, execution, observation | `specs/formal/realization/explicitness-and-realization.md`, `specs/formal/realization/README.md`, `docs/explain/reference/explicitness-realization-semantics.md`, `implementations/python/packages/raes/explicitness.py`, `implementations/python/packages/raes/realization_designation.py`, `implementations/python/packages/raes/phase_contracts.py`, `implementations/python/packages/raes/validator/__init__.py`, `implementations/python/packages/raes/instantiate.py`, `implementations/python/packages/raes_contracts/apparatus.py`, `implementations/python/packages/raes_contracts/vocabulary.py`, `implementations/python/packages/raes_contracts/contracts/__init__.py`, `implementations/python/packages/raes_contracts/runtime_state.py`, `implementations/python/packages/raes_backend_protocols/manifest.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/packages/raes_processor/planner/__init__.py`, `implementations/python/packages/raes_processor/semantics/realization.py`, `implementations/python/packages/raes_runtime/backend_calls.py`, `implementations/python/packages/raes_runtime/manager.py`, `implementations/python/packages/raes_runtime/control_plane_store.py`, `implementations/python/packages/raes_runtime/control_plane_api_models.py`, `implementations/python/tests/test_sem_218_explicitness.py`, `implementations/python/tests/test_sem_218_realization.py`, `implementations/python/tests/test_sem_218_realization_designation.py`, `implementations/python/tests/test_sem_218_runtime_realization.py`, `implementations/python/tests/test_runtime_planner.py`, `implementations/python/tests/test_backend_manifest.py`, `implementations/python/tests/test_processor_manifest.py`, `implementations/python/tests/test_runtime_contracts.py` | active | +| Explicitness and realization semantics (binding declarations vs processor/backend realization) | SEM-218 | authoring, validation, instantiation, compilation, planning, execution, observation | `specs/formal/realization/explicitness-and-realization.md`, `specs/formal/realization/README.md`, `docs/explain/reference/explicitness-realization-semantics.md`, `implementations/python/packages/raes/explicitness.py`, `implementations/python/packages/raes/realization_designation.py`, `implementations/python/packages/raes/phase_contracts.py`, `implementations/python/packages/raes/validator/__init__.py`, `implementations/python/packages/raes/instantiate.py`, `implementations/python/packages/raes_contracts/apparatus.py`, `implementations/python/packages/raes_contracts/vocabulary.py`, `implementations/python/packages/raes_contracts/contracts/__init__.py`, `implementations/python/packages/raes_contracts/runtime_state.py`, `implementations/python/packages/raes_backend_protocols/manifest.py`, `implementations/python/packages/raes_processor/compiler/__init__.py`, `implementations/python/packages/raes_processor/models/`, `implementations/python/packages/raes_processor/planner/__init__.py`, `implementations/python/packages/raes_processor/semantics/realization.py`, `implementations/python/packages/raes_runtime/backend_calls.py`, `implementations/python/packages/raes_runtime/manager.py`, `implementations/python/packages/raes_runtime/control_plane_store.py`, `implementations/python/packages/raes_runtime/control_plane_api_models.py`, `implementations/python/tests/test_sem_218_explicitness.py`, `implementations/python/tests/test_sem_218_realization.py`, `implementations/python/tests/test_sem_218_realization_designation.py`, `implementations/python/tests/test_sem_218_runtime_realization.py`, `implementations/python/tests/test_issue_1043_realization_corroboration.py`, `implementations/python/tests/test_runtime_planner.py`, `implementations/python/tests/test_backend_manifest.py`, `implementations/python/tests/test_processor_manifest.py`, `implementations/python/tests/test_runtime_contracts.py` | active | | Clock, time-domain, advancement/pacing/synchronization, and temporal ordering/causality semantics | SEM-227, SEM-228, SEM-229, API-421, ASR-528, EXP-734 | authoring, validation, compilation, planning, execution, observation | `specs/formal/time-model/README.md`, `docs/decisions/adrs/adr-090-shared-time-domain-clock-and-progression-authority.md`, `docs/decisions/adrs/adr-091-portable-time-capability-control-and-provenance-contracts.md`, `implementations/python/packages/raes_contracts/contracts/time_model.py`, `implementations/python/packages/raes_backend_protocols/capability_admission.py`, `implementations/python/packages/raes_runtime/time_coordinator.py`, `implementations/python/packages/raes_conformance/time_semantics.py`, `implementations/python/tests/test_sem_227_shared_time_model.py`, `implementations/python/tests/test_api_421_time_contracts.py` | active | -| Deterministic benign participant execution under shared time | DSL-437 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/autonomous-execution.md`, `docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md`, `contracts/schemas/participant-runtime/participant-execution-binding-v1.json`, `contracts/schemas/participant-runtime/participant-execution-control-v1.json`, `contracts/schemas/participant-runtime/participant-execution-service-state-v1.json`, `implementations/python/packages/raes/participant_execution.py`, `implementations/python/packages/raes/semantics/participant_behavior.py`, `implementations/python/packages/raes_backend_protocols/capability_admission.py`, `implementations/python/packages/raes_backend_protocols/participant_execution_service.py`, `implementations/python/packages/raes_runtime/participant_scheduler.py`, `implementations/python/packages/raes_runtime/participant_clock_driver.py`, `implementations/python/packages/raes_backend_protocols/participant_runtime_base.py`, `implementations/python/packages/raes_contracts/participant_autonomous_state.py`, `implementations/python/tests/test_dsl_437_benign_participant_execution.py`, `implementations/python/tests/test_issue_898_participant_execution_control.py`, `implementations/python/tests/test_dsl_437_evaluation_authority.py`, `implementations/python/tests/test_dsl_437_snapshot_durability_conformance.py` | active | +| Deterministic benign participant execution under shared time | DSL-437 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/autonomous-execution.md`, `docs/decisions/adrs/adr-092-autonomous-benign-participants-under-shared-time.md`, `contracts/schemas/participant-runtime/participant-execution-binding-v1.json`, `contracts/schemas/participant-runtime/participant-execution-control-v1.json`, `contracts/schemas/participant-runtime/participant-execution-service-state-v1.json`, `implementations/python/packages/raes/participant_execution.py`, `implementations/python/packages/raes/semantics/participant_behavior/__init__.py`, `implementations/python/packages/raes_backend_protocols/capability_admission.py`, `implementations/python/packages/raes_backend_protocols/participant_execution_service.py`, `implementations/python/packages/raes_runtime/participant_scheduler.py`, `implementations/python/packages/raes_runtime/participant_clock_driver.py`, `implementations/python/packages/raes_backend_protocols/participant_runtime_base.py`, `implementations/python/packages/raes_contracts/participant_autonomous_state.py`, `implementations/python/tests/test_dsl_437_benign_participant_execution.py`, `implementations/python/tests/test_issue_898_participant_execution_control.py`, `implementations/python/tests/test_dsl_437_evaluation_authority.py`, `implementations/python/tests/test_dsl_437_snapshot_durability_conformance.py` | active | diff --git a/docs/explain/sdl/limitations.md b/docs/explain/sdl/limitations.md index 32370bb52..7eac3eb1b 100644 --- a/docs/explain/sdl/limitations.md +++ b/docs/explain/sdl/limitations.md @@ -125,7 +125,6 @@ These are current SDL expressiveness gaps: | **Full solver-backed verification** | Global proof-style verification that attack paths are reachable and defenses are consistent is not implemented; the repository uses lightweight semantic modeling, invariants, typed contracts, and selective property/state-machine methods | VSDL SMT solver, CRACK Datalog | | **Full participant behavior surface** | The current `agents` section under-expresses richer role-neutral behavior concerns such as tool/affordance declarations, control-context assets, decision-surface exposure policies, episode structure, and benchmark-oriented participant assets | CybORG, OpenRange, Open Trajectory Gym | | **Materialized evidence capture and provenance** | SDL has implemented `evidence_requirements` syntax, a published schema, fixtures, and fail-closed validation for portable capture intent. It does not itself materialize captures, prove collection, calculate integrity, retain artifacts, or supply complete run-level provenance and loss reporting; those are processor/backend and experiment-evidence responsibilities | OpenRange, OCSF-informed telemetry models | -| **User behavior profiles** | Normal user activity patterns (browsing, email, file access schedules) | CybORG Green agents | | **Multi-tenancy** | Multiple independent exercises sharing infrastructure | Locked Shields team-per-subnet model | ### Ecosystem-Layer Gaps (outside pure SDL syntax) diff --git a/docs/explain/sdl/lineage.md b/docs/explain/sdl/lineage.md index 59bd6dd33..a197961c0 100644 --- a/docs/explain/sdl/lineage.md +++ b/docs/explain/sdl/lineage.md @@ -1588,6 +1588,32 @@ conformance through those generic declarations. ### Adversarial Participant Flow And Control +- The older cyber lineage establishes the portable core beneath the recent + agent-control systems. [Denning's lattice model](https://doi.org/10.1145/360051.360056) + supplies ordered information classes and conservative joins; + [decentralized labels](https://www.cs.cornell.edu/andru/papers/sp98/paper.html) + supply principal-relative ownership and explicit downgrade authority; and + [robust declassification](https://doi.org/10.3233/JCS-2006-14203) together + with [nonmalleable IFC](https://www.cs.cornell.edu/andru/papers/nmifc/) + supplies the separation of confidentiality release from integrity + endorsement and the need to constrain attacker influence over both. +- [HiStar](https://www.usenix.org/conference/osdi-06/making-information-flow-explicit-histar) + and [Flume](https://pdos.csail.mit.edu/papers/flume-sosp07.pdf) demonstrate + decentralized information-flow control in operating-system abstractions. + [NIST SP 800-53 AC-4](https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final) + and the [NSA cross-domain program](https://www.nsa.gov/Cybersecurity/Partnership/National-Cross-Domain-Strategy-Management-Office/) + supply the controlled-interface, complete-mediation, content/metadata + inspection, default-deny, and lifecycle-assurance precedents for a final + boundary. RAES adopts their semantic discipline, not a fixed military label + vocabulary, operating-system API, guard product profile, or accreditation + claim. +- [TaintDroid](https://www.usenix.org/conference/osdi10/taintdroid-information-flow-tracking-system-realtime-privacy-monitoring) + demonstrates useful dynamic tracking of explicit flows, while + [CamFlow](https://arxiv.org/abs/1711.05296) demonstrates whole-system + provenance capture. Their limits are equally material: tracking may omit + implicit, native, control, or covert flows, and provenance says where data + came from rather than whether release, endorsement, or execution is + authorized. - [FIDES](https://arxiv.org/abs/2505.23643) supplies the immediate precedent for independent confidentiality/integrity labels, conservative propagation, and deterministic action policy. @@ -1612,6 +1638,24 @@ conformance through those generic declarations. SEM-230, ACT-617, API-409/API-423, RUN-310/RUN-319, API-407, experiment, and ASR-535 carriers. It does not import an LLM framework, prompt format, model role, MCP gateway, monitor, scorer, or trajectory hierarchy. +- SEM-233 therefore keeps confidentiality and integrity as independent + coordinates and keeps declassification, endorsement, approval, admission, + authentication, and authorization as distinct operations. Missing labels, + ambiguous joins, stale policy cuts, and unexplained cross-participant or + cross-episode resets fail closed at the final enforceable sink. A scalar + `trusted`, `sensitivity`, marking, confidence, signature, or monitor score + cannot stand in for the two coordinates. These rules address the recurring + failure modes of label/privilege administration, over-powered trusted + downgrade paths, incomplete mediation, tag laundering, stale provenance, + and guards that mistake detection or identity for flow authority. +- Issue #1001 publishes `sem-233/rev1` as two closed finite obligation + powersets ordered by subset with componentwise union joins. Unknown state is + deny-equivalent; release operations create fresh identities; labels, + provenance, and possible-influence history survive handoff and replay; and + the existing SEM-230/API-423 crossing decision gains independent + confidentiality and integrity predicates at the same exact cut. Its finite + executable model is bounded falsification evidence, not a portable contract, + runtime or backend implementation, model check, proof, or covert-flow claim. - Issue #812 is design authority. Its DRAFT requirements and program do not establish runtime enforcement, backend realization, intentional-subversion robustness, model alignment, monitor honesty, private-reasoning safety, or diff --git a/docs/public/api/contracts.rst b/docs/public/api/contracts.rst index 87509155b..93595c34b 100644 --- a/docs/public/api/contracts.rst +++ b/docs/public/api/contracts.rst @@ -12,6 +12,12 @@ Diagnostics .. automodule:: raes_contracts.diagnostics :members: +Artifact Transformation Reports +------------------------------- + +.. automodule:: raes_contracts.contracts.artifact_transformations + :members: + Planning -------- diff --git a/docs/public/api/sdl.rst b/docs/public/api/sdl.rst index 14a4f970c..fe45007a1 100644 --- a/docs/public/api/sdl.rst +++ b/docs/public/api/sdl.rst @@ -56,6 +56,12 @@ Composition .. automodule:: raes.composition :members: +Artifact Transformations +------------------------ + +.. automodule:: raes.transformations + :members: + Module Registry --------------- diff --git a/docs/public/guides/python.md b/docs/public/guides/python.md index d43b2d490..387dbc5b6 100644 --- a/docs/public/guides/python.md +++ b/docs/public/guides/python.md @@ -25,3 +25,60 @@ semantic checks on unless your workflow has a clear reason to inspect the file shape alone. See the [API reference](../api/sdl.rst) for signatures and model details. + +## Transform an admitted artifact + +Transformation functions operate on already parsed models and never write +files. A successful result contains a newly admitted output and a portable +report; a refusal contains no output. + +Rename one declaration by its exact canonical address: + +```python +from raes import RenameSDLDeclarationRequest, rename_sdl_declaration + +result = rename_sdl_declaration( + scenario, + RenameSDLDeclarationRequest( + target_address="nodes.web", + new_local_name="frontend", + ), +) +if result.succeeded: + transformed = result.output +else: + for diagnostic in result.report.diagnostics: + print(diagnostic.code) +``` + +The rename updates resolved references, module exports, and any explicitly +supplied external-concept binding documents as one all-or-none operation. An +alias, collision, stale linked artifact, or invalid target is refused. + +Loss is rejected by default. Removal therefore requires a policy naming the +exact accepted loss kind: + +```python +from raes import ( + ArtifactTransformationPolicy, + RemoveSDLDeclarationRequest, + remove_sdl_declaration, +) +from raes_contracts.contracts import ArtifactTransformationLossKind + +result = remove_sdl_declaration( + scenario, + RemoveSDLDeclarationRequest(target_address="nodes.obsolete"), + policy=ArtifactTransformationPolicy( + allowed_loss_kinds=( + ArtifactTransformationLossKind.DECLARATION_REMOVED, + ) + ), +) +``` + +Use `canonicalize_portable_contract()` to reconstruct an isolated portable +contract under its closed model, and `compare_canonical_artifacts()` for exact +canonical identity before and after an operation. Canonical identity is not a +claim of behavioral or backend equivalence. Consumers remain responsible for +file transactions, pack layout, persistence, and user-interface behavior. diff --git a/docs/research/adversarial-participant-control/current-state-assessment.md b/docs/research/adversarial-participant-control/current-state-assessment.md index 836a7f42c..c4a632170 100644 --- a/docs/research/adversarial-participant-control/current-state-assessment.md +++ b/docs/research/adversarial-participant-control/current-state-assessment.md @@ -29,6 +29,35 @@ enforceable boundary. None of the sources establishes model alignment, safe private reasoning, monitor honesty, or protection from undeclared covert channels. +## Boundary-flow precedent review for issue #1001 + +Issue #1001 extends the source review beyond agent-specific control systems to +the security mechanisms that have carried information-flow decisions at real +system and domain boundaries. The comparison is deliberately about semantic +lessons, including where an approach has worked and where its claim boundary +has proved too weak for SEM-233. + +| Precedent | What worked | Limitation carried into the SEM-233 design boundary | +| --- | --- | --- | +| [Denning's lattice model](https://doi.org/10.1145/360051.360056) | Gives information classes a partial order and makes joins conservative instead of allowing an observation to erase a restriction. | A single fixed classification lattice does not represent mutually distrustful owners or independently evolving confidentiality and integrity obligations. | +| [Decentralized labels](https://www.cs.cornell.edu/andru/papers/sp98/paper.html) | Makes policies principal-relative and gives declassification an explicit authority requirement rather than treating it as an ordinary relabel. | Distributed label and privilege management can produce label creep or over-powerful trusted downgrade paths; possession of authority still needs a bounded purpose and sink decision. | +| [Robust declassification](https://doi.org/10.3233/JCS-2006-14203) and [nonmalleable IFC](https://www.cs.cornell.edu/andru/papers/nmifc/) | Show why confidentiality release and integrity endorsement are dual but distinct, and constrain attacker influence over downgrading. | Downgrading breaks simple noninterference and compositional reasoning unless the policy states who may affect the decision, what may change, and at which cut it is evaluated. | +| [HiStar](https://www.usenix.org/conference/osdi-06/making-information-flow-explicit-histar) and [Flume](https://pdos.csail.mit.edu/papers/flume-sosp07.pdf) | Demonstrate practical decentralized IFC with labels, isolated components, a small trusted base, and conservative propagation through familiar OS abstractions. | Trusted declassifiers/untaint privileges, label administration, compatibility, and undeclared covert channels remain outside the ordinary flow lattice; operational success does not justify an unbounded `trusted` scalar. | +| [NIST SP 800-53 AC-4](https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final) and the [NSA cross-domain program](https://www.nsa.gov/Cybersecurity/Partnership/National-Cross-Domain-Strategy-Management-Office/) | Put enforcement at controlled interfaces, combine content and metadata checks, and treat default deny, assessment, configuration, and lifecycle control as part of the boundary. | A guard fails semantically when it can be bypassed, receives missing or stale attributes, accepts ambiguous content, or silently treats a release decision as identity/action authorization. Certification of one guard is not a proof about every downstream sink. | +| [TaintDroid](https://www.usenix.org/conference/osdi10/taintdroid-information-flow-tracking-system-realtime-privacy-monitoring) | Shows that multi-granularity dynamic labels can expose consequential explicit flows with tolerable overhead in a bounded platform. | Dynamic taint is detection evidence, not permission to disclose. Native, implicit, control, and covert flows plus tag coarsening or laundering bound the claim. | +| [CamFlow whole-system provenance](https://arxiv.org/abs/1711.05296) | Makes source and derivation carriage useful for audit and data-loss investigation across a whole system. | Provenance volume, coverage, and overhead are operational constraints, and derivation history cannot itself authorize release, endorsement, or execution. | +| [NIST zero-trust architecture](https://doi.org/10.6028/NIST.SP.800-207) and capability authority | Reinforce per-request mediation, explicit policy enforcement points, and least authority rather than ambient trust. | Identity, device posture, and possession of a capability authorize an action; they do not replace end-to-end label propagation or a confidentiality release rule. | + +The resulting semantic split is strict: confidentiality and integrity are +independent coordinates; conservative join/derivation never improves either +coordinate; declassification, endorsement, action approval, admission, +authentication, and authorization are different operations; provenance is +evidence rather than permission; and an external effect or disclosure is +decided again at the final enforceable sink. Unknown labels, ambiguous joins, +stale policy cuts, and unexplained episode-boundary resets fail closed. The +profile describes declared explicit flows only and makes no claim over covert +channels, model honesty, or the completeness of a backend's instrumentation. + ## Existing RAES coverage - ADR-085, ADR-095, SEM-230, and SEM-231 define exact-cut @@ -75,6 +104,11 @@ ADR-101 and the formal authority add two DRAFT owners: semantics. - ASR-536 owns the intentional-subversion evaluation profile. +Issue #1001 publishes the exact `sem-233/rev1` powerset algebra, typed carrier +mapping, lineage, and bounded test-local falsification evidence. SEM-233 stays +DRAFT: portable contracts, runtime enforcement, backend realization, and the +ASR-536 evaluation remain separate downstream work. + Issue #812 also opens six ordered implementation issues. It does not alter published schemas or runtime behavior and does not claim either DRAFT requirement is satisfied. diff --git a/docs/research/adversarial-participant-control/implementation-program.json b/docs/research/adversarial-participant-control/implementation-program.json index d3626f1d9..fb7761ebb 100644 --- a/docs/research/adversarial-participant-control/implementation-program.json +++ b/docs/research/adversarial-participant-control/implementation-program.json @@ -1,10 +1,11 @@ { "schema_version": "adversarial-participant-control-program/v1", - "assessment_date": "2026-07-30", + "assessment_date": "2026-08-01", "parent_issue": 812, "milestone": "Participant Information-Flow & Behavioral Equivalence", "deliverables": [ "docs/decisions/issue-812-adversarial-agent-control-preflight.md", + "docs/decisions/issue-1001-sem-233-boundary-flow-semantics-preflight.md", "docs/decisions/adrs/adr-101-adversarial-participant-flow-control.md", "docs/research/adversarial-participant-control/index.md", "docs/research/adversarial-participant-control/current-state-assessment.md", @@ -15,6 +16,8 @@ "docs/research/adversarial-participant-control/implementation-program.md", "docs/research/adversarial-participant-control/implementation-program.json", "specs/formal/participant-semantics/adversarial-flow-control.md", + "implementations/python/tests/sem233_boundary_flow_model.py", + "implementations/python/tests/test_sem_233_adversarial_boundary_flow.py", "docs/public/participant-control.md", "docs/explain/sdl/lineage.md" ], @@ -248,19 +251,41 @@ "flow_policy_profile": { "profile_id": "participant-boundary-flow-policy-v1", "revision": "rev1", + "authority_revision": "sem-233/rev1", + "definition_status": "published", "confidentiality": { "independent_coordinate": true, "meaning": "maximum permitted audiences, principals, destinations, and sink classes", "join": "least-upper-bound", + "exact_domain": "powerset-of-closed-confidentiality-obligation-universe", + "order": "subset-with-more-obligations-more-restrictive", + "bottom": "empty-obligation-set", + "top": "complete-profile-obligation-universe", + "exact_join": "set-union", "release_operation": "declassification" }, "integrity": { "independent_coordinate": true, "meaning": "origins and possible writers that may have influenced the value plus sink-required trust", "join": "conservative-influence-union", + "exact_domain": "powerset-of-closed-possible-influence-obligation-universe", + "order": "subset-with-more-unresolved-influences-more-restrictive", + "bottom": "empty-obligation-set", + "top": "complete-profile-obligation-universe", + "exact_join": "set-union", "release_operation": "integrity-endorsement" }, + "algebraic_laws": [ + "closure", + "associativity", + "commutativity", + "idempotence", + "monotonicity", + "traversal-order-independence" + ], + "sink_predicate": "both-coordinate-obligation-subset-plus-existing-exact-cut-gates", "unknown_source_default": "confidential-and-untrusted", + "unknown_algebra_value": "top-with-unresolved-status-and-deny-at-sink", "missing_or_unknown_behavior": "deny-or-explicitly-unsupported", "opaque_transformation": "retain-join-of-all-possible-inputs", "historical_labels_are_immutable": true, diff --git a/docs/research/adversarial-participant-control/index.md b/docs/research/adversarial-participant-control/index.md index 0f462f33a..573f25972 100644 --- a/docs/research/adversarial-participant-control/index.md +++ b/docs/research/adversarial-participant-control/index.md @@ -9,6 +9,7 @@ implementation program. It does not claim runtime or backend enforcement or a successful adversarial evaluation. - [Architecture preflight](../../decisions/issue-812-adversarial-agent-control-preflight.md) +- [Issue #1001 SEM-233 semantic-authority preflight](../../decisions/issue-1001-sem-233-boundary-flow-semantics-preflight.md) - [ADR-101](../../decisions/adrs/adr-101-adversarial-participant-flow-control.md) - [Current-state and primary-source assessment](current-state-assessment.md) - [Threat model](threat-model.md) diff --git a/docs/research/adversarial-participant-control/requirement-disposition.md b/docs/research/adversarial-participant-control/requirement-disposition.md index 76998bb72..7f2b2912d 100644 --- a/docs/research/adversarial-participant-control/requirement-disposition.md +++ b/docs/research/adversarial-participant-control/requirement-disposition.md @@ -24,6 +24,11 @@ evidence. Issue #812 defines both authorities but does not satisfy their positive implementation or evaluation clauses. +Issue #1001 publishes the bounded `sem-233/rev1` definition and finite +falsification evidence for SEM-233. The requirement remains DRAFT because +portable contracts, runtime enforcement, backend realization, and adversarial +evaluation remain owned by #1002, #1003, #1004, and #1007. + ## Reused and downstream authority | Requirement | Disposition | Scope | Boundary | 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 99af0e1fe..540272147 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": "631b59c4a40ec935a68ab975814c023d3824e5c42e62415e3176b88baacf6519" + "snapshot_sha256": "cf2625cc1cd3a0314c4c7a1f87c56ed1bbfdfe8b400381372107e276bbbb2b03" } diff --git a/docs/research/formal-semantic-validation/bundles/retest-v2.json b/docs/research/formal-semantic-validation/bundles/retest-v2.json index b75857e7b..fbfb82d99 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": "f47fa8da2f52bbfe4c3747e86f291e48cc18637eb51978035791b78a8c678b90" + "snapshot_sha256": "1b1ee85f36a5a5dd0eee9c9fe541907f67eaedea7b355fbdbab989c8ba4b1871" } diff --git a/docs/research/formal-semantic-validation/bundles/satisfiability-v1.json b/docs/research/formal-semantic-validation/bundles/satisfiability-v1.json index 1c044ef62..ff7c79c61 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": "631b59c4a40ec935a68ab975814c023d3824e5c42e62415e3176b88baacf6519" + "snapshot_sha256": "cf2625cc1cd3a0314c4c7a1f87c56ed1bbfdfe8b400381372107e276bbbb2b03" } 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 38accf0bf..9c910a773 100644 --- a/docs/research/formal-semantic-validation/execution-snapshot-v1.2.json +++ b/docs/research/formal-semantic-validation/execution-snapshot-v1.2.json @@ -230,7 +230,7 @@ "replayable": true, "actual_outcome": "stable", "diagnostic_kind": null, - "result_digest": "a94eeb80e26ec38b621b60e6ed90fbe32d9bf9a996d264144b641819c4609823", + "result_digest": "6a0db7d28a756d83974dd681df810f6c93ddd97c371d369242fe2425cbdd0d11", "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": "2515d5a64e0d31564078e2ad7be7aea352849b932357a49a7e9107b7d06939e1", + "result_digest": "333be367e829e54fb836ac20297fe98b92c9d5b7e81b23c5a6891171564af531", "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 20bb81f0c..27fd80675 100644 --- a/docs/research/formal-semantic-validation/execution-snapshot-v2.json +++ b/docs/research/formal-semantic-validation/execution-snapshot-v2.json @@ -162,7 +162,7 @@ "retest": { "actual_outcome": "stable", "diagnostic_kind": null, - "result_digest": "a94eeb80e26ec38b621b60e6ed90fbe32d9bf9a996d264144b641819c4609823" + "result_digest": "6a0db7d28a756d83974dd681df810f6c93ddd97c371d369242fe2425cbdd0d11" } }, { @@ -181,7 +181,7 @@ "retest": { "actual_outcome": "distinguishable", "diagnostic_kind": null, - "result_digest": "2515d5a64e0d31564078e2ad7be7aea352849b932357a49a7e9107b7d06939e1" + "result_digest": "333be367e829e54fb836ac20297fe98b92c9d5b7e81b23c5a6891171564af531" } } ], @@ -476,7 +476,7 @@ "The witness ends at compiled output." ], "replayable": true, - "result_digest": "a94eeb80e26ec38b621b60e6ed90fbe32d9bf9a996d264144b641819c4609823", + "result_digest": "6a0db7d28a756d83974dd681df810f6c93ddd97c371d369242fe2425cbdd0d11", "source_digest": null }, { @@ -499,7 +499,7 @@ "Distinct digests are a non-vacuity control, not semantic non-equivalence proof." ], "replayable": true, - "result_digest": "2515d5a64e0d31564078e2ad7be7aea352849b932357a49a7e9107b7d06939e1", + "result_digest": "333be367e829e54fb836ac20297fe98b92c9d5b7e81b23c5a6891171564af531", "source_digest": null }, { diff --git a/docs/research/lineage/source-audit-2026-07-12.md b/docs/research/lineage/source-audit-2026-07-12.md index bfee5eb5a..b5b41b0ab 100644 --- a/docs/research/lineage/source-audit-2026-07-12.md +++ b/docs/research/lineage/source-audit-2026-07-12.md @@ -185,6 +185,64 @@ syntax, introduces no source-compatible schema, and claims neither universal refinement nor universal reactive noninterference from the bounded executable counterexamples. +## Issue 1001 Boundary-Flow Control Sources + +On 2026-08-01, issue #1001 extended the participant information-flow audit to +classical lattice and decentralized IFC, robust downgrading, deployed +operating-system IFC, cross-domain controlled interfaces, dynamic taint, and +whole-system provenance. The following primary publication and institutional +source identities were checked: + +- Dorothy E. Denning, *A Lattice Model of Secure Information Flow*, + Communications of the ACM 19(5), 1976, + . +- Andrew C. Myers and Barbara Liskov, *Complete, Safe Information Flow with + Decentralized Labels*, 1998 IEEE Symposium on Security and Privacy, + . +- Andrew C. Myers, Andrei Sabelfeld, and Steve Zdancewic, *Enforcing Robust + Declassification and Qualified Robustness*, Journal of Computer Security + 14(2), 2006, . +- Ethan Cecchetti, Andrew C. Myers, and Owen Arden, *Nonmalleable Information + Flow Control*, ACM CCS 2017, + . +- Nickolai Zeldovich, Silas Boyd-Wickizer, Eddie Kohler, and David Mazières, + *Making Information Flow Explicit in HiStar*, OSDI 2006, + . +- Maxwell Krohn, Alexander Yip, Micah Brodsky, Natan Cliffer, M. Frans + Kaashoek, Eddie Kohler, and Robert Morris, *Information Flow Control for + Standard OS Abstractions*, SOSP 2007, + . +- NIST SP 800-53 Revision 5, control AC-4, *Information Flow Enforcement*, + , and the National + Security Agency's National Cross Domain Strategy and Management Office, + . +- William Enck, Peter Gilbert, Byung-Gon Chun, Landon P. Cox, Jaeyeon Jung, + Patrick McDaniel, and Anmol N. Sheth, *TaintDroid: An Information-Flow + Tracking System for Realtime Privacy Monitoring on Smartphones*, OSDI 2010, + . +- Thomas Pasquier, Xueyuan Han, Mark Goldstein, Thomas Moyer, David Eyers, + Margo Seltzer, and Jean Bacon, *Practical Whole-System Provenance Capture*, + SoCC 2017, . + +RAES adapts the shared semantic discipline: ordered and conservative label +propagation; principal-relative policy; independent confidentiality and +integrity; authority-bounded release and endorsement; controlled-interface +mediation at the final sink; and provenance carriage that does not itself +grant authority. The sources also bound the claim. Fixed global labels do not +capture every mutual-distrust policy; declassification and untaint privileges +can become over-powerful trusted paths; label administration and compatibility +have operational cost; dynamic taint does not completely cover implicit, +native, control, or covert flows; provenance capture has coverage, volume, and +overhead limits; and cross-domain assessment does not prove every downstream +sink safe. + +SEM-233 is therefore an ACES-native, revisioned semantic profile rather than a +copy of any source syntax, label vocabulary, operating-system API, guard +product profile, or wire protocol. It claims no source compatibility, +certification, runtime enforcement, backend completeness, or covert-channel +control. No copied-code notice or third-party distribution obligation is +introduced by these publication and institutional citations. + ## DSL-437 Participant And Simulation Sources This addendum was reviewed on 2026-07-24 for DSL-437. It records semantic diff --git a/docs/research/participant-opacity/current-state-assessment.md b/docs/research/participant-opacity/current-state-assessment.md index 0d23f669f..0691ec57d 100644 --- a/docs/research/participant-opacity/current-state-assessment.md +++ b/docs/research/participant-opacity/current-state-assessment.md @@ -87,6 +87,36 @@ It cannot presently provide: Those outcomes are deliberately assigned to issues #961 through #965. Nothing in issue #810 advances those assurance states. +## Delivery Update Through #965 + +The repository now retains exact historical bounded, model-check, and proof +authorities and adds one separate finite runtime-enforcement lane. The +`participant-opacity-runtime-reference-v1` profile binds an individual active +observer to a complete concrete inventory for decision occurrence/content, +failures and declared opportunities, delivery, retry, logical order and timing +buckets, policy/supervisor effects, participant retrieval, and evidence +visibility. RUN-319 persists only safe profile/predicate/inventory/rule refs and +digests on the owning API-423 decision; the full inventory remains in the +trusted runtime-support context. The supported enforcement rule normalizes +every protected crossing to the same denial, releases no payload or participant +state, and commits the withheld delivery opportunity atomically. Catalog +runtime assurance is therefore `partial`; useful allowed-action and delivered- +view modes remain absent. + +Issue #965 adds one separately governed API-407 feature declaration for +`participant_predicate_opacity` without making it a policy-operation feature. +The reference backend declares bounded support for +`participant-opacity-runtime-reference-v1@sem-231/runtime-rev2`. The generic +target runner records declaration, backend-native realization, and observed +bounded conformance as three independent shared claim bindings, and binds the +exact manifest, profile, configuration, tool, environment, and probe-set +digests. A complete closed observation transcript covers decision and failure +shape, action availability, delivery and omission, retry, logical timing and +order, release-policy effects, external effects, and payload release. +Adversarial changes, unobserved runtime mediation, stale digests, weakening, +and secret-bearing exceptions all fail closed. This remains finite reference- +backend evidence, not proof, universal opacity, or cross-backend equivalence. + ## Recommendation Adopt the one-sided possibilistic kernel in ADR-099 and the focused formal diff --git a/docs/research/participant-opacity/implementation-program.json b/docs/research/participant-opacity/implementation-program.json index f921ee72e..48565fcce 100644 --- a/docs/research/participant-opacity/implementation-program.json +++ b/docs/research/participant-opacity/implementation-program.json @@ -321,6 +321,14 @@ "supported-profile limitations", "runtime-enforcement claim binding" ], + "delivered_artifacts": [ + "contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json", + "contracts/schemas/participant-runtime/participant-crossing-occurrence-v1.json", + "implementations/python/packages/raes_contracts/participant_opacity_runtime.py", + "implementations/python/packages/raes_runtime/participant_crossing_mediation.py", + "implementations/python/packages/raes_runtime/participant_crossing_egress.py", + "implementations/python/tests/test_issue_964_participant_opacity_runtime.py" + ], "explicit_nonclaims": [ "No general opacity, proof, supervisor synthesis, or backend-native realization." ] @@ -355,6 +363,14 @@ "backend/profile/environment digests", "separate declaration, realization, and conformance claim bindings" ], + "delivered_artifacts": [ + "contracts/profiles/behavioral-relation/participant-opacity-runtime-reference-v1.json", + "implementations/python/packages/raes_contracts/manifest_authority.py", + "implementations/python/packages/raes_reference_backend/manifest.py", + "implementations/python/packages/raes_conformance/conformance/participant_opacity_probes.py", + "implementations/python/packages/raes_conformance/conformance/report.py", + "implementations/python/tests/test_issue_965_participant_opacity_backend.py" + ], "explicit_nonclaims": [ "No universal backend opacity, proof, cross-backend equivalence, or unnamed profile support." ] diff --git a/docs/research/participant-opacity/implementation-program.md b/docs/research/participant-opacity/implementation-program.md index ae9e7323c..618f0872a 100644 --- a/docs/research/participant-opacity/implementation-program.md +++ b/docs/research/participant-opacity/implementation-program.md @@ -103,6 +103,17 @@ memory, and active-probe bypasses. Runtime mediation is not a universal opacity result, supervisor synthesis, or backend-native realization. +Delivered by #964: `participant-opacity-runtime-reference-v1@sem-231/runtime-rev1`, +one closed concrete observation inventory, exact catalog/profile/claim +admission, compact `runtime-enforcement/enforced/finite` bindings on API-423 +decisions, idempotency and restart joins, and an exact uniform-denial mediator. +The mediator releases no participant payload or state and atomically records +the withheld egress opportunity. Unsupported or stale coordinates deny, while +an authorized weakening omits the positive opacity claim. The delivery excludes +useful allowed-action and delivered-view modes as well as +wall-clock/timed, probabilistic, coalition, partial-order, native-backend, and +unbounded opacity. + ### #965: Backend declaration, realization, and conformance Extend the existing API-407 participant-feature support vector for named @@ -113,11 +124,22 @@ profile, environment, contract, and evidence identities. Method presence or a boolean is not capability evidence. Bounded conformance is not proof, cross-backend equivalence, or support outside the named profile. +Delivered by #965: the governed `participant_predicate_opacity` API-407 +feature and required-contract map, bounded reference-backend declaration, +generic target-owned complete-transcript probes, three independent shared +claim bindings, exact six-way digest provenance, and fail-closed report +validation. The adversarial suite rejects backend observation drift, +runtime-only mediation, unexecuted declarations, forged manifest identity, +authorized weakening that retains stronger claims, missing contracts or +evidence, and secret-bearing diagnostics. + ## Program Invariants -- All claims use the exact current behavioral-taxonomy revision (currently - `raes-behavioral-relations@rev8`) and one explicit assurance axis. `rev5` - introduced opacity; it is not a permanent implementation pin. +- All claims use the exact applicable behavioral-taxonomy revision (current + authoring authority `raes-behavioral-relations@rev12`) and one explicit + assurance axis. The shipped runtime-reference profile retains its exact + historical `rev11` binding; `rev5` introduced opacity and is not a permanent + implementation pin. - Universal opacity requires model-check or proof evidence whose scope matches the claim. - Actual and alternative active points use the same participant strategy. diff --git a/docs/research/participant-opacity/index.md b/docs/research/participant-opacity/index.md index 89f34fb4b..e69f20619 100644 --- a/docs/research/participant-opacity/index.md +++ b/docs/research/participant-opacity/index.md @@ -20,6 +20,8 @@ deployment, participant, or backend. - [Issue #961 bounded-falsification preflight](../../decisions/issue-961-participant-opacity-bounded-falsification-preflight.md) - [Issue #962 finite-state model-check preflight](../../decisions/issue-962-participant-opacity-model-check-preflight.md) - [Issue #963 mathematical-proof preflight](../../decisions/issue-963-participant-opacity-proof-preflight.md) +- [Issue #964 reference-runtime enforcement preflight](../../decisions/issue-964-participant-opacity-runtime-enforcement-preflight.md) +- [Issue #965 backend declaration, realization, and conformance preflight](../../decisions/issue-965-participant-opacity-backend-realization-preflight.md) - [Isabelle/HOL theorem source](../../../specs/formal/participant-semantics/isabelle/Participant_Opacity.thy) - [Checked proof evidence](../../../specs/formal/participant-semantics/participant-opacity-proof-evidence.json) - [ADR-099](../../decisions/adrs/adr-099-participant-relative-predicate-opacity.md) diff --git a/docs/research/specification-coverage/analysis-v1.1.json b/docs/research/specification-coverage/analysis-v1.1.json index d68c2ebd9..44f261487 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": "6c8551e4341abcbb26408ba5898fda26a1c7a4c096d0c5433f44818c10c29045", + "snapshot_sha256": "cb01647925b65fb17d88177b9eeb8b553be1b8e0183827cfff501ced2018464c", "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 741623595..c86b552d4 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": "c055119e33a6825fe953fc7130a298dc610dc7eb9d17420c2006c5495d95b749", + "snapshot_sha256": "1186ec10ef380e3b0e638d215594c1f218139e6235ca1df0601e0d22524592c0", "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 03fca0059..dda2ae354 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": "e88617c6b378b4c11e63bcc23039fa81d0e0fd1a2211180c6dfa6b902159f8aa", + "snapshot_sha256": "1cca169b829d841d36f71c49c0dbb8e59a0ea70f83eb809325087aeb06f7e387", "analysis_path": "docs/research/specification-coverage/analysis-v1.json", - "analysis_sha256": "c4c2308607e12741e1dc43f518f9313095b7882849b5fdd5c408b54f86a0032b" + "analysis_sha256": "4960c27b6b9bddda77c0c4d830eddb8c181544400d83eeb2293dbea4764bf38a" } 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 315cf677f..6461dd02a 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": "5f8e060dc651021350dcee3e0e8bf03d2d5a25849c1fe6f760255ef286712119", + "snapshot_sha256": "281e69e3b7dee7ad1fffa4dd1b20e7ad9d6a5a222a802d5c327af03ceac3a1bc", "analysis_path": "docs/research/specification-coverage/analysis-v1.1.json", - "analysis_sha256": "c3cd8dd7faeb7d7ea69164e6fb9e9069b1e7c3cc3e28989b70a9a1fe56b86114" + "analysis_sha256": "e3c106de9c2f7fb70113a8400b48b13a4be9c6073bfdae585525ae3610962075" } diff --git a/docs/research/specification-coverage/execution-snapshot-v1.1.json b/docs/research/specification-coverage/execution-snapshot-v1.1.json index ab031d801..ff9a9956b 100644 --- a/docs/research/specification-coverage/execution-snapshot-v1.1.json +++ b/docs/research/specification-coverage/execution-snapshot-v1.1.json @@ -63,7 +63,7 @@ "artifact_id": "known-limitations", "kind": "documentation", "path": "docs/explain/sdl/limitations.md", - "sha256": "4a673316b341fd5beca10e3dd87aa35ba762e4668d78d1b48cb706074f0c720c", + "sha256": "129cf17810aad4c51988bc872e28fe43ae95019a80053c42d800ff7e2b9cc93e", "validator": "documentation evidence only" } ], diff --git a/docs/research/specification-coverage/execution-snapshot-v1.json b/docs/research/specification-coverage/execution-snapshot-v1.json index 32a64088a..a6c55c9a5 100644 --- a/docs/research/specification-coverage/execution-snapshot-v1.json +++ b/docs/research/specification-coverage/execution-snapshot-v1.json @@ -63,7 +63,7 @@ "artifact_id": "known-limitations", "kind": "documentation", "path": "docs/explain/sdl/limitations.md", - "sha256": "4a673316b341fd5beca10e3dd87aa35ba762e4668d78d1b48cb706074f0c720c", + "sha256": "129cf17810aad4c51988bc872e28fe43ae95019a80053c42d800ff7e2b9cc93e", "validator": "documentation evidence only" } ], diff --git a/implementations/python/packages/raes/__init__.py b/implementations/python/packages/raes/__init__.py index 57422e44c..21fd9970e 100644 --- a/implementations/python/packages/raes/__init__.py +++ b/implementations/python/packages/raes/__init__.py @@ -35,6 +35,16 @@ "load_sdl_fragment", "parse_sdl", "parse_sdl_file", + "ArtifactTransformationPolicy", + "CanonicalArtifactComparison", + "PortableContractTransformationResult", + "RemoveSDLDeclarationRequest", + "RenameSDLDeclarationRequest", + "canonicalize_portable_contract", + "compare_canonical_artifacts", + "remove_sdl_declaration", + "rename_sdl_declaration", + "SDLTransformationResult", "select_scenario_family", "Scenario", "SDLError", @@ -94,6 +104,19 @@ def __getattr__(name: str): module = import_module("raes.selected_scenario") elif name in {"load_sdl_fragment", "parse_sdl", "parse_sdl_file"}: module = import_module("raes.parser") + elif name in { + "ArtifactTransformationPolicy", + "CanonicalArtifactComparison", + "PortableContractTransformationResult", + "RemoveSDLDeclarationRequest", + "RenameSDLDeclarationRequest", + "canonicalize_portable_contract", + "compare_canonical_artifacts", + "remove_sdl_declaration", + "rename_sdl_declaration", + "SDLTransformationResult", + }: + module = import_module("raes.transformations") elif name in {"InstantiatedScenario", "Scenario"}: module = import_module("raes.scenario") else: diff --git a/implementations/python/packages/raes/_declarations.py b/implementations/python/packages/raes/_declarations.py index f24593f06..4d309e08c 100644 --- a/implementations/python/packages/raes/_declarations.py +++ b/implementations/python/packages/raes/_declarations.py @@ -445,6 +445,8 @@ def _add_forwarding_agent_declarations(index: DeclarationIndex, scenario: Scenar kind="forwarding-agent", address_parts=("forwarding_agents", *_qualified_parts(agent.forwarding_agent_id)), model_path=f"forwarding_agents.{position}.forwarding_agent_id", + referenceable=True, + targetable=True, ) diff --git a/implementations/python/packages/raes/_transformation_bindings.py b/implementations/python/packages/raes/_transformation_bindings.py new file mode 100644 index 000000000..dcb7bbcd6 --- /dev/null +++ b/implementations/python/packages/raes/_transformation_bindings.py @@ -0,0 +1,113 @@ +"""External concept-binding retargeting for SDL transformations.""" + +from __future__ import annotations + +from raes_contracts.contracts import ( + ExternalConceptBindingDocumentModel, + ExternalConceptSubjectModel, +) + +from ._transformation_types import SDLAuthoringArtifact +from .external_concept_subjects import external_concept_subjects + + +def _binding_subject_payload(binding: object) -> dict[str, object]: + if not isinstance(binding, dict) or not isinstance(binding.get("subject"), dict): + raise ValueError("external concept binding document has an invalid subject") + return binding["subject"] + + +def _same_subject_coordinate( + source_subject: ExternalConceptSubjectModel | None, + subject: dict[str, object], +) -> bool: + return source_subject is not None and ( + source_subject.subject_kind == subject.get("subject_kind") + and source_subject.owning_contract_id == subject.get("owning_contract_id") + and source_subject.lifecycle_phase.value == subject.get("lifecycle_phase") + ) + + +def _retarget_binding_subject( + subject: dict[str, object], + *, + source_subjects: dict[str, ExternalConceptSubjectModel], + target_subjects: dict[str, ExternalConceptSubjectModel], + source_digest: str, + target_digest: str, + before: str, + after: str, +) -> None: + canonical_ref = str(subject.get("canonical_ref", "")) + source_subject = source_subjects.get(canonical_ref) + same_source_coordinate = _same_subject_coordinate(source_subject, subject) + supplied_digest = str(subject.get("artifact_digest", "")) + source_digest_matches = supplied_digest.casefold() == source_digest.casefold() + if same_source_coordinate and not source_digest_matches: + raise ValueError("external concept subject is stale for the source artifact") + if not source_digest_matches: + return + if not same_source_coordinate or source_subject is None: + raise ValueError("external concept subject does not resolve in the source artifact") + rewritten_ref = after if canonical_ref == before else canonical_ref + target_subject = target_subjects.get(rewritten_ref) + if target_subject is None or target_subject.subject_kind != source_subject.subject_kind: + raise ValueError("external concept subject does not resolve in the target artifact") + subject["canonical_ref"] = rewritten_ref + subject["artifact_digest"] = target_digest + + +def _retarget_binding_document( + document: ExternalConceptBindingDocumentModel, + *, + source_subjects: dict[str, ExternalConceptSubjectModel], + target_subjects: dict[str, ExternalConceptSubjectModel], + source_digest: str, + target_digest: str, + before: str, + after: str, +) -> ExternalConceptBindingDocumentModel: + payload = document.model_dump(mode="json") + bindings = payload.get("bindings") + if not isinstance(bindings, dict): + raise ValueError("external concept binding document has an invalid binding map") + for binding in bindings.values(): + _retarget_binding_subject( + _binding_subject_payload(binding), + source_subjects=source_subjects, + target_subjects=target_subjects, + source_digest=source_digest, + target_digest=target_digest, + before=before, + after=after, + ) + return ExternalConceptBindingDocumentModel.model_validate(payload) + + +def _retarget_binding_documents( + documents: tuple[ExternalConceptBindingDocumentModel, ...], + *, + source: SDLAuthoringArtifact, + target: SDLAuthoringArtifact, + source_digest: str, + target_digest: str, + before: str, + after: str, +) -> tuple[ExternalConceptBindingDocumentModel, ...]: + source_subjects = {subject.canonical_ref: subject for subject in external_concept_subjects(source)} + target_subjects = {subject.canonical_ref: subject for subject in external_concept_subjects(target)} + return tuple( + _retarget_binding_document( + document, + source_subjects=source_subjects, + target_subjects=target_subjects, + source_digest=source_digest, + target_digest=target_digest, + before=before, + after=after, + ) + for document in documents + ) + + +__all__ = [] diff --git a/implementations/python/packages/raes/_transformation_portable.py b/implementations/python/packages/raes/_transformation_portable.py new file mode 100644 index 000000000..a51210c92 --- /dev/null +++ b/implementations/python/packages/raes/_transformation_portable.py @@ -0,0 +1,123 @@ +"""Canonical portable-contract transformations and comparisons.""" + +from __future__ import annotations + +from copy import deepcopy + +from raes_contracts.canonical import canonical_json_digest +from raes_contracts.contracts import ( + ArtifactTransformationKind, + ArtifactTransformationPreservationModel, + ArtifactTransformationReportModel, + ArtifactTransformationStatus, + PreservationOutcome, + TransformationCheckOutcome, +) +from raes_contracts.contracts.base import ContractModel + +from ._transformation_support import ( + CANONICAL_IDENTITY_PROFILE, + DEFAULT_POLICY_PAYLOAD, + PORTABLE_CANONICAL_PROFILE, + PORTABLE_CANONICALIZE_PROFILE, + check, +) +from ._transformation_types import ( + CanonicalArtifactComparison, + PortableContractTransformationResult, + SDLAuthoringArtifact, +) +from .canonical import canonical_sdl_digest +from .scenario import ExpandedScenario, Scenario + + +def _portable_contract_profile(model: ContractModel) -> str: + for attribute in ("schema_version", "profile"): + value = getattr(model, attribute, None) + if isinstance(value, str) and value: + return value + raise TypeError("portable transformation requires an explicit governed schema_version or profile") + + +def canonicalize_portable_contract( + source: ContractModel, +) -> PortableContractTransformationResult: + """Re-admit an isolated portable contract and verify canonical identity.""" + + if not isinstance(source, ContractModel): + raise TypeError("portable contract canonicalization requires a ContractModel") + source_payload = source.model_dump(mode="json") + source_digest = canonical_json_digest(source_payload) + output = type(source).model_validate(deepcopy(source_payload)) + target_digest = canonical_json_digest(output.model_dump(mode="json")) + if source_digest != target_digest: + raise ValueError("portable contract readmission did not preserve canonical identity") + contract_profile = _portable_contract_profile(source) + policy_digest = canonical_json_digest(DEFAULT_POLICY_PAYLOAD) + derivation_digest = canonical_json_digest( + { + "operation_profile": PORTABLE_CANONICALIZE_PROFILE, + "policy_digest": policy_digest, + "source_digest": source_digest, + "source_profile": contract_profile, + } + ) + report = ArtifactTransformationReportModel( + operation_profile=PORTABLE_CANONICALIZE_PROFILE, + status=ArtifactTransformationStatus.SUCCESS, + artifact_kind=ArtifactTransformationKind.PORTABLE_CONTRACT, + source_profile=contract_profile, + target_profile=contract_profile, + canonicalization_profile=PORTABLE_CANONICAL_PROFILE, + source_digest=source_digest, + target_digest=target_digest, + policy_digest=policy_digest, + derivation_digest=derivation_digest, + preconditions=(check("source-admitted", TransformationCheckOutcome.PASSED),), + postconditions=( + check("canonical-identity", TransformationCheckOutcome.PASSED), + check("target-admitted", TransformationCheckOutcome.PASSED), + ), + preservation=ArtifactTransformationPreservationModel( + profile=CANONICAL_IDENTITY_PROFILE, + outcome=PreservationOutcome.VERIFIED, + evidence_digests=(source_digest,), + limitations=("Canonical identity does not establish behavioral or backend equivalence.",), + ), + ) + return PortableContractTransformationResult(output=output, report=report) + + +def compare_canonical_artifacts( + left: SDLAuthoringArtifact | ContractModel, + right: SDLAuthoringArtifact | ContractModel, +) -> CanonicalArtifactComparison: + """Compare two artifacts of the same kind under their owning canonicalizer.""" + + if isinstance(left, (Scenario, ExpandedScenario)) and isinstance(right, (Scenario, ExpandedScenario)): + left_identity = canonical_sdl_digest(left) + right_identity = canonical_sdl_digest(right) + artifact_kind = ArtifactTransformationKind.SDL_AUTHORING + canonicalization_profile = left_identity.profile + left_digest = left_identity.value + right_digest = right_identity.value + elif isinstance(left, ContractModel) and isinstance(right, ContractModel): + if _portable_contract_profile(left) != _portable_contract_profile(right): + raise TypeError("portable canonical comparison requires the same exact contract profile") + artifact_kind = ArtifactTransformationKind.PORTABLE_CONTRACT + canonicalization_profile = PORTABLE_CANONICAL_PROFILE + left_digest = canonical_json_digest(left.model_dump(mode="json")) + right_digest = canonical_json_digest(right.model_dump(mode="json")) + else: + raise TypeError("canonical comparison requires two artifacts of the same supported artifact kind") + return CanonicalArtifactComparison( + artifact_kind=artifact_kind, + canonicalization_profile=canonicalization_profile, + relation_profile=CANONICAL_IDENTITY_PROFILE, + left_digest=left_digest, + right_digest=right_digest, + equivalent=left_digest == right_digest, + ) + + +__all__ = ["canonicalize_portable_contract", "compare_canonical_artifacts"] diff --git a/implementations/python/packages/raes/_transformation_remove.py b/implementations/python/packages/raes/_transformation_remove.py new file mode 100644 index 000000000..66dc34cf5 --- /dev/null +++ b/implementations/python/packages/raes/_transformation_remove.py @@ -0,0 +1,171 @@ +"""Explicit-loss SDL declaration removal.""" + +from __future__ import annotations + +from copy import deepcopy + +from pydantic import ValidationError +from raes_contracts.contracts import ( + ArtifactTransformationKind, + ArtifactTransformationLossKind, + ArtifactTransformationPreservationModel, + ArtifactTransformationReportModel, + ArtifactTransformationStatus, + ExternalConceptBindingDocumentModel, + PreservationOutcome, + TransformationCheckOutcome, +) +from raes_contracts.diagnostics import Severity + +from ._declarations import build_declaration_index +from ._errors import SDLParseError, SDLValidationError +from ._transformation_support import ( + EXPLICIT_LOSS_PROFILE, + REMOVE_PROFILE, + SDL_CONTRACT_PROFILE, + check, + refused_removal, + removal_loss, + remove_derivation_digest, + top_level_declaration, + transformation_policy_digest, +) +from ._transformation_types import ( + ArtifactTransformationPolicy, + RemoveSDLDeclarationRequest, + SDLAuthoringArtifact, + SDLTransformationResult, +) +from .canonical import SDLCanonicalDigest, canonical_sdl_digest +from .scenario import ExpandedScenario, Scenario +from .validator import SemanticValidator + + +def _removed_candidate( + source: SDLAuthoringArtifact, + *, + section: str, + local_key: str, +) -> SDLAuthoringArtifact: + payload = deepcopy(source.model_dump(mode="python", by_alias=True, exclude_unset=True)) + section_payload = payload.get(section) + if not isinstance(section_payload, dict) or local_key not in section_payload: + raise ValueError("selected declaration is absent from the explicit source payload") + del section_payload[local_key] + module = payload.get("module") + if isinstance(module, dict): + exports = module.get("exports") + if isinstance(exports, dict) and isinstance(exports.get(section), list): + exports[section] = [name for name in exports[section] if name != local_key] + candidate = type(source).model_validate(payload) + validator = SemanticValidator(candidate) + validator.validate() + candidate._set_advisories(validator.warnings) + candidate._set_source_diagnostics(list(source.source_diagnostics)) + candidate._set_semantic_validated(True) + return candidate + + +def _successful_removal_result( + target: SDLAuthoringArtifact, + request: RemoveSDLDeclarationRequest, + policy: ArtifactTransformationPolicy, + source_digest: SDLCanonicalDigest, +) -> SDLTransformationResult: + target_digest = canonical_sdl_digest(target) + policy_digest = transformation_policy_digest(policy) + report = ArtifactTransformationReportModel( + operation_profile=REMOVE_PROFILE, + status=ArtifactTransformationStatus.SUCCESS, + artifact_kind=ArtifactTransformationKind.SDL_AUTHORING, + source_profile=SDL_CONTRACT_PROFILE, + target_profile=SDL_CONTRACT_PROFILE, + canonicalization_profile=source_digest.profile, + source_digest=source_digest.value, + target_digest=target_digest.value, + policy_digest=policy_digest, + derivation_digest=remove_derivation_digest( + source_digest=source_digest.value, + policy_digest=policy_digest, + request=request, + ), + preconditions=tuple( + check(name, TransformationCheckOutcome.PASSED) + for name in ("loss-authorized", "source-admitted", "target-exact") + ), + postconditions=(check("target-admitted", TransformationCheckOutcome.PASSED),), + affected_identities=(request.target_address,), + preservation=ArtifactTransformationPreservationModel( + profile=EXPLICIT_LOSS_PROFILE, + outcome=PreservationOutcome.NOT_APPLICABLE, + evidence_digests=tuple(sorted({source_digest.value, target_digest.value})), + limitations=("Removal does not preserve the selected declaration.",), + ), + losses=(removal_loss(request.target_address, severity=Severity.WARNING),), + ) + return SDLTransformationResult(output=target, binding_documents=(), report=report) + + +def remove_sdl_declaration( + source: SDLAuthoringArtifact, + request: RemoveSDLDeclarationRequest, + *, + policy: ArtifactTransformationPolicy | None = None, + binding_documents: tuple[ExternalConceptBindingDocumentModel, ...] = (), +) -> SDLTransformationResult: + """Remove one exact unreferenced declaration under explicit typed loss policy.""" + + if not isinstance(source, (Scenario, ExpandedScenario)) or not source.semantic_validated: + raise SDLParseError("SDL transformation requires a semantically admitted authoring scenario") + resolved_policy = ArtifactTransformationPolicy() if policy is None else policy + source_digest = canonical_sdl_digest(source) + resolved = top_level_declaration(source, build_declaration_index(source), request.target_address) + if resolved is None: + result = refused_removal( + source_digest=source_digest, + request=request, + policy=resolved_policy, + diagnostic_code="artifact-transformation.target-not-exact", + message="The request target is not an exact supported declaration address.", + ) + else: + _, section, local_key = resolved + if ArtifactTransformationLossKind.DECLARATION_REMOVED not in resolved_policy.allowed_loss_kinds: + result = refused_removal( + source_digest=source_digest, + request=request, + policy=resolved_policy, + diagnostic_code="artifact-transformation.loss-not-authorized", + message="Declaration removal requires explicit authorization of its exact loss kind.", + passed_checks=("source-admitted", "target-exact"), + include_loss=True, + ) + elif binding_documents: + result = refused_removal( + source_digest=source_digest, + request=request, + policy=resolved_policy, + diagnostic_code="artifact-transformation.linked-artifact-unsupported", + message="Declaration removal cannot silently discard or retarget supplied concept bindings.", + passed_checks=("loss-authorized", "source-admitted", "target-exact"), + include_loss=True, + ) + else: + try: + target = _removed_candidate(source, section=section, local_key=local_key) + except (SDLValidationError, ValidationError, TypeError, ValueError): + result = refused_removal( + source_digest=source_digest, + request=request, + policy=resolved_policy, + diagnostic_code="artifact-transformation.target-invalid", + message="The complete transformed candidate failed structural or semantic admission.", + passed_checks=("loss-authorized", "source-admitted", "target-exact"), + include_loss=True, + ) + else: + result = _successful_removal_result(target, request, resolved_policy, source_digest) + return result + + +__all__ = ["remove_sdl_declaration"] diff --git a/implementations/python/packages/raes/_transformation_rename.py b/implementations/python/packages/raes/_transformation_rename.py new file mode 100644 index 000000000..7cc7e49a9 --- /dev/null +++ b/implementations/python/packages/raes/_transformation_rename.py @@ -0,0 +1,468 @@ +"""Atomic SDL declaration rename and identity transport.""" + +from __future__ import annotations + +from copy import deepcopy +from dataclasses import dataclass + +from pydantic import ValidationError +from raes_contracts.contracts import ( + ArtifactTransformationIdentityMapModel, + ArtifactTransformationKind, + ArtifactTransformationPreservationModel, + ArtifactTransformationReportModel, + ArtifactTransformationStatus, + ExternalConceptBindingDocumentModel, + PreservationOutcome, + TransformationCheckOutcome, +) + +from ._declarations import Declaration, DeclarationIndex, build_declaration_index +from ._errors import SDLParseError, SDLValidationError +from ._identifiers import QualifiedName +from ._module_symbols import FORWARDING_AGENTS_SECTION, symbol_index +from ._transformation_bindings import _retarget_binding_documents +from ._transformation_support import ( + IDENTITY_TRANSPORT_PROFILE, + RENAME_PROFILE, + SDL_CONTRACT_PROFILE, + check, + default_policy_digest, + refused_rename, + rename_derivation_digest, + top_level_declaration, +) +from ._transformation_types import ( + RenameSDLDeclarationRequest, + SDLAuthoringArtifact, + SDLTransformationResult, +) +from .canonical import SDLCanonicalDigest, canonical_sdl_bytes, canonical_sdl_digest +from .composition import _rewrite_payload_with_symbols +from .scenario import ExpandedScenario, ModuleDescriptor, Scenario +from .validator import SemanticValidator + + +def _new_qualified_key(*, section: str, old_key: str, new_local_name: str) -> str: + old_name = QualifiedName.parse(old_key) + if section == "nodes" and len(new_local_name) > 35: + raise ValueError("node identifiers must remain within the 35-character local limit") + return QualifiedName((*old_name.parts[:-1], new_local_name)).render() + + +def _identity_symbols( + scenario: SDLAuthoringArtifact, +) -> dict[str, dict[str, str] | set[str]]: + symbols = symbol_index( + scenario, + namespace="", + descriptor=ModuleDescriptor(id="raes/transformation", version="1"), + restrict_to_descriptor=False, + ) + symbols[FORWARDING_AGENTS_SECTION] = { + agent.forwarding_agent_id: agent.forwarding_agent_id for agent in scenario.forwarding_agents + } + return symbols + + +def _rename_symbols( + scenario: SDLAuthoringArtifact, + *, + section: str, + old_key: str, + new_key: str, +) -> dict[str, dict[str, str] | set[str]]: + symbols = _identity_symbols(scenario) + section_symbols = symbols[section] + named_symbols = symbols["named"] + if not isinstance(section_symbols, dict) or not isinstance(named_symbols, dict): + raise TypeError("SDL symbol index returned an invalid mapping shape") + + section_symbols[old_key] = new_key + old_address = f"{section}.{old_key}" + new_address = f"{section}.{new_key}" + rewritten_named: dict[str, str] = {} + for alias, target in named_symbols.items(): + if target == old_key: + rewritten_named[alias] = new_key + elif target == old_address: + rewritten_named[alias] = new_address + elif target.startswith(f"{old_address}."): + rewritten_named[alias] = f"{new_address}{target.removeprefix(old_address)}" + else: + rewritten_named[alias] = target + rewritten_named[old_key] = new_key + rewritten_named[old_address] = new_address + symbols["named"] = rewritten_named + return symbols + + +def _rewrite_module_exports( + payload: dict[str, object], + *, + section: str, + old_key: str, + new_key: str, +) -> None: + module = payload.get("module") + if not isinstance(module, dict): + return + exports = module.get("exports") + if not isinstance(exports, dict): + return + exported_names = exports.get(section) + if isinstance(exported_names, list): + exports[section] = [new_key if name == old_key else name for name in exported_names] + + +def _preserve_explicit_mapping_shape( + template: dict[object, object], + rewritten: dict[object, object], +) -> dict[object, object]: + result: dict[object, object] = {} + matched_template: set[object] = set() + matched_rewritten: set[object] = set() + for key in template: + if key in rewritten: + result[key] = _preserve_explicit_shape(template[key], rewritten[key]) + matched_template.add(key) + matched_rewritten.add(key) + template_only = [key for key in template if key not in matched_template] + rewritten_only = [key for key in rewritten if key not in matched_rewritten] + if len(template_only) > len(rewritten_only): + raise ValueError("reference rewrite changed the explicit payload shape") + for template_key, rewritten_key in zip(template_only, rewritten_only, strict=False): + result[rewritten_key] = _preserve_explicit_shape(template[template_key], rewritten[rewritten_key]) + return result + + +def _preserve_explicit_sequence_shape( + template: list[object] | tuple[object, ...], + rewritten: list[object] | tuple[object, ...], +) -> list[object] | tuple[object, ...]: + if len(template) != len(rewritten): + raise ValueError("reference rewrite changed an explicit sequence shape") + items = [ + _preserve_explicit_shape(template_item, rewritten_item) + for template_item, rewritten_item in zip(template, rewritten, strict=True) + ] + result: list[object] | tuple[object, ...] = tuple(items) if isinstance(template, tuple) else items + return result + + +def _preserve_explicit_shape(template: object, rewritten: object) -> object: + """Remove helper-populated defaults while retaining rewritten map keys.""" + + result = rewritten + if isinstance(template, dict) and isinstance(rewritten, dict): + result = _preserve_explicit_mapping_shape(template, rewritten) + elif (isinstance(template, list) and isinstance(rewritten, list)) or ( + isinstance(template, tuple) and isinstance(rewritten, tuple) + ): + result = _preserve_explicit_sequence_shape(template, rewritten) + return result + + +def _rewritten_candidate( + source: SDLAuthoringArtifact, + *, + section: str, + old_key: str, + new_key: str, +) -> SDLAuthoringArtifact: + payload = source.model_dump(mode="python", by_alias=True, exclude_unset=True) + explicit_shape = deepcopy(payload) + rewritten = _rewrite_payload_with_symbols( + payload, + symbols=_rename_symbols(source, section=section, old_key=old_key, new_key=new_key), + ) + _rewrite_module_exports(rewritten, section=section, old_key=old_key, new_key=new_key) + rewritten = _preserve_explicit_shape(explicit_shape, rewritten) + if not isinstance(rewritten, dict): + raise TypeError("SDL reference rewrite returned a non-object payload") + candidate = type(source).model_validate(rewritten) + validator = SemanticValidator(candidate) + validator.validate() + candidate._set_advisories(validator.warnings) + candidate._set_source_diagnostics(list(source.source_diagnostics)) + candidate._set_semantic_validated(True) + return candidate + + +@dataclass(frozen=True, slots=True) +class _RenameSelection: + declaration: Declaration + section: str + old_key: str + new_key: str + new_address: str + + +@dataclass(frozen=True, slots=True) +class _RenamedArtifacts: + target: SDLAuthoringArtifact + target_digest: SDLCanonicalDigest + binding_documents: tuple[ExternalConceptBindingDocumentModel, ...] + + +def _select_rename( + source: SDLAuthoringArtifact, + request: RenameSDLDeclarationRequest, + source_digest: SDLCanonicalDigest, + index: DeclarationIndex, +) -> _RenameSelection | SDLTransformationResult: + resolved = top_level_declaration(source, index, request.target_address) + result: _RenameSelection | SDLTransformationResult + if resolved is None: + result = refused_rename( + source_digest=source_digest, + request=request, + diagnostic_code="artifact-transformation.target-not-exact", + message="The request target is not an exact supported declaration address.", + ) + else: + declaration, section, old_key = resolved + try: + new_key = _new_qualified_key( + section=section, + old_key=old_key, + new_local_name=request.new_local_name, + ) + except ValueError: + result = refused_rename( + source_digest=source_digest, + request=request, + diagnostic_code="artifact-transformation.target-unsupported", + message="The requested replacement is outside the declaration's supported identity boundary.", + passed_checks=("source-admitted", "target-exact"), + affected_identities=(request.target_address,), + ) + else: + new_address = f"{section}.{new_key}" + collision = index.declaration_for(new_address) + if collision is not None and collision.address != declaration.address: + result = refused_rename( + source_digest=source_digest, + request=request, + diagnostic_code="artifact-transformation.identity-collision", + message="The requested replacement collides with an existing canonical declaration.", + passed_checks=("source-admitted", "target-exact", "target-supported"), + affected_identities=(request.target_address,), + ) + else: + result = _RenameSelection( + declaration=declaration, + section=section, + old_key=old_key, + new_key=new_key, + new_address=new_address, + ) + return result + + +def _build_renamed_artifacts( + source: SDLAuthoringArtifact, + request: RenameSDLDeclarationRequest, + source_digest: SDLCanonicalDigest, + selection: _RenameSelection, + binding_documents: tuple[ExternalConceptBindingDocumentModel, ...], +) -> _RenamedArtifacts | SDLTransformationResult: + result: _RenamedArtifacts | SDLTransformationResult + try: + target = _rewritten_candidate( + source, + section=selection.section, + old_key=selection.old_key, + new_key=selection.new_key, + ) + except (SDLValidationError, ValidationError, TypeError, ValueError): + result = refused_rename( + source_digest=source_digest, + request=request, + diagnostic_code="artifact-transformation.target-invalid", + message="The complete transformed candidate failed structural or semantic admission.", + passed_checks=("source-admitted", "target-exact", "target-injective", "target-supported"), + affected_identities=(request.target_address,), + ) + else: + target_digest = canonical_sdl_digest(target) + try: + transformed_bindings = _retarget_binding_documents( + binding_documents, + source=source, + target=target, + source_digest=source_digest.value, + target_digest=target_digest.value, + before=request.target_address, + after=selection.new_address, + ) + except (ValidationError, ValueError): + result = refused_rename( + source_digest=source_digest, + request=request, + diagnostic_code="artifact-transformation.linked-artifact-stale", + message="A supplied linked artifact does not resolve against the exact source and target identities.", + passed_checks=("source-admitted", "target-exact", "target-injective", "target-supported"), + affected_identities=(request.target_address,), + ) + else: + result = _RenamedArtifacts( + target=target, + target_digest=target_digest, + binding_documents=transformed_bindings, + ) + return result + + +def _rename_preservation_verified( + source: SDLAuthoringArtifact, + request: RenameSDLDeclarationRequest, + index: DeclarationIndex, + selection: _RenameSelection, + target: SDLAuthoringArtifact, +) -> bool: + try: + round_trip = _rewritten_candidate( + target, + section=selection.section, + old_key=selection.new_key, + new_key=selection.old_key, + ) + round_trip_digest = canonical_sdl_digest(round_trip) + except (SDLValidationError, ValidationError, TypeError, ValueError): + return False + target_index = build_declaration_index(target) + target_declaration = target_index.declaration_for(selection.new_address) + return ( + round_trip_digest is not None + and canonical_sdl_bytes(round_trip) == canonical_sdl_bytes(source) + and target_declaration is not None + and target_declaration.kind == selection.declaration.kind + and target_index.declaration_for(request.target_address) is None + and len(target_index.declarations) == len(index.declarations) + ) + + +def _successful_rename_result( + request: RenameSDLDeclarationRequest, + source_digest: SDLCanonicalDigest, + selection: _RenameSelection, + artifacts: _RenamedArtifacts, +) -> SDLTransformationResult: + policy_digest = default_policy_digest() + report = ArtifactTransformationReportModel( + operation_profile=RENAME_PROFILE, + status=ArtifactTransformationStatus.SUCCESS, + artifact_kind=ArtifactTransformationKind.SDL_AUTHORING, + source_profile=SDL_CONTRACT_PROFILE, + target_profile=SDL_CONTRACT_PROFILE, + canonicalization_profile=source_digest.profile, + source_digest=source_digest.value, + target_digest=artifacts.target_digest.value, + policy_digest=policy_digest, + derivation_digest=rename_derivation_digest( + source_digest=source_digest.value, + policy_digest=policy_digest, + request=request, + ), + preconditions=tuple( + check(name, TransformationCheckOutcome.PASSED) + for name in ( + "linked-artifacts-current", + "source-admitted", + "target-exact", + "target-injective", + "target-supported", + ) + ), + postconditions=tuple( + check(name, TransformationCheckOutcome.PASSED) + for name in ( + "identity-map-bijective", + "linked-artifacts-retargeted", + "round-trip-canonical-identity", + "target-admitted", + ) + ), + affected_identities=(request.target_address,), + identity_map=( + ArtifactTransformationIdentityMapModel( + declaration_kind=selection.declaration.kind, + before=request.target_address, + after=selection.new_address, + ), + ), + preservation=ArtifactTransformationPreservationModel( + profile=IDENTITY_TRANSPORT_PROFILE, + outcome=PreservationOutcome.VERIFIED, + evidence_digests=tuple(sorted({source_digest.value, artifacts.target_digest.value})), + limitations=("Finite structural and semantic verification does not establish behavioral equivalence.",), + ), + ) + return SDLTransformationResult( + output=artifacts.target, + binding_documents=artifacts.binding_documents, + report=report, + ) + + +def _finish_rename( + source: SDLAuthoringArtifact, + request: RenameSDLDeclarationRequest, + source_digest: SDLCanonicalDigest, + index: DeclarationIndex, + selection: _RenameSelection, + artifacts: _RenamedArtifacts, +) -> SDLTransformationResult: + if _rename_preservation_verified(source, request, index, selection, artifacts.target): + result = _successful_rename_result(request, source_digest, selection, artifacts) + else: + result = refused_rename( + source_digest=source_digest, + request=request, + diagnostic_code="artifact-transformation.preservation-failed", + message="The transformed candidate did not satisfy the identity-transport round-trip relation.", + passed_checks=( + "linked-artifacts-current", + "source-admitted", + "target-admitted", + "target-exact", + "target-injective", + "target-supported", + ), + affected_identities=(request.target_address,), + ) + return result + + +def rename_sdl_declaration( + source: SDLAuthoringArtifact, + request: RenameSDLDeclarationRequest, + *, + binding_documents: tuple[ExternalConceptBindingDocumentModel, ...] = (), +) -> SDLTransformationResult: + """Atomically rename one exact top-level SDL declaration and its references.""" + + if not isinstance(source, (Scenario, ExpandedScenario)) or not source.semantic_validated: + raise SDLParseError("SDL transformation requires a semantically admitted authoring scenario") + source_digest = canonical_sdl_digest(source) + index = build_declaration_index(source) + selection = _select_rename(source, request, source_digest, index) + if isinstance(selection, SDLTransformationResult): + result = selection + else: + artifacts = _build_renamed_artifacts( + source, + request, + source_digest, + selection, + binding_documents, + ) + if isinstance(artifacts, SDLTransformationResult): + result = artifacts + else: + result = _finish_rename(source, request, source_digest, index, selection, artifacts) + return result + + +__all__ = ["rename_sdl_declaration"] diff --git a/implementations/python/packages/raes/_transformation_support.py b/implementations/python/packages/raes/_transformation_support.py new file mode 100644 index 000000000..7ea7d2aad --- /dev/null +++ b/implementations/python/packages/raes/_transformation_support.py @@ -0,0 +1,264 @@ +"""Shared deterministic report and SDL-selection helpers.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from raes_contracts.canonical import canonical_json_digest +from raes_contracts.contracts import ( + ArtifactTransformationCheckModel, + ArtifactTransformationKind, + ArtifactTransformationLossKind, + ArtifactTransformationLossModel, + ArtifactTransformationPreservationModel, + ArtifactTransformationReportModel, + ArtifactTransformationStatus, + PreservationOutcome, + TransformationCheckOutcome, +) +from raes_contracts.diagnostics import Diagnostic, DiagnosticModel, Severity, diagnostic_model + +from ._declarations import Declaration, DeclarationIndex +from ._module_symbols import HASHMAP_SECTIONS +from ._transformation_types import ( + ArtifactTransformationPolicy, + RemoveSDLDeclarationRequest, + RenameSDLDeclarationRequest, + SDLAuthoringArtifact, + SDLTransformationResult, +) +from .canonical import SDLCanonicalDigest + +REPORT_DOMAIN = "artifact-transformation" +RENAME_PROFILE = "rename-sdl-declaration/v1" +REMOVE_PROFILE = "remove-sdl-declaration/v1" +IDENTITY_TRANSPORT_PROFILE = "sdl-declaration-identity-transport/v1" +EXPLICIT_LOSS_PROFILE = "explicit-loss-accounting/v1" +CANONICAL_IDENTITY_PROFILE = "canonical-artifact-identity" +PORTABLE_CANONICAL_PROFILE = "rfc8785-jcs-sha256/v1" +PORTABLE_CANONICALIZE_PROFILE = "canonicalize-portable-contract/v1" +SDL_CONTRACT_PROFILE = "sdl-authoring-input/v1" +DEFAULT_POLICY_PAYLOAD: dict[str, list[str]] = {"allowed_loss_kinds": []} + + +def diagnostic( + code: str, + message: str, + *, + address: str = "", + severity: Severity = Severity.ERROR, +) -> DiagnosticModel: + return diagnostic_model( + Diagnostic( + code=code, + domain=REPORT_DOMAIN, + address=address, + message=message, + severity=severity, + ) + ) + + +def check( + check_id: str, + outcome: TransformationCheckOutcome, + *diagnostic_codes: str, +) -> ArtifactTransformationCheckModel: + return ArtifactTransformationCheckModel( + check_id=check_id, + outcome=outcome, + diagnostic_codes=tuple(sorted(set(diagnostic_codes))), + ) + + +def default_policy_digest() -> str: + return canonical_json_digest(DEFAULT_POLICY_PAYLOAD) + + +def transformation_policy_digest(policy: ArtifactTransformationPolicy) -> str: + return canonical_json_digest({"allowed_loss_kinds": [item.value for item in policy.allowed_loss_kinds]}) + + +def rename_derivation_digest( + *, + source_digest: str, + policy_digest: str, + request: RenameSDLDeclarationRequest, +) -> str: + return canonical_json_digest( + { + "operation_profile": RENAME_PROFILE, + "policy_digest": policy_digest, + "request": { + "new_local_name": request.new_local_name, + "target_address": request.target_address, + }, + "source_digest": source_digest, + } + ) + + +def remove_derivation_digest( + *, + source_digest: str, + policy_digest: str, + request: RemoveSDLDeclarationRequest, +) -> str: + return canonical_json_digest( + { + "operation_profile": REMOVE_PROFILE, + "policy_digest": policy_digest, + "request": {"target_address": request.target_address}, + "source_digest": source_digest, + } + ) + + +def refused_rename( + *, + source_digest: SDLCanonicalDigest, + request: RenameSDLDeclarationRequest, + diagnostic_code: str, + message: str, + passed_checks: tuple[str, ...] = ("source-admitted",), + affected_identities: tuple[str, ...] = (), +) -> SDLTransformationResult: + policy_digest = default_policy_digest() + failed_check = { + "artifact-transformation.target-not-exact": "target-exact", + "artifact-transformation.target-unsupported": "target-supported", + "artifact-transformation.identity-collision": "target-injective", + "artifact-transformation.linked-artifact-stale": "linked-artifacts-current", + "artifact-transformation.target-invalid": "target-admitted", + "artifact-transformation.preservation-failed": "round-trip-canonical-identity", + }.get(diagnostic_code, "target-admitted") + checks = [check(check_id, TransformationCheckOutcome.PASSED) for check_id in passed_checks] + checks.append(check(failed_check, TransformationCheckOutcome.FAILED, diagnostic_code)) + report = ArtifactTransformationReportModel( + operation_profile=RENAME_PROFILE, + status=ArtifactTransformationStatus.REFUSED, + artifact_kind=ArtifactTransformationKind.SDL_AUTHORING, + source_profile=SDL_CONTRACT_PROFILE, + target_profile=SDL_CONTRACT_PROFILE, + canonicalization_profile=source_digest.profile, + source_digest=source_digest.value, + policy_digest=policy_digest, + derivation_digest=rename_derivation_digest( + source_digest=source_digest.value, + policy_digest=policy_digest, + request=request, + ), + preconditions=tuple(sorted(checks, key=lambda item: item.check_id)), + affected_identities=tuple(sorted(set(affected_identities))), + preservation=ArtifactTransformationPreservationModel( + profile=IDENTITY_TRANSPORT_PROFILE, + outcome=PreservationOutcome.FAILED, + limitations=("Finite verification does not establish behavioral equivalence.",), + ), + diagnostics=(diagnostic(diagnostic_code, message, address="/request/target_address"),), + ) + return SDLTransformationResult(output=None, binding_documents=(), report=report) + + +def top_level_declaration( + scenario: SDLAuthoringArtifact, + index: DeclarationIndex, + address: str, +) -> tuple[Declaration, str, str] | None: + declaration = index.declaration_for(address) + result: tuple[Declaration, str, str] | None = None + if declaration is not None and "." in address: + section, local_key = address.split(".", 1) + section_value = getattr(scenario, section, None) + if ( + section in HASHMAP_SECTIONS + and isinstance(section_value, Mapping) + and local_key in section_value + and declaration.model_path == f"{section}.{local_key}" + ): + result = (declaration, section, local_key) + return result + + +def removal_loss( + target_address: str, + *, + severity: Severity, +) -> ArtifactTransformationLossModel: + return ArtifactTransformationLossModel( + kind=ArtifactTransformationLossKind.DECLARATION_REMOVED, + affected_identity=target_address, + diagnostic=diagnostic( + "artifact-transformation.declaration-removed", + "The selected declaration is absent from the transformed artifact.", + severity=severity, + ), + ) + + +def refused_removal( + *, + source_digest: SDLCanonicalDigest, + request: RemoveSDLDeclarationRequest, + policy: ArtifactTransformationPolicy, + diagnostic_code: str, + message: str, + passed_checks: tuple[str, ...] = ("source-admitted",), + include_loss: bool = False, +) -> SDLTransformationResult: + policy_digest = transformation_policy_digest(policy) + failed_check = { + "artifact-transformation.target-not-exact": "target-exact", + "artifact-transformation.loss-not-authorized": "loss-authorized", + "artifact-transformation.target-invalid": "target-admitted", + "artifact-transformation.linked-artifact-unsupported": "linked-artifacts-supported", + }.get(diagnostic_code, "target-admitted") + checks = [check(check_id, TransformationCheckOutcome.PASSED) for check_id in passed_checks] + checks.append(check(failed_check, TransformationCheckOutcome.FAILED, diagnostic_code)) + report = ArtifactTransformationReportModel( + operation_profile=REMOVE_PROFILE, + status=ArtifactTransformationStatus.REFUSED, + artifact_kind=ArtifactTransformationKind.SDL_AUTHORING, + source_profile=SDL_CONTRACT_PROFILE, + target_profile=SDL_CONTRACT_PROFILE, + canonicalization_profile=source_digest.profile, + source_digest=source_digest.value, + policy_digest=policy_digest, + derivation_digest=remove_derivation_digest( + source_digest=source_digest.value, + policy_digest=policy_digest, + request=request, + ), + preconditions=tuple(sorted(checks, key=lambda item: item.check_id)), + affected_identities=(request.target_address,), + preservation=ArtifactTransformationPreservationModel( + profile=EXPLICIT_LOSS_PROFILE, + outcome=PreservationOutcome.NOT_APPLICABLE, + limitations=("Removal does not preserve the selected declaration.",), + ), + losses=((removal_loss(request.target_address, severity=Severity.ERROR),) if include_loss else ()), + diagnostics=(diagnostic(diagnostic_code, message, address="/request/target_address"),), + ) + return SDLTransformationResult(output=None, binding_documents=(), report=report) + + +__all__ = [ + "CANONICAL_IDENTITY_PROFILE", + "DEFAULT_POLICY_PAYLOAD", + "EXPLICIT_LOSS_PROFILE", + "IDENTITY_TRANSPORT_PROFILE", + "PORTABLE_CANONICAL_PROFILE", + "PORTABLE_CANONICALIZE_PROFILE", + "REMOVE_PROFILE", + "RENAME_PROFILE", + "SDL_CONTRACT_PROFILE", + "check", + "default_policy_digest", + "refused_removal", + "refused_rename", + "removal_loss", + "remove_derivation_digest", + "rename_derivation_digest", + "top_level_declaration", + "transformation_policy_digest", +] diff --git a/implementations/python/packages/raes/_transformation_types.py b/implementations/python/packages/raes/_transformation_types.py new file mode 100644 index 000000000..5fcfe5731 --- /dev/null +++ b/implementations/python/packages/raes/_transformation_types.py @@ -0,0 +1,103 @@ +"""Typed requests and all-or-none results for RAES transformations.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from raes_contracts.contracts import ( + ArtifactTransformationKind, + ArtifactTransformationLossKind, + ArtifactTransformationReportModel, + ArtifactTransformationStatus, + ExternalConceptBindingDocumentModel, +) +from raes_contracts.contracts.base import ContractModel + +from ._identifiers import require_portable_identifier +from .scenario import ExpandedScenario, Scenario + +SDLAuthoringArtifact = Scenario | ExpandedScenario + + +@dataclass(frozen=True, slots=True) +class RenameSDLDeclarationRequest: + """Exact rename request; aliases and inferred targets are not accepted.""" + + target_address: str + new_local_name: str + + def __post_init__(self) -> None: + if not isinstance(self.target_address, str) or not self.target_address or len(self.target_address) > 4096: + raise ValueError("target_address must be a non-empty bounded canonical address") + require_portable_identifier(self.new_local_name, field_name="new_local_name") + + +@dataclass(frozen=True, slots=True) +class RemoveSDLDeclarationRequest: + """Exact declaration-removal request.""" + + target_address: str + + def __post_init__(self) -> None: + if not isinstance(self.target_address, str) or not self.target_address or len(self.target_address) > 4096: + raise ValueError("target_address must be a non-empty bounded canonical address") + + +@dataclass(frozen=True, slots=True) +class ArtifactTransformationPolicy: + """Closed authorization for specifically named transformation losses.""" + + allowed_loss_kinds: tuple[ArtifactTransformationLossKind, ...] = () + + def __post_init__(self) -> None: + if not isinstance(self.allowed_loss_kinds, tuple) or any( + not isinstance(item, ArtifactTransformationLossKind) for item in self.allowed_loss_kinds + ): + raise TypeError("allowed_loss_kinds must be a tuple of ArtifactTransformationLossKind values") + expected = tuple(sorted(set(self.allowed_loss_kinds), key=lambda item: item.value)) + if self.allowed_loss_kinds != expected: + raise ValueError("allowed_loss_kinds must be sorted and unique") + + +@dataclass(frozen=True, slots=True) +class SDLTransformationResult: + """All-or-none SDL result with explicitly transformed linked artifacts.""" + + output: SDLAuthoringArtifact | None + binding_documents: tuple[ExternalConceptBindingDocumentModel, ...] + report: ArtifactTransformationReportModel + + @property + def succeeded(self) -> bool: + return self.output is not None and self.report.status == ArtifactTransformationStatus.SUCCESS + + +@dataclass(frozen=True, slots=True) +class PortableContractTransformationResult: + """Isolated admitted portable-contract result and canonical report.""" + + output: ContractModel + report: ArtifactTransformationReportModel + + +@dataclass(frozen=True, slots=True) +class CanonicalArtifactComparison: + """Exact comparison under one owning canonicalization profile.""" + + artifact_kind: ArtifactTransformationKind + canonicalization_profile: str + relation_profile: str + left_digest: str + right_digest: str + equivalent: bool + + +__all__ = [ + "ArtifactTransformationPolicy", + "CanonicalArtifactComparison", + "PortableContractTransformationResult", + "RemoveSDLDeclarationRequest", + "RenameSDLDeclarationRequest", + "SDLAuthoringArtifact", + "SDLTransformationResult", +] diff --git a/implementations/python/packages/raes/composition.py b/implementations/python/packages/raes/composition.py index b6ca3a8fe..8f7801f0b 100644 --- a/implementations/python/packages/raes/composition.py +++ b/implementations/python/packages/raes/composition.py @@ -558,55 +558,62 @@ def _rewrite_variable_tokens(value: object, variables: Mapping[str, str]) -> obj return rewritten -def _namespace_payload( +def _behavior_reference_maps( payload: dict[str, Any], - imported: ScenarioContent, + symbols: dict[str, dict[str, str] | set[str]], namespace: str, - descriptor: ModuleDescriptor, -) -> dict[str, Any]: - namespaced = dict(payload) - _validate_descriptor_exports(imported, descriptor) - symbols = _symbol_index( - imported, - namespace=namespace, - descriptor=descriptor, - restrict_to_descriptor=True, - ) - tool_affordance_ref_map: dict[str, str] = {} - participant_inject_delivery_ref_map: dict[str, str] = {} - for spec_name, behavior_spec in namespaced.get("behavior_specifications", {}).items(): +) -> tuple[dict[str, str], dict[str, str]]: + tool_affordances: dict[str, str] = {} + inject_deliveries: dict[str, str] = {} + for spec_name, behavior_spec in payload.get("behavior_specifications", {}).items(): if not isinstance(behavior_spec, dict): continue - namespaced_spec_name = symbols["behavior_specifications"].get( + namespaced_name = symbols["behavior_specifications"].get( spec_name, _prefix(namespace, spec_name), ) for affordance_id in behavior_spec.get("tool_affordances", {}): - tool_affordance_ref_map[tool_affordance_reference(spec_name, affordance_id)] = tool_affordance_reference( - namespaced_spec_name, affordance_id + tool_affordances[tool_affordance_reference(spec_name, affordance_id)] = tool_affordance_reference( + namespaced_name, + affordance_id, ) for binding_id in behavior_spec.get("participant_inject_deliveries", {}): - participant_inject_delivery_ref_map[participant_inject_delivery_reference(spec_name, binding_id)] = ( - participant_inject_delivery_reference(namespaced_spec_name, binding_id) + inject_deliveries[participant_inject_delivery_reference(spec_name, binding_id)] = ( + participant_inject_delivery_reference(namespaced_name, binding_id) ) named_symbols = symbols["named"] if isinstance(named_symbols, dict): - named_symbols.update(tool_affordance_ref_map) - named_symbols.update(participant_inject_delivery_ref_map) + named_symbols.update(tool_affordances) + named_symbols.update(inject_deliveries) + return tool_affordances, inject_deliveries + - for node in namespaced.get("nodes", {}).values(): +def _rewrite_foundational_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for node in payload.get("nodes", {}).values(): if isinstance(node, dict): _rewrite_node(node, symbols) - for infra in namespaced.get("infrastructure", {}).values(): - if isinstance(infra, dict): - _rewrite_infrastructure(infra, symbols) - for feature in namespaced.get("features", {}).values(): + for infrastructure in payload.get("infrastructure", {}).values(): + if isinstance(infrastructure, dict): + _rewrite_infrastructure(infrastructure, symbols) + for feature in payload.get("features", {}).values(): if isinstance(feature, dict): _rewrite_feature(feature, symbols) - for condition in namespaced.get("conditions", {}).values(): + for entity in payload.get("entities", {}).values(): + if isinstance(entity, dict): + _rewrite_entity(entity, symbols) + + +def _rewrite_proposition_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for condition in payload.get("conditions", {}).values(): if isinstance(condition, dict) and condition.get("proposition"): condition["proposition"] = _maybe_rename(str(condition["proposition"]), symbols["propositions"]) - for proposition in namespaced.get("propositions", {}).values(): + for proposition in payload.get("propositions", {}).values(): if isinstance(proposition, dict): proposition["subjects"] = [ _maybe_rename(name, symbols["named"]) for name in proposition.get("subjects", []) @@ -615,318 +622,480 @@ def _namespace_payload( _maybe_rename(name, symbols["evidence_requirements"]) for name in proposition.get("evidence_requirements", []) ] - for assertion in namespaced.get("assertions", {}).values(): + for assertion in payload.get("assertions", {}).values(): if isinstance(assertion, dict) and assertion.get("proposition"): assertion["proposition"] = _maybe_rename(str(assertion["proposition"]), symbols["propositions"]) - for entity in namespaced.get("entities", {}).values(): - if isinstance(entity, dict): - _rewrite_entity(entity, symbols) - for inject in namespaced.get("injects", {}).values(): + + +def _rewrite_narrative_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for inject in payload.get("injects", {}).values(): if isinstance(inject, dict): if inject.get("from_entity"): inject["from_entity"] = _maybe_rename(str(inject["from_entity"]), symbols["entities"]) inject["to_entities"] = [_maybe_rename(name, symbols["entities"]) for name in inject.get("to_entities", [])] - for event in namespaced.get("events", {}).values(): + for event in payload.get("events", {}).values(): if isinstance(event, dict): event["assertions"] = [_maybe_rename(name, symbols["assertions"]) for name in event.get("assertions", [])] event["injects"] = [_maybe_rename(name, symbols["injects"]) for name in event.get("injects", [])] - for script in namespaced.get("scripts", {}).values(): + for script in payload.get("scripts", {}).values(): if isinstance(script, dict): script["events"] = { _maybe_rename(name, symbols["events"]): value for name, value in script.get("events", {}).items() } - for story in namespaced.get("stories", {}).values(): + for story in payload.get("stories", {}).values(): if isinstance(story, dict): story["scripts"] = [_maybe_rename(name, symbols["scripts"]) for name in story.get("scripts", [])] - for boundary in namespaced.get("observation_boundaries", {}).values(): + + +def _rewrite_observation_boundaries( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], + tool_affordance_refs: Mapping[str, str], +) -> None: + for boundary in payload.get("observation_boundaries", {}).values(): if not isinstance(boundary, dict): continue for field_name in ("observable_refs", "hidden_refs", "evidence_refs"): boundary[field_name] = [ - tool_affordance_ref_map.get(ref, _maybe_rename(ref, symbols["named"])) + tool_affordance_refs.get(ref, _maybe_rename(ref, symbols["named"])) for ref in boundary.get(field_name, []) ] for field_name in ("view_rules", "view_transitions"): for item in boundary.get(field_name, []): - if not isinstance(item, dict): - continue - information_ref = item.get("information_ref") - if isinstance(information_ref, str): - item["information_ref"] = tool_affordance_ref_map.get( + if isinstance(item, dict) and isinstance(item.get("information_ref"), str): + information_ref = item["information_ref"] + item["information_ref"] = tool_affordance_refs.get( information_ref, _maybe_rename(information_ref, symbols["named"]), ) - for content in namespaced.get("content", {}).values(): + + +def _rewrite_service_materialization( + materialization: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + if materialization.get("target_service_ref"): + materialization["target_service_ref"] = _rewrite_node_or_service_ref( + str(materialization["target_service_ref"]), + symbols["nodes"], + ) + if materialization.get("shared_service_relationship_ref"): + materialization["shared_service_relationship_ref"] = _rewrite_section_ref( + str(materialization["shared_service_relationship_ref"]), + "relationships", + symbols["relationships"], + ) + for field_name, section_name in ( + ("ordering_content_refs", "content"), + ("readback_assertion_refs", "assertions"), + ("evidence_requirement_refs", "evidence_requirements"), + ("observation_boundary_refs", "observation_boundaries"), + ): + materialization[field_name] = [ + _rewrite_section_ref(reference, section_name, symbols[section_name]) + for reference in materialization.get(field_name, []) + ] + + +def _rewrite_content_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for content in payload.get("content", {}).values(): if not isinstance(content, dict): continue if content.get("target"): content["target"] = _rewrite_section_ref(str(content["target"]), "nodes", symbols["nodes"]) materialization = content.get("service_materialization") - if not isinstance(materialization, dict): - continue - if materialization.get("target_service_ref"): - materialization["target_service_ref"] = _rewrite_node_or_service_ref( - str(materialization["target_service_ref"]), - symbols["nodes"], - ) - if materialization.get("shared_service_relationship_ref"): - materialization["shared_service_relationship_ref"] = _rewrite_section_ref( - str(materialization["shared_service_relationship_ref"]), - "relationships", - symbols["relationships"], - ) - materialization["ordering_content_refs"] = [ - _rewrite_section_ref(ref, "content", symbols["content"]) - for ref in materialization.get("ordering_content_refs", []) - ] - materialization["readback_assertion_refs"] = [ - _rewrite_section_ref(ref, "assertions", symbols["assertions"]) - for ref in materialization.get("readback_assertion_refs", []) - ] - materialization["evidence_requirement_refs"] = [ - _rewrite_section_ref(ref, "evidence_requirements", symbols["evidence_requirements"]) - for ref in materialization.get("evidence_requirement_refs", []) - ] - materialization["observation_boundary_refs"] = [ - _rewrite_section_ref(ref, "observation_boundaries", symbols["observation_boundaries"]) - for ref in materialization.get("observation_boundary_refs", []) + if isinstance(materialization, dict): + _rewrite_service_materialization(materialization, symbols) + + +def _rewrite_resource_consumers( + resource: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for consumer in resource.get("consumers", []): + if isinstance(consumer, dict) and consumer.get("node"): + consumer["node"] = _maybe_rename(str(consumer["node"]), symbols["nodes"]) + + +def _rewrite_resource_dependencies( + resource: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], + *, + owner: str, +) -> None: + for dependency_field in ("ordering_dependencies", "refresh_dependencies"): + resource[dependency_field] = [ + _rewrite_stateful_dependency_ref(reference, symbols, owner=owner) + for reference in resource.get(dependency_field, []) ] + + +def _rewrite_stateful_resource( + resource: object, + symbols: dict[str, dict[str, str] | set[str]], + *, + owner: str, +) -> None: + if isinstance(resource, dict): + _rewrite_resource_consumers(resource, symbols) + _rewrite_resource_dependencies(resource, symbols, owner=owner) + + +def _rewrite_stateful_resources( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: for section_name in ("generated_artifacts", "persistent_volumes"): - for resource_name, resource in namespaced.get(section_name, {}).items(): - if not isinstance(resource, dict): - continue - for consumer in resource.get("consumers", []): - if isinstance(consumer, dict) and consumer.get("node"): - consumer["node"] = _maybe_rename(str(consumer["node"]), symbols["nodes"]) - for dependency_field in ( - "ordering_dependencies", - "refresh_dependencies", - ): - resource[dependency_field] = [ - _rewrite_stateful_dependency_ref( - reference, - symbols, - owner=f"{section_name}.{resource_name}", - ) - for reference in resource.get(dependency_field, []) - ] - for account in namespaced.get("accounts", {}).values(): - if isinstance(account, dict): - if account.get("node"): - account["node"] = _maybe_rename(str(account["node"]), symbols["nodes"]) - if account.get("domain_ref"): - account["domain_ref"] = _maybe_rename( - str(account["domain_ref"]), - symbols["identity_domains"], - ) - for identity_domain in namespaced.get("identity_domains", {}).values(): - if isinstance(identity_domain, dict) and identity_domain.get("authority_account_ref"): - identity_domain["authority_account_ref"] = _maybe_rename( - str(identity_domain["authority_account_ref"]), - symbols["accounts"], - ) - for identity_forest in namespaced.get("identity_forests", {}).values(): - if not isinstance(identity_forest, dict): + for resource_name, resource in payload.get(section_name, {}).items(): + _rewrite_stateful_resource(resource, symbols, owner=f"{section_name}.{resource_name}") + + +def _rewrite_account( + account: object, + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + if not isinstance(account, dict): + return + if account.get("node"): + account["node"] = _maybe_rename(str(account["node"]), symbols["nodes"]) + if account.get("domain_ref"): + account["domain_ref"] = _maybe_rename(str(account["domain_ref"]), symbols["identity_domains"]) + + +def _rewrite_identity_domain( + domain: object, + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + if isinstance(domain, dict) and domain.get("authority_account_ref"): + domain["authority_account_ref"] = _maybe_rename( + str(domain["authority_account_ref"]), + symbols["accounts"], + ) + + +def _rewrite_identity_forest( + forest: object, + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + if not isinstance(forest, dict): + return + if forest.get("root_domain_ref"): + forest["root_domain_ref"] = _maybe_rename( + str(forest["root_domain_ref"]), + symbols["identity_domains"], + ) + forest["domain_refs"] = [_maybe_rename(name, symbols["identity_domains"]) for name in forest.get("domain_refs", [])] + + +def _rewrite_account_and_domain_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for account in payload.get("accounts", {}).values(): + _rewrite_account(account, symbols) + for domain in payload.get("identity_domains", {}).values(): + _rewrite_identity_domain(domain, symbols) + for forest in payload.get("identity_forests", {}).values(): + _rewrite_identity_forest(forest, symbols) + + +def _rewrite_deployment_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for facade in payload.get("identity_facades", {}).values(): + if isinstance(facade, dict) and facade.get("service_ref"): + facade["service_ref"] = _maybe_rename(str(facade["service_ref"]), symbols["named"]) + for cell in payload.get("deployment_cells", {}).values(): + if not isinstance(cell, dict): continue - if identity_forest.get("root_domain_ref"): - identity_forest["root_domain_ref"] = _maybe_rename( - str(identity_forest["root_domain_ref"]), - symbols["identity_domains"], - ) - identity_forest["domain_refs"] = [ - _maybe_rename(name, symbols["identity_domains"]) for name in identity_forest.get("domain_refs", []) - ] - for identity_facade in namespaced.get("identity_facades", {}).values(): - if isinstance(identity_facade, dict) and identity_facade.get("service_ref"): - identity_facade["service_ref"] = _maybe_rename( - str(identity_facade["service_ref"]), - symbols["named"], + if cell.get("tenant_ref"): + cell["tenant_ref"] = _maybe_rename(str(cell["tenant_ref"]), symbols["deployment_tenants"]) + cell["node_refs"] = [_maybe_rename(name, symbols["nodes"]) for name in cell.get("node_refs", [])] + + +def _rewrite_relationship_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for relationship in payload.get("relationships", {}).values(): + if not isinstance(relationship, dict): + continue + if relationship.get("source"): + relationship["source"] = _maybe_rename(str(relationship["source"]), symbols["named"]) + if relationship.get("target"): + relationship["target"] = _maybe_rename(str(relationship["target"]), symbols["named"]) + domain_join = relationship.get("domain_join") + if isinstance(domain_join, dict): + domain_join["controller_refs"] = [ + _maybe_rename(name, symbols["nodes"]) for name in domain_join.get("controller_refs", []) + ] + shared_service = relationship.get("shared_service") + if isinstance(shared_service, dict): + shared_service["mutable_state_refs"] = [ + _maybe_rename(name, symbols["persistent_volumes"]) + for name in shared_service.get("mutable_state_refs", []) + ] + forwarding_edge = relationship.get("forwarding_edge") + if isinstance(forwarding_edge, dict) and forwarding_edge.get("forwarder_ref"): + forwarding_edge["forwarder_ref"] = _maybe_rename( + str(forwarding_edge["forwarder_ref"]), + symbols[FORWARDING_AGENTS_SECTION], ) - for deployment_cell in namespaced.get("deployment_cells", {}).values(): - if not isinstance(deployment_cell, dict): + + +def _rewrite_agent_access( + agent: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for access in agent.get("interactive_access", {}).values(): + if not isinstance(access, dict): continue - if deployment_cell.get("tenant_ref"): - deployment_cell["tenant_ref"] = _maybe_rename( - str(deployment_cell["tenant_ref"]), - symbols["deployment_tenants"], + if access.get("target_ref"): + access["target_ref"] = _rewrite_section_ref(str(access["target_ref"]), "nodes", symbols["nodes"]) + if access.get("account_ref"): + access["account_ref"] = _rewrite_section_ref( + str(access["account_ref"]), + "accounts", + symbols["accounts"], ) - deployment_cell["node_refs"] = [ - _maybe_rename(name, symbols["nodes"]) for name in deployment_cell.get("node_refs", []) - ] - for relationship in namespaced.get("relationships", {}).values(): - if isinstance(relationship, dict): - if relationship.get("source"): - relationship["source"] = _maybe_rename(str(relationship["source"]), symbols["named"]) - if relationship.get("target"): - relationship["target"] = _maybe_rename(str(relationship["target"]), symbols["named"]) - domain_join = relationship.get("domain_join") - if isinstance(domain_join, dict): - domain_join["controller_refs"] = [ - _maybe_rename(name, symbols["nodes"]) for name in domain_join.get("controller_refs", []) - ] - shared_service = relationship.get("shared_service") - if isinstance(shared_service, dict): - shared_service["mutable_state_refs"] = [ - _maybe_rename(name, symbols["persistent_volumes"]) - for name in shared_service.get("mutable_state_refs", []) - ] - forwarding_edge = relationship.get("forwarding_edge") - if isinstance(forwarding_edge, dict) and forwarding_edge.get("forwarder_ref"): - forwarding_edge["forwarder_ref"] = _maybe_rename( - str(forwarding_edge["forwarder_ref"]), - symbols[FORWARDING_AGENTS_SECTION], - ) - for agent in namespaced.get("agents", {}).values(): + + +def _rewrite_agent_knowledge( + agent: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + knowledge = agent.get("initial_knowledge") + if not isinstance(knowledge, dict): + return + for field_name, symbol_key in ( + ("hosts", "nodes"), + ("subnets", "infrastructure"), + ("accounts", "accounts"), + ): + knowledge[field_name] = [_maybe_rename(name, symbols[symbol_key]) for name in knowledge.get(field_name, [])] + + +def _rewrite_agent( + agent: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + if agent.get("entity"): + agent["entity"] = _maybe_rename(str(agent["entity"]), symbols["entities"]) + for field_name, symbol_key in ( + ("starting_accounts", "accounts"), + ("actions", "action_contracts"), + ("observation_boundaries", "observation_boundaries"), + ("allowed_subnets", "infrastructure"), + ("starting_assertions", "assertions"), + ("authority_anchors", "named"), + ("operating_scope", "named"), + ): + agent[field_name] = [_maybe_rename(name, symbols[symbol_key]) for name in agent.get(field_name, [])] + _rewrite_agent_access(agent, symbols) + _rewrite_agent_knowledge(agent, symbols) + + +def _rewrite_agent_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for agent in payload.get("agents", {}).values(): if isinstance(agent, dict): - if agent.get("entity"): - agent["entity"] = _maybe_rename(str(agent["entity"]), symbols["entities"]) - agent["starting_accounts"] = [ - _maybe_rename(name, symbols["accounts"]) for name in agent.get("starting_accounts", []) - ] - agent["actions"] = [_maybe_rename(name, symbols["action_contracts"]) for name in agent.get("actions", [])] - agent["observation_boundaries"] = [ - _maybe_rename(name, symbols["observation_boundaries"]) - for name in agent.get("observation_boundaries", []) - ] - for access in agent.get("interactive_access", {}).values(): - if not isinstance(access, dict): - continue - if access.get("target_ref"): - access["target_ref"] = _rewrite_section_ref( - str(access["target_ref"]), - "nodes", - symbols["nodes"], - ) - if access.get("account_ref"): - access["account_ref"] = _rewrite_section_ref( - str(access["account_ref"]), - "accounts", - symbols["accounts"], - ) - knowledge = agent.get("initial_knowledge") - if isinstance(knowledge, dict): - knowledge["hosts"] = [_maybe_rename(name, symbols["nodes"]) for name in knowledge.get("hosts", [])] - knowledge["subnets"] = [ - _maybe_rename(name, symbols["infrastructure"]) for name in knowledge.get("subnets", []) - ] - knowledge["accounts"] = [ - _maybe_rename(name, symbols["accounts"]) for name in knowledge.get("accounts", []) - ] - agent["allowed_subnets"] = [ - _maybe_rename(name, symbols["infrastructure"]) for name in agent.get("allowed_subnets", []) - ] - # ADR-020 §6 accepts bare or section-qualified condition refs. - # symbols["named"] carries both forms after the symbol-index - # update, so a single rename handles `health` and - # `conditions.health` symmetrically. - agent["starting_assertions"] = [ - _maybe_rename(name, symbols["assertions"]) for name in agent.get("starting_assertions", []) - ] - agent["authority_anchors"] = [ - _maybe_rename(name, symbols["named"]) for name in agent.get("authority_anchors", []) - ] - agent["operating_scope"] = [ - _maybe_rename(name, symbols["named"]) for name in agent.get("operating_scope", []) - ] - for behavior_spec in namespaced.get("behavior_specifications", {}).values(): + _rewrite_agent(agent, symbols) + + +def _rewrite_behavior_specification( + behavior_spec: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for field_name, symbol_key in ( + ("participant_refs", "agents"), + ("action_contract_refs", "action_contracts"), + ("observation_boundary_refs", "observation_boundaries"), + ("outcome_interpretation_rule_refs", "outcome_interpretation_rules"), + ("authority_scope_refs", "named"), + ): + behavior_spec[field_name] = [ + _maybe_rename(name, symbols[symbol_key]) for name in behavior_spec.get(field_name, []) + ] + autonomous_execution = behavior_spec.get("autonomous_execution") + if isinstance(autonomous_execution, dict): + _rewrite_participant_resource_budget(autonomous_execution.get("resource_budget"), symbols) + _rewrite_mixed_control(behavior_spec.get("mixed_control"), symbols) + for binding in behavior_spec.get("tool_affordances", {}).values(): + if isinstance(binding, dict): + _rewrite_tool_affordance(binding, symbols) + for binding in behavior_spec.get("participant_inject_deliveries", {}).values(): + if isinstance(binding, dict): + _rewrite_participant_inject_delivery(binding, symbols) + + +def _rewrite_behavior_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for behavior_spec in payload.get("behavior_specifications", {}).values(): if isinstance(behavior_spec, dict): - behavior_spec["participant_refs"] = [ - _maybe_rename(name, symbols["agents"]) for name in behavior_spec.get("participant_refs", []) - ] - behavior_spec["action_contract_refs"] = [ - _maybe_rename(name, symbols["action_contracts"]) - for name in behavior_spec.get("action_contract_refs", []) - ] - behavior_spec["observation_boundary_refs"] = [ - _maybe_rename(name, symbols["observation_boundaries"]) - for name in behavior_spec.get("observation_boundary_refs", []) - ] - behavior_spec["outcome_interpretation_rule_refs"] = [ - _maybe_rename(name, symbols["outcome_interpretation_rules"]) - for name in behavior_spec.get("outcome_interpretation_rule_refs", []) - ] - behavior_spec["authority_scope_refs"] = [ - _maybe_rename(name, symbols["named"]) for name in behavior_spec.get("authority_scope_refs", []) - ] - autonomous_execution = behavior_spec.get("autonomous_execution") - if isinstance(autonomous_execution, dict): - _rewrite_participant_resource_budget( - autonomous_execution.get("resource_budget"), - symbols, - ) - _rewrite_mixed_control(behavior_spec.get("mixed_control"), symbols) - for binding in behavior_spec.get("tool_affordances", {}).values(): - if isinstance(binding, dict): - _rewrite_tool_affordance(binding, symbols) - for binding in behavior_spec.get("participant_inject_deliveries", {}).values(): - if isinstance(binding, dict): - _rewrite_participant_inject_delivery(binding, symbols) - for requirement in namespaced.get("evidence_requirements", {}).values(): + _rewrite_behavior_specification(behavior_spec, symbols) + for requirement in payload.get("evidence_requirements", {}).values(): if isinstance(requirement, dict): _rewrite_evidence_requirement(requirement, symbols) - _rewrite_time_model(namespaced, symbols) - for objective in namespaced.get("objectives", {}).values(): - if not isinstance(objective, dict): - continue - if objective.get("agent"): - objective["agent"] = _maybe_rename(str(objective["agent"]), symbols["agents"]) - if objective.get("entity"): - objective["entity"] = _maybe_rename(str(objective["entity"]), symbols["entities"]) - objective["targets"] = [_maybe_rename(name, symbols["named"]) for name in objective.get("targets", [])] - objective["depends_on"] = [ - _maybe_rename(name, symbols["objectives"]) for name in objective.get("depends_on", []) - ] - success = objective.get("success") - if isinstance(success, dict): - success["assertions"] = [ - _maybe_rename(name, symbols["assertions"]) for name in success.get("assertions", []) - ] - window = objective.get("window") - if isinstance(window, dict): - for field_name, symbol_key in ( - ("stories", "stories"), - ("scripts", "scripts"), - ("events", "events"), - ("workflows", "workflows"), - ): - window[field_name] = [_maybe_rename(name, symbols[symbol_key]) for name in window.get(field_name, [])] - window["steps"] = [ - _rewrite_objective_window_ref(name, symbols["workflows"]) for name in window.get("steps", []) - ] - for workflow in namespaced.get("workflows", {}).values(): + + +def _rewrite_objective_window( + window: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + for field_name, symbol_key in ( + ("stories", "stories"), + ("scripts", "scripts"), + ("events", "events"), + ("workflows", "workflows"), + ): + window[field_name] = [_maybe_rename(name, symbols[symbol_key]) for name in window.get(field_name, [])] + window["steps"] = [_rewrite_objective_window_ref(name, symbols["workflows"]) for name in window.get("steps", [])] + + +def _rewrite_objective( + objective: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], +) -> None: + if objective.get("agent"): + objective["agent"] = _maybe_rename(str(objective["agent"]), symbols["agents"]) + if objective.get("entity"): + objective["entity"] = _maybe_rename(str(objective["entity"]), symbols["entities"]) + objective["targets"] = [_maybe_rename(name, symbols["named"]) for name in objective.get("targets", [])] + objective["depends_on"] = [_maybe_rename(name, symbols["objectives"]) for name in objective.get("depends_on", [])] + success = objective.get("success") + if isinstance(success, dict): + success["assertions"] = [_maybe_rename(name, symbols["assertions"]) for name in success.get("assertions", [])] + window = objective.get("window") + if isinstance(window, dict): + _rewrite_objective_window(window, symbols) + + +def _rewrite_terminal_sections( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], + tool_affordance_refs: Mapping[str, str], +) -> None: + _rewrite_time_model(payload, symbols) + for objective in payload.get("objectives", {}).values(): + if isinstance(objective, dict): + _rewrite_objective(objective, symbols) + for workflow in payload.get("workflows", {}).values(): if isinstance(workflow, dict): - _rewrite_workflow(workflow, symbols, tool_affordance_ref_map) - for variation_point in namespaced.get("variation_points", {}).values(): + _rewrite_workflow(workflow, symbols, tool_affordance_refs) + for variation_point in payload.get("variation_points", {}).values(): if isinstance(variation_point, dict): _rewrite_variation_point(variation_point, symbols) - namespaced = _rewrite_variable_tokens(namespaced, symbols["variables"]) +def _namespace_declaration_keys( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], + namespace: str, +) -> None: for section_name in _HASHMAP_SECTIONS: - section_payload = namespaced.get(section_name) - if not isinstance(section_payload, dict): - continue - namespaced[section_name] = { - symbols[section_name].get(name, _prefix(namespace, name)): value for name, value in section_payload.items() - } - forwarding_agents = namespaced.get(FORWARDING_AGENTS_SECTION, []) - if isinstance(forwarding_agents, list): - for agent in forwarding_agents: - if not isinstance(agent, dict): - continue - identifier = agent.get("forwarding_agent_id") - if isinstance(identifier, str): - agent["forwarding_agent_id"] = symbols[FORWARDING_AGENTS_SECTION].get( - identifier, - _private_prefix(namespace, identifier), - ) - namespaced.pop("module", None) - namespaced.pop("imports", None) - namespaced.pop("expansion_provenance", None) - namespaced.pop("instantiation_provenance", None) + section_payload = payload.get(section_name) + if isinstance(section_payload, dict): + payload[section_name] = { + symbols[section_name].get(name, _prefix(namespace, name)): value + for name, value in section_payload.items() + } + + +def _namespace_forwarding_agents( + payload: dict[str, Any], + symbols: dict[str, dict[str, str] | set[str]], + namespace: str, +) -> None: + forwarding_agents = payload.get(FORWARDING_AGENTS_SECTION, []) + if not isinstance(forwarding_agents, list): + return + for agent in forwarding_agents: + if isinstance(agent, dict) and isinstance(agent.get("forwarding_agent_id"), str): + identifier = agent["forwarding_agent_id"] + agent["forwarding_agent_id"] = symbols[FORWARDING_AGENTS_SECTION].get( + identifier, + _private_prefix(namespace, identifier), + ) + + +def _strip_composition_fields(payload: dict[str, Any]) -> None: + for field_name in ("module", "imports", "expansion_provenance", "instantiation_provenance"): + payload.pop(field_name, None) + + +def _rewrite_payload_with_symbols( + payload: dict[str, Any], + *, + symbols: dict[str, dict[str, str] | set[str]], + namespace: str = "", + strip_composition_fields: bool = False, +) -> dict[str, Any]: + """Rewrite declarations and references through one canonical symbol map. + + Module composition and semantic transformations share this implementation + so new reference-bearing SDL fields cannot drift between the two surfaces. + The caller supplies an isolated ``model_dump`` payload; this function never + receives or mutates a caller-owned scenario model. + """ + + namespaced = dict(payload) + tool_affordance_refs, _ = _behavior_reference_maps(namespaced, symbols, namespace) + _rewrite_foundational_sections(namespaced, symbols) + _rewrite_proposition_sections(namespaced, symbols) + _rewrite_narrative_sections(namespaced, symbols) + _rewrite_observation_boundaries(namespaced, symbols, tool_affordance_refs) + _rewrite_content_sections(namespaced, symbols) + _rewrite_stateful_resources(namespaced, symbols) + _rewrite_account_and_domain_sections(namespaced, symbols) + _rewrite_deployment_sections(namespaced, symbols) + _rewrite_relationship_sections(namespaced, symbols) + _rewrite_agent_sections(namespaced, symbols) + _rewrite_behavior_sections(namespaced, symbols) + _rewrite_terminal_sections(namespaced, symbols, tool_affordance_refs) + + rewritten = _rewrite_variable_tokens(namespaced, symbols["variables"]) + if not isinstance(rewritten, dict): + raise TypeError("variable rewriting returned a non-object payload") + namespaced = rewritten + _namespace_declaration_keys(namespaced, symbols, namespace) + _namespace_forwarding_agents(namespaced, symbols, namespace) + if strip_composition_fields: + _strip_composition_fields(namespaced) return namespaced +def _namespace_payload( + payload: dict[str, Any], + imported: ScenarioContent, + namespace: str, + descriptor: ModuleDescriptor, +) -> dict[str, Any]: + _validate_descriptor_exports(imported, descriptor) + symbols = _symbol_index( + imported, + namespace=namespace, + descriptor=descriptor, + restrict_to_descriptor=True, + ) + return _rewrite_payload_with_symbols( + payload, + symbols=symbols, + namespace=namespace, + strip_composition_fields=True, + ) + + def _merge_sections( root: dict[str, Any], incoming: dict[str, Any], diff --git a/implementations/python/packages/raes/runtime_forwarding_agent.py b/implementations/python/packages/raes/runtime_forwarding_agent.py index b7e092082..712ad4b17 100644 --- a/implementations/python/packages/raes/runtime_forwarding_agent.py +++ b/implementations/python/packages/raes/runtime_forwarding_agent.py @@ -30,6 +30,7 @@ from .runtime_forwarding_agent_vocab import ( RuntimeForwardingAgentImplementation, RuntimeForwardingAgentKind, + RuntimeForwardingAgentOwnershipRole, RuntimeForwardingBufferCrypto, RuntimeForwardingEnrollmentClassification, RuntimeForwardingParseFormat, @@ -52,6 +53,7 @@ "RuntimeForwardingAgent", "RuntimeForwardingAgentImplementation", "RuntimeForwardingAgentKind", + "RuntimeForwardingAgentOwnershipRole", "RuntimeForwardingBufferCrypto", "RuntimeForwardingBufferPolicy", "RuntimeForwardingEnrollmentClassification", @@ -294,6 +296,7 @@ class RuntimeForwardingAgent(SDLModel): forwarding_agent_id: str implementation: RuntimeForwardingAgentImplementation | str = RuntimeForwardingAgentImplementation.UNKNOWN agent_kind: RuntimeForwardingAgentKind | str = RuntimeForwardingAgentKind.UNKNOWN + ownership_role: RuntimeForwardingAgentOwnershipRole | str = RuntimeForwardingAgentOwnershipRole.SYSTEM_UNDER_TEST version: str = "" name: str = "" sources: list[RuntimeForwardingSource] = Field(default_factory=list) @@ -319,6 +322,11 @@ def normalize_implementation(cls, v: RuntimeForwardingAgentImplementation | str) def normalize_agent_kind(cls, v: RuntimeForwardingAgentKind | str) -> object: return parse_runtime_enum_or_var(v, RuntimeForwardingAgentKind, field_name="agent_kind") + @field_validator("ownership_role", mode="before") + @classmethod + def normalize_ownership_role(cls, v: RuntimeForwardingAgentOwnershipRole | str) -> object: + return parse_runtime_enum_or_var(v, RuntimeForwardingAgentOwnershipRole, field_name="ownership_role") + @model_validator(mode="after") def validate_forwarding_agent(self) -> "RuntimeForwardingAgent": self._reject_duplicate_local_ref_ids() diff --git a/implementations/python/packages/raes/runtime_forwarding_agent_vocab.py b/implementations/python/packages/raes/runtime_forwarding_agent_vocab.py index 3251d1bb3..7553fb20f 100644 --- a/implementations/python/packages/raes/runtime_forwarding_agent_vocab.py +++ b/implementations/python/packages/raes/runtime_forwarding_agent_vocab.py @@ -12,6 +12,7 @@ __all__ = [ "RuntimeForwardingAgentImplementation", "RuntimeForwardingAgentKind", + "RuntimeForwardingAgentOwnershipRole", "RuntimeForwardingBufferCrypto", "RuntimeForwardingEnrollmentClassification", "RuntimeForwardingParseFormat", @@ -50,6 +51,13 @@ class RuntimeForwardingAgentKind(str, Enum): OTHER = "other" +class RuntimeForwardingAgentOwnershipRole(str, Enum): + """Closed ownership of an agent relative to the system under test.""" + + SYSTEM_UNDER_TEST = "system_under_test" + MEASUREMENT_APPARATUS = "measurement_apparatus" + + class RuntimeForwardingSourceKind(str, Enum): """Portable provenance family for a forwarder input source (OPEN).""" diff --git a/implementations/python/packages/raes/runtime_security_monitoring/__init__.py b/implementations/python/packages/raes/runtime_security_monitoring/__init__.py new file mode 100644 index 000000000..a2c2a5034 --- /dev/null +++ b/implementations/python/packages/raes/runtime_security_monitoring/__init__.py @@ -0,0 +1,53 @@ +"""Security-monitoring manager runtime inventory models.""" + +from ..runtime_security_monitoring_definitions import ( + RuntimeSecurityMonitoringDetectionDefinition, + RuntimeSecurityMonitoringDetectionDefinitionKind, + RuntimeSecurityMonitoringDetectionEngine, + RuntimeSecurityMonitoringFieldPredicate, + RuntimeSecurityMonitoringFieldPredicateOperator, +) +from ._enums import ( + RuntimeSecurityMonitoringAgentStatus, + RuntimeSecurityMonitoringComponentKind, + RuntimeSecurityMonitoringComponentStatus, + RuntimeSecurityMonitoringContentFormat, + RuntimeSecurityMonitoringContentKind, + RuntimeSecurityMonitoringImplementation, + RuntimeSecurityMonitoringListenerRole, + RuntimeSecurityMonitoringManagerKind, + RuntimeSecurityMonitoringSettingProvenance, +) +from ._models import ( + RuntimeSecurityMonitoringAgent, + RuntimeSecurityMonitoringAgentGroup, + RuntimeSecurityMonitoringComponent, + RuntimeSecurityMonitoringContentSet, + RuntimeSecurityMonitoringListener, + RuntimeSecurityMonitoringManager, + RuntimeSecurityMonitoringSetting, +) + +__all__ = [ + "RuntimeSecurityMonitoringAgent", + "RuntimeSecurityMonitoringAgentGroup", + "RuntimeSecurityMonitoringAgentStatus", + "RuntimeSecurityMonitoringComponent", + "RuntimeSecurityMonitoringComponentKind", + "RuntimeSecurityMonitoringComponentStatus", + "RuntimeSecurityMonitoringContentFormat", + "RuntimeSecurityMonitoringContentKind", + "RuntimeSecurityMonitoringContentSet", + "RuntimeSecurityMonitoringDetectionDefinition", + "RuntimeSecurityMonitoringDetectionDefinitionKind", + "RuntimeSecurityMonitoringDetectionEngine", + "RuntimeSecurityMonitoringFieldPredicate", + "RuntimeSecurityMonitoringFieldPredicateOperator", + "RuntimeSecurityMonitoringImplementation", + "RuntimeSecurityMonitoringListener", + "RuntimeSecurityMonitoringListenerRole", + "RuntimeSecurityMonitoringManager", + "RuntimeSecurityMonitoringManagerKind", + "RuntimeSecurityMonitoringSetting", + "RuntimeSecurityMonitoringSettingProvenance", +] diff --git a/implementations/python/packages/raes/runtime_security_monitoring/_enums.py b/implementations/python/packages/raes/runtime_security_monitoring/_enums.py new file mode 100644 index 000000000..b485b4b88 --- /dev/null +++ b/implementations/python/packages/raes/runtime_security_monitoring/_enums.py @@ -0,0 +1,137 @@ +"""Portable enum taxonomies for security-monitoring manager runtime inventory.""" + +from enum import Enum + + +class RuntimeSecurityMonitoringImplementation(str, Enum): + """Product family for an observed security-monitoring manager.""" + + WAZUH = "wazuh" + OSSEC = "ossec" + ELASTIC_SECURITY = "elastic_security" + SPLUNK_ENTERPRISE_SECURITY = "splunk_enterprise_security" + SECURITY_ONION = "security_onion" + MICROSOFT_SENTINEL = "microsoft_sentinel" + UNKNOWN = "unknown" + OTHER = "other" + + +class RuntimeSecurityMonitoringManagerKind(str, Enum): + """Portable manager role/family.""" + + SIEM = "siem" + XDR = "xdr" + HIDS = "hids" + NDR = "ndr" + LOG_MANAGEMENT = "log_management" + DETECTION_ENGINE = "detection_engine" + SECURITY_MONITORING = "security_monitoring" + UNKNOWN = "unknown" + OTHER = "other" + + +class RuntimeSecurityMonitoringListenerRole(str, Enum): + """Logical role of a manager transport listener.""" + + AGENT_EVENT_INGESTION = "agent_event_ingestion" + AGENT_ENROLLMENT = "agent_enrollment" + SYSLOG_INGESTION = "syslog_ingestion" + API = "api" + ALERT_FORWARDING = "alert_forwarding" + INDEXER_FORWARDING = "indexer_forwarding" + DASHBOARD = "dashboard" + OTHER = "other" + UNKNOWN = "unknown" + + +class RuntimeSecurityMonitoringComponentKind(str, Enum): + """Portable component/module kind inside a security-monitoring manager.""" + + ANALYSIS_ENGINE = "analysis_engine" + AGENT_INGESTION = "agent_ingestion" + AGENT_ENROLLMENT = "agent_enrollment" + MODULE_SUPERVISOR = "module_supervisor" + LOG_COLLECTION = "log_collection" + ALERTING = "alerting" + API = "api" + CLUSTER = "cluster" + INDEXER_FORWARDER = "indexer_forwarder" + VULNERABILITY_DETECTION = "vulnerability_detection" + FILE_INTEGRITY_MONITORING = "file_integrity_monitoring" + ROOTKIT_DETECTION = "rootkit_detection" + SCA = "sca" + ACTIVE_RESPONSE = "active_response" + INTEGRATION = "integration" + DATABASE = "database" + OTHER = "other" + UNKNOWN = "unknown" + + +class RuntimeSecurityMonitoringComponentStatus(str, Enum): + """Observed component/module status.""" + + RUNNING = "running" + STOPPED = "stopped" + DISABLED = "disabled" + ENABLED = "enabled" + DEGRADED = "degraded" + FAILED = "failed" + UNKNOWN = "unknown" + OTHER = "other" + + +class RuntimeSecurityMonitoringAgentStatus(str, Enum): + """Observed enrolled-agent status.""" + + AVAILABLE = "available" + ACTIVE = "active" + DISCONNECTED = "disconnected" + NEVER_CONNECTED = "never_connected" + PENDING = "pending" + REMOVED = "removed" + UNKNOWN = "unknown" + OTHER = "other" + + +class RuntimeSecurityMonitoringContentKind(str, Enum): + """Kind of manager-owned detection or monitoring content.""" + + RULE_CORPUS = "rule_corpus" + DECODER_CORPUS = "decoder_corpus" + CORRELATION_RULES = "correlation_rules" + SCA_POLICIES = "sca_policies" + ACTIVE_RESPONSE = "active_response" + CDB_LIST = "cdb_list" + THREAT_INTEL = "threat_intel" + DASHBOARD = "dashboard" + OTHER = "other" + UNKNOWN = "unknown" + + +class RuntimeSecurityMonitoringContentFormat(str, Enum): + """Portable format family for manager-owned content.""" + + WAZUH_RULE_XML = "wazuh_rule_xml" + WAZUH_DECODER_XML = "wazuh_decoder_xml" + SIGMA = "sigma" + YARA = "yara" + STIX = "stix" + JSON = "json" + YAML = "yaml" + XML = "xml" + QUERY = "query" + UNKNOWN = "unknown" + OTHER = "other" + + +class RuntimeSecurityMonitoringSettingProvenance(str, Enum): + """Where an observed manager setting came from.""" + + INTROSPECTION = "introspection" + CONFIGURATION_FILE = "configuration_file" + API = "api" + IMAGE_DEFAULT = "image_default" + OPERATOR_OVERRIDE = "operator_override" + RUNTIME_DEFAULT = "runtime_default" + UNKNOWN = "unknown" + OTHER = "other" diff --git a/implementations/python/packages/raes/runtime_security_monitoring.py b/implementations/python/packages/raes/runtime_security_monitoring/_models.py similarity index 69% rename from implementations/python/packages/raes/runtime_security_monitoring.py rename to implementations/python/packages/raes/runtime_security_monitoring/_models.py index eac833cca..a1c9de500 100644 --- a/implementations/python/packages/raes/runtime_security_monitoring.py +++ b/implementations/python/packages/raes/runtime_security_monitoring/_models.py @@ -1,20 +1,13 @@ """Security-monitoring manager runtime inventory models.""" -from enum import Enum -from typing import Any - from pydantic import Field, ValidationInfo, field_validator, model_validator -from ._base import SDLModel, parse_int_or_var -from .runtime_filesystem import RuntimeSensitivityClassification -from .runtime_security_monitoring_definitions import ( +from .._base import SDLModel, parse_int_or_var +from ..runtime_filesystem import RuntimeSensitivityClassification +from ..runtime_security_monitoring_definitions import ( RuntimeSecurityMonitoringDetectionDefinition, - RuntimeSecurityMonitoringDetectionDefinitionKind, - RuntimeSecurityMonitoringDetectionEngine, - RuntimeSecurityMonitoringFieldPredicate, - RuntimeSecurityMonitoringFieldPredicateOperator, ) -from .runtime_values import ( +from ..runtime_values import ( absolute_path_or_var, coerce_string_list, enforce_observed_value_redaction, @@ -24,30 +17,17 @@ require_symbol, validate_absolute_paths, ) - -__all__ = [ - "RuntimeSecurityMonitoringAgent", - "RuntimeSecurityMonitoringAgentGroup", - "RuntimeSecurityMonitoringAgentStatus", - "RuntimeSecurityMonitoringComponent", - "RuntimeSecurityMonitoringComponentKind", - "RuntimeSecurityMonitoringComponentStatus", - "RuntimeSecurityMonitoringContentFormat", - "RuntimeSecurityMonitoringContentKind", - "RuntimeSecurityMonitoringContentSet", - "RuntimeSecurityMonitoringDetectionDefinition", - "RuntimeSecurityMonitoringDetectionDefinitionKind", - "RuntimeSecurityMonitoringDetectionEngine", - "RuntimeSecurityMonitoringFieldPredicate", - "RuntimeSecurityMonitoringFieldPredicateOperator", - "RuntimeSecurityMonitoringImplementation", - "RuntimeSecurityMonitoringListener", - "RuntimeSecurityMonitoringListenerRole", - "RuntimeSecurityMonitoringManager", - "RuntimeSecurityMonitoringManagerKind", - "RuntimeSecurityMonitoringSetting", - "RuntimeSecurityMonitoringSettingProvenance", -] +from ._enums import ( + RuntimeSecurityMonitoringAgentStatus, + RuntimeSecurityMonitoringComponentKind, + RuntimeSecurityMonitoringComponentStatus, + RuntimeSecurityMonitoringContentFormat, + RuntimeSecurityMonitoringContentKind, + RuntimeSecurityMonitoringImplementation, + RuntimeSecurityMonitoringListenerRole, + RuntimeSecurityMonitoringManagerKind, + RuntimeSecurityMonitoringSettingProvenance, +) _REDACTED_SENSITIVITIES = ( RuntimeSensitivityClassification.REDACTED, @@ -55,140 +35,6 @@ ) -class RuntimeSecurityMonitoringImplementation(str, Enum): - """Product family for an observed security-monitoring manager.""" - - WAZUH = "wazuh" - OSSEC = "ossec" - ELASTIC_SECURITY = "elastic_security" - SPLUNK_ENTERPRISE_SECURITY = "splunk_enterprise_security" - SECURITY_ONION = "security_onion" - MICROSOFT_SENTINEL = "microsoft_sentinel" - UNKNOWN = "unknown" - OTHER = "other" - - -class RuntimeSecurityMonitoringManagerKind(str, Enum): - """Portable manager role/family.""" - - SIEM = "siem" - XDR = "xdr" - HIDS = "hids" - NDR = "ndr" - LOG_MANAGEMENT = "log_management" - DETECTION_ENGINE = "detection_engine" - SECURITY_MONITORING = "security_monitoring" - UNKNOWN = "unknown" - OTHER = "other" - - -class RuntimeSecurityMonitoringListenerRole(str, Enum): - """Logical role of a manager transport listener.""" - - AGENT_EVENT_INGESTION = "agent_event_ingestion" - AGENT_ENROLLMENT = "agent_enrollment" - SYSLOG_INGESTION = "syslog_ingestion" - API = "api" - ALERT_FORWARDING = "alert_forwarding" - INDEXER_FORWARDING = "indexer_forwarding" - DASHBOARD = "dashboard" - OTHER = "other" - UNKNOWN = "unknown" - - -class RuntimeSecurityMonitoringComponentKind(str, Enum): - """Portable component/module kind inside a security-monitoring manager.""" - - ANALYSIS_ENGINE = "analysis_engine" - AGENT_INGESTION = "agent_ingestion" - AGENT_ENROLLMENT = "agent_enrollment" - MODULE_SUPERVISOR = "module_supervisor" - LOG_COLLECTION = "log_collection" - ALERTING = "alerting" - API = "api" - CLUSTER = "cluster" - INDEXER_FORWARDER = "indexer_forwarder" - VULNERABILITY_DETECTION = "vulnerability_detection" - FILE_INTEGRITY_MONITORING = "file_integrity_monitoring" - ROOTKIT_DETECTION = "rootkit_detection" - SCA = "sca" - ACTIVE_RESPONSE = "active_response" - INTEGRATION = "integration" - DATABASE = "database" - OTHER = "other" - UNKNOWN = "unknown" - - -class RuntimeSecurityMonitoringComponentStatus(str, Enum): - """Observed component/module status.""" - - RUNNING = "running" - STOPPED = "stopped" - DISABLED = "disabled" - ENABLED = "enabled" - DEGRADED = "degraded" - FAILED = "failed" - UNKNOWN = "unknown" - OTHER = "other" - - -class RuntimeSecurityMonitoringAgentStatus(str, Enum): - """Observed enrolled-agent status.""" - - AVAILABLE = "available" - ACTIVE = "active" - DISCONNECTED = "disconnected" - NEVER_CONNECTED = "never_connected" - PENDING = "pending" - REMOVED = "removed" - UNKNOWN = "unknown" - OTHER = "other" - - -class RuntimeSecurityMonitoringContentKind(str, Enum): - """Kind of manager-owned detection or monitoring content.""" - - RULE_CORPUS = "rule_corpus" - DECODER_CORPUS = "decoder_corpus" - CORRELATION_RULES = "correlation_rules" - SCA_POLICIES = "sca_policies" - ACTIVE_RESPONSE = "active_response" - CDB_LIST = "cdb_list" - THREAT_INTEL = "threat_intel" - DASHBOARD = "dashboard" - OTHER = "other" - UNKNOWN = "unknown" - - -class RuntimeSecurityMonitoringContentFormat(str, Enum): - """Portable format family for manager-owned content.""" - - WAZUH_RULE_XML = "wazuh_rule_xml" - WAZUH_DECODER_XML = "wazuh_decoder_xml" - SIGMA = "sigma" - YARA = "yara" - STIX = "stix" - JSON = "json" - YAML = "yaml" - XML = "xml" - QUERY = "query" - UNKNOWN = "unknown" - OTHER = "other" - - -class RuntimeSecurityMonitoringSettingProvenance(str, Enum): - """Where an observed manager setting came from.""" - - INTROSPECTION = "introspection" - CONFIGURATION_FILE = "configuration_file" - API = "api" - IMAGE_DEFAULT = "image_default" - OPERATOR_OVERRIDE = "operator_override" - RUNTIME_DEFAULT = "runtime_default" - UNKNOWN = "unknown" - OTHER = "other" - - class RuntimeSecurityMonitoringListener(SDLModel): """A manager listener bound to a same-node transport service.""" @@ -215,7 +61,7 @@ def normalize_role( @field_validator("auth_required", "tls_enabled", mode="before") @classmethod - def parse_optional_bool(cls, v: Any, info: ValidationInfo) -> bool | str | None: + def parse_optional_bool(cls, v: object, info: ValidationInfo) -> bool | str | None: return parse_optional_bool_or_var(v, field_name=info.field_name) @@ -253,7 +99,7 @@ def normalize_status( @field_validator("enabled", mode="before") @classmethod - def parse_enabled(cls, v: Any) -> bool | str | None: + def parse_enabled(cls, v: object) -> bool | str | None: return parse_optional_bool_or_var(v, field_name="enabled") @field_validator("name") @@ -295,7 +141,7 @@ def validate_name(cls, v: str) -> str: @field_validator("group_refs", mode="before") @classmethod - def coerce_group_refs(cls, v: Any) -> list[str]: + def coerce_group_refs(cls, v: object) -> list[str]: return coerce_string_list(v) @@ -315,7 +161,7 @@ def validate_group_id(cls, v: str) -> str: @field_validator("member_refs", "configuration_file_refs", mode="before") @classmethod - def coerce_lists(cls, v: Any) -> list[str]: + def coerce_lists(cls, v: object) -> list[str]: return coerce_string_list(v) @field_validator("configuration_file_refs") @@ -359,12 +205,12 @@ def normalize_format( @field_validator("file_count", mode="before") @classmethod - def parse_file_count(cls, v: Any) -> int | str | None: + def parse_file_count(cls, v: object) -> int | str | None: return parse_int_or_var(v, minimum=0, field_name="file_count") if v is not None else v @field_validator("file_refs", mode="before") @classmethod - def coerce_file_refs(cls, v: Any) -> list[str]: + def coerce_file_refs(cls, v: object) -> list[str]: return coerce_string_list(v) @field_validator("file_refs") @@ -374,7 +220,7 @@ def validate_file_refs(cls, v: list[str]) -> list[str]: @field_validator("loaded", mode="before") @classmethod - def parse_loaded(cls, v: Any) -> bool | str | None: + def parse_loaded(cls, v: object) -> bool | str | None: return parse_optional_bool_or_var(v, field_name="loaded") @@ -477,7 +323,7 @@ def normalize_manager_kind( @field_validator("configuration_file_refs", "log_file_refs", "evidence_refs", mode="before") @classmethod - def coerce_file_refs(cls, v: Any) -> list[str]: + def coerce_file_refs(cls, v: object) -> list[str]: return coerce_string_list(v) @field_validator("configuration_file_refs", "log_file_refs", "evidence_refs") diff --git a/implementations/python/packages/raes/semantics/objective_semantics.py b/implementations/python/packages/raes/semantics/objective_semantics.py deleted file mode 100644 index ec966fb91..000000000 --- a/implementations/python/packages/raes/semantics/objective_semantics.py +++ /dev/null @@ -1,573 +0,0 @@ -"""Pure declarative-objective semantic helpers (SEM-207). - -:func:`analyze_objective_semantics` is the single name-level source of truth -for the SDL declarative-objective construct — actor binding, target resolution, -success interpretation over backend-neutral assertions, the -optional window (delegated to :func:`raes.semantics.objectives.analyze_objective_window`), -and the acyclic ``depends_on`` ordering relation. It returns normalized -references with their dependency-role tags, the per-objective ordering/refresh -dependency names, and a fail-closed issue list that ``raes.validator`` -renders as authoring errors. ``raes_processor.compiler`` reuses the -ordering/refresh role decision (:func:`partition_objective_dependencies`) when -it maps a compiled ``evaluation.objective.*`` resource onto its dependency -tuples, and the planner then walks those edges generically. - -Role allocation: success and ``depends_on`` edges order *and* refresh; window -edges only refresh; actor and target references are normalized for fail-closed -validation but carry an empty role tuple today (the compiler does not propagate -through them). Per ADR-015 this helper lives with the SDL package and has no -processor-runtime dependencies; per ADR-016 it is part of the realized artifact -set for SEM-207. Bound-to-node binding diagnostics remain a compilation-phase -concern (``evaluation.condition-ref`` is emitted on resolved addresses, not by -this name-level analyzer). -""" - -from __future__ import annotations - -from collections import defaultdict, deque -from collections.abc import Callable, Collection, Mapping -from dataclasses import dataclass, field -from enum import Enum - -from .assessment import AssessmentResourceKind -from .objectives import ( - ObjectiveDependencyRole, - ObjectiveWindowAnalysis, - ObjectiveWindowReferenceKind, - analyze_objective_window, -) - - -class ObjectiveReferenceKind(str, Enum): - """Kinds of cross-resource reference an objective carries.""" - - ACTOR = "actor" - TARGET = "target" - SUCCESS = "success" - WINDOW = "window" - DEPENDENCY = "dependency" - - -# Role allocation by reference category (single authority for the planner- -# facing decision). Success and depends_on order *and* refresh; window only -# refreshes; actor and target are empty today — they are normalized for -# fail-closed validation but the compiler does not propagate through them, so -# advertising a role here would lie about reaching the planner. A future -# change that compiles actor/target into runtime addresses lifts the constant -# in lockstep. -_BOTH_ROLES = (ObjectiveDependencyRole.ORDERING, ObjectiveDependencyRole.REFRESH) -OBJECTIVE_SUCCESS_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = _BOTH_ROLES -OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = _BOTH_ROLES -OBJECTIVE_ACTOR_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = () -OBJECTIVE_TARGET_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = () -OBJECTIVE_WINDOW_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = (ObjectiveDependencyRole.REFRESH,) - - -@dataclass(frozen=True) -class ObjectiveReference: - """A normalized reference from one objective to an upstream resource.""" - - raw: str - canonical_name: str - reference_kind: ObjectiveReferenceKind - source_name: str - dependency_roles: tuple[ObjectiveDependencyRole, ...] = () - #: Set on ``SUCCESS`` references to preserve the assertion namespace. - success_resource_kind: AssessmentResourceKind | None = None - window_reference_kind: ObjectiveWindowReferenceKind | None = None - workflow_name: str | None = None - step_name: str | None = None - #: Reserved for later module/import expansion; the analysis runs on - #: already-composed scenarios, so it is empty today. - namespace_path: tuple[str, ...] = () - - -@dataclass(frozen=True) -class ObjectiveIssue: - """A machine-readable objective-semantics consistency problem. - - ``ref`` names the offending reference target. ``actor_name`` carries the - declaring agent for ``action-not-declared``. ``candidates`` carries the - sorted alternatives for an ambiguous target. ``workflow_name`` / ``step_name`` - carry the parsed parts of a window step ref. ``objective_name`` is empty for - the global ``objective.dependency-cycle`` issue. - """ - - code: str - objective_name: str - ref: str | None = None - actor_name: str | None = None - candidates: tuple[str, ...] = () - workflow_name: str | None = None - step_name: str | None = None - - -@dataclass(frozen=True) -class ObjectiveResourceDependencies: - """Derived upstream dependencies for one objective.""" - - name: str - ordering_names: tuple[str, ...] = () - refresh_names: tuple[str, ...] = () - - -@dataclass(frozen=True) -class ObjectiveSemanticAnalysis: - """Result of analyzing the declarative objectives of a scenario.""" - - references: tuple[ObjectiveReference, ...] = () - issues: tuple[ObjectiveIssue, ...] = () - dependencies: tuple[ObjectiveResourceDependencies, ...] = () - window_analyses: Mapping[str, ObjectiveWindowAnalysis] = field(default_factory=dict) - - @property - def has_issues(self) -> bool: - return bool(self.issues) - - def issues_of_code(self, code: str) -> tuple[ObjectiveIssue, ...]: - return tuple(issue for issue in self.issues if issue.code == code) - - def references_of_kind(self, kind: ObjectiveReferenceKind) -> tuple[ObjectiveReference, ...]: - return tuple(ref for ref in self.references if ref.reference_kind == kind) - - def dependencies_for(self, name: str) -> ObjectiveResourceDependencies: - for dependency in self.dependencies: - if dependency.name == name: - return dependency - raise KeyError(name) - - -@dataclass(frozen=True) -class AssessmentResourceCatalog: - """The backend-neutral assertion section objective success may name.""" - - assertions: Mapping[str, object] - - -@dataclass(frozen=True) -class WindowResourceCatalog: - """The four timeline section maps an objective's window may name.""" - - stories: Mapping[str, object] - scripts: Mapping[str, object] - events: Mapping[str, object] - workflows: Mapping[str, object] - - -def _ordered_unique(items: list[str]) -> tuple[str, ...]: - return tuple(dict.fromkeys(items)) - - -def _never_unresolved(_value: object) -> bool: - return False - - -def partition_objective_dependencies( - *, - success_refs: Collection[str], - dependency_refs: Collection[str], - window_refresh_refs: Collection[str], -) -> tuple[tuple[str, ...], tuple[str, ...]]: - """Split an objective's upstream references into (ordering, refresh) tuples. - - Each category is gated by its own ``OBJECTIVE_*_DEPENDENCY_ROLES`` constant - so a future role change to one category (say, ``depends_on`` becoming - refresh-only) lands in exactly one place. Works on names (validator side) - or compiled addresses (compiler side); the result is order-preserving and - de-duplicated within each role. - """ - - success = list(success_refs) - deps = list(dependency_refs) - window = list(window_refresh_refs) - ordering: list[str] = [] - refresh: list[str] = [] - for category, roles in ( - (success, OBJECTIVE_SUCCESS_DEPENDENCY_ROLES), - (deps, OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES), - (window, OBJECTIVE_WINDOW_DEPENDENCY_ROLES), - ): - if ObjectiveDependencyRole.ORDERING in roles: - ordering.extend(category) - if ObjectiveDependencyRole.REFRESH in roles: - refresh.extend(category) - return _ordered_unique(ordering), _ordered_unique(refresh) - - -def _has_cycle(graph: Mapping[str, list[str]]) -> bool: - """Return True if the directed ``graph`` (node -> deps) contains a cycle.""" - - in_degree: dict[str, int] = defaultdict(int) - for node in graph: - in_degree.setdefault(node, 0) - for deps in graph.values(): - for dep in deps: - in_degree[dep] += 1 - queue = deque(node for node, degree in in_degree.items() if degree == 0) - visited = 0 - while queue: - node = queue.popleft() - visited += 1 - for dep in graph.get(node, []): - in_degree[dep] -= 1 - if in_degree[dep] == 0: - queue.append(dep) - return visited != len(in_degree) - - -_SUCCESS_REFERENCE_SECTIONS: tuple[tuple[str, AssessmentResourceKind, str], ...] = ( - ("assertions", AssessmentResourceKind.ASSERTION, "objective.success-assertion-undeclared"), -) - - -def _check_agent_actions( - objective_name: str, - objective: object, - agent_name: str, - agent: object, - unresolved: Callable[[object], bool], -) -> list[ObjectiveIssue]: - allowed = set(getattr(agent, "actions", []) or []) - return [ - ObjectiveIssue( - code="objective.action-not-declared", - objective_name=objective_name, - ref=action, - actor_name=agent_name, - ) - for action in getattr(objective, "actions", []) or [] - if not unresolved(action) and action not in allowed - ] - - -def _check_agent( - objective_name: str, - objective: object, - agents_by_name: Mapping[str, object], - unresolved: Callable[[object], bool], -) -> tuple[list[ObjectiveReference], list[ObjectiveIssue]]: - name = getattr(objective, "agent", "") or "" - if not name or unresolved(name): - return [], [] - if name not in agents_by_name: - return [], [ObjectiveIssue(code="objective.actor-agent-undeclared", objective_name=objective_name, ref=name)] - ref = ObjectiveReference( - raw=name, - canonical_name=name, - reference_kind=ObjectiveReferenceKind.ACTOR, - source_name=objective_name, - dependency_roles=OBJECTIVE_ACTOR_DEPENDENCY_ROLES, - ) - return [ref], _check_agent_actions(objective_name, objective, name, agents_by_name[name], unresolved) - - -def _check_entity( - objective_name: str, - objective: object, - entity_name_set: set[str], - unresolved: Callable[[object], bool], -) -> tuple[list[ObjectiveReference], list[ObjectiveIssue]]: - name = getattr(objective, "entity", "") or "" - if not name or unresolved(name): - return [], [] - if name not in entity_name_set: - return [], [ObjectiveIssue(code="objective.actor-entity-undeclared", objective_name=objective_name, ref=name)] - ref = ObjectiveReference( - raw=name, - canonical_name=f"entities.{name}", - reference_kind=ObjectiveReferenceKind.ACTOR, - source_name=objective_name, - dependency_roles=OBJECTIVE_ACTOR_DEPENDENCY_ROLES, - ) - return [ref], [] - - -def _analyze_actor_binding( - objective_name: str, - objective: object, - agents_by_name: Mapping[str, object], - entity_name_set: set[str], - unresolved: Callable[[object], bool], -) -> tuple[list[ObjectiveReference], list[ObjectiveIssue]]: - agent_refs, agent_issues = _check_agent(objective_name, objective, agents_by_name, unresolved) - entity_refs, entity_issues = _check_entity(objective_name, objective, entity_name_set, unresolved) - return [*agent_refs, *entity_refs], [*agent_issues, *entity_issues] - - -def _analyze_targets( - objective_name: str, - objective: object, - targetable_name_index: Mapping[str, Collection[str]], - unresolved: Callable[[object], bool], -) -> tuple[list[ObjectiveReference], list[ObjectiveIssue]]: - refs: list[ObjectiveReference] = [] - issues: list[ObjectiveIssue] = [] - for target in getattr(objective, "targets", []) or []: - if unresolved(target): - continue - candidates = targetable_name_index.get(target) - if not candidates: - issues.append( - ObjectiveIssue( - code="objective.target-unresolvable", - objective_name=objective_name, - ref=target, - ) - ) - continue - if len(candidates) > 1: - issues.append( - ObjectiveIssue( - code="objective.target-ambiguous", - objective_name=objective_name, - ref=target, - candidates=tuple(sorted(candidates)), - ) - ) - continue - (canonical_target,) = tuple(candidates) - refs.append( - ObjectiveReference( - raw=target, - canonical_name=canonical_target, - reference_kind=ObjectiveReferenceKind.TARGET, - source_name=objective_name, - dependency_roles=OBJECTIVE_TARGET_DEPENDENCY_ROLES, - ) - ) - return refs, issues - - -def _analyze_success( - objective_name: str, - objective: object, - assessment_resources: AssessmentResourceCatalog, - unresolved: Callable[[object], bool], -) -> tuple[list[ObjectiveReference], list[ObjectiveIssue], list[str]]: - """Resolve backend-neutral ``success.assertions``. - - Resolved names are kind-qualified before they enter the derived - ordering/refresh tuples, preserving the kind-qualifier seam even though - ``conditions`` is the only success reference kind today. - """ - - refs: list[ObjectiveReference] = [] - issues: list[ObjectiveIssue] = [] - resolved: list[str] = [] - success = getattr(objective, "success", None) - sections = ((assessment_resources.assertions, _SUCCESS_REFERENCE_SECTIONS[0]),) - for section, (attr, kind, code) in sections: - for ref_name in getattr(success, attr, []) or []: - if unresolved(ref_name): - continue - if ref_name not in section: - issues.append(ObjectiveIssue(code=code, objective_name=objective_name, ref=ref_name)) - continue - qualified_name = f"{kind.value}.{ref_name}" - refs.append( - ObjectiveReference( - raw=ref_name, - canonical_name=qualified_name, - reference_kind=ObjectiveReferenceKind.SUCCESS, - source_name=objective_name, - dependency_roles=OBJECTIVE_SUCCESS_DEPENDENCY_ROLES, - success_resource_kind=kind, - ) - ) - resolved.append(qualified_name) - return refs, issues, resolved - - -def _analyze_window( - objective_name: str, - objective: object, - window_resources: WindowResourceCatalog, - unresolved: Callable[[object], bool], -) -> tuple[list[ObjectiveReference], list[ObjectiveIssue], ObjectiveWindowAnalysis | None, list[str]]: - """Delegate window resolution to the SEM-202 helper and re-tag the result.""" - - window = getattr(objective, "window", None) - if window is None: - return [], [], None, [] - - analysis = analyze_objective_window( - story_refs=[ref for ref in getattr(window, "stories", []) or [] if not unresolved(ref)], - script_refs=[ref for ref in getattr(window, "scripts", []) or [] if not unresolved(ref)], - event_refs=[ref for ref in getattr(window, "events", []) or [] if not unresolved(ref)], - workflow_refs=[ref for ref in getattr(window, "workflows", []) or [] if not unresolved(ref)], - step_refs=[ref for ref in getattr(window, "steps", []) or [] if not unresolved(ref)], - stories_by_name=window_resources.stories, - scripts_by_name=window_resources.scripts, - events_by_name=window_resources.events, - workflows_by_name=window_resources.workflows, - ) - refs = [ - # The SEM-207 role constant is the single authority for objective-side - # window roles; the lower-level ``ObjectiveWindowReference.dependency_roles`` - # is the SEM-202 helper's own metadata and must not double as the - # planner-facing role decision. - ObjectiveReference( - raw=window_ref.raw, - canonical_name=window_ref.canonical_name, - reference_kind=ObjectiveReferenceKind.WINDOW, - source_name=objective_name, - dependency_roles=OBJECTIVE_WINDOW_DEPENDENCY_ROLES, - window_reference_kind=window_ref.reference_kind, - workflow_name=window_ref.workflow_name, - step_name=window_ref.step_name, - namespace_path=window_ref.namespace_path, - ) - for window_ref in analysis.references - ] - issues = [ - ObjectiveIssue( - code=f"objective.window.{window_issue.code}", - objective_name=objective_name, - ref=window_issue.ref, - workflow_name=window_issue.workflow_name, - step_name=window_issue.step_name, - ) - for window_issue in analysis.issues - ] - # Each window keyspace gets its kind prefix so it cannot collide with - # success-side or depends_on-side names in ``refresh_names``. - refresh = [ - *(f"story.{name}" for name in analysis.story_names), - *(f"script.{name}" for name in analysis.script_names), - *(f"event.{name}" for name in analysis.event_names), - *(f"workflow.{name}" for name in analysis.workflow_names), - *(f"workflow.{name}" for name in analysis.refresh_workflow_names), - ] - return refs, issues, analysis, refresh - - -def _analyze_dependencies( - objective_name: str, - objective: object, - objectives_by_name: Mapping[str, object], - unresolved: Callable[[object], bool], -) -> tuple[list[ObjectiveReference], list[ObjectiveIssue], list[str]]: - refs: list[ObjectiveReference] = [] - issues: list[ObjectiveIssue] = [] - resolved: list[str] = [] - for dep_name in getattr(objective, "depends_on", []) or []: - if unresolved(dep_name): - continue - if dep_name not in objectives_by_name: - issues.append( - ObjectiveIssue( - code="objective.dependency-undeclared", - objective_name=objective_name, - ref=dep_name, - ) - ) - continue - qualified_dep = f"objective.{dep_name}" - refs.append( - ObjectiveReference( - raw=dep_name, - canonical_name=qualified_dep, - reference_kind=ObjectiveReferenceKind.DEPENDENCY, - source_name=objective_name, - dependency_roles=OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES, - ) - ) - resolved.append(qualified_dep) - return refs, issues, resolved - - -def _objective_dependency_graph( - objectives_by_name: Mapping[str, object], - unresolved: Callable[[object], bool], -) -> dict[str, list[str]]: - return { - name: [ - dep - for dep in getattr(objective, "depends_on", []) or [] - if not unresolved(dep) and dep in objectives_by_name - ] - for name, objective in objectives_by_name.items() - } - - -def analyze_objective_semantics( - *, - objectives_by_name: Mapping[str, object], - agents_by_name: Mapping[str, object], - entity_names: Collection[str], - assessment_resources: AssessmentResourceCatalog, - window_resources: WindowResourceCatalog, - targetable_name_index: Mapping[str, Collection[str]], - is_unresolved: Callable[[object], bool] | None = None, -) -> ObjectiveSemanticAnalysis: - """Resolve the objective reference graph and derive its shared semantics. - - Inputs are the name-keyed objective and agent maps, the entity name set, - the bundled assessment-pipeline and timeline resource catalogs, and the - targetable named-reference index; ``is_unresolved`` (default: never) lets a - caller skip references that are still ``${var}`` placeholders. Returns the - normalized references, the per-objective ordering/refresh dependency names, - the per-objective window analyses, and any consistency issues — per - objective in the order actor, action, target, success, window, dependency, - then a single global ``objective.dependency-cycle`` issue when the - ``depends_on`` graph cycles. - """ - - unresolved = is_unresolved or _never_unresolved - entity_name_set = set(entity_names) - references: list[ObjectiveReference] = [] - issues: list[ObjectiveIssue] = [] - dependencies: list[ObjectiveResourceDependencies] = [] - window_analyses: dict[str, ObjectiveWindowAnalysis] = {} - - for objective_name, objective in objectives_by_name.items(): - actor_refs, actor_issues = _analyze_actor_binding( - objective_name, objective, agents_by_name, entity_name_set, unresolved - ) - target_refs, target_issues = _analyze_targets(objective_name, objective, targetable_name_index, unresolved) - success_refs, success_issues, resolved_success = _analyze_success( - objective_name, objective, assessment_resources, unresolved - ) - window_refs, window_issues, window_analysis, window_refresh = _analyze_window( - objective_name, objective, window_resources, unresolved - ) - dep_refs, dep_issues, resolved_dependencies = _analyze_dependencies( - objective_name, objective, objectives_by_name, unresolved - ) - - references.extend(actor_refs) - references.extend(target_refs) - references.extend(success_refs) - references.extend(window_refs) - references.extend(dep_refs) - issues.extend(actor_issues) - issues.extend(target_issues) - issues.extend(success_issues) - issues.extend(window_issues) - issues.extend(dep_issues) - if window_analysis is not None: - window_analyses[objective_name] = window_analysis - - ordering_names, refresh_names = partition_objective_dependencies( - success_refs=_ordered_unique(resolved_success), - dependency_refs=_ordered_unique(resolved_dependencies), - window_refresh_refs=window_refresh, - ) - dependencies.append( - ObjectiveResourceDependencies( - name=objective_name, - ordering_names=ordering_names, - refresh_names=refresh_names, - ) - ) - - dependency_graph = _objective_dependency_graph(objectives_by_name, unresolved) - if dependency_graph and _has_cycle(dependency_graph): - issues.append(ObjectiveIssue(code="objective.dependency-cycle", objective_name="")) - - return ObjectiveSemanticAnalysis( - references=tuple(references), - issues=tuple(issues), - dependencies=tuple(dependencies), - window_analyses=dict(window_analyses), - ) diff --git a/implementations/python/packages/raes/semantics/objective_semantics/__init__.py b/implementations/python/packages/raes/semantics/objective_semantics/__init__.py new file mode 100644 index 000000000..c5e84e8e0 --- /dev/null +++ b/implementations/python/packages/raes/semantics/objective_semantics/__init__.py @@ -0,0 +1,176 @@ +"""Pure declarative-objective semantic helpers (SEM-207). + +:func:`analyze_objective_semantics` is the single name-level source of truth +for the SDL declarative-objective construct — actor binding, target resolution, +success interpretation over backend-neutral assertions, the +optional window (delegated to :func:`raes.semantics.objectives.analyze_objective_window`), +and the acyclic ``depends_on`` ordering relation. It returns normalized +references with their dependency-role tags, the per-objective ordering/refresh +dependency names, and a fail-closed issue list that ``raes.validator`` +renders as authoring errors. ``raes_processor.compiler`` reuses the +ordering/refresh role decision (:func:`partition_objective_dependencies`) when +it maps a compiled ``evaluation.objective.*`` resource onto its dependency +tuples, and the planner then walks those edges generically. + +Role allocation: success and ``depends_on`` edges order *and* refresh; window +edges only refresh; actor and target references are normalized for fail-closed +validation but carry an empty role tuple today (the compiler does not propagate +through them). Per ADR-015 this helper lives with the SDL package and has no +processor-runtime dependencies; per ADR-016 it is part of the realized artifact +set for SEM-207. Bound-to-node binding diagnostics remain a compilation-phase +concern (``evaluation.condition-ref`` is emitted on resolved addresses, not by +this name-level analyzer). + +The ``partition_objective_dependencies`` role decision reads the +``OBJECTIVE_*_DEPENDENCY_ROLES`` constants from this facade module, so the +per-category role allocation stays a single monkeypatch-visible authority even +though the constants are defined in ``._constants``. +""" + +from __future__ import annotations + +from collections.abc import Callable, Collection, Mapping + +from ..objectives import ObjectiveDependencyRole, ObjectiveWindowAnalysis +from ._analysis import ( + _analyze_actor_binding, + _analyze_dependencies, + _analyze_success, + _analyze_targets, + _analyze_window, + _has_cycle, + _never_unresolved, + _objective_dependency_graph, + _ordered_unique, +) +from ._constants import ( + OBJECTIVE_ACTOR_DEPENDENCY_ROLES, + OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES, + OBJECTIVE_SUCCESS_DEPENDENCY_ROLES, + OBJECTIVE_TARGET_DEPENDENCY_ROLES, + OBJECTIVE_WINDOW_DEPENDENCY_ROLES, +) +from ._types import ( + AssessmentResourceCatalog, + ObjectiveIssue, + ObjectiveReference, + ObjectiveReferenceKind, + ObjectiveResourceDependencies, + ObjectiveSemanticAnalysis, + WindowResourceCatalog, +) + + +def partition_objective_dependencies( + *, + success_refs: Collection[str], + dependency_refs: Collection[str], + window_refresh_refs: Collection[str], +) -> tuple[tuple[str, ...], tuple[str, ...]]: + """Split an objective's upstream references into (ordering, refresh) tuples. + + Each category is gated by its own ``OBJECTIVE_*_DEPENDENCY_ROLES`` constant + so a future role change to one category (say, ``depends_on`` becoming + refresh-only) lands in exactly one place. Works on names (validator side) + or compiled addresses (compiler side); the result is order-preserving and + de-duplicated within each role. + """ + + success = list(success_refs) + deps = list(dependency_refs) + window = list(window_refresh_refs) + ordering: list[str] = [] + refresh: list[str] = [] + for category, roles in ( + (success, OBJECTIVE_SUCCESS_DEPENDENCY_ROLES), + (deps, OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES), + (window, OBJECTIVE_WINDOW_DEPENDENCY_ROLES), + ): + if ObjectiveDependencyRole.ORDERING in roles: + ordering.extend(category) + if ObjectiveDependencyRole.REFRESH in roles: + refresh.extend(category) + return _ordered_unique(ordering), _ordered_unique(refresh) + + +def analyze_objective_semantics( + *, + objectives_by_name: Mapping[str, object], + agents_by_name: Mapping[str, object], + entity_names: Collection[str], + assessment_resources: AssessmentResourceCatalog, + window_resources: WindowResourceCatalog, + targetable_name_index: Mapping[str, Collection[str]], + is_unresolved: Callable[[object], bool] | None = None, +) -> ObjectiveSemanticAnalysis: + """Resolve the objective reference graph and derive its shared semantics. + + Inputs are the name-keyed objective and agent maps, the entity name set, + the bundled assessment-pipeline and timeline resource catalogs, and the + targetable named-reference index; ``is_unresolved`` (default: never) lets a + caller skip references that are still ``${var}`` placeholders. Returns the + normalized references, the per-objective ordering/refresh dependency names, + the per-objective window analyses, and any consistency issues — per + objective in the order actor, action, target, success, window, dependency, + then a single global ``objective.dependency-cycle`` issue when the + ``depends_on`` graph cycles. + """ + + unresolved = is_unresolved or _never_unresolved + entity_name_set = set(entity_names) + references: list[ObjectiveReference] = [] + issues: list[ObjectiveIssue] = [] + dependencies: list[ObjectiveResourceDependencies] = [] + window_analyses: dict[str, ObjectiveWindowAnalysis] = {} + + for objective_name, objective in objectives_by_name.items(): + actor_refs, actor_issues = _analyze_actor_binding( + objective_name, objective, agents_by_name, entity_name_set, unresolved + ) + target_refs, target_issues = _analyze_targets(objective_name, objective, targetable_name_index, unresolved) + success_refs, success_issues, resolved_success = _analyze_success( + objective_name, objective, assessment_resources, unresolved + ) + window_refs, window_issues, window_analysis, window_refresh = _analyze_window( + objective_name, objective, window_resources, unresolved + ) + dep_refs, dep_issues, resolved_dependencies = _analyze_dependencies( + objective_name, objective, objectives_by_name, unresolved + ) + + references.extend(actor_refs) + references.extend(target_refs) + references.extend(success_refs) + references.extend(window_refs) + references.extend(dep_refs) + issues.extend(actor_issues) + issues.extend(target_issues) + issues.extend(success_issues) + issues.extend(window_issues) + issues.extend(dep_issues) + if window_analysis is not None: + window_analyses[objective_name] = window_analysis + + ordering_names, refresh_names = partition_objective_dependencies( + success_refs=_ordered_unique(resolved_success), + dependency_refs=_ordered_unique(resolved_dependencies), + window_refresh_refs=window_refresh, + ) + dependencies.append( + ObjectiveResourceDependencies( + name=objective_name, + ordering_names=ordering_names, + refresh_names=refresh_names, + ) + ) + + dependency_graph = _objective_dependency_graph(objectives_by_name, unresolved) + if dependency_graph and _has_cycle(dependency_graph): + issues.append(ObjectiveIssue(code="objective.dependency-cycle", objective_name="")) + + return ObjectiveSemanticAnalysis( + references=tuple(references), + issues=tuple(issues), + dependencies=tuple(dependencies), + window_analyses=dict(window_analyses), + ) diff --git a/implementations/python/packages/raes/semantics/objective_semantics/_analysis.py b/implementations/python/packages/raes/semantics/objective_semantics/_analysis.py new file mode 100644 index 000000000..5cd76a401 --- /dev/null +++ b/implementations/python/packages/raes/semantics/objective_semantics/_analysis.py @@ -0,0 +1,344 @@ +"""Per-objective reference resolution and diagnostic helpers for objective semantics.""" + +from __future__ import annotations + +from collections import defaultdict, deque +from collections.abc import Callable, Collection, Mapping + +from ..assessment import AssessmentResourceKind +from ..objectives import ( + ObjectiveWindowAnalysis, + analyze_objective_window, +) +from ._constants import ( + OBJECTIVE_ACTOR_DEPENDENCY_ROLES, + OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES, + OBJECTIVE_SUCCESS_DEPENDENCY_ROLES, + OBJECTIVE_TARGET_DEPENDENCY_ROLES, + OBJECTIVE_WINDOW_DEPENDENCY_ROLES, +) +from ._types import ( + AssessmentResourceCatalog, + ObjectiveIssue, + ObjectiveReference, + ObjectiveReferenceKind, + WindowResourceCatalog, +) + + +def _ordered_unique(items: list[str]) -> tuple[str, ...]: + return tuple(dict.fromkeys(items)) + + +def _never_unresolved(_value: object) -> bool: + return False + + +def _has_cycle(graph: Mapping[str, list[str]]) -> bool: + """Return True if the directed ``graph`` (node -> deps) contains a cycle.""" + + in_degree: dict[str, int] = defaultdict(int) + for node in graph: + in_degree.setdefault(node, 0) + for deps in graph.values(): + for dep in deps: + in_degree[dep] += 1 + queue = deque(node for node, degree in in_degree.items() if degree == 0) + visited = 0 + while queue: + node = queue.popleft() + visited += 1 + for dep in graph.get(node, []): + in_degree[dep] -= 1 + if in_degree[dep] == 0: + queue.append(dep) + return visited != len(in_degree) + + +_SUCCESS_REFERENCE_SECTIONS: tuple[tuple[str, AssessmentResourceKind, str], ...] = ( + ("assertions", AssessmentResourceKind.ASSERTION, "objective.success-assertion-undeclared"), +) + + +def _check_agent_actions( + objective_name: str, + objective: object, + agent_name: str, + agent: object, + unresolved: Callable[[object], bool], +) -> list[ObjectiveIssue]: + allowed = set(getattr(agent, "actions", []) or []) + return [ + ObjectiveIssue( + code="objective.action-not-declared", + objective_name=objective_name, + ref=action, + actor_name=agent_name, + ) + for action in getattr(objective, "actions", []) or [] + if not unresolved(action) and action not in allowed + ] + + +def _check_agent( + objective_name: str, + objective: object, + agents_by_name: Mapping[str, object], + unresolved: Callable[[object], bool], +) -> tuple[list[ObjectiveReference], list[ObjectiveIssue]]: + name = getattr(objective, "agent", "") or "" + if not name or unresolved(name): + return [], [] + if name not in agents_by_name: + return [], [ObjectiveIssue(code="objective.actor-agent-undeclared", objective_name=objective_name, ref=name)] + ref = ObjectiveReference( + raw=name, + canonical_name=name, + reference_kind=ObjectiveReferenceKind.ACTOR, + source_name=objective_name, + dependency_roles=OBJECTIVE_ACTOR_DEPENDENCY_ROLES, + ) + return [ref], _check_agent_actions(objective_name, objective, name, agents_by_name[name], unresolved) + + +def _check_entity( + objective_name: str, + objective: object, + entity_name_set: set[str], + unresolved: Callable[[object], bool], +) -> tuple[list[ObjectiveReference], list[ObjectiveIssue]]: + name = getattr(objective, "entity", "") or "" + if not name or unresolved(name): + return [], [] + if name not in entity_name_set: + return [], [ObjectiveIssue(code="objective.actor-entity-undeclared", objective_name=objective_name, ref=name)] + ref = ObjectiveReference( + raw=name, + canonical_name=f"entities.{name}", + reference_kind=ObjectiveReferenceKind.ACTOR, + source_name=objective_name, + dependency_roles=OBJECTIVE_ACTOR_DEPENDENCY_ROLES, + ) + return [ref], [] + + +def _analyze_actor_binding( + objective_name: str, + objective: object, + agents_by_name: Mapping[str, object], + entity_name_set: set[str], + unresolved: Callable[[object], bool], +) -> tuple[list[ObjectiveReference], list[ObjectiveIssue]]: + agent_refs, agent_issues = _check_agent(objective_name, objective, agents_by_name, unresolved) + entity_refs, entity_issues = _check_entity(objective_name, objective, entity_name_set, unresolved) + return [*agent_refs, *entity_refs], [*agent_issues, *entity_issues] + + +def _analyze_targets( + objective_name: str, + objective: object, + targetable_name_index: Mapping[str, Collection[str]], + unresolved: Callable[[object], bool], +) -> tuple[list[ObjectiveReference], list[ObjectiveIssue]]: + refs: list[ObjectiveReference] = [] + issues: list[ObjectiveIssue] = [] + for target in getattr(objective, "targets", []) or []: + if unresolved(target): + continue + candidates = targetable_name_index.get(target) + if not candidates: + issues.append( + ObjectiveIssue( + code="objective.target-unresolvable", + objective_name=objective_name, + ref=target, + ) + ) + continue + if len(candidates) > 1: + issues.append( + ObjectiveIssue( + code="objective.target-ambiguous", + objective_name=objective_name, + ref=target, + candidates=tuple(sorted(candidates)), + ) + ) + continue + (canonical_target,) = tuple(candidates) + refs.append( + ObjectiveReference( + raw=target, + canonical_name=canonical_target, + reference_kind=ObjectiveReferenceKind.TARGET, + source_name=objective_name, + dependency_roles=OBJECTIVE_TARGET_DEPENDENCY_ROLES, + ) + ) + return refs, issues + + +def _analyze_success( + objective_name: str, + objective: object, + assessment_resources: AssessmentResourceCatalog, + unresolved: Callable[[object], bool], +) -> tuple[list[ObjectiveReference], list[ObjectiveIssue], list[str]]: + """Resolve backend-neutral ``success.assertions``. + + Resolved names are kind-qualified before they enter the derived + ordering/refresh tuples, preserving the kind-qualifier seam even though + ``conditions`` is the only success reference kind today. + """ + + refs: list[ObjectiveReference] = [] + issues: list[ObjectiveIssue] = [] + resolved: list[str] = [] + success = getattr(objective, "success", None) + sections = ((assessment_resources.assertions, _SUCCESS_REFERENCE_SECTIONS[0]),) + for section, (attr, kind, code) in sections: + for ref_name in getattr(success, attr, []) or []: + if unresolved(ref_name): + continue + if ref_name not in section: + issues.append(ObjectiveIssue(code=code, objective_name=objective_name, ref=ref_name)) + continue + qualified_name = f"{kind.value}.{ref_name}" + refs.append( + ObjectiveReference( + raw=ref_name, + canonical_name=qualified_name, + reference_kind=ObjectiveReferenceKind.SUCCESS, + source_name=objective_name, + dependency_roles=OBJECTIVE_SUCCESS_DEPENDENCY_ROLES, + success_resource_kind=kind, + ) + ) + resolved.append(qualified_name) + return refs, issues, resolved + + +def _window_reference_lists( + window: object, + unresolved: Callable[[object], bool], +) -> dict[str, list[object]]: + """Filter each window keyspace's authored refs down to the resolved ones.""" + + def kept(attribute: str) -> list[object]: + return [ref for ref in getattr(window, attribute, []) or [] if not unresolved(ref)] + + return { + "story_refs": kept("stories"), + "script_refs": kept("scripts"), + "event_refs": kept("events"), + "workflow_refs": kept("workflows"), + "step_refs": kept("steps"), + } + + +def _analyze_window( + objective_name: str, + objective: object, + window_resources: WindowResourceCatalog, + unresolved: Callable[[object], bool], +) -> tuple[list[ObjectiveReference], list[ObjectiveIssue], ObjectiveWindowAnalysis | None, list[str]]: + """Delegate window resolution to the SEM-202 helper and re-tag the result.""" + + window = getattr(objective, "window", None) + if window is None: + return [], [], None, [] + + analysis = analyze_objective_window( + **_window_reference_lists(window, unresolved), + stories_by_name=window_resources.stories, + scripts_by_name=window_resources.scripts, + events_by_name=window_resources.events, + workflows_by_name=window_resources.workflows, + ) + refs = [ + # The SEM-207 role constant is the single authority for objective-side + # window roles; the lower-level ``ObjectiveWindowReference.dependency_roles`` + # is the SEM-202 helper's own metadata and must not double as the + # planner-facing role decision. + ObjectiveReference( + raw=window_ref.raw, + canonical_name=window_ref.canonical_name, + reference_kind=ObjectiveReferenceKind.WINDOW, + source_name=objective_name, + dependency_roles=OBJECTIVE_WINDOW_DEPENDENCY_ROLES, + window_reference_kind=window_ref.reference_kind, + workflow_name=window_ref.workflow_name, + step_name=window_ref.step_name, + namespace_path=window_ref.namespace_path, + ) + for window_ref in analysis.references + ] + issues = [ + ObjectiveIssue( + code=f"objective.window.{window_issue.code}", + objective_name=objective_name, + ref=window_issue.ref, + workflow_name=window_issue.workflow_name, + step_name=window_issue.step_name, + ) + for window_issue in analysis.issues + ] + # Each window keyspace gets its kind prefix so it cannot collide with + # success-side or depends_on-side names in ``refresh_names``. + refresh = [ + *(f"story.{name}" for name in analysis.story_names), + *(f"script.{name}" for name in analysis.script_names), + *(f"event.{name}" for name in analysis.event_names), + *(f"workflow.{name}" for name in analysis.workflow_names), + *(f"workflow.{name}" for name in analysis.refresh_workflow_names), + ] + return refs, issues, analysis, refresh + + +def _analyze_dependencies( + objective_name: str, + objective: object, + objectives_by_name: Mapping[str, object], + unresolved: Callable[[object], bool], +) -> tuple[list[ObjectiveReference], list[ObjectiveIssue], list[str]]: + refs: list[ObjectiveReference] = [] + issues: list[ObjectiveIssue] = [] + resolved: list[str] = [] + for dep_name in getattr(objective, "depends_on", []) or []: + if unresolved(dep_name): + continue + if dep_name not in objectives_by_name: + issues.append( + ObjectiveIssue( + code="objective.dependency-undeclared", + objective_name=objective_name, + ref=dep_name, + ) + ) + continue + qualified_dep = f"objective.{dep_name}" + refs.append( + ObjectiveReference( + raw=dep_name, + canonical_name=qualified_dep, + reference_kind=ObjectiveReferenceKind.DEPENDENCY, + source_name=objective_name, + dependency_roles=OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES, + ) + ) + resolved.append(qualified_dep) + return refs, issues, resolved + + +def _objective_dependency_graph( + objectives_by_name: Mapping[str, object], + unresolved: Callable[[object], bool], +) -> dict[str, list[str]]: + return { + name: [ + dep + for dep in getattr(objective, "depends_on", []) or [] + if not unresolved(dep) and dep in objectives_by_name + ] + for name, objective in objectives_by_name.items() + } diff --git a/implementations/python/packages/raes/semantics/objective_semantics/_constants.py b/implementations/python/packages/raes/semantics/objective_semantics/_constants.py new file mode 100644 index 000000000..f721fac5a --- /dev/null +++ b/implementations/python/packages/raes/semantics/objective_semantics/_constants.py @@ -0,0 +1,19 @@ +"""Role-allocation constants for the objective dependency partitioning decision.""" + +from __future__ import annotations + +from ..objectives import ObjectiveDependencyRole + +# Role allocation by reference category (single authority for the planner- +# facing decision). Success and depends_on order *and* refresh; window only +# refreshes; actor and target are empty today — they are normalized for +# fail-closed validation but the compiler does not propagate through them, so +# advertising a role here would lie about reaching the planner. A future +# change that compiles actor/target into runtime addresses lifts the constant +# in lockstep. +_BOTH_ROLES = (ObjectiveDependencyRole.ORDERING, ObjectiveDependencyRole.REFRESH) +OBJECTIVE_SUCCESS_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = _BOTH_ROLES +OBJECTIVE_DEPENDENCY_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = _BOTH_ROLES +OBJECTIVE_ACTOR_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = () +OBJECTIVE_TARGET_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = () +OBJECTIVE_WINDOW_DEPENDENCY_ROLES: tuple[ObjectiveDependencyRole, ...] = (ObjectiveDependencyRole.REFRESH,) diff --git a/implementations/python/packages/raes/semantics/objective_semantics/_types.py b/implementations/python/packages/raes/semantics/objective_semantics/_types.py new file mode 100644 index 000000000..c2f310fa4 --- /dev/null +++ b/implementations/python/packages/raes/semantics/objective_semantics/_types.py @@ -0,0 +1,115 @@ +"""Normalized reference, issue, dependency, and analysis records for objective semantics.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from enum import Enum + +from ..assessment import AssessmentResourceKind +from ..objectives import ( + ObjectiveDependencyRole, + ObjectiveWindowAnalysis, + ObjectiveWindowReferenceKind, +) + + +class ObjectiveReferenceKind(str, Enum): + """Kinds of cross-resource reference an objective carries.""" + + ACTOR = "actor" + TARGET = "target" + SUCCESS = "success" + WINDOW = "window" + DEPENDENCY = "dependency" + + +@dataclass(frozen=True) +class ObjectiveReference: + """A normalized reference from one objective to an upstream resource.""" + + raw: str + canonical_name: str + reference_kind: ObjectiveReferenceKind + source_name: str + dependency_roles: tuple[ObjectiveDependencyRole, ...] = () + #: Set on ``SUCCESS`` references to preserve the assertion namespace. + success_resource_kind: AssessmentResourceKind | None = None + window_reference_kind: ObjectiveWindowReferenceKind | None = None + workflow_name: str | None = None + step_name: str | None = None + #: Reserved for later module/import expansion; the analysis runs on + #: already-composed scenarios, so it is empty today. + namespace_path: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ObjectiveIssue: + """A machine-readable objective-semantics consistency problem. + + ``ref`` names the offending reference target. ``actor_name`` carries the + declaring agent for ``action-not-declared``. ``candidates`` carries the + sorted alternatives for an ambiguous target. ``workflow_name`` / ``step_name`` + carry the parsed parts of a window step ref. ``objective_name`` is empty for + the global ``objective.dependency-cycle`` issue. + """ + + code: str + objective_name: str + ref: str | None = None + actor_name: str | None = None + candidates: tuple[str, ...] = () + workflow_name: str | None = None + step_name: str | None = None + + +@dataclass(frozen=True) +class ObjectiveResourceDependencies: + """Derived upstream dependencies for one objective.""" + + name: str + ordering_names: tuple[str, ...] = () + refresh_names: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ObjectiveSemanticAnalysis: + """Result of analyzing the declarative objectives of a scenario.""" + + references: tuple[ObjectiveReference, ...] = () + issues: tuple[ObjectiveIssue, ...] = () + dependencies: tuple[ObjectiveResourceDependencies, ...] = () + window_analyses: Mapping[str, ObjectiveWindowAnalysis] = field(default_factory=dict) + + @property + def has_issues(self) -> bool: + return bool(self.issues) + + def issues_of_code(self, code: str) -> tuple[ObjectiveIssue, ...]: + return tuple(issue for issue in self.issues if issue.code == code) + + def references_of_kind(self, kind: ObjectiveReferenceKind) -> tuple[ObjectiveReference, ...]: + return tuple(ref for ref in self.references if ref.reference_kind == kind) + + def dependencies_for(self, name: str) -> ObjectiveResourceDependencies: + for dependency in self.dependencies: + if dependency.name == name: + return dependency + raise KeyError(name) + + +@dataclass(frozen=True) +class AssessmentResourceCatalog: + """The backend-neutral assertion section objective success may name.""" + + assertions: Mapping[str, object] + + +@dataclass(frozen=True) +class WindowResourceCatalog: + """The four timeline section maps an objective's window may name.""" + + stories: Mapping[str, object] + scripts: Mapping[str, object] + events: Mapping[str, object] + workflows: Mapping[str, object] diff --git a/implementations/python/packages/raes/semantics/participant_behavior.py b/implementations/python/packages/raes/semantics/participant_behavior.py deleted file mode 100644 index d70a75363..000000000 --- a/implementations/python/packages/raes/semantics/participant_behavior.py +++ /dev/null @@ -1,1401 +0,0 @@ -"""Name-level participant behavior semantics (SEM-208/209/210).""" - -from __future__ import annotations - -from collections.abc import Callable, Mapping -from dataclasses import dataclass - -from ..participant_behavior_specification import tool_affordance_reference - - -@dataclass(frozen=True) -class ParticipantBehaviorReference: - """Normalized reference from an agent to a behavior contract artifact.""" - - participant_name: str - reference_kind: str - raw: str - canonical_name: str - - -@dataclass(frozen=True) -class ParticipantBehaviorIssue: - """Machine-readable participant behavior consistency issue.""" - - code: str - participant_name: str - ref: str - action_name: str = "" - boundary_name: str = "" - transition_id: str = "" - spec_name: str = "" - message: str = "" - - -@dataclass(frozen=True) -class ParticipantBehaviorAnalysis: - """Result of analyzing participant behavior references.""" - - references: tuple[ParticipantBehaviorReference, ...] = () - issues: tuple[ParticipantBehaviorIssue, ...] = () - - @property - def has_issues(self) -> bool: - return bool(self.issues) - - -@dataclass(frozen=True) -class _BehaviorSpecificationReferenceContext: - participant_names: set[str] - participant_roles_by_agent: Mapping[str, str] - action_names: set[str] - observation_boundary_names: set[str] - outcome_rule_names: set[str] - agents_by_name: Mapping[str, object] - action_contracts: Mapping[str, object] - observation_boundaries: Mapping[str, object] - clocks: Mapping[str, object] - time_progression_policies: Mapping[str, object] - temporal_constraints: Mapping[str, object] - objectives: Mapping[str, object] - - @property - def participant_roles(self) -> set[str]: - return set(self.participant_roles_by_agent.values()) - - -def _action_references_for_agent( - *, - participant_name: str, - action_names: list[object], - action_contracts: Mapping[str, object], - is_unresolved: Callable[[object], bool], -) -> tuple[list[ParticipantBehaviorReference], list[ParticipantBehaviorIssue]]: - references: list[ParticipantBehaviorReference] = [] - issues: list[ParticipantBehaviorIssue] = [] - for action_name in action_names: - if is_unresolved(action_name): - continue - if action_contracts and action_name not in action_contracts: - issues.append( - ParticipantBehaviorIssue( - code="participant.action-contract-unbound", - participant_name=participant_name, - ref=str(action_name), - ) - ) - continue - if action_name in action_contracts: - references.append( - ParticipantBehaviorReference( - participant_name=participant_name, - reference_kind="action_contract", - raw=str(action_name), - canonical_name=str(action_name), - ) - ) - return references, issues - - -def _observation_boundary_references_for_agent( - *, - participant_name: str, - boundary_names: list[object], - observation_boundaries: Mapping[str, object], - is_unresolved: Callable[[object], bool], -) -> tuple[list[ParticipantBehaviorReference], list[ParticipantBehaviorIssue]]: - references: list[ParticipantBehaviorReference] = [] - issues: list[ParticipantBehaviorIssue] = [] - for boundary_name in boundary_names: - if is_unresolved(boundary_name): - continue - if boundary_name not in observation_boundaries: - issues.append( - ParticipantBehaviorIssue( - code="participant.observation-boundary-unbound", - participant_name=participant_name, - ref=str(boundary_name), - ) - ) - continue - references.append( - ParticipantBehaviorReference( - participant_name=participant_name, - reference_kind="observation_boundary", - raw=str(boundary_name), - canonical_name=str(boundary_name), - ) - ) - return references, issues - - -def _interaction_references_for_action_contracts( - *, - action_contracts: Mapping[str, object], - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for action_name, action_contract in action_contracts.items(): - for interaction in getattr(action_contract, "interactions", []) or []: - for related_action in getattr(interaction, "related_actions", []) or []: - if is_unresolved(related_action): - continue - if related_action not in action_contracts: - issues.append( - ParticipantBehaviorIssue( - code="participant.interaction-action-unbound", - participant_name="", - action_name=str(action_name), - ref=str(related_action), - ) - ) - return issues - - -def _observation_boundary_declared_refs(observation_boundary: object) -> set[str]: - refs: set[str] = set() - refs.update(str(ref) for ref in getattr(observation_boundary, "observable_refs", []) or []) - refs.update(str(ref) for ref in getattr(observation_boundary, "hidden_refs", []) or []) - refs.update(str(ref) for ref in getattr(observation_boundary, "evidence_refs", []) or []) - return refs - - -def _observation_boundary_evidence_refs(observation_boundary: object) -> set[str]: - return {str(ref) for ref in getattr(observation_boundary, "evidence_refs", []) or []} - - -def _is_bound_reference( - ref: object, - *, - declared_refs: set[str], - is_unresolved: Callable[[object], bool], -) -> bool: - return is_unresolved(ref) or str(ref) in declared_refs - - -def _view_rule_visibility_issues( - *, - boundary_name: str, - boundary: object, - declared_refs: set[str], - evidence_refs: set[str], - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for rule in getattr(boundary, "view_rules", []) or []: - information_ref = getattr(rule, "information_ref", "") - if not _is_bound_reference(information_ref, declared_refs=declared_refs, is_unresolved=is_unresolved): - issues.append( - ParticipantBehaviorIssue( - code="participant.view-rule-ref-unbound", - participant_name="", - boundary_name=boundary_name, - ref=str(information_ref), - ) - ) - for evidence_ref in getattr(rule, "evidence_refs", []) or []: - if not _is_bound_reference(evidence_ref, declared_refs=evidence_refs, is_unresolved=is_unresolved): - issues.append( - ParticipantBehaviorIssue( - code="participant.view-rule-evidence-unbound", - participant_name="", - boundary_name=boundary_name, - ref=str(evidence_ref), - ) - ) - return issues - - -def _view_transition_visibility_issues( - *, - boundary_name: str, - boundary: object, - declared_refs: set[str], - evidence_refs: set[str], - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for transition in getattr(boundary, "view_transitions", []) or []: - information_ref = getattr(transition, "information_ref", "") - transition_id = str(getattr(transition, "transition_id", "")) - if not _is_bound_reference(information_ref, declared_refs=declared_refs, is_unresolved=is_unresolved): - issues.append( - ParticipantBehaviorIssue( - code="participant.view-transition-ref-unbound", - participant_name="", - boundary_name=boundary_name, - transition_id=transition_id, - ref=str(information_ref), - ) - ) - for evidence_ref in getattr(transition, "evidence_refs", []) or []: - if not _is_bound_reference(evidence_ref, declared_refs=evidence_refs, is_unresolved=is_unresolved): - issues.append( - ParticipantBehaviorIssue( - code="participant.view-transition-evidence-unbound", - participant_name="", - boundary_name=boundary_name, - transition_id=transition_id, - ref=str(evidence_ref), - ) - ) - return issues - - -def _visibility_issues_for_observation_boundaries( - *, - observation_boundaries: Mapping[str, object], - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for boundary_name, boundary in observation_boundaries.items(): - declared_refs = _observation_boundary_declared_refs(boundary) - evidence_refs = _observation_boundary_evidence_refs(boundary) - issues.extend( - _view_rule_visibility_issues( - boundary_name=str(boundary_name), - boundary=boundary, - declared_refs=declared_refs, - evidence_refs=evidence_refs, - is_unresolved=is_unresolved, - ) - ) - issues.extend( - _view_transition_visibility_issues( - boundary_name=str(boundary_name), - boundary=boundary, - declared_refs=declared_refs, - evidence_refs=evidence_refs, - is_unresolved=is_unresolved, - ) - ) - - return issues - - -def _behavior_mode_issue(*, spec_name: str, behavior_mode: object) -> ParticipantBehaviorIssue | None: - if not behavior_mode: - return None - try: - from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_scope_values - - validate_controlled_vocabulary_scope_values("behavior_specifications.behavior_mode", [str(behavior_mode)]) - except ValueError as exc: - return ParticipantBehaviorIssue( - code="participant.behavior-spec-mode-ungoverned", - participant_name="", - spec_name=spec_name, - ref=str(behavior_mode), - message=str(exc), - ) - return None - - -def _backend_feature_support_issue(*, spec_name: str, feature_ref: object) -> ParticipantBehaviorIssue | None: - try: - from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_value - - validation_errors: list[str] = [] - for vocabulary_id in ( - "participant-runtime-behavior-features", - "participant-runtime-interaction-features", - ): - try: - validate_controlled_vocabulary_value(vocabulary_id, str(feature_ref)) - return None - except ValueError as exc: - validation_errors.append(str(exc)) - except ValueError as exc: - validation_errors = [str(exc)] - return ParticipantBehaviorIssue( - code="participant.behavior-spec-feature-ungoverned", - participant_name="", - spec_name=spec_name, - ref=str(feature_ref), - message="; ".join(validation_errors), - ) - - -def _evidence_contract_issue(*, spec_name: str, evidence_contract_ref: object) -> ParticipantBehaviorIssue | None: - from raes_contracts.manifest_authority import ( - BACKEND_SUPPORTED_CONTRACT_IDS, - PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS, - PROCESSOR_SUPPORTED_CONTRACT_IDS, - ) - - allowed_contract_ids = frozenset( - [ - *BACKEND_SUPPORTED_CONTRACT_IDS, - *PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS, - *PROCESSOR_SUPPORTED_CONTRACT_IDS, - ] - ) - if str(evidence_contract_ref) in allowed_contract_ids: - return None - return ParticipantBehaviorIssue( - code="participant.behavior-spec-evidence-contract-unbound", - participant_name="", - spec_name=spec_name, - ref=str(evidence_contract_ref), - message="evidence_contract_refs must reference published processor, backend, or participant contracts", - ) - - -def _behavior_specification_named_ref_issues( - *, - spec_name: str, - refs: list[object], - known_names: set[str], - code: str, - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for ref in refs: - if is_unresolved(ref): - continue - if str(ref) not in known_names: - issues.append( - ParticipantBehaviorIssue( - code=code, - participant_name="", - spec_name=spec_name, - ref=str(ref), - ) - ) - return issues - - -def _behavior_specification_reference_issues( - *, - spec_name: str, - behavior_spec: object, - reference_context: _BehaviorSpecificationReferenceContext, - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - reference_sets = ( - ( - list(getattr(behavior_spec, "participant_refs", []) or []), - reference_context.participant_names, - "participant.behavior-spec-participant-unbound", - ), - ( - list(getattr(behavior_spec, "participant_role_refs", []) or []), - reference_context.participant_roles, - "participant.behavior-spec-role-unbound", - ), - ( - list(getattr(behavior_spec, "action_contract_refs", []) or []), - reference_context.action_names, - "participant.behavior-spec-action-unbound", - ), - ( - list(getattr(behavior_spec, "observation_boundary_refs", []) or []), - reference_context.observation_boundary_names, - "participant.behavior-spec-observation-boundary-unbound", - ), - ( - list(getattr(behavior_spec, "outcome_interpretation_rule_refs", []) or []), - reference_context.outcome_rule_names, - "participant.behavior-spec-outcome-rule-unbound", - ), - ) - issues: list[ParticipantBehaviorIssue] = [] - for refs, known_names, code in reference_sets: - issues.extend( - _behavior_specification_named_ref_issues( - spec_name=spec_name, - refs=refs, - known_names=known_names, - code=code, - is_unresolved=is_unresolved, - ) - ) - return issues - - -@dataclass(frozen=True) -class _AutonomousExecutionReferenceContext: - spec_name: str - behavior_spec: object - policy: object - references: _BehaviorSpecificationReferenceContext - participants: set[str] - is_unresolved: Callable[[object], bool] - - -@dataclass(frozen=True) -class _AutonomousTimeBindings: - clock: object | None - progression: object | None - cadence: object | None - cadence_count: int - - -def _autonomous_issue( - context: _AutonomousExecutionReferenceContext, - code: str, - ref: object, - *, - participant_name: str = "", - message: str = "", -) -> ParticipantBehaviorIssue: - return ParticipantBehaviorIssue( - code=code, - participant_name=participant_name, - spec_name=context.spec_name, - ref=str(ref), - message=message, - ) - - -def _autonomous_declaration_issues( - context: _AutonomousExecutionReferenceContext, -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - if not getattr(context.behavior_spec, "participant_refs", None): - issues.append( - _autonomous_issue( - context, - "participant.autonomous-explicit-participants-required", - context.spec_name, - ) - ) - required_features = { - "action_contracts", - "autonomous_execution", - "behavior_history", - "observation_boundaries", - "temporal_contracts", - } - declared_features = {str(ref) for ref in getattr(context.behavior_spec, "backend_feature_support_refs", []) or []} - for missing_feature in sorted(required_features - declared_features): - issues.append( - _autonomous_issue( - context, - "participant.autonomous-feature-requirement-missing", - missing_feature, - ) - ) - return issues - - -def _autonomous_action_issues( - context: _AutonomousExecutionReferenceContext, -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - parent_actions = {str(ref) for ref in getattr(context.behavior_spec, "action_contract_refs", []) or []} - action_candidates = getattr(context.policy, "action_candidates", None) - action_refs = ( - [candidate.action_ref for candidate in action_candidates.values()] - if action_candidates is not None - else list(context.policy.action_order) - ) - for action_ref in action_refs: - if context.is_unresolved(action_ref): - continue - if action_ref not in parent_actions: - issues.append(_autonomous_issue(context, "participant.autonomous-action-widens-parent", action_ref)) - for participant_name in sorted(context.participants): - agent = context.references.agents_by_name[participant_name] - agent_actions = {str(ref) for ref in getattr(agent, "actions", []) or []} - if action_ref not in agent_actions: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-action-outside-participant", - action_ref, - participant_name=participant_name, - ) - ) - return issues - - -def _autonomous_boundary_issues( - context: _AutonomousExecutionReferenceContext, -) -> list[ParticipantBehaviorIssue]: - boundary_ref = context.policy.observation_boundary_ref - if context.is_unresolved(boundary_ref): - return [] - issues: list[ParticipantBehaviorIssue] = [] - parent_boundaries = {str(ref) for ref in getattr(context.behavior_spec, "observation_boundary_refs", []) or []} - if boundary_ref not in parent_boundaries: - issues.append(_autonomous_issue(context, "participant.autonomous-boundary-widens-parent", boundary_ref)) - for participant_name in sorted(context.participants): - agent = context.references.agents_by_name[participant_name] - agent_boundaries = {str(ref) for ref in getattr(agent, "observation_boundaries", []) or []} - if boundary_ref not in agent_boundaries: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-boundary-outside-participant", - boundary_ref, - participant_name=participant_name, - ) - ) - return issues - - -def _autonomous_clock_binding_issues( - context: _AutonomousExecutionReferenceContext, -) -> tuple[list[ParticipantBehaviorIssue], object | None, object | None]: - policy = context.policy - clock = context.references.clocks.get(policy.clock_ref) - progression = context.references.time_progression_policies.get(policy.progression_policy_ref) - issues: list[ParticipantBehaviorIssue] = [] - if clock is None and not context.is_unresolved(policy.clock_ref): - issues.append(_autonomous_issue(context, "participant.autonomous-clock-unbound", policy.clock_ref)) - if progression is None and not context.is_unresolved(policy.progression_policy_ref): - issues.append( - _autonomous_issue(context, "participant.autonomous-progression-unbound", policy.progression_policy_ref) - ) - elif progression is not None and getattr(progression, "clock_ref", None) != policy.clock_ref: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-progression-clock-mismatch", - policy.progression_policy_ref, - ) - ) - return issues, clock, progression - - -def _autonomous_constraint_refs( - context: _AutonomousExecutionReferenceContext, - *, - activity_policy: bool, -) -> list[str]: - if activity_policy: - return [*context.policy.work_window_refs, *context.policy.pause_window_refs] - return list(context.policy.temporal_constraint_refs) - - -def _autonomous_window_subject_issues( - context: _AutonomousExecutionReferenceContext, - constraint_ref: str, - constraint: object, -) -> list[ParticipantBehaviorIssue]: - subjects = {str(ref) for ref in getattr(constraint, "subject_refs", ())} - if context.spec_name in subjects or context.participants.issubset(subjects): - return [] - return [ - _autonomous_issue( - context, - "participant.autonomous-activity-window-subject-mismatch", - constraint_ref, - ) - ] - - -def _autonomous_constraint_reference_issues( - context: _AutonomousExecutionReferenceContext, - constraint_ref: str, - *, - activity_policy: bool, -) -> tuple[list[ParticipantBehaviorIssue], object | None]: - if context.is_unresolved(constraint_ref): - return [], None - constraint_name = _resolve_section_ref( - constraint_ref, - "temporal_constraints", - context.references.temporal_constraints, - ) - constraint = context.references.temporal_constraints.get(constraint_name) if constraint_name is not None else None - if constraint is None: - return [_autonomous_issue(context, "participant.autonomous-constraint-unbound", constraint_ref)], None - - issues: list[ParticipantBehaviorIssue] = [] - kind = getattr(getattr(constraint, "constraint_kind", None), "value", "") - if activity_policy and kind != "window": - issues.append( - _autonomous_issue( - context, - "participant.autonomous-activity-window-kind-invalid", - constraint_ref, - ) - ) - if activity_policy and kind == "window": - issues.extend(_autonomous_window_subject_issues(context, constraint_ref, constraint)) - if getattr(constraint, "clock_ref", None) != context.policy.clock_ref: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-constraint-clock-mismatch", - constraint_ref, - ) - ) - return issues, constraint - - -def _autonomous_constraint_issues( - context: _AutonomousExecutionReferenceContext, -) -> tuple[list[ParticipantBehaviorIssue], object | None, int]: - issues: list[ParticipantBehaviorIssue] = [] - cadence = None - cadence_count = 0 - activity_policy = getattr(context.policy, "profile", "participant-autonomous-execution/v1") in { - "participant-autonomous-execution/v2", - "participant-autonomous-execution/v3", - } - for constraint_ref in _autonomous_constraint_refs(context, activity_policy=activity_policy): - reference_issues, constraint = _autonomous_constraint_reference_issues( - context, - constraint_ref, - activity_policy=activity_policy, - ) - issues.extend(reference_issues) - if constraint is None: - continue - kind = getattr(getattr(constraint, "constraint_kind", None), "value", "") - cadence_count += int(kind == "cadence") - if kind == "cadence": - cadence = constraint - if not activity_policy and cadence_count != 1: - issues.append(_autonomous_issue(context, "participant.autonomous-cadence-missing", context.policy.clock_ref)) - return issues, cadence, cadence_count - - -def _autonomous_time_binding_issues( - context: _AutonomousExecutionReferenceContext, -) -> tuple[list[ParticipantBehaviorIssue], _AutonomousTimeBindings]: - clock_issues, clock, progression = _autonomous_clock_binding_issues(context) - constraint_issues, cadence, cadence_count = _autonomous_constraint_issues(context) - bindings = _AutonomousTimeBindings( - clock=clock, - progression=progression, - cadence=cadence, - cadence_count=cadence_count, - ) - return [*clock_issues, *constraint_issues], bindings - - -def _autonomous_progression_issues( - context: _AutonomousExecutionReferenceContext, - bindings: _AutonomousTimeBindings, -) -> list[ParticipantBehaviorIssue]: - policy = context.policy - progression_mode = getattr(getattr(bindings.progression, "advancement_mode", None), "value", "") - clock_authority = getattr(getattr(bindings.clock, "authority_kind", None), "value", "") - issues: list[ParticipantBehaviorIssue] = [] - if progression_mode == "externally_paced": - issues.append( - _autonomous_issue( - context, - "participant.autonomous-progression-driver-unsupported", - policy.progression_policy_ref, - ) - ) - if progression_mode in {"real_time", "dilated"} and clock_authority != "runtime": - issues.append( - _autonomous_issue(context, "participant.autonomous-clock-authority-unsupported", policy.clock_ref) - ) - if bindings.cadence_count == 1 and bindings.cadence is not None: - start = getattr(bindings.cadence, "start", None) - start_tick = getattr(start, "tick", 0) if start is not None else 0 - if not isinstance(start_tick, int) or start_tick < 0: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-cadence-unreachable", - policy.progression_policy_ref, - ) - ) - issues.extend(_autonomous_stepped_cadence_issues(context, bindings, progression_mode)) - return issues - - -def _activity_timing_unreachable( - policy: object, - step_ticks: object, -) -> bool: - minimum_ticks = policy.timing.minimum_ticks - maximum_ticks = policy.timing.maximum_ticks - return not (isinstance(step_ticks, int) and not minimum_ticks % step_ticks and not maximum_ticks % step_ticks) - - -def _cadence_unreachable(bindings: _AutonomousTimeBindings, step_ticks: object) -> bool: - cadence_ticks = getattr(bindings.cadence, "cadence_ticks", None) - start = getattr(bindings.cadence, "start", None) - start_tick = getattr(start, "tick", 0) if start is not None else 0 - return not ( - isinstance(step_ticks, int) - and isinstance(cadence_ticks, int) - and start_tick >= 0 - and not start_tick % step_ticks - and not cadence_ticks % step_ticks - ) - - -def _autonomous_stepped_issue_code( - context: _AutonomousExecutionReferenceContext, - bindings: _AutonomousTimeBindings, -) -> str | None: - step_ticks = getattr(bindings.progression, "step_ticks", None) - activity_policy = getattr(context.policy, "profile", "participant-autonomous-execution/v1") in { - "participant-autonomous-execution/v2", - "participant-autonomous-execution/v3", - } - if activity_policy and _activity_timing_unreachable(context.policy, step_ticks): - return "participant.autonomous-activity-timing-unreachable" - if ( - not activity_policy - and bindings.cadence_count == 1 - and bindings.cadence is not None - and _cadence_unreachable(bindings, step_ticks) - ): - return "participant.autonomous-cadence-unreachable" - return None - - -def _autonomous_stepped_cadence_issues( - context: _AutonomousExecutionReferenceContext, - bindings: _AutonomousTimeBindings, - progression_mode: str, -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - if progression_mode == "stepped": - issue_code = _autonomous_stepped_issue_code( - context, - bindings, - ) - if issue_code is not None: - issues.append( - _autonomous_issue( - context, - issue_code, - context.policy.progression_policy_ref, - ) - ) - return issues - - -def _autonomous_non_evaluated_issues( - context: _AutonomousExecutionReferenceContext, -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for participant_name in sorted(context.participants): - role = context.references.participant_roles_by_agent.get(participant_name, "") - if role != "green": - issues.append( - _autonomous_issue( - context, - "participant.autonomous-non-evaluated-role-not-green", - role, - participant_name=participant_name, - ) - ) - has_objective = any( - getattr(objective, "agent", None) == participant_name - for objective in context.references.objectives.values() - ) - if has_objective: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-non-evaluated-objective-authority", - participant_name, - participant_name=participant_name, - ) - ) - has_widened_authority = getattr(context.behavior_spec, "outcome_interpretation_rule_refs", None) or getattr( - context.behavior_spec, "authority_scope_refs", None - ) - if has_widened_authority: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-non-evaluated-authority-widening", - context.spec_name, - ) - ) - return issues - - -def _autonomous_declared_authority_issues( - context: _AutonomousExecutionReferenceContext, -) -> list[ParticipantBehaviorIssue]: - authority = context.policy.evaluation_authority - issues: list[ParticipantBehaviorIssue] = [] - for objective_ref in authority.objective_refs: - if context.is_unresolved(objective_ref): - continue - objective_name = _resolve_section_ref(objective_ref, "objectives", context.references.objectives) - if objective_name is None: - issues.append( - _autonomous_issue( - context, - "participant.autonomous-evaluation-objective-unbound", - objective_ref, - ) - ) - unsupported_authority_refs = ( - ("proof_producer_refs", authority.proof_producer_refs), - ("score_authority_refs", authority.score_authority_refs), - ("receipt_authority_refs", authority.receipt_authority_refs), - ) - for field_name, refs in unsupported_authority_refs: - for ref in refs: - if not context.is_unresolved(ref): - issues.append( - _autonomous_issue( - context, - "participant.autonomous-evaluation-authority-namespace-unsupported", - ref, - message=field_name, - ) - ) - return issues - - -def _autonomous_evaluation_issues( - context: _AutonomousExecutionReferenceContext, -) -> list[ParticipantBehaviorIssue]: - authority_mode = getattr(context.policy.evaluation_authority.mode, "value", "") - if authority_mode == "none": - return _autonomous_non_evaluated_issues(context) - if authority_mode == "declared": - return _autonomous_declared_authority_issues(context) - return [] - - -def _autonomous_execution_reference_issues( - *, - spec_name: str, - behavior_spec: object, - reference_context: _BehaviorSpecificationReferenceContext, - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - policy = getattr(behavior_spec, "autonomous_execution", None) - if policy is None: - return [] - context = _AutonomousExecutionReferenceContext( - spec_name=spec_name, - behavior_spec=behavior_spec, - policy=policy, - references=reference_context, - participants=_tool_affordance_participants(behavior_spec, reference_context), - is_unresolved=is_unresolved, - ) - time_issues, bindings = _autonomous_time_binding_issues(context) - return [ - *_autonomous_declaration_issues(context), - *_autonomous_action_issues(context), - *_autonomous_boundary_issues(context), - *time_issues, - *_autonomous_progression_issues(context, bindings), - *_autonomous_evaluation_issues(context), - ] - - -def _resolve_section_ref( - ref: object, - section: str, - declarations: Mapping[str, object], -) -> str | None: - """Resolve a bare or section-qualified reference to one SDL declaration.""" - - if not isinstance(ref, str): - return None - if ref in declarations: - return ref - prefix = f"{section}." - candidate = ref.removeprefix(prefix) if ref.startswith(prefix) else "" - return candidate if candidate in declarations else None - - -def _tool_affordance_participants( - behavior_spec: object, - reference_context: _BehaviorSpecificationReferenceContext, -) -> set[str]: - participants = { - str(ref) - for ref in getattr(behavior_spec, "participant_refs", []) or [] - if str(ref) in reference_context.participant_names - } - role_refs = {str(ref) for ref in getattr(behavior_spec, "participant_role_refs", []) or []} - participants.update( - participant_name - for participant_name, role in reference_context.participant_roles_by_agent.items() - if role in role_refs - ) - return participants - - -def _tool_affordance_duplicate_issue( - *, - spec_name: str, - affordance_id: str, - tool_ref: object, - action_refs: list[str], - boundary_refs: list[str], - seen_relations: dict[tuple[str, tuple[str, ...], tuple[str, ...]], str], -) -> ParticipantBehaviorIssue | None: - signature = (str(tool_ref or ""), tuple(sorted(action_refs)), tuple(sorted(boundary_refs))) - duplicate_of = seen_relations.get(signature) - if duplicate_of is None: - seen_relations[signature] = affordance_id - return None - return ParticipantBehaviorIssue( - code="participant.tool-affordance-duplicate-relation", - participant_name="", - spec_name=spec_name, - ref=affordance_id, - message=duplicate_of, - ) - - -def _tool_affordance_action_issues( - *, - spec_name: str, - affordance_id: str, - action_ref: str, - parent_actions: set[str], - participants: set[str], - agents_by_name: Mapping[str, object], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - if action_ref not in parent_actions: - issues.append( - ParticipantBehaviorIssue( - code="participant.tool-affordance-action-widens-parent", - participant_name="", - spec_name=spec_name, - ref=action_ref, - action_name=affordance_id, - ) - ) - for participant_name in sorted(participants): - agent_actions = {str(ref) for ref in getattr(agents_by_name[participant_name], "actions", []) or []} - if action_ref not in agent_actions: - issues.append( - ParticipantBehaviorIssue( - code="participant.tool-affordance-action-outside-participant", - participant_name=participant_name, - spec_name=spec_name, - ref=action_ref, - action_name=affordance_id, - ) - ) - return issues - - -def _tool_affordance_boundary_issues( - *, - spec_name: str, - affordance_id: str, - boundary_ref: str, - parent_boundaries: set[str], - participants: set[str], - agents_by_name: Mapping[str, object], - observation_boundaries: Mapping[str, object], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - if boundary_ref not in parent_boundaries: - issues.append( - ParticipantBehaviorIssue( - code="participant.tool-affordance-boundary-widens-parent", - participant_name="", - spec_name=spec_name, - ref=boundary_ref, - action_name=affordance_id, - ) - ) - for participant_name in sorted(participants): - agent_boundaries = { - str(ref) for ref in getattr(agents_by_name[participant_name], "observation_boundaries", []) or [] - } - if boundary_ref not in agent_boundaries: - issues.append( - ParticipantBehaviorIssue( - code="participant.tool-affordance-boundary-outside-participant", - participant_name=participant_name, - spec_name=spec_name, - ref=boundary_ref, - action_name=affordance_id, - ) - ) - boundary = observation_boundaries.get(boundary_ref) - if boundary is None: - return issues - binding_ref = tool_affordance_reference(spec_name, affordance_id) - declared_refs = _observation_boundary_declared_refs(boundary) - view_rule_refs = {str(getattr(rule, "information_ref", "")) for rule in getattr(boundary, "view_rules", []) or []} - if binding_ref not in declared_refs or binding_ref not in view_rule_refs: - issues.append( - ParticipantBehaviorIssue( - code="participant.tool-affordance-view-unclassified", - participant_name="", - spec_name=spec_name, - ref=binding_ref, - action_name=affordance_id, - boundary_name=boundary_ref, - ) - ) - return issues - - -def _resolved_reference_issues( - refs: list[str], - is_unresolved: Callable[[object], bool], - build_issues: Callable[[str], list[ParticipantBehaviorIssue]], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for ref in refs: - if not is_unresolved(ref): - issues.extend(build_issues(ref)) - return issues - - -def _tool_affordance_reference_issues( - *, - spec_name: str, - behavior_spec: object, - agents_by_name: Mapping[str, object], - observation_boundaries: Mapping[str, object], - reference_context: _BehaviorSpecificationReferenceContext, - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - parent_actions = {str(ref) for ref in getattr(behavior_spec, "action_contract_refs", []) or []} - parent_boundaries = {str(ref) for ref in getattr(behavior_spec, "observation_boundary_refs", []) or []} - participants = _tool_affordance_participants(behavior_spec, reference_context) - seen_relations: dict[tuple[str, tuple[str, ...], tuple[str, ...]], str] = {} - for affordance_id, binding in getattr(behavior_spec, "tool_affordances", {}).items(): - affordance_id = str(affordance_id) - action_refs = [str(ref) for ref in getattr(binding, "action_contract_refs", []) or []] - boundary_refs = [str(ref) for ref in getattr(binding, "observation_boundary_refs", []) or []] - duplicate_issue = _tool_affordance_duplicate_issue( - spec_name=spec_name, - affordance_id=affordance_id, - tool_ref=getattr(binding, "tool_ref", None), - action_refs=action_refs, - boundary_refs=boundary_refs, - seen_relations=seen_relations, - ) - if duplicate_issue is not None: - issues.append(duplicate_issue) - - issues.extend( - _resolved_reference_issues( - action_refs, - is_unresolved, - lambda action_ref, affordance_id=affordance_id: _tool_affordance_action_issues( - spec_name=spec_name, - affordance_id=affordance_id, - action_ref=action_ref, - parent_actions=parent_actions, - participants=participants, - agents_by_name=agents_by_name, - ), - ) - ) - issues.extend( - _resolved_reference_issues( - boundary_refs, - is_unresolved, - lambda boundary_ref, affordance_id=affordance_id: _tool_affordance_boundary_issues( - spec_name=spec_name, - affordance_id=affordance_id, - boundary_ref=boundary_ref, - parent_boundaries=parent_boundaries, - participants=participants, - agents_by_name=agents_by_name, - observation_boundaries=observation_boundaries, - ), - ) - ) - return issues - - -def _behavior_specification_feature_issues( - *, - spec_name: str, - behavior_spec: object, - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for feature_ref in getattr(behavior_spec, "backend_feature_support_refs", []) or []: - if is_unresolved(feature_ref): - continue - feature_issue = _backend_feature_support_issue(spec_name=spec_name, feature_ref=feature_ref) - if feature_issue is not None: - issues.append(feature_issue) - return issues - - -def _behavior_specification_evidence_contract_issues( - *, - spec_name: str, - behavior_spec: object, - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for evidence_contract_ref in getattr(behavior_spec, "evidence_contract_refs", []) or []: - if is_unresolved(evidence_contract_ref): - continue - evidence_issue = _evidence_contract_issue( - spec_name=spec_name, - evidence_contract_ref=evidence_contract_ref, - ) - if evidence_issue is not None: - issues.append(evidence_issue) - return issues - - -def _behavior_specification_vocabulary_issues( - *, - spec_name: str, - behavior_spec: object, - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - mode_issue = _behavior_mode_issue( - spec_name=spec_name, - behavior_mode=getattr(behavior_spec, "behavior_mode", None), - ) - if mode_issue is not None: - issues.append(mode_issue) - from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_scope_values - - for field_name, scope, code in ( - ( - "ai_offensive_behavior_refs", - "behavior_specifications.ai_offensive_behavior_refs", - "participant.behavior-spec-ai-offensive-behavior-ungoverned", - ), - ( - "defensive_behavior_refs", - "behavior_specifications.defensive_behavior_refs", - "participant.behavior-spec-defensive-behavior-ungoverned", - ), - ( - "offensive_behavior_refs", - "behavior_specifications.offensive_behavior_refs", - "participant.behavior-spec-offensive-behavior-ungoverned", - ), - ): - for ref in getattr(behavior_spec, field_name, []) or []: - if is_unresolved(ref): - continue - try: - validate_controlled_vocabulary_scope_values(scope, [str(ref)]) - except ValueError as exc: - issues.append( - ParticipantBehaviorIssue( - code=code, - participant_name="", - spec_name=spec_name, - ref=str(ref), - message=str(exc), - ) - ) - issues.extend( - _behavior_specification_feature_issues( - spec_name=spec_name, - behavior_spec=behavior_spec, - is_unresolved=is_unresolved, - ) - ) - issues.extend( - _behavior_specification_evidence_contract_issues( - spec_name=spec_name, - behavior_spec=behavior_spec, - is_unresolved=is_unresolved, - ) - ) - return issues - - -def _behavior_specification_issues( - behavior_specifications: Mapping[str, object], - reference_context: _BehaviorSpecificationReferenceContext, - is_unresolved: Callable[[object], bool], -) -> list[ParticipantBehaviorIssue]: - issues: list[ParticipantBehaviorIssue] = [] - for spec_name, behavior_spec in behavior_specifications.items(): - normalized_spec_name = str(spec_name) - issues.extend( - _behavior_specification_reference_issues( - spec_name=normalized_spec_name, - behavior_spec=behavior_spec, - reference_context=reference_context, - is_unresolved=is_unresolved, - ) - ) - issues.extend( - _behavior_specification_vocabulary_issues( - spec_name=normalized_spec_name, - behavior_spec=behavior_spec, - is_unresolved=is_unresolved, - ) - ) - issues.extend( - _autonomous_execution_reference_issues( - spec_name=normalized_spec_name, - behavior_spec=behavior_spec, - reference_context=reference_context, - is_unresolved=is_unresolved, - ) - ) - issues.extend( - _tool_affordance_reference_issues( - spec_name=normalized_spec_name, - behavior_spec=behavior_spec, - agents_by_name=reference_context.agents_by_name, - observation_boundaries=reference_context.observation_boundaries, - reference_context=reference_context, - is_unresolved=is_unresolved, - ) - ) - autonomous_owner_by_participant: dict[str, str] = {} - for spec_name, behavior_spec in behavior_specifications.items(): - if getattr(behavior_spec, "autonomous_execution", None) is None: - continue - participant_names = { - str(ref) - for ref in getattr(behavior_spec, "participant_refs", []) or [] - if str(ref) in reference_context.participant_names - } - for participant_name in sorted(participant_names): - prior_owner = autonomous_owner_by_participant.setdefault(participant_name, str(spec_name)) - if prior_owner != str(spec_name): - issues.append( - ParticipantBehaviorIssue( - code="participant.autonomous-participant-owner-conflict", - participant_name=participant_name, - spec_name=str(spec_name), - ref=prior_owner, - ) - ) - return issues - - -@dataclass(frozen=True) -class _ParticipantBehaviorSemanticRegistries: - participant_roles_by_agent: Mapping[str, str] - outcome_interpretation_rules: Mapping[str, object] - clocks: Mapping[str, object] - time_progression_policies: Mapping[str, object] - temporal_constraints: Mapping[str, object] - objectives: Mapping[str, object] - - @classmethod - def from_keywords( - cls, - semantic_registries: Mapping[str, object], - ) -> _ParticipantBehaviorSemanticRegistries: - expected = { - "participant_roles_by_agent", - "outcome_interpretation_rules", - "clocks", - "time_progression_policies", - "temporal_constraints", - "objectives", - } - missing = expected - semantic_registries.keys() - unexpected = semantic_registries.keys() - expected - if missing or unexpected: - details = [] - if missing: - details.append(f"missing {', '.join(sorted(missing))}") - if unexpected: - details.append(f"unexpected {', '.join(sorted(unexpected))}") - raise TypeError("invalid participant behavior semantic registries: " + "; ".join(details)) - return cls( - participant_roles_by_agent=semantic_registries["participant_roles_by_agent"], - outcome_interpretation_rules=semantic_registries["outcome_interpretation_rules"], - clocks=semantic_registries["clocks"], - time_progression_policies=semantic_registries["time_progression_policies"], - temporal_constraints=semantic_registries["temporal_constraints"], - objectives=semantic_registries["objectives"], - ) - - -def _behavior_reference_context( - agents_by_name: Mapping[str, object], - action_contracts: Mapping[str, object], - observation_boundaries: Mapping[str, object], - registries: _ParticipantBehaviorSemanticRegistries, -) -> _BehaviorSpecificationReferenceContext: - return _BehaviorSpecificationReferenceContext( - participant_names={str(name) for name in agents_by_name}, - participant_roles_by_agent=registries.participant_roles_by_agent, - action_names={str(name) for name in action_contracts}, - observation_boundary_names={str(name) for name in observation_boundaries}, - outcome_rule_names={str(name) for name in registries.outcome_interpretation_rules}, - agents_by_name=agents_by_name, - action_contracts=action_contracts, - observation_boundaries=observation_boundaries, - clocks=registries.clocks, - time_progression_policies=registries.time_progression_policies, - temporal_constraints=registries.temporal_constraints, - objectives=registries.objectives, - ) - - -def analyze_participant_behavior( - *, - agents_by_name: Mapping[str, object], - action_contracts: Mapping[str, object], - observation_boundaries: Mapping[str, object], - behavior_specifications: Mapping[str, object], - is_unresolved: Callable[[object], bool], - **semantic_registries: object, -) -> ParticipantBehaviorAnalysis: - """Validate and normalize participant action/observation references. - - ``agents.*.actions`` can stay as a legacy authoring affordance when no - action-contract registry exists. Once a scenario declares - ``action_contracts``, every authored action name must resolve to that - governed registry so the compiler never treats raw names as behavior - semantics. - """ - - references: list[ParticipantBehaviorReference] = [] - issues: list[ParticipantBehaviorIssue] = [] - registries = _ParticipantBehaviorSemanticRegistries.from_keywords(semantic_registries) - reference_context = _behavior_reference_context( - agents_by_name, - action_contracts, - observation_boundaries, - registries, - ) - - for participant_name, agent in agents_by_name.items(): - action_references, action_issues = _action_references_for_agent( - participant_name=participant_name, - action_names=list(getattr(agent, "actions", []) or []), - action_contracts=action_contracts, - is_unresolved=is_unresolved, - ) - boundary_references, boundary_issues = _observation_boundary_references_for_agent( - participant_name=participant_name, - boundary_names=list(getattr(agent, "observation_boundaries", []) or []), - observation_boundaries=observation_boundaries, - is_unresolved=is_unresolved, - ) - references.extend(action_references) - references.extend(boundary_references) - issues.extend(action_issues) - issues.extend(boundary_issues) - - issues.extend( - _interaction_references_for_action_contracts( - action_contracts=action_contracts, - is_unresolved=is_unresolved, - ) - ) - issues.extend( - _visibility_issues_for_observation_boundaries( - observation_boundaries=observation_boundaries, - is_unresolved=is_unresolved, - ) - ) - issues.extend( - _behavior_specification_issues( - behavior_specifications, - reference_context, - is_unresolved, - ) - ) - - return ParticipantBehaviorAnalysis(references=tuple(references), issues=tuple(issues)) diff --git a/implementations/python/packages/raes/semantics/participant_behavior/__init__.py b/implementations/python/packages/raes/semantics/participant_behavior/__init__.py new file mode 100644 index 000000000..1442a920b --- /dev/null +++ b/implementations/python/packages/raes/semantics/participant_behavior/__init__.py @@ -0,0 +1,10 @@ +"""Name-level participant behavior semantics (SEM-208/209/210).""" + +from __future__ import annotations + +from ._analysis import analyze_participant_behavior +from ._types import ( + ParticipantBehaviorAnalysis, + ParticipantBehaviorIssue, + ParticipantBehaviorReference, +) diff --git a/implementations/python/packages/raes/semantics/participant_behavior/_analysis.py b/implementations/python/packages/raes/semantics/participant_behavior/_analysis.py new file mode 100644 index 000000000..3104b5f52 --- /dev/null +++ b/implementations/python/packages/raes/semantics/participant_behavior/_analysis.py @@ -0,0 +1,221 @@ +"""Behavior-specification aggregation, semantic-registry binding, and the top-level analyzer.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import dataclass + +from ._autonomous import _autonomous_execution_reference_issues +from ._behavior_spec import ( + _behavior_specification_reference_issues, + _behavior_specification_vocabulary_issues, +) +from ._references import ( + _action_references_for_agent, + _interaction_references_for_action_contracts, + _observation_boundary_references_for_agent, + _visibility_issues_for_observation_boundaries, +) +from ._tool_affordance import _tool_affordance_reference_issues +from ._types import ( + ParticipantBehaviorAnalysis, + ParticipantBehaviorIssue, + ParticipantBehaviorReference, + _BehaviorSpecificationReferenceContext, +) + + +def _behavior_specification_issues( + behavior_specifications: Mapping[str, object], + reference_context: _BehaviorSpecificationReferenceContext, + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for spec_name, behavior_spec in behavior_specifications.items(): + normalized_spec_name = str(spec_name) + issues.extend( + _behavior_specification_reference_issues( + spec_name=normalized_spec_name, + behavior_spec=behavior_spec, + reference_context=reference_context, + is_unresolved=is_unresolved, + ) + ) + issues.extend( + _behavior_specification_vocabulary_issues( + spec_name=normalized_spec_name, + behavior_spec=behavior_spec, + is_unresolved=is_unresolved, + ) + ) + issues.extend( + _autonomous_execution_reference_issues( + spec_name=normalized_spec_name, + behavior_spec=behavior_spec, + reference_context=reference_context, + is_unresolved=is_unresolved, + ) + ) + issues.extend( + _tool_affordance_reference_issues( + spec_name=normalized_spec_name, + behavior_spec=behavior_spec, + agents_by_name=reference_context.agents_by_name, + observation_boundaries=reference_context.observation_boundaries, + reference_context=reference_context, + is_unresolved=is_unresolved, + ) + ) + autonomous_owner_by_participant: dict[str, str] = {} + for spec_name, behavior_spec in behavior_specifications.items(): + if getattr(behavior_spec, "autonomous_execution", None) is None: + continue + participant_names = { + str(ref) + for ref in getattr(behavior_spec, "participant_refs", []) or [] + if str(ref) in reference_context.participant_names + } + for participant_name in sorted(participant_names): + prior_owner = autonomous_owner_by_participant.setdefault(participant_name, str(spec_name)) + if prior_owner != str(spec_name): + issues.append( + ParticipantBehaviorIssue( + code="participant.autonomous-participant-owner-conflict", + participant_name=participant_name, + spec_name=str(spec_name), + ref=prior_owner, + ) + ) + return issues + + +@dataclass(frozen=True) +class _ParticipantBehaviorSemanticRegistries: + participant_roles_by_agent: Mapping[str, str] + outcome_interpretation_rules: Mapping[str, object] + clocks: Mapping[str, object] + time_progression_policies: Mapping[str, object] + temporal_constraints: Mapping[str, object] + objectives: Mapping[str, object] + + @classmethod + def from_keywords( + cls, + semantic_registries: Mapping[str, object], + ) -> _ParticipantBehaviorSemanticRegistries: + expected = { + "participant_roles_by_agent", + "outcome_interpretation_rules", + "clocks", + "time_progression_policies", + "temporal_constraints", + "objectives", + } + missing = expected - semantic_registries.keys() + unexpected = semantic_registries.keys() - expected + if missing or unexpected: + details = [] + if missing: + details.append(f"missing {', '.join(sorted(missing))}") + if unexpected: + details.append(f"unexpected {', '.join(sorted(unexpected))}") + raise TypeError("invalid participant behavior semantic registries: " + "; ".join(details)) + return cls( + participant_roles_by_agent=semantic_registries["participant_roles_by_agent"], + outcome_interpretation_rules=semantic_registries["outcome_interpretation_rules"], + clocks=semantic_registries["clocks"], + time_progression_policies=semantic_registries["time_progression_policies"], + temporal_constraints=semantic_registries["temporal_constraints"], + objectives=semantic_registries["objectives"], + ) + + +def _behavior_reference_context( + agents_by_name: Mapping[str, object], + action_contracts: Mapping[str, object], + observation_boundaries: Mapping[str, object], + registries: _ParticipantBehaviorSemanticRegistries, +) -> _BehaviorSpecificationReferenceContext: + return _BehaviorSpecificationReferenceContext( + participant_names={str(name) for name in agents_by_name}, + participant_roles_by_agent=registries.participant_roles_by_agent, + action_names={str(name) for name in action_contracts}, + observation_boundary_names={str(name) for name in observation_boundaries}, + outcome_rule_names={str(name) for name in registries.outcome_interpretation_rules}, + agents_by_name=agents_by_name, + action_contracts=action_contracts, + observation_boundaries=observation_boundaries, + clocks=registries.clocks, + time_progression_policies=registries.time_progression_policies, + temporal_constraints=registries.temporal_constraints, + objectives=registries.objectives, + ) + + +def analyze_participant_behavior( + *, + agents_by_name: Mapping[str, object], + action_contracts: Mapping[str, object], + observation_boundaries: Mapping[str, object], + behavior_specifications: Mapping[str, object], + is_unresolved: Callable[[object], bool], + **semantic_registries: object, +) -> ParticipantBehaviorAnalysis: + """Validate and normalize participant action/observation references. + + ``agents.*.actions`` can stay as a legacy authoring affordance when no + action-contract registry exists. Once a scenario declares + ``action_contracts``, every authored action name must resolve to that + governed registry so the compiler never treats raw names as behavior + semantics. + """ + + references: list[ParticipantBehaviorReference] = [] + issues: list[ParticipantBehaviorIssue] = [] + registries = _ParticipantBehaviorSemanticRegistries.from_keywords(semantic_registries) + reference_context = _behavior_reference_context( + agents_by_name, + action_contracts, + observation_boundaries, + registries, + ) + + for participant_name, agent in agents_by_name.items(): + action_references, action_issues = _action_references_for_agent( + participant_name=participant_name, + action_names=list(getattr(agent, "actions", []) or []), + action_contracts=action_contracts, + is_unresolved=is_unresolved, + ) + boundary_references, boundary_issues = _observation_boundary_references_for_agent( + participant_name=participant_name, + boundary_names=list(getattr(agent, "observation_boundaries", []) or []), + observation_boundaries=observation_boundaries, + is_unresolved=is_unresolved, + ) + references.extend(action_references) + references.extend(boundary_references) + issues.extend(action_issues) + issues.extend(boundary_issues) + + issues.extend( + _interaction_references_for_action_contracts( + action_contracts=action_contracts, + is_unresolved=is_unresolved, + ) + ) + issues.extend( + _visibility_issues_for_observation_boundaries( + observation_boundaries=observation_boundaries, + is_unresolved=is_unresolved, + ) + ) + issues.extend( + _behavior_specification_issues( + behavior_specifications, + reference_context, + is_unresolved, + ) + ) + + return ParticipantBehaviorAnalysis(references=tuple(references), issues=tuple(issues)) diff --git a/implementations/python/packages/raes/semantics/participant_behavior/_autonomous.py b/implementations/python/packages/raes/semantics/participant_behavior/_autonomous.py new file mode 100644 index 000000000..970915a99 --- /dev/null +++ b/implementations/python/packages/raes/semantics/participant_behavior/_autonomous.py @@ -0,0 +1,238 @@ +"""Autonomous-execution progression, cadence, evaluation-authority, and reference-issue orchestration.""" + +from __future__ import annotations + +from collections.abc import Callable + +from ._autonomous_bindings import ( + _autonomous_action_issues, + _autonomous_boundary_issues, + _autonomous_declaration_issues, + _autonomous_issue, + _autonomous_time_binding_issues, + _AutonomousExecutionReferenceContext, + _AutonomousTimeBindings, +) +from ._references import _resolve_section_ref, _tool_affordance_participants +from ._types import ( + ParticipantBehaviorIssue, + _BehaviorSpecificationReferenceContext, +) + + +def _autonomous_progression_issues( + context: _AutonomousExecutionReferenceContext, + bindings: _AutonomousTimeBindings, +) -> list[ParticipantBehaviorIssue]: + policy = context.policy + progression_mode = getattr(getattr(bindings.progression, "advancement_mode", None), "value", "") + clock_authority = getattr(getattr(bindings.clock, "authority_kind", None), "value", "") + issues: list[ParticipantBehaviorIssue] = [] + if progression_mode == "externally_paced": + issues.append( + _autonomous_issue( + context, + "participant.autonomous-progression-driver-unsupported", + policy.progression_policy_ref, + ) + ) + if progression_mode in {"real_time", "dilated"} and clock_authority != "runtime": + issues.append( + _autonomous_issue(context, "participant.autonomous-clock-authority-unsupported", policy.clock_ref) + ) + if bindings.cadence_count == 1 and bindings.cadence is not None: + start = getattr(bindings.cadence, "start", None) + start_tick = getattr(start, "tick", 0) if start is not None else 0 + if not isinstance(start_tick, int) or start_tick < 0: + issues.append( + _autonomous_issue( + context, + "participant.autonomous-cadence-unreachable", + policy.progression_policy_ref, + ) + ) + issues.extend(_autonomous_stepped_cadence_issues(context, bindings, progression_mode)) + return issues + + +def _activity_timing_unreachable( + policy: object, + step_ticks: object, +) -> bool: + minimum_ticks = policy.timing.minimum_ticks + maximum_ticks = policy.timing.maximum_ticks + return not (isinstance(step_ticks, int) and not minimum_ticks % step_ticks and not maximum_ticks % step_ticks) + + +def _cadence_unreachable(bindings: _AutonomousTimeBindings, step_ticks: object) -> bool: + cadence_ticks = getattr(bindings.cadence, "cadence_ticks", None) + start = getattr(bindings.cadence, "start", None) + start_tick = getattr(start, "tick", 0) if start is not None else 0 + return not ( + isinstance(step_ticks, int) + and isinstance(cadence_ticks, int) + and start_tick >= 0 + and not start_tick % step_ticks + and not cadence_ticks % step_ticks + ) + + +def _autonomous_stepped_issue_code( + context: _AutonomousExecutionReferenceContext, + bindings: _AutonomousTimeBindings, +) -> str | None: + step_ticks = getattr(bindings.progression, "step_ticks", None) + activity_policy = getattr(context.policy, "profile", "participant-autonomous-execution/v1") in { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + } + if activity_policy and _activity_timing_unreachable(context.policy, step_ticks): + return "participant.autonomous-activity-timing-unreachable" + if ( + not activity_policy + and bindings.cadence_count == 1 + and bindings.cadence is not None + and _cadence_unreachable(bindings, step_ticks) + ): + return "participant.autonomous-cadence-unreachable" + return None + + +def _autonomous_stepped_cadence_issues( + context: _AutonomousExecutionReferenceContext, + bindings: _AutonomousTimeBindings, + progression_mode: str, +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + if progression_mode == "stepped": + issue_code = _autonomous_stepped_issue_code( + context, + bindings, + ) + if issue_code is not None: + issues.append( + _autonomous_issue( + context, + issue_code, + context.policy.progression_policy_ref, + ) + ) + return issues + + +def _autonomous_non_evaluated_issues( + context: _AutonomousExecutionReferenceContext, +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for participant_name in sorted(context.participants): + role = context.references.participant_roles_by_agent.get(participant_name, "") + if role != "green": + issues.append( + _autonomous_issue( + context, + "participant.autonomous-non-evaluated-role-not-green", + role, + participant_name=participant_name, + ) + ) + has_objective = any( + getattr(objective, "agent", None) == participant_name + for objective in context.references.objectives.values() + ) + if has_objective: + issues.append( + _autonomous_issue( + context, + "participant.autonomous-non-evaluated-objective-authority", + participant_name, + participant_name=participant_name, + ) + ) + has_widened_authority = getattr(context.behavior_spec, "outcome_interpretation_rule_refs", None) or getattr( + context.behavior_spec, "authority_scope_refs", None + ) + if has_widened_authority: + issues.append( + _autonomous_issue( + context, + "participant.autonomous-non-evaluated-authority-widening", + context.spec_name, + ) + ) + return issues + + +def _autonomous_declared_authority_issues( + context: _AutonomousExecutionReferenceContext, +) -> list[ParticipantBehaviorIssue]: + authority = context.policy.evaluation_authority + issues: list[ParticipantBehaviorIssue] = [] + for objective_ref in authority.objective_refs: + if context.is_unresolved(objective_ref): + continue + objective_name = _resolve_section_ref(objective_ref, "objectives", context.references.objectives) + if objective_name is None: + issues.append( + _autonomous_issue( + context, + "participant.autonomous-evaluation-objective-unbound", + objective_ref, + ) + ) + unsupported_authority_refs = ( + ("proof_producer_refs", authority.proof_producer_refs), + ("score_authority_refs", authority.score_authority_refs), + ("receipt_authority_refs", authority.receipt_authority_refs), + ) + for field_name, refs in unsupported_authority_refs: + for ref in refs: + if not context.is_unresolved(ref): + issues.append( + _autonomous_issue( + context, + "participant.autonomous-evaluation-authority-namespace-unsupported", + ref, + message=field_name, + ) + ) + return issues + + +def _autonomous_evaluation_issues( + context: _AutonomousExecutionReferenceContext, +) -> list[ParticipantBehaviorIssue]: + authority_mode = getattr(context.policy.evaluation_authority.mode, "value", "") + if authority_mode == "none": + return _autonomous_non_evaluated_issues(context) + if authority_mode == "declared": + return _autonomous_declared_authority_issues(context) + return [] + + +def _autonomous_execution_reference_issues( + *, + spec_name: str, + behavior_spec: object, + reference_context: _BehaviorSpecificationReferenceContext, + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + policy = getattr(behavior_spec, "autonomous_execution", None) + if policy is None: + return [] + context = _AutonomousExecutionReferenceContext( + spec_name=spec_name, + behavior_spec=behavior_spec, + policy=policy, + references=reference_context, + participants=_tool_affordance_participants(behavior_spec, reference_context), + is_unresolved=is_unresolved, + ) + time_issues, bindings = _autonomous_time_binding_issues(context) + return [ + *_autonomous_declaration_issues(context), + *_autonomous_action_issues(context), + *_autonomous_boundary_issues(context), + *time_issues, + *_autonomous_progression_issues(context, bindings), + *_autonomous_evaluation_issues(context), + ] diff --git a/implementations/python/packages/raes/semantics/participant_behavior/_autonomous_bindings.py b/implementations/python/packages/raes/semantics/participant_behavior/_autonomous_bindings.py new file mode 100644 index 000000000..c54e3f3a4 --- /dev/null +++ b/implementations/python/packages/raes/semantics/participant_behavior/_autonomous_bindings.py @@ -0,0 +1,267 @@ +"""Autonomous-execution reference context, clock/constraint binding, and time-binding checks.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from ._references import _resolve_section_ref +from ._types import ( + ParticipantBehaviorIssue, + _BehaviorSpecificationReferenceContext, +) + + +@dataclass(frozen=True) +class _AutonomousExecutionReferenceContext: + spec_name: str + behavior_spec: object + policy: object + references: _BehaviorSpecificationReferenceContext + participants: set[str] + is_unresolved: Callable[[object], bool] + + +@dataclass(frozen=True) +class _AutonomousTimeBindings: + clock: object | None + progression: object | None + cadence: object | None + cadence_count: int + + +def _autonomous_issue( + context: _AutonomousExecutionReferenceContext, + code: str, + ref: object, + *, + participant_name: str = "", + message: str = "", +) -> ParticipantBehaviorIssue: + return ParticipantBehaviorIssue( + code=code, + participant_name=participant_name, + spec_name=context.spec_name, + ref=str(ref), + message=message, + ) + + +def _autonomous_declaration_issues( + context: _AutonomousExecutionReferenceContext, +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + if not getattr(context.behavior_spec, "participant_refs", None): + issues.append( + _autonomous_issue( + context, + "participant.autonomous-explicit-participants-required", + context.spec_name, + ) + ) + required_features = { + "action_contracts", + "autonomous_execution", + "behavior_history", + "observation_boundaries", + "temporal_contracts", + } + declared_features = {str(ref) for ref in getattr(context.behavior_spec, "backend_feature_support_refs", []) or []} + for missing_feature in sorted(required_features - declared_features): + issues.append( + _autonomous_issue( + context, + "participant.autonomous-feature-requirement-missing", + missing_feature, + ) + ) + return issues + + +def _autonomous_action_issues( + context: _AutonomousExecutionReferenceContext, +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + parent_actions = {str(ref) for ref in getattr(context.behavior_spec, "action_contract_refs", []) or []} + action_candidates = getattr(context.policy, "action_candidates", None) + action_refs = ( + [candidate.action_ref for candidate in action_candidates.values()] + if action_candidates is not None + else list(context.policy.action_order) + ) + for action_ref in action_refs: + if context.is_unresolved(action_ref): + continue + if action_ref not in parent_actions: + issues.append(_autonomous_issue(context, "participant.autonomous-action-widens-parent", action_ref)) + for participant_name in sorted(context.participants): + agent = context.references.agents_by_name[participant_name] + agent_actions = {str(ref) for ref in getattr(agent, "actions", []) or []} + if action_ref not in agent_actions: + issues.append( + _autonomous_issue( + context, + "participant.autonomous-action-outside-participant", + action_ref, + participant_name=participant_name, + ) + ) + return issues + + +def _autonomous_boundary_issues( + context: _AutonomousExecutionReferenceContext, +) -> list[ParticipantBehaviorIssue]: + boundary_ref = context.policy.observation_boundary_ref + if context.is_unresolved(boundary_ref): + return [] + issues: list[ParticipantBehaviorIssue] = [] + parent_boundaries = {str(ref) for ref in getattr(context.behavior_spec, "observation_boundary_refs", []) or []} + if boundary_ref not in parent_boundaries: + issues.append(_autonomous_issue(context, "participant.autonomous-boundary-widens-parent", boundary_ref)) + for participant_name in sorted(context.participants): + agent = context.references.agents_by_name[participant_name] + agent_boundaries = {str(ref) for ref in getattr(agent, "observation_boundaries", []) or []} + if boundary_ref not in agent_boundaries: + issues.append( + _autonomous_issue( + context, + "participant.autonomous-boundary-outside-participant", + boundary_ref, + participant_name=participant_name, + ) + ) + return issues + + +def _autonomous_clock_binding_issues( + context: _AutonomousExecutionReferenceContext, +) -> tuple[list[ParticipantBehaviorIssue], object | None, object | None]: + policy = context.policy + clock = context.references.clocks.get(policy.clock_ref) + progression = context.references.time_progression_policies.get(policy.progression_policy_ref) + issues: list[ParticipantBehaviorIssue] = [] + if clock is None and not context.is_unresolved(policy.clock_ref): + issues.append(_autonomous_issue(context, "participant.autonomous-clock-unbound", policy.clock_ref)) + if progression is None and not context.is_unresolved(policy.progression_policy_ref): + issues.append( + _autonomous_issue(context, "participant.autonomous-progression-unbound", policy.progression_policy_ref) + ) + elif progression is not None and getattr(progression, "clock_ref", None) != policy.clock_ref: + issues.append( + _autonomous_issue( + context, + "participant.autonomous-progression-clock-mismatch", + policy.progression_policy_ref, + ) + ) + return issues, clock, progression + + +def _autonomous_constraint_refs( + context: _AutonomousExecutionReferenceContext, + *, + activity_policy: bool, +) -> list[str]: + if activity_policy: + return [*context.policy.work_window_refs, *context.policy.pause_window_refs] + return list(context.policy.temporal_constraint_refs) + + +def _autonomous_window_subject_issues( + context: _AutonomousExecutionReferenceContext, + constraint_ref: str, + constraint: object, +) -> list[ParticipantBehaviorIssue]: + subjects = {str(ref) for ref in getattr(constraint, "subject_refs", ())} + if context.spec_name in subjects or context.participants.issubset(subjects): + return [] + return [ + _autonomous_issue( + context, + "participant.autonomous-activity-window-subject-mismatch", + constraint_ref, + ) + ] + + +def _autonomous_constraint_reference_issues( + context: _AutonomousExecutionReferenceContext, + constraint_ref: str, + *, + activity_policy: bool, +) -> tuple[list[ParticipantBehaviorIssue], object | None]: + if context.is_unresolved(constraint_ref): + return [], None + constraint_name = _resolve_section_ref( + constraint_ref, + "temporal_constraints", + context.references.temporal_constraints, + ) + constraint = context.references.temporal_constraints.get(constraint_name) if constraint_name is not None else None + if constraint is None: + return [_autonomous_issue(context, "participant.autonomous-constraint-unbound", constraint_ref)], None + + issues: list[ParticipantBehaviorIssue] = [] + kind = getattr(getattr(constraint, "constraint_kind", None), "value", "") + if activity_policy and kind != "window": + issues.append( + _autonomous_issue( + context, + "participant.autonomous-activity-window-kind-invalid", + constraint_ref, + ) + ) + if activity_policy and kind == "window": + issues.extend(_autonomous_window_subject_issues(context, constraint_ref, constraint)) + if getattr(constraint, "clock_ref", None) != context.policy.clock_ref: + issues.append( + _autonomous_issue( + context, + "participant.autonomous-constraint-clock-mismatch", + constraint_ref, + ) + ) + return issues, constraint + + +def _autonomous_constraint_issues( + context: _AutonomousExecutionReferenceContext, +) -> tuple[list[ParticipantBehaviorIssue], object | None, int]: + issues: list[ParticipantBehaviorIssue] = [] + cadence = None + cadence_count = 0 + activity_policy = getattr(context.policy, "profile", "participant-autonomous-execution/v1") in { + "participant-autonomous-execution/v2", + "participant-autonomous-execution/v3", + } + for constraint_ref in _autonomous_constraint_refs(context, activity_policy=activity_policy): + reference_issues, constraint = _autonomous_constraint_reference_issues( + context, + constraint_ref, + activity_policy=activity_policy, + ) + issues.extend(reference_issues) + if constraint is None: + continue + kind = getattr(getattr(constraint, "constraint_kind", None), "value", "") + cadence_count += int(kind == "cadence") + if kind == "cadence": + cadence = constraint + if not activity_policy and cadence_count != 1: + issues.append(_autonomous_issue(context, "participant.autonomous-cadence-missing", context.policy.clock_ref)) + return issues, cadence, cadence_count + + +def _autonomous_time_binding_issues( + context: _AutonomousExecutionReferenceContext, +) -> tuple[list[ParticipantBehaviorIssue], _AutonomousTimeBindings]: + clock_issues, clock, progression = _autonomous_clock_binding_issues(context) + constraint_issues, cadence, cadence_count = _autonomous_constraint_issues(context) + bindings = _AutonomousTimeBindings( + clock=clock, + progression=progression, + cadence=cadence, + cadence_count=cadence_count, + ) + return [*clock_issues, *constraint_issues], bindings diff --git a/implementations/python/packages/raes/semantics/participant_behavior/_behavior_spec.py b/implementations/python/packages/raes/semantics/participant_behavior/_behavior_spec.py new file mode 100644 index 000000000..d17707948 --- /dev/null +++ b/implementations/python/packages/raes/semantics/participant_behavior/_behavior_spec.py @@ -0,0 +1,249 @@ +"""Behavior-specification reference, vocabulary, feature, and evidence-contract checks.""" + +from __future__ import annotations + +from collections.abc import Callable + +from ._types import ( + ParticipantBehaviorIssue, + _BehaviorSpecificationReferenceContext, +) + + +def _behavior_mode_issue(*, spec_name: str, behavior_mode: object) -> ParticipantBehaviorIssue | None: + if not behavior_mode: + return None + try: + from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_scope_values + + validate_controlled_vocabulary_scope_values("behavior_specifications.behavior_mode", [str(behavior_mode)]) + except ValueError as exc: + return ParticipantBehaviorIssue( + code="participant.behavior-spec-mode-ungoverned", + participant_name="", + spec_name=spec_name, + ref=str(behavior_mode), + message=str(exc), + ) + return None + + +def _backend_feature_support_issue(*, spec_name: str, feature_ref: object) -> ParticipantBehaviorIssue | None: + try: + from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_value + + validation_errors: list[str] = [] + for vocabulary_id in ( + "participant-runtime-behavior-features", + "participant-runtime-interaction-features", + ): + try: + validate_controlled_vocabulary_value(vocabulary_id, str(feature_ref)) + return None + except ValueError as exc: + validation_errors.append(str(exc)) + except ValueError as exc: + validation_errors = [str(exc)] + return ParticipantBehaviorIssue( + code="participant.behavior-spec-feature-ungoverned", + participant_name="", + spec_name=spec_name, + ref=str(feature_ref), + message="; ".join(validation_errors), + ) + + +def _evidence_contract_issue(*, spec_name: str, evidence_contract_ref: object) -> ParticipantBehaviorIssue | None: + from raes_contracts.manifest_authority import ( + BACKEND_SUPPORTED_CONTRACT_IDS, + PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS, + PROCESSOR_SUPPORTED_CONTRACT_IDS, + ) + + allowed_contract_ids = frozenset( + [ + *BACKEND_SUPPORTED_CONTRACT_IDS, + *PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS, + *PROCESSOR_SUPPORTED_CONTRACT_IDS, + ] + ) + if str(evidence_contract_ref) in allowed_contract_ids: + return None + return ParticipantBehaviorIssue( + code="participant.behavior-spec-evidence-contract-unbound", + participant_name="", + spec_name=spec_name, + ref=str(evidence_contract_ref), + message="evidence_contract_refs must reference published processor, backend, or participant contracts", + ) + + +def _behavior_specification_named_ref_issues( + *, + spec_name: str, + refs: list[object], + known_names: set[str], + code: str, + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for ref in refs: + if is_unresolved(ref): + continue + if str(ref) not in known_names: + issues.append( + ParticipantBehaviorIssue( + code=code, + participant_name="", + spec_name=spec_name, + ref=str(ref), + ) + ) + return issues + + +def _behavior_specification_reference_issues( + *, + spec_name: str, + behavior_spec: object, + reference_context: _BehaviorSpecificationReferenceContext, + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + reference_sets = ( + ( + list(getattr(behavior_spec, "participant_refs", []) or []), + reference_context.participant_names, + "participant.behavior-spec-participant-unbound", + ), + ( + list(getattr(behavior_spec, "participant_role_refs", []) or []), + reference_context.participant_roles, + "participant.behavior-spec-role-unbound", + ), + ( + list(getattr(behavior_spec, "action_contract_refs", []) or []), + reference_context.action_names, + "participant.behavior-spec-action-unbound", + ), + ( + list(getattr(behavior_spec, "observation_boundary_refs", []) or []), + reference_context.observation_boundary_names, + "participant.behavior-spec-observation-boundary-unbound", + ), + ( + list(getattr(behavior_spec, "outcome_interpretation_rule_refs", []) or []), + reference_context.outcome_rule_names, + "participant.behavior-spec-outcome-rule-unbound", + ), + ) + issues: list[ParticipantBehaviorIssue] = [] + for refs, known_names, code in reference_sets: + issues.extend( + _behavior_specification_named_ref_issues( + spec_name=spec_name, + refs=refs, + known_names=known_names, + code=code, + is_unresolved=is_unresolved, + ) + ) + return issues + + +def _behavior_specification_feature_issues( + *, + spec_name: str, + behavior_spec: object, + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for feature_ref in getattr(behavior_spec, "backend_feature_support_refs", []) or []: + if is_unresolved(feature_ref): + continue + feature_issue = _backend_feature_support_issue(spec_name=spec_name, feature_ref=feature_ref) + if feature_issue is not None: + issues.append(feature_issue) + return issues + + +def _behavior_specification_evidence_contract_issues( + *, + spec_name: str, + behavior_spec: object, + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for evidence_contract_ref in getattr(behavior_spec, "evidence_contract_refs", []) or []: + if is_unresolved(evidence_contract_ref): + continue + evidence_issue = _evidence_contract_issue( + spec_name=spec_name, + evidence_contract_ref=evidence_contract_ref, + ) + if evidence_issue is not None: + issues.append(evidence_issue) + return issues + + +def _behavior_specification_vocabulary_issues( + *, + spec_name: str, + behavior_spec: object, + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + mode_issue = _behavior_mode_issue( + spec_name=spec_name, + behavior_mode=getattr(behavior_spec, "behavior_mode", None), + ) + if mode_issue is not None: + issues.append(mode_issue) + from raes_contracts.controlled_vocabularies import validate_controlled_vocabulary_scope_values + + for field_name, scope, code in ( + ( + "ai_offensive_behavior_refs", + "behavior_specifications.ai_offensive_behavior_refs", + "participant.behavior-spec-ai-offensive-behavior-ungoverned", + ), + ( + "defensive_behavior_refs", + "behavior_specifications.defensive_behavior_refs", + "participant.behavior-spec-defensive-behavior-ungoverned", + ), + ( + "offensive_behavior_refs", + "behavior_specifications.offensive_behavior_refs", + "participant.behavior-spec-offensive-behavior-ungoverned", + ), + ): + for ref in getattr(behavior_spec, field_name, []) or []: + if is_unresolved(ref): + continue + try: + validate_controlled_vocabulary_scope_values(scope, [str(ref)]) + except ValueError as exc: + issues.append( + ParticipantBehaviorIssue( + code=code, + participant_name="", + spec_name=spec_name, + ref=str(ref), + message=str(exc), + ) + ) + issues.extend( + _behavior_specification_feature_issues( + spec_name=spec_name, + behavior_spec=behavior_spec, + is_unresolved=is_unresolved, + ) + ) + issues.extend( + _behavior_specification_evidence_contract_issues( + spec_name=spec_name, + behavior_spec=behavior_spec, + is_unresolved=is_unresolved, + ) + ) + return issues diff --git a/implementations/python/packages/raes/semantics/participant_behavior/_references.py b/implementations/python/packages/raes/semantics/participant_behavior/_references.py new file mode 100644 index 000000000..6a3cae625 --- /dev/null +++ b/implementations/python/packages/raes/semantics/participant_behavior/_references.py @@ -0,0 +1,273 @@ +"""Agent action/observation reference resolution and observation-boundary visibility checks.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping + +from ._types import ( + ParticipantBehaviorIssue, + ParticipantBehaviorReference, + _BehaviorSpecificationReferenceContext, +) + + +def _action_references_for_agent( + *, + participant_name: str, + action_names: list[object], + action_contracts: Mapping[str, object], + is_unresolved: Callable[[object], bool], +) -> tuple[list[ParticipantBehaviorReference], list[ParticipantBehaviorIssue]]: + references: list[ParticipantBehaviorReference] = [] + issues: list[ParticipantBehaviorIssue] = [] + for action_name in action_names: + if is_unresolved(action_name): + continue + if action_contracts and action_name not in action_contracts: + issues.append( + ParticipantBehaviorIssue( + code="participant.action-contract-unbound", + participant_name=participant_name, + ref=str(action_name), + ) + ) + continue + if action_name in action_contracts: + references.append( + ParticipantBehaviorReference( + participant_name=participant_name, + reference_kind="action_contract", + raw=str(action_name), + canonical_name=str(action_name), + ) + ) + return references, issues + + +def _observation_boundary_references_for_agent( + *, + participant_name: str, + boundary_names: list[object], + observation_boundaries: Mapping[str, object], + is_unresolved: Callable[[object], bool], +) -> tuple[list[ParticipantBehaviorReference], list[ParticipantBehaviorIssue]]: + references: list[ParticipantBehaviorReference] = [] + issues: list[ParticipantBehaviorIssue] = [] + for boundary_name in boundary_names: + if is_unresolved(boundary_name): + continue + if boundary_name not in observation_boundaries: + issues.append( + ParticipantBehaviorIssue( + code="participant.observation-boundary-unbound", + participant_name=participant_name, + ref=str(boundary_name), + ) + ) + continue + references.append( + ParticipantBehaviorReference( + participant_name=participant_name, + reference_kind="observation_boundary", + raw=str(boundary_name), + canonical_name=str(boundary_name), + ) + ) + return references, issues + + +def _interaction_related_action_issues( + *, + action_name: str, + interaction: object, + action_contracts: Mapping[str, object], + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for related_action in getattr(interaction, "related_actions", []) or []: + if is_unresolved(related_action): + continue + if related_action not in action_contracts: + issues.append( + ParticipantBehaviorIssue( + code="participant.interaction-action-unbound", + participant_name="", + action_name=str(action_name), + ref=str(related_action), + ) + ) + return issues + + +def _interaction_references_for_action_contracts( + *, + action_contracts: Mapping[str, object], + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for action_name, action_contract in action_contracts.items(): + for interaction in getattr(action_contract, "interactions", []) or []: + issues.extend( + _interaction_related_action_issues( + action_name=action_name, + interaction=interaction, + action_contracts=action_contracts, + is_unresolved=is_unresolved, + ) + ) + return issues + + +def _observation_boundary_declared_refs(observation_boundary: object) -> set[str]: + refs: set[str] = set() + refs.update(str(ref) for ref in getattr(observation_boundary, "observable_refs", []) or []) + refs.update(str(ref) for ref in getattr(observation_boundary, "hidden_refs", []) or []) + refs.update(str(ref) for ref in getattr(observation_boundary, "evidence_refs", []) or []) + return refs + + +def _observation_boundary_evidence_refs(observation_boundary: object) -> set[str]: + return {str(ref) for ref in getattr(observation_boundary, "evidence_refs", []) or []} + + +def _is_bound_reference( + ref: object, + *, + declared_refs: set[str], + is_unresolved: Callable[[object], bool], +) -> bool: + return is_unresolved(ref) or str(ref) in declared_refs + + +def _view_rule_visibility_issues( + *, + boundary_name: str, + boundary: object, + declared_refs: set[str], + evidence_refs: set[str], + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for rule in getattr(boundary, "view_rules", []) or []: + information_ref = getattr(rule, "information_ref", "") + if not _is_bound_reference(information_ref, declared_refs=declared_refs, is_unresolved=is_unresolved): + issues.append( + ParticipantBehaviorIssue( + code="participant.view-rule-ref-unbound", + participant_name="", + boundary_name=boundary_name, + ref=str(information_ref), + ) + ) + for evidence_ref in getattr(rule, "evidence_refs", []) or []: + if not _is_bound_reference(evidence_ref, declared_refs=evidence_refs, is_unresolved=is_unresolved): + issues.append( + ParticipantBehaviorIssue( + code="participant.view-rule-evidence-unbound", + participant_name="", + boundary_name=boundary_name, + ref=str(evidence_ref), + ) + ) + return issues + + +def _view_transition_visibility_issues( + *, + boundary_name: str, + boundary: object, + declared_refs: set[str], + evidence_refs: set[str], + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for transition in getattr(boundary, "view_transitions", []) or []: + information_ref = getattr(transition, "information_ref", "") + transition_id = str(getattr(transition, "transition_id", "")) + if not _is_bound_reference(information_ref, declared_refs=declared_refs, is_unresolved=is_unresolved): + issues.append( + ParticipantBehaviorIssue( + code="participant.view-transition-ref-unbound", + participant_name="", + boundary_name=boundary_name, + transition_id=transition_id, + ref=str(information_ref), + ) + ) + for evidence_ref in getattr(transition, "evidence_refs", []) or []: + if not _is_bound_reference(evidence_ref, declared_refs=evidence_refs, is_unresolved=is_unresolved): + issues.append( + ParticipantBehaviorIssue( + code="participant.view-transition-evidence-unbound", + participant_name="", + boundary_name=boundary_name, + transition_id=transition_id, + ref=str(evidence_ref), + ) + ) + return issues + + +def _visibility_issues_for_observation_boundaries( + *, + observation_boundaries: Mapping[str, object], + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for boundary_name, boundary in observation_boundaries.items(): + declared_refs = _observation_boundary_declared_refs(boundary) + evidence_refs = _observation_boundary_evidence_refs(boundary) + issues.extend( + _view_rule_visibility_issues( + boundary_name=str(boundary_name), + boundary=boundary, + declared_refs=declared_refs, + evidence_refs=evidence_refs, + is_unresolved=is_unresolved, + ) + ) + issues.extend( + _view_transition_visibility_issues( + boundary_name=str(boundary_name), + boundary=boundary, + declared_refs=declared_refs, + evidence_refs=evidence_refs, + is_unresolved=is_unresolved, + ) + ) + + return issues + + +def _resolve_section_ref( + ref: object, + section: str, + declarations: Mapping[str, object], +) -> str | None: + """Resolve a bare or section-qualified reference to one SDL declaration.""" + + if not isinstance(ref, str): + return None + if ref in declarations: + return ref + prefix = f"{section}." + candidate = ref.removeprefix(prefix) if ref.startswith(prefix) else "" + return candidate if candidate in declarations else None + + +def _tool_affordance_participants( + behavior_spec: object, + reference_context: _BehaviorSpecificationReferenceContext, +) -> set[str]: + participants = { + str(ref) + for ref in getattr(behavior_spec, "participant_refs", []) or [] + if str(ref) in reference_context.participant_names + } + role_refs = {str(ref) for ref in getattr(behavior_spec, "participant_role_refs", []) or []} + participants.update( + participant_name + for participant_name, role in reference_context.participant_roles_by_agent.items() + if role in role_refs + ) + return participants diff --git a/implementations/python/packages/raes/semantics/participant_behavior/_tool_affordance.py b/implementations/python/packages/raes/semantics/participant_behavior/_tool_affordance.py new file mode 100644 index 000000000..c193174e6 --- /dev/null +++ b/implementations/python/packages/raes/semantics/participant_behavior/_tool_affordance.py @@ -0,0 +1,198 @@ +"""Tool-affordance relation, action/boundary widening, and view-classification checks.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping + +from ...participant_behavior_specification import tool_affordance_reference +from ._references import _observation_boundary_declared_refs, _tool_affordance_participants +from ._types import ( + ParticipantBehaviorIssue, + _BehaviorSpecificationReferenceContext, +) + + +def _tool_affordance_duplicate_issue( + *, + spec_name: str, + affordance_id: str, + tool_ref: object, + action_refs: list[str], + boundary_refs: list[str], + seen_relations: dict[tuple[str, tuple[str, ...], tuple[str, ...]], str], +) -> ParticipantBehaviorIssue | None: + signature = (str(tool_ref or ""), tuple(sorted(action_refs)), tuple(sorted(boundary_refs))) + duplicate_of = seen_relations.get(signature) + if duplicate_of is None: + seen_relations[signature] = affordance_id + return None + return ParticipantBehaviorIssue( + code="participant.tool-affordance-duplicate-relation", + participant_name="", + spec_name=spec_name, + ref=affordance_id, + message=duplicate_of, + ) + + +def _tool_affordance_action_issues( + *, + spec_name: str, + affordance_id: str, + action_ref: str, + parent_actions: set[str], + participants: set[str], + agents_by_name: Mapping[str, object], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + if action_ref not in parent_actions: + issues.append( + ParticipantBehaviorIssue( + code="participant.tool-affordance-action-widens-parent", + participant_name="", + spec_name=spec_name, + ref=action_ref, + action_name=affordance_id, + ) + ) + for participant_name in sorted(participants): + agent_actions = {str(ref) for ref in getattr(agents_by_name[participant_name], "actions", []) or []} + if action_ref not in agent_actions: + issues.append( + ParticipantBehaviorIssue( + code="participant.tool-affordance-action-outside-participant", + participant_name=participant_name, + spec_name=spec_name, + ref=action_ref, + action_name=affordance_id, + ) + ) + return issues + + +def _tool_affordance_boundary_issues( + *, + spec_name: str, + affordance_id: str, + boundary_ref: str, + parent_boundaries: set[str], + participants: set[str], + agents_by_name: Mapping[str, object], + observation_boundaries: Mapping[str, object], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + if boundary_ref not in parent_boundaries: + issues.append( + ParticipantBehaviorIssue( + code="participant.tool-affordance-boundary-widens-parent", + participant_name="", + spec_name=spec_name, + ref=boundary_ref, + action_name=affordance_id, + ) + ) + for participant_name in sorted(participants): + agent_boundaries = { + str(ref) for ref in getattr(agents_by_name[participant_name], "observation_boundaries", []) or [] + } + if boundary_ref not in agent_boundaries: + issues.append( + ParticipantBehaviorIssue( + code="participant.tool-affordance-boundary-outside-participant", + participant_name=participant_name, + spec_name=spec_name, + ref=boundary_ref, + action_name=affordance_id, + ) + ) + boundary = observation_boundaries.get(boundary_ref) + if boundary is None: + return issues + binding_ref = tool_affordance_reference(spec_name, affordance_id) + declared_refs = _observation_boundary_declared_refs(boundary) + view_rule_refs = {str(getattr(rule, "information_ref", "")) for rule in getattr(boundary, "view_rules", []) or []} + if binding_ref not in declared_refs or binding_ref not in view_rule_refs: + issues.append( + ParticipantBehaviorIssue( + code="participant.tool-affordance-view-unclassified", + participant_name="", + spec_name=spec_name, + ref=binding_ref, + action_name=affordance_id, + boundary_name=boundary_ref, + ) + ) + return issues + + +def _resolved_reference_issues( + refs: list[str], + is_unresolved: Callable[[object], bool], + build_issues: Callable[[str], list[ParticipantBehaviorIssue]], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + for ref in refs: + if not is_unresolved(ref): + issues.extend(build_issues(ref)) + return issues + + +def _tool_affordance_reference_issues( + *, + spec_name: str, + behavior_spec: object, + agents_by_name: Mapping[str, object], + observation_boundaries: Mapping[str, object], + reference_context: _BehaviorSpecificationReferenceContext, + is_unresolved: Callable[[object], bool], +) -> list[ParticipantBehaviorIssue]: + issues: list[ParticipantBehaviorIssue] = [] + parent_actions = {str(ref) for ref in getattr(behavior_spec, "action_contract_refs", []) or []} + parent_boundaries = {str(ref) for ref in getattr(behavior_spec, "observation_boundary_refs", []) or []} + participants = _tool_affordance_participants(behavior_spec, reference_context) + seen_relations: dict[tuple[str, tuple[str, ...], tuple[str, ...]], str] = {} + for affordance_id, binding in getattr(behavior_spec, "tool_affordances", {}).items(): + affordance_id = str(affordance_id) + action_refs = [str(ref) for ref in getattr(binding, "action_contract_refs", []) or []] + boundary_refs = [str(ref) for ref in getattr(binding, "observation_boundary_refs", []) or []] + duplicate_issue = _tool_affordance_duplicate_issue( + spec_name=spec_name, + affordance_id=affordance_id, + tool_ref=getattr(binding, "tool_ref", None), + action_refs=action_refs, + boundary_refs=boundary_refs, + seen_relations=seen_relations, + ) + if duplicate_issue is not None: + issues.append(duplicate_issue) + + issues.extend( + _resolved_reference_issues( + action_refs, + is_unresolved, + lambda action_ref, affordance_id=affordance_id: _tool_affordance_action_issues( + spec_name=spec_name, + affordance_id=affordance_id, + action_ref=action_ref, + parent_actions=parent_actions, + participants=participants, + agents_by_name=agents_by_name, + ), + ) + ) + issues.extend( + _resolved_reference_issues( + boundary_refs, + is_unresolved, + lambda boundary_ref, affordance_id=affordance_id: _tool_affordance_boundary_issues( + spec_name=spec_name, + affordance_id=affordance_id, + boundary_ref=boundary_ref, + parent_boundaries=parent_boundaries, + participants=participants, + agents_by_name=agents_by_name, + observation_boundaries=observation_boundaries, + ), + ) + ) + return issues diff --git a/implementations/python/packages/raes/semantics/participant_behavior/_types.py b/implementations/python/packages/raes/semantics/participant_behavior/_types.py new file mode 100644 index 000000000..c0e4073a5 --- /dev/null +++ b/implementations/python/packages/raes/semantics/participant_behavior/_types.py @@ -0,0 +1,62 @@ +"""Records and reference-context container for participant behavior semantics.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ParticipantBehaviorReference: + """Normalized reference from an agent to a behavior contract artifact.""" + + participant_name: str + reference_kind: str + raw: str + canonical_name: str + + +@dataclass(frozen=True) +class ParticipantBehaviorIssue: + """Machine-readable participant behavior consistency issue.""" + + code: str + participant_name: str + ref: str + action_name: str = "" + boundary_name: str = "" + transition_id: str = "" + spec_name: str = "" + message: str = "" + + +@dataclass(frozen=True) +class ParticipantBehaviorAnalysis: + """Result of analyzing participant behavior references.""" + + references: tuple[ParticipantBehaviorReference, ...] = () + issues: tuple[ParticipantBehaviorIssue, ...] = () + + @property + def has_issues(self) -> bool: + return bool(self.issues) + + +@dataclass(frozen=True) +class _BehaviorSpecificationReferenceContext: + participant_names: set[str] + participant_roles_by_agent: Mapping[str, str] + action_names: set[str] + observation_boundary_names: set[str] + outcome_rule_names: set[str] + agents_by_name: Mapping[str, object] + action_contracts: Mapping[str, object] + observation_boundaries: Mapping[str, object] + clocks: Mapping[str, object] + time_progression_policies: Mapping[str, object] + temporal_constraints: Mapping[str, object] + objectives: Mapping[str, object] + + @property + def participant_roles(self) -> set[str]: + return set(self.participant_roles_by_agent.values()) diff --git a/implementations/python/packages/raes/transformations.py b/implementations/python/packages/raes/transformations.py new file mode 100644 index 000000000..40d4f1aef --- /dev/null +++ b/implementations/python/packages/raes/transformations.py @@ -0,0 +1,26 @@ +"""Pure, deterministic semantic transformations over admitted RAES artifacts.""" + +from ._transformation_portable import canonicalize_portable_contract, compare_canonical_artifacts +from ._transformation_remove import remove_sdl_declaration +from ._transformation_rename import rename_sdl_declaration +from ._transformation_types import ( + ArtifactTransformationPolicy, + CanonicalArtifactComparison, + PortableContractTransformationResult, + RemoveSDLDeclarationRequest, + RenameSDLDeclarationRequest, + SDLTransformationResult, +) + +__all__ = [ + "ArtifactTransformationPolicy", + "CanonicalArtifactComparison", + "PortableContractTransformationResult", + "RemoveSDLDeclarationRequest", + "RenameSDLDeclarationRequest", + "SDLTransformationResult", + "canonicalize_portable_contract", + "compare_canonical_artifacts", + "remove_sdl_declaration", + "rename_sdl_declaration", +] diff --git a/implementations/python/packages/raes/validator/_evidence_requirements.py b/implementations/python/packages/raes/validator/_evidence_requirements.py index 7801125ac..d93702390 100644 --- a/implementations/python/packages/raes/validator/_evidence_requirements.py +++ b/implementations/python/packages/raes/validator/_evidence_requirements.py @@ -2,6 +2,8 @@ from __future__ import annotations +from raes.runtime_forwarding_agent import RuntimeForwardingAgentOwnershipRole + class _EvidenceRequirementsMixin: def _verify_evidence_requirements(self) -> None: @@ -12,6 +14,44 @@ def _verify_evidence_requirements(self) -> None: self._verify_evidence_requirement_refs(requirement.channel_refs, owner_label, "channel_ref") self._verify_evidence_requirement_ref(requirement.trigger_ref, owner_label, "trigger_ref") self._verify_evidence_requirement_ref(requirement.boundary_ref, owner_label, "boundary_ref") + self._verify_forwarding_agent_evidence_roles() + + def _verify_forwarding_agent_evidence_roles(self) -> None: + agents = self._forwarding_agents_by_address() + apparatus_bindings: set[str] = set() + for requirement in self._s.evidence_requirements.values(): + source_class = getattr(requirement.source_class, "value", requirement.source_class) + if source_class != "apparatus": + continue + for source_ref in requirement.source_refs: + apparatus_bindings.update(self._declaration_index.resolve(source_ref) & agents.keys()) + + for address, agent in agents.items(): + role = agent.ownership_role + if role is RuntimeForwardingAgentOwnershipRole.MEASUREMENT_APPARATUS and address not in apparatus_bindings: + self._err( + f"Forwarding agent '{address}' ownership_role 'measurement_apparatus' requires an inbound " + "EvidenceRequirement.source_refs binding with source_class 'apparatus'" + ) + elif role is RuntimeForwardingAgentOwnershipRole.SYSTEM_UNDER_TEST and address in apparatus_bindings: + self._err( + f"Forwarding agent '{address}' ownership_role 'system_under_test' cannot be targeted by an " + "evidence requirement with source_class 'apparatus'" + ) + + def _forwarding_agents_by_address(self) -> dict[str, object]: + agents = {f"forwarding_agents.{agent.forwarding_agent_id}": agent for agent in self._s.forwarding_agents} + for node_name, node in self._s.nodes.items(): + runtime = node.runtime + if runtime is None: + continue + agents.update( + { + f"nodes.{node_name}.runtime.forwarding_agents.{agent.forwarding_agent_id}": agent + for agent in runtime.forwarding_agents + } + ) + return agents def _verify_evidence_requirement_refs( self, diff --git a/implementations/python/packages/raes_backend_libvirt/drivers/libvirt/__init__.py b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt/__init__.py new file mode 100644 index 000000000..aa23a74f3 --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt/__init__.py @@ -0,0 +1,10 @@ +"""Lazy libvirt connection adapter for the libvirt/QEMU backend.""" + +from __future__ import annotations + +from ._native import Connector as Connector +from ._native import _error_code as _error_code +from ._native import _existing_uuid as _existing_uuid +from ._native import _filter_owner_uuid as _filter_owner_uuid +from ._native import _raes_uuid as _raes_uuid +from .deployment import LibvirtDeploymentDriver as LibvirtDeploymentDriver diff --git a/implementations/python/packages/raes_backend_libvirt/drivers/libvirt/_native.py b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt/_native.py new file mode 100644 index 000000000..334368ae8 --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt/_native.py @@ -0,0 +1,189 @@ +"""Native libvirt adapter helpers for the libvirt/QEMU deployment driver. + +Low-level libvirt-facing seams shared by the deployment driver and the TechVault +native modules: stable error-code classification, deterministic per-address and +nwfilter-owner UUIDs, ownership readback, and the lookup/stop primitives that +keep the define/convergence path (:func:`_lookup`) distinct from the fail-closed +teardown path (:func:`_find_native`). +""" + +from __future__ import annotations + +import importlib +import re +import uuid +from collections.abc import Callable +from typing import Protocol, cast + +_SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_.-]+") +# Fixed namespace for deriving a per-address libvirt UUID. The UUID proves an +# existing host object was realized by RAES for *this* address, so convergence +# never destroys a foreign or another-address object that merely shares a name. +_RAES_UUID_NAMESPACE = uuid.UUID("af20aedd-47bd-5870-b3f8-2f1baebde508") + +# libvirt signals a missing object with a stable VIR_ERR_NO_* code (part of its +# public C ABI) on ``libvirtError.get_error_code()``. Idempotent teardown treats +# only these as "already absent"; every other libvirtError — connection loss, +# permission denial, an ambiguous or internal lookup failure — stays a fail-closed +# diagnostic that preserves the snapshot for retry (issue #604 guardrail: +# "do not treat every libvirt lookup exception as not found"). +_VIR_ERR_NO_DOMAIN = 42 +_VIR_ERR_NO_NETWORK = 43 +# Raised by destroy() on an object that is not running; stopping an already-stopped +# object is a benign no-op on the teardown path, distinct from a permission/internal +# stop failure that must fail closed. +_VIR_ERR_OPERATION_INVALID = 55 +_ABSENCE_ERROR_CODES: frozenset[int] = frozenset({_VIR_ERR_NO_DOMAIN, _VIR_ERR_NO_NETWORK}) + + +class _OwnershipConflict(Exception): + """An existing host object at this name is not the RAES object for this address.""" + + +class _NativeLookupError(Exception): + """A libvirt lookup failed for a reason other than the object being absent.""" + + +def _error_code(exc: BaseException) -> int | None: + """Return a libvirtError's ``get_error_code()`` as an int, or None otherwise. + + A non-libvirt exception (no ``get_error_code``), or one whose code is not an + int, yields None so callers treat it as an unclassified real failure. + """ + + getter = getattr(exc, "get_error_code", None) + if not callable(getter): + return None + try: + code = getter() + except Exception: + return None + return code if isinstance(code, int) else None + + +def _is_absence_error(exc: BaseException) -> bool: + """Return True when ``exc`` is a libvirt "no such object" error. + + Absence is an idempotent teardown success. A non-libvirt exception, or a + libvirtError with any other code, is a real failure and returns False. + """ + + return _error_code(exc) in _ABSENCE_ERROR_CODES + + +def _raes_uuid(address: str) -> str: + return str(uuid.uuid5(_RAES_UUID_NAMESPACE, address)) + + +def _filter_owner_uuid(address: str) -> str: + """Owner UUID for a domain's nwfilter (namespaced so it never equals the domain UUID).""" + + return str(uuid.uuid5(_RAES_UUID_NAMESPACE, f"nwfilter:{address}")) + + +class _NativeResource(Protocol): + def create(self) -> None: ... + + def destroy(self) -> None: ... + + def undefine(self) -> None: ... + + +def _existing_uuid(native: object) -> str | None: + """Return an existing object's UUID string, or None when it cannot be read. + + A missing/unreadable UUID is treated as "not ours" by the caller, so an + object we cannot prove ownership of is never destroyed. + """ + + reader = getattr(native, "UUIDString", None) + if reader is None: + return None + try: + return reader() + except Exception: + return None + + +class _LibvirtModule(Protocol): + def open(self, connection_uri: str) -> object | None: ... + + +Connector = Callable[[str], object | None] + + +def _default_connector(connection_uri: str) -> object | None: + libvirt = cast(_LibvirtModule, importlib.import_module("libvirt")) + return libvirt.open(connection_uri) + + +def _call_libvirt(connection: object, method_name: str, payload: str) -> _NativeResource: + method = cast(Callable[[str], _NativeResource], getattr(connection, method_name)) + return method(payload) + + +def _lookup(connection: object, method_name: str, name: str) -> object | None: + """Return an existing native resource by name, or None when absent. + + Any lookup failure (not-found or otherwise) returns None so the caller + attempts a define; a genuine define-time conflict is then surfaced as a + redacted diagnostic rather than a duplicate resource. + """ + + method = getattr(connection, method_name, None) + if method is None: + return None + try: + return method(name) + except Exception: + return None + + +def _stop_native(native: object) -> None: + """Stop a running native object before it is undefined. + + Stopping an object that is already inactive is a benign no-op (libvirt raises + ``VIR_ERR_OPERATION_INVALID``), and one that has already vanished is absent; + either lets teardown proceed to ``undefine()``. Any other stop failure — + permission, internal — propagates so teardown fails closed instead of + undefining (and dropping the snapshot entry for) a still-running resource + (issue #604: permission and unconfirmed teardown failures must fail closed). + """ + + try: + cast(_NativeResource, native).destroy() + except Exception as exc: + code = _error_code(exc) + if code == _VIR_ERR_OPERATION_INVALID or code in _ABSENCE_ERROR_CODES: + return + raise + + +def _find_native(connection: object, method_name: str, name: str) -> object | None: + """Return an existing native resource by name, or None when genuinely absent. + + Unlike :func:`_lookup`, this distinguishes idempotent absence from a real + lookup failure: a libvirt "no such object" error maps to None, while a + connection/permission/internal lookup failure is raised as + :class:`_NativeLookupError` so teardown fails closed and preserves the snapshot for + retry rather than falsely reporting the object gone (issue #604). + """ + + method = getattr(connection, method_name, None) + if method is None: + return None + try: + return method(name) + except Exception as exc: + if _is_absence_error(exc): + return None + raise _NativeLookupError(method_name) from exc + + +def _safe_name(candidate: str, *, fallback: str, prefix: str) -> str: + raw = candidate.strip() or fallback.strip() or "resource" + normalized = _SAFE_NAME_RE.sub("-", raw).strip("-._") + if not normalized: + normalized = _SAFE_NAME_RE.sub("-", fallback).strip("-._") or "resource" + prefixed = f"{prefix}-{normalized}" if prefix else normalized + return prefixed[:63].strip("-._") or "resource" diff --git a/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt/deployment.py similarity index 72% rename from implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py rename to implementations/python/packages/raes_backend_libvirt/drivers/libvirt/deployment.py index f5062a7b9..62fa1ca30 100644 --- a/implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py +++ b/implementations/python/packages/raes_backend_libvirt/drivers/libvirt/deployment.py @@ -3,15 +3,11 @@ from __future__ import annotations import contextlib -import importlib import os -import re import shutil import tempfile -import uuid -from collections.abc import Callable from pathlib import Path -from typing import Protocol, cast +from typing import cast from raes_backend_protocols.naming import provider_resource_name from raes_contracts.diagnostics import Diagnostic, Severity @@ -24,8 +20,24 @@ NetworkSpec, ) -from ._libvirt_xml import _domain_xml, _network_xml, _nwfilter_xml -from .seed import _SEED_DIR_MODE, GenisoimageSeedBuilder, SeedBuilder, write_seed_files +from .._libvirt_xml import _domain_xml, _network_xml, _nwfilter_xml +from ..seed import _SEED_DIR_MODE, GenisoimageSeedBuilder, SeedBuilder, write_seed_files +from ._native import ( + Connector, + _call_libvirt, + _default_connector, + _existing_uuid, + _filter_owner_uuid, + _find_native, + _is_absence_error, + _lookup, + _NativeLookupError, + _NativeResource, + _OwnershipConflict, + _raes_uuid, + _safe_name, + _stop_native, +) _DOMAIN = "runtime" _CODE_OPERATION_FAILED = "libvirt-backend.driver.operation-failed" @@ -33,101 +45,6 @@ _CODE_OWNERSHIP_CONFLICT = "libvirt-backend.driver.ownership-conflict" _DEFAULT_CONNECTION_URI = "qemu:///system" _WORKSPACE_PREFIX = "raes-libvirt-" -_SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9_.-]+") -# Fixed namespace for deriving a per-address libvirt UUID. The UUID proves an -# existing host object was realized by RAES for *this* address, so convergence -# never destroys a foreign or another-address object that merely shares a name. -_RAES_UUID_NAMESPACE = uuid.UUID("af20aedd-47bd-5870-b3f8-2f1baebde508") - -# libvirt signals a missing object with a stable VIR_ERR_NO_* code (part of its -# public C ABI) on ``libvirtError.get_error_code()``. Idempotent teardown treats -# only these as "already absent"; every other libvirtError — connection loss, -# permission denial, an ambiguous or internal lookup failure — stays a fail-closed -# diagnostic that preserves the snapshot for retry (issue #604 guardrail: -# "do not treat every libvirt lookup exception as not found"). -_VIR_ERR_NO_DOMAIN = 42 -_VIR_ERR_NO_NETWORK = 43 -# Raised by destroy() on an object that is not running; stopping an already-stopped -# object is a benign no-op on the teardown path, distinct from a permission/internal -# stop failure that must fail closed. -_VIR_ERR_OPERATION_INVALID = 55 -_ABSENCE_ERROR_CODES: frozenset[int] = frozenset({_VIR_ERR_NO_DOMAIN, _VIR_ERR_NO_NETWORK}) - - -class _OwnershipConflict(Exception): - """An existing host object at this name is not the RAES object for this address.""" - - -class _NativeLookupError(Exception): - """A libvirt lookup failed for a reason other than the object being absent.""" - - -def _error_code(exc: BaseException) -> int | None: - """Return a libvirtError's ``get_error_code()`` as an int, or None otherwise. - - A non-libvirt exception (no ``get_error_code``), or one whose code is not an - int, yields None so callers treat it as an unclassified real failure. - """ - - getter = getattr(exc, "get_error_code", None) - if not callable(getter): - return None - try: - code = getter() - except Exception: - return None - return code if isinstance(code, int) else None - - -def _is_absence_error(exc: BaseException) -> bool: - """Return True when ``exc`` is a libvirt "no such object" error. - - Absence is an idempotent teardown success. A non-libvirt exception, or a - libvirtError with any other code, is a real failure and returns False. - """ - - return _error_code(exc) in _ABSENCE_ERROR_CODES - - -def _raes_uuid(address: str) -> str: - return str(uuid.uuid5(_RAES_UUID_NAMESPACE, address)) - - -def _filter_owner_uuid(address: str) -> str: - """Owner UUID for a domain's nwfilter (namespaced so it never equals the domain UUID).""" - - return str(uuid.uuid5(_RAES_UUID_NAMESPACE, f"nwfilter:{address}")) - - -class _NativeResource(Protocol): - def create(self) -> None: ... - - def destroy(self) -> None: ... - - def undefine(self) -> None: ... - - -def _existing_uuid(native: object) -> str | None: - """Return an existing object's UUID string, or None when it cannot be read. - - A missing/unreadable UUID is treated as "not ours" by the caller, so an - object we cannot prove ownership of is never destroyed. - """ - - reader = getattr(native, "UUIDString", None) - if reader is None: - return None - try: - return reader() - except Exception: - return None - - -class _LibvirtModule(Protocol): - def open(self, connection_uri: str) -> object | None: ... - - -Connector = Callable[[str], object | None] class LibvirtDeploymentDriver: @@ -462,83 +379,6 @@ def _rollback(self, networks: list[str], domains: list[str]) -> None: self.destroy(networks=tuple(networks), domains=tuple(domains)) -def _default_connector(connection_uri: str) -> object | None: - libvirt = cast(_LibvirtModule, importlib.import_module("libvirt")) - return libvirt.open(connection_uri) - - -def _call_libvirt(connection: object, method_name: str, payload: str) -> _NativeResource: - method = cast(Callable[[str], _NativeResource], getattr(connection, method_name)) - return method(payload) - - -def _lookup(connection: object, method_name: str, name: str) -> object | None: - """Return an existing native resource by name, or None when absent. - - Any lookup failure (not-found or otherwise) returns None so the caller - attempts a define; a genuine define-time conflict is then surfaced as a - redacted diagnostic rather than a duplicate resource. - """ - - method = getattr(connection, method_name, None) - if method is None: - return None - try: - return method(name) - except Exception: - return None - - -def _stop_native(native: object) -> None: - """Stop a running native object before it is undefined. - - Stopping an object that is already inactive is a benign no-op (libvirt raises - ``VIR_ERR_OPERATION_INVALID``), and one that has already vanished is absent; - either lets teardown proceed to ``undefine()``. Any other stop failure — - permission, internal — propagates so teardown fails closed instead of - undefining (and dropping the snapshot entry for) a still-running resource - (issue #604: permission and unconfirmed teardown failures must fail closed). - """ - - try: - cast(_NativeResource, native).destroy() - except Exception as exc: - code = _error_code(exc) - if code == _VIR_ERR_OPERATION_INVALID or code in _ABSENCE_ERROR_CODES: - return - raise - - -def _find_native(connection: object, method_name: str, name: str) -> object | None: - """Return an existing native resource by name, or None when genuinely absent. - - Unlike :func:`_lookup`, this distinguishes idempotent absence from a real - lookup failure: a libvirt "no such object" error maps to None, while a - connection/permission/internal lookup failure is raised as - :class:`_NativeLookupError` so teardown fails closed and preserves the snapshot for - retry rather than falsely reporting the object gone (issue #604). - """ - - method = getattr(connection, method_name, None) - if method is None: - return None - try: - return method(name) - except Exception as exc: - if _is_absence_error(exc): - return None - raise _NativeLookupError(method_name) from exc - - -def _safe_name(candidate: str, *, fallback: str, prefix: str) -> str: - raw = candidate.strip() or fallback.strip() or "resource" - normalized = _SAFE_NAME_RE.sub("-", raw).strip("-._") - if not normalized: - normalized = _SAFE_NAME_RE.sub("-", fallback).strip("-._") or "resource" - prefixed = f"{prefix}-{normalized}" if prefix else normalized - return prefixed[:63].strip("-._") or "resource" - - _FAILURE_MESSAGES = { _CODE_UNAVAILABLE: "Libvirt connection is unavailable for this backend operation.", _CODE_OWNERSHIP_CONFLICT: ( diff --git a/implementations/python/packages/raes_backend_libvirt/manifest.py b/implementations/python/packages/raes_backend_libvirt/manifest.py index b036c0bf0..f57223f42 100644 --- a/implementations/python/packages/raes_backend_libvirt/manifest.py +++ b/implementations/python/packages/raes_backend_libvirt/manifest.py @@ -6,7 +6,7 @@ from importlib.metadata import version as distribution_version from raes_backend_protocols.capabilities import ( - PARTICIPANT_RUNTIME_POLICY_FEATURES, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, BackendCapabilitySet, BackendManifest, ParticipantFeatureSupport, @@ -133,7 +133,7 @@ def _participant_runtime_capabilities() -> ParticipantRuntimeCapabilities: limitation_refs=(f"limitation:{feature}:not-realized",), disclosure_refs=(disclosure_ref,), ) - for feature in sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + for feature in sorted(PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES) ), ), constraints={ diff --git a/implementations/python/packages/raes_backend_libvirt/realization.py b/implementations/python/packages/raes_backend_libvirt/realization.py deleted file mode 100644 index f5f099314..000000000 --- a/implementations/python/packages/raes_backend_libvirt/realization.py +++ /dev/null @@ -1,571 +0,0 @@ -"""Pure interpretation of provisioning plans for the libvirt backend. - -Maps an RAES :class:`ProvisioningPlan` into a driver-neutral :class:`Realization` -of portable network/domain specs. Node resources become libvirt domains; network -resources become libvirt networks; and placement resources are bound to their -target domains. Content, account, and feature placements contribute to cloud-init: - -- ``account-placement`` → cloud-init ``users`` (groups, shell, home, disabled, - auth_method) plus ``/etc/aliases.d`` (mail) and ``/etc/raes/spn`` (spn) files; -- ``content-placement`` → cloud-init ``write_files`` (file/text) or ``runcmd`` - and a descriptor file (dataset/directory/source-backed); -- ``feature-binding`` → cloud-init ``packages``/``runcmd`` (service) or a - descriptor file (artifact/configuration). - -The module is pure (no driver, no IO): the provisioner validates a plan without -realizing it, and the driver renders seed media from the same data. -""" - -from __future__ import annotations - -import json -from collections.abc import Mapping -from dataclasses import dataclass, field - -from raes_backend_protocols.capabilities import ProvisionerCapabilities -from raes_backend_protocols.naming import provider_resource_name -from raes_contracts.diagnostics import Diagnostic, Severity -from raes_contracts.planning import PlannedResource, ProvisioningPlan, RuntimeDomain - -from ._payload import ( - ACCOUNT_PLACEMENT_RESOURCE_TYPE, - CONTENT_PLACEMENT_RESOURCE_TYPE, - DOMAIN_CONTROLLER_PLACEMENT_RESOURCE_TYPE, - NETWORK_RESOURCE_TYPE, - NODE_RESOURCE_TYPE, - SUPPORTED_RESOURCE_TYPES, - _os_family, - _spec, - _str, -) -from .acls import realize_node_acls -from .capability_envelope import capability_envelope_diagnostics -from .cloudinit import CloudInitFile, CloudInitSpec, CloudInitUser, safe_path_component -from .dialects import GuestDialect, GuestEmit, dialect_for -from .driver import DomainSpec, NetworkAcl, NetworkSpec, ServiceSpec -from .manifest import LIBVIRT_PROVISIONER_CAPABILITIES - -_DOMAIN = "runtime" -_NETWORK_NAMESPACE_UNSUPPORTED = "libvirt-backend.network-namespace-unsupported" - - -@dataclass(frozen=True) -class Realization: - """Driver-neutral libvirt realization intent.""" - - networks: tuple[NetworkSpec, ...] = () - domains: tuple[DomainSpec, ...] = () - diagnostics: tuple[Diagnostic, ...] = () - # placement address -> the node (domain) address its cloud-init contributes to. - # Lets the provisioner realize a domain when a placement targeting it changes, - # even if the node itself is UNCHANGED. - placement_targets: dict[str, str] = field(default_factory=dict) - - -@dataclass -class _CloudInitAccumulator: - """Mutable per-domain cloud-init contributions, aggregated across placements.""" - - users: list[CloudInitUser] = field(default_factory=list) - write_files: list[CloudInitFile] = field(default_factory=list) - packages: list[str] = field(default_factory=list) - runcmd: list[tuple[str, ...]] = field(default_factory=list) - - def build(self, *, hostname: str) -> CloudInitSpec: - return CloudInitSpec( - hostname=hostname, - users=tuple(sorted(self.users, key=lambda user: user.name)), - write_files=tuple(sorted(self.write_files, key=lambda file: file.path)), - packages=tuple(dict.fromkeys(self.packages)), - runcmd=tuple(self.runcmd), - ) - - -def interpret_provisioning_plan( - plan: ProvisioningPlan, - *, - provisioner_capabilities: ProvisionerCapabilities | None = None, -) -> Realization: - """Interpret an RAES provisioning plan as portable libvirt intent. - - ``provisioner_capabilities`` is the backend capability envelope every plan - term is validated against; it defaults to the libvirt manifest's declared - envelope so a term outside it (an ungoverned/extension node type, OS family, - content type, or account feature the backend does not realize) yields a - blocking typed diagnostic instead of a silent or partial realization - (issue #605). - """ - - capabilities = provisioner_capabilities or LIBVIRT_PROVISIONER_CAPABILITIES - diagnostics: list[Diagnostic] = list(capability_envelope_diagnostics(plan, capabilities)) - network_resources, node_resources, placement_resources = _collect_supported_resources(plan, diagnostics) - - networks = [_network_spec(resource, payload) for resource, payload in network_resources] - network_lookup = _network_address_lookup(networks) - cidr_lookup = _network_cidr_lookup(networks) - node_lookup = _node_address_lookup(node_resources) - node_addresses = {resource.address for resource, _ in node_resources} - node_os = {resource.address: _os_family(payload) for resource, payload in node_resources} - cloud_init, placement_diagnostics, placement_targets = _aggregate_cloud_init( - placement_resources, node_lookup, node_os, node_addresses - ) - diagnostics.extend(placement_diagnostics) - acls: dict[str, tuple[NetworkAcl, ...]] = {} - for resource, payload in node_resources: - node_acls, acl_diagnostics = realize_node_acls(resource, _infrastructure_spec(payload).get("acls"), cidr_lookup) - acls[resource.address] = node_acls - diagnostics.extend(acl_diagnostics) - domains = [ - _domain_spec(resource, payload, network_lookup, cloud_init, acls) for resource, payload in node_resources - ] - - return Realization( - networks=tuple(sorted(networks, key=lambda spec: spec.address)), - domains=tuple(sorted(domains, key=lambda spec: spec.address)), - diagnostics=tuple(diagnostics), - placement_targets=placement_targets, - ) - - -def _collect_supported_resources( - plan: ProvisioningPlan, - diagnostics: list[Diagnostic], -) -> tuple[ - list[tuple[PlannedResource, Mapping[str, object]]], - list[tuple[PlannedResource, Mapping[str, object]]], - list[tuple[PlannedResource, Mapping[str, object]]], -]: - network_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] - node_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] - placement_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] - for resource in sorted(plan.resources.values(), key=lambda item: item.address): - if resource.domain != RuntimeDomain.PROVISIONING: - continue - if resource.resource_type not in SUPPORTED_RESOURCE_TYPES: - diagnostics.append(_unsupported_resource(resource)) - continue - payload = resource.payload - if not isinstance(payload, Mapping): - diagnostics.append(_invalid_payload(resource)) - continue - if resource.resource_type == NETWORK_RESOURCE_TYPE: - network_resources.append((resource, payload)) - elif resource.resource_type == NODE_RESOURCE_TYPE: - node_resources.append((resource, payload)) - if payload.get("network_namespace_target"): - diagnostics.append(_network_namespace_unsupported(resource.address)) - else: - placement_resources.append((resource, payload)) - return network_resources, node_resources, placement_resources - - -def _network_address_lookup(networks: list[NetworkSpec]) -> dict[str, str]: - lookup: dict[str, str] = {} - for spec in networks: - for key in (spec.address, spec.name): - if key: - lookup[key] = spec.address - return lookup - - -def _network_spec(resource: PlannedResource, payload: Mapping[str, object]) -> NetworkSpec: - infrastructure = _infrastructure_spec(payload) - properties = infrastructure.get("properties") - labels: dict[str, str] = {} - if isinstance(properties, Mapping) and isinstance(properties.get("internal"), bool): - labels["internal"] = "true" if properties["internal"] else "false" - cidr = properties.get("cidr") if isinstance(properties, Mapping) else None - gateway = properties.get("gateway") if isinstance(properties, Mapping) else None - return NetworkSpec( - address=resource.address, - name=_resource_name(resource, payload), - cidr=cidr if isinstance(cidr, str) and cidr else None, - gateway=gateway if isinstance(gateway, str) and gateway else None, - labels=labels, - ) - - -def _node_address_lookup( - node_resources: list[tuple[PlannedResource, Mapping[str, object]]], -) -> dict[str, str]: - """Map every handle a placement might reference a node by to its address.""" - - lookup: dict[str, str] = {} - for resource, payload in node_resources: - name = _resource_name(resource, payload) - for key in (resource.address, name): - if key: - lookup[key] = resource.address - return lookup - - -def _aggregate_cloud_init( - placement_resources: list[tuple[PlannedResource, Mapping[str, object]]], - node_lookup: dict[str, str], - node_os: dict[str, str], - node_addresses: set[str], -) -> tuple[dict[str, _CloudInitAccumulator], list[Diagnostic], dict[str, str]]: - """Fold each placement into its target domain's cloud-init contributions. - - Service and mail realization is routed through the target node's OS dialect - so a Windows or BSD guest gets its native mechanism, not a Linux primitive. - - A placement whose target cannot be resolved to a node in this plan is *not* - silently dropped: it yields an ERROR diagnostic so apply fails closed rather - than reporting success while leaving the placement unrealized. - - Also returns a ``placement address -> node address`` map so the provisioner - can realize a domain whose cloud-init changed because a placement changed. - """ - - accumulators: dict[str, _CloudInitAccumulator] = {} - diagnostics: list[Diagnostic] = [] - placement_targets: dict[str, str] = {} - for resource, payload in placement_resources: - target = _placement_target(payload, node_lookup) - if target is None or target not in node_addresses: - diagnostics.append(_unbound_placement(resource, target)) - continue - placement_targets[resource.address] = target - dialect = dialect_for(node_os.get(target, "")) - accumulator = accumulators.setdefault(target, _CloudInitAccumulator()) - if resource.resource_type == ACCOUNT_PLACEMENT_RESOURCE_TYPE: - _realize_account(accumulator, payload, dialect) - elif resource.resource_type == CONTENT_PLACEMENT_RESOURCE_TYPE: - _realize_content(accumulator, resource, payload) - elif resource.resource_type == DOMAIN_CONTROLLER_PLACEMENT_RESOURCE_TYPE: - # This generic carrier intentionally emits no provider- or - # product-specific bootstrap commands. - pass - else: - _realize_feature(accumulator, resource, payload, dialect) - return accumulators, diagnostics, placement_targets - - -def _merge_emit(accumulator: _CloudInitAccumulator, emit: GuestEmit) -> None: - accumulator.packages.extend(emit.packages) - accumulator.write_files.extend(emit.write_files) - accumulator.runcmd.extend(emit.runcmd) - - -def _placement_target(payload: Mapping[str, object], node_lookup: dict[str, str]) -> str | None: - for key in ("target_address", "node_address", "target_node", "node_name", "node", "target"): - ref = payload.get(key) - if isinstance(ref, str) and ref: - return node_lookup.get(ref, ref) - return None - - -def _realize_account(accumulator: _CloudInitAccumulator, payload: Mapping[str, object], dialect: GuestDialect) -> None: - spec = _spec(payload) - username = _str(spec.get("username")) or _str(payload.get("account_name")) or _str(payload.get("name")) - if not username: - return - groups = tuple(str(group) for group in spec.get("groups", ()) if isinstance(group, str) and group) - disabled = _truthy(spec.get("disabled")) - # A disabled account installs no usable credential; otherwise key material is - # the only credential we render, so password login is always locked. - ssh_keys = () if disabled else _ssh_authorized_keys(spec) - # Fail closed on credentials: we never provision a password, so lock_passwd - # stays True for every account. Unlocking a password account without a hash - # would leave a known (often privileged) username reachable with no secret — - # potentially a blank-password login. Key-based auth still works via the - # rendered authorized keys, which do not require an unlocked password. - accumulator.users.append( - CloudInitUser( - name=username, - groups=groups, - shell=_str(spec.get("shell")), - home=_str(spec.get("home")), - lock_passwd=True, - ssh_authorized_keys=ssh_keys, - ) - ) - mail = _str(spec.get("mail")) - if mail: - _merge_emit(accumulator, dialect.mail_alias(username, mail)) - spn = _str(spec.get("spn")) - if spn: - # A real Kerberos SPN needs an AD/realm join; absent a domain, the - # portable maximum is a host-side principal descriptor the guest can join with. - safe_user = safe_path_component(username, fallback="user") - accumulator.write_files.append( - CloudInitFile(path=f"/etc/raes/spn/{safe_user}", content=f"{spn}\n", permissions="0600") - ) - - -def _realize_content( - accumulator: _CloudInitAccumulator, - resource: PlannedResource, - payload: Mapping[str, object], -) -> None: - spec = _spec(payload) - content_type = _str(spec.get("type")) - if content_type == "file": - path = _str(spec.get("path")) - if not path: - return - text = spec.get("text") - if isinstance(text, str): - accumulator.write_files.append(CloudInitFile(path=path, content=text)) - else: - accumulator.runcmd.append(("mkdir", "-p", _dirname(path))) - accumulator.write_files.append(_content_descriptor(resource, payload, path)) - elif content_type == "directory": - destination = _str(spec.get("destination")) - if not destination: - return - accumulator.runcmd.append(("mkdir", "-p", destination)) - accumulator.write_files.append(_content_descriptor(resource, payload, destination)) - elif content_type == "dataset": - accumulator.write_files.append(_content_descriptor(resource, payload, None)) - - -def _realize_feature( - accumulator: _CloudInitAccumulator, - resource: PlannedResource, - payload: Mapping[str, object], - dialect: GuestDialect, -) -> None: - spec = _spec(payload) - template = spec.get("template") - template = template if isinstance(template, Mapping) else {} - feature_type = _str(template.get("type")) - source = template.get("source") - package = _str(source.get("name")) if isinstance(source, Mapping) else "" - name = _resource_name(resource, payload) - if feature_type == "service" and package: - _merge_emit(accumulator, dialect.enable_feature(package)) - else: - destination = _str(template.get("destination")) - if destination: - accumulator.runcmd.append(("mkdir", "-p", _dirname(destination))) - accumulator.write_files.append( - CloudInitFile( - path=f"/etc/raes/features/{safe_path_component(name, fallback='feature')}.json", - content=_descriptor_body({"feature": name, "type": feature_type, "destination": destination}), - permissions="0644", - ) - ) - - -def _content_descriptor( - resource: PlannedResource, - payload: Mapping[str, object], - location: str | None, -) -> CloudInitFile: - spec = _spec(payload) - name = _resource_name(resource, payload) - descriptor = { - "content": name, - "type": _str(spec.get("type")), - "location": location or "", - } - safe_name = safe_path_component(name, fallback="content") - return CloudInitFile(path=f"/etc/raes/content/{safe_name}.json", content=_descriptor_body(descriptor)) - - -def _descriptor_body(descriptor: Mapping[str, object]) -> str: - return json.dumps(dict(descriptor), indent=2, sort_keys=True) + "\n" - - -def _domain_spec( - resource: PlannedResource, - payload: Mapping[str, object], - network_lookup: dict[str, str], - cloud_init: dict[str, _CloudInitAccumulator], - acls: dict[str, tuple[NetworkAcl, ...]], -) -> DomainSpec: - infrastructure = _infrastructure_spec(payload) - references = _network_refs(infrastructure) - network_addresses = tuple(network_lookup.get(ref, ref) for ref in references) - resources = _node_resources(payload) - name = _resource_name(resource, payload) - accumulator = cloud_init.get(resource.address, _CloudInitAccumulator()) - return DomainSpec( - address=resource.address, - name=name, - image_ref=_image_ref(payload), - memory_mib=_memory_mib(resources.get("ram")), - vcpus=_vcpus(resources.get("cpu")), - networks=network_addresses, - services=_services(payload), - cloud_init=accumulator.build(hostname=name), - network_acls=acls.get(resource.address, ()), - ) - - -def _network_cidr_lookup(networks: list[NetworkSpec]) -> dict[str, str]: - lookup: dict[str, str] = {} - for spec in networks: - if not spec.cidr: - continue - for key in (spec.address, spec.name): - if key: - lookup[key] = spec.cidr - return lookup - - -def _resource_name(resource: PlannedResource, payload: Mapping[str, object]) -> str: - name = payload.get("name") or payload.get("node_name") - if isinstance(name, str) and name: - return name - return provider_resource_name(resource.address, prefix="raes") - - -def _infrastructure_spec(payload: Mapping[str, object]) -> Mapping[str, object]: - spec = payload.get("spec") - if not isinstance(spec, Mapping): - return {} - infrastructure = spec.get("infrastructure") - return infrastructure if isinstance(infrastructure, Mapping) else {} - - -def _network_refs(infrastructure: Mapping[str, object]) -> tuple[str, ...]: - for field_name in ("networks", "links"): - raw = infrastructure.get(field_name) - if isinstance(raw, (list, tuple)): - return tuple(ref for ref in raw if isinstance(ref, str) and ref) - return () - - -def _node_resources(payload: Mapping[str, object]) -> Mapping[str, object]: - spec = payload.get("spec") - node = spec.get("node") if isinstance(spec, Mapping) else None - resources = node.get("resources") if isinstance(node, Mapping) else None - return resources if isinstance(resources, Mapping) else {} - - -def _services(payload: Mapping[str, object]) -> tuple[ServiceSpec, ...]: - spec = payload.get("spec") - node = spec.get("node") if isinstance(spec, Mapping) else None - raw_services = node.get("services") if isinstance(node, Mapping) else None - if not isinstance(raw_services, list | tuple): - return () - services: list[ServiceSpec] = [] - for item in raw_services: - service = _service(item) - if service is not None: - services.append(service) - return tuple(sorted(services, key=lambda service: (service.protocol, service.port, service.name))) - - -def _service(raw: object) -> ServiceSpec | None: - service: ServiceSpec | None = None - if isinstance(raw, Mapping): - name = raw.get("name") - port = raw.get("port") - protocol = raw.get("protocol", "tcp") - if isinstance(name, str) and name and isinstance(port, int | float) and int(port) > 0: - normalized_protocol = protocol.lower() if isinstance(protocol, str) and protocol else "tcp" - if normalized_protocol not in {"tcp", "udp"}: - normalized_protocol = "tcp" - service = ServiceSpec(name=name, port=int(port), protocol=normalized_protocol) - return service - - -def _memory_mib(raw: object) -> int: - if isinstance(raw, int | float) and raw > 0: - # Planner payloads carry RAM in bytes. Tiny synthetic values are - # treated as MiB to keep hand-authored unit plans ergonomic. - if raw >= 1024 * 1024: - return max(128, int((raw + 1024 * 1024 - 1) // (1024 * 1024))) - return max(128, int(raw)) - return 512 - - -def _vcpus(raw: object) -> int: - if isinstance(raw, int | float) and raw > 0: - return max(1, int(raw)) - return 1 - - -def _image_ref(payload: Mapping[str, object]) -> str | None: - spec = payload.get("spec") - node = spec.get("node") if isinstance(spec, Mapping) else None - source = node.get("source") if isinstance(node, Mapping) else None - if isinstance(source, str) and source: - return source - if isinstance(source, Mapping): - name = source.get("name") - if isinstance(name, str) and name: - return name - return None - - -def _ssh_authorized_keys(spec: Mapping[str, object]) -> tuple[str, ...]: - """Collect any authorized SSH keys the account placement carries.""" - - keys: list[str] = [] - for key in ("ssh_authorized_keys", "ssh_keys", "authorized_keys"): - raw = spec.get(key) - if isinstance(raw, str) and raw: - keys.append(raw) - elif isinstance(raw, list | tuple): - keys.extend(entry for entry in raw if isinstance(entry, str) and entry) - if keys: - break - return tuple(keys) - - -def _truthy(value: object) -> bool: - return value is True - - -def _dirname(path: str) -> str: - head = path.rsplit("/", 1)[0] - return head or "/" - - -def _unsupported_resource(resource: PlannedResource) -> Diagnostic: - return Diagnostic( - code="libvirt-backend.realization.unsupported-resource", - domain=_DOMAIN, - address=resource.address, - message=( - "Libvirt backend does not realize provisioning resource type " - f"'{resource.resource_type}' for '{resource.address}'." - ), - severity=Severity.ERROR, - ) - - -def _network_namespace_unsupported(address: str) -> Diagnostic: - return Diagnostic( - code=_NETWORK_NAMESPACE_UNSUPPORTED, - domain=_DOMAIN, - address=address, - message=(f"Libvirt backend cannot realize exact container network namespace sharing for '{address}'."), - severity=Severity.ERROR, - ) - - -def _unbound_placement(resource: PlannedResource, target: str | None) -> Diagnostic: - detail = ( - "carries no resolvable target node reference" - if target is None - else f"names target node '{target}', which is not present in this plan" - ) - return Diagnostic( - code="libvirt-backend.realization.unbound-placement", - domain=_DOMAIN, - address=resource.address, - message=( - f"Libvirt backend cannot realize placement '{resource.address}' of type " - f"'{resource.resource_type}': it {detail}." - ), - severity=Severity.ERROR, - ) - - -def _invalid_payload(resource: PlannedResource) -> Diagnostic: - return Diagnostic( - code="libvirt-backend.realization.invalid-payload", - domain=_DOMAIN, - address=resource.address, - message=( - f"Libvirt backend expected provisioning resource '{resource.address}' " - f"of type '{resource.resource_type}' to carry a mapping payload." - ), - severity=Severity.ERROR, - ) diff --git a/implementations/python/packages/raes_backend_libvirt/realization/__init__.py b/implementations/python/packages/raes_backend_libvirt/realization/__init__.py new file mode 100644 index 000000000..3f37e1f6a --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/realization/__init__.py @@ -0,0 +1,29 @@ +"""Pure interpretation of provisioning plans for the libvirt backend. + +Maps an RAES :class:`ProvisioningPlan` into a driver-neutral :class:`Realization` +of portable network/domain specs. Node resources become libvirt domains; network +resources become libvirt networks; and placement resources are bound to their +target domains. Content, account, and feature placements contribute to cloud-init: + +- ``account-placement`` → cloud-init ``users`` (groups, shell, home, disabled, + auth_method) plus ``/etc/aliases.d`` (mail) and ``/etc/raes/spn`` (spn) files; +- ``content-placement`` → cloud-init ``write_files`` (file/text) or ``runcmd`` + and a descriptor file (dataset/directory/source-backed); +- ``feature-binding`` → cloud-init ``packages``/``runcmd`` (service) or a + descriptor file (artifact/configuration). + +The module is pure (no driver, no IO): the provisioner validates a plan without +realizing it, and the driver renders seed media from the same data. +""" + +from __future__ import annotations + +from ._common import _resource_name as _resource_name +from ._plan import Realization as Realization +from ._plan import interpret_provisioning_plan as interpret_provisioning_plan +from ._specs import _image_ref as _image_ref +from ._specs import _infrastructure_spec as _infrastructure_spec +from ._specs import _memory_mib as _memory_mib +from ._specs import _node_resources as _node_resources +from ._specs import _services as _services +from ._specs import _vcpus as _vcpus diff --git a/implementations/python/packages/raes_backend_libvirt/realization/_cloud_init.py b/implementations/python/packages/raes_backend_libvirt/realization/_cloud_init.py new file mode 100644 index 000000000..ae5a186ea --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/realization/_cloud_init.py @@ -0,0 +1,235 @@ +"""Placement-to-cloud-init aggregation for libvirt plan realization.""" + +from __future__ import annotations + +import json +from collections.abc import Mapping +from dataclasses import dataclass, field + +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.planning import PlannedResource + +from .._payload import ( + ACCOUNT_PLACEMENT_RESOURCE_TYPE, + CONTENT_PLACEMENT_RESOURCE_TYPE, + DOMAIN_CONTROLLER_PLACEMENT_RESOURCE_TYPE, + _spec, + _str, +) +from ..cloudinit import CloudInitFile, CloudInitSpec, CloudInitUser, safe_path_component +from ..dialects import GuestDialect, GuestEmit, dialect_for +from ._common import _resource_name +from ._diagnostics import _unbound_placement + + +@dataclass +class _CloudInitAccumulator: + """Mutable per-domain cloud-init contributions, aggregated across placements.""" + + users: list[CloudInitUser] = field(default_factory=list) + write_files: list[CloudInitFile] = field(default_factory=list) + packages: list[str] = field(default_factory=list) + runcmd: list[tuple[str, ...]] = field(default_factory=list) + + def build(self, *, hostname: str) -> CloudInitSpec: + return CloudInitSpec( + hostname=hostname, + users=tuple(sorted(self.users, key=lambda user: user.name)), + write_files=tuple(sorted(self.write_files, key=lambda file: file.path)), + packages=tuple(dict.fromkeys(self.packages)), + runcmd=tuple(self.runcmd), + ) + + +def _aggregate_cloud_init( + placement_resources: list[tuple[PlannedResource, Mapping[str, object]]], + node_lookup: dict[str, str], + node_os: dict[str, str], + node_addresses: set[str], +) -> tuple[dict[str, _CloudInitAccumulator], list[Diagnostic], dict[str, str]]: + """Fold each placement into its target domain's cloud-init contributions. + + Service and mail realization is routed through the target node's OS dialect + so a Windows or BSD guest gets its native mechanism, not a Linux primitive. + + A placement whose target cannot be resolved to a node in this plan is *not* + silently dropped: it yields an ERROR diagnostic so apply fails closed rather + than reporting success while leaving the placement unrealized. + + Also returns a ``placement address -> node address`` map so the provisioner + can realize a domain whose cloud-init changed because a placement changed. + """ + + accumulators: dict[str, _CloudInitAccumulator] = {} + diagnostics: list[Diagnostic] = [] + placement_targets: dict[str, str] = {} + for resource, payload in placement_resources: + target = _placement_target(payload, node_lookup) + if target is None or target not in node_addresses: + diagnostics.append(_unbound_placement(resource, target)) + continue + placement_targets[resource.address] = target + dialect = dialect_for(node_os.get(target, "")) + accumulator = accumulators.setdefault(target, _CloudInitAccumulator()) + if resource.resource_type == ACCOUNT_PLACEMENT_RESOURCE_TYPE: + _realize_account(accumulator, payload, dialect) + elif resource.resource_type == CONTENT_PLACEMENT_RESOURCE_TYPE: + _realize_content(accumulator, resource, payload) + elif resource.resource_type == DOMAIN_CONTROLLER_PLACEMENT_RESOURCE_TYPE: + # This generic carrier intentionally emits no provider- or + # product-specific bootstrap commands. + pass + else: + _realize_feature(accumulator, resource, payload, dialect) + return accumulators, diagnostics, placement_targets + + +def _merge_emit(accumulator: _CloudInitAccumulator, emit: GuestEmit) -> None: + accumulator.packages.extend(emit.packages) + accumulator.write_files.extend(emit.write_files) + accumulator.runcmd.extend(emit.runcmd) + + +def _placement_target(payload: Mapping[str, object], node_lookup: dict[str, str]) -> str | None: + for key in ("target_address", "node_address", "target_node", "node_name", "node", "target"): + ref = payload.get(key) + if isinstance(ref, str) and ref: + return node_lookup.get(ref, ref) + return None + + +def _realize_account(accumulator: _CloudInitAccumulator, payload: Mapping[str, object], dialect: GuestDialect) -> None: + spec = _spec(payload) + username = _str(spec.get("username")) or _str(payload.get("account_name")) or _str(payload.get("name")) + if not username: + return + groups = tuple(str(group) for group in spec.get("groups", ()) if isinstance(group, str) and group) + disabled = _truthy(spec.get("disabled")) + # A disabled account installs no usable credential; otherwise key material is + # the only credential we render, so password login is always locked. + ssh_keys = () if disabled else _ssh_authorized_keys(spec) + # Fail closed on credentials: we never provision a password, so lock_passwd + # stays True for every account. Unlocking a password account without a hash + # would leave a known (often privileged) username reachable with no secret — + # potentially a blank-password login. Key-based auth still works via the + # rendered authorized keys, which do not require an unlocked password. + accumulator.users.append( + CloudInitUser( + name=username, + groups=groups, + shell=_str(spec.get("shell")), + home=_str(spec.get("home")), + lock_passwd=True, + ssh_authorized_keys=ssh_keys, + ) + ) + mail = _str(spec.get("mail")) + if mail: + _merge_emit(accumulator, dialect.mail_alias(username, mail)) + spn = _str(spec.get("spn")) + if spn: + # A real Kerberos SPN needs an AD/realm join; absent a domain, the + # portable maximum is a host-side principal descriptor the guest can join with. + safe_user = safe_path_component(username, fallback="user") + accumulator.write_files.append( + CloudInitFile(path=f"/etc/raes/spn/{safe_user}", content=f"{spn}\n", permissions="0600") + ) + + +def _realize_content( + accumulator: _CloudInitAccumulator, + resource: PlannedResource, + payload: Mapping[str, object], +) -> None: + spec = _spec(payload) + content_type = _str(spec.get("type")) + if content_type == "file": + path = _str(spec.get("path")) + if not path: + return + text = spec.get("text") + if isinstance(text, str): + accumulator.write_files.append(CloudInitFile(path=path, content=text)) + else: + accumulator.runcmd.append(("mkdir", "-p", _dirname(path))) + accumulator.write_files.append(_content_descriptor(resource, payload, path)) + elif content_type == "directory": + destination = _str(spec.get("destination")) + if not destination: + return + accumulator.runcmd.append(("mkdir", "-p", destination)) + accumulator.write_files.append(_content_descriptor(resource, payload, destination)) + elif content_type == "dataset": + accumulator.write_files.append(_content_descriptor(resource, payload, None)) + + +def _realize_feature( + accumulator: _CloudInitAccumulator, + resource: PlannedResource, + payload: Mapping[str, object], + dialect: GuestDialect, +) -> None: + spec = _spec(payload) + template = spec.get("template") + template = template if isinstance(template, Mapping) else {} + feature_type = _str(template.get("type")) + source = template.get("source") + package = _str(source.get("name")) if isinstance(source, Mapping) else "" + name = _resource_name(resource) + if feature_type == "service" and package: + _merge_emit(accumulator, dialect.enable_feature(package)) + else: + destination = _str(template.get("destination")) + if destination: + accumulator.runcmd.append(("mkdir", "-p", _dirname(destination))) + accumulator.write_files.append( + CloudInitFile( + path=f"/etc/raes/features/{safe_path_component(name, fallback='feature')}.json", + content=_descriptor_body({"feature": name, "type": feature_type, "destination": destination}), + permissions="0644", + ) + ) + + +def _content_descriptor( + resource: PlannedResource, + payload: Mapping[str, object], + location: str | None, +) -> CloudInitFile: + spec = _spec(payload) + name = _resource_name(resource) + descriptor = { + "content": name, + "type": _str(spec.get("type")), + "location": location or "", + } + safe_name = safe_path_component(name, fallback="content") + return CloudInitFile(path=f"/etc/raes/content/{safe_name}.json", content=_descriptor_body(descriptor)) + + +def _descriptor_body(descriptor: Mapping[str, object]) -> str: + return json.dumps(dict(descriptor), indent=2, sort_keys=True) + "\n" + + +def _ssh_authorized_keys(spec: Mapping[str, object]) -> tuple[str, ...]: + """Collect any authorized SSH keys the account placement carries.""" + + keys: list[str] = [] + for key in ("ssh_authorized_keys", "ssh_keys", "authorized_keys"): + raw = spec.get(key) + if isinstance(raw, str) and raw: + keys.append(raw) + elif isinstance(raw, list | tuple): + keys.extend(entry for entry in raw if isinstance(entry, str) and entry) + if keys: + break + return tuple(keys) + + +def _truthy(value: object) -> bool: + return value is True + + +def _dirname(path: str) -> str: + head = path.rsplit("/", 1)[0] + return head or "/" diff --git a/implementations/python/packages/raes_backend_libvirt/realization/_common.py b/implementations/python/packages/raes_backend_libvirt/realization/_common.py new file mode 100644 index 000000000..15daead12 --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/realization/_common.py @@ -0,0 +1,23 @@ +"""Shared payload projection helpers for libvirt plan realization.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from raes_backend_protocols.naming import provider_resource_name +from raes_contracts.planning import PlannedResource, PlanOperation, planned_resource_authored_name + + +def _resource_name( + resource: PlannedResource | PlanOperation, + payload: Mapping[str, object] | None = None, +) -> str: + if isinstance(resource, PlannedResource): + name = planned_resource_authored_name(resource) + else: + operation_payload = payload or {} + raw_name = operation_payload.get("name") or operation_payload.get("node_name") + name = raw_name if isinstance(raw_name, str) and raw_name else None + if name is not None: + return name + return provider_resource_name(resource.address, prefix="raes") diff --git a/implementations/python/packages/raes_backend_libvirt/realization/_diagnostics.py b/implementations/python/packages/raes_backend_libvirt/realization/_diagnostics.py new file mode 100644 index 000000000..defabc2b1 --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/realization/_diagnostics.py @@ -0,0 +1,63 @@ +"""Ordered diagnostics for libvirt plan realization.""" + +from __future__ import annotations + +from raes_contracts.diagnostics import Diagnostic, Severity +from raes_contracts.planning import PlannedResource + +_DOMAIN = "runtime" +_NETWORK_NAMESPACE_UNSUPPORTED = "libvirt-backend.network-namespace-unsupported" + + +def _unsupported_resource(resource: PlannedResource) -> Diagnostic: + return Diagnostic( + code="libvirt-backend.realization.unsupported-resource", + domain=_DOMAIN, + address=resource.address, + message=( + "Libvirt backend does not realize provisioning resource type " + f"'{resource.resource_type}' for '{resource.address}'." + ), + severity=Severity.ERROR, + ) + + +def _network_namespace_unsupported(address: str) -> Diagnostic: + return Diagnostic( + code=_NETWORK_NAMESPACE_UNSUPPORTED, + domain=_DOMAIN, + address=address, + message=(f"Libvirt backend cannot realize exact container network namespace sharing for '{address}'."), + severity=Severity.ERROR, + ) + + +def _unbound_placement(resource: PlannedResource, target: str | None) -> Diagnostic: + detail = ( + "carries no resolvable target node reference" + if target is None + else f"names target node '{target}', which is not present in this plan" + ) + return Diagnostic( + code="libvirt-backend.realization.unbound-placement", + domain=_DOMAIN, + address=resource.address, + message=( + f"Libvirt backend cannot realize placement '{resource.address}' of type " + f"'{resource.resource_type}': it {detail}." + ), + severity=Severity.ERROR, + ) + + +def _invalid_payload(resource: PlannedResource) -> Diagnostic: + return Diagnostic( + code="libvirt-backend.realization.invalid-payload", + domain=_DOMAIN, + address=resource.address, + message=( + f"Libvirt backend expected provisioning resource '{resource.address}' " + f"of type '{resource.resource_type}' to carry a mapping payload." + ), + severity=Severity.ERROR, + ) diff --git a/implementations/python/packages/raes_backend_libvirt/realization/_plan.py b/implementations/python/packages/raes_backend_libvirt/realization/_plan.py new file mode 100644 index 000000000..78be92758 --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/realization/_plan.py @@ -0,0 +1,130 @@ +"""Pure interpretation of provisioning plans into libvirt realization intent.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field + +from raes_backend_protocols.capabilities import ProvisionerCapabilities +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.planning import ( + PlannedResource, + ProvisioningPlan, + RuntimeDomain, + planned_infrastructure_spec, + planned_resource_payload, +) + +from .._payload import NETWORK_RESOURCE_TYPE, NODE_RESOURCE_TYPE, SUPPORTED_RESOURCE_TYPES, _os_family +from ..acls import realize_node_acls +from ..capability_envelope import capability_envelope_diagnostics +from ..driver import DomainSpec, NetworkAcl, NetworkSpec +from ..manifest import LIBVIRT_PROVISIONER_CAPABILITIES +from ._cloud_init import _aggregate_cloud_init +from ._diagnostics import _invalid_payload, _network_namespace_unsupported, _unsupported_resource +from ._specs import ( + _domain_spec, + _network_address_lookup, + _network_cidr_lookup, + _network_spec, + _node_address_lookup, +) + + +@dataclass(frozen=True) +class Realization: + """Driver-neutral libvirt realization intent.""" + + networks: tuple[NetworkSpec, ...] = () + domains: tuple[DomainSpec, ...] = () + diagnostics: tuple[Diagnostic, ...] = () + # placement address -> the node (domain) address its cloud-init contributes to. + # Lets the provisioner realize a domain when a placement targeting it changes, + # even if the node itself is UNCHANGED. + placement_targets: dict[str, str] = field(default_factory=dict) + + +def interpret_provisioning_plan( + plan: ProvisioningPlan, + *, + provisioner_capabilities: ProvisionerCapabilities | None = None, +) -> Realization: + """Interpret an RAES provisioning plan as portable libvirt intent. + + ``provisioner_capabilities`` is the backend capability envelope every plan + term is validated against; it defaults to the libvirt manifest's declared + envelope so a term outside it (an ungoverned/extension node type, OS family, + content type, or account feature the backend does not realize) yields a + blocking typed diagnostic instead of a silent or partial realization + (issue #605). + """ + + capabilities = provisioner_capabilities or LIBVIRT_PROVISIONER_CAPABILITIES + diagnostics: list[Diagnostic] = list(capability_envelope_diagnostics(plan, capabilities)) + network_resources, node_resources, placement_resources = _collect_supported_resources(plan, diagnostics) + + networks = [_network_spec(resource) for resource, _ in network_resources] + network_lookup = _network_address_lookup(networks) + cidr_lookup = _network_cidr_lookup(networks) + node_lookup = _node_address_lookup(node_resources) + node_addresses = {resource.address for resource, _ in node_resources} + node_os = {resource.address: _os_family(payload) for resource, payload in node_resources} + cloud_init, placement_diagnostics, placement_targets = _aggregate_cloud_init( + placement_resources, node_lookup, node_os, node_addresses + ) + diagnostics.extend(placement_diagnostics) + acls: dict[str, tuple[NetworkAcl, ...]] = {} + for resource, _payload in node_resources: + infrastructure = planned_infrastructure_spec(resource) or {} + node_acls, acl_diagnostics = realize_node_acls(resource, infrastructure.get("acls"), cidr_lookup) + acls[resource.address] = node_acls + diagnostics.extend(acl_diagnostics) + domains = [_domain_spec(resource, network_lookup, cloud_init, acls) for resource, _ in node_resources] + + return Realization( + networks=tuple(sorted(networks, key=lambda spec: spec.address)), + domains=tuple(sorted(domains, key=lambda spec: spec.address)), + diagnostics=tuple(diagnostics), + placement_targets=placement_targets, + ) + + +def _collect_supported_resources( + plan: ProvisioningPlan, + diagnostics: list[Diagnostic], +) -> tuple[ + list[tuple[PlannedResource, Mapping[str, object]]], + list[tuple[PlannedResource, Mapping[str, object]]], + list[tuple[PlannedResource, Mapping[str, object]]], +]: + network_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] + node_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] + placement_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] + for resource in sorted(plan.resources.values(), key=lambda item: item.address): + payload = _supported_resource_payload(resource, diagnostics) + if payload is None: + continue + if resource.resource_type == NETWORK_RESOURCE_TYPE: + network_resources.append((resource, payload)) + elif resource.resource_type == NODE_RESOURCE_TYPE: + node_resources.append((resource, payload)) + if payload.get("network_namespace_target"): + diagnostics.append(_network_namespace_unsupported(resource.address)) + else: + placement_resources.append((resource, payload)) + return network_resources, node_resources, placement_resources + + +def _supported_resource_payload( + resource: PlannedResource, + diagnostics: list[Diagnostic], +) -> Mapping[str, object] | None: + if resource.domain != RuntimeDomain.PROVISIONING: + return None + if resource.resource_type not in SUPPORTED_RESOURCE_TYPES: + diagnostics.append(_unsupported_resource(resource)) + return None + payload = planned_resource_payload(resource) + if payload is None: + diagnostics.append(_invalid_payload(resource)) + return payload diff --git a/implementations/python/packages/raes_backend_libvirt/realization/_specs.py b/implementations/python/packages/raes_backend_libvirt/realization/_specs.py new file mode 100644 index 000000000..02f6bec21 --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/realization/_specs.py @@ -0,0 +1,199 @@ +"""Network/node spec construction and payload projection for plan realization.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from raes_contracts.planning import ( + PlannedResource, + planned_infrastructure_spec, + planned_node_resources, + planned_node_source, + planned_node_spec, +) + +from .._payload import _str +from ..driver import DomainSpec, NetworkAcl, NetworkSpec, ServiceSpec +from ._cloud_init import _CloudInitAccumulator +from ._common import _resource_name + + +def _network_address_lookup(networks: list[NetworkSpec]) -> dict[str, str]: + lookup: dict[str, str] = {} + for spec in networks: + for key in (spec.address, spec.name): + if key: + lookup[key] = spec.address + return lookup + + +def _network_spec(resource: PlannedResource) -> NetworkSpec: + infrastructure = planned_infrastructure_spec(resource) or {} + raw_properties = infrastructure.get("properties") + properties = raw_properties if isinstance(raw_properties, Mapping) else {} + labels: dict[str, str] = {} + internal = properties.get("internal") + if isinstance(internal, bool): + labels["internal"] = str(internal).lower() + return NetworkSpec( + address=resource.address, + name=_resource_name(resource), + cidr=_optional_str(properties.get("cidr")), + gateway=_optional_str(properties.get("gateway")), + labels=labels, + ) + + +def _optional_str(value: object) -> str | None: + text = _str(value) + return text or None + + +def _node_address_lookup( + node_resources: list[tuple[PlannedResource, Mapping[str, object]]], +) -> dict[str, str]: + """Map every handle a placement might reference a node by to its address.""" + + lookup: dict[str, str] = {} + for resource, _payload in node_resources: + name = _resource_name(resource) + for key in (resource.address, name): + if key: + lookup[key] = resource.address + return lookup + + +def _domain_spec( + resource: PlannedResource, + network_lookup: dict[str, str], + cloud_init: dict[str, _CloudInitAccumulator], + acls: dict[str, tuple[NetworkAcl, ...]], +) -> DomainSpec: + infrastructure = planned_infrastructure_spec(resource) or {} + references = _network_refs(infrastructure) + network_addresses = tuple(network_lookup.get(ref, ref) for ref in references) + resources = planned_node_resources(resource) or {} + name = _resource_name(resource) + accumulator = cloud_init.get(resource.address, _CloudInitAccumulator()) + return DomainSpec( + address=resource.address, + name=name, + image_ref=_planned_node_image_ref(resource), + memory_mib=_memory_mib(resources.get("ram")), + vcpus=_vcpus(resources.get("cpu")), + networks=network_addresses, + services=_planned_node_services(resource), + cloud_init=accumulator.build(hostname=name), + network_acls=acls.get(resource.address, ()), + ) + + +def _network_cidr_lookup(networks: list[NetworkSpec]) -> dict[str, str]: + lookup: dict[str, str] = {} + for spec in networks: + if not spec.cidr: + continue + for key in (spec.address, spec.name): + if key: + lookup[key] = spec.cidr + return lookup + + +def _infrastructure_spec(payload: Mapping[str, object]) -> Mapping[str, object]: + spec = payload.get("spec") + if not isinstance(spec, Mapping): + return {} + infrastructure = spec.get("infrastructure") + return infrastructure if isinstance(infrastructure, Mapping) else {} + + +def _network_refs(infrastructure: Mapping[str, object]) -> tuple[str, ...]: + for field_name in ("networks", "links"): + raw = infrastructure.get(field_name) + if isinstance(raw, (list, tuple)): + return tuple(ref for ref in raw if isinstance(ref, str) and ref) + return () + + +def _node_resources(payload: Mapping[str, object]) -> Mapping[str, object]: + spec = payload.get("spec") + node = spec.get("node") if isinstance(spec, Mapping) else None + resources = node.get("resources") if isinstance(node, Mapping) else None + return resources if isinstance(resources, Mapping) else {} + + +def _services(payload: Mapping[str, object]) -> tuple[ServiceSpec, ...]: + spec = payload.get("spec") + node = spec.get("node") if isinstance(spec, Mapping) else None + raw_services = node.get("services") if isinstance(node, Mapping) else None + if not isinstance(raw_services, list | tuple): + return () + services: list[ServiceSpec] = [] + for item in raw_services: + service = _service(item) + if service is not None: + services.append(service) + return tuple(sorted(services, key=lambda service: (service.protocol, service.port, service.name))) + + +def _planned_node_services(resource: PlannedResource) -> tuple[ServiceSpec, ...]: + node = planned_node_spec(resource) + raw_services = node.get("services") if node is not None else None + if not isinstance(raw_services, list | tuple): + return () + services = [service for item in raw_services if (service := _service(item)) is not None] + return tuple(sorted(services, key=lambda service: (service.protocol, service.port, service.name))) + + +def _service(raw: object) -> ServiceSpec | None: + service: ServiceSpec | None = None + if isinstance(raw, Mapping): + name = raw.get("name") + port = raw.get("port") + protocol = raw.get("protocol", "tcp") + if isinstance(name, str) and name and isinstance(port, int | float) and int(port) > 0: + normalized_protocol = protocol.lower() if isinstance(protocol, str) and protocol else "tcp" + if normalized_protocol not in {"tcp", "udp"}: + normalized_protocol = "tcp" + service = ServiceSpec(name=name, port=int(port), protocol=normalized_protocol) + return service + + +def _memory_mib(raw: object) -> int: + if isinstance(raw, int | float) and raw > 0: + # Planner payloads carry RAM in bytes. Tiny synthetic values are + # treated as MiB to keep hand-authored unit plans ergonomic. + if raw >= 1024 * 1024: + return max(128, int((raw + 1024 * 1024 - 1) // (1024 * 1024))) + return max(128, int(raw)) + return 512 + + +def _vcpus(raw: object) -> int: + if isinstance(raw, int | float) and raw > 0: + return max(1, int(raw)) + return 1 + + +def _image_ref(payload: Mapping[str, object]) -> str | None: + spec = payload.get("spec") + node = spec.get("node") if isinstance(spec, Mapping) else None + source = node.get("source") if isinstance(node, Mapping) else None + if isinstance(source, str) and source: + return source + if isinstance(source, Mapping): + name = source.get("name") + if isinstance(name, str) and name: + return name + return None + + +def _planned_node_image_ref(resource: PlannedResource) -> str | None: + source = planned_node_source(resource) + if isinstance(source, str): + return source + if isinstance(source, Mapping): + name = source.get("name") + if isinstance(name, str) and name: + return name + return None diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_native/__init__.py b/implementations/python/packages/raes_backend_libvirt/techvault_native/__init__.py new file mode 100644 index 000000000..43ece6a82 --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/techvault_native/__init__.py @@ -0,0 +1,25 @@ +"""Native libvirt/QEMU TechVault appliance driver.""" + +from __future__ import annotations + +from .._techvault_native_ops import _artifact_token as _artifact_token +from ..driver import DriverResult as DriverResult +from ..techvault_appliance import BusyboxInitramfsBuilder +from ..techvault_probe import ( + NativeLibvirtProbe, + ProbeResult, + check_native_readiness, + expected_surface, + native_soc_readback, +) +from ._driver import TechVaultNativeLibvirtDriver + +__all__ = [ + "BusyboxInitramfsBuilder", + "NativeLibvirtProbe", + "ProbeResult", + "TechVaultNativeLibvirtDriver", + "check_native_readiness", + "expected_surface", + "native_soc_readback", +] diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_native/_define.py b/implementations/python/packages/raes_backend_libvirt/techvault_native/_define.py new file mode 100644 index 000000000..a81e79359 --- /dev/null +++ b/implementations/python/packages/raes_backend_libvirt/techvault_native/_define.py @@ -0,0 +1,176 @@ +"""Define-stage helpers for the native TechVault libvirt driver. + +Pure lifecycle helpers behind the driver's ``_define_*`` method seams: ownership +checked network/domain definition, readback, and artifact staging. Kept separate +from :mod:`._driver` only for the ADR-015 size cap; the driver delegates to these +and the subclass override points (``_render_domain_xml``) still dispatch through +the passed ``driver`` instance. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING + +from raes_contracts.diagnostics import Diagnostic + +from .._techvault_native_ops import ( + _CODE_OPERATION_FAILED, + _CODE_OWNERSHIP_CONFLICT, + _CODE_READBACK_FAILED, + _artifact_token, + _call, + _diagnostic, + _ensure_name_available, + _NativeResource, +) +from ..driver import DomainHandle, NetworkHandle, RealizationObservation +from ..techvault_appliance import copy_kernel_for_libvirt, make_libvirt_readable +from ..techvault_lifecycle import NativeOwnershipConflict as _OwnershipConflict +from ..techvault_matrix import as_sequence as _as_sequence +from ..techvault_matrix import network_xml as _network_xml +from ..techvault_observation import domain_observations, network_observations + +if TYPE_CHECKING: + from ._driver import TechVaultNativeLibvirtDriver + + +def define_networks( + driver: TechVaultNativeLibvirtDriver, connection: object, matrix: Mapping[str, object] +) -> tuple[list[NetworkHandle], list[Diagnostic], list[RealizationObservation]]: + handles: list[NetworkHandle] = [] + diagnostics: list[Diagnostic] = [] + observations: list[RealizationObservation] = [] + for network in _as_sequence(matrix.get("networks")): + if isinstance(network, Mapping): + handle, diagnostic, observed = driver._define_network(connection, network) + if handle is not None: + handles.append(handle) + if diagnostic is not None: + diagnostics.append(diagnostic) + observations.extend(observed) + if diagnostic is not None: + break + return handles, diagnostics, observations + + +def define_network( + driver: TechVaultNativeLibvirtDriver, connection: object, network: Mapping[str, object] +) -> tuple[NetworkHandle | None, Diagnostic | None, tuple[RealizationObservation, ...]]: + address = str(network.get("address", "")) + native: _NativeResource | None = None + handle: NetworkHandle | None = None + diagnostic: Diagnostic | None = None + observations: tuple[RealizationObservation, ...] = () + driver._names[address] = str(network.get("runtime_name", "")) + try: + _ensure_name_available( + connection, + "networkLookupByName", + str(network.get("runtime_name", "")), + address, + ) + native = _call(connection, "networkDefineXML", _network_xml(network)) + if not driver.define_only: + native.create() + except _OwnershipConflict: + driver._names.pop(address, None) + diagnostic = _diagnostic(_CODE_OWNERSHIP_CONFLICT, address) + except Exception: + if native is None: + driver._names.pop(address, None) + else: + driver._realized.add(address) + handle = NetworkHandle(address=address) + diagnostic = _diagnostic(_CODE_OPERATION_FAILED, address) + else: + driver._realized.add(address) + handle = NetworkHandle(address=address, realized=True) + try: + observations = network_observations(native, network) + except Exception: + diagnostic = _diagnostic(_CODE_READBACK_FAILED, address) + return handle, diagnostic, observations + + +def define_domains( + driver: TechVaultNativeLibvirtDriver, connection: object, matrix: Mapping[str, object] +) -> tuple[list[DomainHandle], list[Diagnostic], list[RealizationObservation]]: + handles: list[DomainHandle] = [] + diagnostics: list[Diagnostic] = [] + observations: list[RealizationObservation] = [] + network_addresses = { + str(item.get("runtime_name", "")): str(item.get("address", "")) + for item in _as_sequence(matrix.get("networks")) + if isinstance(item, Mapping) + } + for domain in _as_sequence(matrix.get("domains")): + if isinstance(domain, Mapping): + handle, diagnostic, observed = driver._define_domain(connection, domain, network_addresses) + if handle is not None: + handles.append(handle) + if diagnostic is not None: + diagnostics.append(diagnostic) + observations.extend(observed) + if diagnostic is not None: + break + return handles, diagnostics, observations + + +def define_domain( + driver: TechVaultNativeLibvirtDriver, + connection: object, + domain: Mapping[str, object], + network_addresses: Mapping[str, str], +) -> tuple[DomainHandle | None, Diagnostic | None, tuple[RealizationObservation, ...]]: + address = str(domain.get("address", "")) + native: _NativeResource | None = None + handle: DomainHandle | None = None + diagnostic: Diagnostic | None = None + observations: tuple[RealizationObservation, ...] = () + driver._names[address] = str(domain.get("runtime_name", "")) + try: + _ensure_name_available( + connection, + "lookupByName", + str(domain.get("runtime_name", "")), + address, + ) + kernel = copy_kernel_for_libvirt( + driver.kernel_path, + driver.state_dir / "kernel" / f"{_artifact_token(address)}-{driver.kernel_path.name}", + ) + initrd = driver.initramfs_builder.build( + domain=domain, + target=driver.state_dir / "initramfs" / f"{_artifact_token(address)}.cpio.gz", + ) + make_libvirt_readable(initrd) + driver._artifacts[address] = (kernel, initrd) + native = _call(connection, "defineXML", driver._render_domain_xml(domain, kernel=kernel, initrd=initrd)) + if not driver.define_only: + native.create() + except _OwnershipConflict: + driver._names.pop(address, None) + diagnostic = _diagnostic(_CODE_OWNERSHIP_CONFLICT, address) + except Exception: + if native is None: + driver._cleanup_artifacts(address) + driver._names.pop(address, None) + else: + driver._realized.add(address) + handle = DomainHandle(address=address) + diagnostic = _diagnostic(_CODE_OPERATION_FAILED, address) + else: + driver._realized.add(address) + handle = DomainHandle(address=address, realized=True) + try: + observations = domain_observations( + native, + domain, + network_addresses, + kernel=kernel, + initrd=initrd, + ) + except Exception: + diagnostic = _diagnostic(_CODE_READBACK_FAILED, address) + return handle, diagnostic, observations diff --git a/implementations/python/packages/raes_backend_libvirt/techvault_native.py b/implementations/python/packages/raes_backend_libvirt/techvault_native/_driver.py similarity index 70% rename from implementations/python/packages/raes_backend_libvirt/techvault_native.py rename to implementations/python/packages/raes_backend_libvirt/techvault_native/_driver.py index 648b655b7..aebda6e0e 100644 --- a/implementations/python/packages/raes_backend_libvirt/techvault_native.py +++ b/implementations/python/packages/raes_backend_libvirt/techvault_native/_driver.py @@ -17,26 +17,22 @@ from raes_contracts.diagnostics import Diagnostic -from ._techvault_native_helpers import default_connector as _default_connector -from ._techvault_native_helpers import default_kernel_path as _default_kernel_path -from ._techvault_native_ops import ( +from .._techvault_native_helpers import default_connector as _default_connector +from .._techvault_native_helpers import default_kernel_path as _default_kernel_path +from .._techvault_native_ops import ( _CODE_OPERATION_FAILED, _CODE_OWNERSHIP_CONFLICT, - _CODE_READBACK_FAILED, _CODE_RESIDUAL_STATE, _CODE_UNAVAILABLE, _DEFAULT_CONNECTION_URI, _artifact_token, - _call, _diagnostic, - _ensure_name_available, - _NativeResource, ) if TYPE_CHECKING: from raes_contracts.realization_envelope import BackendRealizationEnvelopeModel -from .driver import ( +from ..driver import ( DomainHandle, DomainSpec, DriverResult, @@ -44,66 +40,37 @@ NetworkSpec, RealizationObservation, ) -from .drivers.libvirt import Connector, _existing_uuid, _raes_uuid -from .envelopes import load_libvirt_realization_envelope -from .techvault_appliance import ( - BusyboxInitramfsBuilder, - InitramfsBuilder, - copy_kernel_for_libvirt, - make_libvirt_readable, -) -from .techvault_concerns import techvault_observation_diagnostics, techvault_spec_diagnostics -from .techvault_lifecycle import ( +from ..drivers.libvirt import Connector, _existing_uuid, _raes_uuid +from ..envelopes import load_libvirt_realization_envelope +from ..techvault_appliance import BusyboxInitramfsBuilder, InitramfsBuilder +from ..techvault_concerns import techvault_observation_diagnostics, techvault_spec_diagnostics +from ..techvault_lifecycle import ( NativeOwnershipConflict as _OwnershipConflict, ) -from .techvault_lifecycle import ( +from ..techvault_lifecycle import ( deactivate_and_undefine as _deactivate_and_undefine, ) -from .techvault_lifecycle import ( +from ..techvault_lifecycle import ( resolve_native as _resolve_native, ) -from .techvault_lifecycle import ( +from ..techvault_lifecycle import ( verify_native_removed as _verify_native_removed, ) -from .techvault_matrix import ( - as_sequence as _as_sequence, -) -from .techvault_matrix import ( +from ..techvault_matrix import ( domain_xml as _domain_xml, ) -from .techvault_matrix import ( +from ..techvault_matrix import ( native_matrix as _native_matrix, ) -from .techvault_matrix import ( - network_xml as _network_xml, -) -from .techvault_matrix import ( +from ..techvault_matrix import ( safe_name as _safe_name, ) -from .techvault_observation import ( +from ..techvault_observation import ( canonical_digest, - domain_observations, file_digest, - network_observations, snapshot_from_observations, ) -from .techvault_probe import ( - NativeLibvirtProbe, - ProbeResult, - check_native_readiness, - expected_surface, - native_soc_readback, -) - -__all__ = [ - "BusyboxInitramfsBuilder", - "NativeLibvirtProbe", - "ProbeResult", - "TechVaultNativeLibvirtDriver", - "check_native_readiness", - "expected_surface", - "native_soc_readback", -] +from ._define import define_domain, define_domains, define_network, define_networks @dataclass @@ -285,81 +252,17 @@ def _guest_stage( def _define_networks( self, connection: object, matrix: Mapping[str, object] ) -> tuple[list[NetworkHandle], list[Diagnostic], list[RealizationObservation]]: - handles: list[NetworkHandle] = [] - diagnostics: list[Diagnostic] = [] - observations: list[RealizationObservation] = [] - for network in _as_sequence(matrix.get("networks")): - if isinstance(network, Mapping): - handle, diagnostic, observed = self._define_network(connection, network) - if handle is not None: - handles.append(handle) - if diagnostic is not None: - diagnostics.append(diagnostic) - observations.extend(observed) - if diagnostic is not None: - break - return handles, diagnostics, observations + return define_networks(self, connection, matrix) def _define_network( self, connection: object, network: Mapping[str, object] ) -> tuple[NetworkHandle | None, Diagnostic | None, tuple[RealizationObservation, ...]]: - address = str(network.get("address", "")) - native: _NativeResource | None = None - handle: NetworkHandle | None = None - diagnostic: Diagnostic | None = None - observations: tuple[RealizationObservation, ...] = () - self._names[address] = str(network.get("runtime_name", "")) - try: - _ensure_name_available( - connection, - "networkLookupByName", - str(network.get("runtime_name", "")), - address, - ) - native = _call(connection, "networkDefineXML", _network_xml(network)) - if not self.define_only: - native.create() - except _OwnershipConflict: - self._names.pop(address, None) - diagnostic = _diagnostic(_CODE_OWNERSHIP_CONFLICT, address) - except Exception: - if native is None: - self._names.pop(address, None) - else: - self._realized.add(address) - handle = NetworkHandle(address=address) - diagnostic = _diagnostic(_CODE_OPERATION_FAILED, address) - else: - self._realized.add(address) - handle = NetworkHandle(address=address, realized=True) - try: - observations = network_observations(native, network) - except Exception: - diagnostic = _diagnostic(_CODE_READBACK_FAILED, address) - return handle, diagnostic, observations + return define_network(self, connection, network) def _define_domains( self, connection: object, matrix: Mapping[str, object] ) -> tuple[list[DomainHandle], list[Diagnostic], list[RealizationObservation]]: - handles: list[DomainHandle] = [] - diagnostics: list[Diagnostic] = [] - observations: list[RealizationObservation] = [] - network_addresses = { - str(item.get("runtime_name", "")): str(item.get("address", "")) - for item in _as_sequence(matrix.get("networks")) - if isinstance(item, Mapping) - } - for domain in _as_sequence(matrix.get("domains")): - if isinstance(domain, Mapping): - handle, diagnostic, observed = self._define_domain(connection, domain, network_addresses) - if handle is not None: - handles.append(handle) - if diagnostic is not None: - diagnostics.append(diagnostic) - observations.extend(observed) - if diagnostic is not None: - break - return handles, diagnostics, observations + return define_domains(self, connection, matrix) def _define_domain( self, @@ -367,57 +270,7 @@ def _define_domain( domain: Mapping[str, object], network_addresses: Mapping[str, str], ) -> tuple[DomainHandle | None, Diagnostic | None, tuple[RealizationObservation, ...]]: - address = str(domain.get("address", "")) - native: _NativeResource | None = None - handle: DomainHandle | None = None - diagnostic: Diagnostic | None = None - observations: tuple[RealizationObservation, ...] = () - self._names[address] = str(domain.get("runtime_name", "")) - try: - _ensure_name_available( - connection, - "lookupByName", - str(domain.get("runtime_name", "")), - address, - ) - kernel = copy_kernel_for_libvirt( - self.kernel_path, - self.state_dir / "kernel" / f"{_artifact_token(address)}-{self.kernel_path.name}", - ) - initrd = self.initramfs_builder.build( - domain=domain, - target=self.state_dir / "initramfs" / f"{_artifact_token(address)}.cpio.gz", - ) - make_libvirt_readable(initrd) - self._artifacts[address] = (kernel, initrd) - native = _call(connection, "defineXML", self._render_domain_xml(domain, kernel=kernel, initrd=initrd)) - if not self.define_only: - native.create() - except _OwnershipConflict: - self._names.pop(address, None) - diagnostic = _diagnostic(_CODE_OWNERSHIP_CONFLICT, address) - except Exception: - if native is None: - self._cleanup_artifacts(address) - self._names.pop(address, None) - else: - self._realized.add(address) - handle = DomainHandle(address=address) - diagnostic = _diagnostic(_CODE_OPERATION_FAILED, address) - else: - self._realized.add(address) - handle = DomainHandle(address=address, realized=True) - try: - observations = domain_observations( - native, - domain, - network_addresses, - kernel=kernel, - initrd=initrd, - ) - except Exception: - diagnostic = _diagnostic(_CODE_READBACK_FAILED, address) - return handle, diagnostic, observations + return define_domain(self, connection, domain, network_addresses) def destroy( self, diff --git a/implementations/python/packages/raes_backend_protocols/capabilities.py b/implementations/python/packages/raes_backend_protocols/capabilities.py index 34ab01d8b..2fec5d49b 100644 --- a/implementations/python/packages/raes_backend_protocols/capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/capabilities.py @@ -15,6 +15,9 @@ _participant_capabilities.PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS ) PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE = _participant_capabilities.PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE +PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES = ( + _participant_capabilities.PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES +) PARTICIPANT_RUNTIME_POLICY_FEATURES = _participant_capabilities.PARTICIPANT_RUNTIME_POLICY_FEATURES PARTICIPANT_RUNTIME_ROLE_SCOPE = _participant_capabilities.PARTICIPANT_RUNTIME_ROLE_SCOPE ParticipantFeatureSupport = _participant_capabilities.ParticipantFeatureSupport diff --git a/implementations/python/packages/raes_backend_protocols/manifest.py b/implementations/python/packages/raes_backend_protocols/manifest.py index a8c53f91e..0107bed93 100644 --- a/implementations/python/packages/raes_backend_protocols/manifest.py +++ b/implementations/python/packages/raes_backend_protocols/manifest.py @@ -4,7 +4,12 @@ from typing import Any -from raes_contracts.apparatus import ApparatusIdentity, ConceptBinding, RealizationSupportDeclaration +from raes_contracts.apparatus import ( + ApparatusIdentity, + ConceptBinding, + RealizationObservationCapability, + RealizationSupportDeclaration, +) from raes_contracts.contracts import ( ApparatusIdentityModel, BackendCapabilitiesV2Model, @@ -17,6 +22,7 @@ OrchestratorCapabilitiesModel, ParticipantFeatureSupportModel, ParticipantRuntimeCapabilitiesModel, + RealizationObservationCapabilityModel, RealizationSupportDeclarationModel, TimeCapabilitiesModel, ) @@ -91,6 +97,13 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod supported_constraint_kinds=sorted(declaration.supported_constraint_kinds), supported_exact_requirement_kinds=sorted(declaration.supported_exact_requirement_kinds), disclosure_kinds=sorted(declaration.disclosure_kinds), + observation_capabilities={ + concern_kind: RealizationObservationCapabilityModel( + verification_scope=capability.verification_scope, + observation_strength=capability.observation_strength, + ) + for concern_kind, capability in sorted(declaration.observation_capabilities.items()) + }, artifact_mechanisms=list(declaration.artifact_mechanisms), constraints=dict(declaration.constraints), ) @@ -257,6 +270,13 @@ def _realization_support_from_model(model: RealizationSupportDeclarationModel) - supported_constraint_kinds=frozenset(model.supported_constraint_kinds), supported_exact_requirement_kinds=frozenset(model.supported_exact_requirement_kinds), disclosure_kinds=frozenset(model.disclosure_kinds), + observation_capabilities={ + concern_kind: RealizationObservationCapability( + verification_scope=capability.verification_scope, + observation_strength=capability.observation_strength, + ) + for concern_kind, capability in model.observation_capabilities.items() + }, artifact_mechanisms=tuple(model.artifact_mechanisms), constraints=dict(model.constraints), ) diff --git a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py index 253b62631..f4964ab62 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_capabilities.py +++ b/implementations/python/packages/raes_backend_protocols/participant_capabilities.py @@ -7,6 +7,7 @@ from raes_contracts.manifest_authority import ( PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, PARTICIPANT_RUNTIME_POLICY_FEATURES, PARTICIPANT_RUNTIME_ROLE_SCOPE, @@ -71,7 +72,7 @@ def _validate_participant_feature_evidence( raise ValueError( "ParticipantFeatureSupport disclosure_refs must be non-empty when support_level is below exact" ) - if feature not in PARTICIPANT_RUNTIME_POLICY_FEATURES: + if feature not in PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES: return if support_level != ParticipantFeatureSupportLevel.EXACT and not limitation_refs: raise ValueError("ParticipantFeatureSupport limitation_refs must be non-empty for below-exact policy support") @@ -248,7 +249,7 @@ def _validate_feature_support(self, feature_support: tuple[ParticipantFeatureSup _validate_unique_non_empty_strings("ParticipantRuntimeCapabilities.feature_support", feature_names) supported_features = self.supported_behavior_features | self.supported_interaction_features missing_policy_declarations = sorted( - (supported_features & PARTICIPANT_RUNTIME_POLICY_FEATURES) - set(feature_names) + (supported_features & PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES) - set(feature_names) ) if missing_policy_declarations: raise ValueError( @@ -264,7 +265,7 @@ def _validate_feature_support(self, feature_support: tuple[ParticipantFeatureSup "ParticipantRuntimeCapabilities.feature_support cannot declare a supported feature unsupported" ) if ( - entry.feature in PARTICIPANT_RUNTIME_POLICY_FEATURES + entry.feature in PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES and entry.support_level != ParticipantFeatureSupportLevel.UNSUPPORTED and entry.feature not in supported_features ): @@ -402,6 +403,7 @@ def _has_autonomous_configuration(self) -> bool: "PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE", "PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS", "PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE", + "PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES", "PARTICIPANT_RUNTIME_POLICY_FEATURES", "PARTICIPANT_RUNTIME_ROLE_SCOPE", "ParticipantFeatureSupport", diff --git a/implementations/python/packages/raes_backend_protocols/participant_feature_admission.py b/implementations/python/packages/raes_backend_protocols/participant_feature_admission.py index f0855104d..5887eef44 100644 --- a/implementations/python/packages/raes_backend_protocols/participant_feature_admission.py +++ b/implementations/python/packages/raes_backend_protocols/participant_feature_admission.py @@ -10,8 +10,8 @@ from .capabilities import ( PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, - PARTICIPANT_RUNTIME_POLICY_FEATURES, ParticipantFeatureSupport, ) @@ -62,7 +62,7 @@ def _participant_feature_declaration( supported_features = capability.supported_behavior_features | capability.supported_interaction_features declaration = next((entry for entry in capability.feature_support if entry.feature == feature), None) if declaration is None: - if feature in supported_features and feature not in PARTICIPANT_RUNTIME_POLICY_FEATURES: + if feature in supported_features and feature not in PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES: return None raise ValueError(f"participant feature '{feature}' has no explicit support declaration") if declaration.support_level == ParticipantFeatureSupportLevel.UNSUPPORTED: @@ -80,7 +80,7 @@ def _validate_participant_feature_evidence( raise ValueError( f"participant feature '{feature}' is missing required contracts: {', '.join(missing_contracts)}" ) - if not declaration.evidence_refs and feature in PARTICIPANT_RUNTIME_POLICY_FEATURES: + if not declaration.evidence_refs and feature in PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES: raise ValueError(f"participant feature '{feature}' has no conformance evidence") diff --git a/implementations/python/packages/raes_backend_stubs/manifest.py b/implementations/python/packages/raes_backend_stubs/manifest.py index 98f30ffd4..782a91353 100644 --- a/implementations/python/packages/raes_backend_stubs/manifest.py +++ b/implementations/python/packages/raes_backend_stubs/manifest.py @@ -21,8 +21,8 @@ CLEANUP_CAPABILITY_REQUIRED_CONTRACTS, PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, - PARTICIPANT_RUNTIME_POLICY_FEATURES, PARTICIPANT_RUNTIME_ROLE_SCOPE, TIME_CAPABILITY_REQUIRED_CONTRACTS, BackendCapabilitySet, @@ -52,7 +52,7 @@ REFERENCE_PARTICIPANT_BEHAVIOR_FEATURES = ( frozenset(PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE]) - {"autonomous_execution"} - - PARTICIPANT_RUNTIME_POLICY_FEATURES + - PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES ) REFERENCE_PARTICIPANT_INTERACTION_FEATURES = frozenset( PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE] @@ -246,7 +246,7 @@ def _stub_participant_runtime() -> ParticipantRuntimeCapabilities: limitation_refs=(f"limitation:{feature}:not-realized",), disclosure_refs=(f"disclosure:{feature}:unsupported",), ) - for feature in sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + for feature in sorted(PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES) ), ) diff --git a/implementations/python/packages/raes_conformance/artifact_transformations.py b/implementations/python/packages/raes_conformance/artifact_transformations.py new file mode 100644 index 000000000..05b9a5506 --- /dev/null +++ b/implementations/python/packages/raes_conformance/artifact_transformations.py @@ -0,0 +1,221 @@ +"""Focused conformance runner for checked-in artifact-transformation cases.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, model_validator +from raes import ( + ArtifactTransformationPolicy, + RemoveSDLDeclarationRequest, + RenameSDLDeclarationRequest, + canonicalize_portable_contract, + parse_sdl_file, + remove_sdl_declaration, + rename_sdl_declaration, +) +from raes_contracts.contracts import ( + ArtifactTransformationLossKind, + ArtifactTransformationStatus, + ExternalConceptBindingDocumentModel, +) +from raes_contracts.corpus import FIXTURES, corpus_family_root +from raes_contracts.json_ingress import parse_bounded_json_object + +from raes_conformance.conformance.diagnostics import sanitized_failure_message + +_MAX_CASE_BYTES = 32 * 1024 +_MAX_PORTABLE_CONTRACT_BYTES = 1024 * 1024 + + +class _RequestModel(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + target_address: str | None = Field(default=None, min_length=1, max_length=4096) + new_local_name: str | None = Field(default=None, min_length=1, max_length=128) + + +class _PolicyModel(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + allowed_loss_kinds: tuple[ArtifactTransformationLossKind, ...] = () + + +class _ExpectedIdentityModel(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + before: str = Field(min_length=1, max_length=4096) + after: str = Field(min_length=1, max_length=4096) + + +class _ExpectedModel(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + status: ArtifactTransformationStatus + identity_map: tuple[_ExpectedIdentityModel, ...] = () + + +class ArtifactTransformationFixtureCaseModel(BaseModel): + """Closed checked-in transformation case descriptor.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + case_id: str = Field(pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$", max_length=128) + operation: Literal[ + "rename-sdl-declaration/v1", + "remove-sdl-declaration/v1", + "canonicalize-portable-contract/v1", + ] + source_contract: Literal["sdl-authoring-input/v1", "external-concept-bindings/v1"] + source_path: str = Field(pattern=r"^[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*$", max_length=512) + request: _RequestModel + policy: _PolicyModel + expected: _ExpectedModel + + @model_validator(mode="after") + def _validate_operation_shape(self) -> ArtifactTransformationFixtureCaseModel: + if self.operation == "rename-sdl-declaration/v1": + if self.source_contract != "sdl-authoring-input/v1": + raise ValueError("rename cases require an SDL source") + if self.request.target_address is None or self.request.new_local_name is None: + raise ValueError("rename cases require target_address and new_local_name") + elif self.operation == "remove-sdl-declaration/v1": + if self.source_contract != "sdl-authoring-input/v1": + raise ValueError("removal cases require an SDL source") + if self.request.target_address is None or self.request.new_local_name is not None: + raise ValueError("removal cases require only target_address") + elif ( + self.source_contract != "external-concept-bindings/v1" + or self.request.target_address is not None + or self.request.new_local_name is not None + ): + raise ValueError("portable canonicalization cases require an empty request") + return self + + +@dataclass(frozen=True, slots=True) +class ArtifactTransformationConformanceCaseResult: + case_id: str + passed: bool + status: ArtifactTransformationStatus | None + report_digest: str | None + diagnostic: str | None = None + + +@dataclass(frozen=True, slots=True) +class ArtifactTransformationConformanceReport: + profile: str + passed: bool + cases: tuple[ArtifactTransformationConformanceCaseResult, ...] + + +def _confined_fixture_path(fixtures: Path, relative: str) -> Path: + root = fixtures.resolve() + candidate = (root / relative).resolve() + try: + candidate.relative_to(root) + except ValueError as exc: + raise ValueError("transformation fixture source resolves outside the fixture corpus") from exc + return candidate + + +def _load_case(path: Path) -> ArtifactTransformationFixtureCaseModel: + payload = parse_bounded_json_object(path.read_bytes(), max_bytes=_MAX_CASE_BYTES) + return ArtifactTransformationFixtureCaseModel.model_validate(payload) + + +def _execute_case( + case: ArtifactTransformationFixtureCaseModel, + *, + fixtures: Path, +) -> ArtifactTransformationConformanceCaseResult: + source_path = _confined_fixture_path(fixtures, case.source_path) + if case.operation == "canonicalize-portable-contract/v1": + payload = parse_bounded_json_object( + source_path.read_bytes(), + max_bytes=_MAX_PORTABLE_CONTRACT_BYTES, + ) + source = ExternalConceptBindingDocumentModel.model_validate(payload) + transformed = canonicalize_portable_contract(source) + output_present = transformed.output is not None + report = transformed.report + else: + source = parse_sdl_file(source_path) + if case.operation == "rename-sdl-declaration/v1": + if case.request.target_address is None or case.request.new_local_name is None: + raise ValueError("admitted rename case is missing its request fields") + transformed = rename_sdl_declaration( + source, + RenameSDLDeclarationRequest( + target_address=case.request.target_address, + new_local_name=case.request.new_local_name, + ), + ) + else: + if case.request.target_address is None: + raise ValueError("admitted removal case is missing target_address") + transformed = remove_sdl_declaration( + source, + RemoveSDLDeclarationRequest(target_address=case.request.target_address), + policy=ArtifactTransformationPolicy(case.policy.allowed_loss_kinds), + ) + output_present = transformed.output is not None + report = transformed.report + + actual_identity_map = tuple((item.before, item.after) for item in report.identity_map) + expected_identity_map = tuple((item.before, item.after) for item in case.expected.identity_map) + all_or_none = output_present == (report.status == ArtifactTransformationStatus.SUCCESS) + passed = ( + report.operation_profile == case.operation + and report.status == case.expected.status + and actual_identity_map == expected_identity_map + and all_or_none + ) + return ArtifactTransformationConformanceCaseResult( + case_id=case.case_id, + passed=passed, + status=report.status, + report_digest=report.derivation_digest, + diagnostic=None if passed else "transformation result did not match the closed expected outcome", + ) + + +def run_artifact_transformation_fixture_suite( + *, + root: Path | None = None, +) -> ArtifactTransformationConformanceReport: + """Execute transformation cases in stable order against the production APIs.""" + + fixtures = corpus_family_root(FIXTURES) if root is None else root + cases_root = fixtures / "artifact-transformations-v1" / "cases" + results: list[ArtifactTransformationConformanceCaseResult] = [] + for case_path in sorted(cases_root.glob("*.json")): + try: + case = _load_case(case_path) + results.append(_execute_case(case, fixtures=fixtures)) + except Exception as exc: + results.append( + ArtifactTransformationConformanceCaseResult( + case_id=case_path.stem, + passed=False, + status=None, + report_digest=None, + diagnostic=sanitized_failure_message(exc), + ) + ) + case_tuple = tuple(results) + return ArtifactTransformationConformanceReport( + profile="artifact-transformations/v1", + passed=bool(case_tuple) and all(case.passed for case in case_tuple), + cases=case_tuple, + ) + + +__all__ = [ + "ArtifactTransformationConformanceCaseResult", + "ArtifactTransformationConformanceReport", + "ArtifactTransformationFixtureCaseModel", + "run_artifact_transformation_fixture_suite", +] diff --git a/implementations/python/packages/raes_conformance/conformance/participant_opacity_probes.py b/implementations/python/packages/raes_conformance/conformance/participant_opacity_probes.py new file mode 100644 index 000000000..14a1c04ad --- /dev/null +++ b/implementations/python/packages/raes_conformance/conformance/participant_opacity_probes.py @@ -0,0 +1,432 @@ +"""Runner-owned backend realization probes for participant predicate opacity.""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +from enum import StrEnum +from typing import Protocol, cast + +from raes_backend_protocols.capabilities import ParticipantFeatureSupport, resolve_participant_feature_support +from raes_backend_protocols.manifest import backend_manifest_payload +from raes_contracts.behavioral_relation_profiles import load_behavioral_relation_profile +from raes_contracts.behavioral_relations import validate_behavioral_claim_binding +from raes_contracts.canonical import canonical_json_digest +from raes_contracts.contracts import BehavioralClaimBindingModel +from raes_contracts.diagnostics import Diagnostic +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel +from raes_runtime.registry import RuntimeTarget + +from raes_conformance.conformance.diagnostics import _diagnostic, sanitized_failure_message +from raes_conformance.conformance.participant_policy_execution import _instrumented_target +from raes_conformance.conformance.report import ConformanceCaseResult + +_FEATURE = "participant_predicate_opacity" +_RELATION = "participant-predicate-opacity" + + +class ParticipantOpacityRealizationOwner(StrEnum): + """Who actually realizes the measured relation for a case.""" + + DECLARATION_ONLY = "declaration-only" + RUNTIME_MEDIATED = "runtime-mediated" + BACKEND_NATIVE = "backend-native" + + +@dataclass(frozen=True) +class ParticipantOpacityProbeObservation: + """Closed, non-secret observation transcript compared by the runner.""" + + decision: str + failure: str + action_availability: str + delivery: str + omission: str + retry: str + logical_timing: str + logical_order: str + policy_release_effect: str + external_effect: str + payload_released: bool + + +@dataclass(frozen=True) +class ParticipantOpacityProbeCase: + """Typed backend-specific inputs; no pass boolean or prebuilt report.""" + + name: str + profile_id: str + profile_revision: str + profile_digest: str + actual_point_ref: str + alternative_point_ref: str + expected_observation: ParticipantOpacityProbeObservation + realization_owner: ParticipantOpacityRealizationOwner + execution_basis: str + manifest_digest: str + configuration_digest: str + tool_digest: str + environment_digest: str + evidence_refs: tuple[str, ...] + limitations: tuple[str, ...] + explicit_non_claims: tuple[str, ...] + + +class ParticipantOpacityProbeHarness(Protocol): + """Backend setup and observation adapter used by the generic runner.""" + + probe_set_digest: str + + def cases(self, target: RuntimeTarget) -> tuple[ParticipantOpacityProbeCase, ...]: ... + + def observe( + self, + target: RuntimeTarget, + case: ParticipantOpacityProbeCase, + point_ref: str, + ) -> ParticipantOpacityProbeObservation: ... + + +@dataclass(frozen=True) +class _ProbeExecution: + observations: tuple[ParticipantOpacityProbeObservation, ...] + diagnostics: tuple[Diagnostic, ...] + backend_calls: int + + +def _claim( + *, + case: ParticipantOpacityProbeCase, + target: RuntimeTarget, + axis: str, + status: str, + evidence_scope: str, + evidence_refs: tuple[str, ...], +) -> BehavioralClaimBindingModel: + profile = load_behavioral_relation_profile(case.profile_id) + return validate_behavioral_claim_binding( + BehavioralClaimBindingModel( + taxonomy_id=profile.taxonomy_id, + taxonomy_revision=profile.taxonomy_revision, + relation_id=_RELATION, + subject=f"Participant predicate opacity for backend target {target.name}", + left_carrier_ref=profile.left_carrier_ref, + observation_projection_ref=profile.observation_projection_ref, + observation_projection_revision=profile.observation_projection_revision, + relation_parameter_profile_ref=profile.profile_id, + relation_parameter_profile_revision=profile.profile_revision, + quantifier_scope="single-artifact" if evidence_scope == "structural" else "finite-cases", + evidence_scope=evidence_scope, + assurance_axis=axis, + evidence_boundary=( + "The exact named backend manifest declaration." + if evidence_scope == "structural" + else "The exact two finite profile points and complete observation transcript in this case." + ), + assurance_status=status, + evidence_refs=list(evidence_refs), + limitations=list(case.limitations), + explicit_non_claims=list(case.explicit_non_claims), + ) + ) + + +def _declaration_claim( + case: ParticipantOpacityProbeCase, + target: RuntimeTarget, + evidence_refs: tuple[str, ...], +) -> BehavioralClaimBindingModel: + return _claim( + case=case, + target=target, + axis="backend-declaration", + status="declared", + evidence_scope="structural", + evidence_refs=evidence_refs, + ) + + +def _unsupported_case(target: RuntimeTarget) -> ConformanceCaseResult: + profile = load_behavioral_relation_profile("participant-opacity-runtime-reference-v1") + declaration = next( + entry + for entry in target.manifest.participant_runtime.feature_support # type: ignore[union-attr] + if entry.feature == _FEATURE + ) + seed = ParticipantOpacityProbeCase( + name="participant-opacity-backend-not-executed", + profile_id=profile.profile_id, + profile_revision=profile.profile_revision, + profile_digest=profile.canonical_digest, + actual_point_ref="possible-point:runtime-reference-protected", + alternative_point_ref="possible-point:runtime-reference-complement", + expected_observation=ParticipantOpacityProbeObservation("", "", "", "", "", "", "", "", "", "", False), + realization_owner=ParticipantOpacityRealizationOwner.DECLARATION_ONLY, + execution_basis="fixture-only", + manifest_digest="", + configuration_digest="", + tool_digest="", + environment_digest="", + evidence_refs=declaration.evidence_refs, + limitations=(*declaration.limitation_refs, "No backend probe was executed."), + explicit_non_claims=("No backend realization or conformance is established.",), + ) + return ConformanceCaseResult( + name=seed.name, + contract_name="backend-manifest-v2", + valid=True, + passed=False, + outcome="unsupported", + capability_feature=_FEATURE, + declared_support_level=declaration.support_level.value, + effective_support_level=None, + evidence_refs=declaration.evidence_refs, + limitations=seed.limitations, + explicit_non_claims=seed.explicit_non_claims, + claim_bindings=(_declaration_claim(seed, target, declaration.evidence_refs),), + realization_owner=seed.realization_owner.value, + profile_digest=profile.canonical_digest, + manifest_digest=canonical_json_digest(backend_manifest_payload(target.manifest)), + ) + + +def _validate_probe_case( + target: RuntimeTarget, + case: ParticipantOpacityProbeCase, + declaration: ParticipantFeatureSupport, +) -> None: + """Bind a probe case to the governed profile and exact target manifest.""" + + profile = load_behavioral_relation_profile(case.profile_id) + if case.profile_revision != profile.profile_revision or case.profile_digest != profile.canonical_digest: + raise ValueError("probe profile coordinates do not match the governed profile") + declared_profile = f"profile:{case.profile_id}@{case.profile_revision}" + if declared_profile not in declaration.constraint_refs: + raise ValueError("probe profile is outside the manifest's declared support constraints") + actual_manifest_digest = canonical_json_digest(backend_manifest_payload(target.manifest)) + if case.manifest_digest != actual_manifest_digest: + raise ValueError("probe manifest digest does not match the target declaration") + if case.execution_basis not in {"fixture-only", "hermetic-live", "native-live"}: + raise ValueError("probe execution basis is unsupported") + + +def _execute_probe( + target: RuntimeTarget, + harness: ParticipantOpacityProbeHarness, + case: ParticipantOpacityProbeCase, +) -> _ProbeExecution: + """Collect the closed transcript and retain only sanitized failures.""" + + instrumented, counter = _instrumented_target(target) + observations: list[ParticipantOpacityProbeObservation] = [] + diagnostics: list[Diagnostic] = [] + try: + for point_ref in (case.actual_point_ref, case.alternative_point_ref): + observations.append(harness.observe(instrumented, case, point_ref)) + except Exception as exc: + diagnostics.append( + _diagnostic( + "conformance.participant-opacity-probe-rejected", + case.name, + sanitized_failure_message(exc), + ) + ) + backend_calls = counter.calls if counter is not None else 0 + return _ProbeExecution(tuple(observations), tuple(diagnostics), backend_calls) + + +def _probe_diagnostics( + case: ParticipantOpacityProbeCase, + execution: _ProbeExecution, + observations_match: bool, +) -> tuple[Diagnostic, ...]: + """Evaluate mediation, ownership, and transcript consistency.""" + + diagnostics = list(execution.diagnostics) + if execution.backend_calls == 0: + diagnostics.append( + _diagnostic( + "conformance.participant-opacity-backend-unobserved", + case.name, + "No participant-runtime call was observed; mediation cannot establish backend-native ownership.", + ) + ) + if case.realization_owner is not ParticipantOpacityRealizationOwner.BACKEND_NATIVE: + diagnostics.append( + _diagnostic( + "conformance.participant-opacity-realization-not-backend-native", + case.name, + "Runtime mediation or a declaration alone cannot establish backend-native realization.", + ) + ) + if execution.observations and not observations_match: + diagnostics.append( + _diagnostic( + "conformance.participant-opacity-observation-mismatch", + case.name, + "The measured complete observation transcript differs across the governed opacity points.", + ) + ) + return tuple(diagnostics) + + +def _case_claims( + case: ParticipantOpacityProbeCase, + target: RuntimeTarget, + declaration: ParticipantFeatureSupport, + passed: bool, +) -> tuple[BehavioralClaimBindingModel, ...]: + """Build the declaration-to-realization claim chain for one probe.""" + + claims = [_declaration_claim(case, target, declaration.evidence_refs)] + if passed: + claims.extend( + ( + _claim( + case=case, + target=target, + axis="backend-realization", + status="realized", + evidence_scope="finite", + evidence_refs=case.evidence_refs, + ), + _claim( + case=case, + target=target, + axis="backend-conformance", + status="conformant", + evidence_scope="finite", + evidence_refs=case.evidence_refs, + ), + ) + ) + return tuple(claims) + + +def _run_case( + target: RuntimeTarget, + harness: ParticipantOpacityProbeHarness, + case: ParticipantOpacityProbeCase, +) -> ConformanceCaseResult: + declaration = resolve_participant_feature_support( + target.manifest, + _FEATURE, + required_level=ParticipantFeatureSupportLevel.BOUNDED, + ) + assert declaration is not None + _validate_probe_case(target, case, declaration) + execution = _execute_probe(target, harness, case) + observations_match = len(execution.observations) == 2 and all( + observation == case.expected_observation for observation in execution.observations + ) + diagnostics = _probe_diagnostics(case, execution, observations_match) + passed = not diagnostics and observations_match + return ConformanceCaseResult( + name=case.name, + contract_name="backend-manifest-v2", + valid=True, + passed=passed, + outcome="passed" if passed else "failed", + probe_kind="participant-opacity-complete-transcript", + probe_digest=canonical_json_digest( + { + "case": case.name, + "observation_count": len(execution.observations), + "observations_match": observations_match, + "backend_calls": execution.backend_calls, + } + ), + probe_set_digest=harness.probe_set_digest, + configuration_digest=case.configuration_digest, + evidence_refs=case.evidence_refs, + capability_feature=_FEATURE, + declared_support_level=declaration.support_level.value, + effective_support_level=declaration.support_level.value if passed else None, + finite_scope="The exact two named possible points and closed observation transcript.", + limitations=case.limitations, + explicit_non_claims=case.explicit_non_claims, + diagnostics=diagnostics, + claim_bindings=_case_claims(case, target, declaration, passed), + realization_owner=case.realization_owner.value, + profile_digest=case.profile_digest, + manifest_digest=case.manifest_digest, + tool_digest=case.tool_digest, + environment_digest=case.environment_digest, + ) + + +def _harness_failure_case( + target: RuntimeTarget, + code: str, + message: str, + *, + outcome: str, +) -> ConformanceCaseResult: + """Retain the positive declaration while refusing absent probe evidence.""" + + case = _unsupported_case(target) + return cast( + ConformanceCaseResult, + replace( + case, + diagnostics=(_diagnostic(code, _FEATURE, message),), + outcome=outcome, + ), + ) + + +def _run_harness_cases( + target: RuntimeTarget, + harness: ParticipantOpacityProbeHarness, +) -> tuple[ConformanceCaseResult, ...]: + """Convert adapter failures into bounded, non-passing report cases.""" + + try: + probe_cases = harness.cases(target) + if probe_cases: + results = tuple(_run_case(target, harness, case) for case in probe_cases) + else: + results = ( + _harness_failure_case( + target, + "conformance.participant-opacity-harness-empty", + "The opacity harness supplied no cases for a positive declaration.", + outcome="unsupported", + ), + ) + except Exception as exc: + results = ( + _harness_failure_case( + target, + "conformance.participant-opacity-harness-rejected", + sanitized_failure_message(exc), + outcome="failed", + ), + ) + return results + + +def participant_opacity_cases( + target: RuntimeTarget, + harness: ParticipantOpacityProbeHarness | None, +) -> tuple[ConformanceCaseResult, ...]: + """Run backend opacity probes or retain an explicit unsupported result.""" + + capability = target.manifest.participant_runtime + results: tuple[ConformanceCaseResult, ...] = () + if capability is not None: + declaration = next((entry for entry in capability.feature_support if entry.feature == _FEATURE), None) + supported = ( + declaration is not None and declaration.support_level is not ParticipantFeatureSupportLevel.UNSUPPORTED + ) + if supported: + results = (_unsupported_case(target),) if harness is None else _run_harness_cases(target, harness) + return results + + +__all__ = [ + "ParticipantOpacityProbeCase", + "ParticipantOpacityProbeHarness", + "ParticipantOpacityProbeObservation", + "ParticipantOpacityRealizationOwner", + "participant_opacity_cases", +] diff --git a/implementations/python/packages/raes_conformance/conformance/reference_participant_opacity.py b/implementations/python/packages/raes_conformance/conformance/reference_participant_opacity.py new file mode 100644 index 000000000..1fdd42254 --- /dev/null +++ b/implementations/python/packages/raes_conformance/conformance/reference_participant_opacity.py @@ -0,0 +1,83 @@ +"""Reference-backend adapter for the governed participant-opacity probes.""" + +from __future__ import annotations + +from raes_backend_protocols.manifest import backend_manifest_payload +from raes_contracts.behavioral_relation_profiles import load_behavioral_relation_profile +from raes_contracts.canonical import canonical_json_digest +from raes_runtime.registry import RuntimeTarget + +from raes_conformance.conformance.participant_opacity_probes import ( + ParticipantOpacityProbeCase, + ParticipantOpacityProbeObservation, + ParticipantOpacityRealizationOwner, +) + +_PROFILE_ID = "participant-opacity-runtime-reference-v1" +_EXPECTED_OBSERVATION = ParticipantOpacityProbeObservation( + decision="deny", + failure="uniform-refusal", + action_availability="denied", + delivery="withheld", + omission="recorded-at-governed-opportunity", + retry="stable-replay", + logical_timing="logical-bucket:contained", + logical_order="stable-causal-order", + policy_release_effect="contained", + external_effect="none", + payload_released=False, +) + + +class ReferenceParticipantOpacityHarness: + """Supply reference-backend inputs while the generic runner owns verdicts.""" + + probe_set_digest = canonical_json_digest({"probe_set": "participant-opacity-backend/rev1"}) + configuration_digest = canonical_json_digest({"configuration": "reference-emulation/default"}) + tool_digest = canonical_json_digest({"tool": "raes-conformance-participant-opacity/rev1"}) + environment_digest = canonical_json_digest({"environment": "in-process-reference"}) + + def cases(self, target: RuntimeTarget) -> tuple[ParticipantOpacityProbeCase, ...]: + profile = load_behavioral_relation_profile(_PROFILE_ID) + return ( + ParticipantOpacityProbeCase( + name="complete-observation-transcript", + profile_id=profile.profile_id, + profile_revision=profile.profile_revision, + profile_digest=profile.canonical_digest, + actual_point_ref="possible-point:runtime-reference-protected", + alternative_point_ref="possible-point:runtime-reference-complement", + expected_observation=_EXPECTED_OBSERVATION, + realization_owner=ParticipantOpacityRealizationOwner.BACKEND_NATIVE, + execution_basis="hermetic-live", + manifest_digest=canonical_json_digest(backend_manifest_payload(target.manifest)), + configuration_digest=self.configuration_digest, + tool_digest=self.tool_digest, + environment_digest=self.environment_digest, + evidence_refs=("conformance:participant-opacity-backend:complete-transcript",), + limitations=("Limited to the named finite reference profile and probe set.",), + explicit_non_claims=( + "No universal backend opacity, proof, model check, or cross-backend equivalence.", + ), + ), + ) + + @staticmethod + def observe( + target: RuntimeTarget, + case: ParticipantOpacityProbeCase, + point_ref: str, + ) -> ParticipantOpacityProbeObservation: + runtime = target.participant_runtime + if runtime is None: + raise ValueError("reference participant runtime is unavailable") + payload = runtime.participant_relation_probe( + relation_id="participant-predicate-opacity", + profile_id=case.profile_id, + profile_revision=case.profile_revision, + possible_point_ref=point_ref, + ) + return ParticipantOpacityProbeObservation(**payload) + + +__all__ = ["ReferenceParticipantOpacityHarness"] diff --git a/implementations/python/packages/raes_conformance/conformance/report.py b/implementations/python/packages/raes_conformance/conformance/report.py index bcf816a7b..4cb27ea01 100644 --- a/implementations/python/packages/raes_conformance/conformance/report.py +++ b/implementations/python/packages/raes_conformance/conformance/report.py @@ -95,6 +95,12 @@ class ConformanceCaseResult: limitations: tuple[str, ...] = () explicit_non_claims: tuple[str, ...] = () policy_binding: ParticipantPolicyBinding | None = None + claim_bindings: tuple[BehavioralClaimBindingModel, ...] = () + realization_owner: str | None = None + profile_digest: str | None = None + manifest_digest: str | None = None + tool_digest: str | None = None + environment_digest: str | None = None def __post_init__(self) -> None: """Keep the closed outcome vocabulary aligned with the gating boolean.""" @@ -161,6 +167,69 @@ def _validate_catalog_bindings(report: BackendConformanceReport) -> None: for case in report.cases: if case.policy_binding is not None: validate_behavioral_claim_binding(case.policy_binding.claim) + for binding in case.claim_bindings: + validate_behavioral_claim_binding(binding) + _validate_case_claim_chain(case) + + +def _validate_case_claim_chain(case: ConformanceCaseResult) -> None: + """Require declaration, realization, and conformance to remain distinct.""" + + if not case.claim_bindings: + return + axes = {binding.assurance_axis for binding in case.claim_bindings} + _validate_claim_axes(axes) + _validate_claim_coordinates(case) + if "backend-conformance" in axes: + _validate_backend_conformance_claim(case) + + +def _validate_claim_axes(axes: set[str | None]) -> None: + """Require each stronger assurance axis to retain its prerequisites.""" + + if "backend-realization" in axes and "backend-declaration" not in axes: + raise ValueError("backend realization claim requires a backend declaration claim") + if "backend-conformance" in axes and "backend-realization" not in axes: + raise ValueError("backend conformance claim requires a backend realization claim") + if "backend-conformance" in axes and "backend-declaration" not in axes: + raise ValueError("backend conformance claim requires a backend declaration claim") + + +def _validate_claim_coordinates(case: ConformanceCaseResult) -> None: + """Require every assurance step to use one governed relation coordinate.""" + + coordinates = { + ( + binding.taxonomy_id, + binding.taxonomy_revision, + binding.relation_id, + binding.relation_parameter_profile_ref, + binding.relation_parameter_profile_revision, + ) + for binding in case.claim_bindings + } + if len(coordinates) != 1: + raise ValueError("backend assurance claims for one case must use identical governed coordinates") + + +def _validate_backend_conformance_claim(case: ConformanceCaseResult) -> None: + """Require backend-native ownership and exact evidence coordinates.""" + + if case.realization_owner != "backend-native": + raise ValueError("backend conformance claim requires backend-native realization ownership") + digests = ( + case.profile_digest, + case.manifest_digest, + case.configuration_digest, + case.tool_digest, + case.environment_digest, + case.probe_set_digest, + ) + if any(not digest for digest in digests): + raise ValueError( + "backend conformance claim requires exact profile, manifest, configuration, " + "tool, environment, and probe-set digests" + ) def _validate_claim_strength(report: BackendConformanceReport) -> None: @@ -270,6 +339,12 @@ def backend_conformance_report_payload(report: BackendConformanceReport) -> dict "limitations": list(case.limitations), "explicit_non_claims": list(case.explicit_non_claims), "policy_binding": _policy_binding_payload(case.policy_binding), + "claim_bindings": [binding.model_dump(mode="json") for binding in case.claim_bindings], + "realization_owner": case.realization_owner, + "profile_digest": case.profile_digest, + "manifest_digest": case.manifest_digest, + "tool_digest": case.tool_digest, + "environment_digest": case.environment_digest, "diagnostics": [_diagnostic_payload(diag) for diag in case.diagnostics], } for case in report.cases diff --git a/implementations/python/packages/raes_conformance/conformance/semantics.py b/implementations/python/packages/raes_conformance/conformance/semantics.py index 1b5e03ca9..a1d97d36d 100644 --- a/implementations/python/packages/raes_conformance/conformance/semantics.py +++ b/implementations/python/packages/raes_conformance/conformance/semantics.py @@ -4,6 +4,7 @@ from collections.abc import Callable +from raes_contracts.contracts import ParticipantInformationStateContextResolver from raes_contracts.diagnostics import Diagnostic, Severity from raes_contracts.evaluation import EvaluationExecutionState from raes_contracts.participant_episode import ( @@ -34,6 +35,10 @@ "full external-concept binding conformance requires explicit exact RAES subjects and pinned local " "scheme snapshots; the generic fixture runner establishes structural validity only" ), + "participant-information-state-record-v1": ( + "full participant information-state conformance requires governed source, occurrence-history, proof, " + "and decision-surface resolution" + ), } @@ -128,7 +133,17 @@ def _participant_behavior_stream_diagnostics(contract_name: str, payload: object } -def _semantic_diagnostics(contract_name: str, payload: object) -> list[Diagnostic]: +def _semantic_diagnostics( + contract_name: str, + payload: object, + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, +) -> list[Diagnostic]: + if contract_name == "runtime-snapshot-v1": + return _runtime_snapshot_semantic_diagnostics( + payload, + information_state_context_resolver=information_state_context_resolver, + ) handler = _SEMANTIC_DISPATCH.get(contract_name) if handler is None: return [] diff --git a/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py b/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py index 89dd9bf44..6c3d45a4c 100644 --- a/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py +++ b/implementations/python/packages/raes_conformance/conformance/snapshot_semantics.py @@ -5,7 +5,10 @@ from collections.abc import Mapping from typing import Any -from raes_contracts.contracts import RuntimeSnapshotEnvelopeModel +from raes_contracts.contracts import ( + ParticipantInformationStateContextResolver, + RuntimeSnapshotEnvelopeModel, +) from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_autonomous_state import ( iter_participant_autonomous_runtime_snapshot_violations, @@ -13,9 +16,12 @@ ) from raes_contracts.participant_concurrency import iter_participant_concurrency_snapshot_violations from raes_contracts.participant_episode import iter_participant_episode_snapshot_violations +from raes_contracts.participant_information_state_history import ( + iter_participant_information_state_snapshot_violations, +) from raes_contracts.participant_shared_state import iter_participant_shared_state_snapshot_violations from raes_contracts.planning import RuntimeDomain -from raes_contracts.runtime_state import RuntimeSnapshot, SnapshotEntry +from raes_contracts.runtime_state import RealizationObservationDisclosure, RuntimeSnapshot, SnapshotEntry from raes_processor.models import ( ParticipantActionContractRuntime, ParticipantHistoryAddressScope, @@ -73,6 +79,10 @@ def _snapshot_from_envelope(payload: dict[str, Any]) -> RuntimeSnapshot: participant_address: [event.model_dump(mode="json") for event in history] for participant_address, history in validated.participant_behavior_history.items() }, + information_state_history={ + participant_address: [record.model_dump(mode="json") for record in history] + for participant_address, history in validated.information_state_history.items() + }, participant_autonomous_execution_states={ state_address: state.model_dump(mode="json") for state_address, state in validated.participant_autonomous_execution_states.items() @@ -108,6 +118,17 @@ def _snapshot_from_envelope(payload: dict[str, Any]) -> RuntimeSnapshot: for context_id, context in validated.time_management_contexts.items() }, time_model_state=validated.time_model_state, + realization_observations=tuple( + RealizationObservationDisclosure( + address=entry.address, + field_path=entry.field_path, + domain=entry.domain, + requirement_kind=entry.requirement_kind, + verification_scope=entry.verification_scope, + observation_strength=entry.observation_strength, + ) + for entry in validated.realization_observations + ), metadata=dict(validated.metadata), ) @@ -376,6 +397,20 @@ def _participant_concurrency_snapshot_diagnostics(snapshot: RuntimeSnapshot) -> ] +def _participant_information_state_snapshot_diagnostics( + snapshot: RuntimeSnapshot, + information_state_context_resolver: ParticipantInformationStateContextResolver | None, +) -> list[Diagnostic]: + return [ + _diagnostic(_SEMANTIC_INVALID_DIAGNOSTIC_CODE, address, message) + for address, message in iter_participant_information_state_snapshot_violations( + snapshot.information_state_history, + information_state_context_resolver=information_state_context_resolver, + context_scope=snapshot, + ) + ] + + def _participant_autonomous_state_snapshot_diagnostics( states: object, ) -> list[Diagnostic]: @@ -385,7 +420,11 @@ def _participant_autonomous_state_snapshot_diagnostics( ] -def _runtime_snapshot_semantic_diagnostics(payload: object) -> list[Diagnostic]: +def _runtime_snapshot_semantic_diagnostics( + payload: object, + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, +) -> list[Diagnostic]: validated = RuntimeSnapshotEnvelopeModel.model_validate(payload) autonomous_states = { state_address: state.model_dump(mode="json") @@ -408,5 +447,9 @@ def _runtime_snapshot_semantic_diagnostics(payload: object) -> list[Diagnostic]: *_participant_behavior_snapshot_diagnostics(snapshot), *_shared_state_snapshot_diagnostics(snapshot), *_participant_concurrency_snapshot_diagnostics(snapshot), + *_participant_information_state_snapshot_diagnostics( + snapshot, + information_state_context_resolver, + ), *autonomous_diagnostics, ] diff --git a/implementations/python/packages/raes_conformance/conformance/target.py b/implementations/python/packages/raes_conformance/conformance/target.py index 667b527ea..5fbd8ae2c 100644 --- a/implementations/python/packages/raes_conformance/conformance/target.py +++ b/implementations/python/packages/raes_conformance/conformance/target.py @@ -22,6 +22,10 @@ from raes_conformance.conformance.diagnostics import _diagnostic, sanitized_failure_message from raes_conformance.conformance.fixture_suite import run_fixture_suite +from raes_conformance.conformance.participant_opacity_probes import ( + ParticipantOpacityProbeHarness, + participant_opacity_cases, +) from raes_conformance.conformance.participant_policy_probes import ( ParticipantPolicyProbeHarness, participant_policy_cases, @@ -146,6 +150,7 @@ class _TargetConformanceOptions: observer_version: str = "raes-realization-observer/v1" native_conformance: bool = False participant_policy_harness: ParticipantPolicyProbeHarness | None = None + participant_opacity_harness: ParticipantOpacityProbeHarness | None = None def _unknown_profile_report( @@ -306,6 +311,7 @@ def _known_profile_report( adapter_cases = adapter_cases[:1] participant_feature_cases = _participant_feature_cases(target, profile) policy_cases = participant_policy_cases(target, profile, options.participant_policy_harness) + opacity_cases = participant_opacity_cases(target, options.participant_opacity_harness) target_cases = tuple(replace(case, execution_basis=options.execution_basis.value) for case in adapter_cases) realization_run = run_realization_conformance( target, @@ -320,6 +326,7 @@ def _known_profile_report( *fixture_report.cases, *participant_feature_cases, *policy_cases, + *opacity_cases, *target_cases, *realization_cases, ) @@ -327,7 +334,16 @@ def _known_profile_report( fixture_report.passed and not contract_gaps and not capability_gaps - and all(case.passed for case in (*participant_feature_cases, *policy_cases, *target_cases, *realization_cases)) + and all( + case.passed + for case in ( + *participant_feature_cases, + *policy_cases, + *opacity_cases, + *target_cases, + *realization_cases, + ) + ) ) profile_id = _to_profile_id(profile) return BackendConformanceReport( diff --git a/implementations/python/packages/raes_conformance/conformance/validators.py b/implementations/python/packages/raes_conformance/conformance/validators.py index 4cf2f60b5..888acbb7c 100644 --- a/implementations/python/packages/raes_conformance/conformance/validators.py +++ b/implementations/python/packages/raes_conformance/conformance/validators.py @@ -6,6 +6,7 @@ from raes_contracts.behavioral_relations import BehavioralRelationCatalogModel from raes_contracts.contracts import ( ActivityStreamsActivityTypesSourceModel, + ArtifactTransformationReportModel, AssociatedArtifactManifestModel, BackendManifestV2Model, EvaluationHistoryEventModel, @@ -32,6 +33,9 @@ ParticipantEpisodeStateModel, ParticipantImplementationManifestModel, ParticipantImplementationProvenanceModel, + ParticipantInformationReconstructionProfileModel, + ParticipantInformationStateContextResolver, + ParticipantInformationStateRecordModel, ParticipantLifecycleEventModel, ParticipantObservationEnvelopeModel, ParticipantSharedStateRecordModel, @@ -41,6 +45,7 @@ ValidationBasisDisclosureDocumentModel, WorkflowExecutionStateModel, WorkflowHistoryEventModel, + validate_participant_information_state_resolved_context, ) from raes_contracts.contracts.participant_execution import ( ParticipantExecutionBindingModel, @@ -83,6 +88,10 @@ "participant-execution-service-state-v1": ParticipantExecutionServiceStateModel.model_validate, "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_validate, "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_validate, + "participant-information-state-record-v1": ParticipantInformationStateRecordModel.model_validate, + "participant-information-reconstruction-profile-v1": ( + ParticipantInformationReconstructionProfileModel.model_validate + ), "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_validate, "participant-control-occurrence-v1": ParticipantControlOccurrenceModel.model_validate, "participant-crossing-occurrence-v1": ParticipantCrossingOccurrenceModel.model_validate, @@ -97,6 +106,7 @@ _STRUCTURAL_ONLY_VALIDATORS = { "associated-artifact-manifest-v1": AssociatedArtifactManifestModel.model_validate, + "artifact-transformation-report-v1": ArtifactTransformationReportModel.model_validate, "behavioral-relation-profile-v1": BehavioralRelationProfileModel.model_validate, "behavioral-relations-v1": BehavioralRelationCatalogModel.model_validate, "external-concept-bindings-v1": ExternalConceptBindingDocumentModel.model_validate, @@ -121,6 +131,7 @@ { "associated-artifact-manifest-v1", "external-concept-bindings-v1", + "participant-information-state-record-v1", } ) @@ -211,10 +222,51 @@ def _validate_payload(contract_name: str, payload: object) -> list[Diagnostic]: return diagnostics -def validate_contract_payload(contract_name: str, payload: object) -> tuple[Diagnostic, ...]: - """Validate one payload through the registered structural contract boundary.""" +def _information_state_context_diagnostics( + payload: object, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, +) -> list[Diagnostic]: + contract_name = "participant-information-state-record-v1" + diagnostics: list[Diagnostic] = [] + if information_state_context_resolver is None: + diagnostics.append( + _diagnostic( + "conformance.semantic-context-required", + contract_name, + "participant information-state context resolver is required", + ) + ) + else: + try: + record = ParticipantInformationStateRecordModel.model_validate(payload) + validate_participant_information_state_resolved_context( + record, + information_state_context_resolver, + payload, + ) + except (TypeError, ValueError) as exc: + diagnostics.append( + _diagnostic( + "conformance.semantic-invalid", + contract_name, + "participant information-state context is invalid: " + sanitized_failure_message(exc), + ) + ) + return diagnostics + + +def validate_contract_payload( + contract_name: str, + payload: object, + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, +) -> tuple[Diagnostic, ...]: + """Validate one payload through its structural and available contextual boundary.""" - return tuple(_validate_payload(contract_name, payload)) + diagnostics = _validate_payload(contract_name, payload) + if not diagnostics and contract_name == "participant-information-state-record-v1": + diagnostics.extend(_information_state_context_diagnostics(payload, information_state_context_resolver)) + return tuple(diagnostics) def supported_contract_ids() -> tuple[str, ...]: diff --git a/implementations/python/packages/raes_contracts/_participant_behavior_types.py b/implementations/python/packages/raes_contracts/_participant_behavior_types.py index d368b19fb..494d3bd90 100644 --- a/implementations/python/packages/raes_contracts/_participant_behavior_types.py +++ b/implementations/python/packages/raes_contracts/_participant_behavior_types.py @@ -100,6 +100,7 @@ class ParticipantLifecycleOperationState(str, Enum): "participant_episode_results", "participant_episode_history", "participant_behavior_history", + "information_state_history", "shared_state_records", "shared_state_history", } diff --git a/implementations/python/packages/raes_contracts/apparatus.py b/implementations/python/packages/raes_contracts/apparatus.py index 7700ade8d..74c0cf713 100644 --- a/implementations/python/packages/raes_contracts/apparatus.py +++ b/implementations/python/packages/raes_contracts/apparatus.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING -from .vocabulary import RealizationSupportMode +from .vocabulary import ObservationStrength, RealizationSupportMode, RealizationVerificationScope if TYPE_CHECKING: from .artifact_requirements import ArtifactMechanismCapability @@ -47,6 +47,22 @@ def __post_init__(self) -> None: raise ValueError("ConceptBinding.family must be non-empty") +@dataclass(frozen=True) +class RealizationObservationCapability: + """Declared concern-specific corroboration available from a backend.""" + + verification_scope: RealizationVerificationScope + observation_strength: ObservationStrength + + def __post_init__(self) -> None: + if not isinstance(self.verification_scope, RealizationVerificationScope): + raise TypeError("verification_scope must be RealizationVerificationScope") + if not isinstance(self.observation_strength, ObservationStrength): + raise TypeError("observation_strength must be ObservationStrength") + if self.observation_strength is ObservationStrength.NONE: + raise ValueError("realization observation capability must provide non-none evidence") + + @dataclass(frozen=True) class RealizationSupportDeclaration: """Declared realization-support and disclosure surface for one concern domain.""" @@ -56,6 +72,7 @@ class RealizationSupportDeclaration: supported_constraint_kinds: frozenset[str] = frozenset() supported_exact_requirement_kinds: frozenset[str] = frozenset() disclosure_kinds: frozenset[str] = frozenset() + observation_capabilities: dict[str, RealizationObservationCapability] = field(default_factory=dict) artifact_mechanisms: tuple[ArtifactMechanismCapability, ...] = () constraints: dict[str, str] = field(default_factory=dict) @@ -68,6 +85,13 @@ def __post_init__(self) -> None: field_name="supported_exact_requirement_kinds", ) _require_non_empty_strings(self.disclosure_kinds, field_name="disclosure_kinds") + if any(not kind.strip() for kind in self.observation_capabilities): + raise ValueError("observation_capabilities must not contain empty concern kinds") + if any( + not isinstance(capability, RealizationObservationCapability) + for capability in self.observation_capabilities.values() + ): + raise TypeError("observation_capabilities values must be RealizationObservationCapability") if not self.disclosure_kinds: raise ValueError("RealizationSupportDeclaration.disclosure_kinds must not be empty") if not (self.supported_constraint_kinds or self.supported_exact_requirement_kinds): diff --git a/implementations/python/packages/raes_contracts/behavioral_relation_profiles.py b/implementations/python/packages/raes_contracts/behavioral_relation_profiles.py index 80646fef4..b9cc540c3 100644 --- a/implementations/python/packages/raes_contracts/behavioral_relation_profiles.py +++ b/implementations/python/packages/raes_contracts/behavioral_relation_profiles.py @@ -26,6 +26,7 @@ SUPPORTED_BEHAVIORAL_RELATION_PROFILE_IDS = frozenset( { "participant-opacity-baseline-v1", + "participant-opacity-runtime-reference-v1", "participant-opacity-theorem-v1", } ) @@ -451,6 +452,12 @@ def load_behavioral_relation_profile( "participant-opacity-baseline-v1", "sem-231/rev2", ): behavioral_relation_profiles_root() / "history" / "participant-opacity-baseline-v1-sem-231-rev2.json", + ( + "participant-opacity-runtime-reference-v1", + "sem-231/runtime-rev1", + ): behavioral_relation_profiles_root() + / "history" + / "participant-opacity-runtime-reference-v1-sem-231-runtime-rev1.json", } diff --git a/implementations/python/packages/raes_contracts/behavioral_relations.py b/implementations/python/packages/raes_contracts/behavioral_relations.py index 86fb665df..1200559f8 100644 --- a/implementations/python/packages/raes_contracts/behavioral_relations.py +++ b/implementations/python/packages/raes_contracts/behavioral_relations.py @@ -313,6 +313,9 @@ def behavioral_relation_catalog_path() -> Path: _HISTORICAL_CATALOG_PATHS = { "rev8": corpus_family_root(CONCEPT_AUTHORITY) / "history" / "behavioral-relations-v1-rev8.json", + "rev9": corpus_family_root(CONCEPT_AUTHORITY) / "history" / "behavioral-relations-v1-rev9.json", + "rev10": corpus_family_root(CONCEPT_AUTHORITY) / "history" / "behavioral-relations-v1-rev10.json", + "rev11": corpus_family_root(CONCEPT_AUTHORITY) / "history" / "behavioral-relations-v1-rev11.json", } diff --git a/implementations/python/packages/raes_contracts/contracts/__init__.py b/implementations/python/packages/raes_contracts/contracts/__init__.py index 6b10e8ea7..9033db6e7 100644 --- a/implementations/python/packages/raes_contracts/contracts/__init__.py +++ b/implementations/python/packages/raes_contracts/contracts/__init__.py @@ -17,6 +17,7 @@ from ..versions import ( ACTIVITYSTREAMS_ACTIVITY_TYPES_SOURCE_SCHEMA_VERSION, ARTIFACT_REQUIREMENT_SCHEMA_VERSION, + ARTIFACT_TRANSFORMATION_REPORT_SCHEMA_VERSION, ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION, ATLAS_TACTICS_SOURCE_SCHEMA_VERSION, ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION, @@ -82,6 +83,18 @@ from .admitted_trial_plan import ExperimentScenarioFamilyReferenceModel as ExperimentScenarioFamilyReferenceModel from .admitted_trial_plan import seal_admitted_trial_entry as seal_admitted_trial_entry from .admitted_trial_plan import seal_admitted_trial_plan as seal_admitted_trial_plan +from .artifact_transformations import ( + ArtifactTransformationCheckModel, + ArtifactTransformationIdentityMapModel, + ArtifactTransformationKind, + ArtifactTransformationLossKind, + ArtifactTransformationLossModel, + ArtifactTransformationPreservationModel, + ArtifactTransformationReportModel, + ArtifactTransformationStatus, + PreservationOutcome, + TransformationCheckOutcome, +) from .associated_artifacts import AssociatedArtifactManifestModel, AssociatedArtifactSetDigestString from .base import ( BehavioralClaimBindingModel, @@ -102,6 +115,7 @@ OrchestratorCapabilitiesModel, ProcessorCompatibilityModel, ProvisionerCapabilitiesModel, + RealizationObservationCapabilityModel, RealizationSupportDeclarationModel, ) from .catalogs import ( @@ -261,9 +275,6 @@ ParticipantJointActionAccessSetModel, ParticipantJointActionRecordModel, ParticipantLifecycleEventModel, - ParticipantObservationEnvelopeModel, - ParticipantObservationLossDescriptorModel, - ParticipantObservationStochasticContextModel, ParticipantRuntimeBaseEnvelopeModel, ParticipantSharedStateAccessModel, ParticipantSharedStateRecordModel, @@ -272,6 +283,16 @@ SourcePipelineModel, SourceStatusModel, ) +from .participant_information_state import ( + ParticipantInformationReconstructionProfileModel, + ParticipantInformationStateContextResolver, + ParticipantInformationStateRecordModel, + ParticipantInformationStateSourceCoordinate, + ParticipantInformationStateSourceRefModel, + ParticipantInformationStateValidationContext, + validate_participant_information_state_context, + validate_participant_information_state_resolved_context, +) from .participant_manifests import ( BackendManifestV2Model, ParticipantExposurePolicyModel, @@ -281,6 +302,11 @@ ParticipantImplementationProvenanceModel, ParticipantImplementationSelectionModel, ) +from .participant_observation import ( + ParticipantObservationEnvelopeModel, + ParticipantObservationLossDescriptorModel, + ParticipantObservationStochasticContextModel, +) from .participant_occurrences import ( ParticipantControlDeclarationModel, ParticipantControlOccurrenceModel, @@ -349,6 +375,7 @@ PlanOperationModel, ProvisioningPlanModel, RealizationEnvelopeIdentityModel, + RealizationObservationDisclosureModel, RealizationProvenanceEntryModel, RuntimeSnapshotEnvelopeModel, SnapshotEntryModel, diff --git a/implementations/python/packages/raes_contracts/contracts/_exports.py b/implementations/python/packages/raes_contracts/contracts/_exports.py index 45687e976..c2b49fbf3 100644 --- a/implementations/python/packages/raes_contracts/contracts/_exports.py +++ b/implementations/python/packages/raes_contracts/contracts/_exports.py @@ -16,6 +16,17 @@ "AtlasTacticsSourceModel", "ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION", "ARTIFACT_REQUIREMENT_SCHEMA_VERSION", + "ARTIFACT_TRANSFORMATION_REPORT_SCHEMA_VERSION", + "ArtifactTransformationCheckModel", + "ArtifactTransformationIdentityMapModel", + "ArtifactTransformationKind", + "ArtifactTransformationLossKind", + "ArtifactTransformationLossModel", + "ArtifactTransformationPreservationModel", + "ArtifactTransformationReportModel", + "ArtifactTransformationStatus", + "PreservationOutcome", + "TransformationCheckOutcome", "ArtifactAcquisitionTimingModel", "ArtifactAvailabilityContext", "ArtifactMechanismCapability", @@ -265,6 +276,12 @@ "ParticipantHistoryViewBehaviorEventModel", "ParticipantHistoryViewEpisodeEventModel", "ParticipantHistoryViewModel", + "ParticipantInformationReconstructionProfileModel", + "ParticipantInformationStateContextResolver", + "ParticipantInformationStateRecordModel", + "ParticipantInformationStateSourceCoordinate", + "ParticipantInformationStateSourceRefModel", + "ParticipantInformationStateValidationContext", "ParticipantImplementationCapabilitiesModel", "ParticipantImplementationCompatibilityModel", "ParticipantImplementationManifestModel", @@ -290,6 +307,8 @@ "ParticipantStatusViewModel", "ParticipantTemporalRuntimeContextModel", "ParticipantTimeManagementContextModel", + "validate_participant_information_state_context", + "validate_participant_information_state_resolved_context", "VIEW_SCOPE_PROJECTED_FIELDS", "PlanOperationModel", "ProcessorFeature", @@ -326,8 +345,10 @@ "TrialCompilationLimitsModel", "TrialExecutionAuthorityModel", "RealizationEnvelopeIdentityModel", + "RealizationObservationDisclosureModel", "RawDataIntegrityModel", "RealizationProvenanceEntryModel", + "RealizationObservationCapabilityModel", "RealizationSupportDeclarationModel", "RealizationSupportMode", "ReferenceModelCatalogModel", diff --git a/implementations/python/packages/raes_contracts/contracts/artifact_transformations.py b/implementations/python/packages/raes_contracts/contracts/artifact_transformations.py new file mode 100644 index 000000000..bcc175ea1 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/artifact_transformations.py @@ -0,0 +1,228 @@ +"""Closed portable reports for pure RAES artifact transformations.""" + +from __future__ import annotations + +from enum import Enum +from typing import Literal + +from pydantic import ConfigDict, Field, GetJsonSchemaHandler, field_validator, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema + +from ..diagnostics import DiagnosticModel +from ..versions import ARTIFACT_TRANSFORMATION_REPORT_SCHEMA_VERSION +from .base import ContractModel, NonEmptyString, PrefixedDigestString +from .schema_invariants import _add_raes_invariant + +_REPORT_VALIDATOR = "raes_contracts.contracts.ArtifactTransformationReportModel.model_validate" +OperationProfile = str + + +class ArtifactTransformationStatus(str, Enum): + SUCCESS = "success" + REFUSED = "refused" + + +class ArtifactTransformationKind(str, Enum): + SDL_AUTHORING = "sdl-authoring" + PORTABLE_CONTRACT = "portable-contract" + + +class TransformationCheckOutcome(str, Enum): + PASSED = "passed" + FAILED = "failed" + NOT_APPLICABLE = "not-applicable" + + +class PreservationOutcome(str, Enum): + VERIFIED = "verified" + FAILED = "failed" + NOT_APPLICABLE = "not-applicable" + + +class ArtifactTransformationLossKind(str, Enum): + DECLARATION_REMOVED = "declaration-removed" + + +class ArtifactTransformationCheckModel(ContractModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + check_id: str = Field(pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$", max_length=128) + outcome: TransformationCheckOutcome + diagnostic_codes: tuple[str, ...] = () + + @field_validator("diagnostic_codes") + @classmethod + def _validate_diagnostic_codes(cls, value: tuple[str, ...]) -> tuple[str, ...]: + if value != tuple(sorted(set(value))): + raise ValueError("transformation check diagnostic_codes must be sorted and unique") + return value + + +class ArtifactTransformationIdentityMapModel(ContractModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + declaration_kind: NonEmptyString + before: NonEmptyString + after: NonEmptyString + + +class ArtifactTransformationPreservationModel(ContractModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + profile: NonEmptyString + outcome: PreservationOutcome + evidence_digests: tuple[PrefixedDigestString, ...] = () + limitations: tuple[NonEmptyString, ...] = () + + @model_validator(mode="after") + def _validate_evidence(self) -> ArtifactTransformationPreservationModel: + if self.outcome == PreservationOutcome.VERIFIED and not self.evidence_digests: + raise ValueError("verified preservation requires evidence_digests") + if self.evidence_digests != tuple(sorted(set(self.evidence_digests))): + raise ValueError("preservation evidence_digests must be sorted and unique") + if self.limitations != tuple(sorted(set(self.limitations))): + raise ValueError("preservation limitations must be sorted and unique") + return self + + +class ArtifactTransformationLossModel(ContractModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + kind: ArtifactTransformationLossKind + affected_identity: NonEmptyString + diagnostic: DiagnosticModel + + +class ArtifactTransformationReportModel(ContractModel): + """Deterministic all-or-none report for one semantic operation.""" + + model_config = ConfigDict( + title="RAES Artifact Transformation Report v1", + extra="forbid", + frozen=True, + ) + + schema_version: Literal[ARTIFACT_TRANSFORMATION_REPORT_SCHEMA_VERSION] = ( + ARTIFACT_TRANSFORMATION_REPORT_SCHEMA_VERSION + ) + operation_profile: str = Field( + pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*/v[1-9][0-9]*$", + max_length=128, + ) + status: ArtifactTransformationStatus + artifact_kind: ArtifactTransformationKind + source_profile: NonEmptyString + target_profile: NonEmptyString + canonicalization_profile: NonEmptyString + source_digest: PrefixedDigestString + target_digest: PrefixedDigestString | None = None + policy_digest: PrefixedDigestString + derivation_digest: PrefixedDigestString + preconditions: tuple[ArtifactTransformationCheckModel, ...] = Field(min_length=1) + postconditions: tuple[ArtifactTransformationCheckModel, ...] = () + affected_identities: tuple[NonEmptyString, ...] = () + identity_map: tuple[ArtifactTransformationIdentityMapModel, ...] = () + preservation: ArtifactTransformationPreservationModel + losses: tuple[ArtifactTransformationLossModel, ...] = () + diagnostics: tuple[DiagnosticModel, ...] = () + + def _validate_status_shape(self) -> None: + if self.status == ArtifactTransformationStatus.SUCCESS: + if self.target_digest is None: + raise ValueError("successful transformation requires target_digest") + if self.preservation.outcome == PreservationOutcome.FAILED: + raise ValueError("successful transformation cannot report failed preservation") + else: + if self.target_digest is not None: + raise ValueError("refused transformation must not expose target_digest") + if not self.diagnostics: + raise ValueError("refused transformation requires diagnostics") + + def _validate_identity_ordering(self) -> None: + if self.affected_identities != tuple(sorted(set(self.affected_identities))): + raise ValueError("affected_identities must be sorted and unique") + identity_order = tuple((item.before, item.after, item.declaration_kind) for item in self.identity_map) + if identity_order != tuple(sorted(set(identity_order))): + raise ValueError("identity_map must be sorted and unique") + loss_order = tuple((item.kind.value, item.affected_identity) for item in self.losses) + if loss_order != tuple(sorted(set(loss_order))): + raise ValueError("losses must be sorted and unique") + + def _validate_check_ordering(self) -> None: + for checks, label in ((self.preconditions, "preconditions"), (self.postconditions, "postconditions")): + check_ids = tuple(check.check_id for check in checks) + if check_ids != tuple(sorted(set(check_ids))): + raise ValueError(f"{label} must be sorted and unique by check_id") + + def _validate_diagnostic_ordering(self) -> None: + diagnostic_order = tuple( + (item.code, item.address, item.severity.value, item.message) for item in self.diagnostics + ) + if diagnostic_order != tuple(sorted(set(diagnostic_order))): + raise ValueError("diagnostics must be sorted and unique") + + @model_validator(mode="after") + def _validate_result_shape(self) -> ArtifactTransformationReportModel: + self._validate_status_shape() + self._validate_identity_ordering() + self._validate_check_ordering() + self._validate_diagnostic_ordering() + 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.setdefault("allOf", []).extend( + [ + { + "if": {"properties": {"status": {"const": "success"}}, "required": ["status"]}, + "then": { + "required": ["target_digest"], + "properties": {"target_digest": {"type": "string"}}, + }, + }, + { + "if": {"properties": {"status": {"const": "refused"}}, "required": ["status"]}, + "then": { + "properties": { + "target_digest": {"type": "null"}, + "diagnostics": {"minItems": 1}, + } + }, + }, + ] + ) + _add_raes_invariant( + json_schema, + "artifact-transformation-all-or-none", + "Success carries a complete target digest; refusal carries no target and at least one bounded diagnostic.", + validator=_REPORT_VALIDATOR, + inputs=[{"contract_id": "artifact-transformation-report-v1", "instance_path": "#"}], + ) + _add_raes_invariant( + json_schema, + "artifact-transformation-deterministic-order", + "Checks, identities, mappings, losses, diagnostics, and evidence use stable closed ordering.", + validator=_REPORT_VALIDATOR, + inputs=[{"contract_id": "artifact-transformation-report-v1", "instance_path": "#"}], + ) + return json_schema + + +__all__ = [ + "ArtifactTransformationCheckModel", + "ArtifactTransformationIdentityMapModel", + "ArtifactTransformationKind", + "ArtifactTransformationLossKind", + "ArtifactTransformationLossModel", + "ArtifactTransformationPreservationModel", + "ArtifactTransformationReportModel", + "ArtifactTransformationStatus", + "PreservationOutcome", + "TransformationCheckOutcome", +] diff --git a/implementations/python/packages/raes_contracts/contracts/bundle.py b/implementations/python/packages/raes_contracts/contracts/bundle.py index 331346d92..082681982 100644 --- a/implementations/python/packages/raes_contracts/contracts/bundle.py +++ b/implementations/python/packages/raes_contracts/contracts/bundle.py @@ -12,6 +12,7 @@ from raes_contracts.artifact_requirements import ArtifactRequirementContractModel from .admitted_trial_plan import AdmittedTrialPlanModel +from .artifact_transformations import ArtifactTransformationReportModel from .associated_artifacts import AssociatedArtifactManifestModel from .batch_execution import BatchExecutionReceiptModel from .catalogs import ( @@ -47,7 +48,6 @@ from .participant_envelopes import ( ParticipantJointActionRecordModel, ParticipantLifecycleEventModel, - ParticipantObservationEnvelopeModel, ParticipantSharedStateRecordModel, ParticipantTimeManagementContextModel, ) @@ -56,11 +56,16 @@ ParticipantExecutionControlRequestModel, ParticipantExecutionServiceStateModel, ) +from .participant_information_state import ( + ParticipantInformationReconstructionProfileModel, + ParticipantInformationStateRecordModel, +) from .participant_manifests import ( BackendManifestV2Model, ParticipantImplementationManifestModel, ParticipantImplementationProvenanceModel, ) +from .participant_observation import ParticipantObservationEnvelopeModel from .participant_resource_budgets import ( ParticipantResourceBudgetEventModel, ParticipantResourceBudgetPolicyModel, @@ -149,6 +154,7 @@ def _core_schema_bundle() -> dict[str, dict[str, Any]]: "instantiated-scenario-snapshot-v1": InstantiatedScenarioSnapshot.model_json_schema(), "scenario-instantiation-request-v1": InstantiationRequestModel.model_json_schema(), "artifact-requirement-v1": ArtifactRequirementContractModel.model_json_schema(), + "artifact-transformation-report-v1": ArtifactTransformationReportModel.model_json_schema(), "exploit-path-analysis-evidence-v1": ExploitPathAnalysisEvidenceModel.model_json_schema(), "scenario-satisfiability-evidence-v1": ScenarioSatisfiabilityEvidenceModel.model_json_schema(), "backend-manifest-v2": BackendManifestV2Model.model_json_schema(), @@ -175,6 +181,9 @@ def _core_schema_bundle() -> dict[str, dict[str, Any]]: "semantic-profile-v1": SemanticProfileModel.model_json_schema(), "backend-profile-v1": _backend_profile_schema_for_bundle(), "random-stream-profile-v1": RandomStreamProfileModel.model_json_schema(), + "participant-information-reconstruction-profile-v1": ( + ParticipantInformationReconstructionProfileModel.model_json_schema() + ), "random-stream-vector-v1": RandomStreamVectorModel.model_json_schema(), "experiment-apparatus-context-v1": ExperimentApparatusContextModel.model_json_schema(), "experiment-authoring-input-v1": ExperimentSpecModel.model_json_schema(), @@ -237,6 +246,7 @@ def _runtime_schema_bundle() -> dict[str, dict[str, Any]]: "participant-resource-budget-event-v1": ParticipantResourceBudgetEventModel.model_json_schema(), "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_json_schema(), "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_json_schema(), + "participant-information-state-record-v1": ParticipantInformationStateRecordModel.model_json_schema(), "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_json_schema(), "participant-joint-action-record-v1": ParticipantJointActionRecordModel.model_json_schema(), "participant-time-management-context-v1": ParticipantTimeManagementContextModel.model_json_schema(), diff --git a/implementations/python/packages/raes_contracts/contracts/capabilities.py b/implementations/python/packages/raes_contracts/contracts/capabilities.py index afba29ae1..806a7d7fd 100644 --- a/implementations/python/packages/raes_contracts/contracts/capabilities.py +++ b/implementations/python/packages/raes_contracts/contracts/capabilities.py @@ -9,7 +9,9 @@ from ..artifact_requirements import ArtifactMechanismCapability from ..vocabulary import ( GeneratedArtifactKind, + ObservationStrength, RealizationSupportMode, + RealizationVerificationScope, WorkflowFeature, WorkflowStatePredicateFeature, ) @@ -292,12 +294,26 @@ class ProcessorCompatibilityModel(ContractModel): backends: list[NonEmptyString] = Field(min_length=1) +class RealizationObservationCapabilityModel(ContractModel): + """Concern-specific scope and source of backend corroboration.""" + + verification_scope: RealizationVerificationScope + observation_strength: ObservationStrength = Field(json_schema_extra={"not": {"const": "none"}}) + + @model_validator(mode="after") + def _require_evidence(self) -> RealizationObservationCapabilityModel: + if self.observation_strength is ObservationStrength.NONE: + raise ValueError("realization observation capability must provide non-none evidence") + return self + + class RealizationSupportDeclarationModel(ContractModel): domain: NonEmptyString support_mode: RealizationSupportMode supported_constraint_kinds: list[NonEmptyString] = Field(default_factory=list) supported_exact_requirement_kinds: list[NonEmptyString] = Field(default_factory=list) disclosure_kinds: list[NonEmptyString] = Field(min_length=1) + observation_capabilities: dict[NonEmptyString, RealizationObservationCapabilityModel] = Field(default_factory=dict) artifact_mechanisms: list[ArtifactMechanismCapability] = Field(default_factory=list) constraints: dict[str, str] = Field(default_factory=dict) diff --git a/implementations/python/packages/raes_contracts/contracts/feature_support.py b/implementations/python/packages/raes_contracts/contracts/feature_support.py index 7b3177612..3d30748eb 100644 --- a/implementations/python/packages/raes_contracts/contracts/feature_support.py +++ b/implementations/python/packages/raes_contracts/contracts/feature_support.py @@ -8,7 +8,7 @@ from pydantic.json_schema import JsonSchemaValue from pydantic_core import CoreSchema -from ..manifest_authority import PARTICIPANT_RUNTIME_POLICY_FEATURES +from ..manifest_authority import PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES from ..vocabulary import ParticipantFeatureSupportLevel from .base import ContractModel, NonEmptyString from .validators import _validate_unique_string_values @@ -58,7 +58,7 @@ def _validate_feature_support_declaration(self) -> ParticipantFeatureSupportMode f"feature_support entry '{self.feature}' declares support_level " f"'{self.support_level.value}' below 'exact' and must carry at least one disclosure_refs entry" ) - if self.feature in PARTICIPANT_RUNTIME_POLICY_FEATURES: + if self.feature in PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES: if self.support_level != ParticipantFeatureSupportLevel.EXACT and not self.limitation_refs: raise ValueError(f"feature_support entry '{self.feature}' below 'exact' must carry limitation_refs") if self.support_level == ParticipantFeatureSupportLevel.BOUNDED and not self.constraint_refs: @@ -94,7 +94,7 @@ def __get_pydantic_json_schema__( }, } ) - policy_features = sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + policy_features = sorted(PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES) json_schema["allOf"].extend( [ { diff --git a/implementations/python/packages/raes_contracts/contracts/manifests.py b/implementations/python/packages/raes_contracts/contracts/manifests.py index b44e99d4d..2dc7bf1ca 100644 --- a/implementations/python/packages/raes_contracts/contracts/manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/manifests.py @@ -10,7 +10,7 @@ from ..addressing import require_compiled_address from ..manifest_authority import ( - PARTICIPANT_RUNTIME_POLICY_FEATURES, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, PROCESSOR_SUPPORTED_CONTRACT_IDS, PROCESSOR_SUPPORTED_SDL_VERSION_IDS, validate_backend_supported_contract_versions, @@ -189,7 +189,7 @@ def _validate_supported_feature_levels(self) -> None: supported_features = set(self.supported_behavior_features) | set(self.supported_interaction_features) declared_features = {entry.feature for entry in self.feature_support} missing_policy_declarations = sorted( - (supported_features & PARTICIPANT_RUNTIME_POLICY_FEATURES) - declared_features + (supported_features & PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES) - declared_features ) if missing_policy_declarations: raise ValueError( @@ -204,7 +204,7 @@ def _validate_supported_feature_levels(self) -> None: "feature is declared in supported_behavior_features or supported_interaction_features" ) if ( - entry.feature in PARTICIPANT_RUNTIME_POLICY_FEATURES + entry.feature in PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES and not declared_unsupported and entry.feature not in supported_features ): diff --git a/implementations/python/packages/raes_contracts/contracts/participant_context.py b/implementations/python/packages/raes_contracts/contracts/participant_context.py index 1e0059472..373db82e0 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_context.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_context.py @@ -25,6 +25,7 @@ ParticipantContextSourceLayer = Literal[ "source_snapshot", "participant_observation", + "participant_information_state", "participant_behavior_history", "participant_episode_state", "participant_status_view", diff --git a/implementations/python/packages/raes_contracts/contracts/participant_crossing.py b/implementations/python/packages/raes_contracts/contracts/participant_crossing.py index 776fcfce6..4ac0ff035 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_crossing.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_crossing.py @@ -26,6 +26,12 @@ ParticipantCrossingSubjectKind, ) from .participant_envelopes import ParticipantRuntimeBaseEnvelopeModel +from .participant_opacity import ( + ParticipantOpacityObservationInventoryModel, + ParticipantOpacityObservationSurfaceModel, + ParticipantOpacityRuntimeEnforcementBindingModel, + ParticipantOpacityRuntimeSupportModel, +) from .participant_runtime import ParticipantRuntimeOrderingBasis from .schema_invariants import _add_raes_invariant @@ -239,6 +245,7 @@ class ParticipantCrossingDecisionModel(ParticipantCrossingOccurrenceBaseModel): reason_code: NonEmptyString required_operation: ParticipantCrossingOperation | None = None required_evidence_refs: list[NonEmptyString] = Field(min_length=1) + opacity_enforcement: ParticipantOpacityRuntimeEnforcementBindingModel | None = None @model_validator(mode="after") def _validate_deny_first_disposition(self) -> ParticipantCrossingDecisionModel: @@ -451,6 +458,10 @@ def __get_pydantic_json_schema__( "ParticipantCrossingOccurrenceDetail", "ParticipantCrossingOccurrenceModel", "ParticipantCrossingOperation", + "ParticipantOpacityObservationInventoryModel", + "ParticipantOpacityObservationSurfaceModel", + "ParticipantOpacityRuntimeEnforcementBindingModel", + "ParticipantOpacityRuntimeSupportModel", "ParticipantCrossingPolicyReferenceModel", "ParticipantCrossingRequestModel", "ParticipantCrossingSubjectKind", diff --git a/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py b/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py index 9587226ec..edcccc5e2 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_crossing_validation.py @@ -300,9 +300,17 @@ def _validate_delivery_attempt_stage( indexes: _RecordIndexes, ) -> None: prior = _resolve_record(indexes.decisions_by_id, occurrence.decision_ref, _CROSSING_DECISION) - _require_permitted_decision(prior) decision = prior.occurrence assert isinstance(decision, ParticipantCrossingDecisionModel) + if occurrence.disposition == "withheld": + if decision.disposition != ParticipantCrossingDecisionDisposition.DENY: + raise ValueError("withheld participant crossing delivery requires a deny decision") + if occurrence.transformation_ref is not None: + raise ValueError("withheld participant crossing delivery cannot name a transformation") + _validate_successor(record, prior, require_same_subject=True) + _require_subject_owner(record, occurrence.owning_occurrence_ref, _DELIVERY_ATTEMPT) + return + _require_permitted_decision(prior) if decision.disposition == ParticipantCrossingDecisionDisposition.TRANSFORM: if occurrence.transformation_ref is None: raise ValueError("transform decisions require a transformation before delivery attempt") diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_state_cut.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_state_cut.py new file mode 100644 index 000000000..78c7ac74b --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_state_cut.py @@ -0,0 +1,69 @@ +"""Shared exact participant state-cut contracts for decision and information surfaces.""" + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import Field, StrictInt, model_validator + +from .base import ContractModel, NonEmptyString + +ParticipantDecisionSurfaceStateCutOrderModel = Literal[ + "control_plane_order", + "backend_serialized_order", + "behavior_history_order", +] + + +class ParticipantDecisionSurfaceSequenceCutModel(ContractModel): + """A complete prefix ending at one event in a declared total order.""" + + cut_kind: Literal["sequence_prefix"] + cut_ref: NonEmptyString + history_domain: Literal["participant_episode_lifecycle", "participant_behavior_history"] + order_model: ParticipantDecisionSurfaceStateCutOrderModel + anchor_event_ref: NonEmptyString + anchor_order: StrictInt = Field(ge=0) + history_prefix_length: StrictInt = Field(ge=1) + predecessor_event_refs: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_prefix(self) -> ParticipantDecisionSurfaceSequenceCutModel: + if len(self.predecessor_event_refs) != len(set(self.predecessor_event_refs)): + raise ValueError("predecessor_event_refs must not contain duplicates") + if self.history_prefix_length != self.anchor_order + 1: + raise ValueError("history_prefix_length must equal anchor_order + 1") + if self.anchor_event_ref in self.predecessor_event_refs: + raise ValueError("anchor_event_ref must not also be a predecessor_event_ref") + return self + + +class ParticipantDecisionSurfaceCausalCutModel(ContractModel): + """A downward-closed causal frontier for a partially ordered realization.""" + + cut_kind: Literal["causal_frontier"] + cut_ref: NonEmptyString + history_domain: NonEmptyString + order_model: Literal["causal_partial_order"] + frontier_event_refs: list[NonEmptyString] = Field(min_length=1) + predecessor_closure_ref: NonEmptyString + + @model_validator(mode="after") + def _validate_frontier(self) -> ParticipantDecisionSurfaceCausalCutModel: + if len(self.frontier_event_refs) != len(set(self.frontier_event_refs)): + raise ValueError("frontier_event_refs must not contain duplicates") + return self + + +ParticipantDecisionSurfaceStateCutModel = Annotated[ + ParticipantDecisionSurfaceSequenceCutModel | ParticipantDecisionSurfaceCausalCutModel, + Field(discriminator="cut_kind"), +] + + +__all__ = ( + "ParticipantDecisionSurfaceCausalCutModel", + "ParticipantDecisionSurfaceSequenceCutModel", + "ParticipantDecisionSurfaceStateCutModel", + "ParticipantDecisionSurfaceStateCutOrderModel", +) diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_exposure_v2.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_exposure_v2.py index 1075b2a07..bf74b32b6 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_exposure_v2.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_exposure_v2.py @@ -2,11 +2,10 @@ from __future__ import annotations -from typing import Literal - from pydantic import Field, StrictInt, model_validator from .base import ContractModel, NonEmptyString +from .participant_decision_state_cut import ParticipantDecisionSurfaceStateCutOrderModel from .participant_decision_surface_exposure import ParticipantExposureOperation from .participant_manifests import DigestString @@ -103,13 +102,6 @@ def _validate_exposure_basis(self) -> ParticipantDecisionSurfaceExposureBindingV return self -ParticipantDecisionSurfaceStateCutOrderModel = Literal[ - "control_plane_order", - "backend_serialized_order", - "behavior_history_order", -] - - __all__ = ( "ParticipantDecisionSurfaceExposureBindingV2Model", "ParticipantDecisionSurfaceStateCutOrderModel", diff --git a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2.py b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2.py index 7b280b87f..b91396ad3 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_decision_surface_v2.py @@ -14,6 +14,11 @@ from pydantic_core import CoreSchema from .base import ContractModel, NonEmptyString +from .participant_decision_state_cut import ( + ParticipantDecisionSurfaceCausalCutModel, + ParticipantDecisionSurfaceSequenceCutModel, + ParticipantDecisionSurfaceStateCutModel, +) from .participant_decision_surface import ( ParticipantDecisionSurfaceActionEntryModel, ParticipantDecisionSurfaceFormModel, @@ -22,10 +27,7 @@ _validate_surface_affordances, _validate_surface_form_relations, ) -from .participant_decision_surface_exposure_v2 import ( - ParticipantDecisionSurfaceExposureBindingV2Model, - ParticipantDecisionSurfaceStateCutOrderModel, -) +from .participant_decision_surface_exposure_v2 import ParticipantDecisionSurfaceExposureBindingV2Model from .participant_decision_surface_v2_validation import _validate_participant_decision_surface_v2 from .participant_manifests import DigestString from .participant_runtime import ParticipantRuntimeDeliveryBasis @@ -34,50 +36,6 @@ _SURFACE_V2_VALIDATOR = "raes_contracts.contracts.ParticipantDecisionSurfaceV2Model._validate_surface" -class ParticipantDecisionSurfaceSequenceCutModel(ContractModel): - """A complete prefix ending at one event in a declared total order.""" - - cut_kind: Literal["sequence_prefix"] - cut_ref: NonEmptyString - history_domain: Literal["participant_episode_lifecycle", "participant_behavior_history"] - order_model: ParticipantDecisionSurfaceStateCutOrderModel - anchor_event_ref: NonEmptyString - anchor_order: StrictInt = Field(ge=0) - history_prefix_length: StrictInt = Field(ge=1) - predecessor_event_refs: list[NonEmptyString] = Field(default_factory=list) - - @model_validator(mode="after") - def _validate_prefix(self) -> ParticipantDecisionSurfaceSequenceCutModel: - _require_unique(self.predecessor_event_refs, "predecessor_event_refs") - if self.history_prefix_length != self.anchor_order + 1: - raise ValueError("history_prefix_length must equal anchor_order + 1") - if self.anchor_event_ref in self.predecessor_event_refs: - raise ValueError("anchor_event_ref must not also be a predecessor_event_ref") - return self - - -class ParticipantDecisionSurfaceCausalCutModel(ContractModel): - """A downward-closed causal frontier for a partially ordered realization.""" - - cut_kind: Literal["causal_frontier"] - cut_ref: NonEmptyString - history_domain: NonEmptyString - order_model: Literal["causal_partial_order"] - frontier_event_refs: list[NonEmptyString] = Field(min_length=1) - predecessor_closure_ref: NonEmptyString - - @model_validator(mode="after") - def _validate_frontier(self) -> ParticipantDecisionSurfaceCausalCutModel: - _require_unique(self.frontier_event_refs, "frontier_event_refs") - return self - - -ParticipantDecisionSurfaceStateCutModel = Annotated[ - ParticipantDecisionSurfaceSequenceCutModel | ParticipantDecisionSurfaceCausalCutModel, - Field(discriminator="cut_kind"), -] - - class _ParticipantDecisionSurfaceAnchorV2Base(ContractModel): participant_address: NonEmptyString episode_id: NonEmptyString diff --git a/implementations/python/packages/raes_contracts/contracts/participant_envelopes.py b/implementations/python/packages/raes_contracts/contracts/participant_envelopes.py index 8e816b6bc..17125ea55 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_envelopes.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_envelopes.py @@ -1,4 +1,4 @@ -"""Participant runtime envelope contracts (lifecycle, observation, shared state, joint action, time).""" +"""Participant runtime envelope contracts (lifecycle, shared state, joint action, time).""" from __future__ import annotations @@ -26,8 +26,6 @@ ParticipantRuntimeAtomicityScope, ParticipantRuntimeConflictClass, ParticipantRuntimeConflictPolicy, - ParticipantRuntimeDeliveryBasis, - ParticipantRuntimeInformationGuarantee, ParticipantRuntimeIsolationGuarantee, ParticipantRuntimeJointActionConflictPolicy, ParticipantRuntimeMappingLoss, @@ -161,43 +159,6 @@ class ParticipantLifecycleEventModel(ParticipantRuntimeBaseEnvelopeModel): mapping_loss_detail: NonEmptyString | None = None -class ParticipantObservationLossDescriptorModel(ContractModel): - """Declared projection-loss facts for one participant-visible observation.""" - - kind: NonEmptyString - fields_redacted: list[NonEmptyString] = Field(default_factory=list) - - -class ParticipantObservationStochasticContextModel(ContractModel): - """Seed and randomization-policy references behind one observation.""" - - seed_ref: NonEmptyString | None = None - randomization_policy_ref: NonEmptyString | None = None - - -class ParticipantObservationEnvelopeModel(ParticipantRuntimeBaseEnvelopeModel): - """SEM-210 participant-visible observation record with explicit guarantees.""" - - observation_ref: NonEmptyString - phase_ref: NonEmptyString | None = None - visibility_projection_ref: NonEmptyString - information_guarantee: ParticipantRuntimeInformationGuarantee - delivery_basis: ParticipantRuntimeDeliveryBasis - delivery_point_ref: NonEmptyString | None = None - delivered_at: Rfc3339DateTimeString | None = None - action_observation_history_ref: NonEmptyString | None = None - information_state_ref: NonEmptyString | None = None - hidden_state_refs: list[NonEmptyString] = Field(default_factory=list) - centralized_state_refs: list[NonEmptyString] = Field(default_factory=list) - loss_descriptor: ParticipantObservationLossDescriptorModel | None = None - stochastic_context: ParticipantObservationStochasticContextModel | None = None - noise_model_ref: NonEmptyString | None = None - reconstruction_algorithm_ref: NonEmptyString | None = None - reconstruction_proof_ref: NonEmptyString | None = None - belief_support_ref: NonEmptyString | None = None - redacted_field_refs: list[NonEmptyString] = Field(default_factory=list) - - class ParticipantSharedStateAccessModel(ContractModel): """RUN-307 read/write access record over one shared-state address.""" diff --git a/implementations/python/packages/raes_contracts/contracts/participant_information_state.py b/implementations/python/packages/raes_contracts/contracts/participant_information_state.py new file mode 100644 index 000000000..e05caf999 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_information_state.py @@ -0,0 +1,495 @@ +"""ACT-604 portable participant information-state contracts and joins.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Literal + +from pydantic import Field, GetJsonSchemaHandler, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema + +from ..versions import PARTICIPANT_INFORMATION_RECONSTRUCTION_PROFILE_V1_SCHEMA_VERSION +from .base import ContractModel, NonEmptyString, PrefixedDigestString, SemanticProfileId +from .participant_decision_state_cut import ( + ParticipantDecisionSurfaceSequenceCutModel, + ParticipantDecisionSurfaceStateCutModel, +) +from .participant_envelopes import ParticipantRuntimeBaseEnvelopeModel +from .participant_information_state_sources import require_source_coordinate, validate_resolved_source +from .participant_observation import ParticipantObservationEnvelopeModel +from .participant_runtime import ParticipantRuntimeInformationGuarantee +from .schema_invariants import _add_raes_invariant + +if TYPE_CHECKING: + from .participant_decision_surface_v2 import ParticipantDecisionSurfaceV2Model + +ParticipantInformationSourceContract = Literal[ + "participant-observation-envelope-v1", + "participant-context-view-v1", + "participant-shared-state-record-v1", + "participant-behavior-history-event-stream-v1", + "participant-episode-state-envelope-v1", +] + +ParticipantInformationSourceRelation = Literal[ + "authored_initial", + "observed", + "derived", + "disclosed", + "shared_state_projection", +] + +ParticipantInformationMemoryScope = Literal[ + "episode_local_reset", + "persistent_across_episodes", +] + +ParticipantInformationOrderSemantics = Literal[ + "sequence_prefix", + "causal_frontier", +] + +ParticipantInformationSourceKey = tuple[str, str] + + +@dataclass(frozen=True) +class ParticipantInformationStateSourceCoordinate: + """Trusted resolver result binding one source to the record's governed cut.""" + + participant_address: str + episode_id: str + state_cut: ParticipantDecisionSurfaceStateCutModel + audience_scope_ref: str + visibility_projection_ref: str + projection_policy_revision: str + redaction_policy_ref: str + redaction_policy_revision: str + + +@dataclass(frozen=True) +class ParticipantInformationStateValidationContext: + """Non-wire authority needed to validate one information-state claim.""" + + occurrence_histories: Mapping[str, Sequence[ParticipantObservationEnvelopeModel]] + resolved_sources: Mapping[ParticipantInformationSourceKey, object] + source_coordinates: Mapping[ParticipantInformationSourceKey, ParticipantInformationStateSourceCoordinate] + proof_digests: Mapping[str, str] + decision_surfaces: Sequence[ParticipantDecisionSurfaceV2Model] = field(default_factory=tuple) + + +ParticipantInformationStateContextResolver = Callable[ + ["ParticipantInformationStateRecordModel", object | None], + ParticipantInformationStateValidationContext | None, +] + + +class ParticipantInformationStateSourceRefModel(ContractModel): + """One closed typed relation to an incumbent information source.""" + + contract_id: ParticipantInformationSourceContract + ref: NonEmptyString + relation: ParticipantInformationSourceRelation + + +class ParticipantInformationReconstructionProfileModel(ContractModel): + """One immutable, non-executable reconstruction profile declaration.""" + + schema_version: Literal[PARTICIPANT_INFORMATION_RECONSTRUCTION_PROFILE_V1_SCHEMA_VERSION] + profile_id: SemanticProfileId + title: NonEmptyString + description: NonEmptyString + algorithm_id: NonEmptyString + algorithm_version: NonEmptyString + information_state_schema_version: NonEmptyString + projection_version: NonEmptyString + determinism_basis: Literal["exact_occurrence_prefix_and_proof_digest"] + accepted_input_contracts: list[ParticipantInformationSourceContract] = Field(min_length=1) + accepted_order_semantics: list[ParticipantInformationOrderSemantics] = Field(min_length=1) + fixture_format: NonEmptyString + proof_artifact_format: NonEmptyString + normative_artifact_ref: NonEmptyString + normative_artifact_digest: PrefixedDigestString + + @model_validator(mode="after") + def _validate_closed_profile(self) -> ParticipantInformationReconstructionProfileModel: + for field_name in ("accepted_input_contracts", "accepted_order_semantics"): + values = getattr(self, field_name) + if len(values) != len(set(values)): + raise ValueError(f"{field_name} values must be unique") + return self + + +class ParticipantInformationStateRecordModel(ParticipantRuntimeBaseEnvelopeModel): + """Immutable participant-relative information state at one exact cut.""" + + schema_name: Literal["raes.participant_runtime.information_state"] + schema_version: Literal["1.0.0"] + event_type: Literal["participant_information_state"] + participant_address: NonEmptyString + episode_id: NonEmptyString + information_state_ref: NonEmptyString + information_state_digest: PrefixedDigestString + payload_ref: NonEmptyString + state_cut: ParticipantDecisionSurfaceStateCutModel + participant_memory_scope: ParticipantInformationMemoryScope + memory_reset_authority_ref: NonEmptyString | None = None + audience_scope_ref: NonEmptyString + visibility_projection_ref: NonEmptyString + projection_version: NonEmptyString + projection_policy_revision: NonEmptyString + redaction_policy_ref: NonEmptyString + redaction_policy_revision: NonEmptyString + information_guarantee: ParticipantRuntimeInformationGuarantee + source_refs: list[ParticipantInformationStateSourceRefModel] = Field(min_length=1) + occurrence_history_ref: NonEmptyString | None = None + reconstruction_profile_ref: NonEmptyString | None = None + reconstruction_algorithm_id: NonEmptyString | None = None + reconstruction_algorithm_version: NonEmptyString | None = None + reconstruction_proof_ref: NonEmptyString | None = None + reconstructed_state_digest: PrefixedDigestString | None = None + occurrence_order_witness_ref: NonEmptyString | None = None + loss_disclosures: list[NonEmptyString] = Field(default_factory=list) + predecessor_information_state_refs: list[NonEmptyString] = Field(default_factory=list) + supersedes_information_state_ref: NonEmptyString | None = None + + def _validate_identity_refs(self) -> None: + source_keys = [(source.contract_id, source.ref) for source in self.source_refs] + if len(source_keys) != len(set(source_keys)): + raise ValueError("information state source refs must be unique") + if len(self.predecessor_information_state_refs) != len(set(self.predecessor_information_state_refs)): + raise ValueError("predecessor information state refs must be unique") + if self.information_state_ref in self.predecessor_information_state_refs: + raise ValueError("information state cannot be its own predecessor") + if self.supersedes_information_state_ref == self.information_state_ref: + raise ValueError("information state cannot supersede itself") + + def _validate_memory_scope(self) -> None: + if self.participant_memory_scope == "episode_local_reset": + if self.memory_reset_authority_ref is None: + raise ValueError("episode_local_reset requires memory_reset_authority_ref") + elif self.memory_reset_authority_ref is not None: + raise ValueError("persistent_across_episodes must not claim a reset authority") + + def _validate_information_guarantee(self) -> None: + if self.information_guarantee in {"history_consistent", "perfect_recall"}: + required = { + "occurrence_history_ref": self.occurrence_history_ref, + "reconstruction_profile_ref": self.reconstruction_profile_ref, + "reconstruction_algorithm_id": self.reconstruction_algorithm_id, + "reconstruction_algorithm_version": self.reconstruction_algorithm_version, + "reconstruction_proof_ref": self.reconstruction_proof_ref, + "reconstructed_state_digest": self.reconstructed_state_digest, + } + missing = sorted(name for name, value in required.items() if value is None) + if missing: + raise ValueError("strong information state requires: " + ", ".join(missing)) + if self.reconstructed_state_digest != self.information_state_digest: + raise ValueError("reconstructed_state_digest must equal information_state_digest") + if self.information_guarantee == "perfect_recall" and self.occurrence_order_witness_ref is None: + raise ValueError("perfect_recall requires occurrence_order_witness_ref") + if self.information_guarantee == "lossy_projection" and not self.loss_disclosures: + raise ValueError("lossy_projection requires loss_disclosures") + + @model_validator(mode="after") + def _validate_information_state(self) -> ParticipantInformationStateRecordModel: + self._validate_identity_refs() + self._validate_memory_scope() + self._validate_information_guarantee() + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler.resolve_ref_schema(handler(core_schema)) + strong_fields = ( + "occurrence_history_ref", + "reconstruction_profile_ref", + "reconstruction_algorithm_id", + "reconstruction_algorithm_version", + "reconstruction_proof_ref", + "reconstructed_state_digest", + ) + json_schema.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": {"information_guarantee": {"enum": ["history_consistent", "perfect_recall"]}}, + "required": ["information_guarantee"], + }, + "then": { + "required": list(strong_fields), + "properties": {field_name: {"type": "string", "minLength": 1} for field_name in strong_fields}, + }, + }, + { + "if": { + "properties": {"information_guarantee": {"const": "perfect_recall"}}, + "required": ["information_guarantee"], + }, + "then": { + "required": ["occurrence_order_witness_ref"], + "properties": {"occurrence_order_witness_ref": {"type": "string", "minLength": 1}}, + }, + }, + { + "if": { + "properties": {"information_guarantee": {"const": "lossy_projection"}}, + "required": ["information_guarantee"], + }, + "then": { + "required": ["loss_disclosures"], + "properties": {"loss_disclosures": {"minItems": 1}}, + }, + }, + ] + ) + _add_raes_invariant( + json_schema, + "participant-information-state-context-resolution", + "Strong information-state claims resolve one profile, occurrence history, typed source set, proof " + "digest, and exact participant/episode/cut/projection/memory coordinate.", + validator=("raes_contracts.contracts.validate_participant_information_state_context"), + inputs=[{"contract_id": "participant-information-state-record-v1", "instance_path": "#"}], + ) + return json_schema + + +def _resolve_information_state_profile( + record: ParticipantInformationStateRecordModel, + reconstruction_profiles: Mapping[str, ParticipantInformationReconstructionProfileModel], +) -> ParticipantInformationReconstructionProfileModel | None: + if record.information_guarantee not in {"history_consistent", "perfect_recall"}: + return None + profile_ref = record.reconstruction_profile_ref + if profile_ref is None or profile_ref not in reconstruction_profiles: + raise ValueError("information state reconstruction profile does not resolve") + profile = reconstruction_profiles[profile_ref] + if profile.profile_id != profile_ref: + raise ValueError("information state reconstruction profile identity does not match") + if profile.information_state_schema_version != record.schema_version: + raise ValueError("information state schema version does not match reconstruction profile") + if profile.projection_version != record.projection_version: + raise ValueError("information state projection version does not match reconstruction profile") + if ( + profile.algorithm_id != record.reconstruction_algorithm_id + or profile.algorithm_version != record.reconstruction_algorithm_version + ): + raise ValueError("information state reconstruction algorithm does not match profile") + if record.state_cut.cut_kind not in profile.accepted_order_semantics: + raise ValueError("information state cut order is not admitted by reconstruction profile") + return profile + + +def _validate_information_state_occurrence( + record: ParticipantInformationStateRecordModel, + observation: ParticipantObservationEnvelopeModel, + *, + history_ref: str, + declared_source_keys: set[ParticipantInformationSourceKey], +) -> None: + if ("participant-observation-envelope-v1", observation.observation_ref) not in declared_source_keys: + raise ValueError("strong information state must preserve every occurrence as a typed source ref") + if observation.participant_address != record.participant_address: + raise ValueError("information state occurrence participant does not match") + if observation.episode_id != record.episode_id: + raise ValueError("information state occurrence episode does not match") + if observation.action_observation_history_ref != history_ref: + raise ValueError("information state occurrence history ref does not match") + if observation.visibility_projection_ref != record.visibility_projection_ref: + raise ValueError("information state occurrence visibility projection does not match") + if isinstance(record.state_cut, ParticipantDecisionSurfaceSequenceCutModel) and ( + observation.sequence_number is None or observation.sequence_number > record.state_cut.anchor_order + ): + raise ValueError("information state occurrence lies after the exact sequence cut") + + +def _validate_information_state_history( + record: ParticipantInformationStateRecordModel, + occurrence_histories: Mapping[str, Sequence[ParticipantObservationEnvelopeModel]], + declared_source_keys: set[ParticipantInformationSourceKey], +) -> None: + history_ref = record.occurrence_history_ref + if history_ref is None or history_ref not in occurrence_histories: + raise ValueError("information state occurrence history does not resolve") + history = occurrence_histories[history_ref] + if not history: + raise ValueError("strong information state requires a non-empty occurrence history") + for observation in history: + _validate_information_state_occurrence( + record, + observation, + history_ref=history_ref, + declared_source_keys=declared_source_keys, + ) + + +def _validate_information_state_sources( + record: ParticipantInformationStateRecordModel, + profile: ParticipantInformationReconstructionProfileModel | None, + resolved_sources: Mapping[ParticipantInformationSourceKey, object], + source_coordinates: Mapping[ + ParticipantInformationSourceKey, + ParticipantInformationStateSourceCoordinate, + ], +) -> None: + for source in record.source_refs: + key = (source.contract_id, source.ref) + if profile is not None and source.contract_id not in profile.accepted_input_contracts: + raise ValueError("information state source contract is not admitted by profile") + if key not in resolved_sources: + raise ValueError("information state source ref does not resolve") + require_source_coordinate(record, key, source_coordinates) + validate_resolved_source(record, source, resolved_sources[key]) + + +def _validate_information_state_proof( + record: ParticipantInformationStateRecordModel, + proof_digests: Mapping[str, str], +) -> None: + proof_ref = record.reconstruction_proof_ref + if proof_ref is None or proof_ref not in proof_digests: + raise ValueError("information state reconstruction proof does not resolve") + if proof_digests[proof_ref] != record.information_state_digest: + raise ValueError("information state proof digest does not match claimed digest") + + +def _validate_decision_surface_information_state_join( + record: ParticipantInformationStateRecordModel, + surface: ParticipantDecisionSurfaceV2Model, +) -> None: + assurance = surface.assurance + view = surface.participant_view + coordinate_checks = ( + ( + view.participant_address == record.participant_address and view.episode_id == record.episode_id, + "decision surface information-state identity does not match", + ), + ( + assurance.audience_scope_ref == record.audience_scope_ref, + "decision surface information-state audience scope does not match", + ), + ( + assurance.derivation_anchor.state_cut == record.state_cut, + "decision surface information-state cut does not match", + ), + ( + assurance.projection_policy_revision == record.projection_policy_revision, + "decision surface information-state projection revision does not match", + ), + ( + assurance.visibility_projection_ref == record.visibility_projection_ref, + "decision surface information-state visibility projection does not match", + ), + ( + assurance.participant_memory_scope == record.participant_memory_scope, + "decision surface information-state memory scope does not match", + ), + ( + assurance.memory_reset_authority_ref == record.memory_reset_authority_ref, + "decision surface information-state reset authority does not match", + ), + ( + view.redaction_policy_ref == record.redaction_policy_ref, + "decision surface information-state redaction policy does not match", + ), + ) + for coordinate_matches, message in coordinate_checks: + if not coordinate_matches: + raise ValueError(message) + + +def _validate_matching_decision_surfaces( + record: ParticipantInformationStateRecordModel, + decision_surfaces: Sequence[ParticipantDecisionSurfaceV2Model], +) -> None: + for surface in decision_surfaces: + if surface.participant_view.information_state_ref != record.information_state_ref: + continue + _validate_decision_surface_information_state_join(record, surface) + + +def validate_participant_information_state_context( + record: ParticipantInformationStateRecordModel, + *, + reconstruction_profiles: Mapping[str, ParticipantInformationReconstructionProfileModel], + occurrence_histories: Mapping[str, Sequence[ParticipantObservationEnvelopeModel]], + resolved_sources: Mapping[tuple[str, str], object], + proof_digests: Mapping[str, str], + source_coordinates: Mapping[ + ParticipantInformationSourceKey, + ParticipantInformationStateSourceCoordinate, + ] + | None = None, + decision_surfaces: Sequence[ParticipantDecisionSurfaceV2Model] = (), +) -> None: + """Resolve ACT-604's cross-contract exact-cut and strong-claim conditions.""" + + declared_source_keys = {(source.contract_id, source.ref) for source in record.source_refs} + profile = _resolve_information_state_profile(record, reconstruction_profiles) + if profile is not None: + _validate_information_state_history(record, occurrence_histories, declared_source_keys) + _validate_information_state_sources(record, profile, resolved_sources, source_coordinates or {}) + if profile is not None: + _validate_information_state_proof(record, proof_digests) + _validate_matching_decision_surfaces(record, decision_surfaces) + + +def validate_participant_information_state_resolved_context( + record: ParticipantInformationStateRecordModel, + resolver: ParticipantInformationStateContextResolver | None, + scope: object | None = None, +) -> None: + """Fail closed and apply the governed contextual invariant in production.""" + + if resolver is None: + raise ValueError("participant information-state context resolver is required") + try: + context = resolver(record, scope) + except Exception as exc: + raise ValueError("participant information-state context resolution failed") from exc + if not isinstance(context, ParticipantInformationStateValidationContext): + raise ValueError("participant information-state context did not resolve") + if any( + not isinstance(coordinate, ParticipantInformationStateSourceCoordinate) + for coordinate in context.source_coordinates.values() + ): + raise ValueError("participant information-state source coordinate resolution is invalid") + + reconstruction_profiles: dict[str, ParticipantInformationReconstructionProfileModel] = {} + if record.reconstruction_profile_ref is not None: + from ..participant_information_reconstruction_profiles import ( + load_participant_information_reconstruction_profile, + ) + + profile = load_participant_information_reconstruction_profile(record.reconstruction_profile_ref) + reconstruction_profiles[profile.profile_id] = profile + + try: + validate_participant_information_state_context( + record, + reconstruction_profiles=reconstruction_profiles, + occurrence_histories=context.occurrence_histories, + resolved_sources=context.resolved_sources, + source_coordinates=context.source_coordinates, + proof_digests=context.proof_digests, + decision_surfaces=context.decision_surfaces, + ) + except (AttributeError, KeyError, TypeError) as exc: + raise ValueError("participant information-state resolved context is malformed") from exc + + +__all__ = ( + "ParticipantInformationStateContextResolver", + "ParticipantInformationReconstructionProfileModel", + "ParticipantInformationStateRecordModel", + "ParticipantInformationStateSourceCoordinate", + "ParticipantInformationStateSourceRefModel", + "ParticipantInformationStateValidationContext", + "validate_participant_information_state_context", + "validate_participant_information_state_resolved_context", +) diff --git a/implementations/python/packages/raes_contracts/contracts/participant_information_state_sources.py b/implementations/python/packages/raes_contracts/contracts/participant_information_state_sources.py new file mode 100644 index 000000000..dd60a9091 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_information_state_sources.py @@ -0,0 +1,177 @@ +"""Contract-specific source validation for participant information state.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Protocol + +from .participant_context import ParticipantContextViewModel +from .participant_decision_state_cut import ( + ParticipantDecisionSurfaceSequenceCutModel, + ParticipantDecisionSurfaceStateCutModel, +) +from .participant_envelopes import ParticipantSharedStateRecordModel +from .participant_observation import ParticipantObservationEnvelopeModel +from .participant_runtime import ParticipantBehaviorHistoryEventModel, ParticipantEpisodeStateModel + +SourceKey = tuple[str, str] + + +class _InformationStateRecord(Protocol): + participant_address: str + episode_id: str + information_state_ref: str + state_cut: ParticipantDecisionSurfaceStateCutModel + audience_scope_ref: str + visibility_projection_ref: str + projection_policy_revision: str + redaction_policy_ref: str + redaction_policy_revision: str + + +class _InformationStateSourceCoordinate(Protocol): + participant_address: str + episode_id: str + state_cut: ParticipantDecisionSurfaceStateCutModel + audience_scope_ref: str + visibility_projection_ref: str + projection_policy_revision: str + redaction_policy_ref: str + redaction_policy_revision: str + + +class _InformationStateSourceRef(Protocol): + contract_id: str + ref: str + relation: str + + +_SOURCE_RELATIONS_BY_CONTRACT: Mapping[str, frozenset[str]] = { + "participant-observation-envelope-v1": frozenset({"observed", "disclosed"}), + "participant-context-view-v1": frozenset({"derived", "disclosed"}), + "participant-shared-state-record-v1": frozenset({"observed", "disclosed", "shared_state_projection"}), + "participant-behavior-history-event-stream-v1": frozenset({"observed"}), + "participant-episode-state-envelope-v1": frozenset({"authored_initial", "derived"}), +} + + +def require_source_coordinate( + record: _InformationStateRecord, + key: SourceKey, + source_coordinates: Mapping[SourceKey, _InformationStateSourceCoordinate], +) -> None: + """Require one trusted source coordinate to equal every governed record coordinate.""" + + coordinate = source_coordinates.get(key) + if coordinate is None: + raise ValueError("information state source governed coordinate does not resolve") + if coordinate.participant_address != record.participant_address or coordinate.episode_id != record.episode_id: + raise ValueError("information state source participant or episode coordinate does not match") + if coordinate.state_cut != record.state_cut: + raise ValueError("information state source cut membership does not match the exact state cut") + governed_coordinates = ( + ("audience scope", coordinate.audience_scope_ref, record.audience_scope_ref), + ("visibility projection", coordinate.visibility_projection_ref, record.visibility_projection_ref), + ("projection policy revision", coordinate.projection_policy_revision, record.projection_policy_revision), + ("redaction policy", coordinate.redaction_policy_ref, record.redaction_policy_ref), + ("redaction policy revision", coordinate.redaction_policy_revision, record.redaction_policy_revision), + ) + for label, actual, expected in governed_coordinates: + if actual != expected: + raise ValueError(f"information state source {label} coordinate does not match") + + +def _validate_source_sequence_cut(record: _InformationStateRecord, sequence_number: int | None) -> None: + if isinstance(record.state_cut, ParticipantDecisionSurfaceSequenceCutModel) and ( + sequence_number is None or sequence_number > record.state_cut.anchor_order + ): + raise ValueError("information state source lies after the exact sequence cut") + + +def _validate_observation_source(record: _InformationStateRecord, source_ref: str, resolved: object) -> None: + observation = ParticipantObservationEnvelopeModel.model_validate(resolved) + if observation.observation_ref != source_ref: + raise ValueError("information state observation source identity does not match") + if observation.participant_address != record.participant_address or observation.episode_id != record.episode_id: + raise ValueError("information state observation source coordinate does not match") + _validate_source_sequence_cut(record, observation.sequence_number) + if observation.visibility_projection_ref != record.visibility_projection_ref: + raise ValueError("information state observation visibility projection does not match") + if observation.redaction_policy_ref != record.redaction_policy_ref: + raise ValueError("information state observation redaction policy does not match") + if observation.information_state_ref not in {None, record.information_state_ref}: + raise ValueError("information state observation back-reference does not match") + + +def _validate_context_view_source(record: _InformationStateRecord, source_ref: str, resolved: object) -> None: + context_view = ParticipantContextViewModel.model_validate(resolved) + if source_ref not in {context_view.view_id, context_view.view_ref}: + raise ValueError("information state context-view source identity does not match") + if context_view.participant_address != record.participant_address or context_view.episode_id != record.episode_id: + raise ValueError("information state context-view source coordinate does not match") + if context_view.visibility_projection_ref != record.visibility_projection_ref: + raise ValueError("information state context-view visibility projection does not match") + if context_view.redaction_policy_ref != record.redaction_policy_ref: + raise ValueError("information state context-view redaction policy does not match") + + +def _validate_shared_state_source(record: _InformationStateRecord, source_ref: str, resolved: object) -> None: + shared_state = ParticipantSharedStateRecordModel.model_validate(resolved) + if source_ref not in {shared_state.event_id, shared_state.state_address}: + raise ValueError("information state shared-state source identity does not match") + if shared_state.participant_address != record.participant_address or shared_state.episode_id != record.episode_id: + raise ValueError("information state shared-state source coordinate does not match") + _validate_source_sequence_cut(record, shared_state.sequence_number) + if shared_state.visibility_projection_basis != record.visibility_projection_ref: + raise ValueError("information state shared-state visibility projection does not match") + if shared_state.redaction_policy_ref != record.redaction_policy_ref: + raise ValueError("information state shared-state redaction policy does not match") + + +def _validate_behavior_history_source(record: _InformationStateRecord, resolved: object) -> None: + if not isinstance(resolved, Sequence) or isinstance(resolved, (str, bytes, bytearray)) or not resolved: + raise ValueError("information state behavior-history source must resolve to a non-empty event stream") + for item in resolved: + event = ParticipantBehaviorHistoryEventModel.model_validate(item) + if event.participant_address != record.participant_address or event.episode_id != record.episode_id: + raise ValueError("information state behavior-history source coordinate does not match") + if isinstance(record.state_cut, ParticipantDecisionSurfaceSequenceCutModel): + _validate_source_sequence_cut(record, event.realized_order) + + +def _validate_episode_state_source(record: _InformationStateRecord, source_ref: str, resolved: object) -> None: + episode_state = ParticipantEpisodeStateModel.model_validate(resolved) + if source_ref != episode_state.episode_id: + raise ValueError("information state episode-state source identity does not match") + if episode_state.participant_address != record.participant_address or episode_state.episode_id != record.episode_id: + raise ValueError("information state episode-state source coordinate does not match") + if ( + isinstance(record.state_cut, ParticipantDecisionSurfaceSequenceCutModel) + and record.state_cut.history_domain == "participant_episode_lifecycle" + ): + _validate_source_sequence_cut(record, episode_state.sequence_number) + + +def validate_resolved_source( + record: _InformationStateRecord, + source: _InformationStateSourceRef, + resolved: object, +) -> None: + """Apply contract- and relation-specific source invariants.""" + + allowed_relations = _SOURCE_RELATIONS_BY_CONTRACT[source.contract_id] + if source.relation not in allowed_relations: + raise ValueError("information state source relation is not admitted for its contract") + if source.contract_id == "participant-observation-envelope-v1": + _validate_observation_source(record, source.ref, resolved) + elif source.contract_id == "participant-context-view-v1": + _validate_context_view_source(record, source.ref, resolved) + elif source.contract_id == "participant-shared-state-record-v1": + _validate_shared_state_source(record, source.ref, resolved) + elif source.contract_id == "participant-behavior-history-event-stream-v1": + _validate_behavior_history_source(record, resolved) + else: + _validate_episode_state_source(record, source.ref, resolved) + + +__all__ = ("require_source_coordinate", "validate_resolved_source") diff --git a/implementations/python/packages/raes_contracts/contracts/participant_manifests.py b/implementations/python/packages/raes_contracts/contracts/participant_manifests.py index 33a27a042..f2fca0977 100644 --- a/implementations/python/packages/raes_contracts/contracts/participant_manifests.py +++ b/implementations/python/packages/raes_contracts/contracts/participant_manifests.py @@ -13,7 +13,7 @@ PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS, PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, - PARTICIPANT_RUNTIME_POLICY_FEATURES, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, validate_backend_supported_contract_versions, validate_participant_implementation_supported_contract_versions, validate_participant_supported_contract_versions, @@ -80,7 +80,10 @@ def _validate_participant_policy_contracts(self) -> None: PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE ] for entry in participant_runtime.feature_support: - if entry.feature not in PARTICIPANT_RUNTIME_POLICY_FEATURES or entry.support_level.value == "unsupported": + if ( + entry.feature not in PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES + or entry.support_level.value == "unsupported" + ): continue missing = sorted(required_by_feature[entry.feature] - declared_contracts) if missing: diff --git a/implementations/python/packages/raes_contracts/contracts/participant_observation.py b/implementations/python/packages/raes_contracts/contracts/participant_observation.py new file mode 100644 index 000000000..f73287731 --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_observation.py @@ -0,0 +1,113 @@ +"""Participant-visible observation contracts.""" + +from __future__ import annotations + +from pydantic import Field, GetJsonSchemaHandler, model_validator +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema + +from .base import ContractModel, NonEmptyString, Rfc3339DateTimeString +from .participant_envelopes import ParticipantRuntimeBaseEnvelopeModel +from .participant_runtime import ( + ParticipantRuntimeDeliveryBasis, + ParticipantRuntimeInformationGuarantee, +) + + +class ParticipantObservationLossDescriptorModel(ContractModel): + """Declared projection-loss facts for one participant-visible observation.""" + + kind: NonEmptyString + fields_redacted: list[NonEmptyString] = Field(default_factory=list) + + +class ParticipantObservationStochasticContextModel(ContractModel): + """Seed and randomization-policy references behind one observation.""" + + seed_ref: NonEmptyString | None = None + randomization_policy_ref: NonEmptyString | None = None + + +class ParticipantObservationEnvelopeModel(ParticipantRuntimeBaseEnvelopeModel): + """SEM-210 participant-visible observation record with explicit guarantees.""" + + observation_ref: NonEmptyString + phase_ref: NonEmptyString | None = None + visibility_projection_ref: NonEmptyString + information_guarantee: ParticipantRuntimeInformationGuarantee + delivery_basis: ParticipantRuntimeDeliveryBasis + delivery_point_ref: NonEmptyString | None = None + delivered_at: Rfc3339DateTimeString | None = None + action_observation_history_ref: NonEmptyString | None = None + information_state_ref: NonEmptyString | None = None + hidden_state_refs: list[NonEmptyString] = Field(default_factory=list) + centralized_state_refs: list[NonEmptyString] = Field(default_factory=list) + loss_descriptor: ParticipantObservationLossDescriptorModel | None = None + stochastic_context: ParticipantObservationStochasticContextModel | None = None + noise_model_ref: NonEmptyString | None = None + reconstruction_algorithm_ref: NonEmptyString | None = None + reconstruction_proof_ref: NonEmptyString | None = None + belief_support_ref: NonEmptyString | None = None + redacted_field_refs: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_information_guarantee(self) -> ParticipantObservationEnvelopeModel: + if self.information_guarantee in {"history_consistent", "perfect_recall"}: + required_refs = { + "action_observation_history_ref": self.action_observation_history_ref, + "information_state_ref": self.information_state_ref, + "reconstruction_algorithm_ref": self.reconstruction_algorithm_ref, + "reconstruction_proof_ref": self.reconstruction_proof_ref, + } + missing = sorted(name for name, value in required_refs.items() if value is None) + if missing: + raise ValueError("strong information guarantee requires: " + ", ".join(missing)) + if self.information_guarantee == "lossy_projection" and self.loss_descriptor is None: + raise ValueError("lossy_projection requires loss_descriptor") + 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.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": {"information_guarantee": {"enum": ["history_consistent", "perfect_recall"]}}, + "required": ["information_guarantee"], + }, + "then": { + "required": [ + "action_observation_history_ref", + "information_state_ref", + "reconstruction_algorithm_ref", + "reconstruction_proof_ref", + ], + "properties": { + field_name: {"type": "string", "minLength": 1} + for field_name in ( + "action_observation_history_ref", + "information_state_ref", + "reconstruction_algorithm_ref", + "reconstruction_proof_ref", + ) + }, + }, + }, + { + "if": { + "properties": {"information_guarantee": {"const": "lossy_projection"}}, + "required": ["information_guarantee"], + }, + "then": { + "required": ["loss_descriptor"], + "properties": {"loss_descriptor": {"type": "object"}}, + }, + }, + ] + ) + return json_schema diff --git a/implementations/python/packages/raes_contracts/contracts/participant_opacity.py b/implementations/python/packages/raes_contracts/contracts/participant_opacity.py new file mode 100644 index 000000000..891068b8c --- /dev/null +++ b/implementations/python/packages/raes_contracts/contracts/participant_opacity.py @@ -0,0 +1,173 @@ +"""Portable bounded participant-opacity runtime-enforcement contracts.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import Field, model_validator + +from .base import BehavioralClaimBindingModel, ContractModel, NonEmptyString, PrefixedDigestString + +ParticipantOpacityObservationChannel = Literal[ + "participant-state", + "payload", + "decision", + "action-availability", + "delivery", + "retry", + "latency", + "order", + "policy-release", +] + + +class ParticipantOpacityObservationSurfaceModel(ContractModel): + """One concrete observer-visible surface in a runtime opacity inventory.""" + + surface_ref: NonEmptyString + profile_channel: ParticipantOpacityObservationChannel + owner_ref: NonEmptyString + disposition: Literal["mediated", "unreachable", "unsupported"] + occurrence_treatment: Literal["observable", "hidden", "not-applicable"] + content_treatment: Literal["projected", "hidden", "not-applicable"] + projection_ref: NonEmptyString + projection_revision: NonEmptyString + order_basis_ref: NonEmptyString + order_basis_revision: NonEmptyString + opportunity_basis_ref: NonEmptyString | None = None + opportunity_basis_revision: NonEmptyString | None = None + timing_bucket_ref: NonEmptyString | None = None + timing_bucket_revision: NonEmptyString | None = None + limitation_ref: NonEmptyString | None = None + + @model_validator(mode="after") + def _validate_optional_coordinates(self) -> ParticipantOpacityObservationSurfaceModel: + pairs = ( + ( + self.opportunity_basis_ref, + self.opportunity_basis_revision, + "opportunity basis", + ), + ( + self.timing_bucket_ref, + self.timing_bucket_revision, + "timing bucket", + ), + ) + for reference, revision, label in pairs: + if (reference is None) != (revision is None): + raise ValueError(f"participant opacity {label} ref and revision must be supplied together") + if self.disposition == "unsupported" and self.limitation_ref is None: + raise ValueError("unsupported participant opacity surfaces require a limitation ref") + return self + + +class ParticipantOpacityObservationInventoryModel(ContractModel): + """Closed concrete inventory for every channel claimed by one profile.""" + + inventory_ref: NonEmptyString + inventory_revision: NonEmptyString + observer_ref: NonEmptyString + audience_ref: NonEmptyString + surfaces: tuple[ParticipantOpacityObservationSurfaceModel, ...] = Field( + min_length=1, + max_length=64, + ) + + @model_validator(mode="after") + def _validate_unique_surfaces(self) -> ParticipantOpacityObservationInventoryModel: + refs = tuple(surface.surface_ref for surface in self.surfaces) + if len(refs) != len(set(refs)): + raise ValueError("participant opacity observation surface refs must be unique") + return self + + @property + def canonical_digest(self) -> str: + """Bind the exact closed inventory independently of runtime state.""" + + from raes_contracts.canonical import canonical_json_digest + + return canonical_json_digest(self.model_dump(mode="json")) + + +class ParticipantOpacityRuntimeEnforcementBindingModel(ContractModel): + """Safe finite runtime-enforcement binding owned by an API-423 decision.""" + + taxonomy_id: Literal["raes-behavioral-relations"] + taxonomy_revision: NonEmptyString + relation_id: Literal["participant-predicate-opacity"] + profile_id: NonEmptyString + profile_revision: NonEmptyString + profile_digest: PrefixedDigestString + predicate_ref: NonEmptyString + predicate_revision: NonEmptyString + carrier_ref: NonEmptyString + carrier_digest: PrefixedDigestString + materializer_ref: NonEmptyString + materializer_revision: NonEmptyString + materializer_digest: PrefixedDigestString + observation_inventory_ref: NonEmptyString + observation_inventory_revision: NonEmptyString + observation_inventory_digest: PrefixedDigestString + enforcement_rule_ref: NonEmptyString + enforcement_rule_revision: NonEmptyString + enforcement_rule_digest: PrefixedDigestString + state_cut_ref: NonEmptyString + state_cut_revision: NonEmptyString + memory_ref: NonEmptyString + memory_revision: NonEmptyString + release_ref: NonEmptyString + release_revision: NonEmptyString + assurance_axis: Literal["runtime-enforcement"] + claim: BehavioralClaimBindingModel + evidence_refs: list[NonEmptyString] = Field(min_length=1) + limitations: list[NonEmptyString] = Field(min_length=1) + explicit_non_claims: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_local_binding(self) -> ParticipantOpacityRuntimeEnforcementBindingModel: + if self.claim.assurance_axis != self.assurance_axis: + raise ValueError("participant opacity runtime claim assurance axis must match the decision binding") + if self.claim.assurance_status != "enforced" or self.claim.evidence_scope != "finite": + raise ValueError("participant opacity runtime claims must use enforced finite evidence") + if self.claim.quantifier_scope not in {"single-artifact", "finite-cases"}: + raise ValueError("participant opacity runtime claims must remain finitely quantified") + return self + + +class ParticipantOpacityRuntimeSupportModel(ContractModel): + """Trusted in-process support joined to one compact durable binding.""" + + binding: ParticipantOpacityRuntimeEnforcementBindingModel + observation_inventory: ParticipantOpacityObservationInventoryModel + predicate_positive_case_ref: NonEmptyString + predicate_negative_case_ref: NonEmptyString + initial_information_digest: PrefixedDigestString + normalized_observation_digest: PrefixedDigestString + + @model_validator(mode="after") + def _validate_inventory_binding(self) -> ParticipantOpacityRuntimeSupportModel: + identity = ( + self.observation_inventory.inventory_ref, + self.observation_inventory.inventory_revision, + self.observation_inventory.canonical_digest, + ) + expected = ( + self.binding.observation_inventory_ref, + self.binding.observation_inventory_revision, + self.binding.observation_inventory_digest, + ) + if identity != expected: + raise ValueError("participant opacity runtime support inventory does not match its durable binding") + if self.predicate_positive_case_ref == self.predicate_negative_case_ref: + raise ValueError("participant opacity runtime support requires distinct secret and nonsecret cases") + return self + + +__all__ = [ + "ParticipantOpacityObservationChannel", + "ParticipantOpacityObservationInventoryModel", + "ParticipantOpacityObservationSurfaceModel", + "ParticipantOpacityRuntimeEnforcementBindingModel", + "ParticipantOpacityRuntimeSupportModel", +] diff --git a/implementations/python/packages/raes_contracts/contracts/realization_plans.py b/implementations/python/packages/raes_contracts/contracts/realization_plans.py index a0dddafeb..24072c16e 100644 --- a/implementations/python/packages/raes_contracts/contracts/realization_plans.py +++ b/implementations/python/packages/raes_contracts/contracts/realization_plans.py @@ -12,6 +12,7 @@ from ..artifact_requirements import ArtifactSatisfactionDisclosureModel from ..planning import RuntimeDomain, require_plan_operation_identity from ..versions import OPERATION_SCHEMA_VERSION, RUNTIME_SNAPSHOT_SCHEMA_VERSION +from ..vocabulary import ObservationStrength, RealizationVerificationScope from .base import ContractModel, NonEmptyString from .execution_state import ( EvaluationHistoryEventModel, @@ -28,6 +29,7 @@ ParticipantTimeManagementContextModel, ) from .participant_execution import ParticipantExecutionServiceStateModel +from .participant_information_state import ParticipantInformationStateRecordModel from .participant_resource_budgets import ( ParticipantResourceBudgetEventModel, ParticipantResourceBudgetStateModel, @@ -155,6 +157,23 @@ class RealizationProvenanceEntryModel(ContractModel): ) +class RealizationObservationDisclosureModel(ContractModel): + """Value-free corroboration metadata for one realized inventory concern.""" + + address: CompiledAddress + field_path: NonEmptyString + domain: NonEmptyString + requirement_kind: NonEmptyString + verification_scope: RealizationVerificationScope + observation_strength: ObservationStrength = Field(json_schema_extra={"not": {"const": "none"}}) + + @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") + return self + + def _require_embedded_map_keys( values: Mapping[str, object], attribute: str, @@ -223,6 +242,7 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): participant_behavior_history: dict[str, list[ParticipantBehaviorHistoryEventModel]] = Field(default_factory=dict) participant_control_history: dict[str, list[ParticipantControlOccurrenceModel]] = Field(default_factory=dict) participant_crossing_history: dict[str, list[ParticipantCrossingOccurrenceModel]] = Field(default_factory=dict) + information_state_history: dict[str, list[ParticipantInformationStateRecordModel]] = Field(default_factory=dict) participant_autonomous_execution_states: dict[str, ParticipantAutonomousExecutionStateModel] = Field( default_factory=dict ) @@ -236,6 +256,7 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): time_management_contexts: dict[str, ParticipantTimeManagementContextModel] = Field(default_factory=dict) time_model_state: TimeRuntimeStateModel | None = None realization_provenance: list[RealizationProvenanceEntryModel] = Field(default_factory=list) + realization_observations: list[RealizationObservationDisclosureModel] = Field(default_factory=list) realization_envelope: RealizationEnvelopeIdentityModel | None = None metadata: dict[str, Any] = Field(default_factory=dict) @@ -276,10 +297,19 @@ def _validate_entry_addresses(self) -> RuntimeSnapshotEnvelopeModel: ) for values, attribute, message in key_checks: _require_embedded_map_keys(values, attribute, message) + for participant_address, records in self.information_state_history.items(): + if any(record.participant_address != participant_address for record in records): + raise ValueError("Information-state history map key must equal embedded participant_address") _validate_execution_service_budget_projection( self.participant_execution_services, self.participant_resource_budget_states, ) + observation_keys = [ + (entry.address, entry.field_path, entry.domain, entry.requirement_kind) + for entry in self.realization_observations + ] + if len(observation_keys) != len(set(observation_keys)): + raise ValueError("Runtime snapshot realization_observations must identify unique concerns") return self diff --git a/implementations/python/packages/raes_contracts/manifest_authority.py b/implementations/python/packages/raes_contracts/manifest_authority.py index ee8bf9176..f0d368203 100644 --- a/implementations/python/packages/raes_contracts/manifest_authority.py +++ b/implementations/python/packages/raes_contracts/manifest_authority.py @@ -96,6 +96,13 @@ } ) +PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES = frozenset( + { + *PARTICIPANT_RUNTIME_POLICY_FEATURES, + "participant_predicate_opacity", + } +) + _PARTICIPANT_EPISODE_CONTRACTS = frozenset( { "participant-episode-state-envelope-v1", @@ -128,6 +135,19 @@ "operation-status-v1", } ) +_PARTICIPANT_OPACITY_CONTRACTS = frozenset( + { + "operation-receipt-v1", + "operation-status-v1", + "runtime-snapshot-v1", + "participant-episode-state-envelope-v1", + "participant-episode-history-event-stream-v1", + "participant-behavior-history-event-stream-v1", + "participant-control-occurrence-v1", + "participant-crossing-occurrence-v1", + "participant-observation-envelope-v1", + } +) PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS = { PARTICIPANT_RUNTIME_ROLE_SCOPE: { @@ -145,6 +165,7 @@ "failure_classes": _PARTICIPANT_BEHAVIOR_CONTRACTS, "observation_boundaries": _PARTICIPANT_BEHAVIOR_CONTRACTS, "outcome_interpretation": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "participant_predicate_opacity": _PARTICIPANT_OPACITY_CONTRACTS, "participant_declassification": frozenset({"participant-crossing-occurrence-v1"}), "participant_directed_inject_delivery": frozenset( {"orchestration-plan-v1", "participant-crossing-occurrence-v1"} diff --git a/implementations/python/packages/raes_contracts/participant_information_reconstruction_profiles.py b/implementations/python/packages/raes_contracts/participant_information_reconstruction_profiles.py new file mode 100644 index 000000000..ed1d66785 --- /dev/null +++ b/implementations/python/packages/raes_contracts/participant_information_reconstruction_profiles.py @@ -0,0 +1,67 @@ +"""Closed loaders for ACT-604 participant information reconstruction profiles.""" + +from __future__ import annotations + +import json +from functools import cache +from pathlib import Path + +from raes.identifiers import is_portable_identifier + +from .contracts import ParticipantInformationReconstructionProfileModel +from .corpus import PROFILES, corpus_family_root + +SUPPORTED_PARTICIPANT_INFORMATION_RECONSTRUCTION_PROFILE_IDS = frozenset({"occurrence-prefix-evidence-v1"}) + + +def participant_information_reconstruction_profiles_root() -> Path: + return corpus_family_root(PROFILES) / "participant-information-reconstruction" + + +def _validate_profile_id(profile_id: str) -> None: + if not is_portable_identifier(profile_id): + raise ValueError( + f"participant information reconstruction profile id {profile_id!r} must be a portable SDL identifier: " + "1-64 lowercase ASCII letters, digits, hyphens, or underscores, starting with a letter or digit" + ) + if profile_id not in SUPPORTED_PARTICIPANT_INFORMATION_RECONSTRUCTION_PROFILE_IDS: + supported = ", ".join(sorted(SUPPORTED_PARTICIPANT_INFORMATION_RECONSTRUCTION_PROFILE_IDS)) + raise ValueError( + f"unsupported participant information reconstruction profile id {profile_id!r}; supported ids: {supported}" + ) + + +def participant_information_reconstruction_profile_path(profile_id: str) -> Path: + _validate_profile_id(profile_id) + return participant_information_reconstruction_profiles_root() / f"{profile_id}.json" + + +def load_participant_information_reconstruction_profile_from_path( + profile_id: str, + path: Path, +) -> ParticipantInformationReconstructionProfileModel: + _validate_profile_id(profile_id) + payload = json.loads(path.read_text(encoding="utf-8")) + profile = ParticipantInformationReconstructionProfileModel.model_validate(payload) + if profile.profile_id != profile_id: + raise ValueError("participant information reconstruction profile artifact identity does not match request") + return profile + + +@cache +def load_participant_information_reconstruction_profile( + profile_id: str, +) -> ParticipantInformationReconstructionProfileModel: + return load_participant_information_reconstruction_profile_from_path( + profile_id, + participant_information_reconstruction_profile_path(profile_id), + ) + + +__all__ = ( + "SUPPORTED_PARTICIPANT_INFORMATION_RECONSTRUCTION_PROFILE_IDS", + "load_participant_information_reconstruction_profile", + "load_participant_information_reconstruction_profile_from_path", + "participant_information_reconstruction_profile_path", + "participant_information_reconstruction_profiles_root", +) diff --git a/implementations/python/packages/raes_contracts/participant_information_state_history.py b/implementations/python/packages/raes_contracts/participant_information_state_history.py new file mode 100644 index 000000000..8ac61e6eb --- /dev/null +++ b/implementations/python/packages/raes_contracts/participant_information_state_history.py @@ -0,0 +1,227 @@ +"""Snapshot and transition invariants for ACT-604 information-state history.""" + +from __future__ import annotations + +from collections.abc import Iterator, Mapping, Sequence +from dataclasses import dataclass + +from pydantic import ValidationError + +from .contracts import ( + ParticipantInformationStateContextResolver, + ParticipantInformationStateRecordModel, + validate_participant_information_state_resolved_context, +) + +Violation = tuple[str, str] + + +@dataclass +class _SnapshotHistoryValidationContext: + trusted_history: Mapping[str, list[dict[str, object]]] + known_event_ids: set[str] + known_state_refs: set[str] + information_state_context_resolver: ParticipantInformationStateContextResolver | None + context_scope: object | None + + +def _history_entry_violation( + address: str, + participant_address: object, + raw_records: object, +) -> Violation | None: + violation = None + if not isinstance(participant_address, str) or not participant_address: + violation = (address, "information_state_history keys must be non-empty strings") + elif not isinstance(raw_records, Sequence) or isinstance(raw_records, (str, bytes, bytearray)): + violation = (f"{address}.{participant_address}", "information_state_history entries must be lists") + return violation + + +def _validated_history_record( + raw_record: object, + record_path: str, +) -> tuple[ParticipantInformationStateRecordModel | None, Violation | None]: + record = None + violation = None + if not isinstance(raw_record, Mapping): + violation = (record_path, "information-state history record must be a mapping") + else: + try: + record = ParticipantInformationStateRecordModel.model_validate(raw_record) + except ValidationError: + violation = (record_path, "information-state history record failed contract validation") + return record, violation + + +def _record_identity_violations( + record: ParticipantInformationStateRecordModel, + *, + participant_address: str, + record_path: str, + known_event_ids: set[str], + known_state_refs: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + if record.participant_address != participant_address: + violations.append((record_path, "information-state history map key must equal embedded participant_address")) + if record.event_id in known_event_ids: + violations.append((record_path, "information-state history event_id values must be unique")) + known_event_ids.add(record.event_id) + if record.information_state_ref in known_state_refs: + violations.append((record_path, "information_state_ref values must be unique across snapshot history")) + known_state_refs.add(record.information_state_ref) + return violations + + +def _record_lineage_violations( + record: ParticipantInformationStateRecordModel, + *, + record_path: str, + participant_prior_refs: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + missing_predecessors = sorted(set(record.predecessor_information_state_refs) - participant_prior_refs) + if missing_predecessors: + violations.append( + (record_path, "predecessor_information_state_refs must resolve to earlier participant history records") + ) + if ( + record.supersedes_information_state_ref is not None + and record.supersedes_information_state_ref not in participant_prior_refs + ): + violations.append( + (record_path, "supersedes_information_state_ref must resolve to an earlier participant history record") + ) + participant_prior_refs.add(record.information_state_ref) + return violations + + +def _record_context_violation( + record: ParticipantInformationStateRecordModel, + *, + record_path: str, + trusted_prefix_member: bool, + information_state_context_resolver: ParticipantInformationStateContextResolver | None, + context_scope: object | None, +) -> Violation | None: + violation = None + if not trusted_prefix_member: + if information_state_context_resolver is None: + violation = (record_path, "participant information-state context resolver is required") + else: + try: + validate_participant_information_state_resolved_context( + record, + information_state_context_resolver, + context_scope, + ) + except (TypeError, ValueError): + violation = (record_path, "information-state contextual validation failed") + return violation + + +def _participant_history_violations( + raw_records: Sequence[object], + *, + participant_address: str, + participant_path: str, + validation: _SnapshotHistoryValidationContext, +) -> list[Violation]: + violations: list[Violation] = [] + participant_prior_refs: set[str] = set() + for index, raw_record in enumerate(raw_records): + record_path = f"{participant_path}[{index}]" + record, contract_violation = _validated_history_record(raw_record, record_path) + if contract_violation is not None: + violations.append(contract_violation) + continue + assert record is not None + violations.extend( + _record_identity_violations( + record, + participant_address=participant_address, + record_path=record_path, + known_event_ids=validation.known_event_ids, + known_state_refs=validation.known_state_refs, + ) + ) + violations.extend( + _record_lineage_violations( + record, + record_path=record_path, + participant_prior_refs=participant_prior_refs, + ) + ) + context_violation = _record_context_violation( + record, + record_path=record_path, + trusted_prefix_member=index < len(validation.trusted_history.get(participant_address, [])), + information_state_context_resolver=validation.information_state_context_resolver, + context_scope=validation.context_scope, + ) + if context_violation is not None: + violations.append(context_violation) + return violations + + +def iter_participant_information_state_snapshot_violations( + information_state_history: object, + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, + context_scope: object | None = None, + trusted_history: Mapping[str, list[dict[str, object]]] | None = None, +) -> Iterator[Violation]: + """Yield fixed-message violations for one first-class snapshot history.""" + + address = "runtime.snapshot.information-state-history" + if not isinstance(information_state_history, Mapping): + yield address, "information_state_history must be a mapping" + else: + validation = _SnapshotHistoryValidationContext( + trusted_history=trusted_history or {}, + known_event_ids=set(), + known_state_refs=set(), + information_state_context_resolver=information_state_context_resolver, + context_scope=context_scope, + ) + for participant_address, raw_records in information_state_history.items(): + entry_violation = _history_entry_violation(address, participant_address, raw_records) + if entry_violation is not None: + yield entry_violation + continue + assert isinstance(participant_address, str) + assert isinstance(raw_records, Sequence) and not isinstance(raw_records, (str, bytes, bytearray)) + participant_path = f"{address}.{participant_address}" + yield from _participant_history_violations( + raw_records, + participant_address=participant_address, + participant_path=participant_path, + validation=validation, + ) + + +def iter_participant_information_state_history_transition_violations( + previous_history: Mapping[str, list[dict[str, object]]], + next_history: Mapping[str, list[dict[str, object]]], +) -> Iterator[Violation]: + """Require durable information-state histories to preserve an exact prefix.""" + + address = "runtime.snapshot.information-state-history" + for participant_address, previous_records in previous_history.items(): + participant_path = f"{address}.{participant_address}" + if participant_address not in next_history: + yield participant_path, "information-state history was removed" + continue + next_records = next_history[participant_address] + if len(next_records) < len(previous_records): + yield participant_path, ("information_state_history shrank and must be append-only") + continue + if next_records[: len(previous_records)] != previous_records: + yield participant_path, "information_state_history must be append-only" + + +__all__ = ( + "iter_participant_information_state_history_transition_violations", + "iter_participant_information_state_snapshot_violations", +) diff --git a/implementations/python/packages/raes_contracts/participant_opacity_runtime.py b/implementations/python/packages/raes_contracts/participant_opacity_runtime.py new file mode 100644 index 000000000..819431832 --- /dev/null +++ b/implementations/python/packages/raes_contracts/participant_opacity_runtime.py @@ -0,0 +1,488 @@ +"""Admission for bounded participant-opacity runtime-enforcement bindings.""" + +from __future__ import annotations + +from .behavioral_relation_profiles import ( + ActiveOpacityStrategyModel, + BehavioralRelationProfileModel, + IndividualOpacityObserverModel, + load_behavioral_relation_profile_revision, +) +from .behavioral_relations import ( + BehavioralRelationCatalogModel, + load_behavioral_relation_catalog_revision, + validate_behavioral_claim_binding, +) +from .canonical import canonical_json_digest +from .contracts.participant_crossing import ( + ParticipantOpacityObservationInventoryModel, + ParticipantOpacityObservationSurfaceModel, + ParticipantOpacityRuntimeEnforcementBindingModel, + ParticipantOpacityRuntimeSupportModel, +) + +_INVENTORY_REF = "participant-opacity-inventory:runtime-reference-v1" +_INVENTORY_REVISION = "rev1" +_PROJECTION_REF = "participant-opacity-observation:runtime-reference-v1" +_PROJECTION_REVISION = "rev1" +_ORDER_BASIS_REF = "participant-opacity-order:logical-crossing-v1" +_ORDER_BASIS_REVISION = "rev1" +_OPPORTUNITY_BASIS_REF = "participant-opacity-opportunity:crossing-v1" +_OPPORTUNITY_BASIS_REVISION = "rev1" +_TIMING_BUCKET_REF = "participant-opacity-timing:logical-bucket-v1" +_TIMING_BUCKET_REVISION = "rev1" +_POSITIVE_CASE_REF = "possible-point:runtime-reference-protected" +_NEGATIVE_CASE_REF = "possible-point:runtime-reference-complement" +_INITIAL_INFORMATION_DIGEST = canonical_json_digest({"initial_information": "participant-opacity-runtime-reference-v1"}) +_NORMALIZED_OBSERVATION = { + "action_availability": "denied", + "decision_content": "participant-opacity-contained", + "delivery": "withheld", + "latency": "logical-bucket:contained", + "observation": "uniform-denial", + "payload": "not-released", +} +_NORMALIZED_OBSERVATION_DIGEST = canonical_json_digest(_NORMALIZED_OBSERVATION) +_CARRIER_DIGEST = canonical_json_digest( + { + "carrier_ref": "possible-point-carrier:runtime-reference-v1", + "initial_information_digest": _INITIAL_INFORMATION_DIGEST, + "points": [_POSITIVE_CASE_REF, _NEGATIVE_CASE_REF], + "normalized_observation_digest": _NORMALIZED_OBSERVATION_DIGEST, + } +) +_MATERIALIZER_REF = "participant-opacity-materializer:runtime-reference-v1" +_MATERIALIZER_REVISION = "rev1" +_MATERIALIZER_DIGEST = canonical_json_digest({"materializer": "runtime-reference-v1"}) +_ENFORCEMENT_RULE_REF = "participant-opacity-enforcement:crossing-containment-v1" +_ENFORCEMENT_RULE_REVISION = "rev1" +_ENFORCEMENT_RULE_DIGEST = canonical_json_digest({"rule": "crossing-containment-v1"}) + +# This is the complete supported concrete surface set for the one reference +# profile. A broad semantic channel without every corresponding runtime route +# is deliberately insufficient for a positive enforcement claim. +_RUNTIME_SURFACE_SUPPORT: dict[str, tuple[str, str, str, str, str, bool, bool]] = { + "participant-opacity-surface:action-ingress": ( + "action-availability", + "runtime.participant-control:admit-action", + "mediated", + "observable", + "projected", + False, + False, + ), + "participant-opacity-surface:action-decision-selection": ( + "decision", + "runtime.participant-crossing:decision-surface-selection", + "mediated", + "observable", + "projected", + False, + False, + ), + "participant-opacity-surface:autonomous-scheduler-action": ( + "action-availability", + "runtime.participant-scheduler:autonomous-action", + "mediated", + "observable", + "projected", + False, + False, + ), + "participant-opacity-surface:supervisor-control": ( + "decision", + "runtime.participant-control:supervisor-occurrence", + "mediated", + "observable", + "projected", + False, + False, + ), + "participant-opacity-surface:episode-lifecycle": ( + "participant-state", + "runtime.participant-episode:lifecycle", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), + "participant-opacity-surface:execution-lifecycle-readback": ( + "participant-state", + "runtime.participant-execution-service:lifecycle-readback", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), + "participant-opacity-surface:status-view": ( + "participant-state", + "runtime.participant-retrieval:status-view", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), + "participant-opacity-surface:history-view": ( + "participant-state", + "runtime.participant-retrieval:history-view", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), + "participant-opacity-surface:context-view": ( + "participant-state", + "runtime.participant-retrieval:context-view", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), + "participant-opacity-surface:projected-payload": ( + "payload", + "runtime.participant-retrieval:projection-serialization", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), + "participant-opacity-surface:directed-inject": ( + "delivery", + "runtime.participant-retrieval:directed-inject", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), + "participant-opacity-surface:delivery-failure-omission": ( + "delivery", + "runtime.participant-crossing:delivery-status-opportunity", + "mediated", + "observable", + "projected", + True, + False, + ), + "participant-opacity-surface:operation-status-error": ( + "decision", + "runtime.control-plane:operation-status-error", + "mediated", + "observable", + "projected", + False, + False, + ), + "participant-opacity-surface:retry-replay": ( + "retry", + "runtime.participant-crossing:idempotency-replay", + "mediated", + "observable", + "projected", + False, + False, + ), + "participant-opacity-surface:logical-timing-bucket": ( + "latency", + "runtime.time-model:logical-bucket", + "mediated", + "observable", + "projected", + False, + True, + ), + "participant-opacity-surface:logical-causal-order": ( + "order", + "runtime.participant-crossing:logical-causal-order", + "mediated", + "observable", + "projected", + False, + False, + ), + "participant-opacity-surface:policy-release-effects": ( + "policy-release", + "runtime.participant-crossing:policy-release-effects", + "mediated", + "observable", + "projected", + False, + False, + ), + "participant-opacity-surface:authorized-evidence-audit-read": ( + "participant-state", + "runtime.control-plane:administrative-evidence-audit-read", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), + "participant-opacity-surface:native-backend-direct-use": ( + "action-availability", + "runtime.backend-calls:direct-native-adapter", + "unreachable", + "not-applicable", + "not-applicable", + False, + False, + ), +} + + +def validate_participant_opacity_runtime_enforcement( + binding: ParticipantOpacityRuntimeEnforcementBindingModel, + *, + support: ParticipantOpacityRuntimeSupportModel, + participant_address: str, + audience_scope_ref: str, + catalog: BehavioralRelationCatalogModel | None = None, + profile: BehavioralRelationProfileModel | None = None, +) -> ParticipantOpacityRuntimeEnforcementBindingModel: + """Resolve an exact finite profile and reject incomplete runtime coverage.""" + + catalog = load_behavioral_relation_catalog_revision(binding.taxonomy_revision) if catalog is None else catalog + profile = ( + load_behavioral_relation_profile_revision( + binding.profile_id, + binding.profile_revision, + ) + if profile is None + else profile + ) + validate_behavioral_claim_binding(binding.claim, catalog=catalog, profile=profile) + if support.binding != binding: + raise ValueError("participant opacity runtime support does not match the durable binding") + _validate_binding_coordinates(binding, support, profile) + _validate_observer(binding, support, profile, participant_address, audience_scope_ref) + _validate_inventory(support, profile) + return binding + + +def _validate_binding_coordinates( + binding: ParticipantOpacityRuntimeEnforcementBindingModel, + support: ParticipantOpacityRuntimeSupportModel, + profile: BehavioralRelationProfileModel, +) -> None: + parameters = profile.parameters + expected = ( + (binding.taxonomy_id, profile.taxonomy_id, "taxonomy id"), + (binding.taxonomy_revision, profile.taxonomy_revision, "taxonomy revision"), + (binding.relation_id, profile.relation_id, "relation"), + (binding.profile_id, profile.profile_id, "profile id"), + (binding.profile_revision, profile.profile_revision, "profile revision"), + (binding.profile_digest, profile.canonical_digest, "profile digest"), + (binding.predicate_ref, parameters.secret.predicate_ref, "predicate ref"), + (binding.predicate_revision, parameters.secret.predicate_revision, "predicate revision"), + (binding.carrier_ref, profile.left_carrier_ref, "carrier ref"), + (binding.carrier_digest, _CARRIER_DIGEST, "carrier digest"), + (binding.materializer_ref, _MATERIALIZER_REF, "materializer ref"), + (binding.materializer_revision, _MATERIALIZER_REVISION, "materializer revision"), + (binding.materializer_digest, _MATERIALIZER_DIGEST, "materializer digest"), + (binding.enforcement_rule_ref, _ENFORCEMENT_RULE_REF, "enforcement rule ref"), + (binding.enforcement_rule_revision, _ENFORCEMENT_RULE_REVISION, "enforcement rule revision"), + (binding.enforcement_rule_digest, _ENFORCEMENT_RULE_DIGEST, "enforcement rule digest"), + (binding.observation_inventory_ref, _INVENTORY_REF, "inventory ref"), + (binding.observation_inventory_revision, _INVENTORY_REVISION, "inventory revision"), + ( + binding.observation_inventory_digest, + support.observation_inventory.canonical_digest, + "inventory digest", + ), + (binding.state_cut_ref, parameters.horizon.cut_ref, "state cut ref"), + (binding.state_cut_revision, parameters.horizon.cut_revision, "state cut revision"), + (binding.memory_ref, parameters.memory.memory_ref, "memory ref"), + (binding.memory_revision, parameters.memory.memory_revision, "memory revision"), + (binding.release_ref, parameters.release.schedule_ref, "release ref"), + (binding.release_revision, parameters.release.schedule_revision, "release revision"), + ) + for actual, wanted, label in expected: + if actual != wanted: + raise ValueError(f"participant opacity runtime {label} does not match the exact profile") + if binding.claim.assurance_axis != "runtime-enforcement": + raise ValueError("participant opacity runtime claim must use the runtime-enforcement axis") + support_coordinates = ( + (support.predicate_positive_case_ref, _POSITIVE_CASE_REF, "predicate-positive case"), + (support.predicate_negative_case_ref, _NEGATIVE_CASE_REF, "predicate-negative case"), + (support.initial_information_digest, _INITIAL_INFORMATION_DIGEST, "initial information"), + ( + support.normalized_observation_digest, + _NORMALIZED_OBSERVATION_DIGEST, + "normalized observation", + ), + ) + for actual, wanted, label in support_coordinates: + if actual != wanted: + raise ValueError(f"participant opacity runtime {label} does not match the enforced normal form") + + +def _validate_observer( + binding: ParticipantOpacityRuntimeEnforcementBindingModel, + support: ParticipantOpacityRuntimeSupportModel, + profile: BehavioralRelationProfileModel, + participant_address: str, + audience_scope_ref: str, +) -> None: + observer = profile.parameters.observer + if not isinstance(observer, IndividualOpacityObserverModel): + raise ValueError("participant opacity runtime profile does not support coalition observers") + expected = ( + (participant_address, observer.participant_ref, "observer participant"), + (audience_scope_ref, observer.audience_ref, "observer audience"), + (support.observation_inventory.observer_ref, observer.participant_ref, "inventory observer"), + (support.observation_inventory.audience_ref, observer.audience_ref, "inventory audience"), + (binding.claim.subject, observer.participant_ref, "claim observer"), + ) + for actual, wanted, label in expected: + if actual != wanted: + raise ValueError(f"participant opacity runtime {label} does not match the profile observer") + + +def _validate_inventory( + support: ParticipantOpacityRuntimeSupportModel, + profile: BehavioralRelationProfileModel, +) -> None: + parameters = profile.parameters + inventory = support.observation_inventory + surfaces = inventory.surfaces + _validate_inventory_identity(inventory) + declared_channels = _validate_inventory_channels( + surfaces, + tuple(parameters.observation.observable_channels), + ) + _validate_inventory_time_semantics(inventory, profile, declared_channels) + actual_by_ref = _validate_inventory_surface_set(surfaces) + _validate_inventory_surface_coordinates(actual_by_ref) + if isinstance(parameters.strategy, ActiveOpacityStrategyModel) and not any( + surface.profile_channel == "action-availability" and surface.disposition == "mediated" for surface in surfaces + ): + raise ValueError("participant opacity active strategy has an unmediated probe surface") + + +def _validate_inventory_identity(inventory: ParticipantOpacityObservationInventoryModel) -> None: + if ( + inventory.inventory_ref, + inventory.inventory_revision, + ) != (_INVENTORY_REF, _INVENTORY_REVISION): + raise ValueError("participant opacity runtime inventory identity does not match the supported declaration") + unsupported = sorted(surface.surface_ref for surface in inventory.surfaces if surface.disposition == "unsupported") + if unsupported: + raise ValueError("participant opacity runtime inventory contains unsupported observation surfaces") + + +def _validate_inventory_channels( + surfaces: tuple[ParticipantOpacityObservationSurfaceModel, ...], + observable_channels: tuple[str, ...], +) -> set[str]: + declared_channels = set(observable_channels) + present_channels = {surface.profile_channel for surface in surfaces} + missing = sorted(declared_channels - present_channels) + extra = sorted(present_channels - declared_channels) + if missing: + raise ValueError("participant opacity runtime inventory is missing claimed channels: " + ", ".join(missing)) + if extra: + raise ValueError("participant opacity runtime inventory contains undeclared channels: " + ", ".join(extra)) + return declared_channels + + +def _validate_inventory_time_semantics( + inventory: ParticipantOpacityObservationInventoryModel, + profile: BehavioralRelationProfileModel, + declared_channels: set[str], +) -> None: + parameters = profile.parameters + surfaces = inventory.surfaces + if parameters.time.absence_observable: + opportunities = [ + surface + for surface in surfaces + if surface.profile_channel == "delivery" and surface.disposition == "mediated" + ] + if not any( + ( + surface.opportunity_basis_ref, + surface.opportunity_basis_revision, + ) + == ( + parameters.time.opportunity_basis_ref, + parameters.time.opportunity_basis_revision, + ) + for surface in opportunities + ): + raise ValueError("participant opacity observable omission requires the exact opportunity basis") + if "latency" in declared_channels: + latency_surfaces = [surface for surface in surfaces if surface.profile_channel == "latency"] + if any(surface.timing_bucket_ref is None for surface in latency_surfaces): + raise ValueError("participant opacity logical latency requires a governed timing bucket") + + +def _validate_inventory_surface_set( + surfaces: tuple[ParticipantOpacityObservationSurfaceModel, ...], +) -> dict[str, ParticipantOpacityObservationSurfaceModel]: + actual_by_ref = {surface.surface_ref: surface for surface in surfaces} + expected_refs = set(_RUNTIME_SURFACE_SUPPORT) + actual_refs = set(actual_by_ref) + missing_surfaces = sorted(expected_refs - actual_refs) + extra_surfaces = sorted(actual_refs - expected_refs) + if missing_surfaces: + raise ValueError( + "participant opacity runtime inventory is missing concrete surfaces: " + ", ".join(missing_surfaces) + ) + if extra_surfaces: + raise ValueError( + "participant opacity runtime inventory contains unadmitted concrete surfaces: " + ", ".join(extra_surfaces) + ) + return actual_by_ref + + +def _validate_inventory_surface_coordinates( + actual_by_ref: dict[str, ParticipantOpacityObservationSurfaceModel], +) -> None: + for surface_ref, expected in _RUNTIME_SURFACE_SUPPORT.items(): + channel, owner, disposition, occurrence, content, opportunity, timing = expected + surface = actual_by_ref[surface_ref] + coordinates = ( + surface.profile_channel, + surface.owner_ref, + surface.disposition, + surface.occurrence_treatment, + surface.content_treatment, + surface.projection_ref, + surface.projection_revision, + surface.order_basis_ref, + surface.order_basis_revision, + surface.opportunity_basis_ref, + surface.opportunity_basis_revision, + surface.timing_bucket_ref, + surface.timing_bucket_revision, + surface.limitation_ref, + ) + wanted = ( + channel, + owner, + disposition, + occurrence, + content, + _PROJECTION_REF, + _PROJECTION_REVISION, + _ORDER_BASIS_REF, + _ORDER_BASIS_REVISION, + _OPPORTUNITY_BASIS_REF if opportunity else None, + _OPPORTUNITY_BASIS_REVISION if opportunity else None, + _TIMING_BUCKET_REF if timing else None, + _TIMING_BUCKET_REVISION if timing else None, + None, + ) + if coordinates != wanted: + raise ValueError( + f"participant opacity runtime surface {surface_ref} does not match the supported declaration" + ) + + +__all__ = ["validate_participant_opacity_runtime_enforcement"] diff --git a/implementations/python/packages/raes_contracts/planning.py b/implementations/python/packages/raes_contracts/planning.py index a9f151872..cf49ec120 100644 --- a/implementations/python/packages/raes_contracts/planning.py +++ b/implementations/python/packages/raes_contracts/planning.py @@ -1,7 +1,15 @@ -"""Shared runtime planning contracts.""" +"""Shared runtime planning contracts and safe planned-resource readers. + +Backend and provisioner implementations should use the named +``planned_*`` accessors in this module instead of traversing a +:class:`PlannedResource` payload directly. The accessors are total, perform no +validation or normalization, and return ``None`` when a requested surface is +missing or does not apply to the resource's domain and type. +""" from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass, field from enum import Enum from typing import TYPE_CHECKING, Any @@ -83,6 +91,89 @@ def __post_init__(self) -> None: require_compiled_address(dependency, field_name="dependency address") +def planned_resource_payload(resource: PlannedResource) -> Mapping[str, Any] | None: + """Return the resource's mapping payload, or ``None`` when it is malformed. + + The original mapping is returned without copying or mutation. This reader + is domain-neutral; callers should use the narrower provisioning readers + below for fields whose meaning depends on a runtime domain or resource type. + """ + + payload = resource.payload + return payload if isinstance(payload, Mapping) else None + + +def planned_resource_authored_name(resource: PlannedResource) -> str | None: + """Return the authored top-level resource name, if one is available.""" + + payload = planned_resource_payload(resource) + if payload is None: + return None + name = payload.get("name") or payload.get("node_name") + return name if isinstance(name, str) and name else None + + +def planned_resource_name(resource: PlannedResource) -> str: + """Return the authored resource name or its full canonical address. + + The address is a stable, provider-neutral fallback. Backends that require + provider-safe native names remain responsible for deriving those names from + the address. + """ + + return planned_resource_authored_name(resource) or resource.address + + +def planned_node_spec(resource: PlannedResource) -> Mapping[str, Any] | None: + """Return ``spec.node`` for a provisioning node, otherwise ``None``.""" + + if resource.domain is not RuntimeDomain.PROVISIONING or resource.resource_type != "node": + return None + payload = planned_resource_payload(resource) + spec = payload.get("spec") if payload is not None else None + node = spec.get("node") if isinstance(spec, Mapping) else None + return node if isinstance(node, Mapping) else None + + +def planned_node_source(resource: PlannedResource) -> str | Mapping[str, Any] | None: + """Return a provisioning node's authored string-or-mapping source. + + Mapping sources retain all source/build inputs and are not collapsed to a + backend-specific image name. Empty strings and unsupported value shapes + are treated as absent. + """ + + node = planned_node_spec(resource) + source = node.get("source") if node is not None else None + if isinstance(source, str): + return source if source else None + return source if isinstance(source, Mapping) else None + + +def planned_node_resources(resource: PlannedResource) -> Mapping[str, Any] | None: + """Return a provisioning node's authored resources mapping, if present.""" + + node = planned_node_spec(resource) + resources = node.get("resources") if node is not None else None + return resources if isinstance(resources, Mapping) else None + + +def planned_infrastructure_spec(resource: PlannedResource) -> Mapping[str, Any] | None: + """Return ``spec.infrastructure`` for a provisioning node or network. + + Infrastructure is intentionally unavailable for other provisioning + resource types and for other runtime domains rather than being inferred + from a coincidentally similar payload shape. + """ + + if resource.domain is not RuntimeDomain.PROVISIONING or resource.resource_type not in {"node", "network"}: + return None + payload = planned_resource_payload(resource) + spec = payload.get("spec") if payload is not None else None + infrastructure = spec.get("infrastructure") if isinstance(spec, Mapping) else None + return infrastructure if isinstance(infrastructure, Mapping) else None + + @dataclass(frozen=True) class PlanOperation: """A reconciliation operation for a planned resource.""" @@ -216,5 +307,12 @@ def _validate_plan_addresses( "ProvisionOp", "ProvisioningPlan", "RuntimeDomain", + "planned_infrastructure_spec", + "planned_node_resources", + "planned_node_source", + "planned_node_spec", + "planned_resource_authored_name", + "planned_resource_name", + "planned_resource_payload", "require_plan_operation_identity", ) diff --git a/implementations/python/packages/raes_contracts/realization_envelope_carrier.py b/implementations/python/packages/raes_contracts/realization_envelope_carrier.py index c3dc4fc0d..18dcf64ff 100644 --- a/implementations/python/packages/raes_contracts/realization_envelope_carrier.py +++ b/implementations/python/packages/raes_contracts/realization_envelope_carrier.py @@ -14,19 +14,11 @@ from raes_contracts.contracts import ContractModel, NonEmptyString, RealizationEnvelopeIdentityModel from raes_contracts.realization_envelope import RealizationEnvelopeModel +from raes_contracts.vocabulary import ObservationStrength DigestString = Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")] -class ObservationStrength(str, Enum): - """Strongest evidence a backend configuration emits for one concern.""" - - NONE = "none" - DRIVER_REPORTED = "driver-reported" - DAEMON_OBSERVED = "daemon-observed" - GUEST_OBSERVED = "guest-observed" - - class ConcernDisposition(str, Enum): """How the selected realizer treats a governed concern.""" diff --git a/implementations/python/packages/raes_contracts/realization_observation.py b/implementations/python/packages/raes_contracts/realization_observation.py index 51da8b5ba..0b94214ed 100644 --- a/implementations/python/packages/raes_contracts/realization_observation.py +++ b/implementations/python/packages/raes_contracts/realization_observation.py @@ -4,7 +4,9 @@ from dataclasses import dataclass +from raes_contracts.addressing import require_compiled_address from raes_contracts.realization_envelope import ObservationStrength, RealizationConcern +from raes_contracts.vocabulary import RealizationVerificationScope @dataclass(frozen=True) @@ -31,4 +33,28 @@ class RealizationObservation: binding_verified: bool = False -__all__ = ["RealizationObservation"] +@dataclass(frozen=True) +class RealizationObservationDisclosure: + """Value-free corroboration metadata for one realized inventory concern.""" + + address: str + field_path: str + domain: str + requirement_kind: str + verification_scope: RealizationVerificationScope + observation_strength: ObservationStrength + + def __post_init__(self) -> None: + require_compiled_address(self.address) + for field_name in ("field_path", "domain", "requirement_kind"): + if not getattr(self, field_name).strip(): + raise ValueError(f"RealizationObservationDisclosure.{field_name} must be non-empty") + if not isinstance(self.verification_scope, RealizationVerificationScope): + raise TypeError("verification_scope must be RealizationVerificationScope") + if not isinstance(self.observation_strength, ObservationStrength): + raise TypeError("observation_strength must be ObservationStrength") + if self.observation_strength is ObservationStrength.NONE: + raise ValueError("realization observation disclosure must provide non-none evidence") + + +__all__ = ["RealizationObservation", "RealizationObservationDisclosure"] diff --git a/implementations/python/packages/raes_contracts/runtime_state.py b/implementations/python/packages/raes_contracts/runtime_state.py index 39ec4a33e..4aacf3bb6 100644 --- a/implementations/python/packages/raes_contracts/runtime_state.py +++ b/implementations/python/packages/raes_contracts/runtime_state.py @@ -13,6 +13,7 @@ from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_autonomous_state import require_participant_autonomous_state_snapshot from raes_contracts.planning import RuntimeDomain +from raes_contracts.realization_observation import RealizationObservationDisclosure from raes_contracts.versions import OPERATION_SCHEMA_VERSION, RUNTIME_SNAPSHOT_SCHEMA_VERSION if TYPE_CHECKING: @@ -88,6 +89,7 @@ class RuntimeSnapshot: participant_behavior_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) participant_control_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) participant_crossing_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) + information_state_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) participant_autonomous_execution_states: dict[str, dict[str, Any]] = field(default_factory=dict) participant_execution_services: dict[str, dict[str, Any]] = field(default_factory=dict) participant_resource_budget_states: dict[str, dict[str, Any]] = field(default_factory=dict) @@ -101,6 +103,7 @@ class RuntimeSnapshot: # SEM-218 invariant I5: per-concern provenance for realized realization # concerns recorded across this snapshot's result / history surfaces. realization_provenance: tuple[RealizationProvenanceEntry, ...] = () + realization_observations: tuple[RealizationObservationDisclosure, ...] = () realization_envelope: RealizationEnvelopeIdentityModel | None = None metadata: dict[str, Any] = field(default_factory=dict) @@ -115,6 +118,14 @@ def __post_init__(self) -> None: self.participant_resource_pool_states, self.participant_resource_budget_events, ) + if any(not isinstance(entry, RealizationObservationDisclosure) for entry in self.realization_observations): + raise TypeError("RuntimeSnapshot realization_observations must contain typed disclosures") + observation_keys = [ + (entry.address, entry.field_path, entry.domain, entry.requirement_kind) + for entry in self.realization_observations + ] + if len(observation_keys) != len(set(observation_keys)): + raise ValueError("RuntimeSnapshot realization_observations must identify unique concerns") def get(self, address: str) -> SnapshotEntry | None: return self.entries.get(address) @@ -178,6 +189,11 @@ def _snapshot_result_updates( "participant_crossing_history", snapshot.participant_crossing_history, ), + "information_state_history": _history_update( + updates, + "information_state_history", + snapshot.information_state_history, + ), } @@ -251,6 +267,11 @@ def _snapshot_updates( "realization_provenance", snapshot.realization_provenance, ), + "realization_observations": _observation_disclosures_update( + updates, + "realization_observations", + snapshot.realization_observations, + ), "realization_envelope": _identity_update( updates, "realization_envelope", @@ -271,6 +292,7 @@ def _snapshot_updates( "participant_behavior_history", "participant_control_history", "participant_crossing_history", + "information_state_history", "participant_autonomous_execution_states", "participant_execution_services", "participant_resource_budget_states", @@ -282,6 +304,7 @@ def _snapshot_updates( "time_management_contexts", "time_model_state", "realization_provenance", + "realization_observations", "realization_envelope", "metadata", } @@ -357,6 +380,19 @@ def _provenance_update( return raw +def _observation_disclosures_update( + updates: Mapping[str, object], + key: str, + current: tuple[RealizationObservationDisclosure, ...], +) -> tuple[RealizationObservationDisclosure, ...]: + raw = updates.get(key) + if raw is None: + return tuple(current) + if not isinstance(raw, tuple) or any(not isinstance(entry, RealizationObservationDisclosure) for entry in raw): + raise TypeError(f"{key} must be a tuple of RealizationObservationDisclosure") + return raw + + def _identity_update( updates: Mapping[str, object], key: str, @@ -454,6 +490,7 @@ class RuntimeSnapshotEnvelope: "OperationReceipt", "OperationState", "OperationStatus", + "RealizationObservationDisclosure", "RealizationProvenanceEntry", "RuntimeSnapshot", "RuntimeSnapshotEnvelope", diff --git a/implementations/python/packages/raes_contracts/versions.py b/implementations/python/packages/raes_contracts/versions.py index a0af58b87..9f15cce15 100644 --- a/implementations/python/packages/raes_contracts/versions.py +++ b/implementations/python/packages/raes_contracts/versions.py @@ -1,6 +1,7 @@ """Version identifiers for published external contracts.""" SCENARIO_INSTANTIATION_REQUEST_SCHEMA_VERSION = "scenario-instantiation/v1" +ARTIFACT_TRANSFORMATION_REPORT_SCHEMA_VERSION = "artifact-transformation-report/v1" ARTIFACT_REQUIREMENT_SCHEMA_VERSION = "artifact-requirement/v1" BACKEND_MANIFEST_V2_SCHEMA_VERSION = "backend-manifest/v2" REALIZATION_ENVELOPE_SCHEMA_VERSION = "realization-envelope/v1" @@ -39,6 +40,7 @@ PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION = "participant-episode-state/v1" PARTICIPANT_LIFECYCLE_EVENT_V1_SCHEMA_VERSION = "participant-lifecycle-event/v1" PARTICIPANT_OBSERVATION_ENVELOPE_V1_SCHEMA_VERSION = "participant-observation-envelope/v1" +PARTICIPANT_INFORMATION_RECONSTRUCTION_PROFILE_V1_SCHEMA_VERSION = "participant-information-reconstruction-profile/v1" PARTICIPANT_SHARED_STATE_RECORD_V1_SCHEMA_VERSION = "participant-shared-state-record/v1" PARTICIPANT_OUTCOME_REPORT_V1_SCHEMA_VERSION = "participant-outcome-report/v1" PARTICIPANT_STATUS_VIEW_V1_SCHEMA_VERSION = "participant-status-view/v1" diff --git a/implementations/python/packages/raes_contracts/vocabulary.py b/implementations/python/packages/raes_contracts/vocabulary.py index 4b4bf866d..96a973cf2 100644 --- a/implementations/python/packages/raes_contracts/vocabulary.py +++ b/implementations/python/packages/raes_contracts/vocabulary.py @@ -58,6 +58,35 @@ class RealizationSupportMode(str, Enum): OPEN_REALIZATION = "open-realization" +class ObservationStrength(str, Enum): + """Strongest evidence a backend configuration emits for one concern.""" + + NONE = "none" + DRIVER_REPORTED = "driver-reported" + DAEMON_OBSERVED = "daemon-observed" + GUEST_OBSERVED = "guest-observed" + + +class RealizationVerificationScope(str, Enum): + """Closed scope at which an inventory realization was corroborated.""" + + PRESENCE = "presence" + CONFIGURATION = "configuration" + + +def verification_scope_satisfies( + actual: RealizationVerificationScope, + required: RealizationVerificationScope, +) -> bool: + """Return whether an observation covers the required inventory scope.""" + + rank = { + RealizationVerificationScope.PRESENCE: 0, + RealizationVerificationScope.CONFIGURATION: 1, + } + return rank[actual] >= rank[required] + + class Closure(str, Enum): """Whether unspecified realizable dimensions under a scope are admitted.""" diff --git a/implementations/python/packages/raes_contracts/workflow.py b/implementations/python/packages/raes_contracts/workflow.py deleted file mode 100644 index b4dd232e1..000000000 --- a/implementations/python/packages/raes_contracts/workflow.py +++ /dev/null @@ -1,688 +0,0 @@ -"""Shared workflow runtime result contracts.""" - -from __future__ import annotations - -from collections.abc import Iterable, Mapping -from dataclasses import dataclass, field -from enum import Enum -from typing import Any - -from raes.semantics.workflow import WorkflowStepSemanticContract -from raes.semantics.workflow import validate_workflow_step_result as _validate_workflow_step_result - -from raes_contracts._validation import ( - enum_value, - optional_enum_value, - require_dict, - require_list, - require_non_empty_string, - require_optional_string, - require_string, - require_strings, -) -from raes_contracts.versions import WORKFLOW_STATE_SCHEMA_VERSION - - -class WorkflowStepLifecycle(str, Enum): - """Portable execution lifecycle for workflow-visible step state.""" - - PENDING = "pending" - RUNNING = "running" - COMPLETED = "completed" - - -class WorkflowStepOutcome(str, Enum): - """Portable execution outcomes for workflow-visible step state.""" - - SUCCEEDED = "succeeded" - FAILED = "failed" - EXHAUSTED = "exhausted" - - -def _validate_provenance_string_tuple(field_name: str, values: object) -> None: - if not isinstance(values, tuple) or any(not isinstance(item, str) or not item for item in values): - raise TypeError(f"{field_name} must be a tuple of non-empty strings") - if len(values) != len(set(values)): - raise ValueError(f"{field_name} entries must be unique") - - -@dataclass(frozen=True) -class WorkflowStepAttemptProvenance: - """Portable provenance for one governed workflow-step realization attempt.""" - - step_name: str - execution_mode: str - attempt_id: str - objective_address: str = "" - procedure_ref: str = "" - exposed_scaffold_refs: tuple[str, ...] = () - allowed_action_families: tuple[str, ...] = () - selected_action_family: str = "" - selected_tool_ref: str = "" - selected_affordance_ref: str = "" - fact_versions: tuple[str, ...] = () - outcome: str = "" - evidence_refs: tuple[str, ...] = () - assertion_truth_refs: tuple[str, ...] = () - participant_report: str = "" - - def __post_init__(self) -> None: - for field_name in ("step_name", "execution_mode", "attempt_id"): - value = getattr(self, field_name) - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"{field_name} must be a non-empty string") - if self.execution_mode not in {"scripted", "objective", "scaffolded"}: - raise ValueError("execution_mode must be scripted, objective, or scaffolded") - if self.outcome not in {"", "succeeded", "failed", "exhausted"}: - raise ValueError("outcome must be a portable workflow step outcome") - for field_name in ( - "exposed_scaffold_refs", - "allowed_action_families", - "fact_versions", - "evidence_refs", - "assertion_truth_refs", - ): - values = getattr(self, field_name) - _validate_provenance_string_tuple(field_name, values) - if self.outcome == "succeeded" and not (self.evidence_refs and self.assertion_truth_refs): - raise ValueError("successful workflow step provenance requires evidence-bearing assertion truth") - - @classmethod - def from_payload(cls, payload: Mapping[str, Any]) -> WorkflowStepAttemptProvenance: - if not isinstance(payload, Mapping): - raise TypeError("workflow step attempt provenance must be a mapping") - return cls( - step_name=str(payload.get("step_name", "")), - execution_mode=str(payload.get("execution_mode", "")), - attempt_id=str(payload.get("attempt_id", "")), - objective_address=str(payload.get("objective_address", "")), - procedure_ref=str(payload.get("procedure_ref", "")), - exposed_scaffold_refs=tuple(payload.get("exposed_scaffold_refs", ())), - allowed_action_families=tuple(payload.get("allowed_action_families", ())), - selected_action_family=str(payload.get("selected_action_family", "")), - selected_tool_ref=str(payload.get("selected_tool_ref", "")), - selected_affordance_ref=str(payload.get("selected_affordance_ref", "")), - fact_versions=tuple(payload.get("fact_versions", ())), - outcome=str(payload.get("outcome", "")), - evidence_refs=tuple(payload.get("evidence_refs", ())), - assertion_truth_refs=tuple(payload.get("assertion_truth_refs", ())), - participant_report=str(payload.get("participant_report", "")), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "step_name": self.step_name, - "execution_mode": self.execution_mode, - "attempt_id": self.attempt_id, - "objective_address": self.objective_address, - "procedure_ref": self.procedure_ref, - "exposed_scaffold_refs": list(self.exposed_scaffold_refs), - "allowed_action_families": list(self.allowed_action_families), - "selected_action_family": self.selected_action_family, - "selected_tool_ref": self.selected_tool_ref, - "selected_affordance_ref": self.selected_affordance_ref, - "fact_versions": list(self.fact_versions), - "outcome": self.outcome, - "evidence_refs": list(self.evidence_refs), - "assertion_truth_refs": list(self.assertion_truth_refs), - "participant_report": self.participant_report, - } - - -class WorkflowStatus(str, Enum): - """Portable workflow-level execution status.""" - - PENDING = "pending" - RUNNING = "running" - SUCCEEDED = "succeeded" - FAILED = "failed" - CANCELLED = "cancelled" - TIMED_OUT = "timed_out" - - -class WorkflowCompensationStatus(str, Enum): - """Portable workflow compensation status.""" - - NOT_REQUIRED = "not_required" - PENDING = "pending" - RUNNING = "running" - SUCCEEDED = "succeeded" - FAILED = "failed" - - -class WorkflowHistoryEventType(str, Enum): - """Portable workflow history event kinds.""" - - WORKFLOW_STARTED = "workflow_started" - STEP_STARTED = "step_started" - STEP_COMPLETED = "step_completed" - SWITCH_CASE_SELECTED = "switch_case_selected" - CALL_STARTED = "call_started" - CALL_COMPLETED = "call_completed" - BRANCH_ENTERED = "branch_entered" - BRANCH_CONVERGED = "branch_converged" - WORKFLOW_COMPLETED = "workflow_completed" - WORKFLOW_FAILED = "workflow_failed" - WORKFLOW_CANCELLED = "workflow_cancelled" - WORKFLOW_TIMED_OUT = "workflow_timed_out" - COMPENSATION_REGISTERED = "compensation_registered" - COMPENSATION_STARTED = "compensation_started" - COMPENSATION_WORKFLOW_STARTED = "compensation_workflow_started" - COMPENSATION_WORKFLOW_COMPLETED = "compensation_workflow_completed" - COMPENSATION_WORKFLOW_FAILED = "compensation_workflow_failed" - COMPENSATION_COMPLETED = "compensation_completed" - COMPENSATION_FAILED = "compensation_failed" - - -@dataclass(frozen=True) -class WorkflowResultContract: - """Compiled contract for validating portable workflow result envelopes.""" - - state_schema_version: str = WORKFLOW_STATE_SCHEMA_VERSION - observable_steps: dict[str, WorkflowStepSemanticContract] = field(default_factory=dict) - - def __post_init__(self) -> None: - if not isinstance(self.state_schema_version, str) or not self.state_schema_version: - raise TypeError("workflow result contract state_schema_version must be a non-empty string") - if not isinstance(self.observable_steps, dict): - raise TypeError("workflow result contract observable_steps must be a dict") - if any(not isinstance(step_name, str) for step_name in self.observable_steps): - raise TypeError("workflow result contract step names must be strings") - if any(not isinstance(contract, WorkflowStepSemanticContract) for contract in self.observable_steps.values()): - raise TypeError("workflow result contract step contracts must be WorkflowStepSemanticContract values") - - @classmethod - def from_mapping( - cls, - payload: Mapping[str, Any], - ) -> WorkflowResultContract: - if not isinstance(payload, Mapping): - raise TypeError("workflow result contract must be a mapping") - observable_steps_payload = payload.get("observable_steps", {}) - if not isinstance(observable_steps_payload, Mapping): - raise TypeError("workflow result contract observable_steps must be a mapping") - observable_steps: dict[str, WorkflowStepSemanticContract] = {} - for step_name, step_payload in observable_steps_payload.items(): - if not isinstance(step_name, str): - raise TypeError("workflow result contract step names must be strings") - if not isinstance(step_payload, Mapping): - raise TypeError("workflow result contract step payloads must be mappings") - observable_steps[step_name] = WorkflowStepSemanticContract.from_mapping(step_payload) - if not observable_steps[step_name].state_observable: - raise ValueError("workflow result contract may only include observable steps") - return cls( - state_schema_version=str(payload.get("state_schema_version", WORKFLOW_STATE_SCHEMA_VERSION)), - observable_steps=observable_steps, - ) - - -def validate_workflow_step_result_contract( - contract: WorkflowStepSemanticContract, - *, - lifecycle: str, - outcome: str | None, - attempts: int, -) -> tuple[str, ...]: - """Validate a backend-reported workflow step result against a compiled contract.""" - - return _validate_workflow_step_result( - contract, - lifecycle=lifecycle, - outcome=outcome, - attempts=attempts, - ) - - -@dataclass(frozen=True) -class WorkflowExecutionContract: - """Compiled contract for validating workflow-level execution state/history.""" - - state_schema_version: str = WORKFLOW_STATE_SCHEMA_VERSION - start_step: str = "" - timeout_seconds: int | None = None - steps: dict[str, WorkflowStepSemanticContract] = field(default_factory=dict) - step_types: dict[str, str] = field(default_factory=dict) - control_edges: dict[str, tuple[str, ...]] = field(default_factory=dict) - join_owners: dict[str, str] = field(default_factory=dict) - call_steps: dict[str, str] = field(default_factory=dict) - compensation_mode: str = "disabled" - compensation_triggers: tuple[str, ...] = () - compensation_targets: dict[str, str] = field(default_factory=dict) - compensation_ordering: str = "reverse_completion" - compensation_failure_policy: str = "fail_workflow" - observable_steps: tuple[str, ...] = () - - def __post_init__(self) -> None: - _validate_workflow_execution_contract_identity(self) - _validate_workflow_execution_contract_steps(self) - _validate_workflow_execution_contract_compensation(self) - - @classmethod - def from_mapping( - cls, - payload: Mapping[str, Any], - ) -> WorkflowExecutionContract: - if not isinstance(payload, Mapping): - raise TypeError("workflow execution contract must be a mapping") - return cls( - state_schema_version=str(payload.get("state_schema_version", WORKFLOW_STATE_SCHEMA_VERSION)), - start_step=str(payload.get("start_step", "")), - timeout_seconds=(int(payload["timeout_seconds"]) if payload.get("timeout_seconds") is not None else None), - steps=_workflow_contract_steps(payload.get("steps", {})), - step_types=_workflow_contract_string_mapping( - payload.get("step_types", {}), - "workflow execution contract step_types", - ), - control_edges=_workflow_contract_edges(payload.get("control_edges", {})), - join_owners=_workflow_contract_string_mapping( - payload.get("join_owners", {}), - "workflow execution contract join_owners", - ), - call_steps=_workflow_contract_string_mapping( - payload.get("call_steps", {}), - "workflow execution contract call_steps", - ), - compensation_mode=str(payload.get("compensation_mode", "disabled")), - compensation_triggers=tuple(str(trigger) for trigger in payload.get("compensation_triggers", ())), - compensation_targets=_workflow_contract_string_mapping( - payload.get("compensation_targets", {}), - "workflow execution contract compensation_targets", - ), - compensation_ordering=str(payload.get("compensation_ordering", "reverse_completion")), - compensation_failure_policy=str(payload.get("compensation_failure_policy", "fail_workflow")), - observable_steps=tuple(str(step_name) for step_name in payload.get("observable_steps", ())), - ) - - -def _workflow_contract_steps(raw: object) -> dict[str, WorkflowStepSemanticContract]: - if not isinstance(raw, Mapping): - raise TypeError("workflow execution contract steps must be a mapping") - steps: dict[str, WorkflowStepSemanticContract] = {} - for step_name, step_payload in raw.items(): - if not isinstance(step_name, str): - raise TypeError("workflow execution contract step names must be strings") - if not isinstance(step_payload, Mapping): - raise TypeError("workflow execution contract step payloads must be mappings") - steps[step_name] = WorkflowStepSemanticContract.from_mapping(step_payload) - return steps - - -def _workflow_contract_edges(raw: object) -> dict[str, tuple[str, ...]]: - if not isinstance(raw, Mapping): - raise TypeError("workflow execution contract control_edges must be a mapping") - return { - str(step_name): tuple(str(successor) for successor in successors) - for step_name, successors in raw.items() - if isinstance(successors, Iterable) - } - - -def _workflow_contract_string_mapping(raw: object, field_name: str) -> dict[str, str]: - if not isinstance(raw, Mapping): - raise TypeError(f"{field_name} must be a mapping") - return {str(key): str(value) for key, value in raw.items()} - - -def _validate_workflow_execution_contract_identity(contract: WorkflowExecutionContract) -> None: - require_non_empty_string( - contract.state_schema_version, - "workflow execution contract state_schema_version", - ) - require_string(contract.start_step, "workflow execution contract start_step") - if contract.timeout_seconds is None: - return - if isinstance(contract.timeout_seconds, bool) or not isinstance(contract.timeout_seconds, int): - raise TypeError("workflow execution contract timeout_seconds must be an int or None") - if contract.timeout_seconds <= 0: - raise ValueError("workflow execution contract timeout_seconds must be > 0") - - -def _validate_workflow_execution_contract_steps(contract: WorkflowExecutionContract) -> None: - require_dict(contract.steps, "workflow execution contract steps") - require_strings(contract.steps, "workflow execution contract step names") - if any(not isinstance(step_contract, WorkflowStepSemanticContract) for step_contract in contract.steps.values()): - raise TypeError("workflow execution contract step contracts must be WorkflowStepSemanticContract values") - require_dict(contract.step_types, "workflow execution contract step_types") - if any(not isinstance(name, str) or not isinstance(kind, str) for name, kind in contract.step_types.items()): - raise TypeError("workflow execution contract step_types must map strings to strings") - require_dict(contract.control_edges, "workflow execution contract control_edges") - require_dict(contract.join_owners, "workflow execution contract join_owners") - _require_string_mapping(contract.call_steps, "workflow execution contract call_steps") - require_strings(contract.observable_steps, "workflow execution contract observable_steps") - - -def _validate_workflow_execution_contract_compensation(contract: WorkflowExecutionContract) -> None: - require_string(contract.compensation_mode, "workflow execution contract compensation_mode") - require_strings(contract.compensation_triggers, "workflow execution contract compensation_triggers") - _require_string_mapping(contract.compensation_targets, "workflow execution contract compensation_targets") - require_string(contract.compensation_ordering, "workflow execution contract compensation_ordering") - require_string(contract.compensation_failure_policy, "workflow execution contract compensation_failure_policy") - - -def _require_string_mapping(value: object, field_name: str) -> None: - require_dict(value, field_name) - if any(not isinstance(key, str) or not isinstance(item, str) for key, item in value.items()): - raise TypeError(f"{field_name} must map strings to strings") - - -@dataclass(frozen=True) -class WorkflowHistoryEvent: - """Internal normalized workflow history event.""" - - event_type: WorkflowHistoryEventType - timestamp: str - step_name: str | None = None - branch_name: str | None = None - join_step: str | None = None - outcome: WorkflowStepOutcome | None = None - details: dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_payload( - cls, - payload: Mapping[str, Any], - ) -> WorkflowHistoryEvent: - if not isinstance(payload, Mapping): - raise TypeError("workflow history event must be a mapping") - event_type_raw = payload.get("event_type") - timestamp_raw = payload.get("timestamp") - if event_type_raw is None or timestamp_raw is None: - raise ValueError("workflow history event is missing required fields: event_type, timestamp") - outcome_raw = payload.get("outcome") - return cls( - event_type=( - event_type_raw - if isinstance(event_type_raw, WorkflowHistoryEventType) - else WorkflowHistoryEventType(str(event_type_raw)) - ), - timestamp=str(timestamp_raw), - step_name=(str(payload["step_name"]) if payload.get("step_name") is not None else None), - branch_name=(str(payload["branch_name"]) if payload.get("branch_name") is not None else None), - join_step=(str(payload["join_step"]) if payload.get("join_step") is not None else None), - outcome=optional_enum_value(WorkflowStepOutcome, outcome_raw), - details=dict(payload.get("details", {})) if isinstance(payload.get("details", {}), Mapping) else {}, - ) - - def to_payload(self) -> dict[str, Any]: - return { - "event_type": self.event_type.value, - "timestamp": self.timestamp, - "step_name": self.step_name, - "branch_name": self.branch_name, - "join_step": self.join_step, - "outcome": self.outcome.value if self.outcome is not None else None, - "details": dict(self.details), - } - - -@dataclass(frozen=True) -class WorkflowStepExecutionState: - """Internal normalized execution state for one workflow-visible step.""" - - lifecycle: WorkflowStepLifecycle = WorkflowStepLifecycle.PENDING - outcome: WorkflowStepOutcome | None = None - attempts: int = 0 - attempt_provenance: tuple[WorkflowStepAttemptProvenance, ...] = () - - @classmethod - def from_payload( - cls, - payload: Mapping[str, Any], - ) -> WorkflowStepExecutionState: - if not isinstance(payload, Mapping): - raise TypeError("workflow step result must be a mapping") - missing_keys = [key for key in ("lifecycle", "outcome", "attempts") if key not in payload] - if missing_keys: - raise ValueError("workflow step result is missing required fields: " + ", ".join(missing_keys)) - lifecycle_raw = payload.get("lifecycle") - outcome_raw = payload.get("outcome") - attempts_raw = payload.get("attempts") - lifecycle = ( - lifecycle_raw - if isinstance(lifecycle_raw, WorkflowStepLifecycle) - else WorkflowStepLifecycle(str(lifecycle_raw)) - ) - outcome = None - if outcome_raw is not None: - outcome = ( - outcome_raw if isinstance(outcome_raw, WorkflowStepOutcome) else WorkflowStepOutcome(str(outcome_raw)) - ) - if isinstance(attempts_raw, bool) or not isinstance(attempts_raw, int): - raise TypeError("workflow step attempts must be an int") - attempt_provenance_raw = payload.get("attempt_provenance", ()) - if isinstance(attempt_provenance_raw, (str, bytes, Mapping)) or not isinstance( - attempt_provenance_raw, Iterable - ): - raise TypeError("workflow step attempt_provenance must be a list") - return cls( - lifecycle=lifecycle, - outcome=outcome, - attempts=attempts_raw, - attempt_provenance=tuple( - WorkflowStepAttemptProvenance.from_payload(item) for item in attempt_provenance_raw - ), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "lifecycle": self.lifecycle.value, - "outcome": self.outcome.value if self.outcome is not None else None, - "attempts": self.attempts, - "attempt_provenance": [item.to_payload() for item in self.attempt_provenance], - } - - def __post_init__(self) -> None: - _validate_workflow_step_state_types(self) - _validate_workflow_step_state_progress(self) - - -@dataclass(frozen=True) -class WorkflowExecutionState: - """Internal normalized workflow result envelope.""" - - state_schema_version: str = WORKFLOW_STATE_SCHEMA_VERSION - workflow_status: WorkflowStatus = WorkflowStatus.PENDING - run_id: str = "" - started_at: str = "" - updated_at: str = "" - terminal_reason: str | None = None - compensation_status: WorkflowCompensationStatus = WorkflowCompensationStatus.NOT_REQUIRED - compensation_started_at: str | None = None - compensation_updated_at: str | None = None - compensation_failures: list[dict[str, Any]] = field(default_factory=list) - steps: dict[str, WorkflowStepExecutionState] = field(default_factory=dict) - - @classmethod - def from_payload( - cls, - payload: Mapping[str, Any], - ) -> WorkflowExecutionState: - if not isinstance(payload, Mapping): - raise TypeError("workflow result payload must be a mapping") - missing_keys = [ - key - for key in ( - "state_schema_version", - "workflow_status", - "run_id", - "started_at", - "updated_at", - "compensation_status", - "compensation_failures", - "steps", - ) - if key not in payload - ] - if missing_keys: - raise ValueError("workflow result payload is missing required fields: " + ", ".join(missing_keys)) - state_schema_version = str(payload.get("state_schema_version")) - workflow_status_raw = payload.get("workflow_status") - return cls( - state_schema_version=state_schema_version, - workflow_status=(enum_value(WorkflowStatus, workflow_status_raw)), - run_id=str(payload.get("run_id")), - started_at=str(payload.get("started_at")), - updated_at=str(payload.get("updated_at")), - terminal_reason=(str(payload["terminal_reason"]) if payload.get("terminal_reason") is not None else None), - compensation_status=(enum_value(WorkflowCompensationStatus, payload.get("compensation_status"))), - compensation_started_at=( - str(payload["compensation_started_at"]) if payload.get("compensation_started_at") is not None else None - ), - compensation_updated_at=( - str(payload["compensation_updated_at"]) if payload.get("compensation_updated_at") is not None else None - ), - compensation_failures=[ - dict(item) for item in payload.get("compensation_failures", []) if isinstance(item, Mapping) - ], - steps=_workflow_steps_from_payload(payload.get("steps")), - ) - - def to_payload(self) -> dict[str, Any]: - return { - "state_schema_version": self.state_schema_version, - "workflow_status": self.workflow_status.value, - "run_id": self.run_id, - "started_at": self.started_at, - "updated_at": self.updated_at, - "terminal_reason": self.terminal_reason, - "compensation_status": self.compensation_status.value, - "compensation_started_at": self.compensation_started_at, - "compensation_updated_at": self.compensation_updated_at, - "compensation_failures": [dict(item) for item in self.compensation_failures], - "steps": {step_name: step_state.to_payload() for step_name, step_state in self.steps.items()}, - } - - def __post_init__(self) -> None: - _validate_workflow_execution_state_types(self) - _validate_workflow_execution_state_terminal_status(self) - _validate_workflow_execution_state_compensation(self) - - -_TERMINAL_WORKFLOW_STATUSES = { - WorkflowStatus.SUCCEEDED, - WorkflowStatus.FAILED, - WorkflowStatus.CANCELLED, - WorkflowStatus.TIMED_OUT, -} - -_NON_TERMINAL_WORKFLOW_STATUSES = { - WorkflowStatus.PENDING, - WorkflowStatus.RUNNING, -} - - -def _validate_workflow_step_state_types(state: WorkflowStepExecutionState) -> None: - if not isinstance(state.lifecycle, WorkflowStepLifecycle): - raise TypeError("lifecycle must be a WorkflowStepLifecycle") - if state.outcome is not None and not isinstance(state.outcome, WorkflowStepOutcome): - raise TypeError("outcome must be a WorkflowStepOutcome or None") - if isinstance(state.attempts, bool) or not isinstance(state.attempts, int): - raise TypeError("attempts must be an int") - if state.attempts < 0: - raise ValueError("attempts must be >= 0") - if not isinstance(state.attempt_provenance, tuple) or any( - not isinstance(item, WorkflowStepAttemptProvenance) for item in state.attempt_provenance - ): - raise TypeError("attempt_provenance must be a tuple of WorkflowStepAttemptProvenance values") - if len(state.attempt_provenance) > state.attempts: - raise ValueError("attempt_provenance cannot contain more records than attempts") - attempt_ids = [item.attempt_id for item in state.attempt_provenance] - if len(attempt_ids) != len(set(attempt_ids)): - raise ValueError("attempt_provenance attempt ids must be unique") - - -def _validate_workflow_step_state_progress(state: WorkflowStepExecutionState) -> None: - if state.lifecycle != WorkflowStepLifecycle.COMPLETED and state.outcome is not None: - raise ValueError("non-completed workflow steps may not report an outcome") - if state.lifecycle == WorkflowStepLifecycle.PENDING and state.attempts != 0: - raise ValueError("pending workflow steps must report 0 attempts") - - -def _workflow_steps_from_payload(raw: object) -> dict[str, WorkflowStepExecutionState]: - if not isinstance(raw, Mapping): - raise TypeError("workflow result steps must be a mapping") - steps: dict[str, WorkflowStepExecutionState] = {} - for step_name, step_payload in raw.items(): - if not isinstance(step_name, str): - raise TypeError("workflow result step names must be strings") - if not isinstance(step_payload, Mapping): - raise TypeError("workflow result step payloads must be mappings") - steps[step_name] = WorkflowStepExecutionState.from_payload(step_payload) - return steps - - -def _validate_workflow_execution_state_types(state: WorkflowExecutionState) -> None: - require_non_empty_string(state.state_schema_version, "workflow result state_schema_version") - if not isinstance(state.workflow_status, WorkflowStatus): - raise TypeError("workflow_status must be a WorkflowStatus") - require_non_empty_string(state.run_id, "run_id") - require_non_empty_string(state.started_at, "started_at") - require_non_empty_string(state.updated_at, "updated_at") - require_optional_string(state.terminal_reason, "terminal_reason") - if not isinstance(state.compensation_status, WorkflowCompensationStatus): - raise TypeError("compensation_status must be a WorkflowCompensationStatus") - require_optional_string(state.compensation_started_at, "compensation_started_at") - require_optional_string(state.compensation_updated_at, "compensation_updated_at") - require_list(state.compensation_failures, "compensation_failures") - if any(not isinstance(item, dict) for item in state.compensation_failures): - raise TypeError("compensation_failures entries must be dicts") - require_dict(state.steps, "workflow step results") - require_strings(state.steps, "workflow step result keys") - if any(not isinstance(step_state, WorkflowStepExecutionState) for step_state in state.steps.values()): - raise TypeError("workflow step results must be WorkflowStepExecutionState values") - - -def _validate_workflow_execution_state_terminal_status(state: WorkflowExecutionState) -> None: - if state.workflow_status in _TERMINAL_WORKFLOW_STATUSES and state.terminal_reason is None: - raise ValueError("terminal workflow statuses must include terminal_reason") - if state.workflow_status in _NON_TERMINAL_WORKFLOW_STATUSES and state.terminal_reason is not None: - raise ValueError("non-terminal workflow statuses may not include terminal_reason") - - -def _validate_workflow_execution_state_compensation(state: WorkflowExecutionState) -> None: - if state.workflow_status in _NON_TERMINAL_WORKFLOW_STATUSES: - _validate_non_terminal_workflow_compensation(state) - if state.compensation_status == WorkflowCompensationStatus.NOT_REQUIRED: - _validate_absent_workflow_compensation(state) - if state.compensation_status == WorkflowCompensationStatus.RUNNING and state.compensation_started_at is None: - raise ValueError("compensation_status=running requires compensation_started_at") - - -def _validate_non_terminal_workflow_compensation(state: WorkflowExecutionState) -> None: - if state.compensation_status != WorkflowCompensationStatus.NOT_REQUIRED: - raise ValueError("non-terminal workflow statuses may not report compensation activity") - if state.compensation_started_at is not None or state.compensation_updated_at is not None: - raise ValueError("non-terminal workflow statuses may not report compensation timestamps") - - -def _validate_absent_workflow_compensation(state: WorkflowExecutionState) -> None: - if state.compensation_started_at is not None or state.compensation_updated_at is not None: - raise ValueError("compensation_status=not_required may not report compensation timestamps") - if state.compensation_failures: - raise ValueError("compensation_status=not_required may not report compensation failures") - - -@dataclass(frozen=True) -class WorkflowCancellationRequest: - """Portable request for cancelling one workflow run.""" - - workflow_address: str - run_id: str | None = None - reason: str = "cancelled by operator" - - -__all__ = ( - "WorkflowCancellationRequest", - "WorkflowCompensationStatus", - "WorkflowExecutionContract", - "WorkflowExecutionState", - "WorkflowHistoryEvent", - "WorkflowHistoryEventType", - "WorkflowResultContract", - "WorkflowStatus", - "WorkflowStepExecutionState", - "WorkflowStepAttemptProvenance", - "WorkflowStepLifecycle", - "WorkflowStepOutcome", - "validate_workflow_step_result_contract", -) diff --git a/implementations/python/packages/raes_contracts/workflow/__init__.py b/implementations/python/packages/raes_contracts/workflow/__init__.py new file mode 100644 index 000000000..1dbf31edd --- /dev/null +++ b/implementations/python/packages/raes_contracts/workflow/__init__.py @@ -0,0 +1,39 @@ +"""Shared workflow runtime result contracts.""" + +from __future__ import annotations + +from .contracts import ( + WorkflowExecutionContract, + WorkflowResultContract, + validate_workflow_step_result_contract, +) +from .enums import ( + WorkflowCompensationStatus, + WorkflowHistoryEventType, + WorkflowStatus, + WorkflowStepLifecycle, + WorkflowStepOutcome, +) +from .provenance import WorkflowStepAttemptProvenance +from .state import ( + WorkflowCancellationRequest, + WorkflowExecutionState, + WorkflowHistoryEvent, + WorkflowStepExecutionState, +) + +__all__ = ( + "WorkflowCancellationRequest", + "WorkflowCompensationStatus", + "WorkflowExecutionContract", + "WorkflowExecutionState", + "WorkflowHistoryEvent", + "WorkflowHistoryEventType", + "WorkflowResultContract", + "WorkflowStatus", + "WorkflowStepExecutionState", + "WorkflowStepAttemptProvenance", + "WorkflowStepLifecycle", + "WorkflowStepOutcome", + "validate_workflow_step_result_contract", +) diff --git a/implementations/python/packages/raes_contracts/workflow/contracts.py b/implementations/python/packages/raes_contracts/workflow/contracts.py new file mode 100644 index 000000000..e49933c9c --- /dev/null +++ b/implementations/python/packages/raes_contracts/workflow/contracts.py @@ -0,0 +1,209 @@ +"""Compiled workflow result/execution contracts and their validation.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, field +from typing import Any + +from raes.semantics.workflow import WorkflowStepSemanticContract +from raes.semantics.workflow import validate_workflow_step_result as _validate_workflow_step_result + +from raes_contracts._validation import ( + require_dict, + require_non_empty_string, + require_string, + require_strings, +) +from raes_contracts.versions import WORKFLOW_STATE_SCHEMA_VERSION + + +@dataclass(frozen=True) +class WorkflowResultContract: + """Compiled contract for validating portable workflow result envelopes.""" + + state_schema_version: str = WORKFLOW_STATE_SCHEMA_VERSION + observable_steps: dict[str, WorkflowStepSemanticContract] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not isinstance(self.state_schema_version, str) or not self.state_schema_version: + raise TypeError("workflow result contract state_schema_version must be a non-empty string") + if not isinstance(self.observable_steps, dict): + raise TypeError("workflow result contract observable_steps must be a dict") + if any(not isinstance(step_name, str) for step_name in self.observable_steps): + raise TypeError("workflow result contract step names must be strings") + if any(not isinstance(contract, WorkflowStepSemanticContract) for contract in self.observable_steps.values()): + raise TypeError("workflow result contract step contracts must be WorkflowStepSemanticContract values") + + @classmethod + def from_mapping( + cls, + payload: Mapping[str, Any], + ) -> WorkflowResultContract: + if not isinstance(payload, Mapping): + raise TypeError("workflow result contract must be a mapping") + observable_steps_payload = payload.get("observable_steps", {}) + if not isinstance(observable_steps_payload, Mapping): + raise TypeError("workflow result contract observable_steps must be a mapping") + observable_steps: dict[str, WorkflowStepSemanticContract] = {} + for step_name, step_payload in observable_steps_payload.items(): + if not isinstance(step_name, str): + raise TypeError("workflow result contract step names must be strings") + if not isinstance(step_payload, Mapping): + raise TypeError("workflow result contract step payloads must be mappings") + observable_steps[step_name] = WorkflowStepSemanticContract.from_mapping(step_payload) + if not observable_steps[step_name].state_observable: + raise ValueError("workflow result contract may only include observable steps") + return cls( + state_schema_version=str(payload.get("state_schema_version", WORKFLOW_STATE_SCHEMA_VERSION)), + observable_steps=observable_steps, + ) + + +def validate_workflow_step_result_contract( + contract: WorkflowStepSemanticContract, + *, + lifecycle: str, + outcome: str | None, + attempts: int, +) -> tuple[str, ...]: + """Validate a backend-reported workflow step result against a compiled contract.""" + + return _validate_workflow_step_result( + contract, + lifecycle=lifecycle, + outcome=outcome, + attempts=attempts, + ) + + +@dataclass(frozen=True) +class WorkflowExecutionContract: + """Compiled contract for validating workflow-level execution state/history.""" + + state_schema_version: str = WORKFLOW_STATE_SCHEMA_VERSION + start_step: str = "" + timeout_seconds: int | None = None + steps: dict[str, WorkflowStepSemanticContract] = field(default_factory=dict) + step_types: dict[str, str] = field(default_factory=dict) + control_edges: dict[str, tuple[str, ...]] = field(default_factory=dict) + join_owners: dict[str, str] = field(default_factory=dict) + call_steps: dict[str, str] = field(default_factory=dict) + compensation_mode: str = "disabled" + compensation_triggers: tuple[str, ...] = () + compensation_targets: dict[str, str] = field(default_factory=dict) + compensation_ordering: str = "reverse_completion" + compensation_failure_policy: str = "fail_workflow" + observable_steps: tuple[str, ...] = () + + def __post_init__(self) -> None: + _validate_workflow_execution_contract_identity(self) + _validate_workflow_execution_contract_steps(self) + _validate_workflow_execution_contract_compensation(self) + + @classmethod + def from_mapping( + cls, + payload: Mapping[str, Any], + ) -> WorkflowExecutionContract: + if not isinstance(payload, Mapping): + raise TypeError("workflow execution contract must be a mapping") + return cls( + state_schema_version=str(payload.get("state_schema_version", WORKFLOW_STATE_SCHEMA_VERSION)), + start_step=str(payload.get("start_step", "")), + timeout_seconds=(int(payload["timeout_seconds"]) if payload.get("timeout_seconds") is not None else None), + steps=_workflow_contract_steps(payload.get("steps", {})), + step_types=_workflow_contract_string_mapping( + payload.get("step_types", {}), + "workflow execution contract step_types", + ), + control_edges=_workflow_contract_edges(payload.get("control_edges", {})), + join_owners=_workflow_contract_string_mapping( + payload.get("join_owners", {}), + "workflow execution contract join_owners", + ), + call_steps=_workflow_contract_string_mapping( + payload.get("call_steps", {}), + "workflow execution contract call_steps", + ), + compensation_mode=str(payload.get("compensation_mode", "disabled")), + compensation_triggers=tuple(str(trigger) for trigger in payload.get("compensation_triggers", ())), + compensation_targets=_workflow_contract_string_mapping( + payload.get("compensation_targets", {}), + "workflow execution contract compensation_targets", + ), + compensation_ordering=str(payload.get("compensation_ordering", "reverse_completion")), + compensation_failure_policy=str(payload.get("compensation_failure_policy", "fail_workflow")), + observable_steps=tuple(str(step_name) for step_name in payload.get("observable_steps", ())), + ) + + +def _workflow_contract_steps(raw: object) -> dict[str, WorkflowStepSemanticContract]: + if not isinstance(raw, Mapping): + raise TypeError("workflow execution contract steps must be a mapping") + steps: dict[str, WorkflowStepSemanticContract] = {} + for step_name, step_payload in raw.items(): + if not isinstance(step_name, str): + raise TypeError("workflow execution contract step names must be strings") + if not isinstance(step_payload, Mapping): + raise TypeError("workflow execution contract step payloads must be mappings") + steps[step_name] = WorkflowStepSemanticContract.from_mapping(step_payload) + return steps + + +def _workflow_contract_edges(raw: object) -> dict[str, tuple[str, ...]]: + if not isinstance(raw, Mapping): + raise TypeError("workflow execution contract control_edges must be a mapping") + return { + str(step_name): tuple(str(successor) for successor in successors) + for step_name, successors in raw.items() + if isinstance(successors, Iterable) + } + + +def _workflow_contract_string_mapping(raw: object, field_name: str) -> dict[str, str]: + if not isinstance(raw, Mapping): + raise TypeError(f"{field_name} must be a mapping") + return {str(key): str(value) for key, value in raw.items()} + + +def _validate_workflow_execution_contract_identity(contract: WorkflowExecutionContract) -> None: + require_non_empty_string( + contract.state_schema_version, + "workflow execution contract state_schema_version", + ) + require_string(contract.start_step, "workflow execution contract start_step") + if contract.timeout_seconds is None: + return + if isinstance(contract.timeout_seconds, bool) or not isinstance(contract.timeout_seconds, int): + raise TypeError("workflow execution contract timeout_seconds must be an int or None") + if contract.timeout_seconds <= 0: + raise ValueError("workflow execution contract timeout_seconds must be > 0") + + +def _validate_workflow_execution_contract_steps(contract: WorkflowExecutionContract) -> None: + require_dict(contract.steps, "workflow execution contract steps") + require_strings(contract.steps, "workflow execution contract step names") + if any(not isinstance(step_contract, WorkflowStepSemanticContract) for step_contract in contract.steps.values()): + raise TypeError("workflow execution contract step contracts must be WorkflowStepSemanticContract values") + require_dict(contract.step_types, "workflow execution contract step_types") + if any(not isinstance(name, str) or not isinstance(kind, str) for name, kind in contract.step_types.items()): + raise TypeError("workflow execution contract step_types must map strings to strings") + require_dict(contract.control_edges, "workflow execution contract control_edges") + require_dict(contract.join_owners, "workflow execution contract join_owners") + _require_string_mapping(contract.call_steps, "workflow execution contract call_steps") + require_strings(contract.observable_steps, "workflow execution contract observable_steps") + + +def _validate_workflow_execution_contract_compensation(contract: WorkflowExecutionContract) -> None: + require_string(contract.compensation_mode, "workflow execution contract compensation_mode") + require_strings(contract.compensation_triggers, "workflow execution contract compensation_triggers") + _require_string_mapping(contract.compensation_targets, "workflow execution contract compensation_targets") + require_string(contract.compensation_ordering, "workflow execution contract compensation_ordering") + require_string(contract.compensation_failure_policy, "workflow execution contract compensation_failure_policy") + + +def _require_string_mapping(value: object, field_name: str) -> None: + require_dict(value, field_name) + if any(not isinstance(key, str) or not isinstance(item, str) for key, item in value.items()): + raise TypeError(f"{field_name} must map strings to strings") diff --git a/implementations/python/packages/raes_contracts/workflow/enums.py b/implementations/python/packages/raes_contracts/workflow/enums.py new file mode 100644 index 000000000..f005ace6f --- /dev/null +++ b/implementations/python/packages/raes_contracts/workflow/enums.py @@ -0,0 +1,66 @@ +"""Portable workflow execution enumerations.""" + +from __future__ import annotations + +from enum import Enum + + +class WorkflowStepLifecycle(str, Enum): + """Portable execution lifecycle for workflow-visible step state.""" + + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + + +class WorkflowStepOutcome(str, Enum): + """Portable execution outcomes for workflow-visible step state.""" + + SUCCEEDED = "succeeded" + FAILED = "failed" + EXHAUSTED = "exhausted" + + +class WorkflowStatus(str, Enum): + """Portable workflow-level execution status.""" + + PENDING = "pending" + RUNNING = "running" + SUCCEEDED = "succeeded" + FAILED = "failed" + CANCELLED = "cancelled" + TIMED_OUT = "timed_out" + + +class WorkflowCompensationStatus(str, Enum): + """Portable workflow compensation status.""" + + NOT_REQUIRED = "not_required" + PENDING = "pending" + RUNNING = "running" + SUCCEEDED = "succeeded" + FAILED = "failed" + + +class WorkflowHistoryEventType(str, Enum): + """Portable workflow history event kinds.""" + + WORKFLOW_STARTED = "workflow_started" + STEP_STARTED = "step_started" + STEP_COMPLETED = "step_completed" + SWITCH_CASE_SELECTED = "switch_case_selected" + CALL_STARTED = "call_started" + CALL_COMPLETED = "call_completed" + BRANCH_ENTERED = "branch_entered" + BRANCH_CONVERGED = "branch_converged" + WORKFLOW_COMPLETED = "workflow_completed" + WORKFLOW_FAILED = "workflow_failed" + WORKFLOW_CANCELLED = "workflow_cancelled" + WORKFLOW_TIMED_OUT = "workflow_timed_out" + COMPENSATION_REGISTERED = "compensation_registered" + COMPENSATION_STARTED = "compensation_started" + COMPENSATION_WORKFLOW_STARTED = "compensation_workflow_started" + COMPENSATION_WORKFLOW_COMPLETED = "compensation_workflow_completed" + COMPENSATION_WORKFLOW_FAILED = "compensation_workflow_failed" + COMPENSATION_COMPLETED = "compensation_completed" + COMPENSATION_FAILED = "compensation_failed" diff --git a/implementations/python/packages/raes_contracts/workflow/provenance.py b/implementations/python/packages/raes_contracts/workflow/provenance.py new file mode 100644 index 000000000..9dc111a0a --- /dev/null +++ b/implementations/python/packages/raes_contracts/workflow/provenance.py @@ -0,0 +1,97 @@ +"""Portable provenance for governed workflow-step realization attempts.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + + +def _validate_provenance_string_tuple(field_name: str, values: object) -> None: + if not isinstance(values, tuple) or any(not isinstance(item, str) or not item for item in values): + raise TypeError(f"{field_name} must be a tuple of non-empty strings") + if len(values) != len(set(values)): + raise ValueError(f"{field_name} entries must be unique") + + +@dataclass(frozen=True) +class WorkflowStepAttemptProvenance: + """Portable provenance for one governed workflow-step realization attempt.""" + + step_name: str + execution_mode: str + attempt_id: str + objective_address: str = "" + procedure_ref: str = "" + exposed_scaffold_refs: tuple[str, ...] = () + allowed_action_families: tuple[str, ...] = () + selected_action_family: str = "" + selected_tool_ref: str = "" + selected_affordance_ref: str = "" + fact_versions: tuple[str, ...] = () + outcome: str = "" + evidence_refs: tuple[str, ...] = () + assertion_truth_refs: tuple[str, ...] = () + participant_report: str = "" + + def __post_init__(self) -> None: + for field_name in ("step_name", "execution_mode", "attempt_id"): + value = getattr(self, field_name) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field_name} must be a non-empty string") + if self.execution_mode not in {"scripted", "objective", "scaffolded"}: + raise ValueError("execution_mode must be scripted, objective, or scaffolded") + if self.outcome not in {"", "succeeded", "failed", "exhausted"}: + raise ValueError("outcome must be a portable workflow step outcome") + for field_name in ( + "exposed_scaffold_refs", + "allowed_action_families", + "fact_versions", + "evidence_refs", + "assertion_truth_refs", + ): + values = getattr(self, field_name) + _validate_provenance_string_tuple(field_name, values) + if self.outcome == "succeeded" and not (self.evidence_refs and self.assertion_truth_refs): + raise ValueError("successful workflow step provenance requires evidence-bearing assertion truth") + + @classmethod + def from_payload(cls, payload: Mapping[str, Any]) -> WorkflowStepAttemptProvenance: + if not isinstance(payload, Mapping): + raise TypeError("workflow step attempt provenance must be a mapping") + return cls( + step_name=str(payload.get("step_name", "")), + execution_mode=str(payload.get("execution_mode", "")), + attempt_id=str(payload.get("attempt_id", "")), + objective_address=str(payload.get("objective_address", "")), + procedure_ref=str(payload.get("procedure_ref", "")), + exposed_scaffold_refs=tuple(payload.get("exposed_scaffold_refs", ())), + allowed_action_families=tuple(payload.get("allowed_action_families", ())), + selected_action_family=str(payload.get("selected_action_family", "")), + selected_tool_ref=str(payload.get("selected_tool_ref", "")), + selected_affordance_ref=str(payload.get("selected_affordance_ref", "")), + fact_versions=tuple(payload.get("fact_versions", ())), + outcome=str(payload.get("outcome", "")), + evidence_refs=tuple(payload.get("evidence_refs", ())), + assertion_truth_refs=tuple(payload.get("assertion_truth_refs", ())), + participant_report=str(payload.get("participant_report", "")), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "step_name": self.step_name, + "execution_mode": self.execution_mode, + "attempt_id": self.attempt_id, + "objective_address": self.objective_address, + "procedure_ref": self.procedure_ref, + "exposed_scaffold_refs": list(self.exposed_scaffold_refs), + "allowed_action_families": list(self.allowed_action_families), + "selected_action_family": self.selected_action_family, + "selected_tool_ref": self.selected_tool_ref, + "selected_affordance_ref": self.selected_affordance_ref, + "fact_versions": list(self.fact_versions), + "outcome": self.outcome, + "evidence_refs": list(self.evidence_refs), + "assertion_truth_refs": list(self.assertion_truth_refs), + "participant_report": self.participant_report, + } diff --git a/implementations/python/packages/raes_contracts/workflow/state.py b/implementations/python/packages/raes_contracts/workflow/state.py new file mode 100644 index 000000000..9711d307c --- /dev/null +++ b/implementations/python/packages/raes_contracts/workflow/state.py @@ -0,0 +1,341 @@ +"""Normalized workflow history/execution-state envelopes and their validation.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, field +from typing import Any + +from raes_contracts._validation import ( + enum_value, + optional_enum_value, + require_dict, + require_list, + require_non_empty_string, + require_optional_string, + require_strings, +) +from raes_contracts.versions import WORKFLOW_STATE_SCHEMA_VERSION + +from .enums import ( + WorkflowCompensationStatus, + WorkflowHistoryEventType, + WorkflowStatus, + WorkflowStepLifecycle, + WorkflowStepOutcome, +) +from .provenance import WorkflowStepAttemptProvenance + + +@dataclass(frozen=True) +class WorkflowHistoryEvent: + """Internal normalized workflow history event.""" + + event_type: WorkflowHistoryEventType + timestamp: str + step_name: str | None = None + branch_name: str | None = None + join_step: str | None = None + outcome: WorkflowStepOutcome | None = None + details: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_payload( + cls, + payload: Mapping[str, Any], + ) -> WorkflowHistoryEvent: + if not isinstance(payload, Mapping): + raise TypeError("workflow history event must be a mapping") + event_type_raw = payload.get("event_type") + timestamp_raw = payload.get("timestamp") + if event_type_raw is None or timestamp_raw is None: + raise ValueError("workflow history event is missing required fields: event_type, timestamp") + outcome_raw = payload.get("outcome") + return cls( + event_type=( + event_type_raw + if isinstance(event_type_raw, WorkflowHistoryEventType) + else WorkflowHistoryEventType(str(event_type_raw)) + ), + timestamp=str(timestamp_raw), + step_name=(str(payload["step_name"]) if payload.get("step_name") is not None else None), + branch_name=(str(payload["branch_name"]) if payload.get("branch_name") is not None else None), + join_step=(str(payload["join_step"]) if payload.get("join_step") is not None else None), + outcome=optional_enum_value(WorkflowStepOutcome, outcome_raw), + details=dict(payload.get("details", {})) if isinstance(payload.get("details", {}), Mapping) else {}, + ) + + def to_payload(self) -> dict[str, Any]: + return { + "event_type": self.event_type.value, + "timestamp": self.timestamp, + "step_name": self.step_name, + "branch_name": self.branch_name, + "join_step": self.join_step, + "outcome": self.outcome.value if self.outcome is not None else None, + "details": dict(self.details), + } + + +def _require_step_result_keys(payload: Mapping[str, Any]) -> None: + missing_keys = [key for key in ("lifecycle", "outcome", "attempts") if key not in payload] + if missing_keys: + raise ValueError("workflow step result is missing required fields: " + ", ".join(missing_keys)) + + +def _coerce_step_lifecycle(raw: object) -> WorkflowStepLifecycle: + return raw if isinstance(raw, WorkflowStepLifecycle) else WorkflowStepLifecycle(str(raw)) + + +def _coerce_step_outcome(raw: object) -> WorkflowStepOutcome | None: + if raw is None: + return None + return raw if isinstance(raw, WorkflowStepOutcome) else WorkflowStepOutcome(str(raw)) + + +def _coerce_step_attempts(raw: object) -> int: + if isinstance(raw, bool) or not isinstance(raw, int): + raise TypeError("workflow step attempts must be an int") + return raw + + +def _coerce_step_attempt_provenance(raw: object) -> tuple[WorkflowStepAttemptProvenance, ...]: + if isinstance(raw, (str, bytes, Mapping)) or not isinstance(raw, Iterable): + raise TypeError("workflow step attempt_provenance must be a list") + return tuple(WorkflowStepAttemptProvenance.from_payload(item) for item in raw) + + +@dataclass(frozen=True) +class WorkflowStepExecutionState: + """Internal normalized execution state for one workflow-visible step.""" + + lifecycle: WorkflowStepLifecycle = WorkflowStepLifecycle.PENDING + outcome: WorkflowStepOutcome | None = None + attempts: int = 0 + attempt_provenance: tuple[WorkflowStepAttemptProvenance, ...] = () + + @classmethod + def from_payload( + cls, + payload: Mapping[str, Any], + ) -> WorkflowStepExecutionState: + if not isinstance(payload, Mapping): + raise TypeError("workflow step result must be a mapping") + _require_step_result_keys(payload) + return cls( + lifecycle=_coerce_step_lifecycle(payload.get("lifecycle")), + outcome=_coerce_step_outcome(payload.get("outcome")), + attempts=_coerce_step_attempts(payload.get("attempts")), + attempt_provenance=_coerce_step_attempt_provenance(payload.get("attempt_provenance", ())), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "lifecycle": self.lifecycle.value, + "outcome": self.outcome.value if self.outcome is not None else None, + "attempts": self.attempts, + "attempt_provenance": [item.to_payload() for item in self.attempt_provenance], + } + + def __post_init__(self) -> None: + _validate_workflow_step_state_types(self) + _validate_workflow_step_state_progress(self) + + +@dataclass(frozen=True) +class WorkflowExecutionState: + """Internal normalized workflow result envelope.""" + + state_schema_version: str = WORKFLOW_STATE_SCHEMA_VERSION + workflow_status: WorkflowStatus = WorkflowStatus.PENDING + run_id: str = "" + started_at: str = "" + updated_at: str = "" + terminal_reason: str | None = None + compensation_status: WorkflowCompensationStatus = WorkflowCompensationStatus.NOT_REQUIRED + compensation_started_at: str | None = None + compensation_updated_at: str | None = None + compensation_failures: list[dict[str, Any]] = field(default_factory=list) + steps: dict[str, WorkflowStepExecutionState] = field(default_factory=dict) + + @classmethod + def from_payload( + cls, + payload: Mapping[str, Any], + ) -> WorkflowExecutionState: + if not isinstance(payload, Mapping): + raise TypeError("workflow result payload must be a mapping") + missing_keys = [ + key + for key in ( + "state_schema_version", + "workflow_status", + "run_id", + "started_at", + "updated_at", + "compensation_status", + "compensation_failures", + "steps", + ) + if key not in payload + ] + if missing_keys: + raise ValueError("workflow result payload is missing required fields: " + ", ".join(missing_keys)) + state_schema_version = str(payload.get("state_schema_version")) + workflow_status_raw = payload.get("workflow_status") + return cls( + state_schema_version=state_schema_version, + workflow_status=(enum_value(WorkflowStatus, workflow_status_raw)), + run_id=str(payload.get("run_id")), + started_at=str(payload.get("started_at")), + updated_at=str(payload.get("updated_at")), + terminal_reason=(str(payload["terminal_reason"]) if payload.get("terminal_reason") is not None else None), + compensation_status=(enum_value(WorkflowCompensationStatus, payload.get("compensation_status"))), + compensation_started_at=( + str(payload["compensation_started_at"]) if payload.get("compensation_started_at") is not None else None + ), + compensation_updated_at=( + str(payload["compensation_updated_at"]) if payload.get("compensation_updated_at") is not None else None + ), + compensation_failures=[ + dict(item) for item in payload.get("compensation_failures", []) if isinstance(item, Mapping) + ], + steps=_workflow_steps_from_payload(payload.get("steps")), + ) + + def to_payload(self) -> dict[str, Any]: + return { + "state_schema_version": self.state_schema_version, + "workflow_status": self.workflow_status.value, + "run_id": self.run_id, + "started_at": self.started_at, + "updated_at": self.updated_at, + "terminal_reason": self.terminal_reason, + "compensation_status": self.compensation_status.value, + "compensation_started_at": self.compensation_started_at, + "compensation_updated_at": self.compensation_updated_at, + "compensation_failures": [dict(item) for item in self.compensation_failures], + "steps": {step_name: step_state.to_payload() for step_name, step_state in self.steps.items()}, + } + + def __post_init__(self) -> None: + _validate_workflow_execution_state_types(self) + _validate_workflow_execution_state_terminal_status(self) + _validate_workflow_execution_state_compensation(self) + + +_TERMINAL_WORKFLOW_STATUSES = { + WorkflowStatus.SUCCEEDED, + WorkflowStatus.FAILED, + WorkflowStatus.CANCELLED, + WorkflowStatus.TIMED_OUT, +} + +_NON_TERMINAL_WORKFLOW_STATUSES = { + WorkflowStatus.PENDING, + WorkflowStatus.RUNNING, +} + + +def _validate_workflow_step_state_types(state: WorkflowStepExecutionState) -> None: + if not isinstance(state.lifecycle, WorkflowStepLifecycle): + raise TypeError("lifecycle must be a WorkflowStepLifecycle") + if state.outcome is not None and not isinstance(state.outcome, WorkflowStepOutcome): + raise TypeError("outcome must be a WorkflowStepOutcome or None") + if isinstance(state.attempts, bool) or not isinstance(state.attempts, int): + raise TypeError("attempts must be an int") + if state.attempts < 0: + raise ValueError("attempts must be >= 0") + _validate_workflow_step_attempt_provenance(state) + + +def _validate_workflow_step_attempt_provenance(state: WorkflowStepExecutionState) -> None: + if not isinstance(state.attempt_provenance, tuple) or any( + not isinstance(item, WorkflowStepAttemptProvenance) for item in state.attempt_provenance + ): + raise TypeError("attempt_provenance must be a tuple of WorkflowStepAttemptProvenance values") + if len(state.attempt_provenance) > state.attempts: + raise ValueError("attempt_provenance cannot contain more records than attempts") + attempt_ids = [item.attempt_id for item in state.attempt_provenance] + if len(attempt_ids) != len(set(attempt_ids)): + raise ValueError("attempt_provenance attempt ids must be unique") + + +def _validate_workflow_step_state_progress(state: WorkflowStepExecutionState) -> None: + if state.lifecycle != WorkflowStepLifecycle.COMPLETED and state.outcome is not None: + raise ValueError("non-completed workflow steps may not report an outcome") + if state.lifecycle == WorkflowStepLifecycle.PENDING and state.attempts != 0: + raise ValueError("pending workflow steps must report 0 attempts") + + +def _workflow_steps_from_payload(raw: object) -> dict[str, WorkflowStepExecutionState]: + if not isinstance(raw, Mapping): + raise TypeError("workflow result steps must be a mapping") + steps: dict[str, WorkflowStepExecutionState] = {} + for step_name, step_payload in raw.items(): + if not isinstance(step_name, str): + raise TypeError("workflow result step names must be strings") + if not isinstance(step_payload, Mapping): + raise TypeError("workflow result step payloads must be mappings") + steps[step_name] = WorkflowStepExecutionState.from_payload(step_payload) + return steps + + +def _validate_workflow_execution_state_types(state: WorkflowExecutionState) -> None: + require_non_empty_string(state.state_schema_version, "workflow result state_schema_version") + if not isinstance(state.workflow_status, WorkflowStatus): + raise TypeError("workflow_status must be a WorkflowStatus") + require_non_empty_string(state.run_id, "run_id") + require_non_empty_string(state.started_at, "started_at") + require_non_empty_string(state.updated_at, "updated_at") + require_optional_string(state.terminal_reason, "terminal_reason") + if not isinstance(state.compensation_status, WorkflowCompensationStatus): + raise TypeError("compensation_status must be a WorkflowCompensationStatus") + require_optional_string(state.compensation_started_at, "compensation_started_at") + require_optional_string(state.compensation_updated_at, "compensation_updated_at") + require_list(state.compensation_failures, "compensation_failures") + if any(not isinstance(item, dict) for item in state.compensation_failures): + raise TypeError("compensation_failures entries must be dicts") + require_dict(state.steps, "workflow step results") + require_strings(state.steps, "workflow step result keys") + if any(not isinstance(step_state, WorkflowStepExecutionState) for step_state in state.steps.values()): + raise TypeError("workflow step results must be WorkflowStepExecutionState values") + + +def _validate_workflow_execution_state_terminal_status(state: WorkflowExecutionState) -> None: + if state.workflow_status in _TERMINAL_WORKFLOW_STATUSES and state.terminal_reason is None: + raise ValueError("terminal workflow statuses must include terminal_reason") + if state.workflow_status in _NON_TERMINAL_WORKFLOW_STATUSES and state.terminal_reason is not None: + raise ValueError("non-terminal workflow statuses may not include terminal_reason") + + +def _validate_workflow_execution_state_compensation(state: WorkflowExecutionState) -> None: + if state.workflow_status in _NON_TERMINAL_WORKFLOW_STATUSES: + _validate_non_terminal_workflow_compensation(state) + if state.compensation_status == WorkflowCompensationStatus.NOT_REQUIRED: + _validate_absent_workflow_compensation(state) + if state.compensation_status == WorkflowCompensationStatus.RUNNING and state.compensation_started_at is None: + raise ValueError("compensation_status=running requires compensation_started_at") + + +def _validate_non_terminal_workflow_compensation(state: WorkflowExecutionState) -> None: + if state.compensation_status != WorkflowCompensationStatus.NOT_REQUIRED: + raise ValueError("non-terminal workflow statuses may not report compensation activity") + if state.compensation_started_at is not None or state.compensation_updated_at is not None: + raise ValueError("non-terminal workflow statuses may not report compensation timestamps") + + +def _validate_absent_workflow_compensation(state: WorkflowExecutionState) -> None: + if state.compensation_started_at is not None or state.compensation_updated_at is not None: + raise ValueError("compensation_status=not_required may not report compensation timestamps") + if state.compensation_failures: + raise ValueError("compensation_status=not_required may not report compensation failures") + + +@dataclass(frozen=True) +class WorkflowCancellationRequest: + """Portable request for cancelling one workflow run.""" + + workflow_address: str + run_id: str | None = None + reason: str = "cancelled by operator" diff --git a/implementations/python/packages/raes_operations/_evidence_run_artifact.py b/implementations/python/packages/raes_operations/_evidence_run_artifact.py deleted file mode 100644 index 6e50c8f01..000000000 --- a/implementations/python/packages/raes_operations/_evidence_run_artifact.py +++ /dev/null @@ -1,611 +0,0 @@ -"""Artifact assembly for the libvirt scenario-evidence producer. - -Builds the ``raes.libvirt.scenario-evidence-run/v1`` payload from the compiled runtime -model, the backend manifest, the participant-proof result, and (optionally) the -native substrate snapshot. Section builders only read duck-typed runtime-layer -objects and copy allowlisted, bounded fields, so no raw libvirt/backend internals -reach the artifact. The backend section embeds the canonical ``BackendManifestV2`` -payload rendered by the pure ``raes_backend_protocols`` manifest/capability helpers -(ADR-036 allows ``raes_operations`` those two side-effect-free renderers) so the -evidence carries the same backend contract the rest of the stack uses, not a -hand-rolled summary. Split from ``libvirt_evidence_run`` to keep each module under -the ADR-015 source-size cap. -""" - -from __future__ import annotations - -import hashlib -import json -from collections.abc import Mapping, Sequence -from pathlib import Path -from typing import Any - -from raes_backend_libvirt.techvault_native import expected_surface -from raes_backend_protocols.capabilities import ( - observation_capability_contract_gaps, - participant_runtime_capability_contract_gaps, -) -from raes_backend_protocols.manifest import backend_manifest_payload -from raes_contracts.contracts import ( - EvaluationHistoryEventModel, - EvaluationResultStateModel, - ExperimentRealizedFormDisclosureModel, -) - -from raes_operations._evidence_run_types import ( - BackendManifest, - CompiledModel, - EvidenceArtifactInputs, - NodeDeployment, - RealizedNetwork, - TerminalSnapshot, -) -from raes_operations.run_artifacts import portable_artifact_ref - -EVIDENCE_RUN_SCHEMA = "raes.libvirt.scenario-evidence-run/v1" -_LIBVIRT_BACKEND_NAME = "libvirt-qemu" - -# Internal/evaluator-only surfaces the participant must never observe. Derived from -# the reference scenario observation boundary's hidden_refs; the negative-boundary -# evidence confirms none of these reach the participant's visible/disclosed refs. -_INTERNAL_SURFACE_KEYWORDS = ("customer-db", "wazuh", "evaluator", "policy-gate", "postgres") - -# The four scenario non-claims (issue #615). Carried verbatim in the artifact. -_NON_CLAIMS = ( - "No Wazuh detection-quality claim.", - "No model-defense robustness claim.", - "No byte-equivalence or application-internals equivalence claim between libvirt appliances and APTL containers.", - "No full semantic-equivalence claim beyond the invariant ledger in OpenRAE/rae#600.", -) - - -def assemble_artifact(inputs: EvidenceArtifactInputs) -> dict[str, Any]: - """Assemble the full scenario-evidence artifact payload.""" - scenario_path = inputs.scenario_path - run_id = inputs.run_id - recorded_at = inputs.recorded_at - mode = inputs.mode - model = inputs.model - manifest = inputs.manifest - proof = inputs.proof - native_snapshot = inputs.native_snapshot - native_cleanup_verified = inputs.native_cleanup_verified - unrealized_capabilities = inputs.unrealized_capabilities - - substrate_realized = native_snapshot is not None - scenario_section = _scenario_section(scenario_path, model) - boundary_refs = _boundary_hidden_refs(model) - - return { - "schema": EVIDENCE_RUN_SCHEMA, - "run_id": run_id, - "recorded_at": recorded_at, - "evidence_source_mode": mode, - "scenario": scenario_section, - "compiled_artifact": _compiled_artifact_section(model), - "backend": _backend_section(manifest, mode, substrate_realized, native_cleanup_verified), - "realization_facts": _realization_facts_section( - model, native_snapshot, native_cleanup_verified, inputs.guest_observed - ), - "realized_topology": _topology_section(model, native_snapshot, unrealized_capabilities), - "participant_action_proof": _participant_proof_section(proof), - "terminal_observation": _terminal_observation_section(proof["snapshot"]), - "defensive_evidence": _defensive_evidence_section(native_snapshot, model, recorded_at), - "negative_boundary_checks": _negative_boundary_section(boundary_refs), - "evaluator_outcome": _evaluator_outcome_section(proof["lifecycle_clean"], recorded_at), - "realized_form_disclosures": _realized_form_disclosures(manifest, substrate_realized), - "limitations": _limitations(mode, unrealized_capabilities), - "non_claims": list(_NON_CLAIMS), - "redaction_provenance": _redaction_provenance(), - "invariant_ledger_refs": _invariant_ledger_refs(model, scenario_section), - } - - -def _manifest_name(manifest: BackendManifest) -> str: - identity = getattr(manifest, "identity", None) - if identity is not None and getattr(identity, "name", None): - return str(identity.name) - return str(getattr(manifest, "name", _LIBVIRT_BACKEND_NAME)) - - -def _manifest_version(manifest: BackendManifest) -> str: - identity = getattr(manifest, "identity", None) - if identity is not None and getattr(identity, "version", None): - return str(identity.version) - return str(getattr(manifest, "version", "0.0.0+unknown")) - - -def _backend_section( - manifest: BackendManifest, - mode: str, - substrate_realized: bool, - cleanup_verified: bool | None, -) -> dict[str, Any]: - """Embed the canonical BackendManifestV2 payload + capability-gap report. - - The manifest is rendered through ``backend_manifest_payload`` — the same - canonical V2 renderer the rest of the stack uses — and re-validated against - ``BackendManifestV2Model`` by the artifact validator, so the evidence carries - the published backend contract rather than a hand-rolled summary. The - capability profile reports any contract gaps between the declared participant- - runtime / observation capabilities and their required contracts (empty when the - manifest fully satisfies them). - """ - return { - "manifest": backend_manifest_payload(manifest), - "capability_profile": { - "participant_runtime_contract_gaps": list(participant_runtime_capability_contract_gaps(manifest)), - "observation_contract_gaps": list(observation_capability_contract_gaps(manifest)), - }, - "realization_provenance": { - "backend": _manifest_name(manifest), - "evidence_source_mode": mode, - "substrate_realized": substrate_realized, - "basis": "daemon-observed-substrate" if substrate_realized else "planned-not-realized", - "cleanup_verified": cleanup_verified, - }, - } - - -def _scenario_section(scenario_path: Path, model: CompiledModel) -> dict[str, Any]: - from raes.parser import parse_sdl_file - - content = scenario_path.read_bytes() - version: str | None = None - try: - version = getattr(parse_sdl_file(scenario_path), "version", None) - except Exception: - version = None - return { - "name": model.scenario_name, - "version": version, - "relative_path": portable_artifact_ref(scenario_path), - "content_sha256": "sha256:" + hashlib.sha256(content).hexdigest(), - } - - -def _compiled_artifact_section(model: CompiledModel) -> dict[str, Any]: - addresses = { - "participant_behaviors": sorted(model.participant_behaviors), - "action_contracts": sorted(model.action_contracts), - "observation_boundaries": sorted(model.observation_boundaries), - "objectives": sorted(model.objectives), - "networks": sorted(model.networks), - "node_deployments": sorted(model.node_deployments), - } - fingerprint = hashlib.sha256(json.dumps(addresses, sort_keys=True).encode("utf-8")).hexdigest() - return { - "processor": "raes-reference-processor", - "compiled_address_sets": addresses, - "compiled_model_fingerprint": "sha256:" + fingerprint, - } - - -def _node_services(node: NodeDeployment) -> list[dict[str, Any]]: - spec = getattr(node, "spec", {}) or {} - node_spec = spec.get("node", {}) if isinstance(spec, Mapping) else {} - services = node_spec.get("services", []) if isinstance(node_spec, Mapping) else [] - return [ - {"name": svc.get("name"), "port": svc.get("port"), "protocol": svc.get("protocol")} - for svc in services - if isinstance(svc, Mapping) - ] - - -def _node_network_links(node: NodeDeployment) -> list[str]: - spec = getattr(node, "spec", {}) or {} - infra = spec.get("infrastructure", {}) if isinstance(spec, Mapping) else {} - links = infra.get("links", []) if isinstance(infra, Mapping) else [] - return [str(link) for link in links] - - -def _network_properties(network: RealizedNetwork) -> dict[str, Any]: - spec = getattr(network, "spec", {}) or {} - infra = spec.get("infrastructure", {}) if isinstance(spec, Mapping) else {} - props = infra.get("properties") if isinstance(infra, Mapping) else None - if not isinstance(props, Mapping): - return {} - return {"cidr": props.get("cidr"), "gateway": props.get("gateway"), "internal": props.get("internal")} - - -def _realization_facts_section( - model: CompiledModel, - native_snapshot: Mapping[str, Any] | None, - cleanup_verified: bool | None, - guest_observed: Mapping[str, Any] | None = None, -) -> dict[str, Any]: - observed = native_snapshot if isinstance(native_snapshot, Mapping) else {} - daemon_domains = observed.get("domains", ()) - daemon_networks = observed.get("networks", ()) - realized_addresses = observed.get("realized_addresses", ()) - return { - "authored": { - "source": "authored", - "scenario_name": model.scenario_name, - }, - "planned": { - "source": "planned", - "node_addresses": sorted(model.node_deployments), - "network_addresses": sorted(model.networks), - }, - "driver_reported": { - "source": "driver-reported", - "realized_addresses": list(realized_addresses) if isinstance(realized_addresses, list | tuple) else [], - }, - "daemon_observed": { - "source": "daemon-observed", - "domains": list(daemon_domains) if isinstance(daemon_domains, list | tuple) else [], - "networks": list(daemon_networks) if isinstance(daemon_networks, list | tuple) else [], - }, - "guest_observed": _guest_observed_section(guest_observed), - "cleanup": { - "source": "driver-reported", - "status": _cleanup_status(cleanup_verified), - }, - "binding": observed.get("binding"), - } - - -def _guest_observed_section(guest_observed: Mapping[str, Any] | None) -> dict[str, Any]: - """Return the guest-observed fact section: the bound report, or a not-observed stub. - - A daemon-only run discloses ``not-observed`` honestly; a guest-certified run - embeds the operation-joined, challenge-bound, per-domain guest report. - """ - - if not isinstance(guest_observed, Mapping): - return {"source": "guest-observed", "status": "not-observed"} - return {**guest_observed, "source": "guest-observed"} - - -def _cleanup_status(cleanup_verified: bool | None) -> str: - if cleanup_verified is None: - return "not-required" - return "verified" if cleanup_verified else "failed" - - -def _topology_section( - model: CompiledModel, - native_snapshot: Mapping[str, Any] | None, - unrealized_capabilities: tuple[str, ...] = (), -) -> dict[str, Any]: - substrate_realized = native_snapshot is not None - nodes = [ - { - "source": "planned", - "address": node.address, - "name": node.name, - "node_type": getattr(node, "node_type", None), - "os_family": getattr(node, "os_family", None), - "services": _node_services(node), - "networks": _node_network_links(node), - } - for node in model.node_deployments.values() - ] - networks = [ - {"source": "planned", "address": net.address, "name": net.name, **_network_properties(net)} - for net in model.networks.values() - ] - section: dict[str, Any] = { - "basis": "mixed-source" if substrate_realized else "planned", - "disclosure": ( - "Compiled topology remains planned; the native surface contains only independently daemon-observed " - "substrate fields." - if substrate_realized - else "Compiled/planned topology from the authored scenario; no live substrate realized. Network CIDRs and " - "gateways are authored values, not host-private libvirt addresses." - ), - "networks": networks, - "nodes": nodes, - "network_attachment_matrix": {node["name"]: node["networks"] for node in nodes}, - } - if native_snapshot is not None: - section["native_surface"] = expected_surface(native_snapshot) - if unrealized_capabilities: - section["unrealized_capabilities"] = list(unrealized_capabilities) - section["unrealized_capabilities_disclosure"] = ( - "The libvirt backend realizes the provisioning substrate (VMs and networks) only. The capabilities listed " - "above (content placement, orchestration, and evaluation) are not realized by this backend and remain " - "evaluator-only or translated; this is the n=2 backend-diversity limitation, not a substrate-realization " - "failure." - ) - return section - - -def _participant_proof_section(proof: Mapping[str, Any]) -> dict[str, Any]: - snapshot = proof["snapshot"] - episodes = {addr: _redact_episode_state(state) for addr, state in snapshot.participant_episode_results.items()} - return { - "runtime": "libvirt-deterministic-participant-runtime", - "lifecycle_clean": proof["lifecycle_clean"], - "diagnostics": list(proof["diagnostics"]), - "admitted_action_addresses": list(proof["admitted_action_addresses"]), - "episode_states": episodes, - # The participant runtime never received any visible/disclosed refs: the - # admission surface exposes nothing of the internal or evaluator state. - "participant_visible_refs": [], - "participant_disclosed_refs": [], - "structural_validation_note": ( - "Deep behavior-history and episode-snapshot invariant validation is performed by the issue #614 " - "participant-runtime test suite (processor-layer iterators); this artifact records the libvirt " - "participant-runtime lifecycle outcome." - ), - } - - -def _redact_episode_state(state: Mapping[str, Any]) -> dict[str, Any]: - if not isinstance(state, Mapping): - return {} - keep = ( - "state_schema_version", - "participant_address", - "episode_id", - "sequence_number", - "status", - "terminal_reason", - "last_control_action", - ) - return {key: state.get(key) for key in keep if key in state} - - -def _terminal_observation_section(snapshot: TerminalSnapshot) -> dict[str, Any]: - behavior_history = { - addr: _redact_behavior_history(events) for addr, events in snapshot.participant_behavior_history.items() - } - return { - "form": "participant-projected-history", - "taxonomy": { - "taxonomy_id": "raes-behavioral-relations", - "taxonomy_revision": "rev8", - "non_claimed_relation_ids": [ - "participant-projected-history-equivalence", - "epistemic-indistinguishability", - "alternating-strategic-equivalence", - ], - }, - "observation_projection": { - "subject": "the participant addressed by each behavior-history stream", - "policy_ref": "participant-observation-boundary", - "policy_revision": "participant-observation-envelope/v1", - "redaction_scope": "Only the fields retained by _redact_behavior_history are disclosed.", - "order_treatment": "Recorded participant sequence order is preserved.", - "simultaneity_treatment": "No simultaneity equivalence is inferred from the serialized order.", - }, - "evidence_boundary": ( - "The terminal snapshot's named participant behavior-history streams for this single run; " - "no second execution or universal trace set is compared." - ), - "disclosure": ( - "The libvirt participant runtime emits a behavior-history event stream rather than a standalone SEM-210 " - "observation envelope; the terminal participant view is reported as a bounded participant projection." - ), - "explicit_non_claims": [ - "This single projected record does not establish participant-projected-history-equivalence.", - "It does not establish epistemic or strategic equivalence.", - ], - "behavior_history": behavior_history, - } - - -def _redact_behavior_history(events: Sequence[Any]) -> list[dict[str, Any]]: - out: list[dict[str, Any]] = [] - if not isinstance(events, Sequence): - return out - for event in events: - if not isinstance(event, Mapping): - continue - out.append( - { - "event_type": event.get("event_type"), - "action_instance_id": event.get("action_instance_id"), - "action_contract_address": event.get("action_contract_address"), - "observation_boundary_address": event.get("observation_boundary_address"), - } - ) - return out - - -def _defensive_evidence_section( - native_snapshot: Mapping[str, Any] | None, model: CompiledModel, recorded_at: str -) -> dict[str, Any]: - # captured_at is the run's recorded_at timestamp threaded through artifact - # assembly, not a freshly synthesized one, so every section shares one - # consistent run timestamp and the artifact stays reproducible. - evidence_channels = _boundary_evidence_refs(model) - substrate_note = ( - " Daemon-observed libvirt substrate state is present, but it is not guest SOC observation." - if native_snapshot is not None - else "" - ) - return { - "evidence_kind": "telemetry", - "evidence_source": "structural-evaluator-channel", - "visibility": "evaluator-only", - "sensitivity": "restricted", - "redaction_state": "withheld", - "loss_disclosure": ( - "Deterministic mode: no live SOC substrate is booted. Wazuh/SOC defensive evidence is reported as the " - "evaluator-only evidence channels declared by the scenario observation boundary, not upstream Wazuh " - f"detection output; no detection-quality claim is made.{substrate_note}" - ), - "evaluator_evidence_channels": evidence_channels, - "payload_summary": ( - "Evaluator-only Wazuh/SOC and policy-decision evidence channels are declared and kept off the participant " - "view; neither evidence mode claims guest SOC readback." - ), - "captured_at": recorded_at, - } - - -def _negative_boundary_section(boundary_refs: Sequence[str]) -> dict[str, Any]: - internal_refs = [ref for ref in boundary_refs if any(kw in ref for kw in _INTERNAL_SURFACE_KEYWORDS)] - checks = [{"ref": ref, "exposed_to_participant": False} for ref in internal_refs] - return { - "method": ( - "Structural boundary analysis over the compiled observation boundary (hidden_refs) and the participant " - "exposure policy (empty visible/disclosed refs). The participant action surface does not expose the " - "internal DB, Wazuh, evaluator, or policy-gate surfaces." - ), - "value_status": "reported", - "all_internal_surfaces_withheld": all(not c["exposed_to_participant"] for c in checks), - "checks": checks, - "disclosure": "Negative boundary checks are evaluator-side derived analysis, not participant observations.", - } - - -def _evaluator_outcome_section(lifecycle_clean: bool, recorded_at: str) -> dict[str, Any]: - status = "ready" if lifecycle_clean else "failed" - result = EvaluationResultStateModel.model_validate( - { - "resource_type": "participant-loop-evaluation", - "run_id": "scenario-evidence", - "status": status, - "observed_at": recorded_at, - "updated_at": recorded_at, - "passed": lifecycle_clean, - "detail": "Structural participant-loop proof over the libvirt deterministic participant runtime.", - "evidence_refs": ["participant_action_proof", "negative_boundary_checks"], - } - ) - history = EvaluationHistoryEventModel.model_validate( - { - "event_type": "evaluation_completed", - "timestamp": recorded_at, - "status": status, - "passed": lifecycle_clean, - "detail": "Scenario-evidence evaluator outcome derived from the structural participant proof.", - "evidence_refs": ["participant_action_proof"], - } - ) - return { - "result": result.model_dump(mode="json"), - "history": [history.model_dump(mode="json")], - "limitations": [ - "Evaluator outcome reflects the structural participant-loop proof; the libvirt backend ships no generic " - "evaluator component, so this is a evidence-run evaluator record, not a generic backend evaluator result.", - ], - } - - -def _realized_form_disclosures(manifest: BackendManifest, substrate_realized: bool) -> list[dict[str, Any]]: - backend_version = _manifest_version(manifest) - backend_name = _manifest_name(manifest) - backend_ref = {"ref_kind": "backend", "ref_id": backend_name, "ref_version": backend_version} - disclosures = [ - ExperimentRealizedFormDisclosureModel.model_validate( - { - "concern_id": "libvirt-backend-selection", - "concern_kind": "backend-selection", - "basis": "backend-realized", - "realized_by_ref": backend_ref, - "realized_value_summary": ( - f"{backend_name} backend ({backend_version}); substrate " - f"{'daemon-observed at bounded fields' if substrate_realized else 'planned, not realized'}." - ), - "disclosure": ( - "The libvirt-qemu backend supplied the run; live claims are limited to independently " - "daemon-observed substrate fields." - ), - } - ), - ExperimentRealizedFormDisclosureModel.model_validate( - { - "concern_id": "libvirt-participant-implementation", - "concern_kind": "participant-implementation", - "basis": "backend-realized", - "realized_by_ref": backend_ref, - "realized_value_summary": ( - "Deterministic libvirt participant runtime (no live domain execution); see issue #614." - ), - "disclosure": ( - "The participant action proof uses the deterministic domain adapter; live domain execution is not " - "performed." - ), - } - ), - ] - return [d.model_dump(mode="json") for d in disclosures] - - -def _limitations(mode: str, unrealized_capabilities: tuple[str, ...] = ()) -> list[str]: - limitations = [ - "The libvirt participant runtime uses the deterministic domain adapter; no live participant domain is " - "executed (issue #614).", - "Wazuh/SOC evidence is evaluator-only structural evidence; daemon substrate state is not promoted to guest " - "or application observation.", - ] - if mode != "native-live": - limitations.append( - "Deterministic mode does not realize a live libvirt substrate; topology and defensive evidence channels " - "are compiled/structural, explicitly disclosed as not-live observations." - ) - if unrealized_capabilities: - limitations.append( - "Native-live mode realizes the provisioning substrate only; content placement, orchestration, and " - "evaluation declared by the scenario are not realized by the libvirt backend." - ) - return limitations - - -def _redaction_provenance() -> dict[str, Any]: - return { - "policy": ( - "Only allowlisted, bounded fields are copied into the artifact. Raw libvirt XML, domain UUIDs, QEMU " - "command lines, host paths, connection URIs, credentials, private keys, and backend-private inspect " - "payloads are never written." - ), - "redacted_field_classes": [ - "raw-libvirt-xml", - "domain-uuid", - "qemu-command-line", - "host-path", - "connection-uri", - "credential", - "private-key", - "backend-private-inspect-payload", - ], - "provenance_refs": [ - "docs/decisions/issue-615-libvirt-paper-evidence-preflight.md", - "docs/decisions/issue-614-libvirt-participant-runtime.md", - ], - } - - -def _invariant_ledger_refs(model: CompiledModel, scenario_section: Mapping[str, Any]) -> dict[str, Any]: - return { - "scenario_name": model.scenario_name, - "scenario_content_sha256": scenario_section["content_sha256"], - "participant_behaviors": sorted(model.participant_behaviors), - "action_contracts": sorted(model.action_contracts), - "observation_boundaries": sorted(model.observation_boundaries), - "evidence_refs": [ - "participant_action_proof", - "terminal_observation", - "defensive_evidence", - "negative_boundary_checks", - "evaluator_outcome", - ], - "note": ( - "Stable RAES addresses and evidence refs for the OpenRAE/rae#600 cross-backend invariant ledger; " - "no libvirt domain UUIDs, host paths, or APTL-private identifiers." - ), - } - - -def _boundary_hidden_refs(model: CompiledModel) -> list[str]: - return _boundary_spec_refs(model, "hidden_refs") - - -def _boundary_evidence_refs(model: CompiledModel) -> list[str]: - return _boundary_spec_refs(model, "evidence_refs") - - -def _boundary_spec_refs(model: CompiledModel, key: str) -> list[str]: - refs: list[str] = [] - for boundary in model.observation_boundaries.values(): - spec = getattr(boundary, "spec", None) - if isinstance(spec, Mapping): - for ref in spec.get(key, []) or []: - if isinstance(ref, str): - refs.append(ref) - return refs diff --git a/implementations/python/packages/raes_operations/_evidence_run_artifact/__init__.py b/implementations/python/packages/raes_operations/_evidence_run_artifact/__init__.py new file mode 100644 index 000000000..28a90b2cd --- /dev/null +++ b/implementations/python/packages/raes_operations/_evidence_run_artifact/__init__.py @@ -0,0 +1,91 @@ +"""Artifact assembly for the libvirt scenario-evidence producer. + +Builds the ``raes.libvirt.scenario-evidence-run/v1`` payload from the compiled runtime +model, the backend manifest, the participant-proof result, and (optionally) the +native substrate snapshot. Section builders only read duck-typed runtime-layer +objects and copy allowlisted, bounded fields, so no raw libvirt/backend internals +reach the artifact. The backend section embeds the canonical ``BackendManifestV2`` +payload rendered by the pure ``raes_backend_protocols`` manifest/capability helpers +(ADR-036 allows ``raes_operations`` those two side-effect-free renderers) so the +evidence carries the same backend contract the rest of the stack uses, not a +hand-rolled summary. Split from ``libvirt_evidence_run`` to keep each module under +the ADR-015 source-size cap. +""" + +from __future__ import annotations + +from typing import Any + +from raes_operations._evidence_run_types import ( + EvidenceArtifactInputs, +) + +from ._backend import _backend_section, _realized_form_disclosures +from ._evidence import ( + _boundary_hidden_refs, + _defensive_evidence_section, + _evaluator_outcome_section, + _limitations, + _negative_boundary_section, + _redaction_provenance, +) +from ._participant import _participant_proof_section, _terminal_observation_section +from ._topology import ( + _compiled_artifact_section, + _invariant_ledger_refs, + _realization_facts_section, + _scenario_section, + _topology_section, +) + +EVIDENCE_RUN_SCHEMA = "raes.libvirt.scenario-evidence-run/v1" + +# The four scenario non-claims (issue #615). Carried verbatim in the artifact. +_NON_CLAIMS = ( + "No Wazuh detection-quality claim.", + "No model-defense robustness claim.", + "No byte-equivalence or application-internals equivalence claim between libvirt appliances and APTL containers.", + "No full semantic-equivalence claim beyond the invariant ledger in OpenRAE/rae#600.", +) + + +def assemble_artifact(inputs: EvidenceArtifactInputs) -> dict[str, Any]: + """Assemble the full scenario-evidence artifact payload.""" + scenario_path = inputs.scenario_path + run_id = inputs.run_id + recorded_at = inputs.recorded_at + mode = inputs.mode + model = inputs.model + manifest = inputs.manifest + proof = inputs.proof + native_snapshot = inputs.native_snapshot + native_cleanup_verified = inputs.native_cleanup_verified + unrealized_capabilities = inputs.unrealized_capabilities + + substrate_realized = native_snapshot is not None + scenario_section = _scenario_section(scenario_path, model) + boundary_refs = _boundary_hidden_refs(model) + + return { + "schema": EVIDENCE_RUN_SCHEMA, + "run_id": run_id, + "recorded_at": recorded_at, + "evidence_source_mode": mode, + "scenario": scenario_section, + "compiled_artifact": _compiled_artifact_section(model), + "backend": _backend_section(manifest, mode, substrate_realized, native_cleanup_verified), + "realization_facts": _realization_facts_section( + model, native_snapshot, native_cleanup_verified, inputs.guest_observed + ), + "realized_topology": _topology_section(model, native_snapshot, unrealized_capabilities), + "participant_action_proof": _participant_proof_section(proof), + "terminal_observation": _terminal_observation_section(proof["snapshot"]), + "defensive_evidence": _defensive_evidence_section(native_snapshot, model, recorded_at), + "negative_boundary_checks": _negative_boundary_section(boundary_refs), + "evaluator_outcome": _evaluator_outcome_section(proof["lifecycle_clean"], recorded_at), + "realized_form_disclosures": _realized_form_disclosures(manifest, substrate_realized), + "limitations": _limitations(mode, unrealized_capabilities), + "non_claims": list(_NON_CLAIMS), + "redaction_provenance": _redaction_provenance(), + "invariant_ledger_refs": _invariant_ledger_refs(model, scenario_section), + } diff --git a/implementations/python/packages/raes_operations/_evidence_run_artifact/_backend.py b/implementations/python/packages/raes_operations/_evidence_run_artifact/_backend.py new file mode 100644 index 000000000..fb6663b3f --- /dev/null +++ b/implementations/python/packages/raes_operations/_evidence_run_artifact/_backend.py @@ -0,0 +1,106 @@ +"""Backend-manifest and realized-form section builders for the evidence artifact.""" + +from __future__ import annotations + +from typing import Any + +from raes_backend_protocols.capabilities import ( + observation_capability_contract_gaps, + participant_runtime_capability_contract_gaps, +) +from raes_backend_protocols.manifest import backend_manifest_payload +from raes_contracts.contracts import ( + ExperimentRealizedFormDisclosureModel, +) + +from raes_operations._evidence_run_types import ( + BackendManifest, +) + +_LIBVIRT_BACKEND_NAME = "libvirt-qemu" + + +def _manifest_name(manifest: BackendManifest) -> str: + identity = getattr(manifest, "identity", None) + if identity is not None and getattr(identity, "name", None): + return str(identity.name) + return str(getattr(manifest, "name", _LIBVIRT_BACKEND_NAME)) + + +def _manifest_version(manifest: BackendManifest) -> str: + identity = getattr(manifest, "identity", None) + if identity is not None and getattr(identity, "version", None): + return str(identity.version) + return str(getattr(manifest, "version", "0.0.0+unknown")) + + +def _backend_section( + manifest: BackendManifest, + mode: str, + substrate_realized: bool, + cleanup_verified: bool | None, +) -> dict[str, Any]: + """Embed the canonical BackendManifestV2 payload + capability-gap report. + + The manifest is rendered through ``backend_manifest_payload`` — the same + canonical V2 renderer the rest of the stack uses — and re-validated against + ``BackendManifestV2Model`` by the artifact validator, so the evidence carries + the published backend contract rather than a hand-rolled summary. The + capability profile reports any contract gaps between the declared participant- + runtime / observation capabilities and their required contracts (empty when the + manifest fully satisfies them). + """ + return { + "manifest": backend_manifest_payload(manifest), + "capability_profile": { + "participant_runtime_contract_gaps": list(participant_runtime_capability_contract_gaps(manifest)), + "observation_contract_gaps": list(observation_capability_contract_gaps(manifest)), + }, + "realization_provenance": { + "backend": _manifest_name(manifest), + "evidence_source_mode": mode, + "substrate_realized": substrate_realized, + "basis": "daemon-observed-substrate" if substrate_realized else "planned-not-realized", + "cleanup_verified": cleanup_verified, + }, + } + + +def _realized_form_disclosures(manifest: BackendManifest, substrate_realized: bool) -> list[dict[str, Any]]: + backend_version = _manifest_version(manifest) + backend_name = _manifest_name(manifest) + backend_ref = {"ref_kind": "backend", "ref_id": backend_name, "ref_version": backend_version} + disclosures = [ + ExperimentRealizedFormDisclosureModel.model_validate( + { + "concern_id": "libvirt-backend-selection", + "concern_kind": "backend-selection", + "basis": "backend-realized", + "realized_by_ref": backend_ref, + "realized_value_summary": ( + f"{backend_name} backend ({backend_version}); substrate " + f"{'daemon-observed at bounded fields' if substrate_realized else 'planned, not realized'}." + ), + "disclosure": ( + "The libvirt-qemu backend supplied the run; live claims are limited to independently " + "daemon-observed substrate fields." + ), + } + ), + ExperimentRealizedFormDisclosureModel.model_validate( + { + "concern_id": "libvirt-participant-implementation", + "concern_kind": "participant-implementation", + "basis": "backend-realized", + "realized_by_ref": backend_ref, + "realized_value_summary": ( + "Deterministic libvirt participant runtime (no live domain execution); see issue #614." + ), + "disclosure": ( + "The participant action proof uses the deterministic domain adapter; live domain execution is not " + "performed." + ), + } + ), + ] + return [d.model_dump(mode="json") for d in disclosures] diff --git a/implementations/python/packages/raes_operations/_evidence_run_artifact/_evidence.py b/implementations/python/packages/raes_operations/_evidence_run_artifact/_evidence.py new file mode 100644 index 000000000..36e3d9d57 --- /dev/null +++ b/implementations/python/packages/raes_operations/_evidence_run_artifact/_evidence.py @@ -0,0 +1,165 @@ +"""Defensive-evidence, negative-boundary, evaluator-outcome, and limitation section builders.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Any + +from raes_contracts.contracts import ( + EvaluationHistoryEventModel, + EvaluationResultStateModel, +) + +from raes_operations._evidence_run_types import ( + CompiledModel, +) + +# Internal/evaluator-only surfaces the participant must never observe. Derived from +# the reference scenario observation boundary's hidden_refs; the negative-boundary +# evidence confirms none of these reach the participant's visible/disclosed refs. +_INTERNAL_SURFACE_KEYWORDS = ("customer-db", "wazuh", "evaluator", "policy-gate", "postgres") + + +def _defensive_evidence_section( + native_snapshot: Mapping[str, Any] | None, model: CompiledModel, recorded_at: str +) -> dict[str, Any]: + # captured_at is the run's recorded_at timestamp threaded through artifact + # assembly, not a freshly synthesized one, so every section shares one + # consistent run timestamp and the artifact stays reproducible. + evidence_channels = _boundary_evidence_refs(model) + substrate_note = ( + " Daemon-observed libvirt substrate state is present, but it is not guest SOC observation." + if native_snapshot is not None + else "" + ) + return { + "evidence_kind": "telemetry", + "evidence_source": "structural-evaluator-channel", + "visibility": "evaluator-only", + "sensitivity": "restricted", + "redaction_state": "withheld", + "loss_disclosure": ( + "Deterministic mode: no live SOC substrate is booted. Wazuh/SOC defensive evidence is reported as the " + "evaluator-only evidence channels declared by the scenario observation boundary, not upstream Wazuh " + f"detection output; no detection-quality claim is made.{substrate_note}" + ), + "evaluator_evidence_channels": evidence_channels, + "payload_summary": ( + "Evaluator-only Wazuh/SOC and policy-decision evidence channels are declared and kept off the participant " + "view; neither evidence mode claims guest SOC readback." + ), + "captured_at": recorded_at, + } + + +def _negative_boundary_section(boundary_refs: Sequence[str]) -> dict[str, Any]: + internal_refs = [ref for ref in boundary_refs if any(kw in ref for kw in _INTERNAL_SURFACE_KEYWORDS)] + checks = [{"ref": ref, "exposed_to_participant": False} for ref in internal_refs] + return { + "method": ( + "Structural boundary analysis over the compiled observation boundary (hidden_refs) and the participant " + "exposure policy (empty visible/disclosed refs). The participant action surface does not expose the " + "internal DB, Wazuh, evaluator, or policy-gate surfaces." + ), + "value_status": "reported", + "all_internal_surfaces_withheld": all(not c["exposed_to_participant"] for c in checks), + "checks": checks, + "disclosure": "Negative boundary checks are evaluator-side derived analysis, not participant observations.", + } + + +def _evaluator_outcome_section(lifecycle_clean: bool, recorded_at: str) -> dict[str, Any]: + status = "ready" if lifecycle_clean else "failed" + result = EvaluationResultStateModel.model_validate( + { + "resource_type": "participant-loop-evaluation", + "run_id": "scenario-evidence", + "status": status, + "observed_at": recorded_at, + "updated_at": recorded_at, + "passed": lifecycle_clean, + "detail": "Structural participant-loop proof over the libvirt deterministic participant runtime.", + "evidence_refs": ["participant_action_proof", "negative_boundary_checks"], + } + ) + history = EvaluationHistoryEventModel.model_validate( + { + "event_type": "evaluation_completed", + "timestamp": recorded_at, + "status": status, + "passed": lifecycle_clean, + "detail": "Scenario-evidence evaluator outcome derived from the structural participant proof.", + "evidence_refs": ["participant_action_proof"], + } + ) + return { + "result": result.model_dump(mode="json"), + "history": [history.model_dump(mode="json")], + "limitations": [ + "Evaluator outcome reflects the structural participant-loop proof; the libvirt backend ships no generic " + "evaluator component, so this is a evidence-run evaluator record, not a generic backend evaluator result.", + ], + } + + +def _limitations(mode: str, unrealized_capabilities: tuple[str, ...] = ()) -> list[str]: + limitations = [ + "The libvirt participant runtime uses the deterministic domain adapter; no live participant domain is " + "executed (issue #614).", + "Wazuh/SOC evidence is evaluator-only structural evidence; daemon substrate state is not promoted to guest " + "or application observation.", + ] + if mode != "native-live": + limitations.append( + "Deterministic mode does not realize a live libvirt substrate; topology and defensive evidence channels " + "are compiled/structural, explicitly disclosed as not-live observations." + ) + if unrealized_capabilities: + limitations.append( + "Native-live mode realizes the provisioning substrate only; content placement, orchestration, and " + "evaluation declared by the scenario are not realized by the libvirt backend." + ) + return limitations + + +def _redaction_provenance() -> dict[str, Any]: + return { + "policy": ( + "Only allowlisted, bounded fields are copied into the artifact. Raw libvirt XML, domain UUIDs, QEMU " + "command lines, host paths, connection URIs, credentials, private keys, and backend-private inspect " + "payloads are never written." + ), + "redacted_field_classes": [ + "raw-libvirt-xml", + "domain-uuid", + "qemu-command-line", + "host-path", + "connection-uri", + "credential", + "private-key", + "backend-private-inspect-payload", + ], + "provenance_refs": [ + "docs/decisions/issue-615-libvirt-paper-evidence-preflight.md", + "docs/decisions/issue-614-libvirt-participant-runtime.md", + ], + } + + +def _boundary_hidden_refs(model: CompiledModel) -> list[str]: + return _boundary_spec_refs(model, "hidden_refs") + + +def _boundary_evidence_refs(model: CompiledModel) -> list[str]: + return _boundary_spec_refs(model, "evidence_refs") + + +def _boundary_spec_refs(model: CompiledModel, key: str) -> list[str]: + refs: list[str] = [] + for boundary in model.observation_boundaries.values(): + spec = getattr(boundary, "spec", None) + if isinstance(spec, Mapping): + for ref in spec.get(key, []) or []: + if isinstance(ref, str): + refs.append(ref) + return refs diff --git a/implementations/python/packages/raes_operations/_evidence_run_artifact/_participant.py b/implementations/python/packages/raes_operations/_evidence_run_artifact/_participant.py new file mode 100644 index 000000000..7d8047637 --- /dev/null +++ b/implementations/python/packages/raes_operations/_evidence_run_artifact/_participant.py @@ -0,0 +1,103 @@ +"""Participant-proof and terminal-observation section builders for the evidence artifact.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Any + +from raes_operations._evidence_run_types import ( + TerminalSnapshot, +) + + +def _participant_proof_section(proof: Mapping[str, Any]) -> dict[str, Any]: + snapshot = proof["snapshot"] + episodes = {addr: _redact_episode_state(state) for addr, state in snapshot.participant_episode_results.items()} + return { + "runtime": "libvirt-deterministic-participant-runtime", + "lifecycle_clean": proof["lifecycle_clean"], + "diagnostics": list(proof["diagnostics"]), + "admitted_action_addresses": list(proof["admitted_action_addresses"]), + "episode_states": episodes, + # The participant runtime never received any visible/disclosed refs: the + # admission surface exposes nothing of the internal or evaluator state. + "participant_visible_refs": [], + "participant_disclosed_refs": [], + "structural_validation_note": ( + "Deep behavior-history and episode-snapshot invariant validation is performed by the issue #614 " + "participant-runtime test suite (processor-layer iterators); this artifact records the libvirt " + "participant-runtime lifecycle outcome." + ), + } + + +def _redact_episode_state(state: Mapping[str, Any]) -> dict[str, Any]: + if not isinstance(state, Mapping): + return {} + keep = ( + "state_schema_version", + "participant_address", + "episode_id", + "sequence_number", + "status", + "terminal_reason", + "last_control_action", + ) + return {key: state.get(key) for key in keep if key in state} + + +def _terminal_observation_section(snapshot: TerminalSnapshot) -> dict[str, Any]: + behavior_history = { + addr: _redact_behavior_history(events) for addr, events in snapshot.participant_behavior_history.items() + } + return { + "form": "participant-projected-history", + "taxonomy": { + "taxonomy_id": "raes-behavioral-relations", + "taxonomy_revision": "rev8", + "non_claimed_relation_ids": [ + "participant-projected-history-equivalence", + "epistemic-indistinguishability", + "alternating-strategic-equivalence", + ], + }, + "observation_projection": { + "subject": "the participant addressed by each behavior-history stream", + "policy_ref": "participant-observation-boundary", + "policy_revision": "participant-observation-envelope/v1", + "redaction_scope": "Only the fields retained by _redact_behavior_history are disclosed.", + "order_treatment": "Recorded participant sequence order is preserved.", + "simultaneity_treatment": "No simultaneity equivalence is inferred from the serialized order.", + }, + "evidence_boundary": ( + "The terminal snapshot's named participant behavior-history streams for this single run; " + "no second execution or universal trace set is compared." + ), + "disclosure": ( + "The libvirt participant runtime emits a behavior-history event stream rather than a standalone SEM-210 " + "observation envelope; the terminal participant view is reported as a bounded participant projection." + ), + "explicit_non_claims": [ + "This single projected record does not establish participant-projected-history-equivalence.", + "It does not establish epistemic or strategic equivalence.", + ], + "behavior_history": behavior_history, + } + + +def _redact_behavior_history(events: Sequence[Any]) -> list[dict[str, Any]]: + out: list[dict[str, Any]] = [] + if not isinstance(events, Sequence): + return out + for event in events: + if not isinstance(event, Mapping): + continue + out.append( + { + "event_type": event.get("event_type"), + "action_instance_id": event.get("action_instance_id"), + "action_contract_address": event.get("action_contract_address"), + "observation_boundary_address": event.get("observation_boundary_address"), + } + ) + return out diff --git a/implementations/python/packages/raes_operations/_evidence_run_artifact/_topology.py b/implementations/python/packages/raes_operations/_evidence_run_artifact/_topology.py new file mode 100644 index 000000000..0f2c4b0cd --- /dev/null +++ b/implementations/python/packages/raes_operations/_evidence_run_artifact/_topology.py @@ -0,0 +1,204 @@ +"""Scenario, compiled-artifact, realization-facts, and topology section builders.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +from raes_backend_libvirt.techvault_native import expected_surface + +from raes_operations._evidence_run_types import ( + CompiledModel, + NodeDeployment, + RealizedNetwork, +) +from raes_operations.run_artifacts import portable_artifact_ref + + +def _scenario_section(scenario_path: Path, model: CompiledModel) -> dict[str, Any]: + from raes.parser import parse_sdl_file + + content = scenario_path.read_bytes() + version: str | None = None + try: + version = getattr(parse_sdl_file(scenario_path), "version", None) + except Exception: + version = None + return { + "name": model.scenario_name, + "version": version, + "relative_path": portable_artifact_ref(scenario_path), + "content_sha256": "sha256:" + hashlib.sha256(content).hexdigest(), + } + + +def _compiled_artifact_section(model: CompiledModel) -> dict[str, Any]: + addresses = { + "participant_behaviors": sorted(model.participant_behaviors), + "action_contracts": sorted(model.action_contracts), + "observation_boundaries": sorted(model.observation_boundaries), + "objectives": sorted(model.objectives), + "networks": sorted(model.networks), + "node_deployments": sorted(model.node_deployments), + } + fingerprint = hashlib.sha256(json.dumps(addresses, sort_keys=True).encode("utf-8")).hexdigest() + return { + "processor": "raes-reference-processor", + "compiled_address_sets": addresses, + "compiled_model_fingerprint": "sha256:" + fingerprint, + } + + +def _node_services(node: NodeDeployment) -> list[dict[str, Any]]: + spec = getattr(node, "spec", {}) or {} + node_spec = spec.get("node", {}) if isinstance(spec, Mapping) else {} + services = node_spec.get("services", []) if isinstance(node_spec, Mapping) else [] + return [ + {"name": svc.get("name"), "port": svc.get("port"), "protocol": svc.get("protocol")} + for svc in services + if isinstance(svc, Mapping) + ] + + +def _node_network_links(node: NodeDeployment) -> list[str]: + spec = getattr(node, "spec", {}) or {} + infra = spec.get("infrastructure", {}) if isinstance(spec, Mapping) else {} + links = infra.get("links", []) if isinstance(infra, Mapping) else [] + return [str(link) for link in links] + + +def _network_properties(network: RealizedNetwork) -> dict[str, Any]: + spec = getattr(network, "spec", {}) or {} + infra = spec.get("infrastructure", {}) if isinstance(spec, Mapping) else {} + props = infra.get("properties") if isinstance(infra, Mapping) else None + if not isinstance(props, Mapping): + return {} + return {"cidr": props.get("cidr"), "gateway": props.get("gateway"), "internal": props.get("internal")} + + +def _realization_facts_section( + model: CompiledModel, + native_snapshot: Mapping[str, Any] | None, + cleanup_verified: bool | None, + guest_observed: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + observed = native_snapshot if isinstance(native_snapshot, Mapping) else {} + daemon_domains = observed.get("domains", ()) + daemon_networks = observed.get("networks", ()) + realized_addresses = observed.get("realized_addresses", ()) + return { + "authored": { + "source": "authored", + "scenario_name": model.scenario_name, + }, + "planned": { + "source": "planned", + "node_addresses": sorted(model.node_deployments), + "network_addresses": sorted(model.networks), + }, + "driver_reported": { + "source": "driver-reported", + "realized_addresses": list(realized_addresses) if isinstance(realized_addresses, list | tuple) else [], + }, + "daemon_observed": { + "source": "daemon-observed", + "domains": list(daemon_domains) if isinstance(daemon_domains, list | tuple) else [], + "networks": list(daemon_networks) if isinstance(daemon_networks, list | tuple) else [], + }, + "guest_observed": _guest_observed_section(guest_observed), + "cleanup": { + "source": "driver-reported", + "status": _cleanup_status(cleanup_verified), + }, + "binding": observed.get("binding"), + } + + +def _guest_observed_section(guest_observed: Mapping[str, Any] | None) -> dict[str, Any]: + """Return the guest-observed fact section: the bound report, or a not-observed stub. + + A daemon-only run discloses ``not-observed`` honestly; a guest-certified run + embeds the operation-joined, challenge-bound, per-domain guest report. + """ + + if not isinstance(guest_observed, Mapping): + return {"source": "guest-observed", "status": "not-observed"} + return {**guest_observed, "source": "guest-observed"} + + +def _cleanup_status(cleanup_verified: bool | None) -> str: + if cleanup_verified is None: + return "not-required" + return "verified" if cleanup_verified else "failed" + + +def _topology_section( + model: CompiledModel, + native_snapshot: Mapping[str, Any] | None, + unrealized_capabilities: tuple[str, ...] = (), +) -> dict[str, Any]: + substrate_realized = native_snapshot is not None + nodes = [ + { + "source": "planned", + "address": node.address, + "name": node.name, + "node_type": getattr(node, "node_type", None), + "os_family": getattr(node, "os_family", None), + "services": _node_services(node), + "networks": _node_network_links(node), + } + for node in model.node_deployments.values() + ] + networks = [ + {"source": "planned", "address": net.address, "name": net.name, **_network_properties(net)} + for net in model.networks.values() + ] + section: dict[str, Any] = { + "basis": "mixed-source" if substrate_realized else "planned", + "disclosure": ( + "Compiled topology remains planned; the native surface contains only independently daemon-observed " + "substrate fields." + if substrate_realized + else "Compiled/planned topology from the authored scenario; no live substrate realized. Network CIDRs and " + "gateways are authored values, not host-private libvirt addresses." + ), + "networks": networks, + "nodes": nodes, + "network_attachment_matrix": {node["name"]: node["networks"] for node in nodes}, + } + if native_snapshot is not None: + section["native_surface"] = expected_surface(native_snapshot) + if unrealized_capabilities: + section["unrealized_capabilities"] = list(unrealized_capabilities) + section["unrealized_capabilities_disclosure"] = ( + "The libvirt backend realizes the provisioning substrate (VMs and networks) only. The capabilities listed " + "above (content placement, orchestration, and evaluation) are not realized by this backend and remain " + "evaluator-only or translated; this is the n=2 backend-diversity limitation, not a substrate-realization " + "failure." + ) + return section + + +def _invariant_ledger_refs(model: CompiledModel, scenario_section: Mapping[str, Any]) -> dict[str, Any]: + return { + "scenario_name": model.scenario_name, + "scenario_content_sha256": scenario_section["content_sha256"], + "participant_behaviors": sorted(model.participant_behaviors), + "action_contracts": sorted(model.action_contracts), + "observation_boundaries": sorted(model.observation_boundaries), + "evidence_refs": [ + "participant_action_proof", + "terminal_observation", + "defensive_evidence", + "negative_boundary_checks", + "evaluator_outcome", + ], + "note": ( + "Stable RAES addresses and evidence refs for the OpenRAE/rae#600 cross-backend invariant ledger; " + "no libvirt domain UUIDs, host paths, or APTL-private identifiers." + ), + } diff --git a/implementations/python/packages/raes_processor/compiler/realization_requirements.py b/implementations/python/packages/raes_processor/compiler/realization_requirements.py index deb68719c..391c783d0 100644 --- a/implementations/python/packages/raes_processor/compiler/realization_requirements.py +++ b/implementations/python/packages/raes_processor/compiler/realization_requirements.py @@ -325,6 +325,7 @@ def _compiled_registered_requirement( provenance=provenance, governing_scope=governing_scope, delegated=delegated, + verification_scope=descriptor.required_verification_scope(authored_value), ) diff --git a/implementations/python/packages/raes_processor/semantics/realization.py b/implementations/python/packages/raes_processor/semantics/realization.py index d69e19681..6fdf3c1b5 100644 --- a/implementations/python/packages/raes_processor/semantics/realization.py +++ b/implementations/python/packages/raes_processor/semantics/realization.py @@ -17,6 +17,7 @@ from raes_contracts.apparatus import ( DECLARED_CAPABILITY_MATCH_REQUIREMENT_KIND, RUNTIME_REALIZATION_DOMAIN, + RealizationSupportDeclaration, ) from raes_contracts.artifact_requirements import ArtifactAvailabilityContext from raes_contracts.diagnostics import Diagnostic, Severity @@ -29,7 +30,12 @@ RealizationEnvelopeModel, ) from raes_contracts.runtime_state import RealizationProvenanceEntry, RuntimeSnapshot -from raes_contracts.vocabulary import Closure, RealizationSupportMode +from raes_contracts.vocabulary import ( + Closure, + RealizationSupportMode, + RealizationVerificationScope, + verification_scope_satisfies, +) from .artifact_realization import ( artifact_requirement_diagnostics, @@ -96,11 +102,17 @@ class CompiledRealizationRequirement: governing_scope: str | None = None delegated: bool = False artifact_requirement: ArtifactRequirement | None = None + verification_scope: RealizationVerificationScope | None = None def __post_init__(self) -> None: require_compiled_address(self.address) if self.delegated != (self.explicitness is None): raise ValueError("delegated realization requirements must carry unresolved explicitness") + if self.verification_scope is not None and not isinstance( + self.verification_scope, + RealizationVerificationScope, + ): + raise TypeError("verification_scope must be RealizationVerificationScope") if self.artifact_requirement is not None: if self.requirement_kind != "source-artifact": raise ValueError("artifact_requirement requires requirement_kind='source-artifact'") @@ -177,71 +189,116 @@ def realization_support_diagnostics( security / host-exposure gate). """ - diagnostics: list[Diagnostic] = [] - for requirement in requirements: - explicitness = _effective_explicitness(requirement, manifest, apparatus_default) - declarations = [ - declaration for declaration in manifest.realization_support if declaration.domain == requirement.domain - ] - if explicitness is ExplicitnessClass.OPEN: - supported = any( - declaration.support_mode is RealizationSupportMode.OPEN_REALIZATION for declaration in declarations - ) - if not supported: - diagnostics.append( - Diagnostic( - code="realization.unsupported-open-requirement", - domain=requirement.domain, - address=requirement.address, - message=( - "Backend declares no open realization support for " - f"'{requirement.requirement_kind}' requirement at " - f"'{requirement.field_path}' in domain '{requirement.domain}'." - ), - severity=Severity.ERROR, - ) - ) - continue - if explicitness is ExplicitnessClass.EXACT: - supported = any( - EXACT_REQUIREMENT_KIND in declaration.supported_exact_requirement_kinds for declaration in declarations - ) - if not supported: - diagnostics.append( - Diagnostic( - code="realization.unsupported-exact-requirement", - domain=requirement.domain, - address=requirement.address, - message=( - f"Backend declares no exact realization support " - f"('{EXACT_REQUIREMENT_KIND}') for exact " - f"'{requirement.requirement_kind}' requirement at " - f"'{requirement.field_path}' in domain " - f"'{requirement.domain}'." - ), - severity=Severity.ERROR, - ) - ) - elif explicitness is ExplicitnessClass.CONSTRAINED: - supported = any( - requirement.requirement_kind in declaration.supported_constraint_kinds for declaration in declarations + return [ + diagnostic + for requirement in requirements + if ( + diagnostic := _realization_support_diagnostic( + requirement, + manifest, + apparatus_default, ) - if not supported: - diagnostics.append( - Diagnostic( - code="realization.unsupported-constraint-requirement", - domain=requirement.domain, - address=requirement.address, - message=( - f"Backend declares no constraint realization support " - f"for constraint kind '{requirement.requirement_kind}' at " - f"'{requirement.field_path}' in domain " - f"'{requirement.domain}'." - ), - severity=Severity.ERROR, - ) - ) - return diagnostics + ) + is not None + ] + + +def _realization_support_diagnostic( + requirement: CompiledRealizationRequirement, + manifest: BackendManifest, + apparatus_default: ApparatusRealizationDefaultResolver | None, +) -> Diagnostic | None: + explicitness = _effective_explicitness(requirement, manifest, apparatus_default) + declarations = [ + declaration for declaration in manifest.realization_support if declaration.domain == requirement.domain + ] + if explicitness is ExplicitnessClass.OPEN: + diagnostic = _open_support_diagnostic(requirement, declarations) + elif explicitness is ExplicitnessClass.EXACT: + diagnostic = _exact_support_diagnostic(requirement, declarations) + elif explicitness is ExplicitnessClass.CONSTRAINED: + diagnostic = _constraint_support_diagnostic(requirement, declarations) + else: + diagnostic = None + return diagnostic + + +def _open_support_diagnostic( + requirement: CompiledRealizationRequirement, + declarations: list[RealizationSupportDeclaration], +) -> Diagnostic | None: + if any(declaration.support_mode is RealizationSupportMode.OPEN_REALIZATION for declaration in declarations): + return None + return Diagnostic( + code="realization.unsupported-open-requirement", + domain=requirement.domain, + address=requirement.address, + message=( + "Backend declares no open realization support for " + f"'{requirement.requirement_kind}' requirement at " + f"'{requirement.field_path}' in domain '{requirement.domain}'." + ), + severity=Severity.ERROR, + ) + + +def _exact_support_diagnostic( + requirement: CompiledRealizationRequirement, + declarations: list[RealizationSupportDeclaration], +) -> Diagnostic | None: + exact_declarations = [ + declaration + for declaration in declarations + if EXACT_REQUIREMENT_KIND in declaration.supported_exact_requirement_kinds + ] + if not exact_declarations: + return Diagnostic( + code="realization.unsupported-exact-requirement", + domain=requirement.domain, + address=requirement.address, + message=( + f"Backend declares no exact realization support ('{EXACT_REQUIREMENT_KIND}') for exact " + f"'{requirement.requirement_kind}' requirement at '{requirement.field_path}' in domain " + f"'{requirement.domain}'." + ), + 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 + ): + return None + return Diagnostic( + code="realization.under-observed-exact-requirement", + domain=requirement.domain, + address=requirement.address, + message=( + f"Backend declares no '{requirement.verification_scope.value}' corroboration " + f"for exact '{requirement.requirement_kind}' requirement at " + f"'{requirement.field_path}' in domain '{requirement.domain}'." + ), + severity=Severity.ERROR, + ) + + +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, + ) def realization_envelope_diagnostics( @@ -387,6 +444,7 @@ def realization_disclosure( requirement, declared_ops, returned_snapshot, + manifest=manifest, ) if diagnostic is not None: diagnostics.append(diagnostic) diff --git a/implementations/python/packages/raes_processor/semantics/realization_concern_projections.py b/implementations/python/packages/raes_processor/semantics/realization_concern_projections.py index 41af0aa82..2a4325fe3 100644 --- a/implementations/python/packages/raes_processor/semantics/realization_concern_projections.py +++ b/implementations/python/packages/raes_processor/semantics/realization_concern_projections.py @@ -279,6 +279,9 @@ def project_forwarding_agents(value: object, observed: bool = False) -> object: if not isinstance(agent_id, str) or not agent_id: raise ValueError("forwarding agents require a forwarding_agent_id") agent = _without_annotations(record) + # Ownership is authored experiment meaning, not backend-observed + # forwarding-agent configuration. + agent.pop("ownership_role", None) for field, identity in ( ("sources", "source_id"), ("transforms", "transform_id"), diff --git a/implementations/python/packages/raes_processor/semantics/realization_concerns.py b/implementations/python/packages/raes_processor/semantics/realization_concerns.py index 675a21aa5..5405d8643 100644 --- a/implementations/python/packages/raes_processor/semantics/realization_concerns.py +++ b/implementations/python/packages/raes_processor/semantics/realization_concerns.py @@ -5,6 +5,8 @@ from collections.abc import Callable, Iterable, Mapping from dataclasses import dataclass +from raes_contracts.vocabulary import RealizationVerificationScope + from .realization_concern_observations import ( validate_capability_policy_observation, validate_environment_observation, @@ -35,6 +37,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 non_stateful_mounts_only: bool = False @property @@ -63,6 +66,11 @@ def sanitize(self, value: object, *, observed: bool) -> object: projector = self.sanitizer or self.projector return projector(value, observed) if projector is not None else value + def required_verification_scope(self, value: object) -> RealizationVerificationScope | None: + """Return the authored inventory scope that must be corroborated.""" + + return self.verification_scope(value) if self.verification_scope is not None else None + @dataclass(frozen=True) class RegisteredRealizationConcern: @@ -81,6 +89,22 @@ def _mount_source_kind(item: object) -> object: return getattr(source_kind, "value", source_kind) +def _forwarding_agent_verification_scope(value: object) -> RealizationVerificationScope: + """Classify identity-only inventory separately from authored configuration.""" + + for agent in value if isinstance(value, list) else (): + for field_name in ("sources", "transforms", "ship_targets", "reload_channels", "settings"): + field_value = agent.get(field_name) if isinstance(agent, Mapping) else getattr(agent, field_name, None) + if field_value: + return RealizationVerificationScope.CONFIGURATION + buffer_policy = ( + agent.get("buffer_policy") if isinstance(agent, Mapping) else getattr(agent, "buffer_policy", None) + ) + if buffer_policy is not None: + return RealizationVerificationScope.CONFIGURATION + return RealizationVerificationScope.PRESENCE + + _REALIZATION_CONCERNS: tuple[RealizationConcernDescriptor, ...] = ( RealizationConcernDescriptor( section="nodes", @@ -141,6 +165,7 @@ def _mount_source_kind(item: object) -> object: payload_path=("spec", "node", "runtime", "forwarding_agents"), projector=project_forwarding_agents, observed_validator=validate_forwarding_agents_observation, + verification_scope=_forwarding_agent_verification_scope, ), RealizationConcernDescriptor( section="nodes", 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 e463d77f6..23a7b7fc4 100644 --- a/implementations/python/packages/raes_processor/semantics/realization_runtime_evaluation.py +++ b/implementations/python/packages/raes_processor/semantics/realization_runtime_evaluation.py @@ -5,9 +5,16 @@ from typing import TYPE_CHECKING from raes.explicitness import ExplicitnessClass, ExplicitnessProvenance +from raes_backend_protocols.capabilities import BackendManifest +from raes_contracts.apparatus import DECLARED_CAPABILITY_MATCH_REQUIREMENT_KIND from raes_contracts.diagnostics import Diagnostic, Severity from raes_contracts.planning import ChangeAction, ProvisionOp -from raes_contracts.runtime_state import RealizationProvenanceEntry, RuntimeSnapshot +from raes_contracts.runtime_state import ( + RealizationObservationDisclosure, + RealizationProvenanceEntry, + RuntimeSnapshot, +) +from raes_contracts.vocabulary import verification_scope_satisfies from .realization_concerns import CONCERN_PAYLOAD_PATH, project_realization_concern from .realization_snapshot_sanitization import invalid_observation_diagnostic @@ -23,6 +30,8 @@ def evaluate_registered_realization( requirement: CompiledRealizationRequirement, declared_ops: dict[str, ProvisionOp], returned_snapshot: RuntimeSnapshot, + *, + manifest: BackendManifest | None = None, ) -> tuple[Diagnostic | None, RealizationProvenanceEntry | None]: """Gate one compiled requirement against its realized value.""" @@ -40,6 +49,8 @@ def evaluate_registered_realization( requirement, _concern_value(op.payload, path), realized_value, + returned_snapshot, + manifest, ) @@ -59,9 +70,14 @@ def _evaluate_declared_realization( requirement: CompiledRealizationRequirement, declared_value: object, realized_value: object, + returned_snapshot: RuntimeSnapshot, + manifest: BackendManifest | None, ) -> tuple[Diagnostic | None, RealizationProvenanceEntry | None]: if declared_value is _MISSING_CONCERN_VALUE: return None, None + corroboration_diagnostic = _corroboration_diagnostic(requirement, returned_snapshot, manifest) + if corroboration_diagnostic is not None: + return corroboration_diagnostic, None try: declared_projection = project_realization_concern( requirement.requirement_kind, @@ -83,6 +99,78 @@ def _evaluate_declared_realization( return result +def _corroboration_diagnostic( + requirement: CompiledRealizationRequirement, + returned_snapshot: RuntimeSnapshot, + manifest: BackendManifest | None, +) -> Diagnostic | None: + """Reject exact inventory equality that lacks its declared observation basis.""" + + required_scope = requirement.verification_scope + if requirement.explicitness is not ExplicitnessClass.EXACT or required_scope is None: + return None + observation = _matching_observation(requirement, returned_snapshot) + if ( + observation is not None + and verification_scope_satisfies(observation.verification_scope, required_scope) + and _manifest_corroborates(requirement, observation, manifest) + ): + return None + return Diagnostic( + code=_BACKEND_CONTRACT_INVALID, + domain=requirement.domain, + address=requirement.address, + message=( + f"Backend returned no valid '{required_scope.value}' corroboration for exact " + f"'{requirement.requirement_kind}' requirement at '{requirement.field_path}'; " + "matching inventory values alone do not establish realization (SEM-218 I2)." + ), + severity=Severity.ERROR, + ) + + +def _matching_observation( + requirement: CompiledRealizationRequirement, + returned_snapshot: RuntimeSnapshot, +) -> RealizationObservationDisclosure | None: + return next( + ( + entry + for entry in returned_snapshot.realization_observations + if ( + entry.address, + entry.field_path, + entry.domain, + entry.requirement_kind, + ) + == ( + requirement.address, + requirement.field_path, + requirement.domain, + requirement.requirement_kind, + ) + ), + None, + ) + + +def _manifest_corroborates( + requirement: CompiledRealizationRequirement, + observation: RealizationObservationDisclosure, + manifest: BackendManifest | None, +) -> bool: + if manifest is None: + return False + return any( + (capability := declaration.observation_capabilities.get(requirement.requirement_kind)) is not None + and DECLARED_CAPABILITY_MATCH_REQUIREMENT_KIND in declaration.supported_exact_requirement_kinds + and verification_scope_satisfies(capability.verification_scope, observation.verification_scope) + and capability.observation_strength is observation.observation_strength + for declaration in manifest.realization_support + if declaration.domain == requirement.domain + ) + + def _observed_projection( requirement: CompiledRealizationRequirement, realized_value: object, diff --git a/implementations/python/packages/raes_reference_backend/manifest.py b/implementations/python/packages/raes_reference_backend/manifest.py index 9c83a3617..8bdc4d3a8 100644 --- a/implementations/python/packages/raes_reference_backend/manifest.py +++ b/implementations/python/packages/raes_reference_backend/manifest.py @@ -18,6 +18,7 @@ CLEANUP_CAPABILITY_REQUIRED_CONTRACTS, PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, PARTICIPANT_RUNTIME_POLICY_FEATURES, PARTICIPANT_RUNTIME_ROLE_SCOPE, @@ -48,11 +49,9 @@ _TIME_DEDICATED_CONTRACT_VERSIONS = frozenset({"time-model-v1", "time-runtime-state-v1", "realized-time-model-v1"}) _PARTICIPANT_ROLES = frozenset(PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_ROLE_SCOPE]) -_PARTICIPANT_BEHAVIOR_FEATURES = ( - frozenset(PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE]) - - {"autonomous_execution"} - - PARTICIPANT_RUNTIME_POLICY_FEATURES -) +_PARTICIPANT_BEHAVIOR_FEATURES = frozenset( + PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE] +) - {"autonomous_execution"} - PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES | {"participant_predicate_opacity"} _PARTICIPANT_INTERACTION_FEATURES = frozenset( PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE] ) @@ -170,14 +169,36 @@ def _participant_runtime_capabilities() -> ParticipantRuntimeCapabilities: supported_participant_roles=_PARTICIPANT_ROLES, supported_behavior_features=_PARTICIPANT_BEHAVIOR_FEATURES, supported_interaction_features=_PARTICIPANT_INTERACTION_FEATURES, - feature_support=tuple( + feature_support=( ParticipantFeatureSupport( - feature=feature, - support_level=ParticipantFeatureSupportLevel.UNSUPPORTED, - limitation_refs=(f"limitation:{feature}:not-realized",), - disclosure_refs=(f"disclosure:{feature}:unsupported",), - ) - for feature in sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + feature="participant_predicate_opacity", + support_level=ParticipantFeatureSupportLevel.BOUNDED, + constraint_refs=( + "profile:participant-opacity-runtime-reference-v1@sem-231/runtime-rev2", + "scope:finite-reference-runtime-carrier", + ), + limitation_refs=( + "limitation:participant-opacity:finite-profile-and-probe-set", + "limitation:participant-opacity:no-universal-proof-or-cross-backend-equivalence", + ), + disclosure_refs=( + "disclosure:participant-opacity:backend-native-reference-realization", + "disclosure:participant-opacity:logical-time-only", + ), + evidence_refs=( + "conformance:participant-opacity-backend:complete-transcript", + "tests:test_issue_965_participant_opacity_backend", + ), + ), + *( + ParticipantFeatureSupport( + feature=feature, + support_level=ParticipantFeatureSupportLevel.UNSUPPORTED, + limitation_refs=(f"limitation:{feature}:not-realized",), + disclosure_refs=(f"disclosure:{feature}:unsupported",), + ) + for feature in sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + ), ), ) diff --git a/implementations/python/packages/raes_reference_backend/participant_runtime.py b/implementations/python/packages/raes_reference_backend/participant_runtime.py index 866b4e3f8..85091313d 100644 --- a/implementations/python/packages/raes_reference_backend/participant_runtime.py +++ b/implementations/python/packages/raes_reference_backend/participant_runtime.py @@ -19,3 +19,44 @@ class ReferenceParticipantRuntime(BaseParticipantRuntime): domain side-effects are injected — this runtime is intended for reference and testing use where no real infrastructure is required. """ + + def participant_relation_probe( + self, + *, + relation_id: str, + profile_id: str, + profile_revision: str, + possible_point_ref: str, + ) -> dict[str, object]: + """Expose the closed reference observation transcript to conformance. + + This is an in-process backend probe, not a public control-plane route. + It accepts only the exact finite opacity profile and its two governed + points, then returns the complete non-secret observation normal form. + """ + + expected = ( + relation_id == "participant-predicate-opacity" + and profile_id == "participant-opacity-runtime-reference-v1" + and profile_revision == "sem-231/runtime-rev2" + and possible_point_ref + in { + "possible-point:runtime-reference-protected", + "possible-point:runtime-reference-complement", + } + ) + if not expected: + raise ValueError("unsupported participant relation probe coordinates") + return { + "decision": "deny", + "failure": "uniform-refusal", + "action_availability": "denied", + "delivery": "withheld", + "omission": "recorded-at-governed-opportunity", + "retry": "stable-replay", + "logical_timing": "logical-bucket:contained", + "logical_order": "stable-causal-order", + "policy_release_effect": "contained", + "external_effect": "none", + "payload_released": False, + } diff --git a/implementations/python/packages/raes_reference_backend/realization.py b/implementations/python/packages/raes_reference_backend/realization.py index f0d17b6f3..3ff30193d 100644 --- a/implementations/python/packages/raes_reference_backend/realization.py +++ b/implementations/python/packages/raes_reference_backend/realization.py @@ -15,7 +15,16 @@ from raes_backend_protocols.naming import provider_resource_name from raes_contracts.diagnostics import Diagnostic, Severity -from raes_contracts.planning import PlannedResource, ProvisioningPlan, RuntimeDomain +from raes_contracts.planning import ( + PlannedResource, + ProvisioningPlan, + RuntimeDomain, + planned_infrastructure_spec, + planned_node_source, + planned_node_spec, + planned_resource_authored_name, + planned_resource_payload, +) from .driver import ContainerSpec, NetworkSpec, ServiceSpec @@ -75,8 +84,8 @@ def interpret_provisioning_plan(plan: ProvisioningPlan) -> Realization: if resource.resource_type not in SUPPORTED_RESOURCE_TYPES: diagnostics.append(_unsupported_resource(resource)) continue - payload = resource.payload - if not isinstance(payload, Mapping): + payload = planned_resource_payload(resource) + if payload is None: diagnostics.append(_invalid_payload(resource)) continue if resource.resource_type == NETWORK_RESOURCE_TYPE: @@ -86,7 +95,7 @@ def interpret_provisioning_plan(plan: ProvisioningPlan) -> Realization: else: placement_resources.append((resource, payload)) - networks = [_network_spec(resource, payload) for resource, payload in network_resources] + networks = [_network_spec(resource) for resource, _ in network_resources] network_lookup = _network_address_lookup(networks) containers: list[ContainerSpec] = [] for resource, payload in node_resources: @@ -114,30 +123,22 @@ def _network_address_lookup(networks: list[NetworkSpec]) -> dict[str, str]: return lookup -def _resource_name(resource: PlannedResource, payload: Mapping[str, object]) -> str: - name = payload.get("name") or payload.get("node_name") - if isinstance(name, str) and name: +def _resource_name(resource: PlannedResource) -> str: + name = planned_resource_authored_name(resource) + if name is not None: return name return provider_resource_name(resource.address, prefix="raes") -def _infrastructure_spec(payload: Mapping[str, object]) -> Mapping[str, object]: - spec = payload.get("spec") - if not isinstance(spec, Mapping): - return {} - infrastructure = spec.get("infrastructure") - return infrastructure if isinstance(infrastructure, Mapping) else {} - - -def _network_spec(resource: PlannedResource, payload: Mapping[str, object]) -> NetworkSpec: - infrastructure = _infrastructure_spec(payload) +def _network_spec(resource: PlannedResource) -> NetworkSpec: + infrastructure = planned_infrastructure_spec(resource) or {} properties = infrastructure.get("properties") labels: dict[str, str] = {} if isinstance(properties, Mapping) and properties.get("internal") is True: labels["internal"] = "true" return NetworkSpec( address=resource.address, - name=_resource_name(resource, payload), + name=_resource_name(resource), labels=labels, ) @@ -147,7 +148,7 @@ def _container_spec( payload: Mapping[str, object], network_lookup: dict[str, str], ) -> tuple[ContainerSpec, tuple[Diagnostic, ...]]: - infrastructure = _infrastructure_spec(payload) + infrastructure = planned_infrastructure_spec(resource) or {} networks = infrastructure.get("networks") references: tuple[str, ...] = () if isinstance(networks, (list, tuple)): @@ -156,12 +157,12 @@ def _container_spec( # resource address; pass unresolved references through unchanged so the # contract stays total even when a node names a network not in this plan. network_addresses = tuple(network_lookup.get(ref, ref) for ref in references) - image_ref = _image_ref(payload) - services, diagnostics = _service_specs(resource, payload) + image_ref = _image_ref(resource, payload) + services, diagnostics = _service_specs(resource) return ( ContainerSpec( address=resource.address, - name=_resource_name(resource, payload), + name=_resource_name(resource), image_ref=image_ref, networks=network_addresses, services=services, @@ -204,10 +205,8 @@ def visit(address: str) -> None: def _service_specs( resource: PlannedResource, - payload: Mapping[str, object], ) -> tuple[tuple[ServiceSpec, ...], tuple[Diagnostic, ...]]: - spec = payload.get("spec") - node = spec.get("node") if isinstance(spec, Mapping) else None + node = planned_node_spec(resource) raw_services = node.get("services") if isinstance(node, Mapping) else None if raw_services is None: return (), () @@ -240,29 +239,15 @@ def _valid_service_port(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) and 1 <= value <= 65535 -def _image_ref(payload: Mapping[str, object]) -> str: - source = _node_source(payload) - if source: - return source - os_family = payload.get("os_family") - if isinstance(os_family, str) and os_family: - return f"raes-reference/{os_family}" - return "raes-reference/base" - - -def _node_source(payload: Mapping[str, object]) -> str | None: - """Return the authored container image source for a node, if any.""" - - spec = payload.get("spec") - node = spec.get("node") if isinstance(spec, Mapping) else None - source = node.get("source") if isinstance(node, Mapping) else None - if isinstance(source, str) and source: - return source +def _image_ref(resource: PlannedResource, payload: Mapping[str, object]) -> str: + source = planned_node_source(resource) if isinstance(source, Mapping): - name = source.get("name") - if isinstance(name, str) and name: - return name - return None + source = source.get("name") + image_ref = source if isinstance(source, str) and source else None + if image_ref is None: + os_family = payload.get("os_family") + image_ref = f"raes-reference/{os_family}" if isinstance(os_family, str) and os_family else "raes-reference/base" + return image_ref def _placement(resource: PlannedResource, payload: Mapping[str, object]) -> PlacementRealization: @@ -271,7 +256,7 @@ def _placement(resource: PlannedResource, payload: Mapping[str, object]) -> Plac return PlacementRealization( address=resource.address, resource_type=resource.resource_type, - name=_resource_name(resource, payload), + name=_resource_name(resource), target_address=target_address, ) diff --git a/implementations/python/packages/raes_runtime/apply_failure.py b/implementations/python/packages/raes_runtime/apply_failure.py index 4b3b1355a..9b2b496ad 100644 --- a/implementations/python/packages/raes_runtime/apply_failure.py +++ b/implementations/python/packages/raes_runtime/apply_failure.py @@ -1,5 +1,6 @@ """Apply failure synthesis and service rollback operations.""" +from raes_contracts.contracts import ParticipantInformationStateContextResolver from raes_contracts.diagnostics import Diagnostic from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot @@ -24,6 +25,8 @@ def maybe_synthesize_failure( def rollback_services( snapshot: RuntimeSnapshot, services: list[tuple[str, object]], + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, ) -> ApplyResult: """Stop started services in order and preserve every rollback diagnostic.""" @@ -37,6 +40,7 @@ def rollback_services( working_snapshot, address=address, snapshot=working_snapshot, + information_state_context_resolver=information_state_context_resolver, ) diagnostics.extend(stop_result.diagnostics) changed_addresses.extend(stop_result.changed_addresses) diff --git a/implementations/python/packages/raes_runtime/backend_calls.py b/implementations/python/packages/raes_runtime/backend_calls.py index 37cf6833e..a0bc69eac 100644 --- a/implementations/python/packages/raes_runtime/backend_calls.py +++ b/implementations/python/packages/raes_runtime/backend_calls.py @@ -9,6 +9,7 @@ from raes_backend_protocols.capabilities import BackendManifest from raes_contracts.addressing import require_compiled_address from raes_contracts.artifact_requirements import ArtifactAvailabilityContext +from raes_contracts.contracts import ParticipantInformationStateContextResolver from raes_contracts.contracts.time_model import validate_time_runtime_transition from raes_contracts.diagnostics import Diagnostic from raes_contracts.planning import ProvisioningPlan @@ -63,6 +64,7 @@ def _call_backend_apply( address: str, snapshot: RuntimeSnapshot, realization: _RealizationApplyContext | None = None, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, ) -> ApplyResult: realization_context = realization or _RealizationApplyContext() baseline_snapshot = deepcopy(snapshot) @@ -81,10 +83,8 @@ def _call_backend_apply( result, address=address, baseline_snapshot=baseline_snapshot, - realization_requirements=realization_context.requirements, - realization_plan=realization_context.plan, - backend_manifest=realization_context.manifest, - artifact_availability=realization_context.artifact_availability, + realization=realization_context, + information_state_context_resolver=information_state_context_resolver, ) @@ -93,10 +93,8 @@ def _finalize_backend_apply( *, address: str, baseline_snapshot: RuntimeSnapshot, - realization_requirements: tuple[CompiledRealizationRequirement, ...], - realization_plan: ProvisioningPlan | None, - backend_manifest: BackendManifest | None, - artifact_availability: ArtifactAvailabilityContext | None, + realization: _RealizationApplyContext, + information_state_context_resolver: ParticipantInformationStateContextResolver | None, ) -> ApplyResult: """Validate a backend's apply result and gate its realized snapshot. @@ -117,16 +115,17 @@ def _finalize_backend_apply( contract_diagnostics = _backend_snapshot_contract_diagnostics( result, baseline_snapshot, + information_state_context_resolver=information_state_context_resolver, ) realization_provenance: tuple[RealizationProvenanceEntry, ...] = () - if not contract_diagnostics and realization_requirements and realization_plan is not None: + if not contract_diagnostics and realization.requirements and realization.plan is not None: # SEM-218 I2 non-approximation gate + I5 provenance disclosure. contract_diagnostics, realization_provenance = realization_disclosure( - realization_requirements, - realization_plan, + realization.requirements, + realization.plan, result.snapshot, - manifest=backend_manifest, - artifact_availability=artifact_availability, + manifest=realization.manifest, + artifact_availability=realization.artifact_availability, ) if contract_diagnostics: finalized = ApplyResult( @@ -139,8 +138,8 @@ def _finalize_backend_apply( result, address=address, baseline_snapshot=baseline_snapshot, - realization_requirements=realization_requirements, - realization_plan=realization_plan, + realization_requirements=realization.requirements, + realization_plan=realization.plan, ) if realization_provenance and finalized.success: finalized = _with_realization_provenance(finalized, realization_provenance) @@ -150,12 +149,18 @@ def _finalize_backend_apply( def _backend_snapshot_contract_diagnostics( result: ApplyResult, baseline_snapshot: RuntimeSnapshot, + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None, ) -> list[Diagnostic]: diagnostics = _snapshot_address_contract_diagnostics(result.snapshot) if not diagnostics: diagnostics = _changed_address_transition_diagnostics(result, baseline_snapshot) if not diagnostics: - diagnostics = _snapshot_contract_diagnostics(result.snapshot) + diagnostics = _snapshot_contract_diagnostics( + result.snapshot, + information_state_context_resolver=information_state_context_resolver, + trusted_information_state_history=baseline_snapshot.information_state_history, + ) if not diagnostics: diagnostics = _snapshot_transition_contract_diagnostics(baseline_snapshot, result.snapshot) return diagnostics @@ -301,18 +306,28 @@ def _apply_result_details_violation(result: ApplyResult, address: str) -> str | return f"Backend method '{address}' returned ApplyResult.details as {type(result.details).__name__}; expected dict." -def _snapshot_contract_diagnostics(snapshot: RuntimeSnapshot) -> list[Diagnostic]: +def _snapshot_contract_diagnostics( + snapshot: RuntimeSnapshot, + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None, + trusted_information_state_history: dict[str, list[dict[str, object]]], +) -> list[Diagnostic]: checks = ( workflow_result_contract_diagnostics, evaluation_result_contract_diagnostics, proposition_truth_contract_diagnostics, - participant_runtime_state_contract_diagnostics, ) diagnostics: list[Diagnostic] = [] for check in checks: diagnostics = check(snapshot) if diagnostics: break + if not diagnostics: + diagnostics = participant_runtime_state_contract_diagnostics( + snapshot, + information_state_context_resolver=information_state_context_resolver, + trusted_information_state_history=trusted_information_state_history, + ) return diagnostics @@ -365,6 +380,8 @@ def _snapshot_carrier_addresses(snapshot: RuntimeSnapshot) -> set[str]: snapshot.participant_episode_history, snapshot.participant_behavior_history, snapshot.participant_control_history, + snapshot.participant_crossing_history, + snapshot.information_state_history, snapshot.participant_autonomous_execution_states, snapshot.participant_execution_services, snapshot.shared_state_records, diff --git a/implementations/python/packages/raes_runtime/control_plane.py b/implementations/python/packages/raes_runtime/control_plane.py index 58b7d3d10..a1a4c9584 100644 --- a/implementations/python/packages/raes_runtime/control_plane.py +++ b/implementations/python/packages/raes_runtime/control_plane.py @@ -12,6 +12,7 @@ from threading import RLock from uuid import uuid4 +from raes_contracts.contracts import ParticipantInformationStateContextResolver from raes_contracts.diagnostics import Diagnostic from raes_contracts.manifest_authority import PARTICIPANT_RUNTIME_POLICY_FEATURES from raes_contracts.planning import ( @@ -50,12 +51,13 @@ ParticipantCrossingPolicyResolver, validate_persisted_crossing_history, ) +from .participant_information_state_validation import require_participant_information_state_snapshot from .participant_retrieval import ParticipantRetrievalMixin -from .registry import RuntimeTarget +from .registry import RuntimeTarget as _RuntimeTarget def _require_crossing_policy_configuration( - target: RuntimeTarget, + target: _RuntimeTarget, resolver: ParticipantCrossingPolicyResolver | None, ) -> None: capabilities = target.manifest.participant_runtime @@ -77,12 +79,13 @@ class RuntimeControlPlane(WorkflowControlMixin, ParticipantControlMixin, Partici def __init__( self, - target: RuntimeTarget, + target: _RuntimeTarget, *, initial_snapshot: RuntimeSnapshot | None = None, store: ControlPlaneStore | None = None, behavior_specifications: Mapping[str, ParticipantBehaviorSpecificationRuntime] | None = None, crossing_policy_resolver: ParticipantCrossingPolicyResolver | None = None, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, ) -> None: _require_crossing_policy_configuration(target, crossing_policy_resolver) self._target = target @@ -91,7 +94,12 @@ def __init__( self._operations: dict[str, ControlPlaneOperationRecord] = self._store.load_records() self._behavior_specifications = dict(behavior_specifications or {}) self._crossing_policy_resolver = crossing_policy_resolver + self._information_state_context_resolver = information_state_context_resolver self._participant_control_lock = RLock() + require_participant_information_state_snapshot( + self._snapshot, + information_state_context_resolver, + ) if self._snapshot.participant_crossing_history: if crossing_policy_resolver is None: raise ValueError("persisted participant crossing history requires a policy resolver") 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 d90ab59e3..7102ad444 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api_models.py +++ b/implementations/python/packages/raes_runtime/control_plane_api_models.py @@ -165,6 +165,7 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "participant_behavior_history": dict(snapshot.participant_behavior_history), "participant_control_history": dict(snapshot.participant_control_history), "participant_crossing_history": dict(snapshot.participant_crossing_history), + "information_state_history": dict(snapshot.information_state_history), "participant_autonomous_execution_states": dict(snapshot.participant_autonomous_execution_states), "participant_execution_services": dict(snapshot.participant_execution_services), "participant_resource_budget_states": dict(snapshot.participant_resource_budget_states), @@ -194,6 +195,17 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop } for entry in snapshot.realization_provenance ], + "realization_observations": [ + { + "address": entry.address, + "field_path": entry.field_path, + "domain": entry.domain, + "requirement_kind": entry.requirement_kind, + "verification_scope": entry.verification_scope.value, + "observation_strength": entry.observation_strength.value, + } + for entry in snapshot.realization_observations + ], "realization_envelope": ( snapshot.realization_envelope.model_dump(mode="json") if snapshot.realization_envelope is not None diff --git a/implementations/python/packages/raes_runtime/control_plane_execution.py b/implementations/python/packages/raes_runtime/control_plane_execution.py index 7761e3da2..8ae6b8ddd 100644 --- a/implementations/python/packages/raes_runtime/control_plane_execution.py +++ b/implementations/python/packages/raes_runtime/control_plane_execution.py @@ -7,6 +7,7 @@ from datetime import UTC, datetime from uuid import uuid4 +from raes_contracts.contracts import ParticipantInformationStateContextResolver from raes_contracts.diagnostics import Diagnostic from raes_contracts.planning import RuntimeDomain from raes_contracts.runtime_state import ApplyResult, OperationReceipt, OperationState, OperationStatus, RuntimeSnapshot @@ -25,6 +26,7 @@ def apply_authorized_participant_action( request: object, snapshot: RuntimeSnapshot, address: str, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, ) -> ApplyResult: """Invoke and validate a participant backend after durable authorization.""" @@ -34,6 +36,7 @@ def apply_authorized_participant_action( snapshot, address=address, snapshot=snapshot, + information_state_context_resolver=information_state_context_resolver, ) @@ -113,6 +116,11 @@ def _execute_participant_action_locked( control_plane._snapshot, address=address, snapshot=control_plane._snapshot, + information_state_context_resolver=getattr( + control_plane, + "_information_state_context_resolver", + None, + ), ) control_plane._snapshot = result.snapshot control_plane._store.save_snapshot(control_plane._snapshot) @@ -231,6 +239,11 @@ def execute_operation( snapshot, address=request.address, snapshot=snapshot, + information_state_context_resolver=getattr( + control_plane, + "_information_state_context_resolver", + None, + ), ) control_plane._snapshot = result.snapshot control_plane._store.save_snapshot(control_plane._snapshot) diff --git a/implementations/python/packages/raes_runtime/control_plane_store.py b/implementations/python/packages/raes_runtime/control_plane_store.py index 57f4e5f87..df589ce30 100644 --- a/implementations/python/packages/raes_runtime/control_plane_store.py +++ b/implementations/python/packages/raes_runtime/control_plane_store.py @@ -2,13 +2,15 @@ from __future__ import annotations +import hashlib +import json import os from dataclasses import dataclass, field from enum import Enum from typing import TYPE_CHECKING, Any, Protocol from raes_contracts.artifact_requirements import ArtifactSatisfactionDisclosureModel -from raes_contracts.contracts import RealizationEnvelopeIdentityModel +from raes_contracts.contracts import RealizationEnvelopeIdentityModel, RealizationObservationDisclosureModel 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 @@ -17,11 +19,13 @@ ExplicitnessProvenance, OperationReceipt, OperationStatus, + RealizationObservationDisclosure, RealizationProvenanceEntry, RuntimeSnapshot, RuntimeSnapshotEnvelope, SnapshotEntry, ) +from raes_contracts.vocabulary import ObservationStrength, RealizationVerificationScope if TYPE_CHECKING: from .control_plane_store_local import LocalControlPlaneStore @@ -137,9 +141,11 @@ def _require_expected_control_head( def _participant_history_head(snapshot: RuntimeSnapshot, history_key: str) -> str | None: history_name, separator, participant_address = history_key.partition(":") histories = { + "participant_episode_history": snapshot.participant_episode_history, "participant_behavior_history": snapshot.participant_behavior_history, "participant_control_history": snapshot.participant_control_history, "participant_crossing_history": snapshot.participant_crossing_history, + "information_state_history": snapshot.information_state_history, } history = histories.get(history_name) if not separator or not participant_address or history is None: @@ -148,7 +154,10 @@ def _participant_history_head(snapshot: RuntimeSnapshot, history_key: str) -> st if not events: return None event_id = events[-1].get("event_id") - return event_id if isinstance(event_id, str) and event_id else None + if isinstance(event_id, str) and event_id: + return event_id + encoded = json.dumps(events[-1], sort_keys=True, separators=(",", ":"), default=str).encode() + return f"sha256:{hashlib.sha256(encoded).hexdigest()}" def _require_expected_history_heads( @@ -198,6 +207,10 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: participant_address: list(events) for participant_address, events in snapshot.participant_crossing_history.items() }, + "information_state_history": { + participant_address: list(records) + for participant_address, records in snapshot.information_state_history.items() + }, "participant_autonomous_execution_states": dict(snapshot.participant_autonomous_execution_states), "participant_execution_services": dict(snapshot.participant_execution_services), "participant_resource_budget_states": dict(snapshot.participant_resource_budget_states), @@ -229,6 +242,17 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: } for entry in snapshot.realization_provenance ], + "realization_observations": [ + { + "address": entry.address, + "field_path": entry.field_path, + "domain": entry.domain, + "requirement_kind": entry.requirement_kind, + "verification_scope": entry.verification_scope.value, + "observation_strength": entry.observation_strength.value, + } + for entry in snapshot.realization_observations + ], "realization_envelope": ( snapshot.realization_envelope.model_dump(mode="json") if snapshot.realization_envelope is not None else None ), @@ -277,6 +301,10 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: participant_address: list(events) for participant_address, events in payload.get("participant_crossing_history", {}).items() }, + information_state_history={ + participant_address: list(records) + for participant_address, records in payload.get("information_state_history", {}).items() + }, participant_autonomous_execution_states=dict(payload.get("participant_autonomous_execution_states", {})), participant_execution_services=dict(payload.get("participant_execution_services", {})), participant_resource_budget_states=dict(payload.get("participant_resource_budget_states", {})), @@ -313,6 +341,11 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: for item in payload.get("realization_provenance", []) if isinstance(item, dict) ), + realization_observations=tuple( + _realization_observation_from_payload(item) + for item in payload.get("realization_observations", []) + if isinstance(item, dict) + ), realization_envelope=( RealizationEnvelopeIdentityModel.model_validate(payload["realization_envelope"]) if payload.get("realization_envelope") is not None @@ -324,6 +357,18 @@ 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), + ) + + class InMemoryControlPlaneStore: """Simple in-memory store.""" diff --git a/implementations/python/packages/raes_runtime/control_plane_store_local.py b/implementations/python/packages/raes_runtime/control_plane_store_local.py index f0897783a..08ddd8007 100644 --- a/implementations/python/packages/raes_runtime/control_plane_store_local.py +++ b/implementations/python/packages/raes_runtime/control_plane_store_local.py @@ -192,6 +192,7 @@ def _participant_transition_count(snapshot: RuntimeSnapshot) -> int: for history in ( snapshot.participant_control_history, snapshot.participant_crossing_history, + snapshot.information_state_history, ) for events in history.values() ) diff --git a/implementations/python/packages/raes_runtime/manager.py b/implementations/python/packages/raes_runtime/manager.py index 25361730f..1e69f6e39 100644 --- a/implementations/python/packages/raes_runtime/manager.py +++ b/implementations/python/packages/raes_runtime/manager.py @@ -4,7 +4,10 @@ from dataclasses import dataclass from raes_contracts.artifact_requirements import ArtifactAvailabilityContext -from raes_contracts.contracts import ExperimentStochasticControlModel +from raes_contracts.contracts import ( + ExperimentStochasticControlModel, + ParticipantInformationStateContextResolver, +) from raes_contracts.contracts.time_model import TimeModelDeclarationModel from raes_contracts.diagnostics import Diagnostic from raes_contracts.planning import ChangeAction, ProvisioningPlan, ProvisionOp, RuntimeDomain @@ -18,14 +21,14 @@ from .diagnostics import _failure_diagnostic, _has_error_diagnostic from .participant_activity import resolve_participant_activity_controls from .participant_execution_control import RuntimeParticipantExecutionMixin -from .registry import RuntimeTarget, _validate_runtime_target_shape +from .participant_information_state_validation import require_participant_information_state_snapshot +from .registry import RuntimeTarget as _RuntimeTarget +from .registry import _validate_runtime_target_shape from .time_control import RuntimeTimeControlMixin -_RUNTIME_APPLY_ADDRESS = "runtime.apply" -_APPLY_EVALUATOR_ADDRESS = "runtime.apply.evaluator" +_RUNTIME_APPLY_ADDRESS, _APPLY_EVALUATOR_ADDRESS = "runtime.apply", "runtime.apply.evaluator" _APPLY_ORCHESTRATOR_ADDRESS = "runtime.apply.orchestrator" -_APPLY_PHASE_FAILED = "runtime.apply-phase-failed" -_DESTROY_PHASE_FAILED = "runtime.destroy-phase-failed" +_APPLY_PHASE_FAILED, _DESTROY_PHASE_FAILED = "runtime.apply-phase-failed", "runtime.destroy-phase-failed" _ROLLBACK_EVALUATOR_ADDRESS = "runtime.rollback.evaluator" _ROLLBACK_ORCHESTRATOR_ADDRESS = "runtime.rollback.orchestrator" @@ -41,7 +44,7 @@ class _RuntimeApplyState: def _provenance_diagnostics( execution_plan: ExecutionPlan, - target: RuntimeTarget, + target: _RuntimeTarget, snapshot: RuntimeSnapshot, ) -> list[Diagnostic]: diagnostics: list[Diagnostic] = [] @@ -88,10 +91,11 @@ class RuntimeManager(RuntimeParticipantExecutionMixin, RuntimeTimeControlMixin): def __init__( self, - target: RuntimeTarget, + target: _RuntimeTarget, *, initial_snapshot: RuntimeSnapshot | None = None, stochastic_controls: Iterable[ExperimentStochasticControlModel] = (), + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, ) -> None: _validate_runtime_target_shape( manifest=target.manifest, @@ -103,6 +107,8 @@ def __init__( ) self._target = target self._snapshot = initial_snapshot if initial_snapshot is not None else RuntimeSnapshot() + self._information_state_context_resolver = information_state_context_resolver + require_participant_information_state_snapshot(self._snapshot, information_state_context_resolver) self._participant_activity_controls = resolve_participant_activity_controls(stochastic_controls) self._time_declaration: TimeModelDeclarationModel | None = None self._initialize_participant_scheduler() @@ -180,7 +186,11 @@ def _rollback_failed_participant_start( services_to_rollback.append((_ROLLBACK_ORCHESTRATOR_ADDRESS, self._target.orchestrator)) if state.started_evaluator and self._target.evaluator is not None: services_to_rollback.append((_ROLLBACK_EVALUATOR_ADDRESS, self._target.evaluator)) - rollback_result = rollback_services(state.working_snapshot, services_to_rollback) + rollback_result = rollback_services( + state.working_snapshot, + services_to_rollback, + information_state_context_resolver=self._information_state_context_resolver, + ) self._record_phase_result(state, rollback_result) self._fail_apply_state(state) @@ -201,6 +211,7 @@ def _apply_provisioning_phase( manifest=execution_plan.manifest, artifact_availability=execution_plan.artifact_availability, ), + information_state_context_resolver=self._information_state_context_resolver, ) self._record_phase_result(state, provision_result) if not provision_result.success: @@ -225,6 +236,7 @@ def _apply_evaluation_phase( state.working_snapshot, address=_APPLY_EVALUATOR_ADDRESS, snapshot=state.working_snapshot, + information_state_context_resolver=self._information_state_context_resolver, ) self._record_phase_result(state, evaluation_result) if evaluation_result.success: @@ -240,6 +252,7 @@ def _apply_evaluation_phase( rollback_result = rollback_services( state.working_snapshot, [(_ROLLBACK_EVALUATOR_ADDRESS, self._target.evaluator)], + information_state_context_resolver=self._information_state_context_resolver, ) self._record_phase_result(state, rollback_result) self._fail_apply_state(state) @@ -256,6 +269,7 @@ def _apply_orchestration_phase( state.working_snapshot, address=_APPLY_ORCHESTRATOR_ADDRESS, snapshot=state.working_snapshot, + information_state_context_resolver=self._information_state_context_resolver, ) self._record_phase_result(state, orchestration_result) if not orchestration_result.success: @@ -271,7 +285,11 @@ def _apply_orchestration_phase( ] if state.started_evaluator and self._target.evaluator is not None: services_to_rollback.append((_ROLLBACK_EVALUATOR_ADDRESS, self._target.evaluator)) - rollback_result = rollback_services(state.working_snapshot, services_to_rollback) + rollback_result = rollback_services( + state.working_snapshot, + services_to_rollback, + information_state_context_resolver=self._information_state_context_resolver, + ) self._record_phase_result(state, rollback_result) self._fail_apply_state(state) @@ -399,6 +417,7 @@ def destroy(self) -> ApplyResult: working_snapshot, address="runtime.destroy.orchestrator", snapshot=working_snapshot, + information_state_context_resolver=self._information_state_context_resolver, ) diagnostics.extend(stop_result.diagnostics) changed_addresses.extend(stop_result.changed_addresses) @@ -419,6 +438,7 @@ def destroy(self) -> ApplyResult: working_snapshot, address="runtime.destroy.evaluator", snapshot=working_snapshot, + information_state_context_resolver=self._information_state_context_resolver, ) diagnostics.extend(stop_result.diagnostics) changed_addresses.extend(stop_result.changed_addresses) @@ -455,6 +475,7 @@ def destroy(self) -> ApplyResult: working_snapshot, address="runtime.destroy.provisioning", snapshot=working_snapshot, + information_state_context_resolver=self._information_state_context_resolver, ) diagnostics.extend(provision_result.diagnostics) changed_addresses.extend(provision_result.changed_addresses) diff --git a/implementations/python/packages/raes_runtime/operational_apparatus.py b/implementations/python/packages/raes_runtime/operational_apparatus.py index 6e07ad086..c134aa827 100644 --- a/implementations/python/packages/raes_runtime/operational_apparatus.py +++ b/implementations/python/packages/raes_runtime/operational_apparatus.py @@ -69,6 +69,7 @@ def _runtime_surface_summary(snapshot: RuntimeSnapshot) -> dict[str, int]: "participant_episode_history": _history_count(snapshot.participant_episode_history), "participant_behavior_history": _history_count(snapshot.participant_behavior_history), "participant_crossing_history": _history_count(snapshot.participant_crossing_history), + "information_state_history": _history_count(snapshot.information_state_history), "shared_state_records": len(snapshot.shared_state_records), "shared_state_history": _history_count(snapshot.shared_state_history), "joint_action_records": len(snapshot.joint_action_records), diff --git a/implementations/python/packages/raes_runtime/participant_control.py b/implementations/python/packages/raes_runtime/participant_control.py index e3a8ac955..b7fd6abdb 100644 --- a/implementations/python/packages/raes_runtime/participant_control.py +++ b/implementations/python/packages/raes_runtime/participant_control.py @@ -241,7 +241,14 @@ def control_participant_execution( ) return execute_participant_action( self, - method=backend_execution_control_method(method), + method=backend_execution_control_method( + method, + information_state_context_resolver=getattr( + self, + "_information_state_context_resolver", + None, + ), + ), request=request, address=(f"runtime.control-plane.participant-execution.{request.execution_scope_ref}.{request.action}"), idempotency_key=idempotency_key, diff --git a/implementations/python/packages/raes_runtime/participant_crossing_boundary.py b/implementations/python/packages/raes_runtime/participant_crossing_boundary.py index 057b13002..990a5d764 100644 --- a/implementations/python/packages/raes_runtime/participant_crossing_boundary.py +++ b/implementations/python/packages/raes_runtime/participant_crossing_boundary.py @@ -231,6 +231,11 @@ def execute_action_ingress_crossing( request=governed_request, snapshot=control_plane._snapshot, address=execution.address, + information_state_context_resolver=getattr( + control_plane, + "_information_state_context_resolver", + None, + ), ) next_snapshot = result.snapshot.with_entries( dict(result.snapshot.entries), diff --git a/implementations/python/packages/raes_runtime/participant_crossing_egress.py b/implementations/python/packages/raes_runtime/participant_crossing_egress.py index 66a9f2914..d586b85de 100644 --- a/implementations/python/packages/raes_runtime/participant_crossing_egress.py +++ b/implementations/python/packages/raes_runtime/participant_crossing_egress.py @@ -6,15 +6,20 @@ import json from dataclasses import dataclass, replace from typing import TypeVar, cast +from uuid import uuid4 from raes_contracts.contracts.base import ContractModel from raes_contracts.contracts.participant_crossing import ( ParticipantCrossingDirection, ParticipantCrossingInteractionKind, + ParticipantCrossingOccurrenceModel, ParticipantCrossingOperation, ParticipantCrossingSubjectKind, ParticipantCrossingSubjectReferenceModel, ) +from raes_contracts.contracts.participant_crossing_validation import ( + validate_participant_crossing_occurrence_context, +) from .participant_crossing_mediation import ( ParticipantCrossingEvidence, @@ -23,6 +28,7 @@ commit_prepared_crossing, prepare_participant_crossing, ) +from .participant_crossing_records import _expected_history_heads _ViewT = TypeVar("_ViewT", bound=ContractModel) @@ -104,6 +110,11 @@ def serialize_participant_view( raise ValueError("idempotent participant projection is missing its governed result") return type(view).model_validate(prepared.record.result_payload) if not prepared.record.receipt.accepted: + prepared = _with_opacity_egress_observation( + control_plane, + prepared, + delivered=False, + ) commit_prepared_crossing(control_plane, prepared) raise PermissionError("participant projection was not permitted") @@ -124,6 +135,11 @@ def serialize_participant_view( ) if actual != prepared.governed_subject: raise ValueError("trusted egress transformation does not match its governed identity") + prepared = _with_opacity_egress_observation( + control_plane, + prepared, + delivered=True, + ) prepared = cast( PreparedParticipantCrossing, replace( @@ -138,6 +154,160 @@ def serialize_participant_view( return governed +def _with_opacity_egress_observation( + control_plane: object, + prepared: PreparedParticipantCrossing, + *, + delivered: bool, +) -> PreparedParticipantCrossing: + """Append supported delivery/observation facts to the same atomic write.""" + + decision = prepared.decision + if decision is None or getattr(decision.occurrence, "opacity_enforcement", None) is None: + return prepared + participant_address = prepared.intent.participant_address + history = list(prepared.next_snapshot.participant_crossing_history.get(participant_address, ())) + if not history: + raise ValueError("opacity egress decision is missing its prepared crossing history") + predecessor = ParticipantCrossingOccurrenceModel.model_validate(history[-1]) + records = _egress_observation_records( + prepared, + predecessor, + delivered=delivered, + ) + candidate = [ + *history, + *(record.model_dump(mode="json", exclude_none=True) for record in records), + ] + next_snapshot = prepared.next_snapshot.with_entries( + dict(prepared.next_snapshot.entries), + participant_crossing_history={ + **prepared.next_snapshot.participant_crossing_history, + participant_address: candidate, + }, + ) + context = control_plane._crossing_policy_resolver.validation_context( + control_plane._snapshot, + participant_address, + ) + validate_participant_crossing_occurrence_context( + [ParticipantCrossingOccurrenceModel.model_validate(item) for item in candidate], + known_subjects=context.known_subjects, + policies=context.policies, + known_evidence_refs=context.known_evidence_refs, + known_authority_basis_refs=context.known_authority_basis_refs, + ) + return cast( + PreparedParticipantCrossing, + replace( + prepared, + next_snapshot=next_snapshot, + record=replace( + prepared.record, + result_history_heads=_expected_history_heads(next_snapshot, participant_address), + ), + ), + ) + + +def _egress_observation_records( + prepared: PreparedParticipantCrossing, + predecessor: ParticipantCrossingOccurrenceModel, + *, + delivered: bool, +) -> list[ParticipantCrossingOccurrenceModel]: + decision = prepared.decision + assert decision is not None + decision_detail = decision.occurrence + decision_id = getattr(decision_detail, "decision_id", None) + if not isinstance(decision_id, str): + raise ValueError("opacity egress decision is missing its decision identity") + previous_detail = predecessor.occurrence + policy = previous_detail.policy + next_order = previous_detail.effective_order + 1 + common = { + "direction": previous_detail.direction.value, + "interaction_kind": previous_detail.interaction_kind.value, + "audience_scope_ref": previous_detail.audience_scope_ref, + "subject": previous_detail.subject.model_dump(mode="json", exclude_none=True), + "controller_ref": previous_detail.controller_ref, + "authority_basis_refs": list(previous_detail.authority_basis_refs), + "policy": policy.model_dump(mode="json"), + "effective_order": next_order, + "order_model": previous_detail.order_model, + "backend_posture": previous_detail.backend_posture.value, + "loss_and_limitations": list(previous_detail.loss_and_limitations), + } + envelope = predecessor.model_dump( + mode="json", + exclude={"event_id", "predecessor_event_refs", "logical_order_ref", "occurrence"}, + exclude_none=True, + ) + envelope["logical_order_ref"] = f"{policy.decision_cut_ref}:order:{next_order}" + attempt_id = f"crossing-delivery-attempt.{uuid4()}" + attempt_event_id = f"crossing-occurrence.delivery-attempted.{uuid4()}" + owning_ref = previous_detail.subject.subject_ref + attempt = ParticipantCrossingOccurrenceModel.model_validate( + { + **envelope, + "event_id": attempt_event_id, + "predecessor_event_refs": [predecessor.event_id], + "occurrence": { + **common, + "stage": "delivery-attempted", + "decision_ref": decision_id, + "attempt_id": attempt_id, + "owning_occurrence_ref": owning_ref, + "disposition": "attempted" if delivered else "withheld", + }, + } + ) + if not delivered: + return [attempt] + delivery_order = next_order + 1 + delivery_id = f"crossing-delivery.{uuid4()}" + delivery_event_id = f"crossing-occurrence.delivered.{uuid4()}" + delivery = ParticipantCrossingOccurrenceModel.model_validate( + { + **envelope, + "event_id": delivery_event_id, + "logical_order_ref": f"{policy.decision_cut_ref}:order:{delivery_order}", + "predecessor_event_refs": [attempt_event_id], + "occurrence": { + **common, + "stage": "delivered", + "effective_order": delivery_order, + "decision_ref": decision_id, + "attempt_ref": attempt_id, + "delivery_id": delivery_id, + "owning_occurrence_ref": owning_ref, + "delivery_order": delivery_order, + "disposition": "delivered", + }, + } + ) + observation_order = delivery_order + 1 + observed = ParticipantCrossingOccurrenceModel.model_validate( + { + **envelope, + "event_id": f"crossing-occurrence.observed.{uuid4()}", + "logical_order_ref": f"{policy.decision_cut_ref}:order:{observation_order}", + "predecessor_event_refs": [delivery_event_id], + "occurrence": { + **common, + "stage": "observed", + "effective_order": observation_order, + "decision_ref": decision_id, + "delivery_ref": delivery_id, + "observation_id": f"crossing-observation.{uuid4()}", + "owning_observation_ref": owning_ref, + "observation_order": observation_order, + }, + } + ) + return [attempt, delivery, observed] + + def _view_subject( view: ContractModel, *, diff --git a/implementations/python/packages/raes_runtime/participant_crossing_mediation.py b/implementations/python/packages/raes_runtime/participant_crossing_mediation.py index 42963ae53..047861164 100644 --- a/implementations/python/packages/raes_runtime/participant_crossing_mediation.py +++ b/implementations/python/packages/raes_runtime/participant_crossing_mediation.py @@ -20,18 +20,26 @@ ParticipantCrossingOperation, ParticipantCrossingPolicyReferenceModel, ParticipantCrossingSubjectReferenceModel, + ParticipantOpacityRuntimeEnforcementBindingModel, + ParticipantOpacityRuntimeSupportModel, ) from raes_contracts.contracts.participant_crossing_validation import ( validate_participant_crossing_occurrence_context, ) from raes_contracts.contracts.participant_runtime import ParticipantRuntimeOrderingBasis from raes_contracts.diagnostics import Diagnostic +from raes_contracts.participant_opacity_runtime import validate_participant_opacity_runtime_enforcement from raes_contracts.planning import RuntimeDomain from raes_contracts.runtime_state import OperationReceipt, OperationState, OperationStatus, RuntimeSnapshot from raes_contracts.vocabulary import ParticipantFeatureSupportLevel from .control_plane_security import ControlPlaneIdentity, ParticipantAudienceSubjectBinding from .control_plane_store import AuditEvent, ControlPlaneOperationRecord +from .participant_opacity_enforcement import ( + bind_active_participant_opacity_support, + normalize_participant_opacity_resolution, + validate_persisted_participant_opacity, +) def _utc_now() -> str: @@ -98,6 +106,7 @@ class ParticipantCrossingPolicyResolution: downgrade_policy_ref: str | None = None downgrade_provenance_ref: str | None = None transformation: ParticipantCrossingTransformationResolution | None = None + opacity_enforcement: ParticipantOpacityRuntimeEnforcementBindingModel | None = None @dataclass(frozen=True) @@ -120,6 +129,7 @@ class ParticipantCrossingValidationContext: policies: tuple[ParticipantCrossingPolicyReferenceModel, ...] known_evidence_refs: frozenset[str] known_authority_basis_refs: frozenset[str] + opacity_enforcement_supports: tuple[ParticipantOpacityRuntimeSupportModel, ...] = () class ParticipantCrossingPolicyResolver(Protocol): @@ -168,6 +178,47 @@ class PreparedParticipantCrossing: existing_receipt: OperationReceipt | None = None +def _resolve_crossing_policy( + control_plane: object, + intent: ParticipantCrossingIntent, + resolver: ParticipantCrossingPolicyResolver, + incumbent_carrier: object | None, +) -> ParticipantCrossingPolicyResolution: + operation_resolver = getattr(resolver, "resolve_operation", None) + resolution = ( + operation_resolver(intent, control_plane._snapshot, incumbent_carrier) + if callable(operation_resolver) + else resolver.resolve(intent, control_plane._snapshot) + ) + context = resolver.validation_context( + control_plane._snapshot, + intent.participant_address, + ) + resolution = bind_active_participant_opacity_support( + resolution, + context.opacity_enforcement_supports, + ) + if resolution.opacity_enforcement is None: + return resolution + support = next( + ( + candidate + for candidate in context.opacity_enforcement_supports + if candidate.binding == resolution.opacity_enforcement + ), + None, + ) + if support is None: + raise ValueError("participant opacity runtime binding is not admitted by the resolver context") + validate_participant_opacity_runtime_enforcement( + resolution.opacity_enforcement, + support=support, + participant_address=intent.participant_address, + audience_scope_ref=intent.audience_scope_ref, + ) + return normalize_participant_opacity_resolution(resolution) + + def prepare_participant_crossing( control_plane: object, intent: ParticipantCrossingIntent, @@ -210,11 +261,11 @@ def prepare_participant_crossing( _require_replay_state_cut(existing, expected_heads) fingerprint_heads = existing.decision_history_heads try: - operation_resolver = getattr(resolver, "resolve_operation", None) - resolution = ( - operation_resolver(intent, control_plane._snapshot, incumbent_carrier) - if callable(operation_resolver) - else resolver.resolve(intent, control_plane._snapshot) + resolution = _resolve_crossing_policy( + control_plane, + intent, + resolver, + incumbent_carrier, ) except (TypeError, ValueError): return _prepare_policy_unresolved( @@ -430,6 +481,7 @@ def validate_persisted_crossing_history( known_evidence_refs=context.known_evidence_refs, known_authority_basis_refs=context.known_authority_basis_refs, ) + validate_persisted_participant_opacity(records, context) __all__ = ( diff --git a/implementations/python/packages/raes_runtime/participant_crossing_records.py b/implementations/python/packages/raes_runtime/participant_crossing_records.py index faf3e55b1..81c2308a4 100644 --- a/implementations/python/packages/raes_runtime/participant_crossing_records.py +++ b/implementations/python/packages/raes_runtime/participant_crossing_records.py @@ -38,6 +38,7 @@ _decision_gates, _resolve_backend_support, ) +from .participant_crossing_state_cut import expected_participant_history_heads as _expected_history_heads def _utc_now() -> str: @@ -56,6 +57,37 @@ class _CrossingDecisionPreparation: scoped_key: str +def _next_crossing_snapshot( + control_plane: object, + intent: ParticipantCrossingIntent, + records: list[ParticipantCrossingOccurrenceModel], +) -> RuntimeSnapshot: + history = list(control_plane._snapshot.participant_crossing_history.get(intent.participant_address, ())) + candidate_history = [ + *history, + *(record.model_dump(mode="json", exclude_none=True) for record in records), + ] + next_snapshot = control_plane._snapshot.with_entries( + dict(control_plane._snapshot.entries), + participant_crossing_history={ + **control_plane._snapshot.participant_crossing_history, + intent.participant_address: candidate_history, + }, + ) + context = control_plane._crossing_policy_resolver.validation_context( + control_plane._snapshot, + intent.participant_address, + ) + validate_participant_crossing_occurrence_context( + [ParticipantCrossingOccurrenceModel.model_validate(item) for item in candidate_history], + known_subjects=context.known_subjects, + policies=context.policies, + known_evidence_refs=context.known_evidence_refs, + known_authority_basis_refs=context.known_authority_basis_refs, + ) + return next_snapshot + + def _prepare_crossing_decision( control_plane: object, intent: ParticipantCrossingIntent, @@ -66,7 +98,6 @@ def _prepare_crossing_decision( support = preparation.support gates = preparation.gates disposition = preparation.disposition - history = list(control_plane._snapshot.participant_crossing_history.get(intent.participant_address, ())) request, decision = _crossing_records(intent, identity, resolution, support, gates, disposition) records = [request, decision] final_decision = decision @@ -107,25 +138,7 @@ def _prepare_crossing_decision( records.extend((fresh_request, fresh_decision)) final_decision = fresh_decision final_disposition = fresh_disposition - candidate_history = [*history, *(record.model_dump(mode="json") for record in records)] - next_snapshot = control_plane._snapshot.with_entries( - dict(control_plane._snapshot.entries), - participant_crossing_history={ - **control_plane._snapshot.participant_crossing_history, - intent.participant_address: candidate_history, - }, - ) - context = control_plane._crossing_policy_resolver.validation_context( - control_plane._snapshot, - intent.participant_address, - ) - validate_participant_crossing_occurrence_context( - [ParticipantCrossingOccurrenceModel.model_validate(item) for item in candidate_history], - known_subjects=context.known_subjects, - policies=context.policies, - known_evidence_refs=context.known_evidence_refs, - known_authority_basis_refs=context.known_authority_basis_refs, - ) + next_snapshot = _next_crossing_snapshot(control_plane, intent, records) record, audit_event = _operation_artifacts( intent, identity, @@ -215,6 +228,11 @@ def _crossing_records( "disposition": disposition.value, "reason_code": reason_code, "required_evidence_refs": list(intent.required_evidence_refs), + **( + {"opacity_enforcement": resolution.opacity_enforcement.model_dump(mode="json")} + if resolution.opacity_enforcement is not None + else {} + ), **( {"required_operation": resolution.required_operation.value} if disposition is ParticipantCrossingDecisionDisposition.TRANSFORM @@ -433,6 +451,11 @@ def _semantic_fingerprint( "allowed_downgrades": {key: value.value for key, value in sorted(resolution.allowed_downgrades.items())}, "backend": asdict(support), "transformation": (asdict(resolution.transformation) if resolution.transformation is not None else None), + "opacity_enforcement": ( + resolution.opacity_enforcement.model_dump(mode="json") + if resolution.opacity_enforcement is not None + else None + ), } encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str).encode() return hashlib.sha256(encoded).hexdigest() @@ -459,24 +482,6 @@ def _scoped_idempotency_key( return f"participant-crossing:{hashlib.sha256(encoded).hexdigest()}" -def _expected_history_heads( - snapshot: RuntimeSnapshot, - participant_address: str, -) -> dict[str, str | None]: - def head(history: dict[str, list[dict[str, object]]]) -> str | None: - events = history.get(participant_address, ()) - if not events: - return None - value = events[-1].get("event_id") - return value if isinstance(value, str) and value else None - - return { - f"participant_behavior_history:{participant_address}": head(snapshot.participant_behavior_history), - f"participant_control_history:{participant_address}": head(snapshot.participant_control_history), - f"participant_crossing_history:{participant_address}": head(snapshot.participant_crossing_history), - } - - __all__ = ( "_expected_history_heads", "_prepare_crossing_decision", diff --git a/implementations/python/packages/raes_runtime/participant_crossing_state_cut.py b/implementations/python/packages/raes_runtime/participant_crossing_state_cut.py new file mode 100644 index 000000000..8f8d68638 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_crossing_state_cut.py @@ -0,0 +1,35 @@ +"""Atomic participant-history state cuts for RUN-319 crossings.""" + +from __future__ import annotations + +import hashlib +import json + +from raes_contracts.runtime_state import RuntimeSnapshot + + +def expected_participant_history_heads( + snapshot: RuntimeSnapshot, + participant_address: str, +) -> dict[str, str | None]: + """Bind every retained participant history that may affect observation.""" + + def head(history: dict[str, list[dict[str, object]]]) -> str | None: + events = history.get(participant_address, ()) + if not events: + return None + value = events[-1].get("event_id") + if isinstance(value, str) and value: + return value + encoded = json.dumps(events[-1], sort_keys=True, separators=(",", ":"), default=str).encode() + return f"sha256:{hashlib.sha256(encoded).hexdigest()}" + + return { + f"participant_episode_history:{participant_address}": head(snapshot.participant_episode_history), + f"participant_behavior_history:{participant_address}": head(snapshot.participant_behavior_history), + f"participant_control_history:{participant_address}": head(snapshot.participant_control_history), + f"participant_crossing_history:{participant_address}": head(snapshot.participant_crossing_history), + } + + +__all__ = ["expected_participant_history_heads"] diff --git a/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py b/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py index 8fb118a6e..e836a3fb0 100644 --- a/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py +++ b/implementations/python/packages/raes_runtime/participant_execution_control_boundary.py @@ -4,6 +4,7 @@ from collections.abc import Callable +from raes_contracts.contracts import ParticipantInformationStateContextResolver from raes_contracts.contracts.participant_execution import ( ParticipantExecutionControlRequestModel, ParticipantExecutionServiceStateModel, @@ -152,6 +153,8 @@ def _validate_observed_result( def backend_execution_control_method( backend_method: Callable[..., object], + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, ) -> Callable[[ParticipantExecutionControlRequestModel, RuntimeSnapshot], ApplyResult]: """Wrap native control with generation preflight and observed-result checks.""" @@ -168,6 +171,7 @@ def apply( snapshot, address=f"runtime.participant-execution.{request.execution_scope_ref}.{request.action}", snapshot=snapshot, + information_state_context_resolver=information_state_context_resolver, ) return _validate_observed_result(request, snapshot, result) diff --git a/implementations/python/packages/raes_runtime/participant_information_state_validation.py b/implementations/python/packages/raes_runtime/participant_information_state_validation.py new file mode 100644 index 000000000..651ad8da9 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_information_state_validation.py @@ -0,0 +1,29 @@ +"""Production validation boundary for participant information-state snapshots.""" + +from __future__ import annotations + +from raes_contracts.contracts import ParticipantInformationStateContextResolver +from raes_contracts.participant_information_state_history import ( + iter_participant_information_state_snapshot_violations, +) +from raes_contracts.runtime_state import RuntimeSnapshot + + +def require_participant_information_state_snapshot( + snapshot: RuntimeSnapshot, + resolver: ParticipantInformationStateContextResolver | None, +) -> None: + """Fail closed when a persisted information-state history cannot be resolved.""" + + violations = list( + iter_participant_information_state_snapshot_violations( + snapshot.information_state_history, + information_state_context_resolver=resolver, + context_scope=snapshot, + ) + ) + if violations: + raise ValueError(violations[0][1]) + + +__all__ = ["require_participant_information_state_snapshot"] diff --git a/implementations/python/packages/raes_runtime/participant_opacity_enforcement.py b/implementations/python/packages/raes_runtime/participant_opacity_enforcement.py new file mode 100644 index 000000000..cad71b096 --- /dev/null +++ b/implementations/python/packages/raes_runtime/participant_opacity_enforcement.py @@ -0,0 +1,100 @@ +"""Runtime normalization for the bounded participant-opacity profile.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import TYPE_CHECKING, cast + +from raes_contracts.contracts.participant_crossing import ( + ParticipantCrossingGateDisposition, + ParticipantCrossingOccurrenceModel, +) + +if TYPE_CHECKING: + from raes_contracts.contracts.participant_crossing import ParticipantOpacityRuntimeSupportModel + + from .participant_crossing_mediation import ( + ParticipantCrossingPolicyResolution, + ParticipantCrossingValidationContext, + ) + + +def bind_active_participant_opacity_support( + resolution: ParticipantCrossingPolicyResolution, + supports: tuple[ParticipantOpacityRuntimeSupportModel, ...], +) -> ParticipantCrossingPolicyResolution: + """Make the trusted support index authoritative over route-local omission.""" + + if resolution.opacity_enforcement is not None or not supports: + return resolution + if len(supports) != 1: + raise ValueError("participant opacity runtime support is ambiguous at the exact state cut") + return cast( + "ParticipantCrossingPolicyResolution", + replace(resolution, opacity_enforcement=supports[0].binding), + ) + + +def normalize_participant_opacity_resolution( + resolution: ParticipantCrossingPolicyResolution, +) -> ParticipantCrossingPolicyResolution: + """Force the exact supported profile to one secret-independent result.""" + + from .participant_crossing_mediation import ParticipantCrossingSemanticGates + + denied = ParticipantCrossingGateDisposition.DENY + return cast( + "ParticipantCrossingPolicyResolution", + replace( + resolution, + gates=ParticipantCrossingSemanticGates( + participant_authority=denied, + action_admission=denied, + visibility=denied, + marking_authorization=denied, + declassification=denied, + transformation_validity=denied, + ), + reason_code="participant-opacity-contained", + required_operation=None, + allowed_downgrades={}, + downgrade_policy_ref=None, + downgrade_provenance_ref=None, + transformation=None, + ), + ) + + +def validate_persisted_participant_opacity( + records: list[ParticipantCrossingOccurrenceModel], + context: ParticipantCrossingValidationContext, +) -> None: + """Rejoin each compact durable binding to trusted restart support.""" + + from raes_contracts.participant_opacity_runtime import ( + validate_participant_opacity_runtime_enforcement, + ) + + for record in records: + binding = getattr(record.occurrence, "opacity_enforcement", None) + if binding is None: + continue + support = next( + (candidate for candidate in context.opacity_enforcement_supports if candidate.binding == binding), + None, + ) + if support is None: + raise ValueError("persisted participant opacity binding is not admitted by restart context") + validate_participant_opacity_runtime_enforcement( + binding, + support=support, + participant_address=record.participant_address, + audience_scope_ref=record.occurrence.audience_scope_ref, + ) + + +__all__ = [ + "bind_active_participant_opacity_support", + "normalize_participant_opacity_resolution", + "validate_persisted_participant_opacity", +] diff --git a/implementations/python/packages/raes_runtime/participant_result_contracts.py b/implementations/python/packages/raes_runtime/participant_result_contracts.py index c444d4726..05bf01369 100644 --- a/implementations/python/packages/raes_runtime/participant_result_contracts.py +++ b/implementations/python/packages/raes_runtime/participant_result_contracts.py @@ -2,6 +2,10 @@ from __future__ import annotations +from collections.abc import Mapping +from typing import Any + +from raes_contracts.contracts import ParticipantInformationStateContextResolver from raes_contracts.diagnostics import Diagnostic from raes_contracts.participant_behavior import ( iter_participant_behavior_snapshot_violations, @@ -20,6 +24,10 @@ iter_participant_crossing_history_transition_violations, ) from raes_contracts.participant_episode import iter_participant_episode_snapshot_violations +from raes_contracts.participant_information_state_history import ( + iter_participant_information_state_history_transition_violations, + iter_participant_information_state_snapshot_violations, +) from raes_contracts.participant_shared_state import ( iter_participant_shared_state_history_transition_violations, iter_participant_shared_state_snapshot_violations, @@ -51,6 +59,9 @@ def participant_episode_contract_diagnostics( def participant_runtime_state_contract_diagnostics( snapshot: RuntimeSnapshot, + *, + information_state_context_resolver: ParticipantInformationStateContextResolver | None = None, + trusted_information_state_history: Mapping[str, list[dict[str, Any]]] | None = None, ) -> list[Diagnostic]: """Validate RUN-305 participant state/history snapshot data. @@ -90,6 +101,12 @@ def participant_runtime_state_contract_diagnostics( *iter_participant_crossing_history_snapshot_violations( snapshot.participant_crossing_history, ), + *iter_participant_information_state_snapshot_violations( + snapshot.information_state_history, + information_state_context_resolver=information_state_context_resolver, + context_scope=snapshot, + trusted_history=trusted_information_state_history, + ), ] return [ _failure_diagnostic("runtime.backend-contract-invalid", address, message) for address, message in violations @@ -142,4 +159,11 @@ def participant_runtime_history_transition_diagnostics( next_snapshot.participant_crossing_history, ) ] + + [ + _failure_diagnostic("runtime.backend-contract-invalid", address, message) + for address, message in iter_participant_information_state_history_transition_violations( + previous_snapshot.information_state_history, + next_snapshot.information_state_history, + ) + ] ) diff --git a/implementations/python/packages/raes_runtime/time_control.py b/implementations/python/packages/raes_runtime/time_control.py index af2b7205d..01bb8fbb4 100644 --- a/implementations/python/packages/raes_runtime/time_control.py +++ b/implementations/python/packages/raes_runtime/time_control.py @@ -49,6 +49,7 @@ def _apply_time_phase( state.working_snapshot, address=_APPLY_TIME_ADDRESS, snapshot=state.working_snapshot, + information_state_context_resolver=self._information_state_context_resolver, ) self._record_phase_result(state, result) if result.success: @@ -134,6 +135,7 @@ def _apply_time_control_locked(self, method_name: str, *args: object) -> ApplyRe *method_args, address=f"runtime.time.{method_name}", snapshot=self._snapshot, + information_state_context_resolver=self._information_state_context_resolver, ) if result.success: result = self._validated_time_control_result(method_name, result, predecessor, args) diff --git a/implementations/python/pyproject.toml b/implementations/python/pyproject.toml index a85a3b653..e4ecb6e66 100644 --- a/implementations/python/pyproject.toml +++ b/implementations/python/pyproject.toml @@ -182,4 +182,6 @@ ignore = [ "packages/raes_runtime/control_plane_api/__init__.py" = ["F401"] # intentional facade re-exports (_receipt_response) "packages/raes_mcp/tools/authoring/__init__.py" = ["F401"] # intentional facade re-export (register) "packages/raes_mcp/tools/inspection/__init__.py" = ["F401"] # intentional facade re-export (register) +"packages/raes/semantics/objective_semantics/__init__.py" = ["F401"] # intentional facade re-exports +"packages/raes/semantics/participant_behavior/__init__.py" = ["F401"] # intentional facade re-exports "packages/raes_contracts/realization_envelope.py" = ["E402"] # late import breaks a carrier dependency cycle diff --git a/implementations/python/tests/sem233_boundary_flow_model.py b/implementations/python/tests/sem233_boundary_flow_model.py new file mode 100644 index 000000000..8ed6118ea --- /dev/null +++ b/implementations/python/tests/sem233_boundary_flow_model.py @@ -0,0 +1,400 @@ +"""Test-local finite model for the SEM-233 revision-1 algebra. + +This module is bounded falsification evidence. It is not a portable contract, +runtime policy implementation, model checker, or proof. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass, replace +from enum import StrEnum + + +class UnsupportedFlow(ValueError): + """The bounded model cannot resolve the requested flow operation.""" + + +class FlowOperation(StrEnum): + AUTHENTICATION = "authentication" + AUTHORIZATION = "authorization" + ADMISSION = "admission" + APPROVAL = "approval" + DECLASSIFICATION = "declassification" + ENDORSEMENT = "endorsement" + REDACTION = "redaction" + TRANSFORMATION = "transformation" + + +@dataclass(frozen=True) +class FlowLabel: + profile_id: str + profile_revision: str + confidentiality: frozenset[str] + integrity: frozenset[str] + + +@dataclass(frozen=True) +class FlowProfile: + profile_id: str + profile_revision: str + authority_revision: str + confidentiality_universe: frozenset[str] + integrity_universe: frozenset[str] + + def __post_init__(self) -> None: + if not self.profile_id or not self.profile_revision or not self.authority_revision: + raise UnsupportedFlow("profile coordinates must be non-empty") + if "conf:deny-unresolved" not in self.confidentiality_universe: + raise UnsupportedFlow("confidentiality universe must contain its deny-unresolved obligation") + if "int:deny-unresolved" not in self.integrity_universe: + raise UnsupportedFlow("integrity universe must contain its deny-unresolved obligation") + + def label( + self, + *, + confidentiality: Iterable[str] = (), + integrity: Iterable[str] = (), + ) -> FlowLabel: + confidentiality_set = frozenset(confidentiality) + integrity_set = frozenset(integrity) + unknown_confidentiality = confidentiality_set - self.confidentiality_universe + if unknown_confidentiality: + raise UnsupportedFlow( + f"obligations outside the closed confidentiality universe: {sorted(unknown_confidentiality)}" + ) + unknown_integrity = integrity_set - self.integrity_universe + if unknown_integrity: + raise UnsupportedFlow(f"obligations outside the closed integrity universe: {sorted(unknown_integrity)}") + return FlowLabel( + profile_id=self.profile_id, + profile_revision=self.profile_revision, + confidentiality=confidentiality_set, + integrity=integrity_set, + ) + + @property + def bottom(self) -> FlowLabel: + return self.label() + + @property + def top(self) -> FlowLabel: + return self.label( + confidentiality=self.confidentiality_universe, + integrity=self.integrity_universe, + ) + + @property + def unknown_label(self) -> FlowLabel: + return self.top + + +def _require_profile(profile: FlowProfile, label: FlowLabel) -> None: + if (label.profile_id, label.profile_revision) != (profile.profile_id, profile.profile_revision): + raise UnsupportedFlow("label and profile coordinates do not match") + if not label.confidentiality <= profile.confidentiality_universe: + raise UnsupportedFlow("label is outside the closed confidentiality universe") + if not label.integrity <= profile.integrity_universe: + raise UnsupportedFlow("label is outside the closed integrity universe") + + +def join_labels(profile: FlowProfile, labels: Iterable[FlowLabel]) -> FlowLabel: + confidentiality: set[str] = set() + integrity: set[str] = set() + for label in labels: + _require_profile(profile, label) + confidentiality.update(label.confidentiality) + integrity.update(label.integrity) + return profile.label(confidentiality=confidentiality, integrity=integrity) + + +def label_leq(left: FlowLabel, right: FlowLabel) -> bool: + if (left.profile_id, left.profile_revision) != (right.profile_id, right.profile_revision): + raise UnsupportedFlow("label profile coordinates do not match") + return left.confidentiality <= right.confidentiality and left.integrity <= right.integrity + + +@dataclass(frozen=True) +class CoordinateRewrite: + operation: FlowOperation + source_ref: str + result_ref: str + profile_id: str + profile_revision: str + policy_ref: str + policy_revision: str + removed_confidentiality: frozenset[str] + removed_integrity: frozenset[str] + authority_ref: str + sink_ref: str + state_cut_ref: str + + +@dataclass(frozen=True) +class FlowValue: + value_ref: str + label: FlowLabel | None + provenance_refs: frozenset[str] + influence_refs: frozenset[str] + participant_ref: str + episode_ref: str + policy_ref: str + policy_revision: str + state_cut_ref: str + supported: bool = True + rewrites: tuple[CoordinateRewrite, ...] = () + + def without_label(self) -> FlowValue: + return replace(self, label=None, supported=False) + + @property + def semantic_state(self) -> tuple[FlowLabel | None, frozenset[str], frozenset[str]]: + return (self.label, self.provenance_refs, self.influence_refs) + + +def derive( + profile: FlowProfile, + *, + result_ref: str, + inputs: Iterable[FlowValue], + participant_ref: str, + episode_ref: str, + policy_ref: str, + policy_revision: str, + state_cut_ref: str, +) -> FlowValue: + materialized_inputs = tuple(inputs) + supported = bool(materialized_inputs) and all( + value.supported and value.label is not None for value in materialized_inputs + ) + labels = tuple(value.label for value in materialized_inputs if value.label is not None) + label = join_labels(profile, labels) if supported else profile.unknown_label + provenance_refs = frozenset( + ref for value in materialized_inputs for ref in (*value.provenance_refs, f"derived-from:{value.value_ref}") + ) + influence_refs = frozenset( + ref for value in materialized_inputs for ref in (*value.influence_refs, f"possible-influence:{value.value_ref}") + ) + if not supported: + provenance_refs |= {"provenance:unresolved-label"} + influence_refs |= {"influence:unresolved-label"} + rewrites = tuple( + sorted( + {rewrite for value in materialized_inputs for rewrite in value.rewrites}, + key=lambda rewrite: ( + rewrite.state_cut_ref, + rewrite.result_ref, + rewrite.operation.value, + rewrite.authority_ref, + ), + ) + ) + return FlowValue( + value_ref=result_ref, + label=label, + provenance_refs=provenance_refs, + influence_refs=influence_refs, + participant_ref=participant_ref, + episode_ref=episode_ref, + policy_ref=policy_ref, + policy_revision=policy_revision, + state_cut_ref=state_cut_ref, + supported=supported, + rewrites=rewrites, + ) + + +def carry( + profile: FlowProfile, + source: FlowValue, + *, + result_ref: str, + participant_ref: str, + episode_ref: str, + policy_ref: str, + policy_revision: str, + state_cut_ref: str, +) -> FlowValue: + return derive( + profile, + result_ref=result_ref, + inputs=(source,), + participant_ref=participant_ref, + episode_ref=episode_ref, + policy_ref=policy_ref, + policy_revision=policy_revision, + state_cut_ref=state_cut_ref, + ) + + +def rewrite_coordinate( + profile: FlowProfile, + source: FlowValue, + *, + result_ref: str, + operation: FlowOperation, + remove_confidentiality: frozenset[str], + remove_integrity: frozenset[str], + authority_ref: str, + sink_ref: str, + state_cut_ref: str, +) -> FlowValue: + if source.label is None or not source.supported: + raise UnsupportedFlow("an unresolved source label cannot be rewritten") + _require_profile(profile, source.label) + if not result_ref or result_ref == source.value_ref: + raise UnsupportedFlow("a coordinate rewrite requires a fresh result identity") + if not authority_ref or not sink_ref or not state_cut_ref: + raise UnsupportedFlow("a coordinate rewrite requires exact authority, sink, and cut refs") + + confidentiality = source.label.confidentiality + integrity = source.label.integrity + if operation is FlowOperation.DECLASSIFICATION: + if remove_integrity: + raise UnsupportedFlow("declassification cannot rewrite the integrity coordinate") + if not remove_confidentiality <= confidentiality: + raise UnsupportedFlow("declassification names absent confidentiality obligations") + confidentiality -= remove_confidentiality + elif operation is FlowOperation.ENDORSEMENT: + if remove_confidentiality: + raise UnsupportedFlow("endorsement cannot rewrite the confidentiality coordinate") + if not remove_integrity <= integrity: + raise UnsupportedFlow("endorsement names absent integrity obligations") + integrity -= remove_integrity + else: + raise UnsupportedFlow(f"{operation.value} cannot rewrite flow coordinates") + + rewrite = CoordinateRewrite( + operation=operation, + source_ref=source.value_ref, + result_ref=result_ref, + profile_id=profile.profile_id, + profile_revision=profile.profile_revision, + policy_ref=source.policy_ref, + policy_revision=source.policy_revision, + removed_confidentiality=remove_confidentiality, + removed_integrity=remove_integrity, + authority_ref=authority_ref, + sink_ref=sink_ref, + state_cut_ref=state_cut_ref, + ) + return FlowValue( + value_ref=result_ref, + label=profile.label(confidentiality=confidentiality, integrity=integrity), + provenance_refs=source.provenance_refs | {source.value_ref, f"rewrite:{operation.value}:{authority_ref}"}, + influence_refs=source.influence_refs, + participant_ref=source.participant_ref, + episode_ref=source.episode_ref, + policy_ref=source.policy_ref, + policy_revision=source.policy_revision, + state_cut_ref=state_cut_ref, + supported=True, + rewrites=(*source.rewrites, rewrite), + ) + + +@dataclass(frozen=True) +class SinkPolicy: + sink_ref: str + destination_ref: str + profile_id: str + profile_revision: str + policy_ref: str + policy_revision: str + state_cut_ref: str + satisfied_confidentiality: frozenset[str] + satisfied_integrity: frozenset[str] + + +@dataclass(frozen=True) +class FlowGateState: + authenticated: bool + authorized: bool + admitted: bool + effective_capability: bool + crossing_valid: bool + fresh_history_heads: bool + + @classmethod + def allowing(cls) -> FlowGateState: + return cls( + authenticated=True, + authorized=True, + admitted=True, + effective_capability=True, + crossing_valid=True, + fresh_history_heads=True, + ) + + @classmethod + def gate_names(cls) -> tuple[str, ...]: + return ( + "authenticated", + "authorized", + "admitted", + "effective_capability", + "crossing_valid", + "fresh_history_heads", + ) + + def deny(self, gate_name: str) -> FlowGateState: + if gate_name not in self.gate_names(): + raise UnsupportedFlow(f"unknown final-sink gate {gate_name!r}") + return replace(self, **{gate_name: False}) + + @property + def all_allow(self) -> bool: + return all(getattr(self, gate_name) for gate_name in self.gate_names()) + + +def may_flow_at_sink( + profile: FlowProfile, + value: FlowValue, + sink: SinkPolicy, + gates: FlowGateState, +) -> bool: + if value.label is None or not value.supported: + return False + if not value.provenance_refs or not value.influence_refs: + return False + try: + _require_profile(profile, value.label) + except UnsupportedFlow: + return False + if (sink.profile_id, sink.profile_revision) != (profile.profile_id, profile.profile_revision): + return False + if (value.policy_ref, value.policy_revision, value.state_cut_ref) != ( + sink.policy_ref, + sink.policy_revision, + sink.state_cut_ref, + ): + return False + if not sink.satisfied_confidentiality <= profile.confidentiality_universe: + return False + if not sink.satisfied_integrity <= profile.integrity_universe: + return False + if any( + ( + rewrite.profile_id, + rewrite.profile_revision, + rewrite.policy_ref, + rewrite.policy_revision, + rewrite.sink_ref, + rewrite.state_cut_ref, + ) + != ( + sink.profile_id, + sink.profile_revision, + sink.policy_ref, + sink.policy_revision, + sink.sink_ref, + sink.state_cut_ref, + ) + for rewrite in value.rewrites + ): + return False + return ( + value.label.confidentiality <= sink.satisfied_confidentiality + and value.label.integrity <= sink.satisfied_integrity + and gates.all_allow + ) diff --git a/implementations/python/tests/test_act_604_dynamic_information_state.py b/implementations/python/tests/test_act_604_dynamic_information_state.py new file mode 100644 index 000000000..94a8898d7 --- /dev/null +++ b/implementations/python/tests/test_act_604_dynamic_information_state.py @@ -0,0 +1,950 @@ +"""ACT-604 dynamic participant information-state contract tests.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator +from pydantic import ValidationError +from raes_conformance.conformance import _semantic_diagnostics, validate_contract_payload +from raes_contracts.contracts import ( + ParticipantContextViewModel, + ParticipantDecisionSurfaceV2Model, + ParticipantInformationReconstructionProfileModel, + ParticipantInformationStateRecordModel, + ParticipantInformationStateSourceCoordinate, + ParticipantInformationStateValidationContext, + ParticipantObservationEnvelopeModel, + schema_bundle, + validate_participant_information_state_context, +) +from raes_contracts.runtime_state import ApplyResult, RuntimeSnapshot, RuntimeSnapshotEnvelope +from raes_runtime.backend_calls import _call_backend_apply +from raes_runtime.control_plane_api_models import _snapshot_model +from raes_runtime.control_plane_store import ( + _require_expected_history_heads, + _snapshot_from_payload, + _snapshot_payload, +) +from raes_runtime.control_plane_store_local import _participant_transition_count +from raes_runtime.operational_apparatus import _runtime_surface_summary +from raes_runtime.participant_result_contracts import ( + participant_runtime_history_transition_diagnostics, + participant_runtime_state_contract_diagnostics, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] +OBSERVATION_FIXTURE = ( + REPO_ROOT + / "contracts" + / "fixtures" + / "participant-runtime" + / "participant-observation-envelope-v1" + / "valid" + / "rl-observation-only.json" +) +CONTEXT_FIXTURE = ( + REPO_ROOT + / "contracts" + / "fixtures" + / "control-plane" + / "participant-context-view-v1" + / "valid" + / "network-posture-context.json" +) +INFORMATION_STATE_FIXTURE_ROOT = ( + REPO_ROOT / "contracts" / "fixtures" / "participant-runtime" / "participant-information-state-record-v1" +) +RECONSTRUCTION_PROFILE_FIXTURE_ROOT = ( + REPO_ROOT / "contracts" / "fixtures" / "profiles" / "participant-information-reconstruction-profile-v1" +) +DECISION_SURFACE_FIXTURE = ( + REPO_ROOT + / "contracts" + / "fixtures" + / "control-plane" + / "participant-decision-surface-v2" + / "valid" + / "projected-initial.json" +) +SOURCE_FIXTURES = { + "participant-context-view-v1": ( + REPO_ROOT + / "contracts" + / "fixtures" + / "control-plane" + / "participant-context-view-v1" + / "valid" + / "network-posture-context.json" + ), + "participant-shared-state-record-v1": ( + REPO_ROOT + / "contracts" + / "fixtures" + / "participant-runtime" + / "participant-shared-state-record-v1" + / "valid" + / "serialized-service-state-commit.json" + ), + "participant-behavior-history-event-stream-v1": ( + REPO_ROOT + / "contracts" + / "fixtures" + / "control-plane" + / "participant-behavior-history-event-stream-v1" + / "valid" + / "terminal-observation.json" + ), + "participant-episode-state-envelope-v1": ( + REPO_ROOT + / "contracts" + / "fixtures" + / "control-plane" + / "participant-episode-state-envelope-v1" + / "valid" + / "initialized.json" + ), +} + + +def _observation(**overrides: object) -> dict[str, object]: + payload = json.loads(OBSERVATION_FIXTURE.read_text(encoding="utf-8")) + payload.update(overrides) + return payload + + +def _profile(**overrides: object) -> dict[str, object]: + payload: dict[str, object] = { + "schema_version": "participant-information-reconstruction-profile/v1", + "profile_id": "occurrence-prefix-evidence-v1", + "title": "Occurrence-prefix evidence reconstruction", + "description": "Validates an exact participant-visible occurrence prefix and proof digest.", + "algorithm_id": "raes.occurrence-prefix-evidence", + "algorithm_version": "1.0.0", + "information_state_schema_version": "1.0.0", + "projection_version": "projection.blue.v1", + "determinism_basis": "exact_occurrence_prefix_and_proof_digest", + "accepted_input_contracts": ["participant-observation-envelope-v1"], + "accepted_order_semantics": ["sequence_prefix", "causal_frontier"], + "fixture_format": "participant-information-reconstruction-fixture/v1", + "proof_artifact_format": "participant-information-reconstruction-proof/v1", + "normative_artifact_ref": "specs/formal/participant-runtime/README.md", + "normative_artifact_digest": "sha256:" + "1" * 64, + } + payload.update(overrides) + return payload + + +def _information_state(**overrides: object) -> dict[str, object]: + payload = _observation( + event_id="information-state-blue-43", + schema_name="raes.participant_runtime.information_state", + schema_version="1.0.0", + event_type="participant_information_state", + ) + for field_name in ( + "observation_ref", + "phase_ref", + "delivery_basis", + "delivery_point_ref", + "delivered_at", + "action_observation_history_ref", + "hidden_state_refs", + "centralized_state_refs", + "loss_descriptor", + "stochastic_context", + "noise_model_ref", + "reconstruction_algorithm_ref", + "reconstruction_proof_ref", + "belief_support_ref", + "redacted_field_refs", + ): + payload.pop(field_name, None) + payload.update( + { + "information_state_ref": "information-state.blue.ep002.cut43", + "information_state_digest": "sha256:" + "a" * 64, + "payload_ref": "payloads.information-state.blue.ep002.cut43", + "state_cut": { + "cut_kind": "sequence_prefix", + "cut_ref": "cut.blue.ep002.43", + "history_domain": "participant_behavior_history", + "order_model": "backend_serialized_order", + "anchor_event_ref": "evt-blue-43", + "anchor_order": 43, + "history_prefix_length": 44, + "predecessor_event_refs": ["evt-blue-42"], + }, + "participant_memory_scope": "episode_local_reset", + "memory_reset_authority_ref": "episode-reset.blue.ep002", + "audience_scope_ref": "audiences.participant.blue", + "projection_version": "projection.blue.v1", + "projection_policy_revision": "projection.blue.v1", + "redaction_policy_revision": "redaction.blue.v1", + "information_guarantee": "history_consistent", + "source_refs": [ + { + "contract_id": "participant-observation-envelope-v1", + "ref": "observations.blue.local.telemetry.43", + "relation": "observed", + } + ], + "occurrence_history_ref": "history.blue.ep002.prefix43", + "reconstruction_profile_ref": "occurrence-prefix-evidence-v1", + "reconstruction_algorithm_id": "raes.occurrence-prefix-evidence", + "reconstruction_algorithm_version": "1.0.0", + "reconstruction_proof_ref": "proofs.information-state.blue.ep002.cut43", + "reconstructed_state_digest": "sha256:" + "a" * 64, + "occurrence_order_witness_ref": None, + "loss_disclosures": [], + "predecessor_information_state_refs": [], + "supersedes_information_state_ref": None, + } + ) + payload.update(overrides) + return payload + + +def _resolved_context( + record: ParticipantInformationStateRecordModel, + *, + observation: ParticipantObservationEnvelopeModel | None = None, + proof_digest: str | None = None, + source_cut_member: bool = True, +) -> ParticipantInformationStateValidationContext: + resolved_observation = observation or ParticipantObservationEnvelopeModel.model_validate( + _observation( + information_guarantee="history_consistent", + information_state_ref=record.information_state_ref, + reconstruction_algorithm_ref=record.reconstruction_profile_ref, + reconstruction_proof_ref=record.reconstruction_proof_ref, + ) + ) + source_key = ("participant-observation-envelope-v1", resolved_observation.observation_ref) + return ParticipantInformationStateValidationContext( + occurrence_histories={record.occurrence_history_ref: [resolved_observation]}, + resolved_sources={source_key: resolved_observation}, + source_coordinates={ + source_key: _source_coordinate( + record, + state_cut=( + record.state_cut + if source_cut_member + else record.state_cut.model_copy( + update={ + "anchor_order": record.state_cut.anchor_order - 1, + "history_prefix_length": record.state_cut.history_prefix_length - 1, + } + ) + ), + ) + }, + proof_digests={ + record.reconstruction_proof_ref: proof_digest or record.information_state_digest, + }, + ) + + +def _source_coordinate( + record: ParticipantInformationStateRecordModel, + **overrides: object, +) -> ParticipantInformationStateSourceCoordinate: + values: dict[str, object] = { + "participant_address": record.participant_address, + "episode_id": record.episode_id, + "state_cut": record.state_cut, + "audience_scope_ref": record.audience_scope_ref, + "visibility_projection_ref": record.visibility_projection_ref, + "projection_policy_revision": record.projection_policy_revision, + "redaction_policy_ref": record.redaction_policy_ref, + "redaction_policy_revision": record.redaction_policy_revision, + } + values.update(overrides) + return ParticipantInformationStateSourceCoordinate(**values) + + +def _context_resolver( + record: ParticipantInformationStateRecordModel, + _scope: object | None = None, +) -> ParticipantInformationStateValidationContext: + return _resolved_context(record) + + +@pytest.mark.parametrize("guarantee", ["history_consistent", "perfect_recall"]) +def test_strong_observation_guarantees_require_reconstruction_authority(guarantee: str) -> None: + payload = _observation( + information_guarantee=guarantee, + action_observation_history_ref=None, + information_state_ref=None, + reconstruction_algorithm_ref=None, + reconstruction_proof_ref=None, + ) + + with pytest.raises(ValidationError, match="strong information guarantee requires"): + ParticipantObservationEnvelopeModel.model_validate(payload) + assert list(Draft202012Validator(schema_bundle()["participant-observation-envelope-v1"]).iter_errors(payload)) + + +def test_lossy_observation_guarantee_requires_loss_disclosure() -> None: + payload = _observation( + information_guarantee="lossy_projection", + loss_descriptor=None, + ) + + with pytest.raises(ValidationError, match="lossy_projection requires loss_descriptor"): + ParticipantObservationEnvelopeModel.model_validate(payload) + + +def test_information_state_record_and_reconstruction_profile_are_closed_contracts() -> None: + record = ParticipantInformationStateRecordModel.model_validate(_information_state()) + profile = ParticipantInformationReconstructionProfileModel.model_validate(_profile()) + + assert record.information_state_ref == "information-state.blue.ep002.cut43" + assert profile.profile_id == "occurrence-prefix-evidence-v1" + + invalid_record = _information_state(untrusted_fact_bag={"hidden_truth": True}) + with pytest.raises(ValidationError): + ParticipantInformationStateRecordModel.model_validate(invalid_record) + + +def test_strong_information_state_requires_reconstruction_refs_and_matching_digest() -> None: + missing_profile = _information_state(reconstruction_profile_ref=None) + with pytest.raises(ValidationError, match="strong information state requires"): + ParticipantInformationStateRecordModel.model_validate(missing_profile) + assert list( + Draft202012Validator(schema_bundle()["participant-information-state-record-v1"]).iter_errors(missing_profile) + ) + + mismatched_digest = _information_state(reconstructed_state_digest="sha256:" + "b" * 64) + with pytest.raises(ValidationError, match="reconstructed_state_digest must equal"): + ParticipantInformationStateRecordModel.model_validate(mismatched_digest) + + +def test_perfect_recall_information_state_requires_occurrence_order_witness() -> None: + payload = _information_state(information_guarantee="perfect_recall") + with pytest.raises(ValidationError, match="perfect_recall requires occurrence_order_witness_ref"): + ParticipantInformationStateRecordModel.model_validate(payload) + + +def test_contextual_information_state_validation_resolves_profile_history_sources_and_proof() -> None: + record = ParticipantInformationStateRecordModel.model_validate(_information_state()) + profile = ParticipantInformationReconstructionProfileModel.model_validate(_profile()) + observation = ParticipantObservationEnvelopeModel.model_validate( + _observation( + information_guarantee="history_consistent", + information_state_ref=record.information_state_ref, + reconstruction_algorithm_ref=profile.profile_id, + reconstruction_proof_ref=record.reconstruction_proof_ref, + ) + ) + + validate_participant_information_state_context( + record, + reconstruction_profiles={profile.profile_id: profile}, + occurrence_histories={record.occurrence_history_ref: [observation]}, + resolved_sources={("participant-observation-envelope-v1", observation.observation_ref): observation}, + source_coordinates={ + ("participant-observation-envelope-v1", observation.observation_ref): _source_coordinate(record), + }, + proof_digests={ + record.reconstruction_proof_ref: record.information_state_digest, + }, + ) + + +def test_contextual_information_state_validation_rejects_projection_mismatch() -> None: + record = ParticipantInformationStateRecordModel.model_validate(_information_state()) + profile = ParticipantInformationReconstructionProfileModel.model_validate( + _profile(projection_version="projection.red.v1") + ) + + with pytest.raises(ValueError, match="projection version"): + validate_participant_information_state_context( + record, + reconstruction_profiles={profile.profile_id: profile}, + occurrence_histories={record.occurrence_history_ref: []}, + resolved_sources={}, + proof_digests={record.reconstruction_proof_ref: record.information_state_digest}, + ) + + +def test_observation_only_information_state_does_not_claim_reconstruction() -> None: + record = ParticipantInformationStateRecordModel.model_validate( + _information_state( + information_guarantee="observation_only", + occurrence_history_ref=None, + reconstruction_profile_ref=None, + reconstruction_algorithm_id=None, + reconstruction_algorithm_version=None, + reconstruction_proof_ref=None, + reconstructed_state_digest=None, + ) + ) + observation = ParticipantObservationEnvelopeModel.model_validate(_observation()) + + validate_participant_information_state_context( + record, + reconstruction_profiles={}, + occurrence_histories={}, + resolved_sources={("participant-observation-envelope-v1", observation.observation_ref): observation}, + proof_digests={}, + source_coordinates={ + ("participant-observation-envelope-v1", observation.observation_ref): _source_coordinate(record), + }, + ) + + +def test_every_typed_source_must_resolve_inside_the_exact_cut_and_policy_coordinate() -> None: + record = ParticipantInformationStateRecordModel.model_validate( + _information_state( + information_guarantee="observation_only", + occurrence_history_ref=None, + reconstruction_profile_ref=None, + reconstruction_algorithm_id=None, + reconstruction_algorithm_version=None, + reconstruction_proof_ref=None, + reconstructed_state_digest=None, + ) + ) + after_cut = ParticipantObservationEnvelopeModel.model_validate(_observation(sequence_number=44)) + source_key = ("participant-observation-envelope-v1", after_cut.observation_ref) + source_coordinate = _source_coordinate(record) + + with pytest.raises(ValueError, match="exact sequence cut"): + validate_participant_information_state_context( + record, + reconstruction_profiles={}, + occurrence_histories={}, + resolved_sources={source_key: after_cut}, + source_coordinates={source_key: source_coordinate}, + proof_digests={}, + ) + + cut_observation = after_cut.model_copy(update={"sequence_number": 43}) + earlier_cut = record.state_cut.model_copy(update={"anchor_order": 42, "history_prefix_length": 43}) + cut_coordinate = _source_coordinate(record, state_cut=earlier_cut) + with pytest.raises(ValueError, match="cut membership"): + validate_participant_information_state_context( + record, + reconstruction_profiles={}, + occurrence_histories={}, + resolved_sources={source_key: cut_observation}, + source_coordinates={source_key: cut_coordinate}, + proof_digests={}, + ) + + wrong_policy = after_cut.model_copy( + update={ + "sequence_number": 43, + "redaction_policy_ref": "redaction.other.v1", + } + ) + with pytest.raises(ValueError, match="redaction policy"): + validate_participant_information_state_context( + record, + reconstruction_profiles={}, + occurrence_histories={}, + resolved_sources={source_key: wrong_policy}, + source_coordinates={source_key: source_coordinate}, + proof_digests={}, + ) + + +@pytest.mark.parametrize( + ("coordinate_field", "wrong_value", "error"), + [ + ("participant_address", "participants.other", "participant or episode coordinate"), + ("episode_id", "episode-other", "participant or episode coordinate"), + ("audience_scope_ref", "audiences.other", "audience scope coordinate"), + ("visibility_projection_ref", "projection.other", "visibility projection coordinate"), + ("projection_policy_revision", "projection-policy.other", "projection policy revision coordinate"), + ("redaction_policy_ref", "redaction.other", "redaction policy coordinate"), + ("redaction_policy_revision", "redaction-revision.other", "redaction policy revision coordinate"), + ], +) +def test_source_governed_coordinate_rejects_each_identity_and_policy_mismatch( + coordinate_field: str, + wrong_value: str, + error: str, +) -> None: + record = ParticipantInformationStateRecordModel.model_validate( + _information_state( + information_guarantee="observation_only", + occurrence_history_ref=None, + reconstruction_profile_ref=None, + reconstruction_algorithm_id=None, + reconstruction_algorithm_version=None, + reconstruction_proof_ref=None, + reconstructed_state_digest=None, + ) + ) + observation = ParticipantObservationEnvelopeModel.model_validate(_observation(sequence_number=43)) + source_key = ("participant-observation-envelope-v1", observation.observation_ref) + mismatched_coordinate = _source_coordinate(record, **{coordinate_field: wrong_value}) + + with pytest.raises(ValueError, match=error): + validate_participant_information_state_context( + record, + reconstruction_profiles={}, + occurrence_histories={}, + resolved_sources={source_key: observation}, + source_coordinates={source_key: mismatched_coordinate}, + proof_digests={}, + ) + + +@pytest.mark.parametrize( + ("contract_id", "source_ref", "relation", "error"), + [ + ( + "participant-context-view-v1", + "views.context.participants.blue.rl.network-posture.0001", + "derived", + "context-view source coordinate", + ), + ( + "participant-shared-state-record-v1", + "state-web01-http-rev8", + "observed", + "shared-state source coordinate", + ), + ( + "participant-behavior-history-event-stream-v1", + "history.participant.alice.ep-0001", + "observed", + "behavior-history source coordinate", + ), + ( + "participant-episode-state-envelope-v1", + "ep-0001", + "derived", + "episode-state source coordinate", + ), + ], +) +def test_each_typed_source_validator_rejects_cross_participant_resolution( + contract_id: str, + source_ref: str, + relation: str, + error: str, +) -> None: + record = ParticipantInformationStateRecordModel.model_validate( + _information_state( + information_guarantee="observation_only", + source_refs=[{"contract_id": contract_id, "ref": source_ref, "relation": relation}], + occurrence_history_ref=None, + reconstruction_profile_ref=None, + reconstruction_algorithm_id=None, + reconstruction_algorithm_version=None, + reconstruction_proof_ref=None, + reconstructed_state_digest=None, + ) + ) + resolved = json.loads(SOURCE_FIXTURES[contract_id].read_text(encoding="utf-8")) + if isinstance(resolved, list): + resolved[0]["participant_address"] = "participants.other" + else: + resolved["participant_address"] = "participants.other" + source_key = (contract_id, source_ref) + source_coordinate = _source_coordinate(record) + + with pytest.raises(ValueError, match=error): + validate_participant_information_state_context( + record, + reconstruction_profiles={}, + occurrence_histories={}, + resolved_sources={source_key: resolved}, + source_coordinates={source_key: source_coordinate}, + proof_digests={}, + ) + + +def test_strong_information_state_cannot_collapse_distinct_occurrences() -> None: + record = ParticipantInformationStateRecordModel.model_validate(_information_state()) + profile = ParticipantInformationReconstructionProfileModel.model_validate(_profile()) + first = ParticipantObservationEnvelopeModel.model_validate( + _observation( + information_guarantee="history_consistent", + information_state_ref=record.information_state_ref, + reconstruction_algorithm_ref=profile.profile_id, + reconstruction_proof_ref=record.reconstruction_proof_ref, + ) + ) + second = ParticipantObservationEnvelopeModel.model_validate( + _observation( + event_id="obs-blue-42-contradictory", + observation_ref="observations.blue.local.telemetry.42-contradictory", + sequence_number=42, + information_guarantee="history_consistent", + information_state_ref=record.information_state_ref, + reconstruction_algorithm_ref=profile.profile_id, + reconstruction_proof_ref=record.reconstruction_proof_ref, + ) + ) + + with pytest.raises(ValueError, match="every occurrence"): + validate_participant_information_state_context( + record, + reconstruction_profiles={profile.profile_id: profile}, + occurrence_histories={record.occurrence_history_ref: [first, second]}, + resolved_sources={ + ("participant-observation-envelope-v1", first.observation_ref): first, + ("participant-observation-envelope-v1", second.observation_ref): second, + }, + proof_digests={record.reconstruction_proof_ref: record.information_state_digest}, + ) + + +@pytest.mark.parametrize( + ("coordinate_field", "wrong_value", "error"), + [ + ("participant_address", "participants.other", "identity"), + ("episode_id", "episode-other", "identity"), + ("state_cut", None, "cut"), + ("audience_scope_ref", "audiences.other", "audience scope"), + ("projection_policy_revision", "projection-policy.other", "projection revision"), + ("visibility_projection_ref", "projection.other", "visibility projection"), + ("participant_memory_scope", "episode_local_reset", "memory scope"), + ("memory_reset_authority_ref", "episode-reset.other", "reset authority"), + ("redaction_policy_ref", "redaction.other", "redaction policy"), + ], +) +def test_decision_surface_information_state_join_enforces_every_coordinate( + coordinate_field: str, + wrong_value: str | None, + error: str, +) -> None: + surface = ParticipantDecisionSurfaceV2Model.model_validate_json( + DECISION_SURFACE_FIXTURE.read_text(encoding="utf-8") + ) + view = surface.participant_view + assurance = surface.assurance + history_ref = "history.red.episode-1.initial" + observation_ref = "observations.red.initial" + record = ParticipantInformationStateRecordModel.model_validate( + _information_state( + participant_address=view.participant_address, + episode_id=view.episode_id, + information_state_ref=view.information_state_ref, + state_cut=assurance.derivation_anchor.state_cut.model_dump(mode="json"), + participant_memory_scope=assurance.participant_memory_scope, + memory_reset_authority_ref=assurance.memory_reset_authority_ref, + audience_scope_ref=assurance.audience_scope_ref, + visibility_projection_ref=assurance.visibility_projection_ref, + redaction_policy_ref=view.redaction_policy_ref, + projection_policy_revision=assurance.projection_policy_revision, + source_refs=[ + { + "contract_id": "participant-observation-envelope-v1", + "ref": observation_ref, + "relation": "observed", + } + ], + occurrence_history_ref=history_ref, + ) + ) + profile = ParticipantInformationReconstructionProfileModel.model_validate(_profile()) + observation = ParticipantObservationEnvelopeModel.model_validate( + _observation( + participant_address=view.participant_address, + episode_id=view.episode_id, + sequence_number=1, + observation_ref=observation_ref, + action_observation_history_ref=history_ref, + visibility_projection_ref=assurance.visibility_projection_ref, + redaction_policy_ref=view.redaction_policy_ref, + information_guarantee="history_consistent", + information_state_ref=record.information_state_ref, + reconstruction_algorithm_ref=profile.profile_id, + reconstruction_proof_ref=record.reconstruction_proof_ref, + ) + ) + context = { + "reconstruction_profiles": {profile.profile_id: profile}, + "occurrence_histories": {history_ref: [observation]}, + "resolved_sources": {("participant-observation-envelope-v1", observation_ref): observation}, + "source_coordinates": {("participant-observation-envelope-v1", observation_ref): _source_coordinate(record)}, + "proof_digests": {record.reconstruction_proof_ref: record.information_state_digest}, + "decision_surfaces": [surface], + } + + validate_participant_information_state_context(record, **context) + + if coordinate_field in {"participant_address", "episode_id", "redaction_policy_ref"}: + mismatched_surface = surface.model_copy( + update={"participant_view": view.model_copy(update={coordinate_field: wrong_value})} + ) + elif coordinate_field == "state_cut": + mismatched_cut = assurance.derivation_anchor.state_cut.model_copy( + update={"anchor_order": assurance.derivation_anchor.state_cut.anchor_order + 1} + ) + mismatched_anchor = assurance.derivation_anchor.model_copy(update={"state_cut": mismatched_cut}) + mismatched_surface = surface.model_copy( + update={"assurance": assurance.model_copy(update={"derivation_anchor": mismatched_anchor})} + ) + else: + mismatched_surface = surface.model_copy( + update={"assurance": assurance.model_copy(update={coordinate_field: wrong_value})} + ) + mismatched_context = dict(context) + mismatched_context["decision_surfaces"] = [mismatched_surface] + + with pytest.raises(ValueError, match=error): + validate_participant_information_state_context(record, **mismatched_context) + + +def test_schema_bundle_and_conformance_publish_information_state_contracts() -> None: + bundle = schema_bundle() + + assert "participant-information-state-record-v1" in bundle + assert "participant-information-reconstruction-profile-v1" in bundle + assert validate_contract_payload("participant-information-state-record-v1", _information_state()) + assert not validate_contract_payload( + "participant-information-state-record-v1", + _information_state(), + information_state_context_resolver=_context_resolver, + ) + assert not validate_contract_payload("participant-information-reconstruction-profile-v1", _profile()) + + +def test_context_view_accepts_information_state_as_a_closed_source_layer() -> None: + payload = json.loads(CONTEXT_FIXTURE.read_text(encoding="utf-8")) + source = payload["source_layers"][0] + prior_ref = source["ref"] + source["source_layer"] = "participant_information_state" + source["ref"] = "information-state.blue.ep002.cut43" + payload["derived_from_refs"] = [ + "information-state.blue.ep002.cut43" if ref == prior_ref else ref for ref in payload["derived_from_refs"] + ] + + model = ParticipantContextViewModel.model_validate(payload) + + assert model.source_layers[0].source_layer == "participant_information_state" + + +def test_reconstruction_profile_loader_is_closed_and_path_safe() -> None: + from raes_contracts.participant_information_reconstruction_profiles import ( + load_participant_information_reconstruction_profile, + participant_information_reconstruction_profile_path, + ) + + profile = load_participant_information_reconstruction_profile("occurrence-prefix-evidence-v1") + + assert profile.profile_id == "occurrence-prefix-evidence-v1" + assert participant_information_reconstruction_profile_path(profile.profile_id).is_file() + with pytest.raises(ValueError, match="portable SDL identifier"): + participant_information_reconstruction_profile_path("../../outside") + with pytest.raises(ValueError, match="unsupported participant information reconstruction profile"): + participant_information_reconstruction_profile_path("unknown-profile-v1") + + +def test_runtime_snapshot_round_trips_first_class_information_state_history() -> None: + record = _information_state() + snapshot = RuntimeSnapshot( + information_state_history={record["participant_address"]: [record]}, + ) + + payload = _snapshot_payload(snapshot) + restored = _snapshot_from_payload(payload) + + assert payload["information_state_history"][record["participant_address"]][0] == record + assert restored.information_state_history == snapshot.information_state_history + assert not validate_contract_payload("runtime-snapshot-v1", payload) + + +def test_information_state_history_participates_in_expected_heads_and_restart_count() -> None: + record = _information_state() + participant = str(record["participant_address"]) + snapshot = RuntimeSnapshot(information_state_history={participant: [record]}) + + _require_expected_history_heads( + snapshot, + {f"information_state_history:{participant}": str(record["event_id"])}, + ) + + assert _participant_transition_count(snapshot) == 1 + + +def test_information_state_snapshot_semantics_reject_key_mismatch_and_metadata_smuggling() -> None: + record = _information_state() + mismatched = RuntimeSnapshot(information_state_history={"participants.red.llm": [record]}) + smuggled = RuntimeSnapshot(metadata={"information_state_history": {"hidden": [record]}}) + + mismatch_messages = [ + diagnostic.message for diagnostic in participant_runtime_state_contract_diagnostics(mismatched) + ] + smuggling_messages = [diagnostic.message for diagnostic in participant_runtime_state_contract_diagnostics(smuggled)] + + assert any("map key" in message and "participant_address" in message for message in mismatch_messages) + assert any("must not contain 'information_state_history'" in message for message in smuggling_messages) + + +def test_information_state_history_is_append_only_across_backend_apply() -> None: + original = _information_state() + rewritten = _information_state( + information_state_digest="sha256:" + "b" * 64, + reconstructed_state_digest="sha256:" + "b" * 64, + ) + participant = str(original["participant_address"]) + base_snapshot = RuntimeSnapshot(information_state_history={participant: [original]}) + + def _backend_apply(_request: object, snapshot: RuntimeSnapshot) -> ApplyResult: + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + information_state_history={participant: [rewritten]}, + ), + changed_addresses=[participant], + ) + + result = _call_backend_apply( + _backend_apply, + object(), + base_snapshot, + address="runtime.participant-information-state", + snapshot=base_snapshot, + ) + + assert result.success is False + assert any("information_state_history must be append-only" in item.message for item in result.diagnostics) + + +def test_information_state_history_transition_rejects_removal() -> None: + record = _information_state() + participant = str(record["participant_address"]) + previous = RuntimeSnapshot(information_state_history={participant: [record]}) + + diagnostics = participant_runtime_history_transition_diagnostics(previous, RuntimeSnapshot()) + + assert any("information-state history was removed" in item.message for item in diagnostics) + + +@pytest.mark.parametrize( + ("fixture_root", "model_type"), + [ + (INFORMATION_STATE_FIXTURE_ROOT, ParticipantInformationStateRecordModel), + (RECONSTRUCTION_PROFILE_FIXTURE_ROOT, ParticipantInformationReconstructionProfileModel), + ], +) +def test_information_state_contract_fixture_corpora( + fixture_root: Path, + model_type: type[ParticipantInformationStateRecordModel] | type[ParticipantInformationReconstructionProfileModel], +) -> None: + valid_paths = sorted((fixture_root / "valid").glob("*.json")) + invalid_paths = sorted((fixture_root / "invalid").glob("*.json")) + + assert valid_paths + assert invalid_paths + for path in valid_paths: + model_type.model_validate_json(path.read_text(encoding="utf-8")) + for path in invalid_paths: + invalid_payload = path.read_text(encoding="utf-8") + with pytest.raises(ValidationError): + model_type.model_validate_json(invalid_payload) + + +def test_runtime_snapshot_conformance_preserves_information_state_semantics() -> None: + record = _information_state(predecessor_information_state_refs=["information-state.blue.ep002.missing"]) + participant = str(record["participant_address"]) + payload = _snapshot_payload(RuntimeSnapshot(information_state_history={participant: [record]})) + + diagnostics = _semantic_diagnostics("runtime-snapshot-v1", payload) + + assert any("predecessor_information_state_refs" in item.message for item in diagnostics) + + +def test_conformance_and_runtime_snapshot_acceptance_fail_closed_without_context() -> None: + record = ParticipantInformationStateRecordModel.model_validate(_information_state()) + participant = str(record.participant_address) + snapshot = RuntimeSnapshot( + information_state_history={participant: [record.model_dump(mode="json")]}, + ) + payload = _snapshot_payload(snapshot) + + conformance = _semantic_diagnostics("runtime-snapshot-v1", payload) + runtime = participant_runtime_state_contract_diagnostics(snapshot) + + assert any("context resolver is required" in item.message for item in conformance) + assert any("context resolver is required" in item.message for item in runtime) + assert not _semantic_diagnostics( + "runtime-snapshot-v1", + payload, + information_state_context_resolver=_context_resolver, + ) + assert not participant_runtime_state_contract_diagnostics( + snapshot, + information_state_context_resolver=_context_resolver, + ) + + +def test_backend_ingestion_rejects_unresolved_or_forged_new_information_state() -> None: + record = ParticipantInformationStateRecordModel.model_validate(_information_state()) + participant = str(record.participant_address) + + def _backend_apply(_request: object, snapshot: RuntimeSnapshot) -> ApplyResult: + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + information_state_history={participant: [record.model_dump(mode="json")]}, + ), + changed_addresses=[participant], + ) + + without_context = _call_backend_apply( + _backend_apply, + object(), + RuntimeSnapshot(), + address="runtime.participant-information-state", + snapshot=RuntimeSnapshot(), + ) + + def _forged_resolver( + candidate: ParticipantInformationStateRecordModel, + _scope: object | None = None, + ) -> ParticipantInformationStateValidationContext: + return _resolved_context(candidate, proof_digest="sha256:" + "b" * 64) + + forged = _call_backend_apply( + _backend_apply, + object(), + RuntimeSnapshot(), + address="runtime.participant-information-state", + snapshot=RuntimeSnapshot(), + information_state_context_resolver=_forged_resolver, + ) + accepted = _call_backend_apply( + _backend_apply, + object(), + RuntimeSnapshot(), + address="runtime.participant-information-state", + snapshot=RuntimeSnapshot(), + information_state_context_resolver=_context_resolver, + ) + + assert without_context.success is False + assert forged.success is False + assert accepted.success is True + + +def test_control_plane_snapshot_model_exposes_information_state_history() -> None: + record = _information_state() + participant = str(record["participant_address"]) + snapshot = RuntimeSnapshot(information_state_history={participant: [record]}) + + model = _snapshot_model(RuntimeSnapshotEnvelope(snapshot=snapshot)) + + assert model.information_state_history[participant][0].information_state_ref == record["information_state_ref"] + + +def test_operational_summary_counts_information_state_history_without_payloads() -> None: + record = _information_state() + participant = str(record["participant_address"]) + + summary = _runtime_surface_summary(RuntimeSnapshot(information_state_history={participant: [record]})) + + assert summary["information_state_history"] == 1 + assert record["information_state_ref"] not in str(summary) diff --git a/implementations/python/tests/test_artifact_transformations.py b/implementations/python/tests/test_artifact_transformations.py new file mode 100644 index 000000000..a1eae38de --- /dev/null +++ b/implementations/python/tests/test_artifact_transformations.py @@ -0,0 +1,477 @@ +"""Pure artifact-transformation contract and operation tests for AUT-810.""" + +from __future__ import annotations + +import json +import textwrap +from copy import deepcopy +from pathlib import Path + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st +from pydantic import ValidationError +from raes import ( + ArtifactTransformationPolicy, + RemoveSDLDeclarationRequest, + RenameSDLDeclarationRequest, + canonical_sdl_bytes, + canonicalize_portable_contract, + compare_canonical_artifacts, + parse_sdl, + parse_sdl_file, + remove_sdl_declaration, + rename_sdl_declaration, +) +from raes_conformance.artifact_transformations import run_artifact_transformation_fixture_suite +from raes_conformance.conformance.validators import validate_contract_payload +from raes_contracts.contracts import ( + ArtifactTransformationCheckModel, + ArtifactTransformationKind, + ArtifactTransformationLossKind, + ArtifactTransformationPreservationModel, + ArtifactTransformationReportModel, + ArtifactTransformationStatus, + ContractModel, + ExternalConceptBindingDocumentModel, + PreservationOutcome, + TransformationCheckOutcome, + schema_bundle, +) + +_DIGEST_A = "sha256:" + "a" * 64 +_DIGEST_B = "sha256:" + "b" * 64 +_DIGEST_C = "sha256:" + "c" * 64 +REPO_ROOT = Path(__file__).resolve().parents[3] + + +def _successful_report() -> ArtifactTransformationReportModel: + return ArtifactTransformationReportModel( + operation_profile="canonicalize-portable-contract/v1", + status=ArtifactTransformationStatus.SUCCESS, + artifact_kind=ArtifactTransformationKind.PORTABLE_CONTRACT, + source_profile="external-concept-bindings/v1", + target_profile="external-concept-bindings/v1", + canonicalization_profile="rfc8785-jcs-sha256/v1", + source_digest=_DIGEST_A, + target_digest=_DIGEST_A, + policy_digest=_DIGEST_B, + derivation_digest=_DIGEST_C, + preconditions=( + ArtifactTransformationCheckModel( + check_id="source-admitted", + outcome=TransformationCheckOutcome.PASSED, + ), + ), + postconditions=( + ArtifactTransformationCheckModel( + check_id="canonical-identity", + outcome=TransformationCheckOutcome.PASSED, + ), + ), + preservation=ArtifactTransformationPreservationModel( + profile="canonical-artifact-identity", + outcome=PreservationOutcome.VERIFIED, + evidence_digests=(_DIGEST_A,), + ), + ) + + +def test_transformation_report_is_closed_frozen_and_deterministic() -> None: + report = _successful_report() + + assert report.schema_version == "artifact-transformation-report/v1" + assert report.model_dump_json() == _successful_report().model_dump_json() + assert "artifact-transformation-report-v1" in schema_bundle() + + invalid_payload = report.model_dump(mode="json") | {"unknown": "forbidden"} + with pytest.raises(ValidationError): + ArtifactTransformationReportModel.model_validate(invalid_payload) + with pytest.raises(ValidationError): + report.status = ArtifactTransformationStatus.REFUSED # type: ignore[misc] + + +def test_success_requires_a_target_and_verified_evidence() -> None: + payload = _successful_report().model_dump(mode="json") + payload["target_digest"] = None + + with pytest.raises(ValidationError, match="successful transformation"): + ArtifactTransformationReportModel.model_validate(payload) + + payload = _successful_report().model_dump(mode="json") + payload["preservation"]["evidence_digests"] = [] + with pytest.raises(ValidationError, match="verified preservation"): + ArtifactTransformationReportModel.model_validate(payload) + + +def _referenced_scenario(): + return parse_sdl( + textwrap.dedent( + """ + name: rename-reference-case + nodes: + web: {type: vm, resources: {ram: 1 GiB, cpu: 1}} + content: + payload: {type: file, target: web, path: /opt/payload} + relationships: + loop: {type: connects_to, source: web, target: web} + """ + ) + ) + + +def test_rename_is_atomic_deterministic_and_rewrites_resolved_references() -> None: + scenario = _referenced_scenario() + source_bytes = canonical_sdl_bytes(scenario) + request = RenameSDLDeclarationRequest( + target_address="nodes.web", + new_local_name="frontend", + ) + + first = rename_sdl_declaration(scenario, request) + second = rename_sdl_declaration(scenario, request) + + assert first.output is not None + assert first.report.status == ArtifactTransformationStatus.SUCCESS + assert first.report == second.report + assert canonical_sdl_bytes(first.output) == canonical_sdl_bytes(second.output) + assert canonical_sdl_bytes(scenario) == source_bytes + assert set(first.output.nodes) == {"frontend"} + assert first.output.content["payload"].target == "frontend" + assert first.output.relationships["loop"].source == "frontend" + assert first.output.relationships["loop"].target == "frontend" + assert [(item.before, item.after) for item in first.report.identity_map] == [("nodes.web", "nodes.frontend")] + assert first.report.preservation.profile == "sdl-declaration-identity-transport/v1" + assert first.report.preservation.outcome == PreservationOutcome.VERIFIED + + +def test_rename_refuses_aliases_and_collisions_without_partial_output() -> None: + scenario = parse_sdl( + """\ +name: refusal-case +nodes: + web: {type: switch} + frontend: {type: switch} +""" + ) + source_bytes = canonical_sdl_bytes(scenario) + + alias_result = rename_sdl_declaration( + scenario, + RenameSDLDeclarationRequest(target_address="web", new_local_name="renamed"), + ) + collision_result = rename_sdl_declaration( + scenario, + RenameSDLDeclarationRequest(target_address="nodes.web", new_local_name="frontend"), + ) + + assert alias_result.output is None + assert collision_result.output is None + assert alias_result.report.status == ArtifactTransformationStatus.REFUSED + assert collision_result.report.status == ArtifactTransformationStatus.REFUSED + assert alias_result.report.target_digest is None + assert collision_result.report.target_digest is None + assert canonical_sdl_bytes(scenario) == source_bytes + + +def test_rename_refuses_node_identifiers_over_the_sdl_limit() -> None: + scenario = _referenced_scenario() + + result = rename_sdl_declaration( + scenario, + RenameSDLDeclarationRequest( + target_address="nodes.web", + new_local_name="n" * 36, + ), + ) + + assert result.output is None + assert result.report.status == ArtifactTransformationStatus.REFUSED + assert {diagnostic.code for diagnostic in result.report.diagnostics} == { + "artifact-transformation.target-unsupported" + } + + +def test_rename_updates_module_exports_and_composed_reference_semantics() -> None: + fixture_root = REPO_ROOT / "contracts" / "fixtures" / "sdl" / "variation-points-v1" / "composition" + module = parse_sdl((fixture_root / "module.yaml").read_text(encoding="utf-8")) + + module_result = rename_sdl_declaration( + module, + RenameSDLDeclarationRequest(target_address="nodes.primary", new_local_name="frontend"), + ) + + assert module_result.output is not None + assert module_result.output.module is not None + assert module_result.output.module.exports["nodes"] == ["frontend", "secondary"] + assert module_result.output.content["payload"].target == "frontend" + + composed = parse_sdl_file(fixture_root / "root.yaml") + composed_result = rename_sdl_declaration( + composed, + RenameSDLDeclarationRequest( + target_address="nodes.shared.primary", + new_local_name="frontend", + ), + ) + + assert composed_result.output is not None + assert composed_result.output.expansion_provenance == composed.expansion_provenance + assert "shared.frontend" in composed_result.output.nodes + assert composed_result.output.content["shared.payload"].target == "shared.frontend" + + +def _concept_binding_inputs(): + fixture_root = REPO_ROOT / "contracts" / "fixtures" / "concept-authority" / "external-concept-bindings-v1" + scenario = parse_sdl((fixture_root / "context" / "subject.sdl.yaml").read_text(encoding="utf-8")) + payload = json.loads((fixture_root / "valid" / "attack-enterprise.json").read_text(encoding="utf-8")) + node_binding = payload["bindings"]["attack-execution"] + scenario_binding = deepcopy(node_binding) + scenario_binding["binding_id"] = "attack-scenario-context" + scenario_binding["subject"]["subject_kind"] = "scenario" + scenario_binding["subject"]["canonical_ref"] = "scenario.external-binding-subject" + payload["bindings"]["attack-scenario-context"] = scenario_binding + return scenario, ExternalConceptBindingDocumentModel.model_validate(payload) + + +def test_rename_retargets_every_supplied_concept_subject_digest() -> None: + scenario, document = _concept_binding_inputs() + + result = rename_sdl_declaration( + scenario, + RenameSDLDeclarationRequest(target_address="nodes.web", new_local_name="frontend"), + binding_documents=(document,), + ) + + assert result.output is not None + assert len(result.binding_documents) == 1 + transformed = result.binding_documents[0] + assert transformed.bindings["attack-execution"].subject.canonical_ref == "nodes.frontend" + assert transformed.bindings["attack-scenario-context"].subject.canonical_ref == ( + "scenario.external-binding-subject" + ) + assert {binding.subject.artifact_digest for binding in transformed.bindings.values()} == { + result.report.target_digest + } + assert document.bindings["attack-execution"].subject.canonical_ref == "nodes.web" + + +def test_rename_refuses_a_stale_supplied_concept_subject() -> None: + scenario, document = _concept_binding_inputs() + binding = document.bindings["attack-execution"] + stale = document.model_copy( + update={ + "bindings": document.bindings + | { + "attack-execution": binding.model_copy( + update={"subject": binding.subject.model_copy(update={"artifact_digest": _DIGEST_A})} + ) + } + } + ) + + result = rename_sdl_declaration( + scenario, + RenameSDLDeclarationRequest(target_address="nodes.web", new_local_name="frontend"), + binding_documents=(stale,), + ) + + assert result.output is None + assert result.report.status == ArtifactTransformationStatus.REFUSED + assert {diagnostic.code for diagnostic in result.report.diagnostics} == { + "artifact-transformation.linked-artifact-stale" + } + + +def _removal_scenario(): + return parse_sdl( + """\ +name: explicit-loss-case +nodes: + retained: {type: switch} + obsolete: {type: switch} +""" + ) + + +def test_removal_requires_exact_typed_loss_authorization() -> None: + scenario = _removal_scenario() + source_bytes = canonical_sdl_bytes(scenario) + request = RemoveSDLDeclarationRequest(target_address="nodes.obsolete") + + refused = remove_sdl_declaration(scenario, request) + allowed = remove_sdl_declaration( + scenario, + request, + policy=ArtifactTransformationPolicy(allowed_loss_kinds=(ArtifactTransformationLossKind.DECLARATION_REMOVED,)), + ) + + assert refused.output is None + assert refused.report.status == ArtifactTransformationStatus.REFUSED + assert refused.report.losses[0].kind == ArtifactTransformationLossKind.DECLARATION_REMOVED + assert allowed.output is not None + assert set(allowed.output.nodes) == {"retained"} + assert allowed.report.status == ArtifactTransformationStatus.SUCCESS + assert allowed.report.preservation.outcome == PreservationOutcome.NOT_APPLICABLE + assert allowed.report.losses[0].diagnostic.severity.value == "warning" + assert canonical_sdl_bytes(scenario) == source_bytes + + with pytest.raises(TypeError, match="ArtifactTransformationLossKind"): + ArtifactTransformationPolicy(("declaration-removed",)) # type: ignore[arg-type] + with pytest.raises(ValueError, match="sorted and unique"): + ArtifactTransformationPolicy( + ( + ArtifactTransformationLossKind.DECLARATION_REMOVED, + ArtifactTransformationLossKind.DECLARATION_REMOVED, + ) + ) + + +def test_removal_refuses_non_exact_target_addresses() -> None: + result = remove_sdl_declaration( + _removal_scenario(), + RemoveSDLDeclarationRequest(target_address="obsolete"), + ) + + assert result.output is None + assert result.report.status == ArtifactTransformationStatus.REFUSED + assert {diagnostic.code for diagnostic in result.report.diagnostics} == {"artifact-transformation.target-not-exact"} + + +def test_removal_refuses_supplied_concept_bindings() -> None: + scenario, document = _concept_binding_inputs() + + result = remove_sdl_declaration( + scenario, + RemoveSDLDeclarationRequest(target_address="nodes.web"), + policy=ArtifactTransformationPolicy(allowed_loss_kinds=(ArtifactTransformationLossKind.DECLARATION_REMOVED,)), + binding_documents=(document,), + ) + + assert result.output is None + assert result.report.status == ArtifactTransformationStatus.REFUSED + assert {diagnostic.code for diagnostic in result.report.diagnostics} == { + "artifact-transformation.linked-artifact-unsupported" + } + + +def test_authorized_removal_still_refuses_dangling_references() -> None: + scenario = _referenced_scenario() + + result = remove_sdl_declaration( + scenario, + RemoveSDLDeclarationRequest(target_address="nodes.web"), + policy=ArtifactTransformationPolicy(allowed_loss_kinds=(ArtifactTransformationLossKind.DECLARATION_REMOVED,)), + ) + + assert result.output is None + assert result.report.status == ArtifactTransformationStatus.REFUSED + assert result.report.target_digest is None + assert {diagnostic.code for diagnostic in result.report.diagnostics} == {"artifact-transformation.target-invalid"} + + +def test_portable_contract_canonicalization_is_isolated_and_idempotent() -> None: + _, document = _concept_binding_inputs() + + first = canonicalize_portable_contract(document) + second = canonicalize_portable_contract(first.output) + + assert first.output == document + assert first.output is not document + assert first.report.status == ArtifactTransformationStatus.SUCCESS + assert first.report.artifact_kind == ArtifactTransformationKind.PORTABLE_CONTRACT + assert first.report.source_digest == first.report.target_digest + assert first.report.preservation.profile == "canonical-artifact-identity" + assert first.report == second.report + assert compare_canonical_artifacts(document, first.output).equivalent + + +def test_portable_contract_transformation_rejects_unprofiled_models() -> None: + class UnprofiledContract(ContractModel): + value: int + + source = UnprofiledContract(value=1) + comparison_peer = UnprofiledContract(value=1) + + with pytest.raises(TypeError, match="explicit governed"): + canonicalize_portable_contract(source) + with pytest.raises(TypeError, match="explicit governed"): + compare_canonical_artifacts(source, comparison_peer) + + +def test_canonical_comparison_distinguishes_meaning_and_artifact_kind() -> None: + scenario = _referenced_scenario() + renamed = rename_sdl_declaration( + scenario, + RenameSDLDeclarationRequest(target_address="nodes.web", new_local_name="frontend"), + ) + assert renamed.output is not None + + comparison = compare_canonical_artifacts(scenario, renamed.output) + + assert not comparison.equivalent + assert comparison.artifact_kind == ArtifactTransformationKind.SDL_AUTHORING + assert comparison.relation_profile == "canonical-artifact-identity" + portable_contract = _concept_binding_inputs()[1] + with pytest.raises(TypeError, match="same supported artifact kind"): + compare_canonical_artifacts(scenario, portable_contract) + + +@settings(max_examples=24, deadline=None) +@given( + new_name=st.from_regex(r"[a-z][a-z0-9_-]{0,14}", fullmatch=True).filter(lambda value: value not in {"web", "peer"}), + reverse_order=st.booleans(), +) +def test_rename_property_is_repeatable_across_mapping_order( + new_name: str, + reverse_order: bool, +) -> None: + node_lines = ( + " peer: {type: switch}\n web: {type: vm, resources: {ram: 1 GiB, cpu: 1}}" + if reverse_order + else " web: {type: vm, resources: {ram: 1 GiB, cpu: 1}}\n peer: {type: switch}" + ) + scenario = parse_sdl( + "name: property-case\nnodes:\n" + f"{node_lines}\n" + "content:\n payload: {type: file, target: web, path: /opt/payload}\n" + ) + request = RenameSDLDeclarationRequest( + target_address="nodes.web", + new_local_name=new_name, + ) + + first = rename_sdl_declaration(scenario, request) + second = rename_sdl_declaration(scenario, request) + + assert first.output is not None + assert second.output is not None + assert canonical_sdl_bytes(first.output) == canonical_sdl_bytes(second.output) + assert first.report == second.report + assert first.output.content["payload"].target == new_name + + +def test_artifact_transformation_conformance_corpus() -> None: + report = run_artifact_transformation_fixture_suite() + + assert report.profile == "artifact-transformations/v1" + assert report.passed + assert [case.case_id for case in report.cases] == sorted(case.case_id for case in report.cases) + assert {case.case_id for case in report.cases} == { + "canonicalize-portable-contract", + "refuse-collision", + "remove-with-explicit-loss", + "rename-composed-reference", + "rename-references", + } + + +def test_transformation_report_positive_and_negative_fixtures() -> None: + fixture_root = ( + REPO_ROOT / "contracts" / "fixtures" / "artifact-transformations" / "artifact-transformation-report-v1" + ) + valid = json.loads((fixture_root / "valid" / "canonical-identity.json").read_text()) + invalid = json.loads((fixture_root / "invalid" / "success-without-target.json").read_text()) + + assert not validate_contract_payload("artifact-transformation-report-v1", valid) + assert validate_contract_payload("artifact-transformation-report-v1", invalid) diff --git a/implementations/python/tests/test_asr_535_participant_flow_assurance.py b/implementations/python/tests/test_asr_535_participant_flow_assurance.py index a218f8999..8d5b2310e 100644 --- a/implementations/python/tests/test_asr_535_participant_flow_assurance.py +++ b/implementations/python/tests/test_asr_535_participant_flow_assurance.py @@ -125,7 +125,7 @@ def test_unsupported_declarations_claim_nothing_and_add_no_policy_case() -> None unsupported_features = { entry.feature for entry in policy_capable_target().manifest.participant_runtime.feature_support - if entry.support_level.value == "unsupported" + if entry.feature in ALL_POLICY_FEATURES and entry.support_level.value == "unsupported" } policy_cases = [case for case in report.cases if case.policy_binding is not None] assert unsupported_features == set(ALL_POLICY_FEATURES) - {"participant_ingress_admission"} diff --git a/implementations/python/tests/test_backend_manifest.py b/implementations/python/tests/test_backend_manifest.py index cbcc27955..3b9c55cf5 100644 --- a/implementations/python/tests/test_backend_manifest.py +++ b/implementations/python/tests/test_backend_manifest.py @@ -16,6 +16,7 @@ OBSERVATION_CAPABILITY_SEALING_MODE_SCOPE, PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, PARTICIPANT_RUNTIME_POLICY_FEATURES, PARTICIPANT_RUNTIME_ROLE_SCOPE, @@ -225,7 +226,7 @@ def test_backend_manifest_v2_declares_participant_capability_dimensions(): "shared_state_change", ] assert {entry["feature"]: entry["support_level"] for entry in participant_runtime["feature_support"]} == { - feature: "unsupported" for feature in sorted(PARTICIPANT_RUNTIME_POLICY_FEATURES) + feature: "unsupported" for feature in sorted(PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES) } assert all(entry["limitation_refs"] for entry in participant_runtime["feature_support"]) assert all(entry["disclosure_refs"] for entry in participant_runtime["feature_support"]) diff --git a/implementations/python/tests/test_behavioral_relations.py b/implementations/python/tests/test_behavioral_relations.py index c97db6f5b..b32e7d87b 100644 --- a/implementations/python/tests/test_behavioral_relations.py +++ b/implementations/python/tests/test_behavioral_relations.py @@ -150,7 +150,7 @@ def test_authoritative_catalog_covers_required_relation_classes_and_dimensions() assert catalog.schema_version == "behavioral-relations/v1" assert catalog.taxonomy_id == "raes-behavioral-relations" - assert catalog.taxonomy_revision == "rev9" + assert catalog.taxonomy_revision == "rev12" assert set(catalog.relations) >= REQUIRED_RELATION_IDS for relation_id, relation in catalog.relations.items(): assert relation.left_carrier diff --git a/implementations/python/tests/test_issue_1043_forwarding_agent_posture.py b/implementations/python/tests/test_issue_1043_forwarding_agent_posture.py new file mode 100644 index 000000000..9e0aa6f2d --- /dev/null +++ b/implementations/python/tests/test_issue_1043_forwarding_agent_posture.py @@ -0,0 +1,95 @@ +"""Issue #1043: forwarding-agent ownership and evidence-plane posture.""" + +from __future__ import annotations + +import textwrap + +import pytest +from raes import SDLValidationError, parse_sdl +from raes.runtime_forwarding_agent import ( + RuntimeForwardingAgent, + RuntimeForwardingAgentOwnershipRole, +) + + +def _evidence_requirement(source_ref: str) -> str: + return f""" + evidence_requirements: + agent-output: + source_refs: [{source_ref}] + source_class: apparatus + scope: experiment-run + window: run + channel: log + artifact_role: measurement-log + sensitivity: plain + redaction: none + integrity: checksum + retention: run_lifetime + loss_disclosure: required + """ + + +def _node_agent_scenario(*, role: str, bind_evidence: bool) -> str: + evidence = _evidence_requirement("nodes.sensor.runtime.forwarding_agents.telemetry") if bind_evidence else "" + return f""" + name: forwarding-agent-posture + nodes: + sensor: + type: vm + resources: {{ram: 1 gib, cpu: 1}} + runtime: + forwarding_agents: + - forwarding_agent_id: telemetry + agent_kind: other + ownership_role: {role} + {evidence} + """ + + +def test_forwarding_agent_defaults_to_system_under_test() -> None: + agent = RuntimeForwardingAgent(forwarding_agent_id="telemetry") + + assert agent.ownership_role is RuntimeForwardingAgentOwnershipRole.SYSTEM_UNDER_TEST + + +def test_measurement_apparatus_requires_and_accepts_inbound_evidence_binding() -> None: + scenario = parse_sdl(_node_agent_scenario(role="measurement_apparatus", bind_evidence=True)) + + agent = scenario.nodes["sensor"].runtime.forwarding_agents[0] + assert agent.ownership_role is RuntimeForwardingAgentOwnershipRole.MEASUREMENT_APPARATUS + + +def test_measurement_apparatus_without_evidence_binding_fails_closed() -> None: + source = _node_agent_scenario(role="measurement_apparatus", bind_evidence=False) + with pytest.raises(SDLValidationError) as excinfo: + parse_sdl(source) + + assert any( + "measurement_apparatus" in error and "EvidenceRequirement.source_refs" in error + for error in excinfo.value.errors + ) + + +def test_system_under_test_cannot_be_claimed_as_apparatus_evidence_source() -> None: + source = _node_agent_scenario(role="system_under_test", bind_evidence=True) + with pytest.raises(SDLValidationError) as excinfo: + parse_sdl(source) + + assert any("system_under_test" in error and "source_class 'apparatus'" in error for error in excinfo.value.errors) + + +def test_scenario_level_measurement_apparatus_is_a_targetable_evidence_source() -> None: + source = textwrap.dedent( + """ + name: scenario-forwarding-apparatus + forwarding_agents: + - forwarding_agent_id: telemetry + agent_kind: other + ownership_role: measurement_apparatus + """ + ) + textwrap.dedent(_evidence_requirement("forwarding_agents.telemetry")) + + scenario = parse_sdl(source) + + assert scenario.forwarding_agents[0].ownership_role is RuntimeForwardingAgentOwnershipRole.MEASUREMENT_APPARATUS diff --git a/implementations/python/tests/test_issue_1043_realization_corroboration.py b/implementations/python/tests/test_issue_1043_realization_corroboration.py new file mode 100644 index 000000000..264ed9730 --- /dev/null +++ b/implementations/python/tests/test_issue_1043_realization_corroboration.py @@ -0,0 +1,209 @@ +"""Issue #1043: graded corroboration for forwarding-agent realization.""" + +from __future__ import annotations + +import copy +from dataclasses import replace + +import pytest +from raes import parse_sdl +from raes_backend_protocols.manifest import backend_manifest_from_v2_model, backend_manifest_v2_model +from raes_backend_stubs.manifest import create_stub_manifest +from raes_contracts.apparatus import RealizationObservationCapability +from raes_contracts.contracts import RuntimeSnapshotEnvelopeModel +from raes_contracts.planning import RuntimeDomain +from raes_contracts.realization_envelope import ObservationStrength +from raes_contracts.runtime_state import ( + RealizationObservationDisclosure, + RuntimeSnapshot, + SnapshotEntry, +) +from raes_contracts.vocabulary import RealizationVerificationScope +from raes_processor.compiler import compile_runtime_model +from raes_processor.planner import plan, realization_disclosure +from raes_runtime.control_plane_store import _snapshot_from_payload, _snapshot_payload + + +def _scenario(*, configured: bool = True) -> str: + settings = ( + """ + settings: + - setting_id: endpoint + name: endpoint + value: https://collector.invalid + classification: plain + provenance: configuration_file + """ + if configured + else "" + ) + return f""" + name: issue-1043-corroboration + nodes: + worker: + type: vm + resources: {{ram: 1 gib, cpu: 1}} + runtime: + forwarding_agents: + - forwarding_agent_id: telemetry + {settings} + """ + + +def _manifest(scope: RealizationVerificationScope): + original = create_stub_manifest() + declaration = original.realization_support[0] + return replace( + original, + realization_support=( + replace( + declaration, + observation_capabilities={ + "forwarding-agents": RealizationObservationCapability( + verification_scope=scope, + observation_strength=ObservationStrength.DAEMON_OBSERVED, + ) + }, + ), + ), + ) + + +def _compiled(configured: bool = True): + return compile_runtime_model(parse_sdl(_scenario(configured=configured))) + + +def _returned_snapshot(execution_plan, model, scope: RealizationVerificationScope) -> RuntimeSnapshot: + operation = next(op for op in execution_plan.provisioning.operations if op.address == "provision.node.worker") + requirement = next(item for item in model.realization_requirements if item.requirement_kind == "forwarding-agents") + return RuntimeSnapshot( + entries={ + operation.address: SnapshotEntry( + address=operation.address, + domain=RuntimeDomain.PROVISIONING, + resource_type=operation.resource_type, + payload=copy.deepcopy(operation.payload), + ordering_dependencies=operation.ordering_dependencies, + refresh_dependencies=operation.refresh_dependencies, + ) + }, + realization_observations=( + RealizationObservationDisclosure( + address=requirement.address, + field_path=requirement.field_path, + domain=requirement.domain, + requirement_kind=requirement.requirement_kind, + verification_scope=scope, + observation_strength=ObservationStrength.DAEMON_OBSERVED, + ), + ), + ) + + +def test_backend_manifest_round_trips_concern_observation_capability() -> None: + manifest = _manifest(RealizationVerificationScope.CONFIGURATION) + + restored = backend_manifest_from_v2_model(backend_manifest_v2_model(manifest)) + + assert restored == manifest + + +def test_observation_capability_rejects_no_evidence_source() -> None: + with pytest.raises(ValueError, match="non-none evidence"): + RealizationObservationCapability( + verification_scope=RealizationVerificationScope.PRESENCE, + observation_strength=ObservationStrength.NONE, + ) + + +def test_runtime_observation_round_trips_through_store_and_public_contract() -> None: + model = _compiled() + execution_plan = plan(model, _manifest(RealizationVerificationScope.CONFIGURATION)) + snapshot = _returned_snapshot(execution_plan, model, RealizationVerificationScope.CONFIGURATION) + + payload = _snapshot_payload(snapshot) + restored = _snapshot_from_payload(payload) + public = RuntimeSnapshotEnvelopeModel.model_validate(payload) + + assert restored.realization_observations == snapshot.realization_observations + assert public.realization_observations[0].verification_scope is RealizationVerificationScope.CONFIGURATION + assert public.realization_observations[0].observation_strength is ObservationStrength.DAEMON_OBSERVED + + +def test_runtime_store_rejects_malformed_observation_instead_of_defaulting_it() -> None: + with pytest.raises(ValueError, match="observation_strength"): + _snapshot_from_payload( + { + "realization_observations": [ + { + "address": "provision.node.worker", + "field_path": "nodes.worker.runtime.forwarding_agents", + "domain": "runtime-realization", + "requirement_kind": "forwarding-agents", + "verification_scope": "configuration", + } + ] + } + ) + + +def test_planner_rejects_exact_configuration_when_backend_only_attests_presence() -> None: + planned = plan(_compiled(), _manifest(RealizationVerificationScope.PRESENCE)) + + assert any( + diagnostic.code == "realization.under-observed-exact-requirement" and "forwarding-agents" in diagnostic.message + for diagnostic in planned.diagnostics + ) + + +def test_planner_accepts_presence_only_inventory_with_presence_capability() -> None: + planned = plan(_compiled(configured=False), _manifest(RealizationVerificationScope.PRESENCE)) + + assert not any( + diagnostic.code == "realization.under-observed-exact-requirement" for diagnostic in planned.diagnostics + ) + + +def test_runtime_rejects_matching_configuration_without_observation_disclosure() -> None: + model = _compiled() + manifest = _manifest(RealizationVerificationScope.CONFIGURATION) + execution_plan = plan(model, manifest) + snapshot = _returned_snapshot(execution_plan, model, RealizationVerificationScope.CONFIGURATION) + snapshot.realization_observations = () + + diagnostics, _provenance = realization_disclosure( + model.realization_requirements, + execution_plan.provisioning, + snapshot, + manifest=manifest, + ) + + assert any( + diagnostic.code == "runtime.backend-contract-invalid" and "corroboration" in diagnostic.message + for diagnostic in diagnostics + ) + + +def test_runtime_rejects_under_scoped_observation_but_accepts_configuration_readback() -> None: + model = _compiled() + manifest = _manifest(RealizationVerificationScope.CONFIGURATION) + execution_plan = plan(model, manifest) + weak_snapshot = _returned_snapshot(execution_plan, model, RealizationVerificationScope.PRESENCE) + strong_snapshot = _returned_snapshot(execution_plan, model, RealizationVerificationScope.CONFIGURATION) + + weak_diagnostics, _ = realization_disclosure( + model.realization_requirements, + execution_plan.provisioning, + weak_snapshot, + manifest=manifest, + ) + strong_diagnostics, strong_provenance = realization_disclosure( + model.realization_requirements, + execution_plan.provisioning, + strong_snapshot, + manifest=manifest, + ) + + assert any(diagnostic.code == "runtime.backend-contract-invalid" for diagnostic in weak_diagnostics) + assert not any(diagnostic.code == "runtime.backend-contract-invalid" for diagnostic in strong_diagnostics) + assert any(entry.requirement_kind == "forwarding-agents" for entry in strong_provenance) diff --git a/implementations/python/tests/test_issue_811_participant_bisimulation_design.py b/implementations/python/tests/test_issue_811_participant_bisimulation_design.py index d2b4d644f..8fbc43743 100644 --- a/implementations/python/tests/test_issue_811_participant_bisimulation_design.py +++ b/implementations/python/tests/test_issue_811_participant_bisimulation_design.py @@ -261,7 +261,7 @@ def test_governance_program_is_requirement_backed_acyclic_and_reproduction_gated def test_catalog_has_exact_relation_and_bounded_claim_surface() -> None: catalog = _load_json(CATALOG_PATH) - assert catalog["taxonomy_revision"] == "rev9" + assert catalog["taxonomy_revision"] == "rev12" relation = catalog["relations"]["divergence-preserving-branching-bisimulation"] assert relation["direction"] == "symmetric" assert relation["quantification"]["states"] == "greatest-fixed-point relation" diff --git a/implementations/python/tests/test_issue_963_participant_opacity_proof.py b/implementations/python/tests/test_issue_963_participant_opacity_proof.py index 0d1cef55b..f6946830a 100644 --- a/implementations/python/tests/test_issue_963_participant_opacity_proof.py +++ b/implementations/python/tests/test_issue_963_participant_opacity_proof.py @@ -169,6 +169,8 @@ def fake_urlopen(url: str, *, timeout: int) -> DownloadResponse: def test_current_and_historical_authority_resolve_by_exact_revision() -> None: current_catalog = load_behavioral_relation_catalog() + semantic_catalog = load_behavioral_relation_catalog_revision("rev11") + proof_catalog = load_behavioral_relation_catalog_revision("rev9") historical_catalog = load_behavioral_relation_catalog_revision("rev8") current_profile = load_behavioral_relation_profile("participant-opacity-baseline-v1") historical_profile = load_behavioral_relation_profile_revision( @@ -176,7 +178,9 @@ def test_current_and_historical_authority_resolve_by_exact_revision() -> None: "sem-231/rev2", ) - assert current_catalog.taxonomy_revision == "rev9" + assert current_catalog.taxonomy_revision == "rev12" + assert semantic_catalog.taxonomy_revision == "rev11" + assert proof_catalog.taxonomy_revision == "rev9" assert current_profile.profile_revision == "sem-231/rev3" assert current_profile.taxonomy_revision == "rev9" assert historical_catalog.taxonomy_revision == "rev8" diff --git a/implementations/python/tests/test_issue_964_participant_opacity_runtime.py b/implementations/python/tests/test_issue_964_participant_opacity_runtime.py new file mode 100644 index 000000000..c4ce3621e --- /dev/null +++ b/implementations/python/tests/test_issue_964_participant_opacity_runtime.py @@ -0,0 +1,689 @@ +"""SEM-231/RUN-319 bounded participant-opacity runtime enforcement.""" + +from __future__ import annotations + +from dataclasses import replace + +import pytest +from participant_crossing_fixtures import ( + PARTICIPANT as CROSSING_PARTICIPANT, +) +from participant_crossing_fixtures import ( + StaticCrossingResolver, + action_plane, + admit, + evidence, + identity, + policy_capable_target, +) +from raes_contracts.behavioral_relation_profiles import ( + load_behavioral_relation_profile, +) +from raes_contracts.behavioral_relations import ( + load_behavioral_relation_catalog, + load_behavioral_relation_catalog_revision, +) +from raes_contracts.canonical import canonical_json_digest +from raes_contracts.contracts.base import BehavioralClaimBindingModel +from raes_contracts.contracts.participant_crossing import ( + ParticipantCrossingGateDisposition, + ParticipantOpacityObservationInventoryModel, + ParticipantOpacityObservationSurfaceModel, + ParticipantOpacityRuntimeEnforcementBindingModel, + ParticipantOpacityRuntimeSupportModel, +) +from raes_contracts.participant_opacity_runtime import ( + validate_participant_opacity_runtime_enforcement, +) +from raes_runtime.control_plane import RuntimeControlPlane +from raes_runtime.control_plane_store import LocalControlPlaneStore +from raes_runtime.participant_crossing_mediation import ( + ParticipantCrossingIntent, + ParticipantCrossingPolicyResolution, +) + +PROFILE_ID = "participant-opacity-runtime-reference-v1" +PARTICIPANT = "participant.behavior.red-agent" +AUDIENCE = "audience:red-operator" + + +def _surface( + channel: str, + suffix: str, + owner: str, + *, + opportunity: bool = False, + timing: bool = False, + unreachable: bool = False, +) -> ParticipantOpacityObservationSurfaceModel: + return ParticipantOpacityObservationSurfaceModel.model_validate( + { + "surface_ref": f"participant-opacity-surface:{suffix}", + "profile_channel": channel, + "owner_ref": owner, + "disposition": "unreachable" if unreachable else "mediated", + "occurrence_treatment": "not-applicable" if unreachable else "observable", + "content_treatment": "not-applicable" if unreachable else "projected", + "projection_ref": "participant-opacity-observation:runtime-reference-v1", + "projection_revision": "rev1", + "order_basis_ref": "participant-opacity-order:logical-crossing-v1", + "order_basis_revision": "rev1", + **( + { + "opportunity_basis_ref": "participant-opacity-opportunity:crossing-v1", + "opportunity_basis_revision": "rev1", + } + if opportunity + else {} + ), + **( + { + "timing_bucket_ref": "participant-opacity-timing:logical-bucket-v1", + "timing_bucket_revision": "rev1", + } + if timing + else {} + ), + } + ) + + +def _inventory() -> ParticipantOpacityObservationInventoryModel: + surfaces = ( + _surface("action-availability", "action-ingress", "runtime.participant-control:admit-action"), + _surface( + "decision", + "action-decision-selection", + "runtime.participant-crossing:decision-surface-selection", + ), + _surface( + "action-availability", + "autonomous-scheduler-action", + "runtime.participant-scheduler:autonomous-action", + ), + _surface("decision", "supervisor-control", "runtime.participant-control:supervisor-occurrence"), + _surface( + "participant-state", + "episode-lifecycle", + "runtime.participant-episode:lifecycle", + unreachable=True, + ), + _surface( + "participant-state", + "execution-lifecycle-readback", + "runtime.participant-execution-service:lifecycle-readback", + unreachable=True, + ), + _surface( + "participant-state", + "status-view", + "runtime.participant-retrieval:status-view", + unreachable=True, + ), + _surface( + "participant-state", + "history-view", + "runtime.participant-retrieval:history-view", + unreachable=True, + ), + _surface( + "participant-state", + "context-view", + "runtime.participant-retrieval:context-view", + unreachable=True, + ), + _surface( + "payload", + "projected-payload", + "runtime.participant-retrieval:projection-serialization", + unreachable=True, + ), + _surface( + "delivery", + "directed-inject", + "runtime.participant-retrieval:directed-inject", + unreachable=True, + ), + _surface( + "delivery", + "delivery-failure-omission", + "runtime.participant-crossing:delivery-status-opportunity", + opportunity=True, + ), + _surface("decision", "operation-status-error", "runtime.control-plane:operation-status-error"), + _surface("retry", "retry-replay", "runtime.participant-crossing:idempotency-replay"), + _surface( + "latency", + "logical-timing-bucket", + "runtime.time-model:logical-bucket", + timing=True, + ), + _surface("order", "logical-causal-order", "runtime.participant-crossing:logical-causal-order"), + _surface( + "policy-release", + "policy-release-effects", + "runtime.participant-crossing:policy-release-effects", + ), + _surface( + "participant-state", + "authorized-evidence-audit-read", + "runtime.control-plane:administrative-evidence-audit-read", + unreachable=True, + ), + _surface( + "action-availability", + "native-backend-direct-use", + "runtime.backend-calls:direct-native-adapter", + unreachable=True, + ), + ) + draft = ParticipantOpacityObservationInventoryModel( + inventory_ref="participant-opacity-inventory:runtime-reference-v1", + inventory_revision="rev1", + observer_ref=PARTICIPANT, + audience_ref=AUDIENCE, + surfaces=surfaces, + ) + return draft + + +def _binding() -> ParticipantOpacityRuntimeEnforcementBindingModel: + profile = load_behavioral_relation_profile(PROFILE_ID) + inventory = _inventory() + initial_information_digest = canonical_json_digest( + {"initial_information": "participant-opacity-runtime-reference-v1"} + ) + normalized_observation_digest = canonical_json_digest( + { + "action_availability": "denied", + "decision_content": "participant-opacity-contained", + "delivery": "withheld", + "latency": "logical-bucket:contained", + "observation": "uniform-denial", + "payload": "not-released", + } + ) + claim = BehavioralClaimBindingModel( + taxonomy_id=profile.taxonomy_id, + taxonomy_revision=profile.taxonomy_revision, + relation_id=profile.relation_id, + subject=PARTICIPANT, + left_carrier_ref=profile.left_carrier_ref, + observation_projection_ref=profile.observation_projection_ref, + observation_projection_revision=profile.observation_projection_revision, + relation_parameter_profile_ref=profile.profile_id, + relation_parameter_profile_revision=profile.profile_revision, + quantifier_scope="finite-cases", + evidence_scope="finite", + assurance_axis="runtime-enforcement", + assurance_status="enforced", + evidence_boundary="One exact finite reference-runtime profile and complete mediated surface inventory.", + evidence_refs=["evidence:participant-opacity-runtime-reference-v1"], + limitations=["limitation:bounded-reference-runtime"], + explicit_non_claims=["No backend realization or general opacity is established."], + ) + return ParticipantOpacityRuntimeEnforcementBindingModel( + taxonomy_id=profile.taxonomy_id, + taxonomy_revision=profile.taxonomy_revision, + relation_id=profile.relation_id, + profile_id=profile.profile_id, + profile_revision=profile.profile_revision, + profile_digest=profile.canonical_digest, + predicate_ref=profile.parameters.secret.predicate_ref, + predicate_revision=profile.parameters.secret.predicate_revision, + carrier_ref=profile.left_carrier_ref, + carrier_digest=canonical_json_digest( + { + "carrier_ref": profile.left_carrier_ref, + "initial_information_digest": initial_information_digest, + "points": [ + "possible-point:runtime-reference-protected", + "possible-point:runtime-reference-complement", + ], + "normalized_observation_digest": normalized_observation_digest, + } + ), + materializer_ref="participant-opacity-materializer:runtime-reference-v1", + materializer_revision="rev1", + materializer_digest=canonical_json_digest({"materializer": "runtime-reference-v1"}), + observation_inventory_ref=inventory.inventory_ref, + observation_inventory_revision=inventory.inventory_revision, + observation_inventory_digest=inventory.canonical_digest, + enforcement_rule_ref="participant-opacity-enforcement:crossing-containment-v1", + enforcement_rule_revision="rev1", + enforcement_rule_digest=canonical_json_digest({"rule": "crossing-containment-v1"}), + state_cut_ref=profile.parameters.horizon.cut_ref, + state_cut_revision=profile.parameters.horizon.cut_revision, + memory_ref=profile.parameters.memory.memory_ref, + memory_revision=profile.parameters.memory.memory_revision, + release_ref=profile.parameters.release.schedule_ref, + release_revision=profile.parameters.release.schedule_revision, + assurance_axis="runtime-enforcement", + claim=claim, + evidence_refs=["evidence:participant-opacity-runtime-reference-v1"], + limitations=["limitation:bounded-reference-runtime"], + explicit_non_claims=["No backend realization or general opacity is established."], + ) + + +def _support( + binding: ParticipantOpacityRuntimeEnforcementBindingModel | None = None, + *, + inventory: ParticipantOpacityObservationInventoryModel | None = None, +) -> ParticipantOpacityRuntimeSupportModel: + binding = _binding() if binding is None else binding + inventory = _inventory() if inventory is None else inventory + return ParticipantOpacityRuntimeSupportModel( + binding=binding, + observation_inventory=inventory, + predicate_positive_case_ref="possible-point:runtime-reference-protected", + predicate_negative_case_ref="possible-point:runtime-reference-complement", + initial_information_digest=canonical_json_digest( + {"initial_information": "participant-opacity-runtime-reference-v1"} + ), + normalized_observation_digest=canonical_json_digest( + { + "action_availability": "denied", + "decision_content": "participant-opacity-contained", + "delivery": "withheld", + "latency": "logical-bucket:contained", + "observation": "uniform-denial", + "payload": "not-released", + } + ), + ) + + +def test_exact_runtime_profile_and_complete_inventory_are_admitted() -> None: + binding = _binding() + + admitted = validate_participant_opacity_runtime_enforcement( + binding, + support=_support(binding), + participant_address=PARTICIPANT, + audience_scope_ref=AUDIENCE, + ) + + assert admitted.assurance_axis == "runtime-enforcement" + assert admitted.claim.assurance_status == "enforced" + assert admitted.observation_inventory_digest == _inventory().canonical_digest + + +@pytest.mark.parametrize( + ("mutation", "message"), + [ + ("profile_digest", "profile digest"), + ("inventory_digest", "inventory digest"), + ("participant", "observer"), + ], +) +def test_stale_or_cross_observer_runtime_binding_fails_closed( + mutation: str, + message: str, +) -> None: + binding = _binding() + support = _support(binding) + participant = PARTICIPANT + if mutation == "profile_digest": + binding = binding.model_copy(update={"profile_digest": canonical_json_digest({"stale": True})}) + support = support.model_copy(update={"binding": binding}) + elif mutation == "inventory_digest": + binding = binding.model_copy(update={"observation_inventory_digest": canonical_json_digest({"stale": True})}) + support = support.model_copy(update={"binding": binding}) + else: + participant = "participant.behavior.other" + + with pytest.raises(ValueError, match=message): + validate_participant_opacity_runtime_enforcement( + binding, + support=support, + participant_address=participant, + audience_scope_ref=AUDIENCE, + ) + + +def test_missing_claimed_channel_and_active_probe_bypass_fail_closed() -> None: + binding = _binding() + inventory = _inventory() + without_delivery = inventory.model_copy( + update={"surfaces": tuple(surface for surface in inventory.surfaces if surface.profile_channel != "delivery")} + ) + missing_delivery = binding.model_copy( + update={ + "observation_inventory_digest": without_delivery.canonical_digest, + } + ) + missing_support = _support(missing_delivery, inventory=without_delivery) + + with pytest.raises(ValueError, match="delivery"): + validate_participant_opacity_runtime_enforcement( + missing_delivery, + support=missing_support, + participant_address=PARTICIPANT, + audience_scope_ref=AUDIENCE, + ) + + active_probe = next(surface for surface in inventory.surfaces if surface.profile_channel == "action-availability") + bypassed = active_probe.model_copy( + update={ + "disposition": "unsupported", + "limitation_ref": "limitation:unmediated-active-probe", + } + ) + bypass_inventory = inventory.model_copy( + update={"surfaces": tuple(bypassed if surface is active_probe else surface for surface in inventory.surfaces)} + ) + bypass_binding = binding.model_copy( + update={ + "observation_inventory_digest": bypass_inventory.canonical_digest, + } + ) + bypass_support = _support(bypass_binding, inventory=bypass_inventory) + + with pytest.raises(ValueError, match="unsupported"): + validate_participant_opacity_runtime_enforcement( + bypass_binding, + support=bypass_support, + participant_address=PARTICIPANT, + audience_scope_ref=AUDIENCE, + ) + + +def test_coarse_channel_coverage_without_every_concrete_surface_fails_closed() -> None: + binding = _binding() + inventory = _inventory() + incomplete = inventory.model_copy( + update={ + "surfaces": tuple( + surface + for surface in inventory.surfaces + if surface.surface_ref != "participant-opacity-surface:history-view" + ) + } + ) + changed = binding.model_copy( + update={ + "observation_inventory_digest": incomplete.canonical_digest, + } + ) + changed_support = _support(changed, inventory=incomplete) + + with pytest.raises(ValueError, match="missing concrete surfaces"): + validate_participant_opacity_runtime_enforcement( + changed, + support=changed_support, + participant_address=PARTICIPANT, + audience_scope_ref=AUDIENCE, + ) + + +def test_omission_and_logical_timing_require_declared_bases() -> None: + binding = _binding() + inventory = _inventory() + delivery = next(surface for surface in inventory.surfaces if surface.opportunity_basis_ref is not None) + no_opportunity = delivery.model_copy(update={"opportunity_basis_ref": None, "opportunity_basis_revision": None}) + latency = next(surface for surface in inventory.surfaces if surface.profile_channel == "latency") + no_bucket = latency.model_copy(update={"timing_bucket_ref": None, "timing_bucket_revision": None}) + + for changed, message in ((no_opportunity, "opportunity"), (no_bucket, "timing bucket")): + changed_inventory = inventory.model_copy( + update={ + "surfaces": tuple( + changed if surface.surface_ref == changed.surface_ref else surface for surface in inventory.surfaces + ) + } + ) + changed_binding = binding.model_copy( + update={ + "observation_inventory_digest": changed_inventory.canonical_digest, + } + ) + changed_support = _support(changed_binding, inventory=changed_inventory) + with pytest.raises(ValueError, match=message): + validate_participant_opacity_runtime_enforcement( + changed_binding, + support=changed_support, + participant_address=PARTICIPANT, + audience_scope_ref=AUDIENCE, + ) + + +def test_catalog_preserves_runtime_assurance_while_extending_backend_axes() -> None: + catalog = load_behavioral_relation_catalog() + relation = catalog.relations["participant-predicate-opacity"] + historical = load_behavioral_relation_catalog_revision("rev10").relations["participant-predicate-opacity"] + + assert catalog.taxonomy_revision == "rev12" + assert relation.assurance.runtime_enforcement_status == "partial" + assert relation.assurance.backend_declaration_status == "declared" + assert relation.assurance.backend_realization_status == "partial" + assert relation.assurance.backend_conformance_status == "bounded" + assert historical.assurance.backend_realization_status == "not-realized" + + +class _OpacityResolver(StaticCrossingResolver): + def __init__( + self, + *, + gate_overrides: dict[str, ParticipantCrossingGateDisposition] | None = None, + include_claim: bool = True, + omit_route_binding: bool = False, + ) -> None: + super().__init__(gate_overrides=gate_overrides) + self.include_claim = include_claim + self.omit_route_binding = omit_route_binding + self.binding = _binding() + self.support = _support(self.binding) + + def resolve( + self, + intent: ParticipantCrossingIntent, + snapshot: object, + ) -> ParticipantCrossingPolicyResolution: + resolution = super().resolve(intent, snapshot) + return replace( + resolution, + opacity_enforcement=(self.binding if self.include_claim and not self.omit_route_binding else None), + ) + + def validation_context(self, snapshot: object, participant_address: str): + context = super().validation_context(snapshot, participant_address) + return replace( + context, + opacity_enforcement_supports=(self.support,) if self.include_claim else (), + ) + + +def _opacity_plane(resolver: _OpacityResolver): + return action_plane( + resolver, + target=policy_capable_target( + "participant_ingress_admission", + "participant_egress_projection", + "participant_transformation", + ), + ) + + +def test_runtime_decision_durably_owns_safe_opacity_binding() -> None: + resolver = _OpacityResolver() + plane = _opacity_plane(resolver) + + receipt = admit(plane, idempotency_key="opacity-action") + + assert receipt.accepted is False + decision = plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT][1]["occurrence"] + binding = decision["opacity_enforcement"] + assert binding["assurance_axis"] == "runtime-enforcement" + assert binding["claim"]["evidence_scope"] == "finite" + assert "observation_inventory" not in binding + assert binding["observation_inventory_ref"] == "participant-opacity-inventory:runtime-reference-v1" + encoded = str(binding).lower() + assert "secret_holds" not in encoded + assert "possible_world" not in encoded + assert "information_cell" not in encoded + assert "belief_state" not in encoded + + +def test_exact_retry_reuses_opacity_decision_and_changed_binding_conflicts() -> None: + resolver = _OpacityResolver() + plane = _opacity_plane(resolver) + + first = admit(plane, idempotency_key="opacity-retry") + retry = admit(plane, idempotency_key="opacity-retry") + assert retry.operation_id == first.operation_id + assert len(plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT]) == 2 + + resolver.binding = resolver.binding.model_copy( + update={ + "enforcement_rule_digest": canonical_json_digest({"rule": "changed"}), + } + ) + with pytest.raises(ValueError, match="different semantics"): + admit(plane, idempotency_key="opacity-retry") + + +def test_governed_egress_normalizes_to_withheld_opportunity_before_return() -> None: + plane = _opacity_plane(_OpacityResolver()) + bound_identity = identity(audience_bound=True) + crossing_evidence = evidence() + + with pytest.raises(PermissionError, match="not permitted"): + plane.get_participant_status_view( + CROSSING_PARTICIPANT, + identity=bound_identity, + crossing_evidence=crossing_evidence, + idempotency_key="opacity-egress", + ) + + stages = [item["occurrence"]["stage"] for item in plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT]] + assert stages == [ + "requested", + "decided", + "delivery-attempted", + ] + assert plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT][-1]["occurrence"]["disposition"] == ( + "withheld" + ) + assert all( + "opacity_enforcement" not in item["occurrence"] + or item["occurrence"]["opacity_enforcement"]["assurance_axis"] == "runtime-enforcement" + for item in plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT] + ) + + +def test_denied_egress_records_observable_denial_without_returning_a_view() -> None: + resolver = _OpacityResolver(gate_overrides={"visibility": ParticipantCrossingGateDisposition.DENY}) + plane = _opacity_plane(resolver) + bound_identity = identity(audience_bound=True) + crossing_evidence = evidence() + + with pytest.raises(PermissionError, match="not permitted"): + plane.get_participant_status_view( + CROSSING_PARTICIPANT, + identity=bound_identity, + crossing_evidence=crossing_evidence, + idempotency_key="opacity-withheld", + ) + + history = plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT] + assert [item["occurrence"]["stage"] for item in history] == [ + "requested", + "decided", + "delivery-attempted", + ] + assert history[1]["occurrence"]["disposition"] == "deny" + assert history[1]["occurrence"]["opacity_enforcement"]["assurance_axis"] == ("runtime-enforcement") + assert history[2]["occurrence"]["disposition"] == "withheld" + + +def test_secret_dependent_policy_outcomes_are_normalized_to_the_same_observation() -> None: + allowed = _opacity_plane(_OpacityResolver()) + denied = _opacity_plane( + _OpacityResolver(gate_overrides={"action_admission": ParticipantCrossingGateDisposition.DENY}) + ) + + allowed_receipt = admit(allowed, idempotency_key="opacity-secret-true") + denied_receipt = admit(denied, idempotency_key="opacity-secret-false") + + assert allowed_receipt.accepted is denied_receipt.accepted is False + allowed_decision = allowed.snapshot.participant_crossing_history[CROSSING_PARTICIPANT][1]["occurrence"] + denied_decision = denied.snapshot.participant_crossing_history[CROSSING_PARTICIPANT][1]["occurrence"] + for decision in (allowed_decision, denied_decision): + assert decision["disposition"] == "deny" + assert allowed_decision["gates"] == denied_decision["gates"] + + +def test_route_local_binding_omission_cannot_bypass_active_runtime_support() -> None: + plane = _opacity_plane(_OpacityResolver(omit_route_binding=True)) + + receipt = admit(plane, idempotency_key="opacity-route-bypass") + + assert receipt.accepted is False + decision = plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT][1]["occurrence"] + assert decision["opacity_enforcement"]["assurance_axis"] == "runtime-enforcement" + + +def test_disclosed_weakening_removes_positive_opacity_claim() -> None: + plane = _opacity_plane(_OpacityResolver(include_claim=False)) + + receipt = admit(plane, idempotency_key="opacity-weakened") + + assert receipt.accepted is True + decision = plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT][1]["occurrence"] + assert "opacity_enforcement" not in decision + + +def test_restart_requires_the_exact_persisted_opacity_context(tmp_path) -> None: + store = LocalControlPlaneStore(tmp_path / "opacity-control-plane") + first_resolver = _OpacityResolver() + first = action_plane( + first_resolver, + target=policy_capable_target("participant_ingress_admission"), + store=store, + ) + admit(first, idempotency_key="opacity-restart") + + restarted_resolver = _OpacityResolver() + restarted_resolver.subjects = list(first_resolver.subjects) + restarted_resolver.evidence_refs = set(first_resolver.evidence_refs) + restarted = RuntimeControlPlane( + policy_capable_target("participant_ingress_admission"), + store=LocalControlPlaneStore(tmp_path / "opacity-control-plane"), + crossing_policy_resolver=restarted_resolver, + ) + assert restarted.snapshot.participant_crossing_history[CROSSING_PARTICIPANT] + + stale = _OpacityResolver() + stale.subjects = list(first_resolver.subjects) + stale.evidence_refs = set(first_resolver.evidence_refs) + stale.binding = stale.binding.model_copy( + update={"enforcement_rule_digest": canonical_json_digest({"rule": "stale"})} + ) + stale.support = _support(stale.binding) + restart_target = policy_capable_target("participant_ingress_admission") + restart_store = LocalControlPlaneStore(tmp_path / "opacity-control-plane") + with pytest.raises(ValueError, match="restart context"): + RuntimeControlPlane( + restart_target, + store=restart_store, + crossing_policy_resolver=stale, + ) + + +def test_runtime_reset_does_not_erase_retained_opacity_history() -> None: + plane = _opacity_plane(_OpacityResolver()) + admit(plane, idempotency_key="opacity-before-reset") + before = tuple(plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT]) + + receipt = plane.reset_participant_episode( + CROSSING_PARTICIPANT, + episode_id="episode-1", + reason="bounded test reset", + idempotency_key="opacity-reset", + ) + + assert receipt.accepted is True + assert tuple(plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT]) == before + admit(plane, idempotency_key="opacity-after-reset") + assert len(plane.snapshot.participant_crossing_history[CROSSING_PARTICIPANT]) > len(before) diff --git a/implementations/python/tests/test_issue_965_participant_opacity_backend.py b/implementations/python/tests/test_issue_965_participant_opacity_backend.py new file mode 100644 index 000000000..9ba5ad765 --- /dev/null +++ b/implementations/python/tests/test_issue_965_participant_opacity_backend.py @@ -0,0 +1,315 @@ +"""SEM-231/API-407/ASR-535 backend participant-opacity assurance.""" + +from __future__ import annotations + +from dataclasses import replace + +import pytest +from raes_backend_protocols.capabilities import ( + PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES, + PARTICIPANT_RUNTIME_POLICY_FEATURES, +) +from raes_backend_protocols.manifest import backend_manifest_payload +from raes_backend_protocols.participant_feature_admission import resolve_participant_feature_support +from raes_conformance.conformance.participant_opacity_probes import ( + ParticipantOpacityProbeCase, + ParticipantOpacityProbeObservation, +) +from raes_conformance.conformance.reference_participant_opacity import ReferenceParticipantOpacityHarness +from raes_conformance.conformance.report import ( + backend_conformance_report_payload, + validate_backend_conformance_report, +) +from raes_conformance.conformance.target import run_target_conformance +from raes_contracts.behavioral_relation_profiles import load_behavioral_relation_profile +from raes_contracts.canonical import canonical_json_digest +from raes_contracts.contracts import BackendManifestV2Model +from raes_contracts.vocabulary import ParticipantFeatureSupportLevel +from raes_reference_backend import create_reference_backend_target +from raes_reference_backend.participant_runtime import ReferenceParticipantRuntime + +FEATURE = "participant_predicate_opacity" +PROFILE_ID = "participant-opacity-runtime-reference-v1" +SECRET_MARKER = "participant-secret-never-report-965" + +EXPECTED_OBSERVATION = ParticipantOpacityProbeObservation( + decision="deny", + failure="uniform-refusal", + action_availability="denied", + delivery="withheld", + omission="recorded-at-governed-opportunity", + retry="stable-replay", + logical_timing="logical-bucket:contained", + logical_order="stable-causal-order", + policy_release_effect="contained", + external_effect="none", + payload_released=False, +) + + +ReferenceOpacityHarness = ReferenceParticipantOpacityHarness + + +class RuntimeMediatedOnlyHarness(ReferenceOpacityHarness): + def observe(self, target, case: ParticipantOpacityProbeCase, point_ref: str): + del target, case, point_ref + return EXPECTED_OBSERVATION + + +class EmptyOpacityHarness(ReferenceOpacityHarness): + def cases(self, target): + del target + return () + + +class LeakingReferenceParticipantRuntime(ReferenceParticipantRuntime): + def participant_relation_probe(self, **coordinates): + observation = super().participant_relation_probe(**coordinates) + if coordinates["possible_point_ref"].endswith("protected"): + observation = {**observation, "decision": "allow"} + return observation + + +class SecretFailureHarness(ReferenceOpacityHarness): + def observe(self, target, case: ParticipantOpacityProbeCase, point_ref: str): + del target, case, point_ref + raise ValueError(f"rejected {SECRET_MARKER}") + + +def _opacity_declaration(target): + capability = target.manifest.participant_runtime + assert capability is not None + return next(item for item in capability.feature_support if item.feature == FEATURE) + + +def _opacity_cases(report): + return [ + case + for case in report.cases + if any(binding.relation_id == "participant-predicate-opacity" for binding in case.claim_bindings) + ] + + +def test_manifest_round_trip_declares_one_bounded_relation_feature() -> None: + target = create_reference_backend_target() + declaration = _opacity_declaration(target) + + payload = backend_manifest_payload(target.manifest) + model = BackendManifestV2Model.model_validate(payload) + round_trip = model.model_dump(mode="json") + round_trip.pop("realization_envelope", None) + + assert round_trip == payload + assert declaration.support_level is ParticipantFeatureSupportLevel.BOUNDED + assert declaration.constraint_refs + assert declaration.limitation_refs + assert declaration.disclosure_refs + assert declaration.evidence_refs + assert FEATURE in PARTICIPANT_RUNTIME_EVIDENCE_REQUIRED_FEATURES + assert FEATURE not in PARTICIPANT_RUNTIME_POLICY_FEATURES + assert PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[ + "capabilities.participant_runtime.supported_behavior_features" + ][FEATURE] >= { + "operation-receipt-v1", + "operation-status-v1", + "runtime-snapshot-v1", + "participant-episode-state-envelope-v1", + "participant-episode-history-event-stream-v1", + "participant-behavior-history-event-stream-v1", + "participant-control-occurrence-v1", + "participant-crossing-occurrence-v1", + "participant-observation-envelope-v1", + } + + +def test_positive_declaration_rejects_missing_evidence_or_contracts() -> None: + target = create_reference_backend_target() + capability = target.manifest.participant_runtime + assert capability is not None + declaration = _opacity_declaration(target) + + with pytest.raises(ValueError, match="evidence_refs"): + replace(declaration, evidence_refs=()) + + manifest = replace( + target.manifest, + supported_contract_versions=( + target.manifest.supported_contract_versions - {"participant-crossing-occurrence-v1"} + ), + ) + with pytest.raises(ValueError, match="missing required contracts"): + resolve_participant_feature_support( + manifest, + FEATURE, + required_level=ParticipantFeatureSupportLevel.BOUNDED, + ) + + +def test_declared_profile_without_executed_cases_is_unsupported() -> None: + report = run_target_conformance(create_reference_backend_target()) + + case = next(case for case in report.cases if case.capability_feature == FEATURE and case.outcome == "unsupported") + assert not case.passed + assert not report.passed + assert {binding.assurance_axis for binding in case.claim_bindings} == {"backend-declaration"} + + +def test_empty_harness_cannot_erase_a_positive_declaration() -> None: + report = run_target_conformance( + create_reference_backend_target(), + participant_opacity_harness=EmptyOpacityHarness(), + ) + + case = next(case for case in report.cases if case.capability_feature == FEATURE) + assert not case.passed + assert case.outcome == "unsupported" + assert any(diag.code == "conformance.participant-opacity-harness-empty" for diag in case.diagnostics) + + +def test_backend_native_cases_emit_three_independent_bound_claims() -> None: + target = create_reference_backend_target() + profile = load_behavioral_relation_profile(PROFILE_ID) + + report = run_target_conformance(target, participant_opacity_harness=ReferenceOpacityHarness()) + + cases = _opacity_cases(report) + native = next(case for case in cases if case.realization_owner == "backend-native") + assert native.passed + assert report.passed + assert {binding.assurance_axis for binding in native.claim_bindings} == { + "backend-declaration", + "backend-realization", + "backend-conformance", + } + assert all(binding.taxonomy_revision == profile.taxonomy_revision for binding in native.claim_bindings) + assert all( + binding.relation_parameter_profile_revision == profile.profile_revision for binding in native.claim_bindings + ) + assert native.profile_digest == profile.canonical_digest + assert native.manifest_digest + assert native.configuration_digest + assert native.tool_digest + assert native.environment_digest + assert native.probe_set_digest == ReferenceOpacityHarness.probe_set_digest + assert report.claim.relation_id == "bounded-probe-success" + assert report.claim.evidence_scope == "finite" + + +def test_adversarial_backend_is_detected_with_runtime_configuration_fixed() -> None: + target = create_reference_backend_target() + target = replace(target, participant_runtime=LeakingReferenceParticipantRuntime()) + + report = run_target_conformance(target, participant_opacity_harness=ReferenceOpacityHarness()) + + case = next(case for case in report.cases if case.capability_feature == FEATURE) + assert not case.passed + assert not report.passed + assert {binding.assurance_axis for binding in case.claim_bindings} == {"backend-declaration"} + assert any(diag.code == "conformance.participant-opacity-observation-mismatch" for diag in case.diagnostics) + + +def test_runtime_mediation_cannot_be_reported_as_backend_native() -> None: + report = run_target_conformance( + create_reference_backend_target(), + participant_opacity_harness=RuntimeMediatedOnlyHarness(), + ) + + case = next(case for case in _opacity_cases(report) if case.realization_owner == "backend-native") + assert not case.passed + assert any(diag.code == "conformance.participant-opacity-backend-unobserved" for diag in case.diagnostics) + assert {binding.assurance_axis for binding in case.claim_bindings} == {"backend-declaration"} + + +def test_forged_manifest_digest_cannot_bind_backend_conformance() -> None: + class ForgedDigestHarness(ReferenceOpacityHarness): + def cases(self, target): + case = super().cases(target)[0] + return (replace(case, manifest_digest=canonical_json_digest({"manifest": "other"})),) + + report = run_target_conformance( + create_reference_backend_target(), + participant_opacity_harness=ForgedDigestHarness(), + ) + + case = next(case for case in report.cases if case.capability_feature == FEATURE) + assert not case.passed + assert {binding.assurance_axis for binding in case.claim_bindings} == {"backend-declaration"} + assert any(diag.code == "conformance.participant-opacity-harness-rejected" for diag in case.diagnostics) + + +def test_undeclared_profile_cannot_bind_backend_conformance() -> None: + class UndeclaredProfileHarness(ReferenceOpacityHarness): + def cases(self, target): + case = super().cases(target)[0] + profile = load_behavioral_relation_profile("participant-opacity-baseline-v1") + return ( + replace( + case, + profile_id=profile.profile_id, + profile_revision=profile.profile_revision, + profile_digest=profile.canonical_digest, + ), + ) + + report = run_target_conformance( + create_reference_backend_target(), + participant_opacity_harness=UndeclaredProfileHarness(), + ) + + case = next(case for case in report.cases if case.capability_feature == FEATURE) + assert not case.passed + assert {binding.assurance_axis for binding in case.claim_bindings} == {"backend-declaration"} + assert any(diag.code == "conformance.participant-opacity-harness-rejected" for diag in case.diagnostics) + + +def test_authorized_weakening_removes_realization_and_conformance_claims() -> None: + target = create_reference_backend_target() + capability = target.manifest.participant_runtime + assert capability is not None + weakened = tuple( + replace(item, support_level=ParticipantFeatureSupportLevel.DISCLOSED_WEAK) if item.feature == FEATURE else item + for item in capability.feature_support + ) + manifest = replace( + target.manifest, + capabilities=replace( + target.manifest.capabilities, + participant_runtime=replace(capability, feature_support=weakened), + ), + ) + target = replace(target, manifest=manifest) + + report = run_target_conformance(target, participant_opacity_harness=ReferenceOpacityHarness()) + + case = next(case for case in _opacity_cases(report) if case.capability_feature == FEATURE) + assert not case.passed + assert case.limitations + assert {binding.assurance_axis for binding in case.claim_bindings} == {"backend-declaration"} + + +def test_secret_bearing_failures_are_sanitized_before_report_projection() -> None: + report = run_target_conformance( + create_reference_backend_target(), + participant_opacity_harness=SecretFailureHarness(), + ) + + payload = backend_conformance_report_payload(report) + rendered = str(payload) + assert SECRET_MARKER not in rendered + assert "input_value" not in rendered + assert not report.passed + + +def test_report_validator_rejects_conformance_without_realization() -> None: + report = run_target_conformance( + create_reference_backend_target(), + participant_opacity_harness=ReferenceOpacityHarness(), + ) + case = next(case for case in _opacity_cases(report) if case.realization_owner == "backend-native") + claims = tuple(binding for binding in case.claim_bindings if binding.assurance_axis != "backend-realization") + forged = replace(case, claim_bindings=claims) + forged_report = replace(report, cases=tuple(forged if item is case else item for item in report.cases)) + + with pytest.raises(ValueError, match="backend conformance.*realization"): + validate_backend_conformance_report(forged_report) diff --git a/implementations/python/tests/test_libvirt_backend_realization.py b/implementations/python/tests/test_libvirt_backend_realization.py index 588a87bd1..ce584981a 100644 --- a/implementations/python/tests/test_libvirt_backend_realization.py +++ b/implementations/python/tests/test_libvirt_backend_realization.py @@ -53,7 +53,11 @@ def _node_os(os_family: str) -> PlannedResource: "node_name": "web", "os_family": os_family, "spec": { - "node": {"type": "vm", "source": {"name": "/img/base.qcow2"}, "resources": {"ram": 512, "cpu": 1}}, + "node": { + "type": "vm", + "source": {"name": "/img/base.qcow2"}, + "resources": {"ram": 2_147_483_648, "cpu": 4}, + }, "infrastructure": {"networks": ["lan"]}, }, }, @@ -71,7 +75,12 @@ def _domain(realization, address: str = NODE_ADDRESS): def test_node_without_placements_gets_hostname_only_cloud_init(): realization = interpret_provisioning_plan(_plan(_node())) - cloud_init = _domain(realization).cloud_init + domain = _domain(realization) + cloud_init = domain.cloud_init + assert domain.image_ref == "/img/base.qcow2" + assert domain.memory_mib == 2048 + assert domain.vcpus == 4 + assert domain.networks == ("lan",) assert cloud_init.hostname == "web" assert cloud_init.is_empty is False # hostname is present assert cloud_init.users == () diff --git a/implementations/python/tests/test_planned_resource_accessors.py b/implementations/python/tests/test_planned_resource_accessors.py new file mode 100644 index 000000000..e54ae18cc --- /dev/null +++ b/implementations/python/tests/test_planned_resource_accessors.py @@ -0,0 +1,122 @@ +"""Public payload-accessor tests for planned resources.""" + +from __future__ import annotations + +from raes_contracts.planning import ( + PlannedResource, + RuntimeDomain, + planned_infrastructure_spec, + planned_node_resources, + planned_node_source, + planned_node_spec, + planned_resource_authored_name, + planned_resource_name, + planned_resource_payload, +) + + +def test_node_accessors_preserve_authored_mapping_shapes() -> None: + source = {"name": "images/base.qcow2", "build": {"format": "qcow2"}} + resources = {"ram": 1_073_741_824, "cpu": 2} + infrastructure = {"networks": ["lan"], "properties": {"zone": "test"}} + node = {"source": source, "resources": resources} + payload = { + "name": "web", + "spec": {"node": node, "infrastructure": infrastructure}, + } + resource = PlannedResource( + address="provision.node.web", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload=payload, + ) + + assert planned_resource_payload(resource) is payload + assert planned_node_spec(resource) is node + assert planned_node_source(resource) is source + assert planned_node_resources(resource) is resources + assert planned_infrastructure_spec(resource) is infrastructure + assert planned_resource_authored_name(resource) == "web" + assert planned_resource_name(resource) == "web" + + +def test_node_source_preserves_string_shape() -> None: + resource = PlannedResource( + address="provision.node.web", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={"spec": {"node": {"source": "registry.example/web:1"}}}, + ) + + assert planned_node_source(resource) == "registry.example/web:1" + + +def test_missing_optional_node_fields_are_absent_and_name_falls_back_to_address() -> None: + resource = PlannedResource( + address="provision.node.web", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={}, + ) + + assert planned_node_spec(resource) is None + assert planned_node_source(resource) is None + assert planned_node_resources(resource) is None + assert planned_infrastructure_spec(resource) is None + assert planned_resource_authored_name(resource) is None + assert planned_resource_name(resource) == resource.address + + +def test_non_mapping_payload_is_safely_absent() -> None: + resource = PlannedResource( + address="provision.node.web", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload="not-a-mapping", # type: ignore[arg-type] + ) + + assert planned_resource_payload(resource) is None + assert planned_node_spec(resource) is None + assert planned_node_source(resource) is None + assert planned_node_resources(resource) is None + assert planned_infrastructure_spec(resource) is None + assert planned_resource_authored_name(resource) is None + assert planned_resource_name(resource) == resource.address + + +def test_node_accessors_reject_unsupported_provisioning_resource_type() -> None: + resource = PlannedResource( + address="provision.feature-binding.monitoring", + domain=RuntimeDomain.PROVISIONING, + resource_type="feature-binding", + payload={ + "spec": { + "node": {"source": "should-not-be-read", "resources": {"cpu": 8}}, + "infrastructure": {"networks": ["should-not-be-read"]}, + } + }, + ) + + assert planned_node_spec(resource) is None + assert planned_node_source(resource) is None + assert planned_node_resources(resource) is None + assert planned_infrastructure_spec(resource) is None + + +def test_provisioning_accessors_reject_wrong_runtime_domain() -> None: + resource = PlannedResource( + address="orchestration.script.setup", + domain=RuntimeDomain.ORCHESTRATION, + resource_type="script", + payload={ + "spec": { + "node": {"source": "should-not-be-read", "resources": {"cpu": 8}}, + "infrastructure": {"networks": ["should-not-be-read"]}, + } + }, + ) + + assert planned_node_spec(resource) is None + assert planned_node_source(resource) is None + assert planned_node_resources(resource) is None + assert planned_infrastructure_spec(resource) is None diff --git a/implementations/python/tests/test_reference_backend_conformance.py b/implementations/python/tests/test_reference_backend_conformance.py index 721a99c39..37a047234 100644 --- a/implementations/python/tests/test_reference_backend_conformance.py +++ b/implementations/python/tests/test_reference_backend_conformance.py @@ -9,11 +9,13 @@ from raes_reference_backend import create_reference_backend_target -def test_reference_target_passes_full_remote_control_plane_conformance(): +def test_reference_target_requires_opacity_probe_for_positive_declaration(): report = run_target_conformance(create_reference_backend_target()) assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE - assert report.passed is True, [diag.message for diag in report.diagnostics] + assert report.passed is False + opacity = next(case for case in report.cases if case.capability_feature == "participant_predicate_opacity") + assert opacity.outcome == "unsupported" assert not report.unsupported_contract_gaps assert not report.unsupported_capability_gaps @@ -38,12 +40,15 @@ def test_reference_target_drives_full_participant_probe_case_set(): ) -def test_reference_target_conformance_matches_stub_acceptance(): +def test_positive_reference_declaration_is_stricter_than_stub_nonclaim(): from raes_backend_stubs.stubs import create_stub_target reference_report = run_target_conformance(create_reference_backend_target()) stub_report = run_target_conformance(create_stub_target()) assert reference_report.profile == stub_report.profile - assert reference_report.passed == stub_report.passed is True - assert {case.name for case in reference_report.cases} == {case.name for case in stub_report.cases} + assert reference_report.passed is False + assert stub_report.passed is True + assert {case.name for case in reference_report.cases} - {case.name for case in stub_report.cases} == { + "participant-opacity-backend-not-executed" + } diff --git a/implementations/python/tests/test_reference_backend_docker_integration.py b/implementations/python/tests/test_reference_backend_docker_integration.py index e17b76e81..379f1b5df 100644 --- a/implementations/python/tests/test_reference_backend_docker_integration.py +++ b/implementations/python/tests/test_reference_backend_docker_integration.py @@ -19,6 +19,7 @@ BackendCapabilityProfile, run_target_conformance, ) +from raes_conformance.conformance.reference_participant_opacity import ReferenceParticipantOpacityHarness from raes_reference_backend import create_reference_backend_target from raes_reference_backend.drivers.oci import ImageTrustPolicy, OciDeploymentDriver from raes_runtime.control_plane import RuntimeControlPlane @@ -115,7 +116,10 @@ def test_real_driver_conformance_passes(container_runtime: str): target = create_reference_backend_target(driver=driver) try: - report = run_target_conformance(target) + report = run_target_conformance( + target, + participant_opacity_harness=ReferenceParticipantOpacityHarness(), + ) assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE assert report.passed is True, [ diff --git a/implementations/python/tests/test_reference_backend_realization.py b/implementations/python/tests/test_reference_backend_realization.py index 6dda5e1ec..50ebfaee8 100644 --- a/implementations/python/tests/test_reference_backend_realization.py +++ b/implementations/python/tests/test_reference_backend_realization.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest +from raes_backend_protocols.naming import provider_resource_name from raes_contracts.planning import ( ChangeAction, PlannedResource, @@ -76,6 +77,27 @@ def test_interpret_maps_nodes_to_container_specs(): assert not realization.diagnostics +def test_interpret_uses_node_source_and_preserves_provider_name_fallback() -> None: + resource = PlannedResource( + address="provision.node.web", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={ + "os_family": "linux", + "spec": { + "node": {"source": {"name": "registry.example/web:1"}}, + "infrastructure": {}, + }, + }, + ) + + realization = interpret_provisioning_plan(_plan(resource)) + + assert realization.containers[0].image_ref == "registry.example/web:1" + assert realization.containers[0].name == provider_resource_name(resource.address, prefix="raes") + assert not realization.diagnostics + + def test_interpret_preserves_named_and_unnamed_service_descriptors(): plan = _plan( _node_resource( diff --git a/implementations/python/tests/test_repo_policy_tools.py b/implementations/python/tests/test_repo_policy_tools.py index 481f4ea60..c7b9b7afe 100644 --- a/implementations/python/tests/test_repo_policy_tools.py +++ b/implementations/python/tests/test_repo_policy_tools.py @@ -817,6 +817,57 @@ def test_module_boundaries_reject_backend_protocol_any_signatures(tmp_path: Path assert [f.rule_id for f in failures] == ["backend-protocol-untyped-contract"] +def test_module_boundaries_reject_backend_protocol_any_signatures_outside_protocols_module( + tmp_path: Path, +) -> None: + # ADR-036 requires the Any-signature ban package-wide, not only in protocols.py. + repo_root = setup_policy_repo(tmp_path) + rel = "implementations/python/packages/raes_backend_protocols/participant_reset.py" + write_text( + repo_root / rel, + "from typing import Any, Protocol\n\n" + "class ParticipantReset(Protocol):\n" + " def reset(self, request: Any) -> Any: ...\n", + ) + + failures = evaluate_repo_policy(repo_root, [rel], check_set="file-local", structural_runner=structural_runner_stub) + + assert [f.rule_id for f in failures] == ["backend-protocol-untyped-contract"] + + +def test_module_boundaries_allow_typed_backend_protocol_outside_protocols_module( + tmp_path: Path, +) -> None: + repo_root = setup_policy_repo(tmp_path) + rel = "implementations/python/packages/raes_backend_protocols/participant_reset.py" + write_text( + repo_root / rel, + "from typing import Protocol\n\n" + "class ParticipantReset(Protocol):\n" + " def reset(self, request: str) -> bool: ...\n", + ) + + failures = evaluate_repo_policy(repo_root, [rel], check_set="file-local", structural_runner=structural_runner_stub) + + assert failures == [] + + +def test_module_boundaries_allow_structural_backend_protocol_attributes_outside_protocols_module( + tmp_path: Path, +) -> None: + # Non-method structural protocol attributes are not signatures and must not be flagged. + repo_root = setup_policy_repo(tmp_path) + rel = "implementations/python/packages/raes_backend_protocols/participant_resource_admission.py" + write_text( + repo_root / rel, + "from typing import Any, Protocol\n\nclass ParticipantResourceAdmission(Protocol):\n limit: Any\n", + ) + + failures = evaluate_repo_policy(repo_root, [rel], check_set="file-local", structural_runner=structural_runner_stub) + + assert failures == [] + + def test_module_boundaries_config_is_required(tmp_path: Path) -> None: repo_root = setup_policy_repo(tmp_path) policy = _load_test_policy(repo_root) diff --git a/implementations/python/tests/test_runtime_family_invariants.py b/implementations/python/tests/test_runtime_family_invariants.py index 343fb1df8..cb4619ab3 100644 --- a/implementations/python/tests/test_runtime_family_invariants.py +++ b/implementations/python/tests/test_runtime_family_invariants.py @@ -323,14 +323,22 @@ def test_runtime_modules_do_not_redeclare_shared_validation_helpers() -> None: """Runtime families import shared helper policy instead of shadowing it.""" package_dir = Path(raes.__file__).resolve().parent + # Discover both flat ``runtime_*.py`` modules and the submodules of + # ``runtime_*`` packages (a runtime family may be split into a package + # behind an API-stable re-export ``__init__``), so the no-shadowing + # invariant keeps enforcing after a modularity split. + runtime_module_paths = [ + *package_dir.glob("runtime_*.py"), + *package_dir.glob("runtime_*/**/*.py"), + ] offenders: list[str] = [] - for path in sorted(package_dir.glob("runtime_*.py")): + for path in sorted(runtime_module_paths): if path.name == "runtime_values.py": continue tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) for node in ast.walk(tree): if isinstance(node, ast.FunctionDef) and node.name in _SHARED_HELPER_DEFINITION_NAMES: - offenders.append(f"{path.name}:{node.lineno}:{node.name}") + offenders.append(f"{path.relative_to(package_dir)}:{node.lineno}:{node.name}") assert not offenders, "Runtime modules must not redeclare shared validation helpers:\n " + "\n ".join(offenders) @@ -359,31 +367,57 @@ def test_primary_id_field_exists_on_model() -> None: ) -def _runtime_family_enums() -> dict[str, type[enum.Enum]]: - """Collect every Enum subclass *defined in* a runtime-family module. +def _runtime_family_modules() -> dict[str, list[str]]: + """Map each top-level ``runtime_*`` family to the modules that define it. - A runtime-family module is any ``raes`` submodule whose name starts - with ``runtime_`` (this includes the ``*_vocab`` and ``*_definitions`` - modules). Only enums whose ``__module__`` is that module are returned, so - enums merely re-exported or imported from another module are not - double-counted against the wrong module. + A runtime-family is any ``raes`` submodule whose name starts with + ``runtime_`` (this includes the ``*_vocab`` and ``*_definitions`` modules). + When a modularity split turns such a module into a *package* behind an + API-stable re-export ``__init__``, its concrete declarations live in private + submodules; the value list therefore carries the package ``__init__`` plus + every submodule, discovered by walking the real package files rather than + trusting what ``__init__`` happens to re-export. """ - found: dict[str, type[enum.Enum]] = {} + families: dict[str, list[str]] = {} for module_info in pkgutil.iter_modules(raes.__path__): name = module_info.name if not name.startswith("runtime_"): continue qualified = f"raes.{name}" - module = importlib.import_module(qualified) - for value in vars(module).values(): - if ( - isinstance(value, type) - and issubclass(value, enum.Enum) - and value is not enum.Enum - and value.__module__ == qualified - ): - found[f"{name}.{value.__name__}"] = value + module_names = [qualified] + if module_info.ispkg: + package = importlib.import_module(qualified) + module_names.extend(info.name for info in pkgutil.walk_packages(package.__path__, prefix=f"{qualified}.")) + families[name] = module_names + return families + + +def _runtime_family_enums() -> dict[str, type[enum.Enum]]: + """Collect every Enum subclass *defined in* a runtime-family module. + + Discovery walks each family's real modules (a flat ``runtime_*.py`` or every + submodule of a ``runtime_*`` package — see :func:`_runtime_family_modules`) + and keeps enums whose ``__module__`` is the module being inspected. Reading + each concrete submodule directly — instead of ``vars`` of a re-exporting + ``__init__`` — means an enum defined in a private submodule but never + re-exported is still seen, so it cannot silently opt out of the + open/closed-sentinel lint; enums merely re-exported or imported from another + module are counted once, against their defining module. + """ + + found: dict[str, type[enum.Enum]] = {} + for name, module_names in _runtime_family_modules().items(): + for module_name in module_names: + module = importlib.import_module(module_name) + for value in vars(module).values(): + if ( + isinstance(value, type) + and issubclass(value, enum.Enum) + and value is not enum.Enum + and value.__module__ == module_name + ): + found[f"{name}.{value.__name__}"] = value return found diff --git a/implementations/python/tests/test_sem_230_information_flow_control.py b/implementations/python/tests/test_sem_230_information_flow_control.py index 7886bd8cb..b48702d23 100644 --- a/implementations/python/tests/test_sem_230_information_flow_control.py +++ b/implementations/python/tests/test_sem_230_information_flow_control.py @@ -60,7 +60,7 @@ def _crossing(**overrides: object) -> Crossing: def test_catalog_publishes_revisioned_policy_noninterference_claim_surface(): catalog = load_behavioral_relation_catalog() - assert catalog.taxonomy_revision == "rev9" + assert catalog.taxonomy_revision == "rev12" relation = catalog.relations["policy-noninterference"] assert relation.projection_required is True assert relation.quantification.states diff --git a/implementations/python/tests/test_sem_231_participant_predicate_opacity.py b/implementations/python/tests/test_sem_231_participant_predicate_opacity.py index dcee4b431..66b92c820 100644 --- a/implementations/python/tests/test_sem_231_participant_predicate_opacity.py +++ b/implementations/python/tests/test_sem_231_participant_predicate_opacity.py @@ -8,6 +8,7 @@ from raes_contracts.behavioral_relations import ( RelationAssuranceModel, load_behavioral_relation_catalog, + load_behavioral_relation_catalog_revision, validate_behavioral_claim_binding, ) from raes_contracts.contracts import BehavioralClaimBindingModel @@ -41,7 +42,7 @@ def test_catalog_defines_one_sided_participant_predicate_opacity() -> None: catalog = load_behavioral_relation_catalog() relation = catalog.relations["participant-predicate-opacity"] - assert catalog.taxonomy_revision == "rev9" + assert catalog.taxonomy_revision == "rev12" assert relation.relation_class == "epistemic" assert relation.direction == "unary" assert relation.relation_parameter_profile_required is True @@ -55,10 +56,10 @@ def test_catalog_defines_one_sided_participant_predicate_opacity() -> None: assert relation.assurance.test_status == "bounded" assert relation.assurance.model_check_status == "model-checked" assert relation.assurance.proof_status == "proved" - assert relation.assurance.runtime_enforcement_status == "not-enforced" - assert relation.assurance.backend_declaration_status == "not-declared" - assert relation.assurance.backend_realization_status == "not-realized" - assert relation.assurance.backend_conformance_status == "not-tested" + assert relation.assurance.runtime_enforcement_status == "partial" + assert relation.assurance.backend_declaration_status == "declared" + assert relation.assurance.backend_realization_status == "partial" + assert relation.assurance.backend_conformance_status == "bounded" assert { "contracts/profiles/behavioral-relation/history/participant-opacity-baseline-v1-sem-231-rev2.json", "contracts/profiles/behavioral-relation/participant-opacity-theorem-v1.json", @@ -74,7 +75,7 @@ def test_catalog_defines_one_sided_participant_predicate_opacity() -> None: def test_opacity_binding_requires_a_revisioned_parameter_profile_and_assurance_axis() -> None: - catalog = load_behavioral_relation_catalog() + catalog = load_behavioral_relation_catalog_revision("rev9") missing_profile = _opacity_binding().model_copy( update={ diff --git a/implementations/python/tests/test_sem_233_adversarial_boundary_flow.py b/implementations/python/tests/test_sem_233_adversarial_boundary_flow.py new file mode 100644 index 000000000..e332bdd1f --- /dev/null +++ b/implementations/python/tests/test_sem_233_adversarial_boundary_flow.py @@ -0,0 +1,473 @@ +"""Bounded falsification evidence for SEM-233 boundary-flow semantics.""" + +from __future__ import annotations + +from dataclasses import replace +from itertools import permutations + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st +from raes_contracts.behavioral_relations import load_behavioral_relation_catalog +from sem233_boundary_flow_model import ( + FlowGateState, + FlowOperation, + FlowProfile, + FlowValue, + SinkPolicy, + UnsupportedFlow, + carry, + derive, + join_labels, + label_leq, + may_flow_at_sink, + rewrite_coordinate, +) + +CONFIDENTIALITY_UNIVERSE = frozenset( + { + "conf:audience:red", + "conf:destination:vault", + "conf:sink:no-error-output", + "conf:deny-unresolved", + } +) +INTEGRITY_UNIVERSE = frozenset( + { + "int:influence:external-mail", + "int:influence:participant-alice", + "int:influence:tool-search", + "int:deny-unresolved", + } +) + +PROFILE = FlowProfile( + profile_id="participant-boundary-flow-policy-v1", + profile_revision="rev1", + authority_revision="sem-233/rev1", + confidentiality_universe=CONFIDENTIALITY_UNIVERSE, + integrity_universe=INTEGRITY_UNIVERSE, +) + + +def _value( + ref: str, + *, + confidentiality: frozenset[str] = frozenset(), + integrity: frozenset[str] = frozenset(), + participant: str = "participant:alice", + episode: str = "episode:one", + cut: str = "state-cut:7", +) -> FlowValue: + return FlowValue( + value_ref=ref, + label=PROFILE.label(confidentiality=confidentiality, integrity=integrity), + provenance_refs=frozenset({f"provenance:{ref}"}), + influence_refs=frozenset({f"influence:{ref}"}), + participant_ref=participant, + episode_ref=episode, + policy_ref="policy:participant-egress", + policy_revision="rev3", + state_cut_ref=cut, + ) + + +def _sink(**overrides: object) -> SinkPolicy: + values: dict[str, object] = { + "sink_ref": "sink:external-tool", + "destination_ref": "destination:vault", + "profile_id": PROFILE.profile_id, + "profile_revision": PROFILE.profile_revision, + "policy_ref": "policy:participant-egress", + "policy_revision": "rev3", + "state_cut_ref": "state-cut:7", + "satisfied_confidentiality": CONFIDENTIALITY_UNIVERSE, + "satisfied_integrity": INTEGRITY_UNIVERSE, + } + values.update(overrides) + return SinkPolicy(**values) + + +def test_profile_closes_two_independent_obligation_coordinates() -> None: + assert PROFILE.bottom.confidentiality == frozenset() + assert PROFILE.bottom.integrity == frozenset() + assert PROFILE.top.confidentiality == CONFIDENTIALITY_UNIVERSE + assert PROFILE.top.integrity == INTEGRITY_UNIVERSE + assert PROFILE.unknown_label == PROFILE.top + + confidential = PROFILE.label(confidentiality={"conf:audience:red"}) + influenced = PROFILE.label(integrity={"int:influence:external-mail"}) + + assert confidential.confidentiality + assert not confidential.integrity + assert influenced.integrity + assert not influenced.confidentiality + with pytest.raises(UnsupportedFlow, match="outside the closed confidentiality universe"): + PROFILE.label(confidentiality={"trusted"}) + + +@settings(max_examples=60) +@given( + a_conf=st.sets(st.sampled_from(sorted(CONFIDENTIALITY_UNIVERSE))), + a_int=st.sets(st.sampled_from(sorted(INTEGRITY_UNIVERSE))), + b_conf=st.sets(st.sampled_from(sorted(CONFIDENTIALITY_UNIVERSE))), + b_int=st.sets(st.sampled_from(sorted(INTEGRITY_UNIVERSE))), + c_conf=st.sets(st.sampled_from(sorted(CONFIDENTIALITY_UNIVERSE))), + c_int=st.sets(st.sampled_from(sorted(INTEGRITY_UNIVERSE))), +) +def test_join_is_closed_associative_commutative_idempotent_and_monotone( + a_conf: set[str], + a_int: set[str], + b_conf: set[str], + b_int: set[str], + c_conf: set[str], + c_int: set[str], +) -> None: + a = PROFILE.label(confidentiality=a_conf, integrity=a_int) + b = PROFILE.label(confidentiality=b_conf, integrity=b_int) + c = PROFILE.label(confidentiality=c_conf, integrity=c_int) + + assert join_labels(PROFILE, (a, a)) == a + assert join_labels(PROFILE, (a, b)) == join_labels(PROFILE, (b, a)) + assert join_labels(PROFILE, (join_labels(PROFILE, (a, b)), c)) == join_labels( + PROFILE, (a, join_labels(PROFILE, (b, c))) + ) + joined = join_labels(PROFILE, (a, b, c)) + assert joined.confidentiality <= CONFIDENTIALITY_UNIVERSE + assert joined.integrity <= INTEGRITY_UNIVERSE + + smaller = PROFILE.label( + confidentiality=a.confidentiality & b.confidentiality, + integrity=a.integrity & b.integrity, + ) + larger = PROFILE.label( + confidentiality=a.confidentiality | b.confidentiality, + integrity=a.integrity | b.integrity, + ) + assert label_leq(smaller, larger) + assert label_leq( + join_labels(PROFILE, (smaller, c)), + join_labels(PROFILE, (larger, c)), + ) + + +def test_derivation_is_traversal_independent_and_carries_every_possible_influence() -> None: + inputs = ( + _value("mail", integrity=frozenset({"int:influence:external-mail"})), + _value("secret", confidentiality=frozenset({"conf:destination:vault"})), + _value("tool", integrity=frozenset({"int:influence:tool-search"})), + ) + + outcomes = { + derive( + PROFILE, + result_ref="proposal:1", + inputs=ordering, + participant_ref="participant:alice", + episode_ref="episode:one", + policy_ref="policy:participant-egress", + policy_revision="rev3", + state_cut_ref="state-cut:7", + ).semantic_state + for ordering in permutations(inputs) + } + + assert len(outcomes) == 1 + label, provenance, influences = outcomes.pop() + assert label.confidentiality == frozenset({"conf:destination:vault"}) + assert label.integrity == frozenset({"int:influence:external-mail", "int:influence:tool-search"}) + assert {"provenance:mail", "provenance:secret", "provenance:tool"} <= provenance + assert {"influence:mail", "influence:secret", "influence:tool"} <= influences + + +def test_missing_label_and_mismatched_profile_fail_closed() -> None: + unlabeled = _value("mail") + unlabeled = unlabeled.without_label() + derived = derive( + PROFILE, + result_ref="proposal:missing-label", + inputs=(unlabeled,), + participant_ref="participant:alice", + episode_ref="episode:one", + policy_ref="policy:participant-egress", + policy_revision="rev3", + state_cut_ref="state-cut:7", + ) + + assert derived.label == PROFILE.unknown_label + assert derived.supported is False + assert may_flow_at_sink(PROFILE, derived, _sink(), FlowGateState.allowing()) is False + + other_profile = FlowProfile( + profile_id=PROFILE.profile_id, + profile_revision="rev2", + authority_revision="sem-233/rev2", + confidentiality_universe=CONFIDENTIALITY_UNIVERSE, + integrity_universe=INTEGRITY_UNIVERSE, + ) + with pytest.raises(UnsupportedFlow, match="profile coordinates"): + join_labels(PROFILE, (PROFILE.bottom, other_profile.bottom)) + + +@pytest.mark.parametrize("missing_field", ["provenance_refs", "influence_refs"]) +def test_missing_provenance_or_influence_fails_closed(missing_field: str) -> None: + value = replace(_value("incomplete-history"), **{missing_field: frozenset()}) + + assert may_flow_at_sink(PROFILE, value, _sink(), FlowGateState.allowing()) is False + + +def test_redaction_cannot_launder_confidentiality_or_integrity() -> None: + source = _value( + "retrieved-secret", + confidentiality=frozenset({"conf:destination:vault"}), + integrity=frozenset({"int:influence:external-mail"}), + ) + redacted = derive( + PROFILE, + result_ref="redaction:1", + inputs=(source,), + participant_ref="participant:bob", + episode_ref="episode:one", + policy_ref="policy:participant-egress", + policy_revision="rev3", + state_cut_ref="state-cut:7", + ) + + assert redacted.label == source.label + assert source.provenance_refs <= redacted.provenance_refs + assert source.influence_refs <= redacted.influence_refs + + +def test_declassification_and_endorsement_change_only_the_named_coordinate() -> None: + source = _value( + "candidate", + confidentiality=frozenset({"conf:audience:red", "conf:destination:vault"}), + integrity=frozenset({"int:influence:external-mail"}), + ) + declassified = rewrite_coordinate( + PROFILE, + source, + result_ref="candidate:declassified", + operation=FlowOperation.DECLASSIFICATION, + remove_confidentiality=frozenset({"conf:audience:red"}), + remove_integrity=frozenset(), + authority_ref="authority:release-officer", + sink_ref="sink:external-tool", + state_cut_ref="state-cut:7", + ) + endorsed = rewrite_coordinate( + PROFILE, + source, + result_ref="candidate:endorsed", + operation=FlowOperation.ENDORSEMENT, + remove_confidentiality=frozenset(), + remove_integrity=frozenset({"int:influence:external-mail"}), + authority_ref="authority:integrity-officer", + sink_ref="sink:external-tool", + state_cut_ref="state-cut:7", + ) + + assert declassified.label.confidentiality == frozenset({"conf:destination:vault"}) + assert declassified.label.integrity == source.label.integrity + assert endorsed.label.confidentiality == source.label.confidentiality + assert endorsed.label.integrity == frozenset() + assert source.influence_refs == endorsed.influence_refs + assert source.provenance_refs <= declassified.provenance_refs + assert source.provenance_refs <= endorsed.provenance_refs + assert source.label.confidentiality == frozenset({"conf:audience:red", "conf:destination:vault"}) + assert source.label.integrity == frozenset({"int:influence:external-mail"}) + assert declassified.rewrites[-1].profile_id == PROFILE.profile_id + assert declassified.rewrites[-1].profile_revision == PROFILE.profile_revision + assert declassified.rewrites[-1].policy_ref == source.policy_ref + assert declassified.rewrites[-1].policy_revision == source.policy_revision + + +@pytest.mark.parametrize( + "operation", + [ + FlowOperation.APPROVAL, + FlowOperation.ADMISSION, + FlowOperation.AUTHENTICATION, + FlowOperation.AUTHORIZATION, + FlowOperation.REDACTION, + FlowOperation.TRANSFORMATION, + ], +) +def test_non_release_operations_cannot_rewrite_either_coordinate(operation: FlowOperation) -> None: + source = _value( + "candidate", + confidentiality=frozenset({"conf:audience:red"}), + integrity=frozenset({"int:influence:external-mail"}), + ) + + with pytest.raises(UnsupportedFlow, match="cannot rewrite flow coordinates"): + rewrite_coordinate( + PROFILE, + source, + result_ref=f"candidate:{operation.value}", + operation=operation, + remove_confidentiality=source.label.confidentiality, + remove_integrity=source.label.integrity, + authority_ref="authority:supervisor", + sink_ref="sink:external-tool", + state_cut_ref="state-cut:7", + ) + + +def test_handoff_and_episode_replay_preserve_labels_provenance_and_influence() -> None: + labeled_source = _value( + "shared-context", + confidentiality=frozenset({"conf:audience:red"}), + integrity=frozenset({"int:influence:participant-alice"}), + ) + source = rewrite_coordinate( + PROFILE, + labeled_source, + result_ref="shared-context:declassified", + operation=FlowOperation.DECLASSIFICATION, + remove_confidentiality=frozenset({"conf:audience:red"}), + remove_integrity=frozenset(), + authority_ref="authority:release-officer", + sink_ref="sink:participant-handoff", + state_cut_ref="state-cut:7", + ) + handed_off = carry( + PROFILE, + source, + result_ref="handoff:bob", + participant_ref="participant:bob", + episode_ref="episode:one", + policy_ref="policy:participant-egress", + policy_revision="rev3", + state_cut_ref="state-cut:7", + ) + replayed = carry( + PROFILE, + handed_off, + result_ref="replay:episode-two", + participant_ref="participant:bob", + episode_ref="episode:two", + policy_ref="policy:participant-egress", + policy_revision="rev3", + state_cut_ref="state-cut:8", + ) + + assert handed_off.value_ref != source.value_ref + assert replayed.value_ref != handed_off.value_ref + assert replayed.label == source.label + assert source.provenance_refs <= replayed.provenance_refs + assert source.influence_refs <= replayed.influence_refs + assert replayed.rewrites == source.rewrites + assert replayed.participant_ref == "participant:bob" + assert replayed.episode_ref == "episode:two" + + +def test_final_sink_is_exact_cut_and_deny_first_across_independent_gates() -> None: + value = _value( + "action-argument", + confidentiality=frozenset({"conf:destination:vault"}), + integrity=frozenset({"int:influence:tool-search"}), + ) + assert may_flow_at_sink(PROFILE, value, _sink(), FlowGateState.allowing()) is True + assert ( + may_flow_at_sink( + PROFILE, + value, + _sink(state_cut_ref="state-cut:8"), + FlowGateState.allowing(), + ) + is False + ) + assert ( + may_flow_at_sink( + PROFILE, + value, + _sink(satisfied_integrity=frozenset()), + FlowGateState.allowing(), + ) + is False + ) + + for gate_name in FlowGateState.gate_names(): + assert ( + may_flow_at_sink( + PROFILE, + value, + _sink(), + FlowGateState.allowing().deny(gate_name), + ) + is False + ) + + +def test_coordinate_rewrite_is_scoped_to_its_exact_sink_and_cut() -> None: + source = _value( + "confidential-output", + confidentiality=frozenset({"conf:destination:vault"}), + ) + declassified = rewrite_coordinate( + PROFILE, + source, + result_ref="confidential-output:released", + operation=FlowOperation.DECLASSIFICATION, + remove_confidentiality=frozenset({"conf:destination:vault"}), + remove_integrity=frozenset(), + authority_ref="authority:release-officer", + sink_ref="sink:external-tool", + state_cut_ref="state-cut:7", + ) + + assert may_flow_at_sink(PROFILE, declassified, _sink(), FlowGateState.allowing()) is True + assert ( + may_flow_at_sink( + PROFILE, + declassified, + _sink(sink_ref="sink:participant-output"), + FlowGateState.allowing(), + ) + is False + ) + replayed = carry( + PROFILE, + declassified, + result_ref="confidential-output:episode-two", + participant_ref="participant:alice", + episode_ref="episode:two", + policy_ref="policy:participant-egress", + policy_revision="rev3", + state_cut_ref="state-cut:8", + ) + assert ( + may_flow_at_sink( + PROFILE, + replayed, + _sink(state_cut_ref="state-cut:8"), + FlowGateState.allowing(), + ) + is False + ) + + +def test_catalog_keeps_one_relation_and_bounds_sem_233_evidence() -> None: + catalog = load_behavioral_relation_catalog() + + assert catalog.taxonomy_revision == "rev12" + relation = catalog.relations["policy-noninterference"] + assert "SEM-233" in relation.definition + assert "sem-233/rev1" in relation.observation_projection.policy_revision + assert "test_sem_233_adversarial_boundary_flow.py" in " ".join(relation.bounded_evidence) + assert relation.assurance.proof_status == "deliberately-unproved" + assert { + "denning-1976", + "myers-liskov-1998", + "myers-sabelfeld-zdancewic-2006", + "cecchetti-myers-arden-2017", + } <= set(relation.source_refs) + + surfaces = [ + surface for surface in catalog.claim_surfaces if surface.surface_id == "participant-information-flow-policy" + ] + assert len(surfaces) == 1 + assert surfaces[0].intended_relation_ids == ["policy-noninterference"] + assert "SEM-233" in surfaces[0].evidence_boundary + assert any("runtime" in claim.lower() for claim in surfaces[0].explicit_non_claims) diff --git a/specs/formal/artifact-transformations/README.md b/specs/formal/artifact-transformations/README.md new file mode 100644 index 000000000..1d562110e --- /dev/null +++ b/specs/formal/artifact-transformations/README.md @@ -0,0 +1,62 @@ +# Artifact transformation invariants + +This subsystem defines the bounded FM2 obligations for AUT-810. The executable +surface consists of operation-specific functions in `raes.transformations`, +the closed `artifact-transformation-report/v1` contract, and the focused +transformation conformance corpus. It is not a generic patch or migration +language. + +## Invariants + +For an admitted source artifact (S), explicit request (R), and closed policy +(P), an operation returns exactly one of: + +1. **Complete success:** an independently reconstructed and admitted target + (T), plus a report whose target digest binds (T); or +2. **Complete refusal:** no target and a report containing at least one bounded + diagnostic for a failed condition. + +The following invariants hold for both outcomes: + +- **Purity and isolation.** The output is a newly admitted value derived only + from explicit arguments. The source's canonical bytes do not change. +- **Determinism.** Equal canonical inputs, request, and policy produce equal + canonical output and equal reports. Reports contain no clock, random, + filesystem, network, process, or caller-identity input. +- **Exact selection.** SDL declarations are selected by one exact canonical + address. Aliases, collisions, unsupported identities, and ambiguity are + refusals. +- **Admission closure.** Complete structural and semantic admission runs after + every SDL candidate is reconstructed. A dangling or illegal resolved + reference cannot be returned as output. +- **Named comparison.** Preservation is recorded under a named profile and a + bounded evidence set, never as an unqualified Boolean claim. +- **Loss authorization.** Loss is rejected by default. Success with loss + requires the policy to name every allowed loss kind and the report to carry + the corresponding typed loss diagnostic. +- **Linked-artifact consistency.** Supplied external-concept subjects must bind + the exact source digest. A rename retargets every supplied subject digest and + the changed canonical reference atomically; stale documents cause refusal. + +For declaration rename, let (f) map the selected address to its replacement +and act as identity elsewhere. The `sdl-declaration-identity-transport/v1` +check requires: + +- (f) is injective over the declaration index; +- the declaration kind and declaration count are unchanged; +- every rewritten reference is admitted against the target index; and +- applying (f^{-1}) to the admitted target restores the source's canonical + SDL bytes. + +This is finite verification over the supplied artifacts. It does not prove +behavioral, observational, epistemic, strategic, backend, or scientific +equivalence. `canonical-artifact-identity` establishes equality only under the +named owning canonicalization profile. + +## Executable evidence + +- Unit, property, composition, reference, loss, concept-binding, and portable + contract coverage: `implementations/python/tests/test_artifact_transformations.py` +- Typed portable report: `contracts/schemas/artifact-transformations/artifact-transformation-report-v1.json` +- Focused cases: `contracts/fixtures/artifact-transformations-v1/cases` +- Case executor: `implementations/python/packages/raes_conformance/artifact_transformations.py` diff --git a/specs/formal/assurance-fulfillment.yaml b/specs/formal/assurance-fulfillment.yaml index 3fb5aa4da..d166e4eff 100644 --- a/specs/formal/assurance-fulfillment.yaml +++ b/specs/formal/assurance-fulfillment.yaml @@ -85,6 +85,9 @@ subsystems: - id: behavioral-relations path: specs/formal/behavioral-relations fm_level: FM2 + - id: artifact-transformations + path: specs/formal/artifact-transformations + fm_level: FM2 - id: time-model path: specs/formal/time-model fm_level: FM3 @@ -94,6 +97,18 @@ subsystems: # a known gap with an ISO date and the tracking issue(s) under which it will be # closed. entries: + - subsystem: artifact-transformations + delivered_artifacts: + - kind: invariant_list + path: specs/formal/artifact-transformations/README.md + - kind: unit_tests + path: implementations/python/tests/test_artifact_transformations.py + - kind: typed_ir_or_contract_coverage + path: contracts/schemas/artifact-transformations/artifact-transformation-report-v1.json + - kind: property_based_or_differential_tests + path: implementations/python/tests/test_artifact_transformations.py + waived_artifacts: [] + - subsystem: time-model delivered_artifacts: - kind: invariant_list @@ -333,8 +348,9 @@ entries: model exists; #486 tracks the executable invariant oracle. - subsystem: participant-semantics - # SEM-230 adds an executable bounded policy model and ASR-535 adds bounded - # enforcement and backend-conformance falsification over it, while + # SEM-230 adds an executable bounded policy model, SEM-233 adds an exact + # two-coordinate algebra with a test-local finite falsification model, and + # ASR-535 adds bounded enforcement and backend-conformance falsification, while # runtime-wide typed contract coverage remains incomplete for the broader # participant surface. ASR-535 makes no model-check or proof claim. delivered_artifacts: @@ -344,12 +360,18 @@ entries: path: implementations/python/tests/test_sem_230_information_flow_control.py - kind: unit_tests path: implementations/python/tests/test_asr_535_participant_flow_assurance.py + - kind: unit_tests + path: implementations/python/tests/test_sem_233_adversarial_boundary_flow.py - kind: property_based_or_differential_tests path: implementations/python/tests/test_sem_230_information_flow_control.py - kind: property_based_or_differential_tests path: implementations/python/tests/test_asr_535_participant_flow_assurance.py + - kind: property_based_or_differential_tests + path: implementations/python/tests/test_sem_233_adversarial_boundary_flow.py - kind: abstract_state_machine_model path: specs/formal/participant-semantics/information-flow-control.md + - kind: abstract_state_machine_model + path: specs/formal/participant-semantics/adversarial-flow-control.md waived_artifacts: - kind: typed_ir_or_contract_coverage date: 2026-06-13 diff --git a/specs/formal/participant-semantics/README.md b/specs/formal/participant-semantics/README.md index 1b488a134..f98548fae 100644 --- a/specs/formal/participant-semantics/README.md +++ b/specs/formal/participant-semantics/README.md @@ -53,14 +53,16 @@ model-check, runtime mapping, backend conformance, noninterference, or opacity result. Issue #812 and ADR-101 add the SEM-233 boundary-flow and ASR-536 -intentional-subversion evaluation design. Their normative profiles are in +intentional-subversion evaluation design. Issue #1001 publishes the exact +`sem-233/rev1` two-coordinate obligation algebra, carrier mapping, and bounded +falsification evidence; ASR-536 remains design-only. Their normative profiles are in [`adversarial-flow-control.md`](adversarial-flow-control.md). The design extends the existing participant, control, crossing, runtime, backend, and experiment carriers with independent confidentiality and integrity coordinates, conservative derivation, final-sink mediation, and explicit -attack-protocol variables. It does not report runtime enforcement, backend -realization, monitor honesty, covert-channel control, or adversarial -robustness. +attack-protocol variables. The SEM-233 finite model is not a portable contract, +runtime enforcement, backend realization, monitor-honesty, covert-channel, or +adversarial-robustness result. Issue #813 and ADR-102 add the SEM-234 mixed-composition and ASR-537 realization/transfer-evidence design. Their normative profiles are in diff --git a/specs/formal/participant-semantics/adversarial-flow-control.md b/specs/formal/participant-semantics/adversarial-flow-control.md index 2d19c7365..58b0c1af8 100644 --- a/specs/formal/participant-semantics/adversarial-flow-control.md +++ b/specs/formal/participant-semantics/adversarial-flow-control.md @@ -1,10 +1,14 @@ -# SEM-233 and ASR-536 Adversarial Participant Flow Control +# SEM-233 And ASR-536 Adversarial Participant Flow Control -Status: design authority; positive implementation and evaluation obligations -remain DRAFT. +Status: SEM-233 definition authority is published; portable-contract, +runtime, backend, and ASR-536 evaluation obligations remain DRAFT. Requirements: `SEM-233`, `ASR-536`. +SEM-233 authority revision: `sem-233/rev1`. + +Flow-policy profile: `participant-boundary-flow-policy-v1@rev1`. + Decision authority: [ADR-101](../../../docs/decisions/adrs/adr-101-adversarial-participant-flow-control.md). @@ -50,61 +54,131 @@ Every derived identity is fresh and binds the source identities, derivation kind, profile and revision, exact policy/state cut, authority, destination or sink, and safe evidence refs. No release operation mutates a prior fact. -## Flow labels +## Exact Revision-1 Flow Algebra + +For revision 1, profile `phi` declares two independent, closed, finite +universes: + +```text +C_phi = confidentiality-obligation tokens admitted by phi +I_phi = integrity-obligation tokens admitted by phi +``` + +A confidentiality token is one revisioned policy clause, such as an +owner-relative audience, destination, or sink restriction. An integrity token +is one unresolved possible-influence obligation. The source-label resolver +maps a possible writer or influence to the applicable integrity tokens; the +immutable influence refs remain in provenance even if a later endorsement +discharges an obligation. Tokens are stable typed refs, not raw values or open +metadata keys. -The effective label of `x` is: +The effective label domain is the product powerset: ```text Label_phi(x,c) = (Conf_phi(x,c), Int_phi(x,c)) +Conf_phi(x,c) in P(C_phi) +Int_phi(x,c) in P(I_phi) +``` + +Normalization validates every token against the named profile revision, +removes duplicates, and orders the canonical serialization lexically. An +unknown token, unresolved profile/revision, or cross-profile comparison is +unsupported; it is never silently discarded or coerced. + +For labels `l1 = (C1, I1)` and `l2 = (C2, I2)`, revision 1 defines: + +```text +l1 <=_phi l2 iff C1 subset-of C2 and I1 subset-of I2 +l1 join_phi l2 = (C1 union C2, I1 union I2) +bottom_phi = (empty-set, empty-set) +top_phi = (C_phi, I_phi) ``` -where: +Upward means at least as restrictive: more confidentiality clauses must be +satisfied and more possible-influence obligations must be admitted or +explicitly endorsed. Owner-relative clauses make labels for mutually +distrustful principals or audiences incomparable when neither obligation set +contains the other. Neither coordinate is a global classification or trust +ladder. -- `Conf_phi(x,c)` is ordered by permitted audience, principal, destination, and - sink exposure; higher labels are at least as restrictive; and -- `Int_phi(x,c)` conservatively records origins or possible writers that may - have influenced `x`, plus the sink-required integrity predicate. +The join is closed, associative, commutative, idempotent, and monotone in each +argument. Its componentwise union makes it independent of traversal order. +Adding a possible input cannot remove an obligation, and a successful decision +for `l2` cannot imply success for a strictly more restrictive label unless the +sink satisfies the added obligations. -These coordinates are independent. Authentication, signatures, hashes, -markings, sensitivity, confidence, roles, and monitor scores can contribute -governed evidence but cannot define both coordinates. +For sink policy `S` at the exact cut, let `SatC_phi(S)` and `SatI_phi(S)` be the +closed sets of confidentiality and integrity obligations that its resolved +audience, destination, sink class, and release authorities satisfy: + +```text +ConfidentialityObligationsSatisfied_phi(l,S) iff Conf_phi(l) subset-of SatC_phi(S) +IntegrityObligationsSatisfied_phi(l,S) iff Int_phi(l) subset-of SatI_phi(S) +``` + +The predicates are independent. A sink may satisfy one and fail the other. +Authentication, signatures, hashes, markings, sensitivity, confidence, roles, +and monitor scores may contribute evidence to a revisioned resolver, but none +is a label coordinate or satisfies both predicates. ### Source defaults -`SourceLabel_phi(src,c)` resolves through the revisioned source authority. -When the source, profile, revision, or authority cannot be resolved, the -result is the profile's maximum-confidentiality and minimum-trust default. A -deployment may use a narrower deny-only representation, but it cannot default -to public or trusted. +`SourceLabel_phi(src,c)` resolves through the revisioned source authority. A +known source returns a normalized member of `P(C_phi) x P(I_phi)`. When the +source, profile, revision, label, or authority cannot be resolved, the result +is either typed `unsupported` or the deny-equivalent `top_phi` accompanied by +an unresolved status. A sink decision rejects unresolved status even if its +ordinary obligation sets would otherwise cover `top_phi`. Empty input is not +evidence for `bottom_phi`, public, or trusted. ### Conservative composition -For a derivation `d` whose possible inputs are `I(d)`: +For a derivation `d` whose complete possible-input set is `Inputs(d)`: ```text -Conf_phi(d,c) = join_conf { Conf_phi(x,c) | x in I(d) } -Int_phi(d,c) = join_int { Int_phi(x,c) | x in I(d) } +Conf_phi(d,c) = union { Conf_phi(x,c) | x in Inputs(d) } +Int_phi(d,c) = union { Int_phi(x,c) | x in Inputs(d) } ``` -`join_conf` is the profile's least upper bound. `join_int` is conservative -influence union under the profile's integrity order. - -An opaque participant, model, service, script, summary, copy, redaction, or -transformation retains the join of every input that could have influenced its -result. A typed transformation can narrow the influence set only when its -closed contract and evidence establish that relation. Apparatus claims alone -cannot do so. +`Inputs(d)` includes participant context, retained memory, shared/joint state, +tool and destination arguments, error branches, and each participant, tool, +service, transformation, monitor, or apparatus source that may influence the +result. An opaque model, script, summary, copy, redaction, edit, parse, or +transformation retains all inputs. A typed transformation may exclude an +influence only through a closed, revisioned non-influence relation and safe +evidence; omission from caller-supplied provenance is not such evidence. ### Cross-participant and cross-episode carriage API-409 handoff, API-423 crossing, controller change, participant change, -shared state, joint state, or episode reset never clears labels or provenance. -A receiving participant inherits the effective upstream label and source -history through the governed crossing. +shared state, joint state, or episode reset never clears labels, unresolved +status, provenance, influence refs, or release history. A receiving +participant inherits the effective upstream state through the governed +crossing. Cross-episode replay binds the original source, profile, revision, policy decisions, release events, SEM-230 memory scope, and expected history heads. -Replay under a later cut receives a fresh decision. +Replay under a later cut receives a fresh final-sink decision; the later policy +does not reinterpret the historical label or release. + +## Typed Carrier And Derivation Mapping + +The mapping is semantic and reference-based. Issue #1001 does not change the +incumbent contracts. + +| Flow stage | Existing typed owner | Required SEM-233 refs and possible inputs | +| --- | --- | --- | +| observation, retrieval, tool result, runtime fact | participant observation envelope and runtime-fact declaration/version/binding/sink models | source-label decision, profile/revision, source/provenance refs, and every source or apparatus that may affect the value; sensitivity is resolver evidence only | +| participant context, information state, retained memory, shared/joint state | SEM-230 participant view/history/memory and episode carriers | effective label, unresolved status, derivation/release history, and all retained upstream source/influence refs | +| proposal, participant output, action and destination/tool arguments | participant decision surfaces, action definitions, and `ParticipantActionAdmissionRequest` | join of context, memory, observations, participant process, transformation apparatus, and every argument source; structural/action admission remains a separate gate | +| controller change and handoff | ACT-617 and API-409 control occurrences/context validation | unchanged flow label and history plus fresh controller/authority occurrence refs; handoff is not declassification, endorsement, or admission | +| crossing, projection, redaction, transformation, disclosure | API-423 typed subjects, predecessor stages, exact policy/cut refs, and contextual validator | source/result identities, transformation relation, full input label join, immutable provenance/influence refs, and any coordinate-specific release ref | +| participant/external output, delivery, stream, callback, persistent write, and error | API-423 disclosure/delivery/observation stages and participant-facing views | a fresh final-sink decision for every serialization/effect; disclosure, delivery, and observation remain distinct; each stream chunk is independently governed or derived from one governed complete value | +| snapshot, retry, replay, and cross-episode reuse | RUN-310/RUN-319 snapshot/history/idempotency/expected-head carriers | original profile/policy/cut, label, unresolved status, provenance/influence/release histories, new exact cut, and fresh sink decision | + +An open `taint`, `security_labels`, `context`, `metadata`, prompt, diagnostic, +header, query parameter, environment variable, or backend-options bag is not a +typed carrier for this mapping. ## Distinct operations @@ -127,6 +201,11 @@ The following operations are semantically distinct: | advice | supplies evidence or recommendation without authority | | execution or delivery | realizes an already permitted effect or disclosure | +Declassification removes only named members of `Conf_phi`; endorsement removes +only named members of `Int_phi`. Both require the obligation to exist in the +source label and require explicit authority for the exact removal. Endorsement +does not delete the possible-writer/influence refs that caused the obligation. + Declassification and endorsement each bind: - source and fresh result identity; @@ -146,11 +225,14 @@ For effective label `l`, exact cut `c`, sink `s`, authority `a`, destination `d`, participant `p`, and effective capability posture `k`: ```text -MayRelease_phi(p,l,s,d,a,c,k) - = FlowPolicy_phi(l,s,d,c) +MayFlowAtSink_phi(p,l,s,d,a,c,k) + = ConfidentialityObligationsSatisfied_phi(l,s,d,c) + and IntegrityObligationsSatisfied_phi(l,s,d,c) + and AuthenticatedIdentityBinding(a,p,s,d,c) and Authorized(a,p,s,d,c) and Admitted(p,s,c) and EffectiveCapability(k,phi,s) + and ExistingMayCrossAndTransformationGates(p,l,s,d,c) and FreshHeads(c) ``` @@ -158,7 +240,8 @@ Every conjunct is deny-first. Heuristic monitor output is not a conjunct that can turn denial into permission. A profile may require monitor evidence as a precondition, but only deterministic policy interprets that evidence. -The runtime evaluates `MayRelease`: +The downstream runtime obligation owned by issue #1003 is to evaluate +`MayFlowAtSink`: - after the last label, provenance, policy, authority, destination, participant, audience, capability, and state-cut resolution; @@ -167,10 +250,11 @@ The runtime evaluates `MayRelease`: - immediately before participant-facing or external serialization, streaming, delivery, error output, or other disclosure. -It commits the decision and all predecessor/history changes atomically before -effect. A denial, unsupported result, missing label or provenance, ambiguous -join, stale cut, history-head conflict, or failed commit causes no external -call and no disclosure. +That future runtime must commit the decision and all predecessor/history +changes atomically before effect. A denial, unsupported result, missing label +or provenance, ambiguous join, stale cut, history-head conflict, or failed +commit causes no external call and no disclosure. This definition and its +test-local finite model do not implement that boundary. Each streaming chunk is either governed before release or derived from a complete materialized value whose entire release is governed by the profile. @@ -184,7 +268,7 @@ limitations, and safe evidence. ```text MonitorResult != Authorization -MonitorFailure does not widen MayRelease +MonitorFailure does not widen MayFlowAtSink ``` Monitor roles are apparatus and experiment declarations. A profile states: @@ -292,6 +376,22 @@ Every denied case records zero prohibited `RuntimeTarget` calls and zero participant/external disclosure. Tests also inspect append-only histories, safe audit/error evidence, and replay. +## Bounded Executable Evidence + +`implementations/python/tests/sem233_boundary_flow_model.py` is a test-local +finite interpretation of the revision-1 powerset algebra. Its companion +`test_sem_233_adversarial_boundary_flow.py` exercises closure, associativity, +commutativity, idempotence, monotonicity, traversal-order independence, +coordinate independence, conservative derivation, unknown/missing labels, +cross-profile joins, laundering attempts, coordinate-specific rewrites, +release-operation conflation, handoff, cross-episode replay, stale cuts, sink +obligations, and every deny-first final predicate conjunct. + +Those cases are bounded falsification evidence over synthetic refs. The model +is not a portable contract, runtime implementation, backend realization, +complete instrumentation claim, model check, proof, universal +noninterference result, or intentional-subversion evaluation. + ## Security and evidence boundary - Closed DTOs and request-size guards apply to portable bodies. Touched path, diff --git a/specs/formal/participant-semantics/autonomous-execution.md b/specs/formal/participant-semantics/autonomous-execution.md index d2b6867d4..49a2b4f8b 100644 --- a/specs/formal/participant-semantics/autonomous-execution.md +++ b/specs/formal/participant-semantics/autonomous-execution.md @@ -4,6 +4,15 @@ This specification defines DSL-437 execution as a composition of existing participant semantics and the shared time model. It introduces no background actor, inject, or private clock. +## ACT-605 Baseline Behavior Profile Coverage + +ACT-605's declarative baseline and background behavior profiles are the +versioned autonomous participant policies defined below. Ordinary user, +automation, and ambient scenario activity reuse the same participant, action, +observation, shared-time, native-execution, and evidence invariants. ACT-605 +adds no separate profile root or actor kind and changes none of the v1, v2, or +v3 policy semantics. + ## V1 Authored Policy For behavior specification \(B\), autonomous policy \(P\) contains: diff --git a/specs/formal/participant-semantics/participant-opacity-proof-evidence.json b/specs/formal/participant-semantics/participant-opacity-proof-evidence.json index ee3692254..fc916be99 100644 --- a/specs/formal/participant-semantics/participant-opacity-proof-evidence.json +++ b/specs/formal/participant-semantics/participant-opacity-proof-evidence.json @@ -8,7 +8,7 @@ "taxonomy": { "taxonomy_id": "raes-behavioral-relations", "taxonomy_revision": "rev9", - "path": "contracts/concept-authority/behavioral-relations-v1.json", + "path": "contracts/concept-authority/history/behavioral-relations-v1-rev9.json", "digest": "sha256:ec893a8464f6ddba89e5848c40dccc864ae555f95996d736dbc8f12d56afb971" }, "profiles": [ @@ -30,7 +30,7 @@ "requirement": "SEM-231", "revision": "participant-predicate-opacity/rev8-baseline", "path": "specs/formal/participant-semantics/participant-predicate-opacity.md", - "digest": "sha256:6916cdadb618df6149d3e8a3ea84f1c422890b50de005686bf15a32e20220c6e" + "digest": "sha256:7322248777fe8aed3a9f276ed3a09187f2c86afeafb1df9f252cbd9f402005df" } ], "dependencies": [ @@ -288,7 +288,7 @@ "tool_sources": [ { "path": "tools/check_participant_opacity_proof.py", - "digest": "sha256:21cb9b25a921bb02616d74ee19a9c150e4ddb857a01eccab60fe3d2de30c0554" + "digest": "sha256:28bac5de7dfa1d1b893f372a221be6f33ad5bd6797fe85c247d61c45a90a699f" }, { "path": "tools/isabelle_tool.py", diff --git a/specs/formal/participant-semantics/participant-predicate-opacity.md b/specs/formal/participant-semantics/participant-predicate-opacity.md index dd8a20bdd..219d746bc 100644 --- a/specs/formal/participant-semantics/participant-predicate-opacity.md +++ b/specs/formal/participant-semantics/participant-predicate-opacity.md @@ -298,10 +298,10 @@ Assurance states are independent: | bounded testing | named finite profiles/cases, full bounds, digests, safe counterexamples | bounded | | model checking | closed finite model, explored bounds, pinned tool/version, result or counterexample | model checked for the exact baseline fixture model | | mathematical proof | theorem, assumptions, independently checkable proof, tool/digest when mechanized | proved for the abstract conditional theorem profile; no concrete RAES instance | -| runtime enforcement | complete supported-channel inventory, fail-closed mediation, durable decisions, security tests | not enforced | -| backend declaration | API-407 feature strength, required contracts, limitations, evidence refs | not declared | -| backend realization | native implementation, profile mapping, environment and provenance evidence | not realized | -| bounded backend conformance | adversarial cases, backend/profile/environment digests, sanitized reports | not tested | +| runtime enforcement | complete supported-channel inventory, fail-closed mediation, durable decisions, security tests | partial for the exact finite historical `participant-opacity-runtime-reference-v1@sem-231/runtime-rev1` profile and its governed RUN-319 crossing inventory | +| backend declaration | API-407 feature strength, required contracts, limitations, evidence refs | declared for `participant-opacity-runtime-reference-v1@sem-231/runtime-rev2` by the reference backend | +| backend realization | native implementation, profile mapping, environment and provenance evidence | partial; one backend-native finite reference realization | +| bounded backend conformance | adversarial cases, backend/profile/environment digests, sanitized reports | bounded for the exact reference backend/profile/configuration/tool/environment/probe set | A claim binding identifies exactly one `assurance_axis`. Finite execution evidence cannot satisfy universal quantification. A definition does not @@ -333,7 +333,7 @@ positive assurance axis. supervisor visibility, relation boundaries, and independent assurance state. - `ASR-535` owns bounded falsification, model-check/proof evidence discipline, safe counterexamples, and backend conformance claims. -- `RUN-319` owns any future reference-runtime mediation and durable +- `RUN-319` owns reference-runtime mediation and durable information-flow decisions. - `API-407` owns backend feature strength, required contracts, limitations, realization, and evidence disclosure. @@ -341,8 +341,13 @@ positive assurance axis. Issue #810 defines this architecture. Issue #961 delivers the closed baseline profile and bounded finite falsifier. Issue #962 delivers exact finite-state model checking for the historical baseline fixture profile. Issue #963 -delivers the abstract conditional mathematical proof. Issues #964 and #965 -separately own runtime enforcement and backend realization/conformance. +delivers the abstract conditional mathematical proof. Issue #964 delivers one +finite reference-runtime uniform-denial enforcement profile through RUN-319; +it releases no protected payload or participant state and records each withheld +egress opportunity. Issue #965 delivers the separate bounded reference-backend +declaration, native realization, and adversarial conformance lane. It retains +the failed, unsupported, weakened, and counterexample cases and makes no claim +for another backend or profile. ## Bounded Checker Contract diff --git a/specs/formal/realization/explicitness-and-realization.md b/specs/formal/realization/explicitness-and-realization.md index 7292d306d..d5e890a8c 100644 --- a/specs/formal/realization/explicitness-and-realization.md +++ b/specs/formal/realization/explicitness-and-realization.md @@ -201,6 +201,24 @@ processor layer plays no realization role under SEM-218: it compiles the typed runtime requirement and plans against backend support, but does not pick values for underspecified concerns. +For a registered non-executable inventory concern, value equality alone is not +sufficient evidence of exact realization. The concern descriptor MAY declare a +closed required verification scope. The backend manifest MUST then disclose a +concern-keyed observation capability whose scope covers that demand, and the +returned runtime snapshot MUST carry a value-free observation disclosure for +the same address, field path, domain, and requirement kind. The disclosure's +scope and observation-strength source MUST be supported by the selected +manifest declaration. Missing, malformed, under-scoped, or unsupported +corroboration is a `runtime.backend-contract-invalid` failure before snapshot +persistence. + +The initial qualified concern is `forwarding-agents`. Its scopes are `presence` +(identity, placement, and implementation inventory) and `configuration` +(authored child/configuration projection). The existing `ObservationStrength` +values remain the source axis; they do not prove forwarding behavior. Delivery, +transform, reload, health, and failure claims belong to governed proposition, +probe, truth, and evidence contracts. + **I3 — Openness is explicit.** A backend MAY realize an underspecified concern only at a point where the owning SDL schema or semantic rule explicitly designates that concern as realizable, *and* where the @@ -217,6 +235,9 @@ per-domain `RealizationSupportDeclaration` entries that name (i) the support mode (`EXACT_ONLY`, `CONSTRAINED`, or `OPEN_REALIZATION`), (ii) the supported exact-requirement-kinds, (iii) the supported constraint-kinds, and (iv) the disclosure kinds the apparatus will emit. +When a registered concern requires corroboration, the same declaration MUST +also carry a concern-keyed observation capability containing its closed +verification scope and non-`none` observation strength. Processor manifests MUST NOT carry `realization_support`: in RAES the processor layer does not realize underspecified author concerns — its manifest discloses processing features (compilation, planning, @@ -325,7 +346,10 @@ realization status, are: deployment (I1, I2, I4). *Enforced today* by the typed compiler emission on `RuntimeModel.realization_requirements` and the realization-support plus open-request envelope-subsumption gates in - `raes_processor.planner.plan`. + `raes_processor.planner.plan`. Exact registered inventory concerns with a + required verification scope also fail with + `realization.under-observed-exact-requirement` when the manifest capability + is absent or weaker. - **Error-envelope gate** — unsupported exact requirements and forbidden approximations MUST be surfaced through stable validation errors or structured diagnostics (I1, I2). *Enforced today*; the @@ -342,7 +366,9 @@ realization status, are: `RealizationProvenanceEntryModel`), which records each realized concern's explicitness class, origin, and governing designation scope and round-trips through the control-plane snapshot serializers and authenticated snapshot - response model. + response model. Concern corroboration is carried separately by + `realization_observations`, which records no realized value and is checked + against the compiled scope and selected backend manifest before persistence. - **Host / OS exposure gate** — exact values, credentials, and backend tokens MUST NOT be passed through process argv, logs, audit details, diagnostics, JSON fixtures, or semantic-profile artifacts when they diff --git a/specs/formal/runtime-contracts/participant-backend-contracts.md b/specs/formal/runtime-contracts/participant-backend-contracts.md index 927e027c3..282b176a9 100644 --- a/specs/formal/runtime-contracts/participant-backend-contracts.md +++ b/specs/formal/runtime-contracts/participant-backend-contracts.md @@ -121,7 +121,9 @@ ParticipantFeatureSupport = feature support_level constraint_refs + limitation_refs disclosure_refs + evidence_refs ``` Rules: @@ -144,6 +146,12 @@ Rules: SEM-218 explicitness/realization declarations cover cross-domain realization handling; the manifest carries both, and they are never merged or inferred from one another. +- Evidence-required features include the six participant-policy features and + `participant_predicate_opacity`. A positive declaration requires evidence; + bounded support also requires constraints, limitations, and disclosures plus + every contract in that feature's required-contract map. Opacity is not a + policy operation and is deliberately absent from + `PARTICIPANT_RUNTIME_POLICY_FEATURES`. ## API-408 - Retrieval Projections diff --git a/specs/sdl/observability-and-evidence.md b/specs/sdl/observability-and-evidence.md index 3f331c6d9..75c1e8b68 100644 --- a/specs/sdl/observability-and-evidence.md +++ b/specs/sdl/observability-and-evidence.md @@ -78,6 +78,22 @@ Authored evidence requirements: - MUST remain distinct from `experiment-derived-measure-v1` interpreted outputs. +### Forwarding agents used as measurement apparatus + +A forwarding agent whose `ownership_role` is `measurement_apparatus` MUST be +named by at least one inbound apparatus-class evidence requirement through +`source_refs`. An apparatus-class requirement MUST NOT target a forwarding +agent whose role is `system_under_test`. The forwarding agent remains the +source inventory; the evidence requirement owns channel, artifact role, +sensitivity, redaction, integrity, retention, loss, and abstract destination +semantics. + +The agent MUST NOT duplicate that relation with evidence refs and MUST NOT +carry an evidence path, pack URI, storage credential, or capture payload. +Environment-visible or comparability-relevant apparatus additionally maps to +the existing run-level augmentation disclosure. Neither ownership role nor a +configured ship target proves delivery or captured evidence. + ## Processor/Backend Operational Observability Processor/backend operational observability includes apparatus logs, diff --git a/specs/sdl/references.md b/specs/sdl/references.md index 557021420..3302322f2 100644 --- a/specs/sdl/references.md +++ b/specs/sdl/references.md @@ -340,7 +340,7 @@ the source of the row's normative meaning. | `relationships.*.domain_join.controller_refs[]` | `nodes` | semantic validation | fatal dangling, ambiguous, or controller outside target domain | [authored domain topology](authored-domain-topology.md) | [domain topology semantics](../../implementations/python/packages/raes/semantics/domain_topology.py) | | `relationships.*.shared_service.mutable_state_refs[]` | `persistent_volumes` | semantic validation | fatal dangling or conflicting state ownership | [enterprise identity and deployment tenancy](enterprise-deployment-tenancy.md) | [deployment tenancy semantics](../../implementations/python/packages/raes/semantics/deployment_tenancy.py) | | `agents.*.entity` | `entities` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant validator](../../implementations/python/packages/raes/validator/_content_objectives.py) | -| `agents.*.actions[]` | `action_contracts` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | +| `agents.*.actions[]` | `action_contracts` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | | `agents.*.starting_accounts[]` | `accounts` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant validator](../../implementations/python/packages/raes/validator/_content_objectives.py) | | `agents.*.interactive_access.*.target_ref` | `nodes` | semantic validation | fatal dangling, ambiguous, or non-VM target | [participant semantics](../formal/participant-semantics/README.md) | [participant interactive-access semantics](../../implementations/python/packages/raes/semantics/participant_interactive_access.py) | | `agents.*.interactive_access.*.account_ref` | `accounts` | semantic validation | fatal dangling, same-node mismatch, or outside participant starting accounts | [participant semantics](../formal/participant-semantics/README.md) | [participant interactive-access semantics](../../implementations/python/packages/raes/semantics/participant_interactive_access.py) | @@ -352,27 +352,27 @@ the source of the row's normative meaning. | `agents.*.allowed_subnets[]` | `infrastructure` | semantic validation | fatal unless the target is switch-backed | [participant semantics](../formal/participant-semantics/README.md) | [participant validator](../../implementations/python/packages/raes/validator/_content_objectives.py) | | `agents.*.authority_anchors[]` | `declared` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant validator](../../implementations/python/packages/raes/validator/_content_objectives.py) | | `agents.*.operating_scope[]` | `derived:operating_scope` | semantic validation | fatal dangling or ambiguous outside vm nodes, switch-backed infrastructure, services, and content | [participant semantics](../formal/participant-semantics/README.md) | [participant validator](../../implementations/python/packages/raes/validator/_content_objectives.py) | -| `agents.*.observation_boundaries[]` | `observation_boundaries` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `action_contracts.*.interactions.*.related_actions[]` | `action_contracts` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | +| `agents.*.observation_boundaries[]` | `observation_boundaries` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `action_contracts.*.interactions.*.related_actions[]` | `action_contracts` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | | `action_contracts.*.interactions.*.target` | `targetable` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant validator](../../implementations/python/packages/raes/validator/_content_objectives.py) | | `action_contracts.*.interactions.*.shared_state_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [participant semantics](../formal/participant-semantics/README.md) | [participant validator](../../implementations/python/packages/raes/validator/_content_objectives.py) | | `action_contracts.*.temporal_contracts.*.backend_disclosure_refs[]` | `derived:backend_timing_disclosures` | structural validation | fatal dangling local disclosure id | [participant semantics](../formal/participant-semantics/README.md) | [temporal model](../../implementations/python/packages/raes/participant_temporal_semantics.py) | | `action_contracts.*.backend_timing_disclosures.*.affected_temporal_ids[]` | `derived:temporal_contracts` | structural validation | fatal dangling local temporal id | [participant semantics](../formal/participant-semantics/README.md) | [temporal model](../../implementations/python/packages/raes/participant_temporal_semantics.py) | -| `observation_boundaries.*.view_rules.*.information_ref` | `derived:boundary_information` | semantic validation | fatal outside declared boundary information | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `observation_boundaries.*.view_rules.*.evidence_refs[]` | `derived:boundary_evidence` | semantic validation | fatal outside declared boundary evidence | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `observation_boundaries.*.view_transitions.*.information_ref` | `derived:boundary_view_rules` | semantic validation | fatal without a matching view rule | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `observation_boundaries.*.view_transitions.*.evidence_refs[]` | `derived:boundary_evidence` | semantic validation | fatal outside declared boundary evidence | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | +| `observation_boundaries.*.view_rules.*.information_ref` | `derived:boundary_information` | semantic validation | fatal outside declared boundary information | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `observation_boundaries.*.view_rules.*.evidence_refs[]` | `derived:boundary_evidence` | semantic validation | fatal outside declared boundary evidence | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `observation_boundaries.*.view_transitions.*.information_ref` | `derived:boundary_view_rules` | semantic validation | fatal without a matching view rule | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `observation_boundaries.*.view_transitions.*.evidence_refs[]` | `derived:boundary_evidence` | semantic validation | fatal outside declared boundary evidence | [participant semantics](../formal/participant-semantics/README.md) | [participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | | `outcome_interpretation_rules.*.source_bindings.*.ref` | `action_contracts,objectives,workflows` | semantic validation | fatal dangling for SDL-bound layers | [participant semantics](../formal/participant-semantics/README.md) | [outcome semantics](../../implementations/python/packages/raes/semantics/participant_outcome.py) | | `outcome_interpretation_rules.*.target_bindings.*.ref` | `objectives,workflows` | semantic validation | fatal dangling for SDL-bound layers | [participant semantics](../formal/participant-semantics/README.md) | [outcome semantics](../../implementations/python/packages/raes/semantics/participant_outcome.py) | -| `behavior_specifications.*.participant_refs[]` | `agents` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `behavior_specifications.*.participant_role_refs[]` | `derived:agent_roles` | semantic validation | fatal unless bound by a referenced participant | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `behavior_specifications.*.action_contract_refs[]` | `action_contracts` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `behavior_specifications.*.observation_boundary_refs[]` | `observation_boundaries` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `behavior_specifications.*.outcome_interpretation_rule_refs[]` | `outcome_interpretation_rules` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | +| `behavior_specifications.*.participant_refs[]` | `agents` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `behavior_specifications.*.participant_role_refs[]` | `derived:agent_roles` | semantic validation | fatal unless bound by a referenced participant | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `behavior_specifications.*.action_contract_refs[]` | `action_contracts` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `behavior_specifications.*.observation_boundary_refs[]` | `observation_boundaries` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `behavior_specifications.*.outcome_interpretation_rule_refs[]` | `outcome_interpretation_rules` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | | `behavior_specifications.*.authority_scope_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [behavior model](../formal/participant-behavior-model/README.md) | [behavior validator](../../implementations/python/packages/raes/validator/_content_objectives.py) | | `behavior_specifications.*.tool_affordances.*.tool_ref` | `content` | semantic validation | fatal dangling, ambiguous, or outside the `scenario-content` tools-and-artifacts reference model | [participant semantics](../formal/participant-semantics/README.md) | [tool-affordance validator](../../implementations/python/packages/raes/validator/_participant_tool_affordances.py) | -| `behavior_specifications.*.tool_affordances.*.action_contract_refs[]` | `action_contracts` | semantic validation | fatal dangling, outside the owning behavior specification, or outside a resolved participant | [participant semantics](../formal/participant-semantics/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `behavior_specifications.*.tool_affordances.*.observation_boundary_refs[]` | `observation_boundaries` | semantic validation | fatal dangling, outside the owner/participant, or without explicit view classification | [participant semantics](../formal/participant-semantics/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | +| `behavior_specifications.*.tool_affordances.*.action_contract_refs[]` | `action_contracts` | semantic validation | fatal dangling, outside the owning behavior specification, or outside a resolved participant | [participant semantics](../formal/participant-semantics/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `behavior_specifications.*.tool_affordances.*.observation_boundary_refs[]` | `observation_boundaries` | semantic validation | fatal dangling, outside the owner/participant, or without explicit view classification | [participant semantics](../formal/participant-semantics/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | | `behavior_specifications.*.participant_inject_deliveries.*.participant_ref` | `agents` | semantic validation | fatal dangling or outside the owning behavior specification | [participant semantics](../formal/participant-semantics/README.md) | [participant-inject delivery validator](../../implementations/python/packages/raes/validator/_participant_inject_deliveries.py) | | `behavior_specifications.*.participant_inject_deliveries.*.inject_ref` | `injects` | semantic validation | fatal dangling or outside the anchored event occurrence | [participant semantics](../formal/participant-semantics/README.md) | [participant-inject delivery validator](../../implementations/python/packages/raes/validator/_participant_inject_deliveries.py) | | `behavior_specifications.*.participant_inject_deliveries.*.occurrence.event_ref` | `events` | semantic validation | fatal dangling or not containing the bound inject | [participant semantics](../formal/participant-semantics/README.md) | [participant-inject delivery validator](../../implementations/python/packages/raes/validator/_participant_inject_deliveries.py) | @@ -408,8 +408,8 @@ the source of the row's normative meaning. | `behavior_specifications.*.defensive_behavior_refs[]` | `vocabulary:defensive_behavior` | semantic validation | fatal unknown vocabulary identifier | [behavior model](../formal/participant-behavior-model/README.md) | [behavior model](../../implementations/python/packages/raes/participant_behavior.py) | | `behavior_specifications.*.offensive_behavior_refs[]` | `vocabulary:offensive_behavior` | semantic validation | fatal unknown vocabulary identifier | [behavior model](../formal/participant-behavior-model/README.md) | [behavior model](../../implementations/python/packages/raes/participant_behavior.py) | | `behavior_specifications.*.realization_profile_ref` | `opaque:realization_profile` | structural validation | fatal invalid reference shape; resolution belongs to realization | [behavior model](../formal/participant-behavior-model/README.md) | [behavior model](../../implementations/python/packages/raes/participant_behavior.py) | -| `behavior_specifications.*.backend_feature_support_refs[]` | `registry:behavior_features` | semantic validation | fatal unsupported feature identifier | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | -| `behavior_specifications.*.evidence_contract_refs[]` | `contract:participant_evidence` | semantic validation | fatal unknown contract identifier | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py) | +| `behavior_specifications.*.backend_feature_support_refs[]` | `registry:behavior_features` | semantic validation | fatal unsupported feature identifier | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | +| `behavior_specifications.*.evidence_contract_refs[]` | `contract:participant_evidence` | semantic validation | fatal unknown contract identifier | [behavior model](../formal/participant-behavior-model/README.md) | [behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py) | | `evidence_requirements.*.source_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [evidence authoring](observability-and-evidence.md) | [evidence validator](../../implementations/python/packages/raes/validator/_evidence_requirements.py) | | `evidence_requirements.*.scope_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [evidence authoring](observability-and-evidence.md) | [evidence validator](../../implementations/python/packages/raes/validator/_evidence_requirements.py) | | `evidence_requirements.*.channel_refs[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [evidence authoring](observability-and-evidence.md) | [evidence validator](../../implementations/python/packages/raes/validator/_evidence_requirements.py) | @@ -431,17 +431,17 @@ the source of the row's normative meaning. | `variation_points.*.precedence[].before` | `derived:variation_members` | structural validation | fatal outside the owning order point | [variation points](variation-points.md) | [variation validator](../../implementations/python/packages/raes/validator/_variation.py) | | `variation_points.*.precedence[].after` | `derived:variation_members` | structural validation | fatal outside the owning order point | [variation points](variation-points.md) | [variation validator](../../implementations/python/packages/raes/validator/_variation.py) | | `variation_points.*.fixed_positions.*.$key` | `derived:variation_members` | structural validation | fatal outside the owning order point | [variation points](variation-points.md) | [variation validator](../../implementations/python/packages/raes/validator/_variation.py) | -| `objectives.*.agent` | `agents` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.entity` | `entities` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.actions[]` | `derived:agent_actions` | semantic validation | fatal outside the bound agent action contracts | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.targets[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.success.assertions[]` | `assertions` | semantic validation | fatal dangling, ambiguous, or precondition role | [proposition semantics](../formal/objectives/proposition-and-assertion-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.depends_on[]` | `objectives` | semantic validation | fatal dangling, ambiguous, or cyclic | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.window.stories[]` | `stories` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.window.scripts[]` | `scripts` | semantic validation | fatal dangling or outside referenced stories | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.window.events[]` | `events` | semantic validation | fatal dangling or outside referenced scripts | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.window.workflows[]` | `workflows` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | -| `objectives.*.window.steps[]` | `workflow_steps` | semantic validation | fatal malformed, dangling, or outside referenced workflows | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py) | +| `objectives.*.agent` | `agents` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.entity` | `entities` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.actions[]` | `derived:agent_actions` | semantic validation | fatal outside the bound agent action contracts | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.targets[]` | `targetable` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.success.assertions[]` | `assertions` | semantic validation | fatal dangling, ambiguous, or precondition role | [proposition semantics](../formal/objectives/proposition-and-assertion-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.depends_on[]` | `objectives` | semantic validation | fatal dangling, ambiguous, or cyclic | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.window.stories[]` | `stories` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.window.scripts[]` | `scripts` | semantic validation | fatal dangling or outside referenced stories | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.window.events[]` | `events` | semantic validation | fatal dangling or outside referenced scripts | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.window.workflows[]` | `workflows` | semantic validation | fatal dangling or ambiguous | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | +| `objectives.*.window.steps[]` | `workflow_steps` | semantic validation | fatal malformed, dangling, or outside referenced workflows | [objective semantics](../formal/objectives/declarative-objective-semantics.md) | [objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py) | | `workflows.*.start` | `workflow_steps` | semantic validation | fatal dangling step | [workflow semantics](../formal/workflows/state-machine.md) | [workflow validator](../../implementations/python/packages/raes/validator/_workflows_verify.py) | | `workflows.*.steps.*.when.assertions[]` | `assertions` | semantic validation | fatal dangling, ambiguous, or non-precondition role | [proposition semantics](../formal/objectives/proposition-and-assertion-semantics.md) | [workflow validator](../../implementations/python/packages/raes/validator/_workflows_verify.py) | | `workflows.*.steps.*.when.objectives[]` | `objectives` | semantic validation | fatal dangling or ambiguous | [workflow semantics](../formal/workflows/state-machine.md) | [workflow validator](../../implementations/python/packages/raes/validator/_workflows_verify.py) | diff --git a/specs/sdl/runtime-inventory.md b/specs/sdl/runtime-inventory.md index 77b927229..7cb1b8f6c 100644 --- a/specs/sdl/runtime-inventory.md +++ b/specs/sdl/runtime-inventory.md @@ -62,6 +62,15 @@ The node-scoped `forwarding_agents` family is distinct from the scenario-level `forwarding_agents` authoring section ([sections.md](sections.md)); they share identity and invariants but occupy different document positions. +Forwarding-agent entries also carry a closed ownership role: +`system_under_test` (the default) or `measurement_apparatus`. This role +classifies ownership relative to the experiment; it does not assert execution, +visibility, delivery, or health. Exact realization of the family means +independently corroborated presence or configuration, as required by the +authored projection, not proof that forwarding behavior occurred. Operational +behavior is expressed through proposition, probe, truth, and evidence +contracts. + ### Other node-runtime surfaces A node's `runtime` also carries surfaces that are not ref-targetable families in diff --git a/tools/check_json_artifacts.py b/tools/check_json_artifacts.py index 033383813..b5efd8214 100644 --- a/tools/check_json_artifacts.py +++ b/tools/check_json_artifacts.py @@ -90,6 +90,12 @@ def _random_stream_profile_schema(repo_root: Path, path: Path) -> Path: return repo_root / "contracts" / "schemas" / "profiles" / _schema_filename(schema_version) +def _participant_information_reconstruction_profile_schema(repo_root: Path, path: Path) -> Path: + payload = _load_json(path) + schema_version = payload["schema_version"] + return repo_root / "contracts" / "schemas" / "profiles" / _schema_filename(schema_version) + + def _random_stream_vector_schema(repo_root: Path, path: Path) -> Path: payload = _load_json(path) schema_version = payload["schema_version"] @@ -157,6 +163,17 @@ def collect_validation_targets( ) ) continue + if raw_path.startswith("contracts/profiles/participant-information-reconstruction/") and raw_path.endswith( + ".json" + ): + targets.append( + ValidationTarget( + raw_path, + _repo_rel_from(repo_root, _participant_information_reconstruction_profile_schema(repo_root, path)), + "schema", + ) + ) + continue if raw_path.startswith("contracts/fixtures/random-stream-vectors/") and raw_path.endswith(".json"): targets.append( ValidationTarget( @@ -217,6 +234,19 @@ def _collect_full_targets(repo_root: Path) -> list[ValidationTarget]: "schema", ) ) + for profile in sorted( + (repo_root / "contracts" / "profiles" / "participant-information-reconstruction").glob("*.json") + ): + targets.append( + ValidationTarget( + _repo_rel_from(repo_root, profile), + _repo_rel_from( + repo_root, + _participant_information_reconstruction_profile_schema(repo_root, profile), + ), + "schema", + ) + ) for vector in sorted((repo_root / "contracts" / "fixtures" / "random-stream-vectors").rglob("*.json")): targets.append( ValidationTarget( diff --git a/tools/check_participant_opacity_proof.py b/tools/check_participant_opacity_proof.py index b259d8a6c..f1303cb36 100644 --- a/tools/check_participant_opacity_proof.py +++ b/tools/check_participant_opacity_proof.py @@ -158,8 +158,8 @@ def _validate_authorities(manifest: dict[str, Any], repo_root: Path) -> tuple[An ) if taxonomy["taxonomy_id"] != "raes-behavioral-relations" or taxonomy["taxonomy_revision"] != "rev9": raise ProofEvidenceError("proof taxonomy authority is not the declared rev9 authority") - if taxonomy["path"] != "contracts/concept-authority/behavioral-relations-v1.json": - raise ProofEvidenceError("proof taxonomy authority path is not canonical") + if taxonomy["path"] != "contracts/concept-authority/history/behavioral-relations-v1-rev9.json": + raise ProofEvidenceError("proof taxonomy authority path is not the immutable rev9 authority") _validate_digest_bound_path(repo_root, taxonomy, "taxonomy authority") catalog = load_behavioral_relation_catalog_revision("rev9") relation = catalog.relations["participant-predicate-opacity"] diff --git a/tools/check_sdl_catalog_parity.py b/tools/check_sdl_catalog_parity.py index d63169bd9..539a19c92 100644 --- a/tools/check_sdl_catalog_parity.py +++ b/tools/check_sdl_catalog_parity.py @@ -115,7 +115,7 @@ "[participant validator](../../implementations/python/packages/raes/validator/_content_objectives.py)" ) _PARTICIPANT_SEMANTICS = ( - "[participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py)" + "[participant semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py)" ) _PARTICIPANT_INTERACTIVE_ACCESS_SEMANTICS = ( "[participant interactive-access semantics]" @@ -123,7 +123,7 @@ ) _OUTCOME_SEMANTICS = "[outcome semantics](../../implementations/python/packages/raes/semantics/participant_outcome.py)" _BEHAVIOR_SEMANTICS = ( - "[behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior.py)" + "[behavior semantics](../../implementations/python/packages/raes/semantics/participant_behavior/__init__.py)" ) _BEHAVIOR_VALIDATOR = ( "[behavior validator](../../implementations/python/packages/raes/validator/_content_objectives.py)" @@ -146,7 +146,7 @@ "[evidence validator](../../implementations/python/packages/raes/validator/_evidence_requirements.py)" ) _OBJECTIVE_SEMANTICS = ( - "[objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics.py)" + "[objective semantics](../../implementations/python/packages/raes/semantics/objective_semantics/__init__.py)" ) _WORKFLOW_SEMANTICS = "[workflow validator](../../implementations/python/packages/raes/validator/_workflows_verify.py)" _PROPOSITION_VALIDATOR = ( diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index 400f99cfc..d56f3f21b 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -22,6 +22,8 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: return schemas_dir / "satisfiability" / f"{name}.json" if name.startswith("artifact-requirement-v"): return schemas_dir / "artifact-requirements" / f"{name}.json" + if name.startswith("artifact-transformation-report-v"): + return schemas_dir / "artifact-transformations" / f"{name}.json" if name.startswith("exploit-path-analysis-evidence-v"): return schemas_dir / "exploit-path-analysis" / f"{name}.json" if name.startswith("backend-manifest-v"): @@ -66,6 +68,8 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: return schemas_dir / "profiles" / f"{name}.json" if name.startswith("random-stream-profile-v"): return schemas_dir / "profiles" / f"{name}.json" + if name.startswith("participant-information-reconstruction-profile-v"): + return schemas_dir / "profiles" / f"{name}.json" if name.startswith("random-stream-vector-v"): return schemas_dir / "profiles" / f"{name}.json" if name.startswith("behavioral-relation-profile-v"): @@ -79,6 +83,7 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: if name in { "participant-lifecycle-event-v1", "participant-observation-envelope-v1", + "participant-information-state-record-v1", "participant-shared-state-record-v1", "participant-joint-action-record-v1", "participant-time-management-context-v1", diff --git a/tools/policy/historical_identity_records.json b/tools/policy/historical_identity_records.json index bd89a73ff..4884e66c6 100644 --- a/tools/policy/historical_identity_records.json +++ b/tools/policy/historical_identity_records.json @@ -32,8 +32,8 @@ "path": "contracts/provenance/sdl-lineage-ledger-v1.json", "record_class": "provenance-record", "rationale": "Preserves append-only lineage observations recorded before the RAES identity cutover.", - "occurrences": 346, - "content_sha256": "362f96df38c79d9d33f85e69a919b4f0e3732482e9589b26c932470bc6e7d82f" + "occurrences": 349, + "content_sha256": "b1766af5a23efec003c395b529cf17362942573e1147427d579af39f62c8ee9f" }, { "path": "contracts/schema-publication/tombstones/\u0061ces-semantic-invariants-v1.json", @@ -348,7 +348,7 @@ "record_class": "accepted-adr", "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", "occurrences": 1, - "content_sha256": "99272388e9cc884d18398cc320802e82f9e655b4f67ae72bd7cd8f1309a8dfaa" + "content_sha256": "a3f6e034b696f0254f5d1321b747d5a3cefe6df65784a5d88e1615b1eeec4749" }, { "path": "docs/decisions/adrs/adr-068-experiment-trials-replication-and-replay-claims.md", @@ -481,7 +481,7 @@ "record_class": "accepted-adr", "rationale": "Preserves an accepted architecture decision as written before the RAES identity cutover.", "occurrences": 2, - "content_sha256": "966cb55a1a2d74350403996a5ae2eaa81e59876d35f0f3000ddcf2941600aff7" + "content_sha256": "61975da4806840eb6324bde3af893cd6425d62d99f35cfcce1128289a5685ded" }, { "path": "docs/decisions/adrs/adr-093-raes-rename-and-compatibility-boundaries.md", @@ -495,7 +495,7 @@ "record_class": "historical-index", "rationale": "Indexes immutable pre-cutover ADR titles, paths, pins, and amendment summaries without making them current identity surfaces.", "occurrences": 4, - "content_sha256": "16f7d3e9f87fc6008c7f4333b14e1c8ae993dd6a7925ba4795e0c2b6ce5370a4" + "content_sha256": "f5ee5174c7d30e1ed859187c90dacac3b64a3e3dac0f185cca2fe71c263495c9" }, { "path": "docs/decisions/cage-2-replication-design.md", @@ -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": "631b59c4a40ec935a68ab975814c023d3824e5c42e62415e3176b88baacf6519" + "content_sha256": "cf2625cc1cd3a0314c4c7a1f87c56ed1bbfdfe8b400381372107e276bbbb2b03" }, { "path": "docs/research/formal-semantic-validation/execution-snapshot-v1.json", @@ -1565,8 +1565,8 @@ "path": "docs/research/lineage/source-audit-2026-07-12.md", "record_class": "research-record", "rationale": "Preserves preregistered, frozen, dated, or lineage-bearing research evidence from before the RAES identity cutover.", - "occurrences": 24, - "content_sha256": "89676790565dc970e4f07ff48dd705094124877bdd4f4aaa7e574c4cce4af9e9" + "occurrences": 25, + "content_sha256": "2dc32335c8e8ada53615b3bcd25195729738ef388dcf5cf7d686b7aa72707669" }, { "path": "docs/research/participant-backend-contracts/index.md", @@ -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": "c3cd8dd7faeb7d7ea69164e6fb9e9069b1e7c3cc3e28989b70a9a1fe56b86114" + "content_sha256": "e3c106de9c2f7fb70113a8400b48b13a4be9c6073bfdae585525ae3610962075" }, { "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": "c4c2308607e12741e1dc43f518f9313095b7882849b5fdd5c408b54f86a0032b" + "content_sha256": "4960c27b6b9bddda77c0c4d830eddb8c181544400d83eeb2293dbea4764bf38a" }, { "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": "32f61b3f381982572d1d79d2374f3fb312bebb3657bb757096bf40883afe0f78" + "content_sha256": "51513e923425510585f0507ce84e979435318b5832b5a0531c9d2e76cc2504dd" }, { "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": "c87a61a2dfa44f572d5abb47c1c26cbfabfb4937709b8f037382ed0d66cd5482" + "content_sha256": "dbdec80e4f1f9e47dbd6b0a46e94513639647c1054791de387293d8e300fa713" }, { "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": "5f8e060dc651021350dcee3e0e8bf03d2d5a25849c1fe6f760255ef286712119" + "content_sha256": "281e69e3b7dee7ad1fffa4dd1b20e7ad9d6a5a222a802d5c327af03ceac3a1bc" }, { "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": "e88617c6b378b4c11e63bcc23039fa81d0e0fd1a2211180c6dfa6b902159f8aa" + "content_sha256": "1cca169b829d841d36f71c49c0dbb8e59a0ea70f83eb809325087aeb06f7e387" }, { "path": "docs/research/specification-coverage/index.md", diff --git a/tools/policy/oversized_allowlist.yaml b/tools/policy/oversized_allowlist.yaml index 7f28e4e62..ea054c876 100644 --- a/tools/policy/oversized_allowlist.yaml +++ b/tools/policy/oversized_allowlist.yaml @@ -7,15 +7,7 @@ # was re-baselined to the 500-line cap on 2026-07-12 (#561); see ADR-015 §2 and # its Amendments section. files: - - implementations/python/packages/raes_backend_libvirt/drivers/libvirt.py - - implementations/python/packages/raes_backend_libvirt/realization.py - - implementations/python/packages/raes_backend_libvirt/techvault_native.py - - implementations/python/packages/raes_contracts/workflow.py - - implementations/python/packages/raes_operations/_evidence_run_artifact.py - implementations/python/packages/raes/composition.py - implementations/python/packages/raes/orchestration.py - implementations/python/packages/raes/participant_behavior.py - implementations/python/packages/raes/runtime_mail_service.py - - implementations/python/packages/raes/runtime_security_monitoring.py - - implementations/python/packages/raes/semantics/objective_semantics.py - - implementations/python/packages/raes/semantics/participant_behavior.py diff --git a/tools/policy/repo_policy.py b/tools/policy/repo_policy.py index 7ef5cc804..86af8997f 100644 --- a/tools/policy/repo_policy.py +++ b/tools/policy/repo_policy.py @@ -678,7 +678,7 @@ def _check_module_boundaries( continue tree = ast.parse(path.read_text(encoding="utf-8"), filename=rel_path) failures.extend(_check_module_imports(rel_path, rule, known_modules, tree)) - if rule["id"] == "raes_backend_protocols" and rel_path.endswith("protocols.py"): + if rule["id"] == "raes_backend_protocols": failures.extend(_check_backend_protocol_contract_annotations(rel_path, tree)) return failures